diff --git a/Moose.lua b/Moose.lua index c150951..06bfb9e 100644 --- a/Moose.lua +++ b/Moose.lua @@ -1,4 +1,4 @@ -env.info( '*** MOOSE GITHUB Commit Hash ID: 2021-08-22T10:02:52.0000000Z-6cc3d73c04a3393cc3566eb93aef03080f2375b7 ***' ) +env.info( '*** MOOSE GITHUB Commit Hash ID: 2023-01-31T10:27:12.0000000Z-48721859fabbb5006ddef989822ad92ee5def497 ***' ) env.info( '*** MOOSE STATIC INCLUDE START *** ' ) --- **Utilities** Enumerators. @@ -14,9 +14,9 @@ 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 +-- @module Utilities.Enums -- @image MOOSE.JPG --- [DCS Enum world](https://wiki.hoggitworld.com/view/DCS_enum_world) @@ -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,18 +451,127 @@ ENUMS.Phonetic = X = 'Xray', Y = 'Yankee', Z = 'Zulu', -}--- Various routines --- @module routines --- @image MOOSE.JPG +} -env.setErrorMessageBoxEnabled(false) +--- 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", + } +} +--- **Utilities** - Various routines. +-- @module Utilities.Routines +-- @image MOOSE.JPG +env.setErrorMessageBoxEnabled( false ) --- Extract of MIST functions. -- @author Grimes routines = {} - -- don't change these routines.majorVersion = 3 routines.minorVersion = 3 @@ -385,291 +583,281 @@ routines.build = 22 -- Utils- conversion, Lua utils, etc. routines.utils = {} -routines.utils.round = function(number, decimals) - local power = 10^decimals - return math.floor(number * power) / power +routines.utils.round = function( number, decimals ) + local power = 10 ^ decimals + return math.floor( number * power ) / power end ---from http://lua-users.org/wiki/CopyTable -routines.utils.deepCopy = function(object) - local lookup_table = {} - local function _copy(object) - if type(object) ~= "table" then - return object - elseif lookup_table[object] then - return lookup_table[object] - end - local new_table = {} - lookup_table[object] = new_table - for index, value in pairs(object) do - new_table[_copy(index)] = _copy(value) - end - return setmetatable(new_table, getmetatable(object)) - end - local objectreturn = _copy(object) - return objectreturn +-- from http://lua-users.org/wiki/CopyTable +routines.utils.deepCopy = function( object ) + local lookup_table = {} + local function _copy( object ) + if type( object ) ~= "table" then + return object + elseif lookup_table[object] then + return lookup_table[object] + end + local new_table = {} + lookup_table[object] = new_table + for index, value in pairs( object ) do + new_table[_copy( index )] = _copy( value ) + end + return setmetatable( new_table, getmetatable( object ) ) + end + local objectreturn = _copy( object ) + return objectreturn end - -- porting in Slmod's serialize_slmod2 -routines.utils.oneLineSerialize = function(tbl) -- serialization of a table all on a single line, no comments, made to replace old get_table_string function +routines.utils.oneLineSerialize = function( tbl ) -- serialization of a table all on a single line, no comments, made to replace old get_table_string function - lookup_table = {} - - local function _Serialize( tbl ) + lookup_table = {} - if type(tbl) == 'table' then --function only works for tables! - - if lookup_table[tbl] then - return lookup_table[object] - end + local function _Serialize( tbl ) - local tbl_str = {} - - lookup_table[tbl] = tbl_str - - tbl_str[#tbl_str + 1] = '{' - - for ind,val in pairs(tbl) do -- serialize its fields - local ind_str = {} - if type(ind) == "number" then - ind_str[#ind_str + 1] = '[' - ind_str[#ind_str + 1] = tostring(ind) - ind_str[#ind_str + 1] = ']=' - else --must be a string - ind_str[#ind_str + 1] = '[' - ind_str[#ind_str + 1] = routines.utils.basicSerialize(ind) - ind_str[#ind_str + 1] = ']=' - end + if type( tbl ) == 'table' then -- function only works for tables! - local val_str = {} - if ((type(val) == 'number') or (type(val) == 'boolean')) then - val_str[#val_str + 1] = tostring(val) - val_str[#val_str + 1] = ',' - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - elseif type(val) == 'string' then - val_str[#val_str + 1] = routines.utils.basicSerialize(val) - val_str[#val_str + 1] = ',' - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - elseif type(val) == 'nil' then -- won't ever happen, right? - val_str[#val_str + 1] = 'nil,' - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - elseif type(val) == 'table' then - if ind == "__index" then - -- tbl_str[#tbl_str + 1] = "__index" - -- tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it - else - - val_str[#val_str + 1] = _Serialize(val) - val_str[#val_str + 1] = ',' --I think this is right, I just added it - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - end - elseif type(val) == 'function' then - -- tbl_str[#tbl_str + 1] = "function " .. tostring(ind) - -- tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it - else --- env.info('unable to serialize value type ' .. routines.utils.basicSerialize(type(val)) .. ' at index ' .. tostring(ind)) --- env.info( debug.traceback() ) - end - - end - tbl_str[#tbl_str + 1] = '}' - return table.concat(tbl_str) - else - if type(tbl) == 'string' then - return tbl - else - return tostring(tbl) - end - end - end - - local objectreturn = _Serialize(tbl) - return objectreturn -end + if lookup_table[tbl] then + return lookup_table[object] + end ---porting in Slmod's "safestring" basic serialize -routines.utils.basicSerialize = function(s) - if s == nil then - return "\"\"" - else - if ((type(s) == 'number') or (type(s) == 'boolean') or (type(s) == 'function') or (type(s) == 'table') or (type(s) == 'userdata') ) then - return tostring(s) - elseif type(s) == 'string' then - s = string.format('%s', s:gsub( "%%", "%%%%" ) ) - return s - end - end + local tbl_str = {} + + lookup_table[tbl] = tbl_str + + tbl_str[#tbl_str + 1] = '{' + + for ind, val in pairs( tbl ) do -- serialize its fields + local ind_str = {} + if type( ind ) == "number" then + ind_str[#ind_str + 1] = '[' + ind_str[#ind_str + 1] = tostring( ind ) + ind_str[#ind_str + 1] = ']=' + else -- must be a string + ind_str[#ind_str + 1] = '[' + ind_str[#ind_str + 1] = routines.utils.basicSerialize( ind ) + ind_str[#ind_str + 1] = ']=' + end + + local val_str = {} + if ((type( val ) == 'number') or (type( val ) == 'boolean')) then + val_str[#val_str + 1] = tostring( val ) + val_str[#val_str + 1] = ',' + tbl_str[#tbl_str + 1] = table.concat( ind_str ) + tbl_str[#tbl_str + 1] = table.concat( val_str ) + elseif type( val ) == 'string' then + val_str[#val_str + 1] = routines.utils.basicSerialize( val ) + val_str[#val_str + 1] = ',' + tbl_str[#tbl_str + 1] = table.concat( ind_str ) + tbl_str[#tbl_str + 1] = table.concat( val_str ) + elseif type( val ) == 'nil' then -- won't ever happen, right? + val_str[#val_str + 1] = 'nil,' + tbl_str[#tbl_str + 1] = table.concat( ind_str ) + tbl_str[#tbl_str + 1] = table.concat( val_str ) + elseif type( val ) == 'table' then + if ind == "__index" then + -- tbl_str[#tbl_str + 1] = "__index" + -- tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it + else + + val_str[#val_str + 1] = _Serialize( val ) + val_str[#val_str + 1] = ',' -- I think this is right, I just added it + tbl_str[#tbl_str + 1] = table.concat( ind_str ) + tbl_str[#tbl_str + 1] = table.concat( val_str ) + end + elseif type( val ) == 'function' then + -- tbl_str[#tbl_str + 1] = "function " .. tostring(ind) + -- tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it + else + -- env.info('unable to serialize value type ' .. routines.utils.basicSerialize(type(val)) .. ' at index ' .. tostring(ind)) + -- env.info( debug.traceback() ) + end + + end + tbl_str[#tbl_str + 1] = '}' + return table.concat( tbl_str ) + else + if type( tbl ) == 'string' then + return tbl + else + return tostring( tbl ) + end + end + end + + local objectreturn = _Serialize( tbl ) + return objectreturn end +-- porting in Slmod's "safestring" basic serialize +routines.utils.basicSerialize = function( s ) + if s == nil then + return "\"\"" + else + if ((type( s ) == 'number') or (type( s ) == 'boolean') or (type( s ) == 'function') or (type( s ) == 'table') or (type( s ) == 'userdata')) then + return tostring( s ) + elseif type( s ) == 'string' then + s = string.format( '%s', s:gsub( "%%", "%%%%" ) ) + return s + end + end +end -routines.utils.toDegree = function(angle) - return angle*180/math.pi +routines.utils.toDegree = function( angle ) + return angle * 180 / math.pi end -routines.utils.toRadian = function(angle) - return angle*math.pi/180 +routines.utils.toRadian = function( angle ) + return angle * math.pi / 180 end -routines.utils.metersToNM = function(meters) - return meters/1852 +routines.utils.metersToNM = function( meters ) + return meters / 1852 end -routines.utils.metersToFeet = function(meters) - return meters/0.3048 +routines.utils.metersToFeet = function( meters ) + return meters / 0.3048 end -routines.utils.NMToMeters = function(NM) - return NM*1852 +routines.utils.NMToMeters = function( NM ) + return NM * 1852 end -routines.utils.feetToMeters = function(feet) - return feet*0.3048 +routines.utils.feetToMeters = function( feet ) + return feet * 0.3048 end -routines.utils.mpsToKnots = function(mps) - return mps*3600/1852 +routines.utils.mpsToKnots = function( mps ) + return mps * 3600 / 1852 end -routines.utils.mpsToKmph = function(mps) - return mps*3.6 +routines.utils.mpsToKmph = function( mps ) + return mps * 3.6 end -routines.utils.knotsToMps = function(knots) - return knots*1852/3600 +routines.utils.knotsToMps = function( knots ) + return knots * 1852 / 3600 end -routines.utils.kmphToMps = function(kmph) - return kmph/3.6 +routines.utils.kmphToMps = function( kmph ) + return kmph / 3.6 end -function routines.utils.makeVec2(Vec3) - if Vec3.z then - return {x = Vec3.x, y = Vec3.z} - else - return {x = Vec3.x, y = Vec3.y} -- it was actually already vec2. - end +function routines.utils.makeVec2( Vec3 ) + if Vec3.z then + return { x = Vec3.x, y = Vec3.z } + else + return { x = Vec3.x, y = Vec3.y } -- it was actually already vec2. + end end -function routines.utils.makeVec3(Vec2, y) - if not Vec2.z then - if not y then - y = 0 - end - return {x = Vec2.x, y = y, z = Vec2.y} - else - return {x = Vec2.x, y = Vec2.y, z = Vec2.z} -- it was already Vec3, actually. - end +function routines.utils.makeVec3( Vec2, y ) + if not Vec2.z then + if not y then + y = 0 + end + return { x = Vec2.x, y = y, z = Vec2.y } + else + return { x = Vec2.x, y = Vec2.y, z = Vec2.z } -- it was already Vec3, actually. + end end -function routines.utils.makeVec3GL(Vec2, offset) - local adj = offset or 0 +function routines.utils.makeVec3GL( Vec2, offset ) + local adj = offset or 0 - if not Vec2.z then - return {x = Vec2.x, y = (land.getHeight(Vec2) + adj), z = Vec2.y} - else - return {x = Vec2.x, y = (land.getHeight({x = Vec2.x, y = Vec2.z}) + adj), z = Vec2.z} - end + if not Vec2.z then + return { x = Vec2.x, y = (land.getHeight( Vec2 ) + adj), z = Vec2.y } + else + return { x = Vec2.x, y = (land.getHeight( { x = Vec2.x, y = Vec2.z } ) + adj), z = Vec2.z } + end end -routines.utils.zoneToVec3 = function(zone) - local new = {} - if type(zone) == 'table' and zone.point then - new.x = zone.point.x - new.y = zone.point.y - new.z = zone.point.z - return new - elseif type(zone) == 'string' then - zone = trigger.misc.getZone(zone) - if zone then - new.x = zone.point.x - new.y = zone.point.y - new.z = zone.point.z - return new - end - end +routines.utils.zoneToVec3 = function( zone ) + local new = {} + if type( zone ) == 'table' and zone.point then + new.x = zone.point.x + new.y = zone.point.y + new.z = zone.point.z + return new + elseif type( zone ) == 'string' then + zone = trigger.misc.getZone( zone ) + if zone then + new.x = zone.point.x + new.y = zone.point.y + new.z = zone.point.z + return new + end + end end -- gets heading-error corrected direction from point along vector vec. -function routines.utils.getDir(vec, point) - local dir = math.atan2(vec.z, vec.x) - dir = dir + routines.getNorthCorrection(point) - if dir < 0 then - dir = dir + 2*math.pi -- put dir in range of 0 to 2*pi - end - return dir +function routines.utils.getDir( vec, point ) + local dir = math.atan2( vec.z, vec.x ) + dir = dir + routines.getNorthCorrection( point ) + if dir < 0 then + dir = dir + 2 * math.pi -- put dir in range of 0 to 2*pi + end + return dir end -- gets distance in meters between two points (2 dimensional) -function routines.utils.get2DDist(point1, point2) - point1 = routines.utils.makeVec3(point1) - point2 = routines.utils.makeVec3(point2) - return routines.vec.mag({x = point1.x - point2.x, y = 0, z = point1.z - point2.z}) +function routines.utils.get2DDist( point1, point2 ) + point1 = routines.utils.makeVec3( point1 ) + point2 = routines.utils.makeVec3( point2 ) + return routines.vec.mag( { x = point1.x - point2.x, y = 0, z = point1.z - point2.z } ) end -- gets distance in meters between two points (3 dimensional) -function routines.utils.get3DDist(point1, point2) - return routines.vec.mag({x = point1.x - point2.x, y = point1.y - point2.y, z = point1.z - point2.z}) +function routines.utils.get3DDist( point1, point2 ) + return routines.vec.mag( { x = point1.x - point2.x, y = point1.y - point2.y, z = point1.z - point2.z } ) end - - - - ---3D Vector manipulation +-- 3D Vector manipulation routines.vec = {} -routines.vec.add = function(vec1, vec2) - return {x = vec1.x + vec2.x, y = vec1.y + vec2.y, z = vec1.z + vec2.z} +routines.vec.add = function( vec1, vec2 ) + return { x = vec1.x + vec2.x, y = vec1.y + vec2.y, z = vec1.z + vec2.z } end -routines.vec.sub = function(vec1, vec2) - return {x = vec1.x - vec2.x, y = vec1.y - vec2.y, z = vec1.z - vec2.z} +routines.vec.sub = function( vec1, vec2 ) + return { x = vec1.x - vec2.x, y = vec1.y - vec2.y, z = vec1.z - vec2.z } end -routines.vec.scalarMult = function(vec, mult) - return {x = vec.x*mult, y = vec.y*mult, z = vec.z*mult} +routines.vec.scalarMult = function( vec, mult ) + return { x = vec.x * mult, y = vec.y * mult, z = vec.z * mult } end routines.vec.scalar_mult = routines.vec.scalarMult -routines.vec.dp = function(vec1, vec2) - return vec1.x*vec2.x + vec1.y*vec2.y + vec1.z*vec2.z +routines.vec.dp = function( vec1, vec2 ) + return vec1.x * vec2.x + vec1.y * vec2.y + vec1.z * vec2.z end -routines.vec.cp = function(vec1, vec2) - return { x = vec1.y*vec2.z - vec1.z*vec2.y, y = vec1.z*vec2.x - vec1.x*vec2.z, z = vec1.x*vec2.y - vec1.y*vec2.x} +routines.vec.cp = function( vec1, vec2 ) + return { x = vec1.y * vec2.z - vec1.z * vec2.y, y = vec1.z * vec2.x - vec1.x * vec2.z, z = vec1.x * vec2.y - vec1.y * vec2.x } end -routines.vec.mag = function(vec) - return (vec.x^2 + vec.y^2 + vec.z^2)^0.5 +routines.vec.mag = function( vec ) + return (vec.x ^ 2 + vec.y ^ 2 + vec.z ^ 2) ^ 0.5 end -routines.vec.getUnitVec = function(vec) - local mag = routines.vec.mag(vec) - return { x = vec.x/mag, y = vec.y/mag, z = vec.z/mag } +routines.vec.getUnitVec = function( vec ) + local mag = routines.vec.mag( vec ) + return { x = vec.x / mag, y = vec.y / mag, z = vec.z / mag } end -routines.vec.rotateVec2 = function(vec2, theta) - return { x = vec2.x*math.cos(theta) - vec2.y*math.sin(theta), y = vec2.x*math.sin(theta) + vec2.y*math.cos(theta)} +routines.vec.rotateVec2 = function( vec2, theta ) + return { x = vec2.x * math.cos( theta ) - vec2.y * math.sin( theta ), y = vec2.x * math.sin( theta ) + vec2.y * math.cos( theta ) } end --------------------------------------------------------------------------------------------------------------------------- - - - -- acc- the accuracy of each easting/northing. 0, 1, 2, 3, 4, or 5. -routines.tostringMGRS = function(MGRS, acc) - if acc == 0 then - return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph - else - return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph .. ' ' .. string.format('%0' .. acc .. 'd', routines.utils.round(MGRS.Easting/(10^(5-acc)), 0)) - .. ' ' .. string.format('%0' .. acc .. 'd', routines.utils.round(MGRS.Northing/(10^(5-acc)), 0)) - end +routines.tostringMGRS = function( MGRS, acc ) + if acc == 0 then + return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph + else + return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph .. ' ' .. string.format( '%0' .. acc .. 'd', routines.utils.round( MGRS.Easting / (10 ^ (5 - acc)), 0 ) ) .. ' ' .. string.format( '%0' .. acc .. 'd', routines.utils.round( MGRS.Northing / (10 ^ (5 - acc)), 0 ) ) + end end --[[acc: @@ -679,86 +867,84 @@ position after the decimal of the least significant digit: So: 42.32 - acc of 2. ]] -routines.tostringLL = function(lat, lon, acc, DMS) +routines.tostringLL = function( lat, lon, acc, DMS ) - local latHemi, lonHemi - if lat > 0 then - latHemi = 'N' - else - latHemi = 'S' - end + local latHemi, lonHemi + if lat > 0 then + latHemi = 'N' + else + latHemi = 'S' + end - if lon > 0 then - lonHemi = 'E' - else - lonHemi = 'W' - end + if lon > 0 then + lonHemi = 'E' + else + lonHemi = 'W' + end - lat = math.abs(lat) - lon = math.abs(lon) + lat = math.abs( lat ) + lon = math.abs( lon ) - local latDeg = math.floor(lat) - local latMin = (lat - latDeg)*60 + local latDeg = math.floor( lat ) + local latMin = (lat - latDeg) * 60 - local lonDeg = math.floor(lon) - local lonMin = (lon - lonDeg)*60 + local lonDeg = math.floor( lon ) + local lonMin = (lon - lonDeg) * 60 - if DMS then -- degrees, minutes, and seconds. - local oldLatMin = latMin - latMin = math.floor(latMin) - local latSec = routines.utils.round((oldLatMin - latMin)*60, acc) + if DMS then -- degrees, minutes, and seconds. + local oldLatMin = latMin + latMin = math.floor( latMin ) + local latSec = routines.utils.round( (oldLatMin - latMin) * 60, acc ) - local oldLonMin = lonMin - lonMin = math.floor(lonMin) - local lonSec = routines.utils.round((oldLonMin - lonMin)*60, acc) + local oldLonMin = lonMin + lonMin = math.floor( lonMin ) + local lonSec = routines.utils.round( (oldLonMin - lonMin) * 60, acc ) - if latSec == 60 then - latSec = 0 - latMin = latMin + 1 - end + if latSec == 60 then + latSec = 0 + latMin = latMin + 1 + end - if lonSec == 60 then - lonSec = 0 - lonMin = lonMin + 1 - end + if lonSec == 60 then + lonSec = 0 + lonMin = lonMin + 1 + end - local secFrmtStr -- create the formatting string for the seconds place - if acc <= 0 then -- no decimal place. - secFrmtStr = '%02d' - else - local width = 3 + acc -- 01.310 - that's a width of 6, for example. - secFrmtStr = '%0' .. width .. '.' .. acc .. 'f' - end + local secFrmtStr -- create the formatting string for the seconds place + if acc <= 0 then -- no decimal place. + secFrmtStr = '%02d' + else + local width = 3 + acc -- 01.310 - that's a width of 6, for example. + secFrmtStr = '%0' .. width .. '.' .. acc .. 'f' + end - return string.format('%02d', latDeg) .. ' ' .. string.format('%02d', latMin) .. '\' ' .. string.format(secFrmtStr, latSec) .. '"' .. latHemi .. ' ' - .. string.format('%02d', lonDeg) .. ' ' .. string.format('%02d', lonMin) .. '\' ' .. string.format(secFrmtStr, lonSec) .. '"' .. lonHemi + return string.format( '%02d', latDeg ) .. ' ' .. string.format( '%02d', latMin ) .. '\' ' .. string.format( secFrmtStr, latSec ) .. '"' .. latHemi .. ' ' .. string.format( '%02d', lonDeg ) .. ' ' .. string.format( '%02d', lonMin ) .. '\' ' .. string.format( secFrmtStr, lonSec ) .. '"' .. lonHemi - else -- degrees, decimal minutes. - latMin = routines.utils.round(latMin, acc) - lonMin = routines.utils.round(lonMin, acc) + else -- degrees, decimal minutes. + latMin = routines.utils.round( latMin, acc ) + lonMin = routines.utils.round( lonMin, acc ) - if latMin == 60 then - latMin = 0 - latDeg = latDeg + 1 - end + if latMin == 60 then + latMin = 0 + latDeg = latDeg + 1 + end - if lonMin == 60 then - lonMin = 0 - lonDeg = lonDeg + 1 - end + if lonMin == 60 then + lonMin = 0 + lonDeg = lonDeg + 1 + end - local minFrmtStr -- create the formatting string for the minutes place - if acc <= 0 then -- no decimal place. - minFrmtStr = '%02d' - else - local width = 3 + acc -- 01.310 - that's a width of 6, for example. - minFrmtStr = '%0' .. width .. '.' .. acc .. 'f' - end + local minFrmtStr -- create the formatting string for the minutes place + if acc <= 0 then -- no decimal place. + minFrmtStr = '%02d' + else + local width = 3 + acc -- 01.310 - that's a width of 6, for example. + minFrmtStr = '%0' .. width .. '.' .. acc .. 'f' + end - return string.format('%02d', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\'' .. latHemi .. ' ' - .. string.format('%02d', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\'' .. lonHemi + return string.format( '%02d', latDeg ) .. ' ' .. string.format( minFrmtStr, latMin ) .. '\'' .. latHemi .. ' ' .. string.format( '%02d', lonDeg ) .. ' ' .. string.format( minFrmtStr, lonMin ) .. '\'' .. lonHemi - end + end end --[[ required: az - radian @@ -766,114 +952,105 @@ end optional: alt - meters (set to false or nil if you don't want to use it). optional: metric - set true to get dist and alt in km and m. precision will always be nearest degree and NM or km.]] -routines.tostringBR = function(az, dist, alt, metric) - az = routines.utils.round(routines.utils.toDegree(az), 0) +routines.tostringBR = function( az, dist, alt, metric ) + az = routines.utils.round( routines.utils.toDegree( az ), 0 ) - if metric then - dist = routines.utils.round(dist/1000, 2) - else - dist = routines.utils.round(routines.utils.metersToNM(dist), 2) - end + if metric then + dist = routines.utils.round( dist / 1000, 2 ) + else + dist = routines.utils.round( routines.utils.metersToNM( dist ), 2 ) + end - local s = string.format('%03d', az) .. ' for ' .. dist + local s = string.format( '%03d', az ) .. ' for ' .. dist - if alt then - if metric then - s = s .. ' at ' .. routines.utils.round(alt, 0) - else - s = s .. ' at ' .. routines.utils.round(routines.utils.metersToFeet(alt), 0) - end - end - return s + if alt then + if metric then + s = s .. ' at ' .. routines.utils.round( alt, 0 ) + else + s = s .. ' at ' .. routines.utils.round( routines.utils.metersToFeet( alt ), 0 ) + end + end + return s end -routines.getNorthCorrection = function(point) --gets the correction needed for true north - 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) +routines.getNorthCorrection = function( point ) -- gets the correction needed for true north + 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 - do - local idNum = 0 - - --Simplified event handler - routines.addEventHandler = function(f) --id is optional! - local handler = {} - idNum = idNum + 1 - handler.id = idNum - handler.f = f - handler.onEvent = function(self, event) - self.f(event) - end - world.addEventHandler(handler) - end + local idNum = 0 - routines.removeEventHandler = function(id) - for key, handler in pairs(world.eventHandlers) do - if handler.id and handler.id == id then - world.eventHandlers[key] = nil - return true - end - end - return false - end + -- Simplified event handler + routines.addEventHandler = function( f ) -- id is optional! + local handler = {} + idNum = idNum + 1 + handler.id = idNum + handler.f = f + handler.onEvent = function( self, event ) + self.f( event ) + end + world.addEventHandler( handler ) + end + + routines.removeEventHandler = function( id ) + for key, handler in pairs( world.eventHandlers ) do + if handler.id and handler.id == id then + world.eventHandlers[key] = nil + return true + end + end + return false + end end -- need to return a Vec3 or Vec2? -function routines.getRandPointInCircle(point, radius, innerRadius) - local theta = 2*math.pi*math.random() - local rad = math.random() + math.random() - if rad > 1 then - rad = 2 - rad - end - - local radMult - if innerRadius and innerRadius <= radius then - radMult = (radius - innerRadius)*rad + innerRadius - else - radMult = radius*rad - end +function routines.getRandPointInCircle( point, radius, innerRadius ) + local theta = 2 * math.pi * math.random() + local rad = math.random() + math.random() + if rad > 1 then + rad = 2 - rad + end - if not point.z then --might as well work with vec2/3 - point.z = point.y - end + local radMult + if innerRadius and innerRadius <= radius then + radMult = (radius - innerRadius) * rad + innerRadius + else + radMult = radius * rad + end - local rndCoord - if radius > 0 then - rndCoord = {x = math.cos(theta)*radMult + point.x, y = math.sin(theta)*radMult + point.z} - else - rndCoord = {x = point.x, y = point.z} - end - return rndCoord -end - -routines.goRoute = function(group, path) - local misTask = { - id = 'Mission', - params = { - route = { - points = routines.utils.deepCopy(path), - }, - }, - } - if type(group) == 'string' then - group = Group.getByName(group) - end - local groupCon = group:getController() - if groupCon then - groupCon:setTask(misTask) - return true - end + if not point.z then -- might as well work with vec2/3 + point.z = point.y + end - Controller.setTask(groupCon, misTask) - return false + local rndCoord + if radius > 0 then + rndCoord = { x = math.cos( theta ) * radMult + point.x, y = math.sin( theta ) * radMult + point.z } + else + rndCoord = { x = point.x, y = point.z } + end + return rndCoord end +routines.goRoute = function( group, path ) + local misTask = { id = 'Mission', params = { route = { points = routines.utils.deepCopy( path ) } } } + if type( group ) == 'string' then + group = Group.getByName( group ) + end + local groupCon = group:getController() + if groupCon then + groupCon:setTask( misTask ) + return true + end + + Controller.setTask( groupCon, misTask ) + return false +end -- Useful atomic functions from mist, ported. @@ -1114,7 +1291,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 @@ -1224,13 +1401,13 @@ end vars.units - table of unit names (NOT unitNameTable- maybe this should change). vars.acc - integer between 0 and 5, inclusive ]] -routines.getMGRSString = function(vars) - local units = vars.units - local acc = vars.acc or 5 - local avgPos = routines.getAvgPos(units) - if avgPos then - return routines.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(avgPos)), acc) - end +routines.getMGRSString = function( vars ) + local units = vars.units + local acc = vars.acc or 5 + local avgPos = routines.getAvgPos( units ) + if avgPos then + return routines.tostringMGRS( coord.LLtoMGRS( coord.LOtoLL( avgPos ) ), acc ) + end end --[[ vars for routines.getLLString @@ -1240,15 +1417,15 @@ vars.DMS - if true, output in degrees, minutes, seconds. Otherwise, output in d ]] -routines.getLLString = function(vars) - local units = vars.units - local acc = vars.acc or 3 - local DMS = vars.DMS - local avgPos = routines.getAvgPos(units) - if avgPos then - local lat, lon = coord.LOtoLL(avgPos) - return routines.tostringLL(lat, lon, acc, DMS) - end +routines.getLLString = function( vars ) + local units = vars.units + local acc = vars.acc or 3 + local DMS = vars.DMS + local avgPos = routines.getAvgPos( units ) + if avgPos then + local lat, lon = coord.LOtoLL( avgPos ) + return routines.tostringLL( lat, lon, acc, DMS ) + end end --[[ @@ -1257,22 +1434,22 @@ vars.ref - vec3 ref point, maybe overload for vec2 as well? vars.alt - boolean, if used, includes altitude in string vars.metric - boolean, gives distance in km instead of NM. ]] -routines.getBRStringZone = function(vars) - local zone = trigger.misc.getZone( vars.zone ) - local ref = routines.utils.makeVec3(vars.ref, 0) -- turn it into Vec3 if it is not already. - local alt = vars.alt - local metric = vars.metric - if zone then - local vec = {x = zone.point.x - ref.x, y = zone.point.y - ref.y, z = zone.point.z - ref.z} - local dir = routines.utils.getDir(vec, ref) - local dist = routines.utils.get2DDist(zone.point, ref) - if alt then - alt = zone.y - end - return routines.tostringBR(dir, dist, alt, metric) - else - env.info( 'routines.getBRStringZone: error: zone is nil' ) - end +routines.getBRStringZone = function( vars ) + local zone = trigger.misc.getZone( vars.zone ) + local ref = routines.utils.makeVec3( vars.ref, 0 ) -- turn it into Vec3 if it is not already. + local alt = vars.alt + local metric = vars.metric + if zone then + local vec = { x = zone.point.x - ref.x, y = zone.point.y - ref.y, z = zone.point.z - ref.z } + local dir = routines.utils.getDir( vec, ref ) + local dist = routines.utils.get2DDist( zone.point, ref ) + if alt then + alt = zone.y + end + return routines.tostringBR( dir, dist, alt, metric ) + else + env.info( 'routines.getBRStringZone: error: zone is nil' ) + end end --[[ @@ -1281,24 +1458,23 @@ vars.ref - vec3 ref point, maybe overload for vec2 as well? vars.alt - boolean, if used, includes altitude in string vars.metric - boolean, gives distance in km instead of NM. ]] -routines.getBRString = function(vars) - local units = vars.units - local ref = routines.utils.makeVec3(vars.ref, 0) -- turn it into Vec3 if it is not already. - local alt = vars.alt - local metric = vars.metric - local avgPos = routines.getAvgPos(units) - if avgPos then - local vec = {x = avgPos.x - ref.x, y = avgPos.y - ref.y, z = avgPos.z - ref.z} - local dir = routines.utils.getDir(vec, ref) - local dist = routines.utils.get2DDist(avgPos, ref) - if alt then - alt = avgPos.y - end - return routines.tostringBR(dir, dist, alt, metric) - end +routines.getBRString = function( vars ) + local units = vars.units + local ref = routines.utils.makeVec3( vars.ref, 0 ) -- turn it into Vec3 if it is not already. + local alt = vars.alt + local metric = vars.metric + local avgPos = routines.getAvgPos( units ) + if avgPos then + local vec = { x = avgPos.x - ref.x, y = avgPos.y - ref.y, z = avgPos.z - ref.z } + local dir = routines.utils.getDir( vec, ref ) + local dist = routines.utils.get2DDist( avgPos, ref ) + if alt then + alt = avgPos.y + end + return routines.tostringBR( dir, dist, alt, metric ) + end end - -- Returns the Vec3 coordinates of the average position of the concentration of units most in the heading direction. --[[ vars for routines.getLeadingPos: vars.units - table of unit names @@ -1306,56 +1482,55 @@ vars.heading - direction vars.radius - number vars.headingDegrees - boolean, switches heading to degrees ]] -routines.getLeadingPos = function(vars) - local units = vars.units - local heading = vars.heading - local radius = vars.radius - if vars.headingDegrees then - heading = routines.utils.toRadian(vars.headingDegrees) - end +routines.getLeadingPos = function( vars ) + local units = vars.units + local heading = vars.heading + local radius = vars.radius + if vars.headingDegrees then + heading = routines.utils.toRadian( vars.headingDegrees ) + end - local unitPosTbl = {} - for i = 1, #units do - local unit = Unit.getByName(units[i]) - if unit and unit:isExist() then - unitPosTbl[#unitPosTbl + 1] = unit:getPosition().p - end - end - if #unitPosTbl > 0 then -- one more more units found. - -- first, find the unit most in the heading direction - local maxPos = -math.huge - - local maxPosInd -- maxPos - the furthest in direction defined by heading; maxPosInd = - for i = 1, #unitPosTbl do - local rotatedVec2 = routines.vec.rotateVec2(routines.utils.makeVec2(unitPosTbl[i]), heading) - if (not maxPos) or maxPos < rotatedVec2.x then - maxPos = rotatedVec2.x - maxPosInd = i - end - end + local unitPosTbl = {} + for i = 1, #units do + local unit = Unit.getByName( units[i] ) + if unit and unit:isExist() then + unitPosTbl[#unitPosTbl + 1] = unit:getPosition().p + end + end + if #unitPosTbl > 0 then -- one more more units found. + -- first, find the unit most in the heading direction + local maxPos = -math.huge - --now, get all the units around this unit... - local avgPos - if radius then - local maxUnitPos = unitPosTbl[maxPosInd] - local avgx, avgy, avgz, totNum = 0, 0, 0, 0 - for i = 1, #unitPosTbl do - if routines.utils.get2DDist(maxUnitPos, unitPosTbl[i]) <= radius then - avgx = avgx + unitPosTbl[i].x - avgy = avgy + unitPosTbl[i].y - avgz = avgz + unitPosTbl[i].z - totNum = totNum + 1 - end - end - avgPos = { x = avgx/totNum, y = avgy/totNum, z = avgz/totNum} - else - avgPos = unitPosTbl[maxPosInd] - end + local maxPosInd -- maxPos - the furthest in direction defined by heading; maxPosInd = + for i = 1, #unitPosTbl do + local rotatedVec2 = routines.vec.rotateVec2( routines.utils.makeVec2( unitPosTbl[i] ), heading ) + if (not maxPos) or maxPos < rotatedVec2.x then + maxPos = rotatedVec2.x + maxPosInd = i + end + end - return avgPos - end -end + -- now, get all the units around this unit... + local avgPos + if radius then + local maxUnitPos = unitPosTbl[maxPosInd] + local avgx, avgy, avgz, totNum = 0, 0, 0, 0 + for i = 1, #unitPosTbl do + if routines.utils.get2DDist( maxUnitPos, unitPosTbl[i] ) <= radius then + avgx = avgx + unitPosTbl[i].x + avgy = avgy + unitPosTbl[i].y + avgz = avgz + unitPosTbl[i].z + totNum = totNum + 1 + end + end + avgPos = { x = avgx / totNum, y = avgy / totNum, z = avgz / totNum } + else + avgPos = unitPosTbl[maxPosInd] + end + return avgPos + end +end --[[ vars for routines.getLeadingMGRSString: vars.units - table of unit names @@ -1364,12 +1539,12 @@ vars.radius - number vars.headingDegrees - boolean, switches heading to degrees vars.acc - number, 0 to 5. ]] -routines.getLeadingMGRSString = function(vars) - local pos = routines.getLeadingPos(vars) - if pos then - local acc = vars.acc or 5 - return routines.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(pos)), acc) - end +routines.getLeadingMGRSString = function( vars ) + local pos = routines.getLeadingPos( vars ) + if pos then + local acc = vars.acc or 5 + return routines.tostringMGRS( coord.LLtoMGRS( coord.LOtoLL( pos ) ), acc ) + end end --[[ vars for routines.getLeadingLLString: @@ -1380,18 +1555,16 @@ vars.headingDegrees - boolean, switches heading to degrees vars.acc - number of digits after decimal point (can be negative) vars.DMS - boolean, true if you want DMS. ]] -routines.getLeadingLLString = function(vars) - local pos = routines.getLeadingPos(vars) - if pos then - local acc = vars.acc or 3 - local DMS = vars.DMS - local lat, lon = coord.LOtoLL(pos) - return routines.tostringLL(lat, lon, acc, DMS) - end +routines.getLeadingLLString = function( vars ) + local pos = routines.getLeadingPos( vars ) + if pos then + local acc = vars.acc or 3 + local DMS = vars.DMS + local lat, lon = coord.LOtoLL( pos ) + return routines.tostringLL( lat, lon, acc, DMS ) + end end - - --[[ vars for routines.getLeadingBRString: vars.units - table of unit names vars.heading - direction, number @@ -1401,21 +1574,21 @@ vars.metric - boolean, if true, use km instead of NM. vars.alt - boolean, if true, include altitude. vars.ref - vec3/vec2 reference point. ]] -routines.getLeadingBRString = function(vars) - local pos = routines.getLeadingPos(vars) - if pos then - local ref = vars.ref - local alt = vars.alt - local metric = vars.metric - - local vec = {x = pos.x - ref.x, y = pos.y - ref.y, z = pos.z - ref.z} - local dir = routines.utils.getDir(vec, ref) - local dist = routines.utils.get2DDist(pos, ref) - if alt then - alt = pos.y - end - return routines.tostringBR(dir, dist, alt, metric) - end +routines.getLeadingBRString = function( vars ) + local pos = routines.getLeadingPos( vars ) + if pos then + local ref = vars.ref + local alt = vars.alt + local metric = vars.metric + + local vec = { x = pos.x - ref.x, y = pos.y - ref.y, z = pos.z - ref.z } + local dir = routines.utils.getDir( vec, ref ) + local dist = routines.utils.get2DDist( pos, ref ) + if alt then + alt = pos.y + end + return routines.tostringBR( dir, dist, alt, metric ) + end end --[[ vars for routines.message.add @@ -1432,26 +1605,22 @@ vars.text - text in the message vars.displayTime - self explanatory vars.msgFor - scope ]] -routines.msgMGRS = function(vars) - local units = vars.units - local acc = vars.acc - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getMGRSString{units = units, acc = acc} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end +routines.msgMGRS = function( vars ) + local units = vars.units + local acc = vars.acc + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = routines.getMGRSString { units = units, acc = acc } + local newText + if string.find( text, '%%s' ) then -- look for %s + newText = string.format( text, s ) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } + routines.message.add { text = newText, displayTime = displayTime, msgFor = msgFor } end --[[ vars for routines.msgLL @@ -1462,31 +1631,26 @@ vars.text - text in the message vars.displayTime - self explanatory vars.msgFor - scope ]] -routines.msgLL = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local acc = vars.acc - local DMS = vars.DMS - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getLLString{units = units, acc = acc, DMS = DMS} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end +routines.msgLL = function( vars ) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local acc = vars.acc + local DMS = vars.DMS + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = routines.getLLString { units = units, acc = acc, DMS = DMS } + local newText + if string.find( text, '%%s' ) then -- look for %s + newText = string.format( text, s ) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } + routines.message.add { text = newText, displayTime = displayTime, msgFor = msgFor } end - --[[ vars.units- table of unit names (NOT unitNameTable- maybe this should change). vars.ref - vec3 ref point, maybe overload for vec2 as well? @@ -1496,32 +1660,27 @@ vars.text - text of the message vars.displayTime vars.msgFor - scope ]] -routines.msgBR = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local ref = vars.ref -- vec2/vec3 will be handled in routines.getBRString - local alt = vars.alt - local metric = vars.metric - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getBRString{units = units, ref = ref, alt = alt, metric = metric} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end +routines.msgBR = function( vars ) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local ref = vars.ref -- vec2/vec3 will be handled in routines.getBRString + local alt = vars.alt + local metric = vars.metric + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = routines.getBRString { units = units, ref = ref, alt = alt, metric = metric } + local newText + if string.find( text, '%%s' ) then -- look for %s + newText = string.format( text, s ) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } + routines.message.add { text = newText, displayTime = displayTime, msgFor = msgFor } end - -------------------------------------------------------------------------------------------- -- basically, just sub-types of routines.msgBR... saves folks the work of getting the ref point. --[[ @@ -1533,14 +1692,14 @@ vars.text - text of the message vars.displayTime vars.msgFor - scope ]] -routines.msgBullseye = function(vars) - if string.lower(vars.ref) == 'red' then - vars.ref = routines.DBs.missionData.bullseye.red - routines.msgBR(vars) - elseif string.lower(vars.ref) == 'blue' then - vars.ref = routines.DBs.missionData.bullseye.blue - routines.msgBR(vars) - end +routines.msgBullseye = function( vars ) + if string.lower( vars.ref ) == 'red' then + vars.ref = routines.DBs.missionData.bullseye.red + routines.msgBR( vars ) + elseif string.lower( vars.ref ) == 'blue' then + vars.ref = routines.DBs.missionData.bullseye.blue + routines.msgBR( vars ) + end end --[[ @@ -1553,14 +1712,14 @@ vars.displayTime vars.msgFor - scope ]] -routines.msgBRA = function(vars) - if Unit.getByName(vars.ref) then - vars.ref = Unit.getByName(vars.ref):getPosition().p - if not vars.alt then - vars.alt = true - end - routines.msgBR(vars) - end +routines.msgBRA = function( vars ) + if Unit.getByName( vars.ref ) then + vars.ref = Unit.getByName( vars.ref ):getPosition().p + if not vars.alt then + vars.alt = true + end + routines.msgBR( vars ) + end end -------------------------------------------------------------------------------------------- @@ -1574,32 +1733,27 @@ vars.text - text of the message vars.displayTime vars.msgFor - scope ]] -routines.msgLeadingMGRS = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local heading = vars.heading - local radius = vars.radius - local headingDegrees = vars.headingDegrees - local acc = vars.acc - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getLeadingMGRSString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } +routines.msgLeadingMGRS = function( vars ) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local heading = vars.heading + local radius = vars.radius + local headingDegrees = vars.headingDegrees + local acc = vars.acc + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + local s = routines.getLeadingMGRSString { units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc } + local newText + if string.find( text, '%%s' ) then -- look for %s + newText = string.format( text, s ) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end + routines.message.add { text = newText, displayTime = displayTime, msgFor = msgFor } end + --[[ vars for routines.msgLeadingLL: vars.units - table of unit names vars.heading - direction, number @@ -1611,31 +1765,26 @@ vars.text - text of the message vars.displayTime vars.msgFor - scope ]] -routines.msgLeadingLL = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local heading = vars.heading - local radius = vars.radius - local headingDegrees = vars.headingDegrees - local acc = vars.acc - local DMS = vars.DMS - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getLeadingLLString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc, DMS = DMS} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end +routines.msgLeadingLL = function( vars ) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local heading = vars.heading + local radius = vars.radius + local headingDegrees = vars.headingDegrees + local acc = vars.acc + local DMS = vars.DMS + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } + local s = routines.getLeadingLLString { units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc, DMS = DMS } + local newText + if string.find( text, '%%s' ) then -- look for %s + newText = string.format( text, s ) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end + routines.message.add { text = newText, displayTime = displayTime, msgFor = msgFor } end --[[ @@ -1650,137 +1799,133 @@ vars.text - text of the message vars.displayTime vars.msgFor - scope ]] -routines.msgLeadingBR = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local heading = vars.heading - local radius = vars.radius - local headingDegrees = vars.headingDegrees - local metric = vars.metric - local alt = vars.alt - local ref = vars.ref -- vec2/vec3 will be handled in routines.getBRString - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getLeadingBRString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, metric = metric, alt = alt, ref = ref} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } -end - - -function spairs(t, order) - -- collect the keys - local keys = {} - for k in pairs(t) do keys[#keys+1] = k end - - -- if order function given, sort by it by passing the table and keys a, b, - -- otherwise just sort the keys - if order then - table.sort(keys, function(a,b) return order(t, a, b) end) - else - table.sort(keys) - end +routines.msgLeadingBR = function( vars ) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local heading = vars.heading + local radius = vars.radius + local headingDegrees = vars.headingDegrees + local metric = vars.metric + local alt = vars.alt + local ref = vars.ref -- vec2/vec3 will be handled in routines.getBRString + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = routines.getLeadingBRString { units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, metric = metric, alt = alt, ref = ref } + local newText + if string.find( text, '%%s' ) then -- look for %s + newText = string.format( text, s ) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end + + routines.message.add { text = newText, displayTime = displayTime, msgFor = msgFor } +end + +function spairs( t, order ) + -- collect the keys + local keys = {} + for k in pairs( t ) do + keys[#keys + 1] = k + end + + -- if order function given, sort by it by passing the table and keys a, b, + -- otherwise just sort the keys + if order then + table.sort( keys, function( a, b ) + return order( t, a, b ) + end ) + else + table.sort( keys ) + end - -- return the iterator function - local i = 0 - return function() - i = i + 1 - if keys[i] then - return keys[i], t[keys[i]] - end + -- return the iterator function + local i = 0 + return function() + i = i + 1 + if keys[i] then + return keys[i], t[keys[i]] end + end end - function routines.IsPartOfGroupInZones( CargoGroup, LandingZones ) ---trace.f() + -- trace.f() - local CurrentZoneID = nil + local CurrentZoneID = nil - if CargoGroup then - local CargoUnits = CargoGroup:getUnits() - for CargoUnitID, CargoUnit in pairs( CargoUnits ) do - if CargoUnit and CargoUnit:getLife() >= 1.0 then - CurrentZoneID = routines.IsUnitInZones( CargoUnit, LandingZones ) - if CurrentZoneID then - break - end - end - end - end + if CargoGroup then + local CargoUnits = CargoGroup:getUnits() + for CargoUnitID, CargoUnit in pairs( CargoUnits ) do + if CargoUnit and CargoUnit:getLife() >= 1.0 then + CurrentZoneID = routines.IsUnitInZones( CargoUnit, LandingZones ) + if CurrentZoneID then + break + end + end + end + end ---trace.r( "", "", { CurrentZoneID } ) - return CurrentZoneID + -- trace.r( "", "", { CurrentZoneID } ) + return CurrentZoneID end +function routines.IsUnitInZones( TransportUnit, LandingZones ) + -- trace.f("", "routines.IsUnitInZones" ) + local TransportZoneResult = nil + local TransportZonePos = nil + local TransportZone = nil -function routines.IsUnitInZones( TransportUnit, LandingZones ) ---trace.f("", "routines.IsUnitInZones" ) - - local TransportZoneResult = nil - local TransportZonePos = nil - local TransportZone = nil - - -- fill-up some local variables to support further calculations to determine location of units within the zone. - if TransportUnit then - local TransportUnitPos = TransportUnit:getPosition().p - if type( LandingZones ) == "table" then - for LandingZoneID, LandingZoneName in pairs( LandingZones ) do - TransportZone = trigger.misc.getZone( LandingZoneName ) - if TransportZone then - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then - TransportZoneResult = LandingZoneID - break - end - end - end - else - TransportZone = trigger.misc.getZone( LandingZones ) - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then - TransportZoneResult = 1 - end - end - if TransportZoneResult then - --trace.i( "routines", "TransportZone:" .. TransportZoneResult ) - else - --trace.i( "routines", "TransportZone:nil logic" ) - end - return TransportZoneResult - else - --trace.i( "routines", "TransportZone:nil hard" ) - return nil - end + -- fill-up some local variables to support further calculations to determine location of units within the zone. + if TransportUnit then + local TransportUnitPos = TransportUnit:getPosition().p + if type( LandingZones ) == "table" then + for LandingZoneID, LandingZoneName in pairs( LandingZones ) do + TransportZone = trigger.misc.getZone( LandingZoneName ) + if TransportZone then + TransportZonePos = { radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z } + if (((TransportUnitPos.x - TransportZonePos.x) ^ 2 + (TransportUnitPos.z - TransportZonePos.z) ^ 2) ^ 0.5 <= TransportZonePos.radius) then + TransportZoneResult = LandingZoneID + break + end + end + end + else + TransportZone = trigger.misc.getZone( LandingZones ) + TransportZonePos = { radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z } + if (((TransportUnitPos.x - TransportZonePos.x) ^ 2 + (TransportUnitPos.z - TransportZonePos.z) ^ 2) ^ 0.5 <= TransportZonePos.radius) then + TransportZoneResult = 1 + end + end + if TransportZoneResult then + -- trace.i( "routines", "TransportZone:" .. TransportZoneResult ) + else + -- trace.i( "routines", "TransportZone:nil logic" ) + end + return TransportZoneResult + else + -- trace.i( "routines", "TransportZone:nil hard" ) + return nil + end end function routines.IsUnitNearZonesRadius( TransportUnit, LandingZones, ZoneRadius ) ---trace.f("", "routines.IsUnitInZones" ) + -- trace.f("", "routines.IsUnitInZones" ) local TransportZoneResult = nil local TransportZonePos = nil local TransportZone = nil - -- fill-up some local variables to support further calculations to determine location of units within the zone. + -- fill-up some local variables to support further calculations to determine location of units within the zone. if TransportUnit then local TransportUnitPos = TransportUnit:getPosition().p if type( LandingZones ) == "table" then for LandingZoneID, LandingZoneName in pairs( LandingZones ) do TransportZone = trigger.misc.getZone( LandingZoneName ) if TransportZone then - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= ZoneRadius ) then + TransportZonePos = { radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z } + if (((TransportUnitPos.x - TransportZonePos.x) ^ 2 + (TransportUnitPos.z - TransportZonePos.z) ^ 2) ^ 0.5 <= ZoneRadius) then TransportZoneResult = LandingZoneID break end @@ -1788,59 +1933,57 @@ function routines.IsUnitNearZonesRadius( TransportUnit, LandingZones, ZoneRadius end else TransportZone = trigger.misc.getZone( LandingZones ) - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= ZoneRadius ) then + TransportZonePos = { radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z } + if (((TransportUnitPos.x - TransportZonePos.x) ^ 2 + (TransportUnitPos.z - TransportZonePos.z) ^ 2) ^ 0.5 <= ZoneRadius) then TransportZoneResult = 1 end end if TransportZoneResult then - --trace.i( "routines", "TransportZone:" .. TransportZoneResult ) + -- trace.i( "routines", "TransportZone:" .. TransportZoneResult ) else - --trace.i( "routines", "TransportZone:nil logic" ) + -- trace.i( "routines", "TransportZone:nil logic" ) end return TransportZoneResult else - --trace.i( "routines", "TransportZone:nil hard" ) + -- trace.i( "routines", "TransportZone:nil hard" ) return nil end end - function routines.IsStaticInZones( TransportStatic, LandingZones ) ---trace.f() - - local TransportZoneResult = nil - local TransportZonePos = nil - local TransportZone = nil - - -- fill-up some local variables to support further calculations to determine location of units within the zone. - local TransportStaticPos = TransportStatic:getPosition().p - if type( LandingZones ) == "table" then - for LandingZoneID, LandingZoneName in pairs( LandingZones ) do - TransportZone = trigger.misc.getZone( LandingZoneName ) - if TransportZone then - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportStaticPos.x - TransportZonePos.x)^2 + (TransportStaticPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then - TransportZoneResult = LandingZoneID - break - end - end - end - else - TransportZone = trigger.misc.getZone( LandingZones ) - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportStaticPos.x - TransportZonePos.x)^2 + (TransportStaticPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then - TransportZoneResult = 1 - end - end + -- trace.f() ---trace.r( "", "", { TransportZoneResult } ) - return TransportZoneResult -end + local TransportZoneResult = nil + local TransportZonePos = nil + local TransportZone = nil + -- fill-up some local variables to support further calculations to determine location of units within the zone. + local TransportStaticPos = TransportStatic:getPosition().p + if type( LandingZones ) == "table" then + for LandingZoneID, LandingZoneName in pairs( LandingZones ) do + TransportZone = trigger.misc.getZone( LandingZoneName ) + if TransportZone then + TransportZonePos = { radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z } + if (((TransportStaticPos.x - TransportZonePos.x) ^ 2 + (TransportStaticPos.z - TransportZonePos.z) ^ 2) ^ 0.5 <= TransportZonePos.radius) then + TransportZoneResult = LandingZoneID + break + end + end + end + else + TransportZone = trigger.misc.getZone( LandingZones ) + TransportZonePos = { radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z } + if (((TransportStaticPos.x - TransportZonePos.x) ^ 2 + (TransportStaticPos.z - TransportZonePos.z) ^ 2) ^ 0.5 <= TransportZonePos.radius) then + TransportZoneResult = 1 + end + end + + -- trace.r( "", "", { TransportZoneResult } ) + return TransportZoneResult +end function routines.IsUnitInRadius( CargoUnit, ReferencePosition, Radius ) ---trace.f() + -- trace.f() local Valid = true @@ -1848,7 +1991,7 @@ function routines.IsUnitInRadius( CargoUnit, ReferencePosition, Radius ) local CargoPos = CargoUnit:getPosition().p local ReferenceP = ReferencePosition.p - if (((CargoPos.x - ReferenceP.x)^2 + (CargoPos.z - ReferenceP.z)^2)^0.5 <= Radius) then + if (((CargoPos.x - ReferenceP.x) ^ 2 + (CargoPos.z - ReferenceP.z) ^ 2) ^ 0.5 <= Radius) then else Valid = false end @@ -1857,7 +2000,7 @@ function routines.IsUnitInRadius( CargoUnit, ReferencePosition, Radius ) end function routines.IsPartOfGroupInRadius( CargoGroup, ReferencePosition, Radius ) ---trace.f() + -- trace.f() local Valid = true @@ -1867,11 +2010,11 @@ function routines.IsPartOfGroupInRadius( CargoGroup, ReferencePosition, Radius ) local CargoUnits = CargoGroup:getUnits() for CargoUnitId, CargoUnit in pairs( CargoUnits ) do local CargoUnitPos = CargoUnit:getPosition().p --- env.info( 'routines.IsPartOfGroupInRadius: CargoUnitPos.x = ' .. CargoUnitPos.x .. ' CargoUnitPos.z = ' .. CargoUnitPos.z ) + -- env.info( 'routines.IsPartOfGroupInRadius: CargoUnitPos.x = ' .. CargoUnitPos.x .. ' CargoUnitPos.z = ' .. CargoUnitPos.z ) local ReferenceP = ReferencePosition.p --- env.info( 'routines.IsPartOfGroupInRadius: ReferenceGroupPos.x = ' .. ReferenceGroupPos.x .. ' ReferenceGroupPos.z = ' .. ReferenceGroupPos.z ) + -- env.info( 'routines.IsPartOfGroupInRadius: ReferenceGroupPos.x = ' .. ReferenceGroupPos.x .. ' ReferenceGroupPos.z = ' .. ReferenceGroupPos.z ) - if ((( CargoUnitPos.x - ReferenceP.x)^2 + (CargoUnitPos.z - ReferenceP.z)^2)^0.5 <= Radius) then + if (((CargoUnitPos.x - ReferenceP.x) ^ 2 + (CargoUnitPos.z - ReferenceP.z) ^ 2) ^ 0.5 <= Radius) then else Valid = false break @@ -1881,11 +2024,10 @@ function routines.IsPartOfGroupInRadius( CargoGroup, ReferencePosition, Radius ) return Valid end - function routines.ValidateString( Variable, VariableName, Valid ) ---trace.f() + -- trace.f() - if type( Variable ) == "string" then + if type( Variable ) == "string" then if Variable == "" then error( "routines.ValidateString: error: " .. VariableName .. " must be filled out!" ) Valid = false @@ -1895,65 +2037,64 @@ function routines.ValidateString( Variable, VariableName, Valid ) Valid = false end ---trace.r( "", "", { Valid } ) + -- trace.r( "", "", { Valid } ) return Valid end function routines.ValidateNumber( Variable, VariableName, Valid ) ---trace.f() + -- trace.f() - if type( Variable ) == "number" then + if type( Variable ) == "number" then else error( "routines.ValidateNumber: error: " .. VariableName .. " is not a number." ) Valid = false end ---trace.r( "", "", { Valid } ) + -- trace.r( "", "", { Valid } ) return Valid - end function routines.ValidateGroup( Variable, VariableName, Valid ) ---trace.f() + -- trace.f() - if Variable == nil then - error( "routines.ValidateGroup: error: " .. VariableName .. " is a nil value!" ) - Valid = false - end + if Variable == nil then + error( "routines.ValidateGroup: error: " .. VariableName .. " is a nil value!" ) + Valid = false + end ---trace.r( "", "", { Valid } ) - return Valid + -- trace.r( "", "", { Valid } ) + return Valid end function routines.ValidateZone( LandingZones, VariableName, Valid ) ---trace.f() + -- trace.f() - if LandingZones == nil then - error( "routines.ValidateGroup: error: " .. VariableName .. " is a nil value!" ) - Valid = false - end + if LandingZones == nil then + error( "routines.ValidateGroup: error: " .. VariableName .. " is a nil value!" ) + Valid = false + end - if type( LandingZones ) == "table" then - for LandingZoneID, LandingZoneName in pairs( LandingZones ) do - if trigger.misc.getZone( LandingZoneName ) == nil then - error( "routines.ValidateGroup: error: Zone " .. LandingZoneName .. " does not exist!" ) - Valid = false - break - end - end - else - if trigger.misc.getZone( LandingZones ) == nil then - error( "routines.ValidateGroup: error: Zone " .. LandingZones .. " does not exist!" ) - Valid = false - end - end + if type( LandingZones ) == "table" then + for LandingZoneID, LandingZoneName in pairs( LandingZones ) do + if trigger.misc.getZone( LandingZoneName ) == nil then + error( "routines.ValidateGroup: error: Zone " .. LandingZoneName .. " does not exist!" ) + Valid = false + break + end + end + else + if trigger.misc.getZone( LandingZones ) == nil then + error( "routines.ValidateGroup: error: Zone " .. LandingZones .. " does not exist!" ) + Valid = false + end + end ---trace.r( "", "", { Valid } ) - return Valid + -- trace.r( "", "", { Valid } ) + return Valid end function routines.ValidateEnumeration( Variable, VariableName, Enum, Valid ) ---trace.f() + -- trace.f() local ValidVariable = false @@ -1964,917 +2105,833 @@ function routines.ValidateEnumeration( Variable, VariableName, Enum, Valid ) end end - if ValidVariable then + if ValidVariable then else error( 'TransportValidateEnum: " .. VariableName .. " is not a valid type.' .. Variable ) Valid = false end ---trace.r( "", "", { Valid } ) + -- trace.r( "", "", { Valid } ) return Valid end -function routines.getGroupRoute(groupIdent, task) -- same as getGroupPoints but returns speed and formation type along with vec2 of point} - -- refactor to search by groupId and allow groupId and groupName as inputs - local gpId = groupIdent - if type(groupIdent) == 'string' and not tonumber(groupIdent) then - gpId = _DATABASE.Templates.Groups[groupIdent].groupId - end - - for coa_name, coa_data in pairs(env.mission.coalition) do - if (coa_name == 'red' or coa_name == 'blue') and type(coa_data) == 'table' then - if coa_data.country then --there is a country table - for cntry_id, cntry_data in pairs(coa_data.country) do - for obj_type_name, obj_type_data in pairs(cntry_data) do - if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" then -- only these types have points - if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! - for group_num, group_data in pairs(obj_type_data.group) do - if group_data and group_data.groupId == gpId then -- this is the group we are looking for - if group_data.route and group_data.route.points and #group_data.route.points > 0 then - local points = {} - - for point_num, point in pairs(group_data.route.points) do - local routeData = {} - if env.mission.version > 7 then - routeData.name = env.getValueDictByKey(point.name) - else - routeData.name = point.name - end - if not point.point then - routeData.x = point.x - routeData.y = point.y - else - routeData.point = point.point --it's possible that the ME could move to the point = Vec2 notation. - end - routeData.form = point.action - routeData.speed = point.speed - routeData.alt = point.alt - routeData.alt_type = point.alt_type - routeData.airdromeId = point.airdromeId - routeData.helipadId = point.helipadId - routeData.type = point.type - routeData.action = point.action - if task then - routeData.task = point.task - end - points[point_num] = routeData - end - - return points - end - return - end --if group_data and group_data.name and group_data.name == 'groupname' - end --for group_num, group_data in pairs(obj_type_data.group) do - end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then - end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then - end --for obj_type_name, obj_type_data in pairs(cntry_data) do - end --for cntry_id, cntry_data in pairs(coa_data.country) do - end --if coa_data.country then --there is a country table - end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then - end --for coa_name, coa_data in pairs(mission.coalition) do -end - -routines.ground.patrolRoute = function(vars) - - - local tempRoute = {} - local useRoute = {} - local gpData = vars.gpData - if type(gpData) == 'string' then - gpData = Group.getByName(gpData) - end - - local useGroupRoute - if not vars.useGroupRoute then - useGroupRoute = vars.gpData - else - useGroupRoute = vars.useGroupRoute - end - local routeProvided = false - if not vars.route then - if useGroupRoute then - tempRoute = routines.getGroupRoute(useGroupRoute) - end - else - useRoute = vars.route - local posStart = routines.getLeadPos(gpData) - useRoute[1] = routines.ground.buildWP(posStart, useRoute[1].action, useRoute[1].speed) - routeProvided = true - end - - - local overRideSpeed = vars.speed or 'default' - local pType = vars.pType - local offRoadForm = vars.offRoadForm or 'default' - local onRoadForm = vars.onRoadForm or 'default' - - if routeProvided == false and #tempRoute > 0 then - local posStart = routines.getLeadPos(gpData) - - - useRoute[#useRoute + 1] = routines.ground.buildWP(posStart, offRoadForm, overRideSpeed) - for i = 1, #tempRoute do - local tempForm = tempRoute[i].action - local tempSpeed = tempRoute[i].speed - - if offRoadForm == 'default' then - tempForm = tempRoute[i].action - end - if onRoadForm == 'default' then - onRoadForm = 'On Road' - end - if (string.lower(tempRoute[i].action) == 'on road' or string.lower(tempRoute[i].action) == 'onroad' or string.lower(tempRoute[i].action) == 'on_road') then - tempForm = onRoadForm - else - tempForm = offRoadForm - end - - if type(overRideSpeed) == 'number' then - tempSpeed = overRideSpeed - end - - - useRoute[#useRoute + 1] = routines.ground.buildWP(tempRoute[i], tempForm, tempSpeed) - end - - if pType and string.lower(pType) == 'doubleback' then - local curRoute = routines.utils.deepCopy(useRoute) - for i = #curRoute, 2, -1 do - useRoute[#useRoute + 1] = routines.ground.buildWP(curRoute[i], curRoute[i].action, curRoute[i].speed) - end - end - - useRoute[1].action = useRoute[#useRoute].action -- make it so the first WP matches the last WP - end - - local cTask3 = {} - local newPatrol = {} - newPatrol.route = useRoute - newPatrol.gpData = gpData:getName() - cTask3[#cTask3 + 1] = 'routines.ground.patrolRoute(' - cTask3[#cTask3 + 1] = routines.utils.oneLineSerialize(newPatrol) - cTask3[#cTask3 + 1] = ')' - cTask3 = table.concat(cTask3) - local tempTask = { - id = 'WrappedAction', - params = { - action = { - id = 'Script', - params = { - command = cTask3, - - }, - }, - }, - } +function routines.getGroupRoute( groupIdent, task ) -- same as getGroupPoints but returns speed and formation type along with vec2 of point} + -- refactor to search by groupId and allow groupId and groupName as inputs + local gpId = groupIdent + if type( groupIdent ) == 'string' and not tonumber( groupIdent ) then + gpId = _DATABASE.Templates.Groups[groupIdent].groupId + end + + for coa_name, coa_data in pairs( env.mission.coalition ) do + if (coa_name == 'red' or coa_name == 'blue') and type( coa_data ) == 'table' then + if coa_data.country then -- there is a country table + for cntry_id, cntry_data in pairs( coa_data.country ) do + for obj_type_name, obj_type_data in pairs( cntry_data ) do + if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" then -- only these types have points + if ((type( obj_type_data ) == 'table') and obj_type_data.group and (type( obj_type_data.group ) == 'table') and (#obj_type_data.group > 0)) then -- there's a group! + for group_num, group_data in pairs( obj_type_data.group ) do + if group_data and group_data.groupId == gpId then -- this is the group we are looking for + if group_data.route and group_data.route.points and #group_data.route.points > 0 then + local points = {} + + for point_num, point in pairs( group_data.route.points ) do + local routeData = {} + if env.mission.version > 7 then + routeData.name = env.getValueDictByKey( point.name ) + else + routeData.name = point.name + end + if not point.point then + routeData.x = point.x + routeData.y = point.y + else + routeData.point = point.point -- it's possible that the ME could move to the point = Vec2 notation. + end + routeData.form = point.action + routeData.speed = point.speed + routeData.alt = point.alt + routeData.alt_type = point.alt_type + routeData.airdromeId = point.airdromeId + routeData.helipadId = point.helipadId + routeData.type = point.type + routeData.action = point.action + if task then + routeData.task = point.task + end + points[point_num] = routeData + end - - useRoute[#useRoute].task = tempTask - routines.goRoute(gpData, useRoute) - - return -end + return points + end + return + end -- if group_data and group_data.name and group_data.name == 'groupname' + end -- for group_num, group_data in pairs(obj_type_data.group) do + end -- if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then + end -- if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then + end -- for obj_type_name, obj_type_data in pairs(cntry_data) do + end -- for cntry_id, cntry_data in pairs(coa_data.country) do + end -- if coa_data.country then --there is a country table + end -- if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then + end -- for coa_name, coa_data in pairs(mission.coalition) do +end + +routines.ground.patrolRoute = function( vars ) + + local tempRoute = {} + local useRoute = {} + local gpData = vars.gpData + if type( gpData ) == 'string' then + gpData = Group.getByName( gpData ) + end + + local useGroupRoute + if not vars.useGroupRoute then + useGroupRoute = vars.gpData + else + useGroupRoute = vars.useGroupRoute + end + local routeProvided = false + if not vars.route then + if useGroupRoute then + tempRoute = routines.getGroupRoute( useGroupRoute ) + end + else + useRoute = vars.route + local posStart = routines.getLeadPos( gpData ) + useRoute[1] = routines.ground.buildWP( posStart, useRoute[1].action, useRoute[1].speed ) + routeProvided = true + end -routines.ground.patrol = function(gpData, pType, form, speed) - local vars = {} - - if type(gpData) == 'table' and gpData:getName() then - gpData = gpData:getName() - end - - vars.useGroupRoute = gpData - vars.gpData = gpData - vars.pType = pType - vars.offRoadForm = form - vars.speed = speed - - routines.ground.patrolRoute(vars) + local overRideSpeed = vars.speed or 'default' + local pType = vars.pType + local offRoadForm = vars.offRoadForm or 'default' + local onRoadForm = vars.onRoadForm or 'default' - return -end + if routeProvided == false and #tempRoute > 0 then + local posStart = routines.getLeadPos( gpData ) -function routines.GetUnitHeight( CheckUnit ) ---trace.f( "routines" ) + useRoute[#useRoute + 1] = routines.ground.buildWP( posStart, offRoadForm, overRideSpeed ) + for i = 1, #tempRoute do + local tempForm = tempRoute[i].action + local tempSpeed = tempRoute[i].speed - local UnitPoint = CheckUnit:getPoint() - local UnitPosition = { x = UnitPoint.x, y = UnitPoint.z } - local UnitHeight = UnitPoint.y + if offRoadForm == 'default' then + tempForm = tempRoute[i].action + end + if onRoadForm == 'default' then + onRoadForm = 'On Road' + end + if (string.lower( tempRoute[i].action ) == 'on road' or string.lower( tempRoute[i].action ) == 'onroad' or string.lower( tempRoute[i].action ) == 'on_road') then + tempForm = onRoadForm + else + tempForm = offRoadForm + end - local LandHeight = land.getHeight( UnitPosition ) + if type( overRideSpeed ) == 'number' then + tempSpeed = overRideSpeed + end - --env.info(( 'CarrierHeight: LandHeight = ' .. LandHeight .. ' CarrierHeight = ' .. CarrierHeight )) + useRoute[#useRoute + 1] = routines.ground.buildWP( tempRoute[i], tempForm, tempSpeed ) + end - --trace.f( "routines", "Unit Height = " .. UnitHeight - LandHeight ) - - return UnitHeight - LandHeight + if pType and string.lower( pType ) == 'doubleback' then + local curRoute = routines.utils.deepCopy( useRoute ) + for i = #curRoute, 2, -1 do + useRoute[#useRoute + 1] = routines.ground.buildWP( curRoute[i], curRoute[i].action, curRoute[i].speed ) + end + end -end + useRoute[1].action = useRoute[#useRoute].action -- make it so the first WP matches the last WP + end + local cTask3 = {} + local newPatrol = {} + newPatrol.route = useRoute + newPatrol.gpData = gpData:getName() + cTask3[#cTask3 + 1] = 'routines.ground.patrolRoute(' + cTask3[#cTask3 + 1] = routines.utils.oneLineSerialize( newPatrol ) + cTask3[#cTask3 + 1] = ')' + cTask3 = table.concat( cTask3 ) + local tempTask = { id = 'WrappedAction', params = { action = { id = 'Script', params = { command = cTask3 } } } } + useRoute[#useRoute].task = tempTask + routines.goRoute( gpData, useRoute ) +end -Su34Status = { status = {} } -boardMsgRed = { statusMsg = "" } -boardMsgAll = { timeMsg = "" } -SpawnSettings = {} -Su34MenuPath = {} -Su34Menus = 0 +routines.ground.patrol = function( gpData, pType, form, speed ) + local vars = {} + if type( gpData ) == 'table' and gpData:getName() then + gpData = gpData:getName() + end -function Su34AttackCarlVinson(groupName) ---trace.menu("", "Su34AttackCarlVinson") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34.getController(groupSu34) - local groupCarlVinson = Group.getByName("US Carl Vinson #001") - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) - if groupCarlVinson ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupCarlVinson:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = true}}) - end - Su34Status.status[groupName] = 1 - MessageToRed( string.format('%s: ',groupName) .. 'Attacking carrier Carl Vinson. ', 10, 'RedStatus' .. groupName ) -end - -function Su34AttackWest(groupName) ---trace.f("","Su34AttackWest") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34.getController(groupSu34) - local groupShipWest1 = Group.getByName("US Ship West #001") - local groupShipWest2 = Group.getByName("US Ship West #002") - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) - if groupShipWest1 ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipWest1:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = true}}) - end - if groupShipWest2 ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipWest2:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = true}}) - end - Su34Status.status[groupName] = 2 - MessageToRed( string.format('%s: ',groupName) .. 'Attacking invading ships in the west. ', 10, 'RedStatus' .. groupName ) -end - -function Su34AttackNorth(groupName) ---trace.menu("","Su34AttackNorth") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34.getController(groupSu34) - local groupShipNorth1 = Group.getByName("US Ship North #001") - local groupShipNorth2 = Group.getByName("US Ship North #002") - local groupShipNorth3 = Group.getByName("US Ship North #003") - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) - if groupShipNorth1 ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipNorth1:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = false}}) - end - if groupShipNorth2 ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipNorth2:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = false}}) - end - if groupShipNorth3 ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipNorth3:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = false}}) - end - Su34Status.status[groupName] = 3 - MessageToRed( string.format('%s: ',groupName) .. 'Attacking invading ships in the north. ', 10, 'RedStatus' .. groupName ) -end + vars.useGroupRoute = gpData + vars.gpData = gpData + vars.pType = pType + vars.offRoadForm = form + vars.speed = speed -function Su34Orbit(groupName) ---trace.menu("","Su34Orbit") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34:getController() - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) - controllerSu34:pushTask( {id = 'ControlledTask', params = { task = { id = 'Orbit', params = { pattern = AI.Task.OrbitPattern.RACE_TRACK } }, stopCondition = { duration = 600 } } } ) - Su34Status.status[groupName] = 4 - MessageToRed( string.format('%s: ',groupName) .. 'In orbit and awaiting further instructions. ', 10, 'RedStatus' .. groupName ) + routines.ground.patrolRoute( vars ) end -function Su34TakeOff(groupName) ---trace.menu("","Su34TakeOff") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34:getController() - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE ) - Su34Status.status[groupName] = 8 - MessageToRed( string.format('%s: ',groupName) .. 'Take-Off. ', 10, 'RedStatus' .. groupName ) -end +function routines.GetUnitHeight( CheckUnit ) + -- trace.f( "routines" ) -function Su34Hold(groupName) ---trace.menu("","Su34Hold") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34:getController() - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE ) - Su34Status.status[groupName] = 5 - MessageToRed( string.format('%s: ',groupName) .. 'Holding Weapons. ', 10, 'RedStatus' .. groupName ) -end + local UnitPoint = CheckUnit:getPoint() + local UnitPosition = { x = UnitPoint.x, y = UnitPoint.z } + local UnitHeight = UnitPoint.y + + local LandHeight = land.getHeight( UnitPosition ) + + -- env.info(( 'CarrierHeight: LandHeight = ' .. LandHeight .. ' CarrierHeight = ' .. CarrierHeight )) -function Su34RTB(groupName) ---trace.menu("","Su34RTB") - Su34Status.status[groupName] = 6 - MessageToRed( string.format('%s: ',groupName) .. 'Return to Krasnodar. ', 10, 'RedStatus' .. groupName ) + -- trace.f( "routines", "Unit Height = " .. UnitHeight - LandHeight ) + + return UnitHeight - LandHeight end -function Su34Destroyed(groupName) ---trace.menu("","Su34Destroyed") - Su34Status.status[groupName] = 7 - MessageToRed( string.format('%s: ',groupName) .. 'Destroyed. ', 30, 'RedStatus' .. groupName ) +Su34Status = { status = {} } +boardMsgRed = { statusMsg = "" } +boardMsgAll = { timeMsg = "" } +SpawnSettings = {} +Su34MenuPath = {} +Su34Menus = 0 + +function Su34AttackCarlVinson( groupName ) + -- trace.menu("", "Su34AttackCarlVinson") + local groupSu34 = Group.getByName( groupName ) + local controllerSu34 = groupSu34.getController( groupSu34 ) + local groupCarlVinson = Group.getByName( "US Carl Vinson #001" ) + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) + if groupCarlVinson ~= nil then + controllerSu34.pushTask( controllerSu34, { id = 'AttackGroup', params = { groupId = groupCarlVinson:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = true } } ) + end + Su34Status.status[groupName] = 1 + MessageToRed( string.format( '%s: ', groupName ) .. 'Attacking carrier Carl Vinson. ', 10, 'RedStatus' .. groupName ) +end + +function Su34AttackWest( groupName ) + -- trace.f("","Su34AttackWest") + local groupSu34 = Group.getByName( groupName ) + local controllerSu34 = groupSu34.getController( groupSu34 ) + local groupShipWest1 = Group.getByName( "US Ship West #001" ) + local groupShipWest2 = Group.getByName( "US Ship West #002" ) + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) + if groupShipWest1 ~= nil then + controllerSu34.pushTask( controllerSu34, { id = 'AttackGroup', params = { groupId = groupShipWest1:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = true } } ) + end + if groupShipWest2 ~= nil then + controllerSu34.pushTask( controllerSu34, { id = 'AttackGroup', params = { groupId = groupShipWest2:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = true } } ) + end + Su34Status.status[groupName] = 2 + MessageToRed( string.format( '%s: ', groupName ) .. 'Attacking invading ships in the west. ', 10, 'RedStatus' .. groupName ) +end + +function Su34AttackNorth( groupName ) + -- trace.menu("","Su34AttackNorth") + local groupSu34 = Group.getByName( groupName ) + local controllerSu34 = groupSu34.getController( groupSu34 ) + local groupShipNorth1 = Group.getByName( "US Ship North #001" ) + local groupShipNorth2 = Group.getByName( "US Ship North #002" ) + local groupShipNorth3 = Group.getByName( "US Ship North #003" ) + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) + if groupShipNorth1 ~= nil then + controllerSu34.pushTask( controllerSu34, { id = 'AttackGroup', params = { groupId = groupShipNorth1:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = false } } ) + end + if groupShipNorth2 ~= nil then + controllerSu34.pushTask( controllerSu34, { id = 'AttackGroup', params = { groupId = groupShipNorth2:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = false } } ) + end + if groupShipNorth3 ~= nil then + controllerSu34.pushTask( controllerSu34, { id = 'AttackGroup', params = { groupId = groupShipNorth3:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = false } } ) + end + Su34Status.status[groupName] = 3 + MessageToRed( string.format( '%s: ', groupName ) .. 'Attacking invading ships in the north. ', 10, 'RedStatus' .. groupName ) +end + +function Su34Orbit( groupName ) + -- trace.menu("","Su34Orbit") + local groupSu34 = Group.getByName( groupName ) + local controllerSu34 = groupSu34:getController() + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) + controllerSu34:pushTask( { id = 'ControlledTask', params = { task = { id = 'Orbit', params = { pattern = AI.Task.OrbitPattern.RACE_TRACK } }, stopCondition = { duration = 600 } } } ) + Su34Status.status[groupName] = 4 + MessageToRed( string.format( '%s: ', groupName ) .. 'In orbit and awaiting further instructions. ', 10, 'RedStatus' .. groupName ) +end + +function Su34TakeOff( groupName ) + -- trace.menu("","Su34TakeOff") + local groupSu34 = Group.getByName( groupName ) + local controllerSu34 = groupSu34:getController() + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE ) + Su34Status.status[groupName] = 8 + MessageToRed( string.format( '%s: ', groupName ) .. 'Take-Off. ', 10, 'RedStatus' .. groupName ) +end + +function Su34Hold( groupName ) + -- trace.menu("","Su34Hold") + local groupSu34 = Group.getByName( groupName ) + local controllerSu34 = groupSu34:getController() + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE ) + Su34Status.status[groupName] = 5 + MessageToRed( string.format( '%s: ', groupName ) .. 'Holding Weapons. ', 10, 'RedStatus' .. groupName ) +end + +function Su34RTB( groupName ) + -- trace.menu("","Su34RTB") + Su34Status.status[groupName] = 6 + MessageToRed( string.format( '%s: ', groupName ) .. 'Return to Krasnodar. ', 10, 'RedStatus' .. groupName ) +end + +function Su34Destroyed( groupName ) + -- trace.menu("","Su34Destroyed") + Su34Status.status[groupName] = 7 + MessageToRed( string.format( '%s: ', groupName ) .. 'Destroyed. ', 30, 'RedStatus' .. groupName ) end function GroupAlive( groupName ) ---trace.menu("","GroupAlive") - local groupTest = Group.getByName( groupName ) + -- trace.menu("","GroupAlive") + local groupTest = Group.getByName( groupName ) - local groupExists = false + local groupExists = false - if groupTest then - groupExists = groupTest:isExist() - end + if groupTest then + groupExists = groupTest:isExist() + end - --trace.r( "", "", { groupExists } ) - return groupExists + -- trace.r( "", "", { groupExists } ) + return groupExists end function Su34IsDead() ---trace.f() + -- trace.f() end function Su34OverviewStatus() ---trace.menu("","Su34OverviewStatus") - local msg = "" - local currentStatus = 0 - local Exists = false - - for groupName, currentStatus in pairs(Su34Status.status) do - - env.info(('Su34 Overview Status: GroupName = ' .. groupName )) - Alive = GroupAlive( groupName ) - - if Alive then - if currentStatus == 1 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Attacking carrier Carl Vinson. " - elseif currentStatus == 2 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Attacking supporting ships in the west. " - elseif currentStatus == 3 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Attacking invading ships in the north. " - elseif currentStatus == 4 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "In orbit and awaiting further instructions. " - elseif currentStatus == 5 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Holding Weapons. " - elseif currentStatus == 6 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Return to Krasnodar. " - elseif currentStatus == 7 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Destroyed. " - elseif currentStatus == 8 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Take-Off. " - end - else - if currentStatus == 7 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Destroyed. " - else - Su34Destroyed(groupName) - end - end - end + -- trace.menu("","Su34OverviewStatus") + local msg = "" + local currentStatus = 0 + local Exists = false + + for groupName, currentStatus in pairs( Su34Status.status ) do + + env.info( ('Su34 Overview Status: GroupName = ' .. groupName) ) + Alive = GroupAlive( groupName ) + + if Alive then + if currentStatus == 1 then + msg = msg .. string.format( "%s: ", groupName ) + msg = msg .. "Attacking carrier Carl Vinson. " + elseif currentStatus == 2 then + msg = msg .. string.format( "%s: ", groupName ) + msg = msg .. "Attacking supporting ships in the west. " + elseif currentStatus == 3 then + msg = msg .. string.format( "%s: ", groupName ) + msg = msg .. "Attacking invading ships in the north. " + elseif currentStatus == 4 then + msg = msg .. string.format( "%s: ", groupName ) + msg = msg .. "In orbit and awaiting further instructions. " + elseif currentStatus == 5 then + msg = msg .. string.format( "%s: ", groupName ) + msg = msg .. "Holding Weapons. " + elseif currentStatus == 6 then + msg = msg .. string.format( "%s: ", groupName ) + msg = msg .. "Return to Krasnodar. " + elseif currentStatus == 7 then + msg = msg .. string.format( "%s: ", groupName ) + msg = msg .. "Destroyed. " + elseif currentStatus == 8 then + msg = msg .. string.format( "%s: ", groupName ) + msg = msg .. "Take-Off. " + end + else + if currentStatus == 7 then + msg = msg .. string.format( "%s: ", groupName ) + msg = msg .. "Destroyed. " + else + Su34Destroyed( groupName ) + end + end + end - boardMsgRed.statusMsg = msg + boardMsgRed.statusMsg = msg end - function UpdateBoardMsg() ---trace.f() - Su34OverviewStatus() - MessageToRed( boardMsgRed.statusMsg, 15, 'RedStatus' ) + -- trace.f() + Su34OverviewStatus() + MessageToRed( boardMsgRed.statusMsg, 15, 'RedStatus' ) end function MusicReset( flg ) ---trace.f() - trigger.action.setUserFlag(95,flg) -end - -function PlaneActivate(groupNameFormat, flg) ---trace.f() - local groupName = groupNameFormat .. string.format("#%03d", trigger.misc.getUserFlag(flg)) - --trigger.action.outText(groupName,10) - trigger.action.activateGroup(Group.getByName(groupName)) -end - -function Su34Menu(groupName) ---trace.f() - - --env.info(( 'Su34Menu(' .. groupName .. ')' )) - local groupSu34 = Group.getByName( groupName ) - - if Su34Status.status[groupName] == 1 or - Su34Status.status[groupName] == 2 or - Su34Status.status[groupName] == 3 or - Su34Status.status[groupName] == 4 or - Su34Status.status[groupName] == 5 then - if Su34MenuPath[groupName] == nil then - if planeMenuPath == nil then - planeMenuPath = missionCommands.addSubMenuForCoalition( - coalition.side.RED, - "SU-34 anti-ship flights", - nil - ) - end - Su34MenuPath[groupName] = missionCommands.addSubMenuForCoalition( - coalition.side.RED, - "Flight " .. groupName, - planeMenuPath - ) - - missionCommands.addCommandForCoalition( - coalition.side.RED, - "Attack carrier Carl Vinson", - Su34MenuPath[groupName], - Su34AttackCarlVinson, - groupName - ) - - missionCommands.addCommandForCoalition( - coalition.side.RED, - "Attack ships in the west", - Su34MenuPath[groupName], - Su34AttackWest, - groupName - ) - - missionCommands.addCommandForCoalition( - coalition.side.RED, - "Attack ships in the north", - Su34MenuPath[groupName], - Su34AttackNorth, - groupName - ) - - missionCommands.addCommandForCoalition( - coalition.side.RED, - "Hold position and await instructions", - Su34MenuPath[groupName], - Su34Orbit, - groupName - ) - - missionCommands.addCommandForCoalition( - coalition.side.RED, - "Report status", - Su34MenuPath[groupName], - Su34OverviewStatus - ) - end - else - if Su34MenuPath[groupName] then - missionCommands.removeItemForCoalition(coalition.side.RED, Su34MenuPath[groupName]) - end - end + -- trace.f() + trigger.action.setUserFlag( 95, flg ) +end + +function PlaneActivate( groupNameFormat, flg ) + -- trace.f() + local groupName = groupNameFormat .. string.format( "#%03d", trigger.misc.getUserFlag( flg ) ) + -- trigger.action.outText(groupName,10) + trigger.action.activateGroup( Group.getByName( groupName ) ) +end + +function Su34Menu( groupName ) + -- trace.f() + + -- env.info(( 'Su34Menu(' .. groupName .. ')' )) + local groupSu34 = Group.getByName( groupName ) + + if Su34Status.status[groupName] == 1 or Su34Status.status[groupName] == 2 or Su34Status.status[groupName] == 3 or Su34Status.status[groupName] == 4 or Su34Status.status[groupName] == 5 then + if Su34MenuPath[groupName] == nil then + if planeMenuPath == nil then + planeMenuPath = missionCommands.addSubMenuForCoalition( coalition.side.RED, "SU-34 anti-ship flights", nil ) + end + Su34MenuPath[groupName] = missionCommands.addSubMenuForCoalition( coalition.side.RED, "Flight " .. groupName, planeMenuPath ) + + missionCommands.addCommandForCoalition( coalition.side.RED, "Attack carrier Carl Vinson", Su34MenuPath[groupName], Su34AttackCarlVinson, groupName ) + + missionCommands.addCommandForCoalition( coalition.side.RED, "Attack ships in the west", Su34MenuPath[groupName], Su34AttackWest, groupName ) + + missionCommands.addCommandForCoalition( coalition.side.RED, "Attack ships in the north", Su34MenuPath[groupName], Su34AttackNorth, groupName ) + + missionCommands.addCommandForCoalition( coalition.side.RED, "Hold position and await instructions", Su34MenuPath[groupName], Su34Orbit, groupName ) + + missionCommands.addCommandForCoalition( coalition.side.RED, "Report status", Su34MenuPath[groupName], Su34OverviewStatus ) + end + else + if Su34MenuPath[groupName] then + missionCommands.removeItemForCoalition( coalition.side.RED, Su34MenuPath[groupName] ) + end + end end --- Obsolete function, but kept to rework in framework. -function ChooseInfantry ( TeleportPrefixTable, TeleportMax ) ---trace.f("Spawn") - --env.info(( 'ChooseInfantry: ' )) +function ChooseInfantry( TeleportPrefixTable, TeleportMax ) + -- trace.f("Spawn") + -- env.info(( 'ChooseInfantry: ' )) - TeleportPrefixTableCount = #TeleportPrefixTable - TeleportPrefixTableIndex = math.random( 1, TeleportPrefixTableCount ) + TeleportPrefixTableCount = #TeleportPrefixTable + TeleportPrefixTableIndex = math.random( 1, TeleportPrefixTableCount ) - --env.info(( 'ChooseInfantry: TeleportPrefixTableIndex = ' .. TeleportPrefixTableIndex .. ' TeleportPrefixTableCount = ' .. TeleportPrefixTableCount .. ' TeleportMax = ' .. TeleportMax )) + -- env.info(( 'ChooseInfantry: TeleportPrefixTableIndex = ' .. TeleportPrefixTableIndex .. ' TeleportPrefixTableCount = ' .. TeleportPrefixTableCount .. ' TeleportMax = ' .. TeleportMax )) - local TeleportFound = false - local TeleportLoop = true - local Index = TeleportPrefixTableIndex - local TeleportPrefix = '' + local TeleportFound = false + local TeleportLoop = true + local Index = TeleportPrefixTableIndex + local TeleportPrefix = '' - while TeleportLoop do - TeleportPrefix = TeleportPrefixTable[Index] - if SpawnSettings[TeleportPrefix] then - if SpawnSettings[TeleportPrefix]['SpawnCount'] - 1 < TeleportMax then - SpawnSettings[TeleportPrefix]['SpawnCount'] = SpawnSettings[TeleportPrefix]['SpawnCount'] + 1 - TeleportFound = true - else - TeleportFound = false - end - else - SpawnSettings[TeleportPrefix] = {} - SpawnSettings[TeleportPrefix]['SpawnCount'] = 0 - TeleportFound = true - end - if TeleportFound then - TeleportLoop = false - else - if Index < TeleportPrefixTableCount then - Index = Index + 1 - else - TeleportLoop = false - end - end - --env.info(( 'ChooseInfantry: Loop 1 - TeleportPrefix = ' .. TeleportPrefix .. ' Index = ' .. Index )) - end + while TeleportLoop do + TeleportPrefix = TeleportPrefixTable[Index] + if SpawnSettings[TeleportPrefix] then + if SpawnSettings[TeleportPrefix]['SpawnCount'] - 1 < TeleportMax then + SpawnSettings[TeleportPrefix]['SpawnCount'] = SpawnSettings[TeleportPrefix]['SpawnCount'] + 1 + TeleportFound = true + else + TeleportFound = false + end + else + SpawnSettings[TeleportPrefix] = {} + SpawnSettings[TeleportPrefix]['SpawnCount'] = 0 + TeleportFound = true + end + if TeleportFound then + TeleportLoop = false + else + if Index < TeleportPrefixTableCount then + Index = Index + 1 + else + TeleportLoop = false + end + end + -- env.info(( 'ChooseInfantry: Loop 1 - TeleportPrefix = ' .. TeleportPrefix .. ' Index = ' .. Index )) + end - if TeleportFound == false then - TeleportLoop = true - Index = 1 - while TeleportLoop do - TeleportPrefix = TeleportPrefixTable[Index] - if SpawnSettings[TeleportPrefix] then - if SpawnSettings[TeleportPrefix]['SpawnCount'] - 1 < TeleportMax then - SpawnSettings[TeleportPrefix]['SpawnCount'] = SpawnSettings[TeleportPrefix]['SpawnCount'] + 1 - TeleportFound = true - else - TeleportFound = false - end - else - SpawnSettings[TeleportPrefix] = {} - SpawnSettings[TeleportPrefix]['SpawnCount'] = 0 - TeleportFound = true - end - if TeleportFound then - TeleportLoop = false - else - if Index < TeleportPrefixTableIndex then - Index = Index + 1 - else - TeleportLoop = false - end - end - --env.info(( 'ChooseInfantry: Loop 2 - TeleportPrefix = ' .. TeleportPrefix .. ' Index = ' .. Index )) - end - end + if TeleportFound == false then + TeleportLoop = true + Index = 1 + while TeleportLoop do + TeleportPrefix = TeleportPrefixTable[Index] + if SpawnSettings[TeleportPrefix] then + if SpawnSettings[TeleportPrefix]['SpawnCount'] - 1 < TeleportMax then + SpawnSettings[TeleportPrefix]['SpawnCount'] = SpawnSettings[TeleportPrefix]['SpawnCount'] + 1 + TeleportFound = true + else + TeleportFound = false + end + else + SpawnSettings[TeleportPrefix] = {} + SpawnSettings[TeleportPrefix]['SpawnCount'] = 0 + TeleportFound = true + end + if TeleportFound then + TeleportLoop = false + else + if Index < TeleportPrefixTableIndex then + Index = Index + 1 + else + TeleportLoop = false + end + end + -- env.info(( 'ChooseInfantry: Loop 2 - TeleportPrefix = ' .. TeleportPrefix .. ' Index = ' .. Index )) + end + end - local TeleportGroupName = '' - if TeleportFound == true then - TeleportGroupName = TeleportPrefix .. string.format("#%03d", SpawnSettings[TeleportPrefix]['SpawnCount'] ) - else - TeleportGroupName = '' - end + local TeleportGroupName = '' + if TeleportFound == true then + TeleportGroupName = TeleportPrefix .. string.format( "#%03d", SpawnSettings[TeleportPrefix]['SpawnCount'] ) + else + TeleportGroupName = '' + end - --env.info(('ChooseInfantry: TeleportGroupName = ' .. TeleportGroupName )) - --env.info(('ChooseInfantry: return')) + -- env.info(('ChooseInfantry: TeleportGroupName = ' .. TeleportGroupName )) + -- env.info(('ChooseInfantry: return')) - return TeleportGroupName + return TeleportGroupName end SpawnedInfantry = 0 -function LandCarrier ( CarrierGroup, LandingZonePrefix ) ---trace.f() - --env.info(( 'LandCarrier: ' )) - --env.info(( 'LandCarrier: CarrierGroup = ' .. CarrierGroup:getName() )) - --env.info(( 'LandCarrier: LandingZone = ' .. LandingZonePrefix )) +function LandCarrier( CarrierGroup, LandingZonePrefix ) + -- trace.f() + -- env.info(( 'LandCarrier: ' )) + -- env.info(( 'LandCarrier: CarrierGroup = ' .. CarrierGroup:getName() )) + -- env.info(( 'LandCarrier: LandingZone = ' .. LandingZonePrefix )) - local controllerGroup = CarrierGroup:getController() + local controllerGroup = CarrierGroup:getController() - local LandingZone = trigger.misc.getZone(LandingZonePrefix) - local LandingZonePos = {} - LandingZonePos.x = LandingZone.point.x + math.random(LandingZone.radius * -1, LandingZone.radius) - LandingZonePos.y = LandingZone.point.z + math.random(LandingZone.radius * -1, LandingZone.radius) + local LandingZone = trigger.misc.getZone( LandingZonePrefix ) + local LandingZonePos = {} + LandingZonePos.x = LandingZone.point.x + math.random( LandingZone.radius * -1, LandingZone.radius ) + LandingZonePos.y = LandingZone.point.z + math.random( LandingZone.radius * -1, LandingZone.radius ) - controllerGroup:pushTask( { id = 'Land', params = { point = LandingZonePos, durationFlag = true, duration = 10 } } ) + controllerGroup:pushTask( { id = 'Land', params = { point = LandingZonePos, durationFlag = true, duration = 10 } } ) - --env.info(( 'LandCarrier: end' )) + -- env.info(( 'LandCarrier: end' )) end EscortCount = 0 -function EscortCarrier ( CarrierGroup, EscortPrefix, EscortLastWayPoint, EscortEngagementDistanceMax, EscortTargetTypes ) ---trace.f() - --env.info(( 'EscortCarrier: ' )) - --env.info(( 'EscortCarrier: CarrierGroup = ' .. CarrierGroup:getName() )) - --env.info(( 'EscortCarrier: EscortPrefix = ' .. EscortPrefix )) +function EscortCarrier( CarrierGroup, EscortPrefix, EscortLastWayPoint, EscortEngagementDistanceMax, EscortTargetTypes ) + -- trace.f() + -- env.info(( 'EscortCarrier: ' )) + -- env.info(( 'EscortCarrier: CarrierGroup = ' .. CarrierGroup:getName() )) + -- env.info(( 'EscortCarrier: EscortPrefix = ' .. EscortPrefix )) - local CarrierName = CarrierGroup:getName() + local CarrierName = CarrierGroup:getName() - local EscortMission = {} - local CarrierMission = {} + local EscortMission = {} + local CarrierMission = {} - local EscortMission = SpawnMissionGroup( EscortPrefix ) - local CarrierMission = SpawnMissionGroup( CarrierGroup:getName() ) + local EscortMission = SpawnMissionGroup( EscortPrefix ) + local CarrierMission = SpawnMissionGroup( CarrierGroup:getName() ) - if EscortMission ~= nil and CarrierMission ~= nil then + if EscortMission ~= nil and CarrierMission ~= nil then - EscortCount = EscortCount + 1 - EscortMissionName = string.format( EscortPrefix .. '#Escort %s', CarrierName ) - EscortMission.name = EscortMissionName - EscortMission.groupId = nil - EscortMission.lateActivation = false - EscortMission.taskSelected = false - - local EscortUnits = #EscortMission.units - for u = 1, EscortUnits do - EscortMission.units[u].name = string.format( EscortPrefix .. '#Escort %s %02d', CarrierName, u ) - EscortMission.units[u].unitId = nil - end + EscortCount = EscortCount + 1 + EscortMissionName = string.format( EscortPrefix .. '#Escort %s', CarrierName ) + EscortMission.name = EscortMissionName + EscortMission.groupId = nil + EscortMission.lateActivation = false + EscortMission.taskSelected = false + local EscortUnits = #EscortMission.units + for u = 1, EscortUnits do + EscortMission.units[u].name = string.format( EscortPrefix .. '#Escort %s %02d', CarrierName, u ) + EscortMission.units[u].unitId = nil + end - EscortMission.route.points[1].task = { id = "ComboTask", - params = - { - tasks = - { - [1] = - { - enabled = true, - auto = false, - id = "Escort", - number = 1, - params = - { - lastWptIndexFlagChangedManually = false, - groupId = CarrierGroup:getID(), - lastWptIndex = nil, - lastWptIndexFlag = false, - engagementDistMax = EscortEngagementDistanceMax, - targetTypes = EscortTargetTypes, - pos = - { - y = 20, - x = 20, - z = 0, - } -- end of ["pos"] - } -- end of ["params"] - } -- end of [1] - } -- end of ["tasks"] - } -- end of ["params"] - } -- end of ["task"] - - SpawnGroupAdd( EscortPrefix, EscortMission ) + EscortMission.route.points[1].task = { + id = "ComboTask", + params = { + tasks = { + [1] = { + enabled = true, + auto = false, + id = "Escort", + number = 1, + params = { + lastWptIndexFlagChangedManually = false, + groupId = CarrierGroup:getID(), + lastWptIndex = nil, + lastWptIndexFlag = false, + engagementDistMax = EscortEngagementDistanceMax, + targetTypes = EscortTargetTypes, + pos = { y = 20, x = 20, z = 0 } -- end of ["pos"] + } -- end of ["params"] + } -- end of [1] + } -- end of ["tasks"] + } -- end of ["params"] + } -- end of ["task"] + + SpawnGroupAdd( EscortPrefix, EscortMission ) - end + end end function SendMessageToCarrier( CarrierGroup, CarrierMessage ) ---trace.f() + -- trace.f() - if CarrierGroup ~= nil then - MessageToGroup( CarrierGroup, CarrierMessage, 30, 'Carrier/' .. CarrierGroup:getName() ) - end + if CarrierGroup ~= nil then + MessageToGroup( CarrierGroup, CarrierMessage, 30, 'Carrier/' .. CarrierGroup:getName() ) + end end function MessageToGroup( MsgGroup, MsgText, MsgTime, MsgName ) ---trace.f() + -- trace.f() - if type(MsgGroup) == 'string' then - --env.info( 'MessageToGroup: Converted MsgGroup string "' .. MsgGroup .. '" into a Group structure.' ) - MsgGroup = Group.getByName( MsgGroup ) - end + if type( MsgGroup ) == 'string' then + -- env.info( 'MessageToGroup: Converted MsgGroup string "' .. MsgGroup .. '" into a Group structure.' ) + MsgGroup = Group.getByName( MsgGroup ) + end - if MsgGroup ~= nil then - local MsgTable = {} - MsgTable.text = MsgText - MsgTable.displayTime = MsgTime - MsgTable.msgFor = { units = { MsgGroup:getUnits()[1]:getName() } } - MsgTable.name = MsgName - --routines.message.add( MsgTable ) - --env.info(('MessageToGroup: Message sent to ' .. MsgGroup:getUnits()[1]:getName() .. ' -> ' .. MsgText )) - end + if MsgGroup ~= nil then + local MsgTable = {} + MsgTable.text = MsgText + MsgTable.displayTime = MsgTime + MsgTable.msgFor = { units = { MsgGroup:getUnits()[1]:getName() } } + MsgTable.name = MsgName + -- routines.message.add( MsgTable ) + -- env.info(('MessageToGroup: Message sent to ' .. MsgGroup:getUnits()[1]:getName() .. ' -> ' .. MsgText )) + end end function MessageToUnit( UnitName, MsgText, MsgTime, MsgName ) ---trace.f() - - if UnitName ~= nil then - local MsgTable = {} - MsgTable.text = MsgText - MsgTable.displayTime = MsgTime - MsgTable.msgFor = { units = { UnitName } } - MsgTable.name = MsgName - --routines.message.add( MsgTable ) - end + -- trace.f() + + if UnitName ~= nil then + local MsgTable = {} + MsgTable.text = MsgText + MsgTable.displayTime = MsgTime + MsgTable.msgFor = { units = { UnitName } } + MsgTable.name = MsgName + -- routines.message.add( MsgTable ) + end end function MessageToAll( MsgText, MsgTime, MsgName ) ---trace.f() + -- trace.f() - MESSAGE:New( MsgText, MsgTime, "Message" ):ToCoalition( coalition.side.RED ):ToCoalition( coalition.side.BLUE ) + MESSAGE:New( MsgText, MsgTime, "Message" ):ToCoalition( coalition.side.RED ):ToCoalition( coalition.side.BLUE ) end function MessageToRed( MsgText, MsgTime, MsgName ) ---trace.f() + -- trace.f() - MESSAGE:New( MsgText, MsgTime, "To Red Coalition" ):ToCoalition( coalition.side.RED ) + MESSAGE:New( MsgText, MsgTime, "To Red Coalition" ):ToCoalition( coalition.side.RED ) end function MessageToBlue( MsgText, MsgTime, MsgName ) ---trace.f() + -- trace.f() - MESSAGE:New( MsgText, MsgTime, "To Blue Coalition" ):ToCoalition( coalition.side.BLUE ) + MESSAGE:New( MsgText, MsgTime, "To Blue Coalition" ):ToCoalition( coalition.side.BLUE ) end function getCarrierHeight( CarrierGroup ) ---trace.f() - - if CarrierGroup ~= nil then - if table.getn(CarrierGroup:getUnits()) == 1 then - local CarrierUnit = CarrierGroup:getUnits()[1] - local CurrentPoint = CarrierUnit:getPoint() + -- trace.f() - local CurrentPosition = { x = CurrentPoint.x, y = CurrentPoint.z } - local CarrierHeight = CurrentPoint.y + if CarrierGroup ~= nil then + if table.getn( CarrierGroup:getUnits() ) == 1 then + local CarrierUnit = CarrierGroup:getUnits()[1] + local CurrentPoint = CarrierUnit:getPoint() - local LandHeight = land.getHeight( CurrentPosition ) + local CurrentPosition = { x = CurrentPoint.x, y = CurrentPoint.z } + local CarrierHeight = CurrentPoint.y - --env.info(( 'CarrierHeight: LandHeight = ' .. LandHeight .. ' CarrierHeight = ' .. CarrierHeight )) + local LandHeight = land.getHeight( CurrentPosition ) - return CarrierHeight - LandHeight - else - return 999999 - end - else - return 999999 - end + -- env.info(( 'CarrierHeight: LandHeight = ' .. LandHeight .. ' CarrierHeight = ' .. CarrierHeight )) + return CarrierHeight - LandHeight + else + return 999999 + end + else + return 999999 + end end function GetUnitHeight( CheckUnit ) ---trace.f() + -- trace.f() - local UnitPoint = CheckUnit:getPoint() - local UnitPosition = { x = CurrentPoint.x, y = CurrentPoint.z } - local UnitHeight = CurrentPoint.y + local UnitPoint = CheckUnit:getPoint() + local UnitPosition = { x = CurrentPoint.x, y = CurrentPoint.z } + local UnitHeight = CurrentPoint.y - local LandHeight = land.getHeight( CurrentPosition ) + local LandHeight = land.getHeight( CurrentPosition ) - --env.info(( 'CarrierHeight: LandHeight = ' .. LandHeight .. ' CarrierHeight = ' .. CarrierHeight )) - - return UnitHeight - LandHeight + -- env.info(( 'CarrierHeight: LandHeight = ' .. LandHeight .. ' CarrierHeight = ' .. CarrierHeight )) + return UnitHeight - LandHeight end - _MusicTable = {} _MusicTable.Files = {} _MusicTable.Queue = {} _MusicTable.FileCnt = 0 - function MusicRegister( SndRef, SndFile, SndTime ) ---trace.f() + -- trace.f() - env.info(( 'MusicRegister: SndRef = ' .. SndRef )) - env.info(( 'MusicRegister: SndFile = ' .. SndFile )) - env.info(( 'MusicRegister: SndTime = ' .. SndTime )) + env.info( ('MusicRegister: SndRef = ' .. SndRef) ) + env.info( ('MusicRegister: SndFile = ' .. SndFile) ) + env.info( ('MusicRegister: SndTime = ' .. SndTime) ) + _MusicTable.FileCnt = _MusicTable.FileCnt + 1 - _MusicTable.FileCnt = _MusicTable.FileCnt + 1 - - _MusicTable.Files[_MusicTable.FileCnt] = {} - _MusicTable.Files[_MusicTable.FileCnt].Ref = SndRef - _MusicTable.Files[_MusicTable.FileCnt].File = SndFile - _MusicTable.Files[_MusicTable.FileCnt].Time = SndTime - - if not _MusicTable.Function then - _MusicTable.Function = routines.scheduleFunction( MusicScheduler, { }, timer.getTime() + 10, 10) - end + _MusicTable.Files[_MusicTable.FileCnt] = {} + _MusicTable.Files[_MusicTable.FileCnt].Ref = SndRef + _MusicTable.Files[_MusicTable.FileCnt].File = SndFile + _MusicTable.Files[_MusicTable.FileCnt].Time = SndTime + if not _MusicTable.Function then + _MusicTable.Function = routines.scheduleFunction( MusicScheduler, {}, timer.getTime() + 10, 10 ) + end end function MusicToPlayer( SndRef, PlayerName, SndContinue ) ---trace.f() - - --env.info(( 'MusicToPlayer: SndRef = ' .. SndRef )) - - local PlayerUnits = AlivePlayerUnits() - for PlayerUnitIdx, PlayerUnit in pairs(PlayerUnits) do - local PlayerUnitName = PlayerUnit:getPlayerName() - --env.info(( 'MusicToPlayer: PlayerUnitName = ' .. PlayerUnitName )) - if PlayerName == PlayerUnitName then - PlayerGroup = PlayerUnit:getGroup() - if PlayerGroup then - --env.info(( 'MusicToPlayer: PlayerGroup = ' .. PlayerGroup:getName() )) - MusicToGroup( SndRef, PlayerGroup, SndContinue ) - end - break - end - end + -- trace.f() + + -- env.info(( 'MusicToPlayer: SndRef = ' .. SndRef )) - --env.info(( 'MusicToPlayer: end' )) + local PlayerUnits = AlivePlayerUnits() + for PlayerUnitIdx, PlayerUnit in pairs( PlayerUnits ) do + local PlayerUnitName = PlayerUnit:getPlayerName() + -- env.info(( 'MusicToPlayer: PlayerUnitName = ' .. PlayerUnitName )) + if PlayerName == PlayerUnitName then + PlayerGroup = PlayerUnit:getGroup() + if PlayerGroup then + -- env.info(( 'MusicToPlayer: PlayerGroup = ' .. PlayerGroup:getName() )) + MusicToGroup( SndRef, PlayerGroup, SndContinue ) + end + break + end + end + -- env.info(( 'MusicToPlayer: end' )) end function MusicToGroup( SndRef, SndGroup, SndContinue ) ---trace.f() - - --env.info(( 'MusicToGroup: SndRef = ' .. SndRef )) - - if SndGroup ~= nil then - if _MusicTable and _MusicTable.FileCnt > 0 then - if SndGroup:isExist() then - if MusicCanStart(SndGroup:getUnit(1):getPlayerName()) then - --env.info(( 'MusicToGroup: OK for Sound.' )) - local SndIdx = 0 - if SndRef == '' then - --env.info(( 'MusicToGroup: SndRef as empty. Queueing at random.' )) - SndIdx = math.random( 1, _MusicTable.FileCnt ) - else - for SndIdx = 1, _MusicTable.FileCnt do - if _MusicTable.Files[SndIdx].Ref == SndRef then - break - end - end - end - --env.info(( 'MusicToGroup: SndIdx = ' .. SndIdx )) - --env.info(( 'MusicToGroup: Queueing Music ' .. _MusicTable.Files[SndIdx].File .. ' for Group ' .. SndGroup:getID() )) - trigger.action.outSoundForGroup( SndGroup:getID(), _MusicTable.Files[SndIdx].File ) - MessageToGroup( SndGroup, 'Playing ' .. _MusicTable.Files[SndIdx].File, 15, 'Music-' .. SndGroup:getUnit(1):getPlayerName() ) - - local SndQueueRef = SndGroup:getUnit(1):getPlayerName() - if _MusicTable.Queue[SndQueueRef] == nil then - _MusicTable.Queue[SndQueueRef] = {} - end - _MusicTable.Queue[SndQueueRef].Start = timer.getTime() - _MusicTable.Queue[SndQueueRef].PlayerName = SndGroup:getUnit(1):getPlayerName() - _MusicTable.Queue[SndQueueRef].Group = SndGroup - _MusicTable.Queue[SndQueueRef].ID = SndGroup:getID() - _MusicTable.Queue[SndQueueRef].Ref = SndIdx - _MusicTable.Queue[SndQueueRef].Continue = SndContinue - _MusicTable.Queue[SndQueueRef].Type = Group - end - end - end - end + -- trace.f() + + -- env.info(( 'MusicToGroup: SndRef = ' .. SndRef )) + + if SndGroup ~= nil then + if _MusicTable and _MusicTable.FileCnt > 0 then + if SndGroup:isExist() then + if MusicCanStart( SndGroup:getUnit( 1 ):getPlayerName() ) then + -- env.info(( 'MusicToGroup: OK for Sound.' )) + local SndIdx = 0 + if SndRef == '' then + -- env.info(( 'MusicToGroup: SndRef as empty. Queueing at random.' )) + SndIdx = math.random( 1, _MusicTable.FileCnt ) + else + for SndIdx = 1, _MusicTable.FileCnt do + if _MusicTable.Files[SndIdx].Ref == SndRef then + break + end + end + end + -- env.info(( 'MusicToGroup: SndIdx = ' .. SndIdx )) + -- env.info(( 'MusicToGroup: Queueing Music ' .. _MusicTable.Files[SndIdx].File .. ' for Group ' .. SndGroup:getID() )) + trigger.action.outSoundForGroup( SndGroup:getID(), _MusicTable.Files[SndIdx].File ) + MessageToGroup( SndGroup, 'Playing ' .. _MusicTable.Files[SndIdx].File, 15, 'Music-' .. SndGroup:getUnit( 1 ):getPlayerName() ) + + local SndQueueRef = SndGroup:getUnit( 1 ):getPlayerName() + if _MusicTable.Queue[SndQueueRef] == nil then + _MusicTable.Queue[SndQueueRef] = {} + end + _MusicTable.Queue[SndQueueRef].Start = timer.getTime() + _MusicTable.Queue[SndQueueRef].PlayerName = SndGroup:getUnit( 1 ):getPlayerName() + _MusicTable.Queue[SndQueueRef].Group = SndGroup + _MusicTable.Queue[SndQueueRef].ID = SndGroup:getID() + _MusicTable.Queue[SndQueueRef].Ref = SndIdx + _MusicTable.Queue[SndQueueRef].Continue = SndContinue + _MusicTable.Queue[SndQueueRef].Type = Group + end + end + end + end end -function MusicCanStart(PlayerName) ---trace.f() +function MusicCanStart( PlayerName ) + -- trace.f() - --env.info(( 'MusicCanStart:' )) + -- env.info(( 'MusicCanStart:' )) - local MusicOut = false + local MusicOut = false - if _MusicTable['Queue'] ~= nil and _MusicTable.FileCnt > 0 then - --env.info(( 'MusicCanStart: PlayerName = ' .. PlayerName )) - local PlayerFound = false - local MusicStart = 0 - local MusicTime = 0 - for SndQueueIdx, SndQueue in pairs( _MusicTable.Queue ) do - if SndQueue.PlayerName == PlayerName then - PlayerFound = true - MusicStart = SndQueue.Start - MusicTime = _MusicTable.Files[SndQueue.Ref].Time - break - end - end - if PlayerFound then - --env.info(( 'MusicCanStart: MusicStart = ' .. MusicStart )) - --env.info(( 'MusicCanStart: MusicTime = ' .. MusicTime )) - --env.info(( 'MusicCanStart: timer.getTime() = ' .. timer.getTime() )) + if _MusicTable['Queue'] ~= nil and _MusicTable.FileCnt > 0 then + -- env.info(( 'MusicCanStart: PlayerName = ' .. PlayerName )) + local PlayerFound = false + local MusicStart = 0 + local MusicTime = 0 + for SndQueueIdx, SndQueue in pairs( _MusicTable.Queue ) do + if SndQueue.PlayerName == PlayerName then + PlayerFound = true + MusicStart = SndQueue.Start + MusicTime = _MusicTable.Files[SndQueue.Ref].Time + break + end + end + if PlayerFound then + -- env.info(( 'MusicCanStart: MusicStart = ' .. MusicStart )) + -- env.info(( 'MusicCanStart: MusicTime = ' .. MusicTime )) + -- env.info(( 'MusicCanStart: timer.getTime() = ' .. timer.getTime() )) - if MusicStart + MusicTime <= timer.getTime() then - MusicOut = true - end - else - MusicOut = true - end - end + if MusicStart + MusicTime <= timer.getTime() then + MusicOut = true + end + else + MusicOut = true + end + end - if MusicOut then - --env.info(( 'MusicCanStart: true' )) - else - --env.info(( 'MusicCanStart: false' )) - end + if MusicOut then + -- env.info(( 'MusicCanStart: true' )) + else + -- env.info(( 'MusicCanStart: false' )) + end - return MusicOut + return MusicOut end function MusicScheduler() ---trace.scheduled("", "MusicScheduler") - - --env.info(( 'MusicScheduler:' )) - if _MusicTable['Queue'] ~= nil and _MusicTable.FileCnt > 0 then - --env.info(( 'MusicScheduler: Walking Sound Queue.')) - for SndQueueIdx, SndQueue in pairs( _MusicTable.Queue ) do - if SndQueue.Continue then - if MusicCanStart(SndQueue.PlayerName) then - --env.info(('MusicScheduler: MusicToGroup')) - MusicToPlayer( '', SndQueue.PlayerName, true ) - end - end - end - end + -- trace.scheduled("", "MusicScheduler") + -- env.info(( 'MusicScheduler:' )) + if _MusicTable['Queue'] ~= nil and _MusicTable.FileCnt > 0 then + -- env.info(( 'MusicScheduler: Walking Sound Queue.')) + for SndQueueIdx, SndQueue in pairs( _MusicTable.Queue ) do + if SndQueue.Continue then + if MusicCanStart( SndQueue.PlayerName ) then + -- env.info(('MusicScheduler: MusicToGroup')) + MusicToPlayer( '', SndQueue.PlayerName, true ) + end + end + end + end 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. --- --- ### Authors: --- +env.info( ('Init: Scripts Loaded v1.1') ) +--- **Utilities** - Derived utilities taken from the MIST framework, added helpers from the MOOSE community. +-- +-- ### Authors: +-- -- * Grimes : Design & Programming of the MIST framework. --- +-- -- ### Contributions: --- --- * FlightControl : Rework to OO framework --- --- @module Utils +-- +-- * FlightControl : Rework to OO framework. +-- * And many more +-- +-- @module Utilities.Utils -- @image MOOSE.JPG @@ -2884,7 +2941,7 @@ env.info(( 'Init: Scripts Loaded v1.1' )) -- @field White -- @field Orange -- @field Blue - + SMOKECOLOR = trigger.smokeColor -- #SMOKECOLOR --- @type FLARECOLOR @@ -2917,6 +2974,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 +2982,8 @@ DCSMAP = { PersianGulf="PersianGulf", TheChannel="TheChannel", Syria="Syria", - MarianaIslands="MarianaIslands" + MarianaIslands="MarianaIslands", + Falklands="Falklands", } @@ -2960,7 +3019,7 @@ CALLSIGN={ Texaco=1, Arco=2, Shell=3, - }, + }, -- JTAC JTAC={ Axeman=1, @@ -2996,6 +3055,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. @@ -3029,31 +3144,31 @@ UTILS = { UTILS.IsInstanceOf = function( object, className ) -- Is className NOT a string ? if not type( className ) == 'string' then - + -- Is className a Moose class ? if type( className ) == 'table' and className.IsInstanceOf ~= nil then - + -- Get the name of the Moose class as a string className = className.ClassName - + -- className is neither a string nor a Moose class, throw an error else - + -- I'm not sure if this should take advantage of MOOSE logging function, or throw an error for pcall local err_str = 'className parameter should be a string; parameter received: '..type( className ) return false -- error( err_str ) - + end end - + -- Is the object a Moose class instance ? if type( object ) == 'table' and object.IsInstanceOf ~= nil then - + -- Use the IsInstanceOf method of the BASE class return object:IsInstanceOf( className ) else - + -- If the object is not an instance of a Moose class, evaluate against lua basic data types local basicDataTypes = { 'string', 'number', 'function', 'boolean', 'nil', 'table' } for _, basicDataType in ipairs( basicDataTypes ) do @@ -3062,7 +3177,7 @@ UTILS.IsInstanceOf = function( object, className ) end end end - + -- Check failed return false end @@ -3074,7 +3189,7 @@ end UTILS.DeepCopy = function(object) local lookup_table = {} - + -- Copy function. local function _copy(object) if type(object) ~= "table" then @@ -3082,20 +3197,20 @@ UTILS.DeepCopy = function(object) elseif lookup_table[object] then return lookup_table[object] end - + local new_table = {} - + lookup_table[object] = new_table - + for index, value in pairs(object) do new_table[_copy(index)] = _copy(value) end - + return setmetatable(new_table, getmetatable(object)) end - + local objectreturn = _copy(object) - + return objectreturn end @@ -3105,19 +3220,19 @@ end UTILS.OneLineSerialize = function( tbl ) -- serialization of a table all on a single line, no comments, made to replace old get_table_string function lookup_table = {} - + local function _Serialize( tbl ) if type(tbl) == 'table' then --function only works for tables! - + if lookup_table[tbl] then return lookup_table[object] end local tbl_str = {} - + lookup_table[tbl] = tbl_str - + tbl_str[#tbl_str + 1] = '{' for ind,val in pairs(tbl) do -- serialize its fields @@ -3165,7 +3280,7 @@ UTILS.OneLineSerialize = function( tbl ) -- serialization of a table all on a s env.info('unable to serialize value type ' .. routines.utils.basicSerialize(type(val)) .. ' at index ' .. tostring(ind)) env.info( debug.traceback() ) end - + end tbl_str[#tbl_str + 1] = '}' return table.concat(tbl_str) @@ -3173,7 +3288,7 @@ UTILS.OneLineSerialize = function( tbl ) -- serialization of a table all on a s return tostring(tbl) end end - + local objectreturn = _Serialize(tbl) return objectreturn end @@ -3205,18 +3320,34 @@ UTILS.MetersToNM = function(meters) return meters/1852 end +UTILS.KiloMetersToNM = function(kilometers) + return kilometers/1852*1000 +end + UTILS.MetersToSM = function(meters) return meters/1609.34 end +UTILS.KiloMetersToSM = function(kilometers) + return kilometers/1609.34*1000 +end + UTILS.MetersToFeet = function(meters) return meters/0.3048 end +UTILS.KiloMetersToFeet = function(kilometers) + return kilometers/0.3048*1000 +end + UTILS.NMToMeters = function(NM) return NM*1852 end +UTILS.NMToKiloMeters = function(NM) + return NM*1852/1000 +end + UTILS.FeetToMeters = function(feet) return feet*0.3048 end @@ -3259,14 +3390,18 @@ 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 Celcius * 9/5 + 32 +-- @return #number Temperature in degrees Fahrenheit. +UTILS.CelsiusToFahrenheit = function( Celcius ) + return Celcius * 9/5 + 32 end --- Convert pressure from hecto Pascal (hPa) to inches of mercury (inHg). @@ -3276,12 +3411,12 @@ UTILS.hPa2inHg = function( hPa ) return hPa * 0.0295299830714 end ---- Convert knots to alitude corrected KIAS, e.g. for tankers. +--- Convert knots to altitude corrected KIAS, e.g. for tankers. -- @param #number knots Speed in knots. -- @param #number altitude Altitude in feet -- @return #number Corrected KIAS UTILS.KnotsToAltKIAS = function( knots, altitude ) - return (knots * 0.018 * (altitude / 1000)) + knots + return (knots * 0.018 * (altitude / 1000)) + knots end --- Convert pressure from hecto Pascal (hPa) to millimeters of mercury (mmHg). @@ -3393,30 +3528,32 @@ end -- acc- the accuracy of each easting/northing. 0, 1, 2, 3, 4, or 5. UTILS.tostringMGRS = function(MGRS, acc) --R2.1 - if acc == 0 then + if acc <= 0 then return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph else - + + if acc > 5 then acc = 5 end + -- Test if Easting/Northing have less than 4 digits. --MGRS.Easting=123 -- should be 00123 --MGRS.Northing=5432 -- should be 05432 - + -- Truncate rather than round MGRS grid! local Easting=tostring(MGRS.Easting) local Northing=tostring(MGRS.Northing) - + -- Count number of missing digits. Easting/Northing should have 5 digits. However, it is passed as a number. Therefore, any leading zeros would not be displayed by lua. - local nE=5-string.len(Easting) + local nE=5-string.len(Easting) local nN=5-string.len(Northing) - + -- Get leading zeros (if any). for i=1,nE do Easting="0"..Easting end for i=1,nN do Northing="0"..Northing end - + -- Return MGRS string. return string.format("%s %s %s %s", MGRS.UTMZone, MGRS.MGRSDigraph, string.sub(Easting, 1, acc), string.sub(Northing, 1, acc)) end - + end @@ -3444,7 +3581,7 @@ function UTILS.spairs( t, order ) for k in pairs(t) do keys[#keys+1] = k end -- if order function given, sort by it by passing the table and keys a, b, - -- otherwise just sort the keys + -- otherwise just sort the keys if order then table.sort(keys, function(a,b) return order(t, a, b) end) else @@ -3470,7 +3607,7 @@ function UTILS.kpairs( t, getkey, order ) for k, o in pairs(t) do keys[#keys+1] = k keyso[#keyso+1] = getkey( o ) end -- if order function given, sort by it by passing the table and keys a, b, - -- otherwise just sort the keys + -- otherwise just sort the keys if order then table.sort(keys, function(a,b) return order(t, a, b) end) else @@ -3490,7 +3627,7 @@ end -- Here is a customized version of pairs, which I called rpairs because it iterates over the table in a random order. function UTILS.rpairs( t ) -- collect the keys - + local keys = {} for k in pairs(t) do keys[#keys+1] = k end @@ -3501,7 +3638,7 @@ function UTILS.rpairs( t ) random[i] = keys[k] table.remove( keys, k ) end - + -- return the iterator function local i = 0 return function() @@ -3527,7 +3664,9 @@ function UTILS.RemoveMark(MarkID, Delay) if Delay and Delay>0 then TIMER:New(UTILS.RemoveMark, MarkID):Start(Delay) else - trigger.action.removeMark(MarkID) + if MarkID then + trigger.action.removeMark(MarkID) + end end end @@ -3598,7 +3737,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. @@ -3617,12 +3756,12 @@ end function UTILS.GetCharacters(str) local chars={} - + for i=1,#str do local c=str:sub(i,i) table.insert(chars, c) end - + return chars end @@ -3631,15 +3770,15 @@ end -- @param #boolean short (Optional) If true, use short output, i.e. (HH:)MM:SS without day. -- @return #string Time in format Hours:Minutes:Seconds+Days (HH:MM:SS+D). function UTILS.SecondsToClock(seconds, short) - + -- Nil check. if seconds==nil then return nil end - + -- Seconds local seconds = tonumber(seconds) - + -- Seconds of this day. local _seconds=seconds%(60*60*24) @@ -3669,10 +3808,10 @@ function UTILS.SecondsOfToday() -- Time in seconds. local time=timer.getAbsTime() - + -- Short format without days since mission start. local clock=UTILS.SecondsToClock(time, true) - + -- Time is now the seconds passed since last midnight. return UTILS.ClockToSeconds(clock) end @@ -3687,24 +3826,24 @@ end -- @param #string clock String of clock time. E.g., "06:12:35" or "5:1:30+1". Format is (H)H:(M)M:((S)S)(+D) H=Hours, M=Minutes, S=Seconds, D=Days. -- @return #number Seconds. Corresponds to what you cet from timer.getAbsTime() function. function UTILS.ClockToSeconds(clock) - + -- Nil check. if clock==nil then return nil end - + -- Seconds init. local seconds=0 - + -- Split additional days. local dsplit=UTILS.Split(clock, "+") - + -- Convert days to seconds. if #dsplit>1 then seconds=seconds+tonumber(dsplit[2])*60*60*24 end - -- Split hours, minutes, seconds + -- Split hours, minutes, seconds local tsplit=UTILS.Split(dsplit[1], ":") -- Get time in seconds @@ -3722,7 +3861,7 @@ function UTILS.ClockToSeconds(clock) end i=i+1 end - + return seconds end @@ -3734,12 +3873,12 @@ function UTILS.DisplayMissionTime(duration) local mission_time=Tnow-timer.getTime0() local mission_time_minutes=mission_time/60 local mission_time_seconds=mission_time%60 - local local_time=UTILS.SecondsToClock(Tnow) + local local_time=UTILS.SecondsToClock(Tnow) local text=string.format("Time: %s - %02d:%02d", local_time, mission_time_minutes, mission_time_seconds) MESSAGE:New(text, duration):ToAll() end ---- Replace illegal characters [<>|/?*:\\] in a string. +--- Replace illegal characters [<>|/?*:\\] in a string. -- @param #string Text Input text. -- @param #string ReplaceBy Replace illegal characters by this character or string. Default underscore "_". -- @return #string The input text with illegal chars replaced. @@ -3760,28 +3899,28 @@ function UTILS.RandomGaussian(x0, sigma, xmin, xmax, imax) -- Standard deviation. Default 10 if not given. sigma=sigma or 10 - + -- Max attempts. imax=imax or 100 - + local r local gotit=false local i=0 while not gotit do - + -- Uniform numbers in [0,1). We need two. local x1=math.random() local x2=math.random() - - -- Transform to Gaussian exp(-(x-x0)²/(2*sigma²). + + -- Transform to Gaussian exp(-(x-x0)°/(2*sigma°). r = math.sqrt(-2*sigma*sigma * math.log(x1)) * math.cos(2*math.pi * x2) + x0 - + i=i+1 if (r>=xmin and r<=xmax) or i>imax then gotit=true end end - + return r end @@ -3806,9 +3945,9 @@ function UTILS.Randomize(value, fac, lower, upper) else max=value+value*fac end - + local r=math.random(min, max) - + return r end @@ -3820,6 +3959,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. @@ -3827,6 +3975,49 @@ 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 2D with x, y components. +-- @param DCS#Vec2 b Vector in 2D with x, y components. +-- @return #number Distance between the vectors. +function UTILS.VecDist2D(a, b) + + local d = math.huge + + if (not a) or (not b) then return d end + + local c={x=b.x-a.x, y=b.y-a.y} + + d=math.sqrt(c.x*c.x+c.y*c.y) + + return d +end + + +--- Calculate the distance 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. +-- @return #number Distance between the vectors. +function UTILS.VecDist3D(a, b) + + + local d = math.huge + + if (not a) or (not b) then return d end + + local c={x=b.x-a.x, y=b.y-a.y, z=b.z-a.z} + + d=math.sqrt(UTILS.VecDot(c, c)) + + return d +end + --- Calculate the [cross product](https://en.wikipedia.org/wiki/Cross_product) of two 3D vectors. The result is a 3D vector. -- @param DCS#Vec3 a Vector in 3D with x, y, z components. -- @param DCS#Vec3 b Vector in 3D with x, y, z components. @@ -3835,7 +4026,7 @@ function UTILS.VecCross(a, b) return {x=a.y*b.z - a.z*b.y, y=a.z*b.x - a.x*b.z, z=a.x*b.y - a.y*b.x} end ---- Calculate the difference between two 3D vectors by substracting the x,y,z components from each other. +--- Calculate the difference between two 3D vectors by substracting the x,y,z components from 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. -- @return DCS#Vec3 Vector c=a-b with c(i)=a(i)-b(i), i=x,y,z. @@ -3843,7 +4034,15 @@ function UTILS.VecSubstract(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 3D vectors by adding the x,y,z components of each other. +--- 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. -- @return DCS#Vec3 Vector c=a+b with c(i)=a(i)+b(i), i=x,y,z. @@ -3851,14 +4050,22 @@ function UTILS.VecAdd(a, b) return {x=a.x+b.x, y=a.y+b.y, z=a.z+b.z} end ---- Calculate the angle between two 3D vectors. +--- 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. --- @return #number Angle alpha between and b in degrees. alpha=acos(a*b)/(|a||b|), (* denotes the dot product). +-- @return #number Angle alpha between and b in degrees. alpha=acos(a*b)/(|a||b|), (* denotes the dot product). function UTILS.VecAngle(a, b) local cosalpha=UTILS.VecDot(a,b)/(UTILS.VecNorm(a)*UTILS.VecNorm(b)) - + local alpha=0 if cosalpha>=0.9999999999 then --acos(1) is not defined. alpha=0 @@ -3866,8 +4073,8 @@ function UTILS.VecAngle(a, b) alpha=math.pi else alpha=math.acos(cosalpha) - end - + end + return math.deg(alpha) end @@ -3882,6 +4089,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. @@ -3891,18 +4109,18 @@ function UTILS.HdgDiff(h1, h2) -- Angle in rad. local alpha= math.rad(tonumber(h1)) local beta = math.rad(tonumber(h2)) - + -- Runway vector. local v1={x=math.cos(alpha), y=0, z=math.sin(alpha)} local v2={x=math.cos(beta), y=0, z=math.sin(beta)} local delta=UTILS.VecAngle(v1, v2) - + return math.abs(delta) end ---- Translate 3D vector in the 2D (x,z) plane. y-component (usually altitude) unchanged. +--- Translate 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 distance The distance to translate. -- @param #number angle Rotation angle in degrees. @@ -3918,26 +4136,61 @@ function UTILS.VecTranslate(a, distance, angle) return {x=TX, y=a.y, z=TY} end ---- Rotate 3D vector in the 2D (x,z) plane. y-component (usually altitude) unchanged. +--- 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. -- @return DCS#Vec3 Vector rotated in the (x,z) plane. function UTILS.Rotate2D(a, angle) local phi=math.rad(angle) - + local x=a.z local y=a.x - + local Z=x*math.cos(phi)-y*math.sin(phi) local X=x*math.sin(phi)+y*math.cos(phi) local Y=a.y - + local A={x=X, y=Y, z=Z} 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". @@ -3950,17 +4203,17 @@ function UTILS.TACANToFrequency(TACANChannel, TACANMode) end if TACANMode ~= "X" and TACANMode ~= "Y" then return nil -- error in arguments - end - + end + -- This code is largely based on ED's code, in DCS World\Scripts\World\Radio\BeaconTypes.lua, line 137. -- I have no idea what it does but it seems to work local A = 1151 -- 'X', channel >= 64 local B = 64 -- channel >= 64 - + if TACANChannel < 64 then B = 1 end - + if TACANMode == 'Y' then A = 1025 if TACANChannel < 64 then @@ -3971,7 +4224,7 @@ function UTILS.TACANToFrequency(TACANChannel, TACANMode) A = 962 end end - + return (A + TACANChannel - B) * 1000000 end @@ -3998,13 +4251,13 @@ end -- @param #number Time (Optional) Abs. time in seconds. Default now, i.e. the value return from timer.getAbsTime(). -- @return #number Day of the mission. Mission starts on day 0. function UTILS.GetMissionDay(Time) - + Time=Time or timer.getAbsTime() - + local clock=UTILS.SecondsToClock(Time, false) - + local x=tonumber(UTILS.Split(clock, "+")[2]) - + return x end @@ -4014,36 +4267,16 @@ end function UTILS.GetMissionDayOfYear(Time) local Date, Year, Month, Day=UTILS.GetDCSMissionDate() - - local d=UTILS.GetMissionDay(Time) - - return UTILS.GetDayOfYear(Year, Month, Day)+d - -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() + local d=UTILS.GetMissionDay(Time) - -- 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 + return UTILS.GetDayOfYear(Year, Month, Day)+d end --- Returns the magnetic declination of the map. -- Returned values for the current maps are: --- +-- -- * Caucasus +6 (East), year ~ 2011 -- * NTTR +12 (East), year ~ 2011 -- * Normandy -10 (West), year ~ 1944 @@ -4051,13 +4284,14 @@ 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) -- Map. map=map or UTILS.GetDCSMap() - + local declination=0 if map==DCSMAP.Caucasus then declination=6 @@ -4073,6 +4307,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 @@ -4094,12 +4330,12 @@ function UTILS.FileExists(file) end else return nil - end + end end --- Checks the current memory usage collectgarbage("count"). Info is printed to the DCS log file. Time stamp is the current mission runtime. --- @param #boolean output If true, print to DCS log file. --- @return #number Memory usage in kByte. +-- @param #boolean output If true, print to DCS log file. +-- @return #number Memory usage in kByte. function UTILS.CheckMemory(output) local time=timer.getTime() local clock=UTILS.SecondsToClock(time) @@ -4111,7 +4347,7 @@ function UTILS.CheckMemory(output) end ---- Get the coalition name from its numerical ID, e.g. coaliton.side.RED. +--- Get the coalition name from its numerical ID, e.g. coalition.side.RED. -- @param #number Coalition The coalition ID. -- @return #string The coalition name, i.e. "Neutral", "Red" or "Blue" (or "Unknown"). function UTILS.GetCoalitionName(Coalition) @@ -4129,7 +4365,31 @@ function UTILS.GetCoalitionName(Coalition) else return "Unknown" end - + +end + +--- Get the enemy coalition for a given coalition. +-- @param #number Coalition The coalition ID. +-- @param #boolean Neutral Include neutral as enemy. +-- @return #table Enemy coalition table. +function UTILS.GetCoalitionEnemy(Coalition, Neutral) + + local Coalitions={} + if Coalition then + if Coalition==coalition.side.RED then + Coalitions={coalition.side.BLUE} + elseif Coalition==coalition.side.BLUE then + Coalitions={coalition.side.RED} + elseif Coalition==coalition.side.NEUTRAL then + Coalitions={coalition.side.RED, coalition.side.BLUE} + end + end + + if Neutral then + table.insert(Coalitions, coalition.side.NEUTRAL) + end + + return Coalitions end --- Get the modulation name from its numerical value. @@ -4148,7 +4408,24 @@ function UTILS.GetModulationName(Modulation) else return "Unknown" end - + +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 @@ -4161,7 +4438,7 @@ function UTILS.GetCallsignName(Callsign) return name end end - + for name, value in pairs(CALLSIGN.AWACS) do if value==Callsign then return name @@ -4173,13 +4450,55 @@ function UTILS.GetCallsignName(Callsign) return name end end - + for name, value in pairs(CALLSIGN.Tanker) do if value==Callsign then 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 @@ -4202,7 +4521,9 @@ function UTILS.GMTToLocalTimeDifference() elseif theatre==DCSMAP.Syria then return 3 -- Damascus is UTC+3 hours elseif theatre==DCSMAP.MarianaIslands then - return 10 -- Guam is UTC+10 hours. + return 10 -- Guam is UTC+10 hours. + elseif theatre==DCSMAP.Falklands then + return -3 -- Fireland is UTC-3 hours. else BASE:E(string.format("ERROR: Unknown Map %s in UTILS.GMTToLocal function. Returning 0", tostring(theatre))) return 0 @@ -4219,11 +4540,11 @@ end function UTILS.GetDayOfYear(Year, Month, Day) local floor = math.floor - + local n1 = floor(275 * Month / 9) local n2 = floor((Month + 9) / 12) local n3 = (1 + floor((Year - 4 * floor(Year / 4) + 2) / 3)) - + return n1 - (n2 * n3) + Day - 30 end @@ -4236,14 +4557,14 @@ end -- @return #number Sun rise/set in seconds of the day. function UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, Rising, Tlocal) - -- Defaults + -- Defaults local zenith=90.83 local latitude=Latitude local longitude=Longitude local rising=Rising local n=DayOfYear Tlocal=Tlocal or 0 - + -- Short cuts. local rad = math.rad @@ -4270,47 +4591,47 @@ function UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, Rising, Tlocal) return val end end - + -- Convert the longitude to hour value and calculate an approximate time local lng_hour = longitude / 15 - + local t if rising then -- Rising time is desired t = n + ((6 - lng_hour) / 24) else -- Setting time is desired t = n + ((18 - lng_hour) / 24) end - + -- Calculate the Sun's mean anomaly local M = (0.9856 * t) - 3.289 - + -- Calculate the Sun's true longitude local L = fit_into_range(M + (1.916 * sin(M)) + (0.020 * sin(2 * M)) + 282.634, 0, 360) - + -- Calculate the Sun's right ascension local RA = fit_into_range(atan(0.91764 * tan(L)), 0, 360) - + -- Right ascension value needs to be in the same quadrant as L local Lquadrant = floor(L / 90) * 90 local RAquadrant = floor(RA / 90) * 90 RA = RA + Lquadrant - RAquadrant - + -- Right ascension value needs to be converted into hours RA = RA / 15 - + -- Calculate the Sun's declination local sinDec = 0.39782 * sin(L) local cosDec = cos(asin(sinDec)) - + -- Calculate the Sun's local hour angle local cosH = (cos(zenith) - (sinDec * sin(latitude))) / (cosDec * cos(latitude)) - + if rising and cosH > 1 then return "N/R" -- The sun never rises on this location on the specified date elseif cosH < -1 then return "N/S" -- The sun never sets on this location on the specified date end - + -- Finish calculating H and convert into hours local H if rising then @@ -4319,13 +4640,13 @@ function UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, Rising, Tlocal) H = acos(cosH) end H = H / 15 - + -- Calculate local mean time of rising/setting local T = H + RA - (0.06571 * t) - 6.622 -- Adjust back to UTC local UT = fit_into_range(T - lng_hour +Tlocal, 0, 24) - + return floor(UT)*60*60+frac(UT)*60*60--+Tlocal*60*60 end @@ -4365,15 +4686,21 @@ end -- @return #number Os time in seconds. function UTILS.GetOSTime() if os then - return os.clock() + local ts = 0 + local t = os.date("*t") + local s = t.sec + local m = t.min * 60 + local h = t.hour * 3600 + ts = s+m+h + return ts + else + return nil end - - return nil end --- Shuffle a table accoring to Fisher Yeates algorithm ---@param #table 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") @@ -4391,43 +4718,107 @@ 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() - - if type_name == "Mi-8MT" and unit:getDrawArgumentValue(38) == 1 or unit:getDrawArgumentValue(86) == 1 or unit:getDrawArgumentValue(250) == 1 then - BASE:T(unit_name .. " Cargo doors are open or cargo door not present") - ret_val = true + 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 + BASE:T(unit_name .. " Cargo doors are open or cargo door not present") + 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 + 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") + 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") + return true end - if ret_val == false then - BASE:T(unit_name .. " all doors are closed") + if string.find(type_name, "Hercules") and (unit:getDrawArgumentValue(1217) == 1) then + BASE:T(unit_name .. " side door is open") + return true end - return ret_val - + + 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") + 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 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 + + 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 + + if type_name == "Bronco-OV-10A" then + BASE:T(unit_name .. " front door(s) are open") + return true -- no doors on this one ;) + end + + return false + end -- nil - + return nil end @@ -4453,26 +4844,26 @@ function UTILS.GenerateVHFrequencies() -- known and sorted map-wise NDBs in kHz local _skipFrequencies = { 214,274,291.5,295,297.5, - 300.5,304,307,309.5,311,312,312.5,316, - 320,324,328,329,330,336,337, + 300.5,304,305,307,309.5,311,312,312.5,316, + 320,324,328,329,330,332,336,337, 342,343,348,351,352,353,358, 363,365,368,372.5,374, - 380,381,384,389,395,396, + 380,381,384,385,389,395,396, 414,420,430,432,435,440,450,455,462,470,485, 507,515,520,525,528,540,550,560,570,577,580, 602,625,641,662,670,680,682,690, 705,720,722,730,735,740,745,750,770,795, 822,830,862,866, 905,907,920,935,942,950,995, - 1000,1025,1030,1050,1065,1116,1175,1182,1210 + 1000,1025,1030,1050,1065,1116,1175,1182,1210,1215 } - + local FreeVHFFrequencies = {} - + -- first range local _start = 200000 while _start < 400000 do - + -- skip existing NDB frequencies# local _found = false for _, value in pairs(_skipFrequencies) do @@ -4486,7 +4877,7 @@ function UTILS.GenerateVHFrequencies() end _start = _start + 10000 end - + -- second range _start = 400000 while _start < 850000 do @@ -4503,7 +4894,7 @@ function UTILS.GenerateVHFrequencies() end _start = _start + 10000 end - + -- third range _start = 850000 while _start <= 999000 do -- adjusted for Gazelle @@ -4532,7 +4923,9 @@ function UTILS.GenerateUHFrequencies() local _start = 220000000 while _start < 399000000 do - table.insert(FreeUHFFrequencies, _start) + if _start ~= 243000000 then + table.insert(FreeUHFFrequencies, _start) + end _start = _start + 500000 end @@ -4543,7 +4936,7 @@ end -- @return #table Laser Codes. function UTILS.GenerateLaserCodes() local jtacGeneratedLaserCodes = {} - + -- helper function local function ContainsDigit(_number, _numberToFind) local _thisNumber = _number @@ -4557,14 +4950,14 @@ function UTILS.GenerateLaserCodes() end return false end - + -- generate list of laser codes local _code = 1111 local _count = 1 while _code < 1777 and _count < 30 do while true do _code = _code + 1 - if not self:_ContainsDigit(_code, 8) + if not ContainsDigit(_code, 8) and not ContainsDigit(_code, 9) and not ContainsDigit(_code, 0) then table.insert(jtacGeneratedLaserCodes, _code) @@ -4575,6 +4968,767 @@ function UTILS.GenerateLaserCodes() end return jtacGeneratedLaserCodes end + +--- Ensure the passed object is a table. +-- @param #table Object The object that should be a table. +-- @param #boolean ReturnNil If `true`, return `#nil` if `Object` is nil. Otherwise an empty table `{}` is returned. +-- @return #table The object that now certainly *is* a table. +function UTILS.EnsureTable(Object, ReturnNil) + + if Object then + if type(Object)~="table" then + Object={Object} + end + else + if ReturnNil then + return nil + else + Object={} + end + + end + + return Object +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. 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 obtain a table of typenames from the group given with the number of units of the same type in the group. +-- @param Wrapper.Group#GROUP Group The group to list +-- @return #table Table of typnames and typename counts, e.g. `{["KAMAZ Truck"]=3,["ATZ-5"]=1}` +function UTILS.GetCountPerTypeName(Group) + local units = Group:GetUnits() + local TypeNameTable = {} + for _,_unt in pairs (units) do + local unit = _unt -- Wrapper.Unit#UNIT + local typen = unit:GetTypeName() + if not TypeNameTable[typen] then + TypeNameTable[typen] = 1 + else + TypeNameTable[typen] = TypeNameTable[typen] + 1 + end + end + return TypeNameTable +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. +-- @param #boolean Structured Append the data with a list of typenames in the group plus their count. +-- @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,Structured) + 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() + if Structured then + local structure = UTILS.GetCountPerTypeName(group) + local strucdata = "" + for typen,anzahl in pairs (structure) do + strucdata = strucdata .. typen .. "=="..anzahl..";" + end + data = string.format("%s%s,%d,%d,%d,%d,%s\n",data,_group,units,position.x,position.y,position.z,strucdata) + else + data = string.format("%s%s,%d,%d,%d,%d\n",data,_group,units,position.x,position.y,position.z) + end + 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. +-- @param #boolean Structured Append the data with a list of typenames in the group plus their count. +-- @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,Structured) + 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() + if Structured then + local structure = UTILS.GetCountPerTypeName(group) + local strucdata = "" + for typen,anzahl in pairs (structure) do + strucdata = strucdata .. typen .. "=="..anzahl..";" + end + data = string.format("%s%s,%s,%d,%d,%d,%d,%s\n",data,name,template,units,position.x,position.y,position.z,strucdata) + else + data = string.format("%s%s,%s,%d,%d,%d,%d\n",data,name,template,units,position.x,position.y,position.z) + end + 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. +-- @param #boolean Structured (Optional, needs Reduce = true) If true, and the data has been saved as structure before, remove the correct unit types as per the saved list. +-- @param #boolean Cinematic (Optional, needs Structured = true) If true, place a fire/smoke effect on the dead static position. +-- @param #number Effect (Optional for Cinematic) What effect to use. Defaults to a random effect. Smoke presets are: 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 for Cinematic) What smoke density to use, can be 0 to 1. Defaults to 0.5. +-- @return #table Table of data objects (tables) containing groupname, coordinate and group object. Returns nil when file cannot be read. +-- @return #table When using Cinematic: table of names of smoke and fire objects, so they can be extinguished with `COORDINATE.StopBigSmokeAndFire( name )` +function UTILS.LoadStationaryListOfGroups(Path,Filename,Reduce,Structured,Cinematic,Effect,Density) + + local fires = {} + + local function Smokers(name,coord,effect,density) + local eff = math.random(8) + if type(effect) == "number" then eff = effect end + coord:BigSmokeAndFire(eff,density,name) + table.insert(fires,name) + end + + local function Cruncher(group,typename,anzahl) + local units = group:GetUnits() + local reduced = 0 + for _,_unit in pairs (units) do + local typo = _unit:GetTypeName() + if typename == typo then + if Cinematic then + local coordinate = _unit:GetCoordinate() + local name = _unit:GetName() + Smokers(name,coordinate,Effect,Density) + end + _unit:Destroy(false) + reduced = reduced + 1 + if reduced == anzahl then break end + end + end + end + + 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 structure = dataset[6] + --BASE:I({structure}) + 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 + if Structured and structure then + --BASE:I("Reducing group structure!") + local loadedstructure = {} + local strcset = UTILS.Split(structure,";") + for _,_data in pairs(strcset) do + local datasplit = UTILS.Split(_data,"==") + loadedstructure[datasplit[1]] = tonumber(datasplit[2]) + end + --BASE:I({loadedstructure}) + local originalstructure = UTILS.GetCountPerTypeName(actualgroup) + --BASE:I({originalstructure}) + for _name,_number in pairs(originalstructure) do + local loadednumber = 0 + if loadedstructure[_name] then + loadednumber = loadedstructure[_name] + end + local reduce = false + if loadednumber < _number then reduce = true end + + --BASE:I(string.format("Looking at: %s | Original number: %d | Loaded number: %d | Reduce: %s",_name,_number,loadednumber,tostring(reduce))) + + if reduce then + Cruncher(actualgroup,_name,_number-loadednumber) + end + + end + else + 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 + end + table.insert(datatable,data) + end + else + return nil + end + return datatable,fires +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. +-- @param #boolean Structured (Optional, needs Spawn=true)If true, and the data has been saved as structure before, remove the correct unit types as per the saved list. +-- @param #boolean Cinematic (Optional, needs Structured=true) If true, place a fire/smoke effect on the dead static position. +-- @param #number Effect (Optional for Cinematic) What effect to use. Defaults to a random effect. Smoke presets are: 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 for Cinematic) What smoke density to use, can be 0 to 1. Defaults to 0.5. +-- @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, template=template }` +-- @return #table When using Cinematic: table of names of smoke and fire objects, so they can be extinguished with `COORDINATE.StopBigSmokeAndFire( name )` +function UTILS.LoadSetOfGroups(Path,Filename,Spawn,Structured,Cinematic,Effect,Density) + + local fires = {} + local usedtemplates = {} + local spawn = true + if Spawn == false then spawn = false end + local filename = Filename or "SetOfGroups" + local setdata = SET_GROUP:New() + local datatable = {} + + local function Smokers(name,coord,effect,density) + local eff = math.random(8) + if type(effect) == "number" then eff = effect end + coord:BigSmokeAndFire(eff,density,name) + table.insert(fires,name) + end + + local function Cruncher(group,typename,anzahl) + local units = group:GetUnits() + local reduced = 0 + for _,_unit in pairs (units) do + local typo = _unit:GetTypeName() + if typename == typo then + if Cinematic then + local coordinate = _unit:GetCoordinate() + local name = _unit:GetName() + Smokers(name,coordinate,Effect,Density) + end + _unit:Destroy(false) + reduced = reduced + 1 + if reduced == anzahl then break end + end + end + end + + local function PostSpawn(args) + local spwndgrp = args[1] + local size = args[2] + local structure = args[3] + + setdata:AddObject(spwndgrp) + local actualsize = spwndgrp:CountAliveUnits() + if actualsize > size then + if Structured and structure then + + local loadedstructure = {} + local strcset = UTILS.Split(structure,";") + for _,_data in pairs(strcset) do + local datasplit = UTILS.Split(_data,"==") + loadedstructure[datasplit[1]] = tonumber(datasplit[2]) + end + + local originalstructure = UTILS.GetCountPerTypeName(spwndgrp) + + for _name,_number in pairs(originalstructure) do + local loadednumber = 0 + if loadedstructure[_name] then + loadednumber = loadedstructure[_name] + end + local reduce = false + if loadednumber < _number then reduce = true end + + if reduce then + Cruncher(spwndgrp,_name,_number-loadednumber) + end + + end + else + 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 + end + + local function MultiUse(Data) + local template = Data.template + if template and usedtemplates[template] and usedtemplates[template].used and usedtemplates[template].used > 1 then + -- multispawn + if not usedtemplates[template].done then + local spwnd = 0 + local spawngrp = SPAWN:New(template) + spawngrp:InitLimit(0,usedtemplates[template].used) + for _,_entry in pairs(usedtemplates[template].data) do + spwnd = spwnd + 1 + local sgrp=spawngrp:SpawnFromCoordinate(_entry.coordinate,spwnd) + BASE:ScheduleOnce(0.5,PostSpawn,{sgrp,_entry.size,_entry.structure}) + end + usedtemplates[template].done = true + end + return true + else + return false + end + end + + --BASE:I("Spawn = "..tostring(spawn)) + 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 structure = dataset[7] + local coordinate = COORDINATE:NewFromVec3({x=posx, y=posy, z=posz}) + local group=nil + if size > 0 then + local data = { groupname=groupname, size=size, coordinate=coordinate, template=template, structure=structure } + table.insert(datatable,data) + if usedtemplates[template] then + usedtemplates[template].used = usedtemplates[template].used + 1 + table.insert(usedtemplates[template].data,data) + else + usedtemplates[template] = { + data = {}, + used = 1, + done = false, + } + table.insert(usedtemplates[template].data,data) + end + end + end + for _id,_entry in pairs (datatable) do + if spawn and not MultiUse(_entry) and _entry.size > 0 then + local group = SPAWN:New(_entry.template) + local sgrp=group:SpawnFromCoordinate(_entry.coordinate) + BASE:ScheduleOnce(0.5,PostSpawn,{sgrp,_entry.size,_entry.structure}) + end + end + else + return nil + end + if spawn then + return setdata,fires + 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,",") + local staticname = dataset[1] + local StaticObject = STATIC:FindByName(staticname,false) + if StaticObject then + datatable:AddObject(StaticObject) + end + 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. +-- @param #boolean Dead (Optional, needs Reduce = true) If Dead is true, re-spawn the dead object as dead and do not just delete it. +-- @param #boolean Cinematic (Optional, needs Dead = true) If true, place a fire/smoke effect on the dead static position. +-- @param #number Effect (Optional for Cinematic) What effect to use. Defaults to a random effect. Smoke presets are: 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 for Cinematic) What smoke density to use, can be 0 to 1. Defaults to 0.5. +-- @return #table Table of data objects (tables) containing staticname, size (0=dead else 1), coordinate and the static object. Dead objects will have coordinate points `{x=0,y=0,z=0}` +-- @return #table When using Cinematic: table of names of smoke and fire objects, so they can be extinguished with `COORDINATE.StopBigSmokeAndFire( name )` +-- Returns nil when file cannot be read. +function UTILS.LoadStationaryListOfStatics(Path,Filename,Reduce,Dead,Cinematic,Effect,Density) + local fires = {} + 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 + if Dead then + local deadobject = SPAWNSTATIC:NewFromStatic(staticname,static:GetCountry()) + deadobject:InitDead(true) + local heading = static:GetHeading() + local coord = static:GetCoordinate() + static:Destroy(false) + deadobject:SpawnFromCoordinate(coord,heading,staticname) + if Cinematic then + local effect = math.random(8) + if type(Effect) == "number" then + effect = Effect + end + coord:BigSmokeAndFire(effect,Density,staticname) + table.insert(fires,staticname) + end + else + static:Destroy(false) + end + end + end + end + else + return nil + end + return datatable,fires +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 = ToGrp: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 + +--- Check if an object is contained in a table. +-- @param #table Table The table. +-- @param #table Object The object to check. +-- @param #string Key (Optional) Key to check. By default, the object itself is checked. +-- @return #booolen Returns `true` if object is in table. +function UTILS.IsInTable(Table, Object, Key) + + for key, object in pairs(Table) do + if Key then + if Object[Key]==object[Key] then + return true + end + else + if object==Object then + return true + end + end + end + + return false +end + +--- Check if any object of multiple given objects is contained in a table. +-- @param #table Table The table. +-- @param #table Objects The objects to check. +-- @param #string Key (Optional) Key to check. +-- @return #booolen Returns `true` if object is in table. +function UTILS.IsAnyInTable(Table, Objects, Key) + + for _,Object in pairs(UTILS.EnsureTable(Objects)) do + + for key, object in pairs(Table) do + if Key then + if Object[Key]==object[Key] then + return true + end + else + if object==Object then + return true + end + end + end + + end + + return false +end --- **Utils** - Lua Profiler. -- -- Find out how many times functions are called and how much real time it costs. @@ -4582,10 +5736,9 @@ end -- === -- -- ### Author: **TAW CougarNL**, *funkyfranky* --- --- @module Utilities.PROFILER --- @image MOOSE.JPG - +-- +-- @module Utilities.Profiler +-- @image Utils_Profiler.jpg --- PROFILER class. -- @type PROFILER @@ -4603,67 +5756,79 @@ end -- @field #string fileNamePrefix Output file name prefix, e.g. "MooseProfiler". -- @field #string fileNameSuffix Output file name prefix, e.g. "txt" ---- *The emperor counsels simplicity. First principles. Of each particular thing, ask: What is it in itself, in its own constitution? What is its causal nature? * +--- *The emperor counsels simplicity.* *First principles. Of each particular thing, ask: What is it in itself, in its own constitution? What is its causal nature?* -- -- === -- -- ![Banner Image](..\Presentations\Utilities\PROFILER_Main.jpg) -- -- # The PROFILER Concept --- +-- -- Profile your lua code. This tells you, which functions are called very often and which consume most real time. --- With this information you can optimize the perfomance of your code. --- +-- With this information you can optimize the performance of your code. +-- -- # Prerequisites --- --- The modules **os** and **lfs** need to be desanizied. --- --- +-- +-- The modules **os**, **io** and **lfs** need to be de-sanitized. Comment out the lines +-- +-- --sanitizeModule('os') +-- --sanitizeModule('io') +-- --sanitizeModule('lfs') +-- +-- in your *"DCS World OpenBeta/Scripts/MissionScripting.lua"* file. +-- +-- But be aware that these changes can make you system vulnerable to attacks. +-- +-- # Disclaimer +-- +-- **Profiling itself is CPU expensive!** Don't use this when you want to fly or host a mission. +-- +-- -- # Start --- +-- -- The profiler can simply be started with the @{#PROFILER.Start}(*Delay, Duration*) function --- +-- -- PROFILER.Start() --- +-- -- The optional parameter *Delay* can be used to delay the start by a certain amount of seconds and the optional parameter *Duration* can be used to -- stop the profiler after a certain amount of seconds. --- +-- -- # Stop --- +-- -- The profiler automatically stops when the mission ends. But it can be stopped any time with the @{#PROFILER.Stop}(*Delay*) function --- +-- -- PROFILER.Stop() --- +-- -- The optional parameter *Delay* can be used to specify a delay after which the profiler is stopped. --- +-- -- When the profiler is stopped, the output is written to a file. --- +-- -- # Output --- +-- -- The profiler output is written to a file in your DCS home folder --- +-- -- X:\User\\Saved Games\DCS OpenBeta\Logs --- +-- -- The default file name is "MooseProfiler.txt". If that file exists, the file name is "MooseProfiler-001.txt" etc. --- +-- -- ## Data --- +-- -- The data in the output file provides information on the functions that were called in the mission. --- +-- -- It will tell you how many times a function was called in total, how many times per second, how much time in total and the percentage of time. --- +-- -- If you only want output for functions that are called more than *X* times per second, you can set --- +-- -- PROFILER.ThreshCPS=1.5 --- +-- -- With this setting, only functions which are called more than 1.5 times per second are displayed. The default setting is PROFILER.ThreshCPS=0.0 (no threshold). --- +-- -- Furthermore, you can limit the output for functions that consumed a certain amount of CPU time in total by --- +-- -- PROFILER.ThreshTtot=0.005 --- +-- -- With this setting, which is also the default, only functions which in total used more than 5 milliseconds CPU time. --- +-- -- @field #PROFILER PROFILER = { ClassName = "PROFILER", @@ -4694,97 +5859,105 @@ PROFILER = { --- Start profiler. -- @param #number Delay Delay in seconds before profiler is stated. Default is immediately. -- @param #number Duration Duration in (game) seconds before the profiler is stopped. Default is when mission ends. -function PROFILER.Start(Delay, Duration) +function PROFILER.Start( Delay, Duration ) -- Check if os, io and lfs are available. - local go=true + local go = true if not os then - env.error("ERROR: Profiler needs os to be desanitized!") - go=false + env.error( "ERROR: Profiler needs os to be de-sanitized!" ) + go = false end if not io then env.error("ERROR: Profiler needs io to be desanitized!") go=false - end + end if not lfs then env.error("ERROR: Profiler needs lfs to be desanitized!") go=false - end + end if not go then return end - if Delay and Delay>0 then - BASE:ScheduleOnce(Delay, PROFILER.Start, 0, Duration) + if Delay and Delay > 0 then + BASE:ScheduleOnce( Delay, PROFILER.Start, 0, Duration ) else - + -- Set start time. PROFILER.TstartGame=timer.getTime() PROFILER.TstartOS=os.clock() - + -- Add event handler. world.addEventHandler(PROFILER.eventHandler) - + -- Info in log. - env.info('############################ Profiler Started ############################') + env.info( '############################ Profiler Started ############################' ) if Duration then - env.info(string.format("- Will be running for %d seconds", Duration)) + env.info( string.format( "- Will be running for %d seconds", Duration ) ) else - env.info(string.format("- Will be stopped when mission ends")) + env.info( string.format( "- Will be stopped when mission ends" ) ) end env.info(string.format("- Calls per second threshold %.3f/sec", PROFILER.ThreshCPS)) env.info(string.format("- Total function time threshold %.3f sec", PROFILER.ThreshTtot)) env.info(string.format("- Output file \"%s\" in your DCS log file folder", PROFILER.getfilename(PROFILER.fileNameSuffix))) env.info(string.format("- Output file \"%s\" in CSV format", PROFILER.getfilename("csv"))) env.info('###############################################################################') - - + + -- Message on screen local duration=Duration or 600 trigger.action.outText("### Profiler running ###", duration) - + -- Set hook. debug.sethook(PROFILER.hook, "cr") - + -- Auto stop profiler. if Duration then - PROFILER.Stop(Duration) + PROFILER.Stop( Duration ) end - + end - + end --- Stop profiler. -- @param #number Delay Delay before stop in seconds. +function PROFILER.Stop( Delay ) + + if Delay and Delay > 0 then + + BASE:ScheduleOnce( Delay, PROFILER.Stop ) + end +end + function PROFILER.Stop(Delay) if Delay and Delay>0 then - + BASE:ScheduleOnce(Delay, PROFILER.Stop) - + else -- Remove hook. debug.sethook() - - + + -- Run time game. local runTimeGame=timer.getTime()-PROFILER.TstartGame - + -- Run time real OS. local runTimeOS=os.clock()-PROFILER.TstartOS - + -- Show info. PROFILER.showInfo(runTimeGame, runTimeOS) - + end end --- Event handler. -function PROFILER.eventHandler:onEvent(event) - if event.id==world.event.S_EVENT_MISSION_END then +function PROFILER.eventHandler:onEvent( event ) + if event.id == world.event.S_EVENT_MISSION_END then PROFILER.Stop() end end @@ -4798,35 +5971,35 @@ end function PROFILER.hook(event) local f=debug.getinfo(2, "f").func - + if event=='call' then - + if PROFILER.Counters[f]==nil then - + PROFILER.Counters[f]=1 PROFILER.dInfo[f]=debug.getinfo(2,"Sn") - + if PROFILER.fTimeTotal[f]==nil then PROFILER.fTimeTotal[f]=0 end - + else - PROFILER.Counters[f]=PROFILER.Counters[f]+1 + PROFILER.Counters[f] = PROFILER.Counters[f] + 1 end - + if PROFILER.fTime[f]==nil then PROFILER.fTime[f]=os.clock() end - + elseif (event=='return') then - + if PROFILER.fTime[f]~=nil then PROFILER.fTimeTotal[f]=PROFILER.fTimeTotal[f]+(os.clock()-PROFILER.fTime[f]) PROFILER.fTime[f]=nil end - + end - + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -4839,105 +6012,104 @@ end -- @return #string Source file name. -- @return #string Line number. -- @return #number Function time in seconds. -function PROFILER.getData(func) +function PROFILER.getData( func ) local n=PROFILER.dInfo[func] - + if n.what=="C" then return n.name, "?", "?", PROFILER.fTimeTotal[func] end - - return n.name, n.short_src, n.linedefined, PROFILER.fTimeTotal[func] + + return n.name, n.short_src, n.linedefined, PROFILER.fTimeTotal[func] end --- Write text to log file. -- @param #function f The file. -- @param #string txt The text. -function PROFILER._flog(f, txt) - f:write(txt.."\r\n") +function PROFILER._flog( f, txt ) + f:write( txt .. "\r\n" ) end --- Show table. -- @param #table data Data table. -- @param #function f The file. -- @param #number runTimeGame Game run time in seconds. -function PROFILER.showTable(data, f, runTimeGame) +function PROFILER.showTable( data, f, runTimeGame ) -- Loop over data. - for i=1, #data do + for i=1, #data do local t=data[i] --#PROFILER.Data - + -- Calls per second. local cps=t.count/runTimeGame - + local threshCPS=cps>=PROFILER.ThreshCPS local threshTot=t.tm>=PROFILER.ThreshTtot - + if threshCPS and threshTot then - + -- Output local text=string.format("%30s: %8d calls %8.1f/sec - Time Total %8.3f sec (%.3f %%) %5.3f sec/call %s line %s", t.func, t.count, cps, t.tm, t.tm/runTimeGame*100, t.tm/t.count, tostring(t.src), tostring(t.line)) PROFILER._flog(f, text) - + end end - + end --- Print csv file. -- @param #table data Data table. -- @param #number runTimeGame Game run time in seconds. -function PROFILER.printCSV(data, runTimeGame) +function PROFILER.printCSV( data, runTimeGame ) -- Output file. - local file=PROFILER.getfilename("csv") - local g=io.open(file, 'w') + local file = PROFILER.getfilename( "csv" ) + local g = io.open( file, 'w' ) -- Header. local text="Function,Total Calls,Calls per Sec,Total Time,Total in %,Sec per Call,Source File;Line Number," g:write(text.."\r\n") - + -- Loop over data. - for i=1, #data do + for i=1, #data do local t=data[i] --#PROFILER.Data - + -- Calls per second. - local cps=t.count/runTimeGame + local cps = t.count / runTimeGame -- Output local txt=string.format("%s,%d,%.1f,%.3f,%.3f,%.3f,%s,%s,", t.func, t.count, cps, t.tm, t.tm/runTimeGame*100, t.tm/t.count, tostring(t.src), tostring(t.line)) g:write(txt.."\r\n") - + end - + -- Close file. g:close() end - --- Write info to output file. -- @param #string ext Extension. -- @return #string File name. function PROFILER.getfilename(ext) local dir=lfs.writedir()..[[Logs\]] - + ext=ext or PROFILER.fileNameSuffix - + local file=dir..PROFILER.fileNamePrefix.."."..ext - + if not UTILS.FileExists(file) then return file end - - for i=1,999 do - - local file=string.format("%s%s-%03d.%s", dir,PROFILER.fileNamePrefix, i, ext) - if not UTILS.FileExists(file) then + for i = 1, 999 do + + local file = string.format( "%s%s-%03d.%s", dir, PROFILER.fileNamePrefix, i, ext ) + + if not UTILS.FileExists( file ) then return file end - + end end @@ -4945,34 +6117,34 @@ end --- Write info to output file. -- @param #number runTimeGame Game time in seconds. -- @param #number runTimeOS OS time in seconds. -function PROFILER.showInfo(runTimeGame, runTimeOS) +function PROFILER.showInfo( runTimeGame, runTimeOS ) -- Output file. local file=PROFILER.getfilename(PROFILER.fileNameSuffix) - local f=io.open(file, 'w') - + local f=io.open(file, 'w') + -- Gather data. local Ttot=0 local Calls=0 - + local t={} - + local tcopy=nil --#PROFILER.Data local tserialize=nil --#PROFILER.Data local tforgen=nil --#PROFILER.Data local tpairs=nil --#PROFILER.Data - - + + for func, count in pairs(PROFILER.Counters) do - + local s,src,line,tm=PROFILER.getData(func) - + if PROFILER.logUnknown==true then if s==nil then s="" end end - + if s~=nil then - + -- Profile data. local T= { func=s, @@ -4981,64 +6153,64 @@ function PROFILER.showInfo(runTimeGame, runTimeOS) count=count, tm=tm, } --#PROFILER.Data - + -- Collect special cases. Somehow, e.g. "_copy" appears multiple times so we try to gather all data. - if s=="_copy" then - if tcopy==nil then - tcopy=T + if s == "_copy" then + if tcopy == nil then + tcopy = T else - tcopy.count=tcopy.count+T.count - tcopy.tm=tcopy.tm+T.tm + tcopy.count = tcopy.count + T.count + tcopy.tm = tcopy.tm + T.tm end - elseif s=="_Serialize" then - if tserialize==nil then - tserialize=T + elseif s == "_Serialize" then + if tserialize == nil then + tserialize = T else tserialize.count=tserialize.count+T.count tserialize.tm=tserialize.tm+T.tm - end + end elseif s=="(for generator)" then if tforgen==nil then tforgen=T else tforgen.count=tforgen.count+T.count tforgen.tm=tforgen.tm+T.tm - end + end elseif s=="pairs" then if tpairs==nil then tpairs=T else tpairs.count=tpairs.count+T.count tpairs.tm=tpairs.tm+T.tm - end + end else - table.insert(t, T) + table.insert( t, T ) end - + -- Total function time. Ttot=Ttot+tm - + -- Total number of calls. Calls=Calls+count - + end - + end - -- Add special cases. + -- Add special cases. if tcopy then - table.insert(t, tcopy) + table.insert( t, tcopy ) end if tserialize then - table.insert(t, tserialize) + table.insert(t, tserialize) end if tforgen then - table.insert(t, tforgen) + table.insert( t, tforgen ) end if tpairs then table.insert(t, tpairs) - end - + end + env.info('############################ Profiler Stopped ############################') env.info(string.format("* Runtime Game : %s = %d sec", UTILS.SecondsToClock(runTimeGame, true), runTimeGame)) env.info(string.format("* Runtime Real : %s = %d sec", UTILS.SecondsToClock(runTimeOS, true), runTimeOS)) @@ -5047,11 +6219,11 @@ function PROFILER.showInfo(runTimeGame, runTimeOS) env.info(string.format("* Total func calls : %d", Calls)) env.info(string.format("* Writing to file : \"%s\"", file)) env.info(string.format("* Writing to file : \"%s\"", PROFILER.getfilename("csv"))) - env.info("##############################################################################") - + env.info("##############################################################################") + -- Sort by total time. table.sort(t, function(a,b) return a.tm>b.tm end) - + -- Write data. PROFILER._flog(f,"") PROFILER._flog(f,"************************************************************************************************************************") @@ -5070,7 +6242,7 @@ function PROFILER.showInfo(runTimeGame, runTimeOS) PROFILER._flog(f,string.format("* Total func calls = %d", Calls)) PROFILER._flog(f,"") PROFILER._flog(f,string.format("* Calls per second threshold = %.3f/sec", PROFILER.ThreshCPS)) - PROFILER._flog(f,string.format("* Total func time threshold = %.3f sec", PROFILER.ThreshTtot)) + PROFILER._flog(f,string.format("* Total func time threshold = %.3f sec", PROFILER.ThreshTtot)) PROFILER._flog(f,"") PROFILER._flog(f,"************************************************************************************************************************") PROFILER._flog(f,"") @@ -5078,7 +6250,7 @@ function PROFILER.showInfo(runTimeGame, runTimeOS) -- Sort by number of calls. table.sort(t, function(a,b) return a.tm/a.count>b.tm/b.count end) - + -- Detailed data. PROFILER._flog(f,"") PROFILER._flog(f,"************************************************************************************************************************") @@ -5088,10 +6260,10 @@ function PROFILER.showInfo(runTimeGame, runTimeOS) PROFILER._flog(f,"--------------------------------------") PROFILER._flog(f,"") PROFILER.showTable(t, f, runTimeGame) - + -- Sort by number of calls. table.sort(t, function(a,b) return a.count>b.count end) - + -- Detailed data. PROFILER._flog(f,"") PROFILER._flog(f,"************************************************************************************************************************") @@ -5101,20 +6273,19 @@ function PROFILER.showInfo(runTimeGame, runTimeOS) PROFILER._flog(f,"------------------------------------") PROFILER._flog(f,"") PROFILER.showTable(t, f, runTimeGame) - + -- Closing. - PROFILER._flog(f,"") - PROFILER._flog(f,"************************************************************************************************************************") - PROFILER._flog(f,"************************************************************************************************************************") - PROFILER._flog(f,"************************************************************************************************************************") + PROFILER._flog( f, "" ) + PROFILER._flog( f, "************************************************************************************************************************" ) + PROFILER._flog( f, "************************************************************************************************************************" ) + PROFILER._flog( f, "************************************************************************************************************************" ) -- Close file. f:close() - + -- Print csv file. - PROFILER.printCSV(t, runTimeGame) + PROFILER.printCSV( t, runTimeGame ) end - ---- **Utils** Templates +--- **Utilities** - Templates. -- -- DCS unit templates -- @@ -5726,11 +6897,10 @@ TEMPLATE.GenericAircraft= ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- **Utilities** DCS Simple Text-To-Speech (STTS). --- --- --- --- @module Utils.STTS +--- **Utilities** - DCS Simple Text-To-Speech (STTS). +-- +-- +-- @module Utilities.STTS -- @image MOOSE.JPG --- [DCS Enum world](https://wiki.hoggitworld.com/view/DCS_enum_world) @@ -5738,29 +6908,29 @@ TEMPLATE.GenericAircraft= -- @field #string DIRECTORY Path of the SRS directory. --- Simple Text-To-Speech --- +-- -- Version 0.4 - Compatible with SRS version 1.9.6.0+ --- +-- -- # DCS Modification Required --- --- You will need to edit MissionScripting.lua in DCS World/Scripts/MissionScripting.lua and remove the sanitisation. +-- +-- You will need to edit MissionScripting.lua in DCS World/Scripts/MissionScripting.lua and remove the sanitization. -- To do this remove all the code below the comment - the line starts "local function sanitizeModule(name)" -- Do this without DCS running to allow mission scripts to use os functions. --- +-- -- *You WILL HAVE TO REAPPLY AFTER EVERY DCS UPDATE* --- +-- -- # USAGE: --- --- Add this script into the mission as a DO SCRIPT or DO SCRIPT FROM FILE to initialise it +-- +-- Add this script into the mission as a DO SCRIPT or DO SCRIPT FROM FILE to initialize it -- Make sure to edit the STTS.SRS_PORT and STTS.DIRECTORY to the correct values before adding to the mission. -- Then its as simple as calling the correct function in LUA as a DO SCRIPT or in your own scripts. --- +-- -- Example calls: -- -- STTS.TextToSpeech("Hello DCS WORLD","251","AM","1.0","SRS",2) --- +-- -- Arguments in order are: --- +-- -- * Message to say, make sure not to use a newline (\n) ! -- * Frequency in MHz -- * Modulation - AM/FM @@ -5771,44 +6941,44 @@ TEMPLATE.GenericAircraft= -- * OPTIONAL - Speed -10 to +10 -- * OPTIONAL - Gender male, female or neuter -- * OPTIONAL - Culture - en-US, en-GB etc --- * OPTIONAL - Voice - a specfic voice by name. Run DCS-SR-ExternalAudio.exe with --help to get the ones you can use on the command line +-- * OPTIONAL - Voice - a specific voice by name. Run DCS-SR-ExternalAudio.exe with --help to get the ones you can use on the command line -- * OPTIONAL - Google TTS - Switch to Google Text To Speech - Requires STTS.GOOGLE_CREDENTIALS path and Google project setup correctly -- -- -- ## Example -- -- This example will say the words "Hello DCS WORLD" on 251 MHz AM at maximum volume with a client called SRS and to the Blue coalition only --- +-- -- STTS.TextToSpeech("Hello DCS WORLD","251","AM","1.0","SRS",2,null,-5,"male","en-GB") --- +-- -- ## Example --- ---This example will say the words "Hello DCS WORLD" on 251 MHz AM at maximum volume with a client called SRS and to the Blue coalition only centered on the position of the Unit called "A UNIT" +-- +-- This example will say the words "Hello DCS WORLD" on 251 MHz AM at maximum volume with a client called SRS and to the Blue coalition only centered on the position of the Unit called "A UNIT" -- -- STTS.TextToSpeech("Hello DCS WORLD","251","AM","1.0","SRS",2,Unit.getByName("A UNIT"):getPoint(),-5,"male","en-GB") --- +-- -- Arguments in order are: --- +-- -- * FULL path to the MP3 OR OGG to play -- * Frequency in MHz - to use multiple separate with a comma - Number of frequencies MUST match number of Modulations -- * Modulation - AM/FM - to use multiple -- * Volume - 1.0 max, 0.5 half -- * Name of the transmitter - ATC, RockFM etc -- * Coalition - 0 spectator, 1 red 2 blue --- +-- -- ## Example --- +-- -- This will play that MP3 on 255MHz AM & 31 FM at half volume with a client called "Multiple" and to Spectators only --- +-- -- STTS.PlayMP3("C:\\Users\\Ciaran\\Downloads\\PR-Music.mp3","255,31","AM,FM","0.5","Multiple",0) --- +-- -- @field #STTS -STTS={ - ClassName="STTS", - DIRECTORY="", - SRS_PORT=5002, - GOOGLE_CREDENTIALS="C:\\Users\\Ciaran\\Downloads\\googletts.json", - EXECUTABLE="DCS-SR-ExternalAudio.exe", +STTS = { + ClassName = "STTS", + DIRECTORY = "", + SRS_PORT = 5002, + GOOGLE_CREDENTIALS = "C:\\Users\\Ciaran\\Downloads\\googletts.json", + EXECUTABLE = "DCS-SR-ExternalAudio.exe" } --- FULL Path to the FOLDER containing DCS-SR-ExternalAudio.exe - EDIT TO CORRECT FOLDER @@ -5820,142 +6990,147 @@ STTS.SRS_PORT = 5002 --- Google credentials file STTS.GOOGLE_CREDENTIALS = "C:\\Users\\Ciaran\\Downloads\\googletts.json" ---- DONT CHANGE THIS UNLESS YOU KNOW WHAT YOU'RE DOING +--- DON'T CHANGE THIS UNLESS YOU KNOW WHAT YOU'RE DOING STTS.EXECUTABLE = "DCS-SR-ExternalAudio.exe" - --- Function for UUID. function STTS.uuid() local random = math.random - local template ='yxxx-xxxxxxxxxxxx' - return string.gsub(template, '[xy]', function (c) - local v = (c == 'x') and random(0, 0xf) or random(8, 0xb) - return string.format('%x', v) - end) + local template = 'yxxx-xxxxxxxxxxxx' + return string.gsub( template, '[xy]', function( c ) + local v = (c == 'x') and random( 0, 0xf ) or random( 8, 0xb ) + return string.format( '%x', v ) + end ) end --- Round a number. -- @param #number x Number. -- @param #number n Precision. -function STTS.round(x, n) - n = math.pow(10, n or 0) +function STTS.round( x, n ) + n = math.pow( 10, n or 0 ) x = x * n - if x >= 0 then x = math.floor(x + 0.5) else x = math.ceil(x - 0.5) end + if x >= 0 then + x = math.floor( x + 0.5 ) + else + x = math.ceil( x - 0.5 ) + end return x / n end --- Function returns estimated speech time in seconds. --- Assumptions for time calc: 100 Words per min, avarage of 5 letters for english word so --- +-- Assumptions for time calc: 100 Words per min, average of 5 letters for english word so +-- -- * 5 chars * 100wpm = 500 characters per min = 8.3 chars per second --- --- So lengh of msg / 8.3 = number of seconds needed to read it. rounded down to 8 chars per sec map function: --- +-- +-- So length of msg / 8.3 = number of seconds needed to read it. rounded down to 8 chars per sec map function: +-- -- * (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 + local maxRateRatio = 3 speed = speed or 1.0 isGoogle = isGoogle or false local speedFactor = 1.0 if isGoogle then - speedFactor = speed + speedFactor = speed else - if speed ~= 0 then - speedFactor = math.abs(speed) * (maxRateRatio - 1) / 10 + 1 - end - if speed < 0 then - speedFactor = 1/speedFactor - end + if speed ~= 0 then + speedFactor = math.abs( speed ) * (maxRateRatio - 1) / 10 + 1 + end + if speed < 0 then + speedFactor = 1 / speedFactor + end end - local wpm = math.ceil(100 * speedFactor) - local cps = math.floor((wpm * 5)/60) + local wpm = math.ceil( 100 * speedFactor ) + local cps = math.floor( (wpm * 5) / 60 ) - if type(length) == "string" then - length = string.len(length) + if type( length ) == "string" then + length = string.len( length ) end - return math.ceil(length/cps) + return length/cps --math.ceil(length/cps) end --- Text to speech function. -function STTS.TextToSpeech(message, freqs, modulations, volume, name, coalition, point, speed, gender, culture, voice, googleTTS) - if os == nil or io == nil then - env.info("[DCS-STTS] LUA modules os or io are sanitized. skipping. ") - return - end +function STTS.TextToSpeech( message, freqs, modulations, volume, name, coalition, point, speed, gender, culture, voice, googleTTS ) + if os == nil or io == nil then + env.info( "[DCS-STTS] LUA modules os or io are sanitized. skipping. " ) + return + end - speed = speed or 1 - gender = gender or "female" - culture = culture or "" - voice = voice or "" - coalition=coalition or "0" - name=name or "ROBOT" - volume=1 - speed=1 + speed = speed or 1 + gender = gender or "female" + culture = culture or "" + voice = voice or "" + coalition = coalition or "0" + name = name or "ROBOT" + volume = 1 + speed = 1 + message = message:gsub( "\"", "\\\"" ) + + local cmd = string.format( "start /min \"\" /d \"%s\" /b \"%s\" -f %s -m %s -c %s -p %s -n \"%s\" -h", STTS.DIRECTORY, STTS.EXECUTABLE, freqs or "305", modulations or "AM", coalition, STTS.SRS_PORT, name ) - message = message:gsub("\"","\\\"") - - local cmd = string.format("start /min \"\" /d \"%s\" /b \"%s\" -f %s -m %s -c %s -p %s -n \"%s\" -h", STTS.DIRECTORY, STTS.EXECUTABLE, freqs or "305", modulations or "AM", coalition, STTS.SRS_PORT, name) - if voice ~= "" then - cmd = cmd .. string.format(" -V \"%s\"",voice) + cmd = cmd .. string.format( " -V \"%s\"", voice ) else - if culture ~= "" then - cmd = cmd .. string.format(" -l %s",culture) - end + if culture ~= "" then + cmd = cmd .. string.format( " -l %s", culture ) + end - if gender ~= "" then - cmd = cmd .. string.format(" -g %s",gender) - end + if gender ~= "" then + cmd = cmd .. string.format( " -g %s", gender ) + end end if googleTTS == true then - cmd = cmd .. string.format(" -G \"%s\"",STTS.GOOGLE_CREDENTIALS) + cmd = cmd .. string.format( " -G \"%s\"", STTS.GOOGLE_CREDENTIALS ) end if speed ~= 1 then - cmd = cmd .. string.format(" -s %s",speed) + cmd = cmd .. string.format( " -s %s", speed ) end if volume ~= 1.0 then - cmd = cmd .. string.format(" -v %s",volume) + cmd = cmd .. string.format( " -v %s", volume ) end - if point and type(point) == "table" and point.x then - local lat, lon, alt = coord.LOtoLL(point) + if point and type( point ) == "table" and point.x then + local lat, lon, alt = coord.LOtoLL( point ) - lat = STTS.round(lat,4) - lon = STTS.round(lon,4) - alt = math.floor(alt) + lat = STTS.round( lat, 4 ) + lon = STTS.round( lon, 4 ) + alt = math.floor( alt ) - cmd = cmd .. string.format(" -L %s -O %s -A %s",lat,lon,alt) + cmd = cmd .. string.format( " -L %s -O %s -A %s", lat, lon, alt ) end - cmd = cmd ..string.format(" -t \"%s\"",message) + cmd = cmd .. string.format( " -t \"%s\"", message ) - if string.len(cmd) > 255 then - local filename = os.getenv('TMP') .. "\\DCS_STTS-" .. STTS.uuid() .. ".bat" - local script = io.open(filename,"w+") - script:write(cmd .. " && exit" ) - script:close() - cmd = string.format("\"%s\"",filename) - timer.scheduleFunction(os.remove, filename, timer.getTime() + 1) + if string.len( cmd ) > 255 then + local filename = os.getenv( 'TMP' ) .. "\\DCS_STTS-" .. STTS.uuid() .. ".bat" + local script = io.open( filename, "w+" ) + script:write( cmd .. " && exit" ) + script:close() + cmd = string.format( "\"%s\"", filename ) + timer.scheduleFunction( os.remove, filename, timer.getTime() + 1 ) end - if string.len(cmd) > 255 then - env.info("[DCS-STTS] - cmd string too long") - env.info("[DCS-STTS] TextToSpeech Command :\n" .. cmd.."\n") + if string.len( cmd ) > 255 then + env.info( "[DCS-STTS] - cmd string too long" ) + env.info( "[DCS-STTS] TextToSpeech Command :\n" .. cmd .. "\n" ) end - os.execute(cmd) + os.execute( cmd ) - return STTS.getSpeechTime(message,speed,googleTTS) + return STTS.getSpeechTime( message, speed, googleTTS ) end --- Play mp3 function. @@ -5963,49 +7138,977 @@ end -- @param #string freqs Frequencies, e.g. "305, 256". -- @param #string modulations Modulations, e.g. "AM, FM". -- @param #string volume Volume, e.g. "0.5". -function STTS.PlayMP3(pathToMP3, freqs, modulations, volume, name, coalition, point) +function STTS.PlayMP3( pathToMP3, freqs, modulations, volume, name, coalition, point ) + + local cmd = string.format( "start \"\" /d \"%s\" /b /min \"%s\" -i \"%s\" -f %s -m %s -c %s -p %s -n \"%s\" -v %s -h", STTS.DIRECTORY, STTS.EXECUTABLE, pathToMP3, freqs or "305", modulations or "AM", coalition or "0", STTS.SRS_PORT, name or "ROBOT", volume or "1" ) + + if point and type( point ) == "table" and point.x then + local lat, lon, alt = coord.LOtoLL( point ) + + lat = STTS.round( lat, 4 ) + lon = STTS.round( lon, 4 ) + alt = math.floor( alt ) + + cmd = cmd .. string.format( " -L %s -O %s -A %s", lat, lon, alt ) + end + + env.info( "[DCS-STTS] MP3/OGG Command :\n" .. cmd .. "\n" ) + os.execute( cmd ) + +end +--- **UTILS** - Classic FiFo 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 Utilities.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 - local cmd = string.format("start \"\" /d \"%s\" /b /min \"%s\" -i \"%s\" -f %s -m %s -c %s -p %s -n \"%s\" -v %s -h", - STTS.DIRECTORY, STTS.EXECUTABLE, pathToMP3, freqs or "305", modulations or "AM", coalition or "0", STTS.SRS_PORT, name or "ROBOT", volume or "1") +--- 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 - if point and type(point) == "table" and point.x then - local lat, lon, alt = coord.LOtoLL(point) + 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() - lat = STTS.round(lat,4) - lon = STTS.round(lon,4) - alt = math.floor(alt) + local status, res = co() + self:T( { status, res } ) - cmd = cmd .. string.format(" -L %s -O %s -A %s",lat,lon,alt) + if status == false then + error( res ) end + if res == false then + return true -- resume next time the loop + end + + return false + end - env.info("[DCS-STTS] MP3/OGG Command :\n" .. cmd.."\n") - os.execute(cmd) + Schedule() -end--- **Core** - The base class within the framework. --- + 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--- **Utilities** - Socket. +-- +-- **Main Features:** +-- +-- * Creates UDP Sockets +-- * Send messages to Discord +-- * Compatible with [FunkMan](https://github.com/funkyfranky/FunkMan) +-- * Compatible with [DCSServerBot](https://github.com/Special-K-s-Flightsim-Bots/DCSServerBot) +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Utilities.Socket +-- @image MOOSE.JPG + + +--- 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 + +--- **At times I feel like a socket that remembers its tooth.** -- Saul Bellow +-- -- === +-- +-- # The SOCKET Concept -- --- ## Features: +-- Create a UDP socket server. It enables you to send messages to discord servers via discord bots. -- +-- **Note** that you have to **de-sanitize** `require` and `package` in your `MissionScripting.lua` file, which is in your `DCS/Scripts` folder. +-- +-- +-- @field #SOCKET +SOCKET = { + ClassName = "SOCKET", + verbose = 0, + lid = nil, +} + +--- Data type. This is the keyword the socket listener uses. +-- @field #string TEXT Plain text. +-- @field #string BOMBRESULT Range bombing. +-- @field #string STRAFERESULT Range strafeing result. +-- @field #string LSOGRADE Airboss LSO grade. +SOCKET.DataType={ + TEXT="moose_text", + BOMBRESULT="moose_bomb_result", + STRAFERESULT="moose_strafe_result", + LSOGRADE="moose_lso_grade", +} + + +--- SOCKET class version. +-- @field #string version +SOCKET.version="0.2.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- 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 `10042`. +-- @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 10042 + 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 + +--- Set port. +-- @param #SOCKET self +-- @param #number Port Port. Default 10042. +-- @return #SOCKET self +function SOCKET:SetPort(Port) + self.port=Port or 10042 +end + +--- Set host. +-- @param #SOCKET self +-- @param #string Host Host. Default `"127.0.0.1"`. +-- @return #SOCKET self +function SOCKET:SetHost(Host) + self.host=Host or "127.0.0.1" +end + + +--- Send a table. +-- @param #SOCKET self +-- @param #table Table Table to send. +-- @return #SOCKET self +function SOCKET:SendTable(Table) + + -- Add server name for DCS + Table.server_name=BASE.ServerName or "Unknown" + + -- Encode json table. + local json= self.json:encode(Table) + + -- Debug info. + self:T("Json table:") + self:T(json) + + -- Send data. + self.socket.try(self.UDPSendSocket:sendto(json, self.host, self.port)) + + return self +end + +--- Send a text message. +-- @param #SOCKET self +-- @param #string Text Test message. +-- @return #SOCKET self +function SOCKET:SendText(Text) + + local message={} + + message.command = SOCKET.DataType.TEXT + message.text = Text + + self:SendTable(message) + + return self +end + + +--- **Core** - The base class within the framework. +-- +-- === +-- +-- ## Features: +-- -- * The construction and inheritance of MOOSE classes. -- * The class naming and numbering system. -- * The class hierarchy search system. --- * The tracing of information or objects during mission execution for debuggin purposes. +-- * The tracing of information or objects during mission execution for debugging purposes. -- * The subscription to DCS events for event handling in MOOSE objects. -- * Object inspection. --- +-- -- === --- +-- -- All classes within the MOOSE framework are derived from the BASE class. -- Note: The BASE class is an abstract class and is not meant to be used directly. --- +-- -- === --- +-- -- ### Author: **FlightControl** --- ### Contributions: --- +-- ### Contributions: +-- -- === --- +-- -- @module Core.Base -- @image Core_Base.JPG @@ -6025,107 +8128,107 @@ local _ClassID = 0 --- BASE class -- -- # 1. BASE constructor. --- --- Any class derived from BASE, will use the @{Core.Base#BASE.New} constructor embedded in the @{Core.Base#BASE.Inherit} method. +-- +-- Any class derived from BASE, will use the @{Core.Base#BASE.New} constructor embedded in the @{Core.Base#BASE.Inherit} method. -- See an example at the @{Core.Base#BASE.New} method how this is done. --- +-- -- # 2. Trace information for debugging. --- +-- -- The BASE class contains trace methods to trace progress within a mission execution of a certain object. --- These trace methods are inherited by each MOOSE class interiting BASE, soeach object created from derived class from BASE can use the tracing methods to trace its execution. --- +-- These trace methods are inherited by each MOOSE class inheriting BASE, thus all objects created from +-- a class derived from BASE can use the tracing methods to trace its execution. +-- -- Any type of information can be passed to these tracing methods. See the following examples: --- +-- -- self:E( "Hello" ) --- +-- -- Result in the word "Hello" in the dcs.log. --- +-- -- local Array = { 1, nil, "h", { "a","b" }, "x" } -- self:E( Array ) --- --- Results with the text [1]=1,[3]="h",[4]={[1]="a",[2]="b"},[5]="x"} in the dcs.log. --- +-- +-- Results with the text [1]=1,[3]="h",[4]={[1]="a",[2]="b"},[5]="x"} in the dcs.log. +-- -- local Object1 = "Object1" -- local Object2 = 3 -- local Object3 = { Object 1, Object 2 } -- self:E( { Object1, Object2, Object3 } ) --- +-- -- Results with the text [1]={[1]="Object",[2]=3,[3]={[1]="Object",[2]=3}} in the dcs.log. --- +-- -- local SpawnObject = SPAWN:New( "Plane" ) -- local GroupObject = GROUP:FindByName( "Group" ) -- self:E( { Spawn = SpawnObject, Group = GroupObject } ) --- --- Results with the text [1]={Spawn={....),Group={...}} in the dcs.log. --- +-- +-- Results with the text [1]={Spawn={....),Group={...}} in the dcs.log. +-- -- Below a more detailed explanation of the different method types for tracing. --- +-- -- ## 2.1. Tracing methods categories. -- -- There are basically 3 types of tracing methods available: --- +-- -- * @{#BASE.F}: Used to trace the entrance of a function and its given parameters. An F is indicated at column 44 in the DCS.log file. -- * @{#BASE.T}: Used to trace further logic within a function giving optional variables or parameters. A T is indicated at column 44 in the DCS.log file. -- * @{#BASE.E}: Used to always trace information giving optional variables or parameters. An E is indicated at column 44 in the DCS.log file. --- +-- -- ## 2.2 Tracing levels. -- --- There are 3 tracing levels within MOOSE. +-- There are 3 tracing levels within MOOSE. -- These tracing levels were defined to avoid bulks of tracing to be generated by lots of objects. --- +-- -- As such, the F and T methods have additional variants to trace level 2 and 3 respectively: -- -- * @{#BASE.F2}: Trace the beginning of a function and its given parameters with tracing level 2. -- * @{#BASE.F3}: Trace the beginning of a function and its given parameters with tracing level 3. -- * @{#BASE.T2}: Trace further logic within a function giving optional variables or parameters with tracing level 2. -- * @{#BASE.T3}: Trace further logic within a function giving optional variables or parameters with tracing level 3. --- +-- -- ## 2.3. Trace activation. --- +-- -- Tracing can be activated in several ways: --- +-- -- * Switch tracing on or off through the @{#BASE.TraceOnOff}() method. -- * Activate all tracing through the @{#BASE.TraceAll}() method. -- * Activate only the tracing of a certain class (name) through the @{#BASE.TraceClass}() method. -- * Activate only the tracing of a certain method of a certain class through the @{#BASE.TraceClassMethod}() method. -- * Activate only the tracing of a certain level through the @{#BASE.TraceLevel}() method. --- +-- -- ## 2.4. Check if tracing is on. --- +-- -- The method @{#BASE.IsTrace}() will validate if tracing is activated or not. --- --- +-- -- # 3. DCS simulator Event Handling. --- --- The BASE class provides methods to catch DCS Events. These are events that are triggered from within the DCS simulator, +-- +-- The BASE class provides methods to catch DCS Events. These are events that are triggered from within the DCS simulator, -- and handled through lua scripting. MOOSE provides an encapsulation to handle these events more efficiently. --- +-- -- ## 3.1. Subscribe / Unsubscribe to DCS Events. --- +-- -- At first, the mission designer will need to **Subscribe** to a specific DCS event for the class. -- So, when the DCS event occurs, the class will be notified of that event. -- There are two methods which you use to subscribe to or unsubscribe from an event. --- +-- -- * @{#BASE.HandleEvent}(): Subscribe to a DCS Event. -- * @{#BASE.UnHandleEvent}(): Unsubscribe from a DCS Event. --- +-- -- ## 3.2. Event Handling of DCS Events. --- +-- -- Once the class is subscribed to the event, an **Event Handling** method on the object or class needs to be written that will be called -- when the DCS event occurs. The Event Handling method receives an @{Core.Event#EVENTDATA} structure, which contains a lot of information -- about the event that occurred. --- --- Find below an example of the prototype how to write an event handling function for two units: +-- +-- Find below an example of the prototype how to write an event handling function for two units: -- -- local Tank1 = UNIT:FindByName( "Tank A" ) -- local Tank2 = UNIT:FindByName( "Tank B" ) --- +-- -- -- Here we subscribe to the Dead events. So, if one of these tanks dies, the Tank1 or Tank2 objects will be notified. -- Tank1:HandleEvent( EVENTS.Dead ) -- Tank2:HandleEvent( EVENTS.Dead ) --- +-- -- --- This function is an Event Handling function that will be called when Tank1 is Dead. --- -- @param Wrapper.Unit#UNIT self +-- -- @param Wrapper.Unit#UNIT self -- -- @param Core.Event#EVENTDATA EventData -- function Tank1:OnEventDead( EventData ) -- @@ -6133,49 +8236,47 @@ local _ClassID = 0 -- end -- -- --- This function is an Event Handling function that will be called when Tank2 is Dead. --- -- @param Wrapper.Unit#UNIT self +-- -- @param Wrapper.Unit#UNIT self -- -- @param Core.Event#EVENTDATA EventData -- function Tank2:OnEventDead( EventData ) -- -- self:SmokeBlue() -- end --- --- --- --- See the @{Event} module for more information about event handling. --- +-- +-- See the @{Core.Event} module for more information about event handling. +-- -- # 4. Class identification methods. --- +-- -- BASE provides methods to get more information of each object: --- +-- -- * @{#BASE.GetClassID}(): Gets the ID (number) of the object. Each object created is assigned a number, that is incremented by one. -- * @{#BASE.GetClassName}(): Gets the name of the object, which is the name of the class the object was instantiated from. -- * @{#BASE.GetClassNameAndID}(): Gets the name and ID of the object. --- +-- -- # 5. All objects derived from BASE can have "States". --- --- A mechanism is in place in MOOSE, that allows to let the objects administer **states**. --- States are essentially properties of objects, which are identified by a **Key** and a **Value**. --- --- The method @{#BASE.SetState}() can be used to set a Value with a reference Key to the object. --- To **read or retrieve** a state Value based on a Key, use the @{#BASE.GetState} method. --- +-- +-- A mechanism is in place in MOOSE, that allows to let the objects administer **states**. +-- States are essentially properties of objects, which are identified by a **Key** and a **Value**. +-- +-- The method @{#BASE.SetState}() can be used to set a Value with a reference Key to the object. +-- To **read or retrieve** a state Value based on a Key, use the @{#BASE.GetState} method. +-- -- These two methods provide a very handy way to keep state at long lasting processes. -- Values can be stored within the objects, and later retrieved or changed when needed. -- There is one other important thing to note, the @{#BASE.SetState}() and @{#BASE.GetState} methods -- receive as the **first parameter the object for which the state needs to be set**. -- Thus, if the state is to be set for the same object as the object for which the method is used, then provide the same -- object name to the method. --- +-- -- # 6. Inheritance. --- +-- -- The following methods are available to implement inheritance --- +-- -- * @{#BASE.Inherit}: Inherits from a class. -- * @{#BASE.GetParent}: Returns the parent object from the object it is handling, or nil if there is no parent object. --- +-- -- === --- +-- -- @field #BASE BASE = { ClassName = "BASE", @@ -6186,13 +8287,12 @@ BASE = { Scheduler = nil, } - --- @field #BASE.__ BASE.__ = {} --- @field #BASE._ BASE._ = { - Schedules = {} --- Contains the Schedulers Active + Schedules = {}, --- Contains the Schedulers Active } --- The Formation Class @@ -6200,35 +8300,34 @@ BASE._ = { -- @field Cone A cone formation. FORMATION = { Cone = "Cone", - Vee = "Vee" + Vee = "Vee", } - - ---- BASE constructor. --- +--- BASE constructor. +-- -- This is an example how to use the BASE:New() constructor in a new class definition when inheriting from BASE. --- +-- -- function EVENT:New() -- local self = BASE:Inherit( self, BASE:New() ) -- #EVENT -- return self -- end --- +-- -- @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 - - -- This is for "private" methods... - -- When a __ is passed to a method as "self", the __index will search for the method on the public method list too! --- if rawget( self, "__" ) then - --setmetatable( self, { __index = self.__ } ) --- end - - return self + _ClassID = _ClassID + 1 + self.ClassID = _ClassID + + -- This is for "private" methods... + -- When a __ is passed to a method as "self", the __index will search for the method on the public method list too! + -- if rawget( self, "__" ) then + -- setmetatable( self, { __index = self.__ } ) + -- end + + return self end --- This is the worker method to inherit from a parent class. @@ -6239,29 +8338,28 @@ end function BASE:Inherit( Child, Parent ) -- Create child. - local Child = routines.utils.deepCopy( Child ) + local Child = routines.utils.deepCopy( Child ) - if Child ~= nil then + if Child ~= nil then - -- This is for "private" methods... - -- When a __ is passed to a method as "self", the __index will search for the method on the public method list of the same object too! + -- This is for "private" methods... + -- When a __ is passed to a method as "self", the __index will search for the method on the public method list of the same object too! if rawget( Child, "__" ) then - setmetatable( Child, { __index = Child.__ } ) + setmetatable( Child, { __index = Child.__ } ) setmetatable( Child.__, { __index = Parent } ) else setmetatable( Child, { __index = Parent } ) end - - --Child:_SetDestructor() - end - - return Child -end + -- Child:_SetDestructor() + end + + return Child +end local function getParent( Child ) local Parent = nil - + if Child.ClassName == 'BASE' then Parent = nil else @@ -6269,46 +8367,44 @@ local function getParent( Child ) Parent = getmetatable( Child.__ ).__index else Parent = getmetatable( Child ).__index - end + end end return Parent end - ---- This is the worker method to retrieve the Parent class. +--- This is the worker method to retrieve the Parent class. -- Note that the Parent class must be passed to call the parent class method. --- +-- -- self:GetParent(self):ParentMethod() --- --- +-- +-- -- @param #BASE self -- @param #BASE Child This is the Child class from which the Parent class needs to be retrieved. -- @param #BASE FromClass (Optional) The class from which to get the parent. -- @return #BASE function BASE:GetParent( Child, FromClass ) - local Parent -- BASE class has no parent if Child.ClassName == 'BASE' then Parent = nil else - - --self:E({FromClass = FromClass}) - --self:E({Child = Child.ClassName}) + + -- self:E({FromClass = FromClass}) + -- self:E({Child = Child.ClassName}) if FromClass then - while( Child.ClassName ~= "BASE" and Child.ClassName ~= FromClass.ClassName ) do + while (Child.ClassName ~= "BASE" and Child.ClassName ~= FromClass.ClassName) do Child = getParent( Child ) - --self:E({Child.ClassName}) + -- self:E({Child.ClassName}) end - end + end if Child.ClassName == 'BASE' then Parent = nil else Parent = getParent( Child ) end end - --self:E({Parent.ClassName}) + -- self:E({Parent.ClassName}) return Parent end @@ -6322,7 +8418,7 @@ end -- * ZONE:New( 'some zone' ):IsInstanceOf( 'BASE' ) will return true -- -- * ZONE:New( 'some zone' ):IsInstanceOf( 'GROUP' ) will return false --- +-- -- @param #BASE self -- @param ClassName is the name of the class or the class itself to run the check against -- @return #boolean @@ -6330,32 +8426,32 @@ function BASE:IsInstanceOf( ClassName ) -- Is className NOT a string ? if type( ClassName ) ~= 'string' then - + -- Is className a Moose class ? if type( ClassName ) == 'table' and ClassName.ClassName ~= nil then - + -- Get the name of the Moose class as a string ClassName = ClassName.ClassName - - -- className is neither a string nor a Moose class, throw an error + + -- className is neither a string nor a Moose class, throw an error else - + -- I'm not sure if this should take advantage of MOOSE logging function, or throw an error for pcall - local err_str = 'className parameter should be a string; parameter received: '..type( ClassName ) + local err_str = 'className parameter should be a string; parameter received: ' .. type( ClassName ) self:E( err_str ) -- error( err_str ) return false - + end end - + ClassName = string.upper( ClassName ) if string.upper( self.ClassName ) == ClassName then return true end - local Parent = getParent(self) + local Parent = getParent( self ) while Parent do @@ -6371,7 +8467,7 @@ function BASE:IsInstanceOf( ClassName ) end --- Get the ClassName + ClassID of the class instance. --- The ClassName + ClassID is formatted as '%s#%09d'. +-- The ClassName + ClassID is formatted as '%s#%09d'. -- @param #BASE self -- @return #string The ClassName + ClassID of the class instance. function BASE:GetClassNameAndID() @@ -6398,71 +8494,72 @@ do -- Event Handling -- @param #BASE self -- @return Core.Event#EVENT function BASE:EventDispatcher() - + return _EVENTDISPATCHER end - - - --- Get the Class @{Event} processing Priority. - -- The Event processing Priority is a number from 1 to 10, + + --- Get the Class @{Core.Event} processing Priority. + -- The Event processing Priority is a number from 1 to 10, -- reflecting the order of the classes subscribed to the Event to be processed. -- @param #BASE self - -- @return #number The @{Event} processing Priority. + -- @return #number The @{Core.Event} processing Priority. function BASE:GetEventPriority() return self._.EventPriority or 5 end - - --- Set the Class @{Event} processing Priority. - -- The Event processing Priority is a number from 1 to 10, + + --- Set the Class @{Core.Event} processing Priority. + -- The Event processing Priority is a number from 1 to 10, -- reflecting the order of the classes subscribed to the Event to be processed. -- @param #BASE self - -- @param #number EventPriority The @{Event} processing Priority. + -- @param #number EventPriority The @{Core.Event} processing Priority. -- @return #BASE self function BASE:SetEventPriority( EventPriority ) self._.EventPriority = EventPriority end - + --- Remove all subscribed events -- @param #BASE self -- @return #BASE function BASE:EventRemoveAll() - + self:EventDispatcher():RemoveAll( self ) - + return self end - + --- Subscribe to a DCS Event. -- @param #BASE self -- @param Core.Event#EVENTS EventID Event ID. -- @param #function EventFunction (optional) The function to be called when the event occurs for the unit. -- @return #BASE function BASE:HandleEvent( EventID, EventFunction ) - + self:EventDispatcher():OnEventGeneric( EventFunction, self, EventID ) - + return self end - + --- UnSubscribe to a DCS event. -- @param #BASE self -- @param Core.Event#EVENTS EventID Event ID. -- @return #BASE function BASE:UnHandleEvent( EventID ) - + self:EventDispatcher():RemoveEvent( self, EventID ) - + 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. @@ -6471,6 +8568,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 @@ -6478,6 +8576,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 @@ -6485,212 +8584,243 @@ 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. + -- target: The unit that is being targeted. -- @function [parent=#BASE] OnEventShootingStart -- @param #BASE self -- @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 -- @param Core.Event#EVENTDATA EventData The EventData structure. - --- Unknown precisely what creates this event, likely tied into newer damage model. Will update this page when new information become available. - -- + -- -- * initiator: The unit that had the failure. - -- + -- -- @function [parent=#BASE] OnEventDetailedFailure -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. - --- Occurs when any modification to the "Score" as seen on the debrief menu would occur. + --- 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 -- * weapon: Weapon Object - -- + -- -- @function [parent=#BASE] OnEventKill -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. - --- Occurs when any modification to the "Score" as seen on the debrief menu would occur. + --- 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. - -- + -- -- @function [parent=#BASE] OnEventUnitLost -- @param #BASE self -- @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. -- * subplace: is always 0 for unknown reasons. - -- + -- -- @function [parent=#BASE] OnEventLandingAfterEjection -- @param #BASE self -- @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 @@ -6698,7 +8828,6 @@ do -- Event Handling -- @param Core.Event#EVENTDATA EventData The EventData structure. end - --- Creation of a Birth Event. -- @param #BASE self @@ -6708,47 +8837,65 @@ end -- @param place -- @param subplace function BASE:CreateEventBirth( EventTime, Initiator, IniUnitName, place, subplace ) - self:F( { EventTime, Initiator, IniUnitName, place, subplace } ) + self:F( { EventTime, Initiator, IniUnitName, place, subplace } ) - local Event = { - id = world.event.S_EVENT_BIRTH, - time = EventTime, - initiator = Initiator, - IniUnitName = IniUnitName, - place = place, - subplace = subplace - } + local Event = { + id = world.event.S_EVENT_BIRTH, + time = EventTime, + initiator = Initiator, + IniUnitName = IniUnitName, + place = place, + subplace = subplace, + } - world.onEvent( Event ) + world.onEvent( Event ) end --- Creation of a Crash Event. -- @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:CreateEventCrash( EventTime, Initiator ) - self:F( { EventTime, Initiator } ) +function BASE:CreateEventCrash( EventTime, Initiator, IniObjectCategory ) + self:F( { EventTime, Initiator } ) - local Event = { - id = world.event.S_EVENT_CRASH, - time = EventTime, - initiator = Initiator, - } + local Event = { + id = world.event.S_EVENT_CRASH, + time = EventTime, + initiator = Initiator, + IniObjectCategory = IniObjectCategory, + } - world.onEvent( Event ) + world.onEvent( Event ) end ---- Creation of a Dead Event. +--- Creation of a Crash Event. -- @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 ) +function BASE:CreateEventUnitLost(EventTime, Initiator) self:F( { EventTime, Initiator } ) + local Event = { + id = world.event.S_EVENT_UNIT_LOST, + time = EventTime, + initiator = Initiator, + } + + world.onEvent( Event ) +end + +--- Creation of a Dead Event. +-- @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, IniObjectCategory ) + self:F( { EventTime, Initiator, IniObjectCategory } ) + local Event = { id = world.event.S_EVENT_DEAD, time = EventTime, initiator = Initiator, + IniObjectCategory = IniObjectCategory, } world.onEvent( Event ) @@ -6765,7 +8912,7 @@ function BASE:CreateEventRemoveUnit( EventTime, Initiator ) id = EVENTS.RemoveUnit, time = EventTime, initiator = Initiator, - } + } world.onEvent( Event ) end @@ -6781,7 +8928,7 @@ function BASE:CreateEventTakeoff( EventTime, Initiator ) id = world.event.S_EVENT_TAKEOFF, time = EventTime, initiator = Initiator, - } + } world.onEvent( Event ) end @@ -6800,37 +8947,36 @@ 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 -function BASE:onEvent(event) +function BASE:onEvent( event ) - if self then - - for EventID, EventObject in pairs(self.Events) do - if EventObject.EventEnabled then - - if event.id == EventObject.Event then - - if self == EventObject.Self then - - if event.initiator and event.initiator:isExist() then - event.IniUnitName = event.initiator:getName() - end - - if event.target and event.target:isExist() then - event.TgtUnitName = event.target:getName() - end - - end - - end - - end - end - end + if self then + + for EventID, EventObject in pairs( self.Events ) do + if EventObject.EventEnabled then + + if event.id == EventObject.Event then + + if self == EventObject.Self then + + if event.initiator and event.initiator:isExist() then + event.IniUnitName = event.initiator:getName() + end + + if event.target and event.target:isExist() then + event.TgtUnitName = event.target:getName() + end + + end + + end + + end + end + end end do -- Scheduling @@ -6840,20 +8986,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, @@ -6863,6 +9011,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 @@ -6877,22 +9029,23 @@ 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( { ... } ) - + local ObjectName = "-" ObjectName = self.ClassName .. self.ClassID - + self:F3( { "ScheduleRepeat: ", ObjectName, Start, Repeat, RandomizeFactor, Stop } ) if not self.Scheduler then 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, @@ -6909,21 +9062,20 @@ 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 end - --- Set a state or property of the Object given a Key and a Value. --- Note that if the Object is destroyed, nillified or garbage collected, then the Values and Keys will also be gone. +-- Note that if the Object is destroyed, set to nil, or garbage collected, then the Values and Keys will also be gone. -- @param #BASE self -- @param Object The object that will hold the Value set by the Key. -- @param Key The key that is used as a reference of the value. Note that the key can be a #string, but it can also be any other type! @@ -6935,13 +9087,12 @@ function BASE:SetState( Object, Key, Value ) self.States[ClassNameAndID] = self.States[ClassNameAndID] or {} self.States[ClassNameAndID][Key] = Value - + return self.States[ClassNameAndID][Key] end - --- Get a Value given a Key from the Object. --- Note that if the Object is destroyed, nillified or garbage collected, then the Values and Keys will also be gone. +-- Note that if the Object is destroyed, set to nil, or garbage collected, then the Values and Keys will also be gone. -- @param #BASE self -- @param Object The object that holds the Value set by the Key. -- @param Key The key that is used to retrieve the value. Note that the key can be a #string, but it can also be any other type! @@ -6954,7 +9105,7 @@ function BASE:GetState( Object, Key ) local Value = self.States[ClassNameAndID][Key] or false return Value end - + return nil end @@ -6993,8 +9144,6 @@ function BASE:TraceOff() self:TraceOnOff( false ) end - - --- Set trace on or off -- Note that when trace is off, no BASE.Debug statement is performed, increasing performance! -- When Moose is loaded statically, (as one file), tracing is switched off by default. @@ -7003,28 +9152,29 @@ end -- @param #BASE self -- @param #boolean TraceOnOff Switch the tracing on or off. -- @usage --- -- Switch the tracing On --- BASE:TraceOnOff( true ) --- --- -- Switch the tracing Off --- BASE:TraceOnOff( false ) +-- +-- -- Switch the tracing On +-- BASE:TraceOnOff( true ) +-- +-- -- Switch the tracing Off +-- BASE:TraceOnOff( false ) +-- function BASE:TraceOnOff( TraceOnOff ) - if TraceOnOff==false then + if TraceOnOff == false then self:I( "Tracing in MOOSE is OFF" ) _TraceOnOff = false else - self:I( "Tracing in MOOSE is ON" ) + self:I( "Tracing in MOOSE is ON" ) _TraceOnOff = true end end - --- Enquires if tracing is on (for the class). -- @param #BASE self -- @return #boolean function BASE:IsTrace() - if BASE.Debug and ( _TraceAll == true ) or ( _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName] ) then + if BASE.Debug and (_TraceAll == true) or (_TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName]) then return true else return false @@ -7043,13 +9193,13 @@ end -- @param #BASE self -- @param #boolean TraceAll true = trace all methods in MOOSE. function BASE:TraceAll( TraceAll ) - - if TraceAll==false then - _TraceAll=false + + if TraceAll == false then + _TraceAll = false else _TraceAll = true end - + if _TraceAll then self:I( "Tracing all methods in MOOSE " ) else @@ -7059,7 +9209,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] = {} @@ -7068,8 +9218,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] = {} @@ -7084,16 +9234,16 @@ end -- @param Arguments A #table or any field. function BASE:_F( Arguments, DebugInfoCurrentParam, DebugInfoFromParam ) - if BASE.Debug and ( _TraceAll == true ) or ( _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName] ) then + if BASE.Debug and (_TraceAll == true) or (_TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName]) then local DebugInfoCurrent = DebugInfoCurrentParam and DebugInfoCurrentParam or BASE.Debug.getinfo( 2, "nl" ) local DebugInfoFrom = DebugInfoFromParam and DebugInfoFromParam or BASE.Debug.getinfo( 3, "l" ) - + local Function = "function" if DebugInfoCurrent.name then Function = DebugInfoCurrent.name end - + if _TraceAll == true or _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName].Method[Function] then local LineCurrent = 0 if DebugInfoCurrent.currentline then @@ -7103,7 +9253,7 @@ function BASE:_F( Arguments, DebugInfoCurrentParam, DebugInfoFromParam ) if DebugInfoFrom then LineFrom = DebugInfoFrom.currentline end - env.info( string.format( "%6d(%6d)/%1s:%30s%05d.%s(%s)" , LineCurrent, LineFrom, "F", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) + env.info( string.format( "%6d(%6d)/%1s:%30s%05d.%s(%s)", LineCurrent, LineFrom, "F", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) end end end @@ -7116,14 +9266,13 @@ function BASE:F( Arguments ) if BASE.Debug and _TraceOnOff then local DebugInfoCurrent = BASE.Debug.getinfo( 2, "nl" ) local DebugInfoFrom = BASE.Debug.getinfo( 3, "l" ) - + if _TraceLevel >= 1 then self:_F( Arguments, DebugInfoCurrent, DebugInfoFrom ) end - end + end end - --- Trace a function call level 2. Must be at the beginning of the function logic. -- @param #BASE self -- @param Arguments A #table or any field. @@ -7132,11 +9281,11 @@ function BASE:F2( Arguments ) if BASE.Debug and _TraceOnOff then local DebugInfoCurrent = BASE.Debug.getinfo( 2, "nl" ) local DebugInfoFrom = BASE.Debug.getinfo( 3, "l" ) - + if _TraceLevel >= 2 then self:_F( Arguments, DebugInfoCurrent, DebugInfoFrom ) end - end + end end --- Trace a function call level 3. Must be at the beginning of the function logic. @@ -7147,11 +9296,11 @@ function BASE:F3( Arguments ) if BASE.Debug and _TraceOnOff then local DebugInfoCurrent = BASE.Debug.getinfo( 2, "nl" ) local DebugInfoFrom = BASE.Debug.getinfo( 3, "l" ) - + if _TraceLevel >= 3 then self:_F( Arguments, DebugInfoCurrent, DebugInfoFrom ) end - end + end end --- Trace a function logic. @@ -7159,28 +9308,28 @@ end -- @param Arguments A #table or any field. function BASE:_T( Arguments, DebugInfoCurrentParam, DebugInfoFromParam ) - if BASE.Debug and ( _TraceAll == true ) or ( _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName] ) then + if BASE.Debug and (_TraceAll == true) or (_TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName]) then local DebugInfoCurrent = DebugInfoCurrentParam and DebugInfoCurrentParam or BASE.Debug.getinfo( 2, "nl" ) local DebugInfoFrom = DebugInfoFromParam and DebugInfoFromParam or BASE.Debug.getinfo( 3, "l" ) - - local Function = "function" - if DebugInfoCurrent.name then - Function = DebugInfoCurrent.name - end + + local Function = "function" + if DebugInfoCurrent.name then + Function = DebugInfoCurrent.name + end if _TraceAll == true or _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName].Method[Function] then local LineCurrent = 0 if DebugInfoCurrent.currentline then LineCurrent = DebugInfoCurrent.currentline end - local LineFrom = 0 - if DebugInfoFrom then - LineFrom = DebugInfoFrom.currentline - end - env.info( string.format( "%6d(%6d)/%1s:%30s%05d.%s" , LineCurrent, LineFrom, "T", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) ) + local LineFrom = 0 + if DebugInfoFrom then + LineFrom = DebugInfoFrom.currentline + end + env.info( string.format( "%6d(%6d)/%1s:%30s%05d.%s", LineCurrent, LineFrom, "T", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) ) end - end + end end --- Trace a function logic level 1. Can be anywhere within the function logic. @@ -7191,14 +9340,13 @@ function BASE:T( Arguments ) if BASE.Debug and _TraceOnOff then local DebugInfoCurrent = BASE.Debug.getinfo( 2, "nl" ) local DebugInfoFrom = BASE.Debug.getinfo( 3, "l" ) - + if _TraceLevel >= 1 then self:_T( Arguments, DebugInfoCurrent, DebugInfoFrom ) end - end + end end - --- Trace a function logic level 2. Can be anywhere within the function logic. -- @param #BASE self -- @param Arguments A #table or any field. @@ -7207,7 +9355,7 @@ function BASE:T2( Arguments ) if BASE.Debug and _TraceOnOff then local DebugInfoCurrent = BASE.Debug.getinfo( 2, "nl" ) local DebugInfoFrom = BASE.Debug.getinfo( 3, "l" ) - + if _TraceLevel >= 2 then self:_T( Arguments, DebugInfoCurrent, DebugInfoFrom ) end @@ -7222,7 +9370,7 @@ function BASE:T3( Arguments ) if BASE.Debug and _TraceOnOff then local DebugInfoCurrent = BASE.Debug.getinfo( 2, "nl" ) local DebugInfoFrom = BASE.Debug.getinfo( 3, "l" ) - + if _TraceLevel >= 3 then self:_T( Arguments, DebugInfoCurrent, DebugInfoFrom ) end @@ -7235,27 +9383,26 @@ end function BASE:E( Arguments ) if BASE.Debug then - local DebugInfoCurrent = BASE.Debug.getinfo( 2, "nl" ) - local DebugInfoFrom = BASE.Debug.getinfo( 3, "l" ) - - local Function = "function" - if DebugInfoCurrent.name then - Function = DebugInfoCurrent.name - end - - local LineCurrent = DebugInfoCurrent.currentline - local LineFrom = -1 - if DebugInfoFrom then - LineFrom = DebugInfoFrom.currentline - end - - env.info( string.format( "%6d(%6d)/%1s:%30s%05d.%s(%s)" , LineCurrent, LineFrom, "E", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) + local DebugInfoCurrent = BASE.Debug.getinfo( 2, "nl" ) + local DebugInfoFrom = BASE.Debug.getinfo( 3, "l" ) + + local Function = "function" + if DebugInfoCurrent.name then + Function = DebugInfoCurrent.name + end + + local LineCurrent = DebugInfoCurrent.currentline + local LineFrom = -1 + if DebugInfoFrom then + LineFrom = DebugInfoFrom.currentline + end + + env.info( string.format( "%6d(%6d)/%1s:%30s%05d.%s(%s)", LineCurrent, LineFrom, "E", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) else - env.info( string.format( "%1s:%30s%05d(%s)" , "E", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) ) + env.info( string.format( "%1s:%30s%05d(%s)", "E", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) ) end - -end +end --- Log an information which will be traced always. Can be anywhere within the function logic. -- @param #BASE self @@ -7265,38 +9412,35 @@ function BASE:I( Arguments ) if BASE.Debug then local DebugInfoCurrent = BASE.Debug.getinfo( 2, "nl" ) local DebugInfoFrom = BASE.Debug.getinfo( 3, "l" ) - + local Function = "function" if DebugInfoCurrent.name then Function = DebugInfoCurrent.name end - + local LineCurrent = DebugInfoCurrent.currentline - local LineFrom = -1 + local LineFrom = -1 if DebugInfoFrom then LineFrom = DebugInfoFrom.currentline end - - env.info( string.format( "%6d(%6d)/%1s:%30s%05d.%s(%s)" , LineCurrent, LineFrom, "I", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) + + env.info( string.format( "%6d(%6d)/%1s:%30s%05d.%s(%s)", LineCurrent, LineFrom, "I", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) else - env.info( string.format( "%1s:%30s%05d(%s)" , "I", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) ) + env.info( string.format( "%1s:%30s%05d(%s)", "I", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) ) end - -end - +end --- old stuff ---function BASE:_Destructor() +-- function BASE:_Destructor() -- --self:E("_Destructor") -- -- --self:EventRemoveAll() ---end - +-- end -- THIS IS WHY WE NEED LUA 5.2 ... ---function BASE:_SetDestructor() +-- function BASE:_SetDestructor() -- -- -- TODO: Okay, this is really technical... -- -- When you set a proxy to a table to catch __gc, weak tables don't behave like weak... @@ -7316,248 +9460,1224 @@ end -- -- table is about to be garbage-collected - then the __gc hook -- -- will be invoked and the destructor called -- rawset( self, '__proxy', proxy ) --- ---end--- **Core** - TACAN and other beacons. --- +-- +-- end +--- **Core** - A* Pathfinding. +-- +-- **Main Features:** +-- +-- * Find path from A to B. +-- * Pre-defined as well as custom valid neighbour functions. +-- * Pre-defined as well as custom cost functions. +-- * Easy rectangular grid setup. +-- -- === --- --- ## Features: --- --- * Provide beacon functionality to assist pilots. -- +-- ### Author: **funkyfranky** +-- -- === +-- @module Core.Astar +-- @image CORE_Astar.png + + +--- ASTAR class. +-- @type ASTAR +-- @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 #table nodes Table of nodes. +-- @field #number counter Node counter. +-- @field #number Nnodes Number of nodes. +-- @field #number nvalid Number of nvalid calls. +-- @field #number nvalidcache Number of cached valid evals. +-- @field #number ncost Number of cost evaluations. +-- @field #number ncostcache Number of cached cost evals. +-- @field #ASTAR.Node startNode Start node. +-- @field #ASTAR.Node endNode End node. +-- @field Core.Point#COORDINATE startCoord Start coordinate. +-- @field Core.Point#COORDINATE endCoord End coordinate. +-- @field #function ValidNeighbourFunc Function to check if a node is valid. +-- @field #table ValidNeighbourArg Optional arguments passed to the valid neighbour function. +-- @field #function CostFunc Function to calculate the heuristic "cost" to go from one node to another. +-- @field #table CostArg Optional arguments passed to the cost function. +-- @extends Core.Base#BASE + +--- *When nothing goes right... Go left!* -- --- ### Authors: Hugues "Grey_Echo" Bousquet, funkyfranky +-- === -- --- @module Core.Beacon --- @image Core_Radio.JPG - ---- *In order for the light to shine so brightly, the darkness must be present.* -- Francis Bacon +-- # The ASTAR Concept -- --- 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. --- 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. +-- Pathfinding algorithm. -- --- ## AA 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. +-- # Start and Goal -- --- ## General Purpose Radio Beacon usage +-- The first thing we need to define is obviously the place where we want to start and where we want to go eventually. -- --- 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. +-- ## Start -- --- @type BEACON --- @field #string ClassName Name of the class "BEACON". --- @field Wrapper.Controllable#CONTROLLABLE Positionable The @{#CONTROLLABLE} that will receive radio capabilities. --- @extends Core.Base#BASE -BEACON = { - ClassName = "BEACON", - Positionable = nil, - name = nil, +-- The start +-- +-- ## Goal +-- +-- +-- # Nodes +-- +-- ## Rectangular Grid +-- +-- A rectangular grid can be created using the @{#ASTAR.CreateGrid}(*ValidSurfaceTypes, BoxHY, SpaceX, deltaX, deltaY, MarkGrid*), where +-- +-- * *ValidSurfaceTypes* is a table of valid surface types. By default all surface types are valid. +-- * *BoxXY* is the width of the grid perpendicular the the line between start and end node. Default is 40,000 meters (40 km). +-- * *SpaceX* is the additional space behind the start and end nodes. Default is 20,000 meters (20 km). +-- * *deltaX* is the grid spacing between nodes in the direction of start and end node. Default is 2,000 meters (2 km). +-- * *deltaY* is the grid spacing perpendicular to the direction of start and end node. Default is the same as *deltaX*. +-- * *MarkGrid* If set to *true*, this places marker on the F10 map on each grid node. Note that this can stall DCS if too many nodes are created. +-- +-- ## Valid Surfaces +-- +-- Certain unit types can only travel on certain surfaces types, for example +-- +-- * Naval units can only travel on water (that also excludes shallow water in DCS currently), +-- * Ground units can only traval on land. +-- +-- By restricting the surface type in the grid construction, we also reduce the number of nodes, which makes the algorithm more efficient. +-- +-- ## Box Width (BoxHY) +-- +-- The box width needs to be large enough to capture all paths you want to consider. +-- +-- ## Space in X +-- +-- The space in X value is important if the algorithm needs to to backwards from the start node or needs to extend even further than the end node. +-- +-- ## Grid Spacing +-- +-- The grid spacing is an important factor as it determines the number of nodes and hence the performance of the algorithm. It should be as large as possible. +-- However, if the value is too large, the algorithm might fail to get a valid path. +-- +-- A good estimate of the grid spacing is to set it to be smaller (~ half the size) of the smallest gap you need to path. +-- +-- # Valid Neighbours +-- +-- The A* algorithm needs to know if a transition from one node to another is allowed or not. By default, hopping from one node to another is always possible. +-- +-- ## Line of Sight +-- +-- For naval +-- +-- +-- # Heuristic Cost +-- +-- In order to determine the optimal path, the pathfinding algorithm needs to know, how costly it is to go from one node to another. +-- Often, this can simply be determined by the distance between two nodes. Therefore, the default cost function is set to be the 2D distance between two nodes. +-- +-- +-- # Calculate the Path +-- +-- Finally, we have to calculate the path. This is done by the @{ASTAR.GetPath}(*ExcludeStart, ExcludeEnd*) function. This function returns a table of nodes, which +-- describe the optimal path from the start node to the end node. +-- +-- By default, the start and end node are include in the table that is returned. +-- +-- Note that a valid path must not always exist. So you should check if the function returns *nil*. +-- +-- Common reasons that a path cannot be found are: +-- +-- * The grid is too small ==> increase grid size, e.g. *BoxHY* and/or *SpaceX* if you use a rectangular grid. +-- * The grid spacing is too large ==> decrease *deltaX* and/or *deltaY* +-- * There simply is no valid path ==> you are screwed :( +-- +-- +-- # Examples +-- +-- ## Strait of Hormuz +-- +-- Carrier Group finds its way through the Stait of Hormuz. +-- +-- ## +-- +-- +-- +-- @field #ASTAR +ASTAR = { + ClassName = "ASTAR", + Debug = nil, + lid = nil, + nodes = {}, + counter = 1, + Nnodes = 0, + ncost = 0, + ncostcache = 0, + nvalid = 0, + nvalidcache = 0, } ---- Beacon types supported by DCS. --- @type BEACON.Type --- @field #number NULL --- @field #number VOR --- @field #number DME --- @field #number VOR_DME --- @field #number TACAN TACtical Air Navigation system. --- @field #number VORTAC --- @field #number RSBN --- @field #number BROADCAST_STATION --- @field #number HOMER --- @field #number AIRPORT_HOMER --- @field #number AIRPORT_HOMER_WITH_MARKER --- @field #number ILS_FAR_HOMER --- @field #number ILS_NEAR_HOMER --- @field #number ILS_LOCALIZER --- @field #number ILS_GLIDESLOPE --- @field #number PRMG_LOCALIZER --- @field #number PRMG_GLIDESLOPE --- @field #number ICLS Same as ICLS glideslope. --- @field #number ICLS_LOCALIZER --- @field #number ICLS_GLIDESLOPE --- @field #number NAUTICAL_HOMER -BEACON.Type={ - NULL = 0, - VOR = 1, - DME = 2, - VOR_DME = 3, - TACAN = 4, - VORTAC = 5, - RSBN = 128, - BROADCAST_STATION = 1024, - HOMER = 8, - AIRPORT_HOMER = 4104, - AIRPORT_HOMER_WITH_MARKER = 4136, - ILS_FAR_HOMER = 16408, - ILS_NEAR_HOMER = 16424, - ILS_LOCALIZER = 16640, - ILS_GLIDESLOPE = 16896, - PRMG_LOCALIZER = 33024, - PRMG_GLIDESLOPE = 33280, - ICLS = 131584, --leaving this in here but it is the same as ICLS_GLIDESLOPE - ICLS_LOCALIZER = 131328, - ICLS_GLIDESLOPE = 131584, - NAUTICAL_HOMER = 65536, -} +--- Node data. +-- @type ASTAR.Node +-- @field #number id Node id. +-- @field Core.Point#COORDINATE coordinate Coordinate of the node. +-- @field #number surfacetype Surface type. +-- @field #table valid Cached valid/invalid nodes. +-- @field #table cost Cached cost. ---- Beacon systems supported by DCS. https://wiki.hoggitworld.com/view/DCS_command_activateBeacon --- @type BEACON.System --- @field #number PAR_10 ? --- @field #number RSBN_5 Russian VOR/DME system. --- @field #number TACAN TACtical Air Navigation system on ground. --- @field #number TACAN_TANKER_X TACtical Air Navigation system for tankers on X band. --- @field #number TACAN_TANKER_Y TACtical Air Navigation system for tankers on Y band. --- @field #number VOR Very High Frequency Omni-Directional Range --- @field #number ILS_LOCALIZER ILS localizer --- @field #number ILS_GLIDESLOPE ILS glideslope. --- @field #number PRGM_LOCALIZER PRGM localizer. --- @field #number PRGM_GLIDESLOPE PRGM glideslope. --- @field #number BROADCAST_STATION Broadcast station. --- @field #number VORTAC Radio-based navigational aid for aircraft pilots consisting of a co-located VHF omnidirectional range (VOR) beacon and a tactical air navigation system (TACAN) beacon. --- @field #number TACAN_AA_MODE_X TACtical Air Navigation for aircraft on X band. --- @field #number TACAN_AA_MODE_Y TACtical Air Navigation for aircraft on Y band. --- @field #number VORDME Radio beacon that combines a VHF omnidirectional range (VOR) with a distance measuring equipment (DME). --- @field #number ICLS_LOCALIZER Carrier landing system. --- @field #number ICLS_GLIDESLOPE Carrier landing system. -BEACON.System={ - PAR_10 = 1, - RSBN_5 = 2, - TACAN = 3, - TACAN_TANKER_X = 4, - TACAN_TANKER_Y = 5, - VOR = 6, - ILS_LOCALIZER = 7, - ILS_GLIDESLOPE = 8, - PRMG_LOCALIZER = 9, - PRMG_GLIDESLOPE = 10, - BROADCAST_STATION = 11, - VORTAC = 12, - TACAN_AA_MODE_X = 13, - TACAN_AA_MODE_Y = 14, - VORDME = 15, - ICLS_LOCALIZER = 16, - ICLS_GLIDESLOPE = 17, -} +--- ASTAR infinity. +-- @field #number INF +ASTAR.INF=1/0 ---- Create a new BEACON Object. This doesn't activate the beacon, though, use @{#BEACON.ActivateTACAN} etc. --- If you want to create a BEACON, you probably should use @{Wrapper.Positionable#POSITIONABLE.GetBeacon}() instead. --- @param #BEACON self --- @param Wrapper.Positionable#POSITIONABLE Positionable The @{Positionable} that will receive radio capabilities. --- @return #BEACON Beacon object or #nil if the positionable is invalid. -function BEACON:New(Positionable) +--- ASTAR class version. +-- @field #string version +ASTAR.version="0.4.0" - -- Inherit BASE. - local self=BASE:Inherit(self, BASE:New()) --#BEACON - - -- Debug. - self:F(Positionable) +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Add more valid neighbour functions. +-- TODO: Write docs. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new ASTAR object. +-- @param #ASTAR self +-- @return #ASTAR self +function ASTAR:New() + + -- Inherit everything from INTEL class. + local self=BASE:Inherit(self, BASE:New()) --#ASTAR + + self.lid="ASTAR | " + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set coordinate from where to start. +-- @param #ASTAR self +-- @param Core.Point#COORDINATE Coordinate Start coordinate. +-- @return #ASTAR self +function ASTAR:SetStartCoordinate(Coordinate) + + self.startCoord=Coordinate - -- Set positionable. - if Positionable:GetPointVec2() then -- It's stupid, but the only way I found to make sure positionable is valid - self.Positionable = Positionable - self.name=Positionable:GetName() - self:I(string.format("New BEACON %s", tostring(self.name))) - return self - end + return self +end + +--- Set coordinate where you want to go. +-- @param #ASTAR self +-- @param Core.Point#COORDINATE Coordinate end coordinate. +-- @return #ASTAR self +function ASTAR:SetEndCoordinate(Coordinate) + + self.endCoord=Coordinate - self:E({"The passed positionable is invalid, no BEACON created", Positionable}) - return nil + return self end +--- Create a node from a given coordinate. +-- @param #ASTAR self +-- @param Core.Point#COORDINATE Coordinate The coordinate where to create the node. +-- @return #ASTAR.Node The node. +function ASTAR:GetNodeFromCoordinate(Coordinate) ---- Activates a TACAN BEACON. --- @param #BEACON self --- @param #number Channel TACAN channel, i.e. the "10" part in "10Y". --- @param #string Mode TACAN mode, i.e. the "Y" part in "10Y". --- @param #string Message The Message that is going to be coded in Morse and broadcasted by the beacon. --- @param #boolean Bearing If true, beacon provides bearing information. If false (or nil), only distance information is available. --- @param #number Duration How long will the beacon last in seconds. Omit for forever. --- @return #BEACON self --- @usage --- -- Let's create a TACAN Beacon for a tanker --- local myUnit = UNIT:FindByName("MyUnit") --- local myBeacon = myUnit:GetBeacon() -- Creates the beacon --- --- myBeacon:ActivateTACAN(20, "Y", "TEXACO", true) -- Activate the beacon -function BEACON:ActivateTACAN(Channel, Mode, Message, Bearing, Duration) - self:T({channel=Channel, mode=Mode, callsign=Message, bearing=Bearing, duration=Duration}) + local node={} --#ASTAR.Node - -- Get frequency. - local Frequency=UTILS.TACANToFrequency(Channel, Mode) + node.coordinate=Coordinate + node.surfacetype=Coordinate:GetSurfaceType() + node.id=self.counter - -- Check. - if not Frequency then - self:E({"The passed TACAN channel is invalid, the BEACON is not emitting"}) - return self - end + node.valid={} + node.cost={} - -- Beacon type. - local Type=BEACON.Type.TACAN + self.counter=self.counter+1 - -- Beacon system. - local System=BEACON.System.TACAN + return node +end + + +--- Add a node to the table of grid nodes. +-- @param #ASTAR self +-- @param #ASTAR.Node Node The node to be added. +-- @return #ASTAR self +function ASTAR:AddNode(Node) + + self.nodes[Node.id]=Node + self.Nnodes=self.Nnodes+1 + + return self +end + +--- Add a node to the table of grid nodes specifying its coordinate. +-- @param #ASTAR self +-- @param Core.Point#COORDINATE Coordinate The coordinate where the node is created. +-- @return #ASTAR.Node The node. +function ASTAR:AddNodeFromCoordinate(Coordinate) + + local node=self:GetNodeFromCoordinate(Coordinate) - -- 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}) - end - end + self:AddNode(node) + + return node +end + +--- Check if the coordinate of a node has is at a valid surface type. +-- @param #ASTAR self +-- @param #ASTAR.Node Node The node to be added. +-- @param #table SurfaceTypes Surface types, for example `{land.SurfaceType.WATER}`. By default all surface types are valid. +-- @return #boolean If true, surface type of node is valid. +function ASTAR:CheckValidSurfaceType(Node, SurfaceTypes) + + if SurfaceTypes then - -- Attached unit. - local UnitID=self.Positionable:GetID() + if type(SurfaceTypes)~="table" then + SurfaceTypes={SurfaceTypes} + end + + for _,surface in pairs(SurfaceTypes) do + if surface==Node.surfacetype then + return true + end + end - -- Debug. - self:I({string.format("BEACON Activating TACAN %s: Channel=%d%s, Morse=%s, Bearing=%s, Duration=%s!", tostring(self.name), Channel, Mode, Message, tostring(Bearing), tostring(Duration))}) + return false - -- Start beacon. - self.Positionable:CommandActivateBeacon(Type, System, Frequency, UnitID, Channel, Mode, AA, Message, Bearing) - - -- Stop sheduler. - if Duration then - self.Positionable:DeactivateBeacon(Duration) + else + return true + end + +end + +--- Add a function to determine if a neighbour of a node is valid. +-- @param #ASTAR self +-- @param #function NeighbourFunction Function that needs to return *true* for a neighbour to be valid. +-- @param ... Condition function arguments if any. +-- @return #ASTAR self +function ASTAR:SetValidNeighbourFunction(NeighbourFunction, ...) + + self.ValidNeighbourFunc=NeighbourFunction + + self.ValidNeighbourArg={} + if arg then + self.ValidNeighbourArg=arg end return self end ---- Activates an ICLS BEACON. The unit the BEACON is attached to should be an aircraft carrier supporting this system. --- @param #BEACON self --- @param #number Channel ICLS channel. --- @param #string Callsign The Message 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 + +--- Set valid neighbours to require line of sight between two nodes. +-- @param #ASTAR self +-- @param #number CorridorWidth Width of LoS corridor in meters. +-- @return #ASTAR self +function ASTAR:SetValidNeighbourLoS(CorridorWidth) + + self:SetValidNeighbourFunction(ASTAR.LoS, CorridorWidth) + + return self +end + +--- Set valid neighbours to be in a certain distance. +-- @param #ASTAR self +-- @param #number MaxDistance Max distance between nodes in meters. Default is 2000 m. +-- @return #ASTAR self +function ASTAR:SetValidNeighbourDistance(MaxDistance) + + self:SetValidNeighbourFunction(ASTAR.DistMax, MaxDistance) + + return self +end + +--- Set valid neighbours to be in a certain distance. +-- @param #ASTAR self +-- @param #number MaxDistance Max distance between nodes in meters. Default is 2000 m. +-- @return #ASTAR self +function ASTAR:SetValidNeighbourRoad(MaxDistance) + + self:SetValidNeighbourFunction(ASTAR.Road, MaxDistance) + + return self +end + +--- Set the function which calculates the "cost" to go from one to another node. +-- The first to arguments of this function are always the two nodes under consideration. But you can add optional arguments. +-- Very often the distance between nodes is a good measure for the cost. +-- @param #ASTAR self +-- @param #function CostFunction Function that returns the "cost". +-- @param ... Condition function arguments if any. +-- @return #ASTAR self +function ASTAR:SetCostFunction(CostFunction, ...) + + self.CostFunc=CostFunction + + self.CostArg={} + if arg then + self.CostArg=arg + end + + return self +end + +--- Set heuristic cost to go from one node to another to be their 2D distance. +-- @param #ASTAR self +-- @return #ASTAR self +function ASTAR:SetCostDist2D() + + self:SetCostFunction(ASTAR.Dist2D) + + return self +end + +--- Set heuristic cost to go from one node to another to be their 3D distance. +-- @param #ASTAR self +-- @return #ASTAR self +function ASTAR:SetCostDist3D() + + self:SetCostFunction(ASTAR.Dist3D) + + return self +end + +--- Set heuristic cost to go from one node to another to be their 3D distance. +-- @param #ASTAR self +-- @return #ASTAR self +function ASTAR:SetCostRoad() + + self:SetCostFunction(ASTAR) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Grid functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a rectangular grid of nodes between star and end coordinate. +-- The coordinate system is oriented along the line between start and end point. +-- @param #ASTAR self +-- @param #table ValidSurfaceTypes Valid surface types. By default is all surfaces are allowed. +-- @param #number BoxHY Box "height" in meters along the y-coordinate. Default 40000 meters (40 km). +-- @param #number SpaceX Additional space in meters before start and after end coordinate. Default 10000 meters (10 km). +-- @param #number deltaX Increment in the direction of start to end coordinate in meters. Default 2000 meters. +-- @param #number deltaY Increment perpendicular to the direction of start to end coordinate in meters. Default is same as deltaX. +-- @param #boolean MarkGrid If true, create F10 map markers at grid nodes. +-- @return #ASTAR self +function ASTAR:CreateGrid(ValidSurfaceTypes, BoxHY, SpaceX, deltaX, deltaY, MarkGrid) + + -- Note that internally + -- x coordinate is z: x-->z Line from start to end + -- y coordinate is x: y-->x Perpendicular + + -- Grid length and width. + local Dz=SpaceX or 10000 + local Dx=BoxHY and BoxHY/2 or 20000 + + -- Increments. + local dz=deltaX or 2000 + local dx=deltaY or dz + + -- Heading from start to end coordinate. + local angle=self.startCoord:HeadingTo(self.endCoord) + + --Distance between start and end. + local dist=self.startCoord:Get2DDistance(self.endCoord)+2*Dz + + -- Origin of map. Needed to translate back to wanted position. + local co=COORDINATE:New(0, 0, 0) + local do1=co:Get2DDistance(self.startCoord) + local ho1=co:HeadingTo(self.startCoord) + + -- Start of grid. + local xmin=-Dx + local zmin=-Dz + + -- Number of grid points. + local nz=dist/dz+1 + local nx=2*Dx/dx+1 + + -- Debug info. + local text=string.format("Building grid with nx=%d ny=%d => total=%d nodes", nx, nz, nx*nz) + self:T(self.lid..text) + + -- Loop over x and z coordinate to create a 2D grid. + for i=1,nx do + + -- x coordinate perpendicular to z. + local x=xmin+dx*(i-1) + + for j=1,nz do + + -- z coordinate connecting start and end. + local z=zmin+dz*(j-1) + + -- Rotate 2D. + local vec3=UTILS.Rotate2D({x=x, y=0, z=z}, angle) + + -- Coordinate of the node. + local c=COORDINATE:New(vec3.z, vec3.y, vec3.x):Translate(do1, ho1, true) + + -- Create a node at this coordinate. + local node=self:GetNodeFromCoordinate(c) + + -- Check if node has valid surface type. + if self:CheckValidSurfaceType(node, ValidSurfaceTypes) then + + if MarkGrid then + c:MarkToAll(string.format("i=%d, j=%d surface=%d", i, j, node.surfacetype)) + end + + -- Add node to grid. + self:AddNode(node) + + end + + end + end + + -- Debug info. + local text=string.format("Done building grid!") + self:T2(self.lid..text) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Valid neighbour functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Function to check if two nodes have line of sight (LoS). +-- @param #ASTAR.Node nodeA First node. +-- @param #ASTAR.Node nodeB Other node. +-- @param #number corridor (Optional) Width of corridor in meters. +-- @return #boolean If true, two nodes have LoS. +function ASTAR.LoS(nodeA, nodeB, corridor) + + local offset=1 + + local dx=corridor and corridor/2 or nil + local dy=dx + + local cA=nodeA.coordinate:GetVec3() + local cB=nodeB.coordinate:GetVec3() + cA.y=offset + cB.y=offset + + local los=land.isVisible(cA, cB) + + if los and corridor then + + -- Heading from A to B. + local heading=nodeA.coordinate:HeadingTo(nodeB.coordinate) + + local Ap=UTILS.VecTranslate(cA, dx, heading+90) + local Bp=UTILS.VecTranslate(cB, dx, heading+90) + + los=land.isVisible(Ap, Bp) + + if los then + + local Am=UTILS.VecTranslate(cA, dx, heading-90) + local Bm=UTILS.VecTranslate(cB, dx, heading-90) + + los=land.isVisible(Am, Bm) + end + + end + + return los +end + +--- Function to check if two nodes have a road connection. +-- @param #ASTAR.Node nodeA First node. +-- @param #ASTAR.Node nodeB Other node. +-- @return #boolean If true, two nodes are connected via a road. +function ASTAR.Road(nodeA, nodeB) + + local path=land.findPathOnRoads("roads", nodeA.coordinate.x, nodeA.coordinate.z, nodeB.coordinate.x, nodeB.coordinate.z) + + if path then + return true + else + return false + end + +end + +--- Function to check if distance between two nodes is less than a threshold distance. +-- @param #ASTAR.Node nodeA First node. +-- @param #ASTAR.Node nodeB Other node. +-- @param #number distmax Max distance in meters. Default is 2000 m. +-- @return #boolean If true, distance between the two nodes is below threshold. +function ASTAR.DistMax(nodeA, nodeB, distmax) + + distmax=distmax or 2000 + + local dist=nodeA.coordinate:Get2DDistance(nodeB.coordinate) + + return dist<=distmax +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Heuristic cost functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Heuristic cost is given by the 2D distance between the nodes. +-- @param #ASTAR.Node nodeA First node. +-- @param #ASTAR.Node nodeB Other node. +-- @return #number Distance between the two nodes. +function ASTAR.Dist2D(nodeA, nodeB) + local dist=nodeA.coordinate:Get2DDistance(nodeB) + return dist +end + +--- Heuristic cost is given by the 3D distance between the nodes. +-- @param #ASTAR.Node nodeA First node. +-- @param #ASTAR.Node nodeB Other node. +-- @return #number Distance between the two nodes. +function ASTAR.Dist3D(nodeA, nodeB) + local dist=nodeA.coordinate:Get3DDistance(nodeB.coordinate) + return dist +end + +--- Heuristic cost is given by the distance between the nodes on road. +-- @param #ASTAR.Node nodeA First node. +-- @param #ASTAR.Node nodeB Other node. +-- @return #number Distance between the two nodes. +function ASTAR.DistRoad(nodeA, nodeB) + + -- Get the path. + local path=land.findPathOnRoads("roads", nodeA.coordinate.x, nodeA.coordinate.z, nodeB.coordinate.x, nodeB.coordinate.z) + + if path then + + local dist=0 + + for i=2,#path do + local b=path[i] --DCS#Vec2 + local a=path[i-1] --DCS#Vec2 + + dist=dist+UTILS.VecDist2D(a,b) + + end + + return dist + end + + + return math.huge +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Find the closest node from a given coordinate. +-- @param #ASTAR self +-- @param Core.Point#COORDINATE Coordinate. +-- @return #ASTAR.Node Cloest node to the coordinate. +-- @return #number Distance to closest node in meters. +function ASTAR:FindClosestNode(Coordinate) + + local distMin=math.huge + local closeNode=nil + + for _,_node in pairs(self.nodes) do + local node=_node --#ASTAR.Node + + local dist=node.coordinate:Get2DDistance(Coordinate) + + if dist1000 then + self:T(self.lid.."Adding start node to node grid!") + self:AddNode(node) + end + + return self +end + +--- Add a node. +-- @param #ASTAR self +-- @param #ASTAR.Node Node The node to be added to the nodes table. +-- @return #ASTAR self +function ASTAR:FindEndNode() + + local node, dist=self:FindClosestNode(self.endCoord) + + self.endNode=node + + if dist>1000 then + self:T(self.lid.."Adding end node to node grid!") + self:AddNode(node) + end + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Main A* pathfinding function +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- A* pathfinding function. This seaches the path along nodes between start and end nodes/coordinates. +-- @param #ASTAR self +-- @param #boolean ExcludeStartNode If *true*, do not include start node in found path. Default is to include it. +-- @param #boolean ExcludeEndNode If *true*, do not include end node in found path. Default is to include it. +-- @return #table Table of nodes from start to finish. +function ASTAR:GetPath(ExcludeStartNode, ExcludeEndNode) + + self:FindStartNode() + self:FindEndNode() + + local nodes=self.nodes + local start=self.startNode + local goal=self.endNode + + -- Sets. + local openset = {} + local closedset = {} + local came_from = {} + local g_score = {} + local f_score = {} + + openset[start.id]=true + local Nopen=1 + + -- Initial scores. + g_score[start.id]=0 + f_score[start.id]=g_score[start.id]+self:_HeuristicCost(start, goal) + + -- Set start time. + local T0=timer.getAbsTime() + + -- Debug message. + local text=string.format("Starting A* pathfinding with %d Nodes", self.Nnodes) + self:T(self.lid..text) + + local Tstart=UTILS.GetOSTime() + + -- Loop while we still have an open set. + while Nopen > 0 do + + -- Get current node. + local current=self:_LowestFscore(openset, f_score) + + -- Check if we are at the end node. + if current.id==goal.id then + + local path=self:_UnwindPath({}, came_from, goal) + + if not ExcludeEndNode then + table.insert(path, goal) + end + + if ExcludeStartNode then + table.remove(path, 1) + end + + local Tstop=UTILS.GetOSTime() + + local dT=nil + if Tstart and Tstop then + dT=Tstop-Tstart + end + + -- Debug message. + local text=string.format("Found path with %d nodes (%d total)", #path, self.Nnodes) + if dT then + text=text..string.format(", OS Time %.6f sec", dT) + end + text=text..string.format(", Nvalid=%d [%d cached]", self.nvalid, self.nvalidcache) + text=text..string.format(", Ncost=%d [%d cached]", self.ncost, self.ncostcache) + self:T(self.lid..text) + + return path + end + + -- Move Node from open to closed set. + openset[current.id]=nil + Nopen=Nopen-1 + closedset[current.id]=true + + -- Get neighbour nodes. + local neighbors=self:_NeighbourNodes(current, nodes) + + -- Loop over neighbours. + for _,neighbor in pairs(neighbors) do + + if self:_NotIn(closedset, neighbor.id) then + + local tentative_g_score=g_score[current.id]+self:_DistNodes(current, neighbor) + + if self:_NotIn(openset, neighbor.id) or tentative_g_score < g_score[neighbor.id] then + + came_from[neighbor]=current + + g_score[neighbor.id]=tentative_g_score + f_score[neighbor.id]=g_score[neighbor.id]+self:_HeuristicCost(neighbor, goal) + + if self:_NotIn(openset, neighbor.id) then + -- Add to open set. + openset[neighbor.id]=true + Nopen=Nopen+1 + end + + end + end + end + end + + -- Debug message. + local text=string.format("WARNING: Could NOT find valid path!") + self:E(self.lid..text) + MESSAGE:New(text, 60, "ASTAR"):ToAllIf(self.Debug) + + return nil -- no valid path +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- A* pathfinding helper functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Heuristic "cost" function to go from node A to node B. Default is the distance between the nodes. +-- @param #ASTAR self +-- @param #ASTAR.Node nodeA Node A. +-- @param #ASTAR.Node nodeB Node B. +-- @return #number "Cost" to go from node A to node B. +function ASTAR:_HeuristicCost(nodeA, nodeB) + + -- Counter. + self.ncost=self.ncost+1 + + -- Get chached cost if available. + local cost=nodeA.cost[nodeB.id] + if cost~=nil then + self.ncostcache=self.ncostcache+1 + return cost + end + + local cost=nil + if self.CostFunc then + cost=self.CostFunc(nodeA, nodeB, unpack(self.CostArg)) + else + cost=self:_DistNodes(nodeA, nodeB) + end + + nodeA.cost[nodeB.id]=cost + nodeB.cost[nodeA.id]=cost -- Symmetric problem. + + return cost +end + +--- Check if going from a node to a neighbour is possible. +-- @param #ASTAR self +-- @param #ASTAR.Node node A node. +-- @param #ASTAR.Node neighbor Neighbour node. +-- @return #boolean If true, transition between nodes is possible. +function ASTAR:_IsValidNeighbour(node, neighbor) + + -- Counter. + self.nvalid=self.nvalid+1 + + local valid=node.valid[neighbor.id] + if valid~=nil then + --env.info(string.format("Node %d has valid=%s neighbour %d", node.id, tostring(valid), neighbor.id)) + self.nvalidcache=self.nvalidcache+1 + return valid + end + + local valid=nil + if self.ValidNeighbourFunc then + valid=self.ValidNeighbourFunc(node, neighbor, unpack(self.ValidNeighbourArg)) + else + valid=true + end + + node.valid[neighbor.id]=valid + neighbor.valid[node.id]=valid -- Symmetric problem. + + return valid +end + +--- Calculate 2D distance between two nodes. +-- @param #ASTAR self +-- @param #ASTAR.Node nodeA Node A. +-- @param #ASTAR.Node nodeB Node B. +-- @return #number Distance between nodes in meters. +function ASTAR:_DistNodes(nodeA, nodeB) + return nodeA.coordinate:Get2DDistance(nodeB.coordinate) +end + +--- Function that calculates the lowest F score. +-- @param #ASTAR self +-- @param #table set The set of nodes IDs. +-- @param #number f_score F score. +-- @return #ASTAR.Node Best node. +function ASTAR:_LowestFscore(set, f_score) + + local lowest, bestNode = ASTAR.INF, nil + + for nid,node in pairs(set) do + + local score=f_score[nid] + + if score 20-60MHz -- * ARKUD -> 100-150MHz (canal 1 : 114166, canal 2 : 114333, canal 3 : 114583, canal 4 : 121500, canal 5 : 123100, canal 6 : 124100) AM @@ -7664,7 +10785,7 @@ end function BEACON:RadioBeacon(FileName, Frequency, Modulation, Power, BeaconDuration) self:F({FileName, Frequency, Modulation, Power, BeaconDuration}) local IsValid = false - + -- Check the filename if type(FileName) == "string" then if FileName:find(".ogg") or FileName:find(".wav") then @@ -7677,32 +10798,32 @@ function BEACON:RadioBeacon(FileName, Frequency, Modulation, Power, BeaconDurati if not IsValid then self:E({"File name invalid. Maybe something wrong with the extension ? ", FileName}) end - + -- Check the Frequency if type(Frequency) ~= "number" and IsValid then self:E({"Frequency invalid. ", Frequency}) IsValid = false end Frequency = Frequency * 1000000 -- Conversion to Hz - + -- Check the modulation if Modulation ~= radio.modulation.AM and Modulation ~= radio.modulation.FM and IsValid then --TODO Maybe make this future proof if ED decides to add an other modulation ? self:E({"Modulation is invalid. Use DCS's enum radio.modulation.", Modulation}) IsValid = false end - + -- Check the Power if type(Power) ~= "number" and IsValid then self:E({"Power is invalid. ", Power}) IsValid = false end Power = math.floor(math.abs(Power)) --TODO Find what is the maximum power allowed by DCS and limit power to that - + if IsValid then self:T2({"Activating Beacon on ", Frequency, Modulation}) -- Note that this is looped. I have to give this transmission a unique name, I use the class ID trigger.action.radioTransmission(FileName, self.Positionable:GetPositionVec3(), Modulation, true, Frequency, Power, tostring(self.ID)) - + if BeaconDuration then -- Schedule the stop of the BEACON if asked by the MD SCHEDULER:New( nil, function() @@ -7712,7 +10833,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() @@ -7736,16 +10857,16 @@ function BEACON:_TACANToFrequency(TACANChannel, TACANMode) return nil -- error in arguments end end - + -- This code is largely based on ED's code, in DCS World\Scripts\World\Radio\BeaconTypes.lua, line 137. -- I have no idea what it does but it seems to work local A = 1151 -- 'X', channel >= 64 local B = 64 -- channel >= 64 - + if TACANChannel < 64 then B = 1 end - + if TACANMode == 'Y' then A = 1025 if TACANChannel < 64 then @@ -7756,9 +10877,473 @@ function BEACON:_TACANToFrequency(TACANChannel, TACANMode) A = 962 end 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** - 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 MOOSE.JPG + +--- CONDITON class. +-- @type CONDITION +-- @field #string ClassName Name of the class. +-- @field #string lid Class id string for output to DCS log file. +-- @field #string name Name of the condition. +-- @field #boolean isAny General functions are evaluated as any condition. +-- @field #boolean negateResult Negate result of evaluation. +-- @field #boolean noneResult Boolean that is returned if no condition functions at all were specified. +-- @field #table functionsGen General condition functions. +-- @field #table functionsAny Any condition functions. +-- @field #table functionsAll All condition functions. +-- @field #number functionCounter Running number to determine the unique ID of condition functions. +-- @field #boolean defaultPersist Default persistence of 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 = {}, + functionCounter = 0, + defaultPersist = false, +} + +--- Condition function. +-- @type CONDITION.Function +-- @field #number uid Unique ID of the condition function. +-- @field #string type Type of the condition function: "gen", "any", "all". +-- @field #boolean persistence If `true`, this is persistent. +-- @field #function func Callback function to check for a condition. Must 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.3.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Make FSM. No sure if really necessary. +-- DONE: Option to remove condition functions. +-- DONE: Persistence option for condition functions. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- 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:SetNoneResult(false) + + 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 + +--- Set whether `true` or `false` is returned, if no conditions at all were specified. By default `false` is returned. +-- @param #CONDITION self +-- @param #boolean ReturnValue Returns this boolean. +-- @return #CONDITION self +function CONDITION:SetNoneResult(ReturnValue) + if not ReturnValue then + self.noneResult=false + else + self.noneResult=true + end + return self +end + +--- Set whether condition functions are persistent, *i.e.* are removed. +-- @param #CONDITION self +-- @param #boolean IsPersistent If `true`, condition functions are persistent. +-- @return #CONDITION self +function CONDITION:SetDefaultPersistence(IsPersistent) + self.defaultPersist=IsPersistent + 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.Function Condition function table. +function CONDITION:AddFunction(Function, ...) + + -- Condition function. + local condition=self:_CreateCondition(0, Function, ...) + + -- Add to table. + table.insert(self.functionsGen, condition) + + return condition +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.Function Condition function table. +function CONDITION:AddFunctionAny(Function, ...) + + -- Condition function. + local condition=self:_CreateCondition(1, Function, ...) + + -- Add to table. + table.insert(self.functionsAny, condition) + + return condition +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.Function Condition function table. +function CONDITION:AddFunctionAll(Function, ...) + + -- Condition function. + local condition=self:_CreateCondition(2, Function, ...) + + -- Add to table. + table.insert(self.functionsAll, condition) + + return condition +end + +--- Remove a condition function. +-- @param #CONDITION self +-- @param #CONDITION.Function ConditionFunction The condition function to be removed. +-- @return #CONDITION self +function CONDITION:RemoveFunction(ConditionFunction) + + if ConditionFunction then + + local data=nil + if ConditionFunction.type==0 then + data=self.functionsGen + elseif ConditionFunction.type==1 then + data=self.functionsAny + elseif ConditionFunction.type==2 then + data=self.functionsAll + end + + if data then + for i=#data,1,-1 do + local cf=data[i] --#CONDITION.Function + if cf.uid==ConditionFunction.uid then + self:T(self.lid..string.format("Removed ConditionFunction UID=%d", cf.uid)) + table.remove(data, i) + return self + end + end + end + + end + + return self +end + +--- Remove all non-persistant condition functions. +-- @param #CONDITION self +-- @return #CONDITION self +function CONDITION:RemoveNonPersistant() + + for i=#self.functionsGen,1,-1 do + local cf=self.functionsGen[i] --#CONDITION.Function + if not cf.persistence then + table.remove(self.functionsGen, i) + end + end + + for i=#self.functionsAll,1,-1 do + local cf=self.functionsAll[i] --#CONDITION.Function + if not cf.persistence then + table.remove(self.functionsAll, i) + end + end + + for i=#self.functionsAny,1,-1 do + local cf=self.functionsAny[i] --#CONDITION.Function + if not cf.persistence then + table.remove(self.functionsAny, i) + end + end + + 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 + return self.noneResult + 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 + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Private Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- 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 function object. +-- @param #CONDITION self +-- @param #number Ftype Function type: 0=Gen, 1=All, 2=Any. +-- @param #function Function The function to call. +-- @param ... (Optional) Parameters passed to the function (if any). +-- @return #CONDITION.Function Condition function. +function CONDITION:_CreateCondition(Ftype, Function, ...) + + -- Increase counter. + self.functionCounter=self.functionCounter+1 + + local condition={} --#CONDITION.Function + + condition.uid=self.functionCounter + condition.type=Ftype or 0 + condition.persistence=self.defaultPersist + condition.func=Function + condition.arg={} + if arg then + condition.arg=arg + end + + return condition +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Global Condition Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Condition to check if time is greater than a given threshold time. +-- @param #number Time Time in seconds. +-- @param #boolean Absolute If `true`, abs. mission time from `timer.getAbsTime()` is checked. Default is relative mission time from `timer.getTime()`. +-- @return #boolean Returns `true` if time is greater than give the time. +function CONDITION.IsTimeGreater(Time, Absolute) + + local Tnow=nil + + if Absolute then + Tnow=timer.getAbsTime() + else + Tnow=timer.getTime() + end + + if Tnow>Time then + return true + else + return false + end + + return nil +end + +--- Function that returns `true` (success) with a certain probability. For example, if you specify `Probability=80` there is an 80% chance that `true` is returned. +-- Technically, a random number between 0 and 100 is created. If the given success probability is less then this number, `true` is returned. +-- @param #number Probability Success probability in percent. Default 50 %. +-- @return #boolean Returns `true` for success and `false` otherwise. +function CONDITION.IsRandomSuccess(Probability) + + Probability=Probability or 50 + + -- Create some randomness. + math.random() + math.random() + math.random() + + -- Number between 0 and 100. + local N=math.random()*100 + + if N0 then self:ScheduleOnce(Delay, USERFLAG.Set, self, Number) else --env.info(string.format("Setting flag \"%s\" to %d at T=%.1f", self.UserFlagName, Number, timer.getTime())) trigger.action.setUserFlag( self.UserFlagName, Number ) end - + return self - end + end - --- Get the userflag Number. -- @param #USERFLAG self -- @return #number Number The number value to be checked if it is the same as the userflag. @@ -7846,12 +11430,10 @@ do -- UserFlag -- local BlueVictoryValue = BlueVictory:Get() -- Get the UserFlag VictoryBlue value. -- function USERFLAG:Get() --R2.3 - + return trigger.misc.getUserFlag( self.UserFlagName ) - end + end - - --- Check if the userflag has a value of Number. -- @param #USERFLAG self -- @param #number Number The number value to be checked if it is the same as the userflag. @@ -7862,21 +11444,21 @@ do -- UserFlag -- return "Blue has won" -- end function USERFLAG:Is( Number ) --R2.3 - + return trigger.misc.getUserFlag( self.UserFlagName ) == Number - - end + + end end--- **Core** - Provides a handy means to create messages and reports. -- -- === --- +-- -- ## Features: --- +-- -- * Create text blocks that are formatted. -- * Create automatic indents. -- * Variate the delimiters between reporting lines. --- +-- -- === -- -- ### Authors: FlightControl : Design & Programming @@ -7884,7 +11466,6 @@ end--- **Core** - Provides a handy means to create messages and reports. -- @module Core.Report -- @image Core_Report.JPG - --- @type REPORT -- @extends Core.Base#BASE @@ -7905,7 +11486,7 @@ function REPORT:New( Title ) self.Report = {} - self:SetTitle( Title or "" ) + self:SetTitle( Title or "" ) self:SetIndent( 3 ) return self @@ -7914,28 +11495,26 @@ end --- Has the REPORT Text? -- @param #REPORT self -- @return #boolean -function REPORT:HasText() --R2.1 - +function REPORT:HasText() -- R2.1 + return #self.Report > 0 end - --- Set indent of a REPORT. -- @param #REPORT self -- @param #number Indent -- @return #REPORT -function REPORT:SetIndent( Indent ) --R2.1 +function REPORT:SetIndent( Indent ) -- R2.1 self.Indent = Indent return self end - --- Add a new line to a REPORT. -- @param #REPORT self -- @param #string Text -- @return #REPORT function REPORT:Add( Text ) - self.Report[#self.Report+1] = Text + self.Report[#self.Report + 1] = Text return self end @@ -7945,17 +11524,17 @@ end -- @param #string Separator (optional) The start of each report line can begin with an optional separator character. This can be a "-", or "#", or "*". You're free to choose what you find the best. -- @return #REPORT function REPORT:AddIndent( Text, Separator ) - self.Report[#self.Report+1] = ( ( Separator and Separator .. string.rep( " ", self.Indent - 1 ) ) or string.rep(" ", self.Indent ) ) .. Text:gsub("\n","\n"..string.rep( " ", self.Indent ) ) + self.Report[#self.Report + 1] = ((Separator and Separator .. string.rep( " ", self.Indent - 1 )) or string.rep( " ", self.Indent )) .. Text:gsub( "\n", "\n" .. string.rep( " ", self.Indent ) ) return self end ---- Produces the text of the report, taking into account an optional delimeter, which is \n by default. +--- Produces the text of the report, taking into account an optional delimiter, which is \n by default. -- @param #REPORT self -- @param #string Delimiter (optional) A delimiter text. -- @return #string The report text. function REPORT:Text( Delimiter ) Delimiter = Delimiter or "\n" - local ReportText = ( self.Title ~= "" and self.Title .. Delimiter or self.Title ) .. table.concat( self.Report, Delimiter ) or "" + local ReportText = (self.Title ~= "" and self.Title .. Delimiter or self.Title) .. table.concat( self.Report, Delimiter ) or "" return ReportText end @@ -7964,7 +11543,7 @@ end -- @param #string Title The title of the report. -- @return #REPORT function REPORT:SetTitle( Title ) - self.Title = Title + self.Title = Title return self end @@ -7979,39 +11558,39 @@ end -- === -- -- ## Features: --- +-- -- * Schedule functions over time, --- * optionally in an optional specified time interval, --- * optionally **repeating** with a specified time repeat interval, --- * optionally **randomizing** with a specified time interval randomization factor, --- * optionally **stop** the repeating after a specified time interval. +-- * optionally in an optional specified time interval, +-- * optionally **repeating** with a specified time repeat interval, +-- * optionally **randomizing** with a specified time interval randomization factor, +-- * optionally **stop** the repeating after a specified time interval. -- -- === --- +-- -- # 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) -- -- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) --- +-- -- === --- +-- -- # YouTube Channel --- +-- -- ### [SCHEDULER YouTube Channel (none)]() --- +-- -- === -- --- ### Contributions: --- +-- ### Contributions: +-- -- * FlightControl : Concept & Testing --- --- ### Authors: --- +-- +-- ### Authors: +-- -- * FlightControl : Design & Programming --- +-- -- === -- -- @module Core.Scheduler @@ -8024,62 +11603,61 @@ end -- @field #boolean ShowTrace Trace info if true. -- @extends Core.Base#BASE - --- Creates and handles schedules over time, which allow to execute code at specific time intervals with randomization. --- +-- -- A SCHEDULER can manage **multiple** (repeating) schedules. Each planned or executing schedule has a unique **ScheduleID**. -- The ScheduleID is returned when the method @{#SCHEDULER.Schedule}() is called. -- It is recommended to store the ScheduleID in a variable, as it is used in the methods @{SCHEDULER.Start}() and @{SCHEDULER.Stop}(), -- which can start and stop specific repeating schedules respectively within a SCHEDULER object. -- -- ## SCHEDULER constructor --- +-- -- The SCHEDULER class is quite easy to use, but note that the New constructor has variable parameters: --- +-- -- The @{#SCHEDULER.New}() method returns 2 variables: --- +-- -- 1. The SCHEDULER object reference. -- 2. The first schedule planned in the SCHEDULER object. --- +-- -- To clarify the different appliances, lets have a look at the following examples: --- +-- -- ### Construct a SCHEDULER object without a persistent schedule. --- +-- -- * @{#SCHEDULER.New}( nil ): Setup a new SCHEDULER object, which is persistently executed after garbage collection. --- +-- -- MasterObject = SCHEDULER:New() -- SchedulerID = MasterObject:Schedule( nil, ScheduleFunction, {} ) --- +-- -- The above example creates a new MasterObject, but does not schedule anything. -- A separate schedule is created by using the MasterObject using the method :Schedule..., which returns a ScheduleID --- +-- -- ### Construct a SCHEDULER object without a volatile schedule, but volatile to the Object existence... --- --- * @{#SCHEDULER.New}( Object ): Setup a new SCHEDULER object, which is linked to the Object. When the Object is nillified or destroyed, the SCHEDULER object will also be destroyed and stopped after garbage collection. --- +-- +-- * @{#SCHEDULER.New}( Object ): Setup a new SCHEDULER object, which is linked to the Object. When the Object is set to nil or destroyed, the SCHEDULER object will also be destroyed and stopped after garbage collection. +-- -- ZoneObject = ZONE:New( "ZoneName" ) -- MasterObject = SCHEDULER:New( ZoneObject ) -- SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {} ) -- ... -- ZoneObject = nil -- garbagecollect() --- +-- -- The above example creates a new MasterObject, but does not schedule anything, and is bound to the existence of ZoneObject, which is a ZONE. -- A separate schedule is created by using the MasterObject using the method :Schedule()..., which returns a ScheduleID -- Later in the logic, the ZoneObject is put to nil, and garbage is collected. -- As a result, the MasterObject will cancel any planned schedule. --- +-- -- ### Construct a SCHEDULER object with a persistent schedule. --- +-- -- * @{#SCHEDULER.New}( nil, Function, FunctionArguments, Start, ... ): Setup a new persistent SCHEDULER object, and start a new schedule for the Function with the defined FunctionArguments according the Start and sequent parameters. --- +-- -- MasterObject, SchedulerID = SCHEDULER:New( nil, ScheduleFunction, {} ) --- +-- -- The above example creates a new MasterObject, and does schedule the first schedule as part of the call. -- Note that 2 variables are returned here: MasterObject, ScheduleID... --- +-- -- ### Construct a SCHEDULER object without a schedule, but volatile to the Object existence... --- +-- -- * @{#SCHEDULER.New}( Object, Function, FunctionArguments, Start, ... ): Setup a new SCHEDULER object, linked to Object, and start a new schedule for the Function with the defined FunctionArguments according the Start and sequent parameters. -- -- ZoneObject = ZONE:New( "ZoneName" ) @@ -8088,13 +11666,13 @@ end -- ... -- ZoneObject = nil -- garbagecollect() --- +-- -- The above example creates a new MasterObject, and schedules a method call (ScheduleFunction), -- and is bound to the existence of ZoneObject, which is a ZONE object (ZoneObject). -- Both a MasterObject and a SchedulerID variable are returned. -- Later in the logic, the ZoneObject is put to nil, and garbage is collected. -- As a result, the MasterObject will cancel the planned schedule. --- +-- -- ## SCHEDULER timer stopping and (re-)starting. -- -- The SCHEDULER can be stopped and restarted with the following methods: @@ -8109,70 +11687,70 @@ end -- MasterObject:Stop( SchedulerID ) -- ... -- MasterObject:Start( SchedulerID ) --- +-- -- The above example creates a new MasterObject, and does schedule the first schedule as part of the call. --- Note that 2 variables are returned here: MasterObject, ScheduleID... --- Later in the logic, the repeating schedule with SchedulerID is stopped. --- A bit later, the repeating schedule with SchedulerId is (re)-started. --- +-- Note that 2 variables are returned here: MasterObject, ScheduleID... +-- Later in the logic, the repeating schedule with SchedulerID is stopped. +-- A bit later, the repeating schedule with SchedulerId is (re)-started. +-- -- ## Create a new schedule --- --- With the method @{#SCHEDULER.Schedule}() a new time event can be scheduled. +-- +-- With the method @{#SCHEDULER.Schedule}() a new time event can be scheduled. -- This method is used by the :New() constructor when a new schedule is planned. --- +-- -- Consider the following code fragment of the SCHEDULER object creation. --- +-- -- ZoneObject = ZONE:New( "ZoneName" ) -- MasterObject = SCHEDULER:New( ZoneObject ) --- --- Several parameters can be specified that influence the behaviour of a Schedule. --- +-- +-- Several parameters can be specified that influence the behavior of a Schedule. +-- -- ### A single schedule, immediately executed --- +-- -- SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {} ) --- --- The above example schedules a new ScheduleFunction call to be executed asynchronously, within milleseconds ... --- +-- +-- The above example schedules a new ScheduleFunction call to be executed asynchronously, within milliseconds ... +-- -- ### A single schedule, planned over time --- +-- -- SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {}, 10 ) --- +-- -- The above example schedules a new ScheduleFunction call to be executed asynchronously, within 10 seconds ... --- +-- -- ### A schedule with a repeating time interval, planned over time --- +-- -- SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {}, 10, 60 ) --- --- The above example schedules a new ScheduleFunction call to be executed asynchronously, within 10 seconds, +-- +-- The above example schedules a new ScheduleFunction call to be executed asynchronously, within 10 seconds, -- and repeating 60 every seconds ... --- +-- -- ### A schedule with a repeating time interval, planned over time, with time interval randomization --- +-- -- SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {}, 10, 60, 0.5 ) --- --- The above example schedules a new ScheduleFunction call to be executed asynchronously, within 10 seconds, +-- +-- The above example schedules a new ScheduleFunction call to be executed asynchronously, within 10 seconds, -- and repeating 60 seconds, with a 50% time interval randomization ... --- So the repeating time interval will be randomized using the **0.5**, --- and will calculate between **60 - ( 60 * 0.5 )** and **60 + ( 60 * 0.5 )** for each repeat, +-- So the repeating time interval will be randomized using the **0.5**, +-- and will calculate between **60 - ( 60 * 0.5 )** and **60 + ( 60 * 0.5 )** for each repeat, -- which is in this example between **30** and **90** seconds. --- +-- -- ### A schedule with a repeating time interval, planned over time, with time interval randomization, and stop after a time interval --- +-- -- SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {}, 10, 60, 0.5, 300 ) --- --- The above example schedules a new ScheduleFunction call to be executed asynchronously, within 10 seconds, +-- +-- The above example schedules a new ScheduleFunction call to be executed asynchronously, within 10 seconds, -- The schedule will repeat every 60 seconds. --- So the repeating time interval will be randomized using the **0.5**, --- and will calculate between **60 - ( 60 * 0.5 )** and **60 + ( 60 * 0.5 )** for each repeat, +-- So the repeating time interval will be randomized using the **0.5**, +-- and will calculate between **60 - ( 60 * 0.5 )** and **60 + ( 60 * 0.5 )** for each repeat, -- which is in this example between **30** and **90** seconds. -- The schedule will stop after **300** seconds. --- +-- -- @field #SCHEDULER SCHEDULER = { - ClassName = "SCHEDULER", - Schedules = {}, - MasterObject = nil, - ShowTrace = nil, + ClassName = "SCHEDULER", + Schedules = {}, + MasterObject = nil, + ShowTrace = nil, } --- SCHEDULER constructor. @@ -8187,15 +11765,15 @@ SCHEDULER = { -- @return #SCHEDULER self. -- @return #table The ScheduleID of the planned schedule. function SCHEDULER:New( MasterObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop ) - + local self = BASE:Inherit( self, BASE:New() ) -- #SCHEDULER self:F2( { Start, Repeat, RandomizeFactor, Stop } ) local ScheduleID = nil - + self.MasterObject = MasterObject self.ShowTrace = false - + if SchedulerFunction then ScheduleID = self:Schedule( MasterObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop, 3 ) end @@ -8211,48 +11789,47 @@ end -- @param #number Start Specifies the amount of seconds that will be waited before the scheduling is started, and the event function is called. -- @param #number Repeat Specifies the time interval in seconds when the scheduler will call the event function. -- @param #number RandomizeFactor Specifies a randomization factor between 0 and 1 to randomize the Repeat. --- @param #number Stop Time interval in seconds after which the scheduler will be stoppe. +-- @param #number Stop Time interval in seconds after which the scheduler will be stopped. -- @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 } ) -- Debug info. local ObjectName = "-" - if MasterObject and MasterObject.ClassName and MasterObject.ClassID then + if MasterObject and MasterObject.ClassName and MasterObject.ClassID then ObjectName = MasterObject.ClassName .. MasterObject.ClassID end - self:F3( { "Schedule :", ObjectName, tostring( MasterObject ), Start, Repeat, RandomizeFactor, Stop } ) - + self:F3( { "Schedule :", ObjectName, tostring( MasterObject ), Start, Repeat, RandomizeFactor, Stop } ) + -- Set master object. self.MasterObject = MasterObject - + -- Add schedule. - local ScheduleID = _SCHEDULEDISPATCHER:AddSchedule( - self, - SchedulerFunction, - SchedulerArguments, - Start, - Repeat, - RandomizeFactor, - Stop, - TraceLevel or 3, - Fsm - ) - - self.Schedules[#self.Schedules+1] = ScheduleID + local ScheduleID = _SCHEDULEDISPATCHER:AddSchedule( self, + SchedulerFunction, + SchedulerArguments, + Start, + Repeat, + RandomizeFactor, + Stop, + TraceLevel or 3, + Fsm + ) + + self.Schedules[#self.Schedules + 1] = ScheduleID return ScheduleID 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))) + self:T( string.format( "Starting scheduler ID=%s", tostring( ScheduleID ) ) ) _SCHEDULEDISPATCHER:Start( self, ScheduleID ) end @@ -8261,7 +11838,7 @@ end -- @param #string ScheduleID (Optional) The ScheduleID of the planned (repeating) schedule. function SCHEDULER:Stop( ScheduleID ) self:F3( { ScheduleID } ) - self:T(string.format("Stopping scheduler ID=%s", tostring(ScheduleID))) + self:T( string.format( "Stopping scheduler ID=%s", tostring( ScheduleID ) ) ) _SCHEDULEDISPATCHER:Stop( self, ScheduleID ) end @@ -8270,15 +11847,15 @@ end -- @param #string ScheduleID (optional) The ScheduleID of the planned (repeating) schedule. function SCHEDULER:Remove( ScheduleID ) self:F3( { ScheduleID } ) - self:T(string.format("Removing scheduler ID=%s", tostring(ScheduleID))) + self:T( string.format( "Removing scheduler ID=%s", tostring( ScheduleID ) ) ) _SCHEDULEDISPATCHER:RemoveSchedule( self, ScheduleID ) end --- Clears all pending schedules. -- @param #SCHEDULER self function SCHEDULER:Clear() - self:F3( ) - self:T(string.format("Clearing scheduler")) + self:F3() + self:T( string.format( "Clearing scheduler" ) ) _SCHEDULEDISPATCHER:Clear( self ) end @@ -8293,39 +11870,39 @@ end function SCHEDULER:NoTrace() _SCHEDULEDISPATCHER:NoTrace( self ) end ---- **Core** -- SCHEDULEDISPATCHER dispatches the different schedules. --- +--- **Core** - SCHEDULEDISPATCHER dispatches the different schedules. +-- -- === --- +-- -- Takes care of the creation and dispatching of scheduled functions for SCHEDULER objects. --- +-- -- This class is tricky and needs some thorough explanation. -- SCHEDULE classes are used to schedule functions for objects, or as persistent objects. -- The SCHEDULEDISPATCHER class ensures that: --- +-- -- - Scheduled functions are planned according the SCHEDULER object parameters. -- - Scheduled functions are repeated when requested, according the SCHEDULER object parameters. -- - Scheduled functions are automatically removed when the schedule is finished, according the SCHEDULER object parameters. --- +-- -- The SCHEDULEDISPATCHER class will manage SCHEDULER object in memory during garbage collection: --- +-- -- - When a SCHEDULER object is not attached to another object (that is, it's first :Schedule() parameter is nil), then the SCHEDULER object is _persistent_ within memory. -- - When a SCHEDULER object *is* attached to another object, then the SCHEDULER object is _not persistent_ within memory after a garbage collection! --- --- The none persistency of SCHEDULERS attached to objects is required to allow SCHEDULER objects to be garbage collectged, when the parent object is also desroyed or nillified and garbage collected. --- Even when there are pending timer scheduled functions to be executed for the SCHEDULER object, +-- +-- The non-persistency of SCHEDULERS attached to objects is required to allow SCHEDULER objects to be garbage collected when the parent object is destroyed, or set to nil and garbage collected. +-- Even when there are pending timer scheduled functions to be executed for the SCHEDULER object, -- these will not be executed anymore when the SCHEDULER object has been destroyed. --- +-- -- The SCHEDULEDISPATCHER allows multiple scheduled functions to be planned and executed for one SCHEDULER object. -- The SCHEDULER object therefore keeps a table of "CallID's", which are returned after each planning of a new scheduled function by the SCHEDULEDISPATCHER. --- The SCHEDULER object plans new scheduled functions through the @{Core.Scheduler#SCHEDULER.Schedule}() method. +-- The SCHEDULER object plans new scheduled functions through the @{Core.Scheduler#SCHEDULER.Schedule}() method. -- The Schedule() method returns the CallID that is the reference ID for each planned schedule. --- +-- -- === --- +-- -- ### Contributions: - -- ### Authors: FlightControl : Design & Programming --- +-- -- @module Core.ScheduleDispatcher -- @image Core_Schedule_Dispatcher.JPG @@ -8333,7 +11910,7 @@ end -- @type SCHEDULEDISPATCHER -- @field #string ClassName Name of the class. -- @field #number CallID Call ID counter. --- @field #table PersistentSchedulers Persistant schedulers. +-- @field #table PersistentSchedulers Persistent schedulers. -- @field #table ObjectSchedulers Schedulers that only exist as long as the master object exists. -- @field #table Schedule Meta table setmetatable( {}, { __mode = "k" } ). -- @extends Core.Base#BASE @@ -8341,11 +11918,11 @@ end --- The SCHEDULEDISPATCHER structure -- @type SCHEDULEDISPATCHER SCHEDULEDISPATCHER = { - ClassName = "SCHEDULEDISPATCHER", - CallID = 0, - PersistentSchedulers = {}, - ObjectSchedulers = {}, - Schedule = nil, + ClassName = "SCHEDULEDISPATCHER", + CallID = 0, + PersistentSchedulers = {}, + ObjectSchedulers = {}, + Schedule = nil, } --- Player data table holding all important parameters of each player. @@ -8353,7 +11930,7 @@ SCHEDULEDISPATCHER = { -- @field #function Function The schedule function to be called. -- @field #table Arguments Schedule function arguments. -- @field #number Start Start time in seconds. --- @field #number Repeat Repeat time intervall in seconds. +-- @field #number Repeat Repeat time interval in seconds. -- @field #number Randomize Randomization factor [0,1]. -- @field #number Stop Stop time in seconds. -- @field #number StartTime Time in seconds when the scheduler is created. @@ -8372,7 +11949,7 @@ end --- Add a Schedule to the ScheduleDispatcher. -- The development of this method was really tidy. --- It is constructed as such that a garbage collection is executed on the weak tables, when the Scheduler is nillified. +-- It is constructed as such that a garbage collection is executed on the weak tables, when the Scheduler is set to nil. -- Nothing of this code should be modified without testing it thoroughly. -- @param #SCHEDULEDISPATCHER self -- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object. @@ -8380,7 +11957,7 @@ end -- @param #table ScheduleArguments Table of arguments passed to the ScheduleFunction. -- @param #number Start Start time in seconds. -- @param #number Repeat Repeat interval in seconds. --- @param #number Randomize Radomization factor [0,1]. +-- @param #number Randomize Randomization factor [0,1]. -- @param #number Stop Stop time in seconds. -- @param #number TraceLevel Trace level [0,3]. -- @param Core.Fsm#FSM Fsm Finite state model. @@ -8390,39 +11967,40 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr -- Increase counter. self.CallID = self.CallID + 1 - + -- Create ID. - local CallID = self.CallID .. "#" .. ( Scheduler.MasterObject and Scheduler.MasterObject.GetClassNameAndID and Scheduler.MasterObject:GetClassNameAndID() or "" ) or "" - - self:T2(string.format("Adding schedule #%d CallID=%s", self.CallID, CallID)) + local CallID = self.CallID .. "#" .. (Scheduler.MasterObject and Scheduler.MasterObject.GetClassNameAndID and Scheduler.MasterObject:GetClassNameAndID() or "") or "" + + self:T2( string.format( "Adding schedule #%d CallID=%s", self.CallID, CallID ) ) -- Initialize PersistentSchedulers self.PersistentSchedulers = self.PersistentSchedulers or {} -- Initialize the ObjectSchedulers array, which is a weakly coupled table. -- If the object used as the key is nil, then the garbage collector will remove the item from the Functions array. - self.ObjectSchedulers = self.ObjectSchedulers or setmetatable( {}, { __mode = "v" } ) - + 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) } ) + 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 - + self.Schedule = self.Schedule or setmetatable( {}, { __mode = "k" } ) self.Schedule[Scheduler] = self.Schedule[Scheduler] or {} - self.Schedule[Scheduler][CallID] = {} --#SCHEDULEDISPATCHER.ScheduleData + self.Schedule[Scheduler][CallID] = {} -- #SCHEDULEDISPATCHER.ScheduleData self.Schedule[Scheduler][CallID].Function = ScheduleFunction self.Schedule[Scheduler][CallID].Arguments = ScheduleArguments self.Schedule[Scheduler][CallID].StartTime = timer.getTime() + ( Start or 0 ) - self.Schedule[Scheduler][CallID].Start = Start + 0.1 + self.Schedule[Scheduler][CallID].Start = Start + 0.001 self.Schedule[Scheduler][CallID].Repeat = Repeat or 0 self.Schedule[Scheduler][CallID].Randomize = Randomize or 0 self.Schedule[Scheduler][CallID].Stop = Stop - - + -- This section handles the tracing of the scheduled calls. -- Because these calls will be executed with a delay, we inspect the place where these scheduled calls are initiated. -- The Info structure contains the output of the debug.getinfo() calls, which inspects the call stack for the function name, line number and source name. @@ -8444,10 +12022,10 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr -- Therefore, in the call stack, at the TraceLevel these functions are mentioned as "tail calls", and the Info.name field will be nil as a result. -- To obtain the correct function name for FSM object calls, the function is mentioned in the call stack at a higher stack level. -- So when function name stored in Info.name is nil, then I inspect the function name within the call stack one level higher. - -- So this little piece of code does its magic wonderfully, preformance overhead is neglectible, as scheduled calls don't happen that often. + -- So this little piece of code does its magic wonderfully, performance overhead is negligible, as scheduled calls don't happen that often. local Info = {} - + if debug then TraceLevel = TraceLevel or 2 Info = debug.getinfo( TraceLevel, "nlS" ) @@ -8461,7 +12039,7 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr --- Function passed to the DCS timer.scheduleFunction() self.Schedule[Scheduler][CallID].CallHandler = function( Params ) - + local CallID = Params.CallID local Info = Params.Info or {} local Source = Info.source or "?" @@ -8475,27 +12053,27 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr end return errmsg end - - -- Get object or persistant scheduler object. - local Scheduler = self.ObjectSchedulers[CallID] --Core.Scheduler#SCHEDULER + + -- Get object or persistent scheduler object. + local Scheduler = self.ObjectSchedulers[CallID] -- Core.Scheduler#SCHEDULER if not Scheduler then Scheduler = self.PersistentSchedulers[CallID] end - - --self:T3( { Scheduler = Scheduler } ) - + + -- self:T3( { Scheduler = Scheduler } ) + if Scheduler then - local MasterObject = tostring(Scheduler.MasterObject) - + local MasterObject = tostring( Scheduler.MasterObject ) + -- Schedule object. - local Schedule = self.Schedule[Scheduler][CallID] --#SCHEDULEDISPATCHER.ScheduleData - - --self:T3( { Schedule = Schedule } ) + local Schedule = self.Schedule[Scheduler][CallID] -- #SCHEDULEDISPATCHER.ScheduleData + + -- self:T3( { Schedule = Schedule } ) - local SchedulerObject = Scheduler.MasterObject --Scheduler.SchedulerObject Now is this the Maste or Scheduler object? + local SchedulerObject = Scheduler.MasterObject -- Scheduler.SchedulerObject Now is this the Master or Scheduler object? local ShowTrace = Scheduler.ShowTrace - + local ScheduleFunction = Schedule.Function local ScheduleArguments = Schedule.Arguments or {} local Start = Schedule.Start @@ -8503,17 +12081,17 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr local Randomize = Schedule.Randomize or 0 local Stop = Schedule.Stop or 0 local ScheduleID = Schedule.ScheduleID - - - local Prefix = ( Repeat == 0 ) and "--->" or "+++>" - + + local Prefix = (Repeat == 0) and "--->" or "+++>" + local Status, Result - --self:E( { SchedulerObject = SchedulerObject } ) + -- self:E( { SchedulerObject = SchedulerObject } ) if SchedulerObject then local function Timer() 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 ) @@ -8522,40 +12100,39 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr if ShowTrace then self:T( Prefix .. Name .. ":" .. Line .. " (" .. Source .. ")" ) end - return ScheduleFunction( unpack( ScheduleArguments ) ) + return ScheduleFunction( unpack( ScheduleArguments ) ) end Status, Result = xpcall( Timer, ErrorHandler ) end - + local CurrentTime = timer.getTime() local StartTime = Schedule.StartTime -- Debug info. - self:F3( { CallID=CallID, ScheduleID=ScheduleID, Master = MasterObject, CurrentTime = CurrentTime, StartTime = StartTime, Start = Start, Repeat = Repeat, Randomize = Randomize, Stop = Stop } ) - - - if Status and (( Result == nil ) or ( Result and Result ~= false ) ) then - - if Repeat ~= 0 and ( ( Stop == 0 ) or ( Stop ~= 0 and CurrentTime <= StartTime + Stop ) ) then - local ScheduleTime = CurrentTime + Repeat + math.random(- ( Randomize * Repeat / 2 ), ( Randomize * Repeat / 2 )) + 0.0001 -- Accuracy - --self:T3( { Repeat = CallID, CurrentTime, ScheduleTime, ScheduleArguments } ) + self:F3( { CallID = CallID, ScheduleID = ScheduleID, Master = MasterObject, CurrentTime = CurrentTime, StartTime = StartTime, Start = Start, Repeat = Repeat, Randomize = Randomize, Stop = Stop } ) + + if Status and ((Result == nil) or (Result and Result ~= false)) then + + if Repeat ~= 0 and ((Stop == 0) or (Stop ~= 0 and CurrentTime <= StartTime + Stop)) then + local ScheduleTime = CurrentTime + Repeat + math.random( -(Randomize * Repeat / 2), (Randomize * Repeat / 2) ) + 0.0001 -- Accuracy + -- self:T3( { Repeat = CallID, CurrentTime, ScheduleTime, ScheduleArguments } ) return ScheduleTime -- returns the next time the function needs to be called. else self:Stop( Scheduler, CallID ) end - + else self:Stop( Scheduler, CallID ) end else self:I( "<<<>" .. Name .. ":" .. Line .. " (" .. Source .. ")" ) end - + return nil end - + self:Start( Scheduler, CallID, Info ) - + return CallID end @@ -8579,67 +12156,67 @@ end -- @param #string Info (Optional) Debug info. function SCHEDULEDISPATCHER:Start( Scheduler, CallID, Info ) self:F2( { Start = CallID, Scheduler = Scheduler } ) - + if CallID then - - local Schedule = self.Schedule[Scheduler][CallID] --#SCHEDULEDISPATCHER.ScheduleData - + + local Schedule = self.Schedule[Scheduler][CallID] -- #SCHEDULEDISPATCHER.ScheduleData + -- Only start when there is no ScheduleID defined! -- This prevents to "Start" the scheduler twice with the same CallID... if not Schedule.ScheduleID then - + -- Current time in seconds. - local Tnow=timer.getTime() - - Schedule.StartTime = Tnow -- Set the StartTime field to indicate when the scheduler started. - + local Tnow = timer.getTime() + + Schedule.StartTime = Tnow -- Set the StartTime field to indicate when the scheduler started. + -- Start DCS schedule function https://wiki.hoggitworld.com/view/DCS_func_scheduleFunction - Schedule.ScheduleID = timer.scheduleFunction(Schedule.CallHandler, { CallID = CallID, Info = Info }, Tnow + Schedule.Start) - - self:T(string.format("Starting scheduledispatcher Call ID=%s ==> Schedule ID=%s", tostring(CallID), tostring(Schedule.ScheduleID))) + Schedule.ScheduleID = timer.scheduleFunction( Schedule.CallHandler, { CallID = CallID, Info = Info }, Tnow + Schedule.Start ) + + self:T( string.format( "Starting SCHEDULEDISPATCHER Call ID=%s ==> Schedule ID=%s", tostring( CallID ), tostring( Schedule.ScheduleID ) ) ) end - + else - + -- Recursive. for CallID, Schedule in pairs( self.Schedule[Scheduler] or {} ) do self:Start( Scheduler, CallID, Info ) -- Recursive end - + end 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 } ) if CallID then - - local Schedule = self.Schedule[Scheduler][CallID] --#SCHEDULEDISPATCHER.ScheduleData - + + local Schedule = self.Schedule[Scheduler][CallID] -- #SCHEDULEDISPATCHER.ScheduleData + -- Only stop when there is a ScheduleID defined for the CallID. So, when the scheduler was stopped before, do nothing. if Schedule.ScheduleID then - - self:T(string.format("scheduledispatcher stopping scheduler CallID=%s, ScheduleID=%s", tostring(CallID), tostring(Schedule.ScheduleID))) - + + self:T( string.format( "SCHEDULEDISPATCHER stopping scheduler CallID=%s, ScheduleID=%s", tostring( CallID ), tostring( Schedule.ScheduleID ) ) ) + -- Remove schedule function https://wiki.hoggitworld.com/view/DCS_func_removeFunction - timer.removeFunction(Schedule.ScheduleID) - + timer.removeFunction( Schedule.ScheduleID ) + Schedule.ScheduleID = nil - + else - self:T(string.format("Error no ScheduleID for CallID=%s", tostring(CallID))) + self:T( string.format( "Error no ScheduleID for CallID=%s", tostring( CallID ) ) ) end - + else - + for CallID, Schedule in pairs( self.Schedule[Scheduler] or {} ) do self:Stop( Scheduler, CallID ) -- Recursive end - + end end @@ -8654,7 +12231,7 @@ function SCHEDULEDISPATCHER:Clear( Scheduler ) end end ---- Shopw tracing info. +--- Show tracing info. -- @param #SCHEDULEDISPATCHER self -- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object. function SCHEDULEDISPATCHER:ShowTrace( Scheduler ) @@ -8686,7 +12263,7 @@ end -- ![Objects](..\Presentations\EVENT\Dia2.JPG) -- -- Within a running mission, various DCS events occur. Units are dynamically created, crash, die, shoot stuff, get hit etc. --- This module provides a mechanism to dispatch those events occuring within your running mission, to the different objects orchestrating your mission. +-- This module provides a mechanism to dispatch those events occurring within your running mission, to the different objects orchestrating your mission. -- -- ![Objects](..\Presentations\EVENT\Dia3.JPG) -- @@ -8704,11 +12281,11 @@ end -- -- ![Objects](..\Presentations\EVENT\Dia5.JPG) -- --- There are 5 levels of kind of objects that the _EVENTDISPATCHER services: +-- There are 5 types/levels of objects that the _EVENTDISPATCHER services: -- -- * _DATABASE object: The core of the MOOSE objects. Any object that is created, deleted or updated, is done in this database. --- * SET_ derived classes: Subsets of the _DATABASE object. These subsets are updated by the _EVENTDISPATCHER as the second priority. --- * UNIT objects: UNIT objects can subscribe to DCS events. Each DCS event will be directly published to teh subscribed UNIT object. +-- * SET_ derived classes: These are subsets of the global _DATABASE object (an instance of @{Core.Database#DATABASE}). These subsets are updated by the _EVENTDISPATCHER as the second priority. +-- * UNIT objects: UNIT objects can subscribe to DCS events. Each DCS event will be directly published to the subscribed UNIT object. -- * GROUP objects: GROUP objects can subscribe to DCS events. Each DCS event will be directly published to the subscribed GROUP object. -- * Any other object: Various other objects can subscribe to DCS events. Each DCS event triggered will be published to each subscribed object. -- @@ -8724,7 +12301,7 @@ end -- -- ![Objects](..\Presentations\EVENT\Dia8.JPG) -- --- The actual event subscribing and handling is not facilitated through the _EVENTDISPATCHER, but it is done through the @{BASE} class, @{UNIT} class and @{GROUP} class. +-- The actual event subscribing and handling is not facilitated through the _EVENTDISPATCHER, but it is done through the @{Core.Base#BASE} class, @{Wrapper.Unit#UNIT} class and @{Wrapper.Group#GROUP} class. -- The _EVENTDISPATCHER is a component that is quietly working in the background of MOOSE. -- -- ![Objects](..\Presentations\EVENT\Dia9.JPG) @@ -8920,6 +12497,18 @@ EVENTS = { TriggerZone = world.event.S_EVENT_TRIGGER_ZONE or -1, LandingQualityMark = world.event.S_EVENT_LANDING_QUALITY_MARK or -1, BDA = world.event.S_EVENT_BDA or -1, + -- Added with DCS 2.8.0 + AIAbortMission = world.event.S_EVENT_AI_ABORT_MISSION or -1, + DayNight = world.event.S_EVENT_DAYNIGHT or -1, + FlightTime = world.event.S_EVENT_FLIGHT_TIME or -1, + SelfKillPilot = world.event.S_EVENT_PLAYER_SELF_KILL_PILOT or -1, + PlayerCaptureAirfield = world.event.S_EVENT_PLAYER_CAPTURE_AIRFIELD or -1, + EmergencyLanding = world.event.S_EVENT_EMERGENCY_LANDING or -1, + UnitCreateTask = world.event.S_EVENT_UNIT_CREATE_TASK or -1, + UnitDeleteTask = world.event.S_EVENT_UNIT_DELETE_TASK or -1, + SimulationStart = world.event.S_EVENT_SIMULATION_START or -1, + WeaponRearm = world.event.S_EVENT_WEAPON_REARM or -1, + WeaponDrop = world.event.S_EVENT_WEAPON_DROP or -1, } --- The Event structure @@ -9232,20 +12821,80 @@ local _EVENTMETA = { Event = "OnEventBDA", Text = "S_EVENT_BDA" }, -} - - ---- The Events structure --- @type EVENT.Events --- @field #number IniUnit - ---- Create new event handler. --- @param #EVENT self --- @return #EVENT self -function EVENT:New() - - -- Inherit base. - local self = BASE:Inherit( self, BASE:New() ) + -- Added with DCS 2.8 + [EVENTS.AIAbortMission] = { + Order = 1, + Side = "I", + Event = "OnEventAIAbortMission", + Text = "S_EVENT_AI_ABORT_MISSION" + }, + [EVENTS.DayNight] = { + Order = 1, + Event = "OnEventDayNight", + Text = "S_EVENT_DAYNIGHT" + }, + [EVENTS.FlightTime] = { + Order = 1, + Event = "OnEventFlightTime", + Text = "S_EVENT_FLIGHT_TIME" + }, + [EVENTS.SelfKillPilot] = { + Order = 1, + Side = "I", + Event = "OnEventSelfKillPilot", + Text = "S_EVENT_PLAYER_SELF_KILL_PILOT" + }, + [EVENTS.PlayerCaptureAirfield] = { + Order = 1, + Event = "OnEventPlayerCaptureAirfield", + Text = "S_EVENT_PLAYER_CAPTURE_AIRFIELD" + }, + [EVENTS.EmergencyLanding] = { + Order = 1, + Side = "I", + Event = "OnEventEmergencyLanding", + Text = "S_EVENT_EMERGENCY_LANDING" + }, + [EVENTS.UnitCreateTask] = { + Order = 1, + Event = "OnEventUnitCreateTask", + Text = "S_EVENT_UNIT_CREATE_TASK" + }, + [EVENTS.UnitDeleteTask] = { + Order = 1, + Event = "OnEventUnitDeleteTask", + Text = "S_EVENT_UNIT_DELETE_TASK" + }, + [EVENTS.SimulationStart] = { + Order = 1, + Event = "OnEventSimulationStart", + Text = "S_EVENT_SIMULATION_START" + }, + [EVENTS.WeaponRearm] = { + Order = 1, + Side = "I", + Event = "OnEventWeaponRearm", + Text = "S_EVENT_WEAPON_REARM" + }, + [EVENTS.WeaponDrop] = { + Order = 1, + Side = "I", + Event = "OnEventWeaponDrop", + Text = "S_EVENT_WEAPON_DROP" + }, +} + +--- The Events structure +-- @type EVENT.Events +-- @field #number IniUnit + +--- Create new event handler. +-- @param #EVENT self +-- @return #EVENT self +function EVENT:New() + + -- Inherit base. + local self = BASE:Inherit( self, BASE:New() ) -- Add world event handler. self.EventHandler = world.addEventHandler(self) @@ -9604,7 +13253,7 @@ do -- Event Creation --- Creation of a ZoneGoal Deletion Event. -- @param #EVENT self - -- @param Core.ZoneGoal#ZONE_GOAL ZoneGoal The ZoneGoal created. + -- @param Functional.ZoneGoal#ZONE_GOAL ZoneGoal The ZoneGoal created. function EVENT:CreateEventDeleteZoneGoal( ZoneGoal ) self:F( { ZoneGoal } ) @@ -9655,13 +13304,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 @@ -9674,6 +13322,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 @@ -9681,35 +13330,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_ @@ -9735,9 +13361,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 @@ -9748,15 +13412,21 @@ 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 @@ -9764,11 +13434,22 @@ function EVENT:onEvent( Event ) Event.IniCoalition = Event.IniDCSUnit:getCoalition() Event.IniCategory = Event.IniDCSUnit:getDesc().category Event.IniTypeName = Event.IniDCSUnit:getTypeName() + + -- If the airbase does not exist in the DB, we add it (e.g. when FARPS are spawned). + if not Event.IniUnit then + _DATABASE:_RegisterAirbase(Event.initiator) + Event.IniUnit = AIRBASE:FindByName(Event.IniDCSUnitName) + end end end if Event.target then + + --- + -- TARGET + --- + -- Target category. Event.TgtObjectCategory = Event.target:getCategory() if Event.TgtObjectCategory == Object.Category.UNIT then @@ -9781,9 +13462,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() @@ -9794,7 +13473,7 @@ function EVENT:onEvent( Event ) if Event.TgtObjectCategory == Object.Category.STATIC then -- get base data Event.TgtDCSUnit = Event.target - if Event.target:isExist() and Event.id ~= 33 then -- leave out ejected seat object + if Event.target:isExist() and Event.id ~= 33 and not Event.TgtObjectCategory == Object.Category.COORDINATE then -- leave out ejected seat object Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() Event.TgtUnitName = Event.TgtDCSUnitName Event.TgtUnit = STATIC:FindByName( Event.TgtDCSUnitName, false ) @@ -9831,6 +13510,7 @@ function EVENT:onEvent( Event ) end end + -- Weapon. if Event.weapon then Event.Weapon = Event.weapon Event.WeaponName = Event.Weapon:getTypeName() @@ -9849,9 +13529,11 @@ function EVENT:onEvent( Event ) --local name=Event.place:getName() -- This returns a DCS error "Airbase doesn't exit" :( -- However, this is not a big thing, as the aircraft the pilot ejected from is usually long crashed before the ejected pilot touches the ground. --Event.Place=UNIT:Find(Event.place) - else - Event.Place=AIRBASE:Find(Event.place) - Event.PlaceName=Event.Place:GetName() + else + if Event.place:isExist() and Event.place:getCategory() ~= Object.Category.SCENERY then + Event.Place=AIRBASE:Find(Event.place) + Event.PlaceName=Event.Place:GetName() + end end end @@ -9865,23 +13547,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 @@ -9894,8 +13575,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 @@ -9905,20 +13586,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 ) @@ -9931,15 +13609,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 @@ -9957,7 +13632,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() @@ -9968,10 +13644,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 ) ) @@ -9984,10 +13656,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 ) ) @@ -10000,7 +13668,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 @@ -10009,9 +13677,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 ) @@ -10023,16 +13688,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 @@ -10076,7 +13739,7 @@ function EVENTHANDLER:New() self = BASE:Inherit( self, BASE:New() ) -- #EVENTHANDLER return self end ---- **Core** - Manages various settings for running missions, consumed by moose classes and provides a menu system for players to tweak settings in running missions. +--- **Core** - Manages various settings for missions, providing a menu for players to tweak settings in running missions. -- -- === -- @@ -10107,15 +13770,14 @@ end -- @module Core.Settings -- @image Core_Settings.JPG - --- @type SETTINGS -- @extends Core.Base#BASE ---- Takes care of various settings that influence the behaviour of certain functionalities and classes within the MOOSE framework. +--- Takes care of various settings that influence the behavior of certain functionalities and classes within the MOOSE framework. -- -- === -- --- The SETTINGS class takes care of various settings that influence the behaviour of certain functionalities and classes within the MOOSE framework. +-- The SETTINGS class takes care of various settings that influence the behavior of certain functionalities and classes within the MOOSE framework. -- SETTINGS can work on 2 levels: -- -- - **Default settings**: A running mission has **Default settings**. @@ -10137,7 +13799,7 @@ end -- -- A menu is created automatically per Command Center that allows to modify the **Default** settings. -- So, when joining a CC unit, a menu will be available that allows to change the settings parameters **FOR ALL THE PLAYERS**! --- Note that the **Default settings** will only be used when a player has not choosen its own settings. +-- Note that the **Default settings** will only be used when a player has not chosen its own settings. -- -- ## 2.2) Player settings menu -- @@ -10147,7 +13809,7 @@ end -- -- ## 2.3) Show or Hide the Player Setting menus -- --- Of course, it may be requried not to show any setting menus. In this case, a method is available on the **\_SETTINGS object**. +-- Of course, it may be required not to show any setting menus. In this case, a method is available on the **\_SETTINGS object**. -- Use @{#SETTINGS.SetPlayerMenuOff}() to hide the player menus, and use @{#SETTINGS.SetPlayerMenuOn}() show the player menus. -- Note that when this method is used, any player already in a slot will not have its menus visibility changed. -- The option will only have effect when a player enters a new slot or changes a slot. @@ -10172,8 +13834,8 @@ end -- -- - A2G BR: [Bearing Range](https://en.wikipedia.org/wiki/Bearing_(navigation)). -- - A2G MGRS: The [Military Grid Reference System](https://en.wikipedia.org/wiki/Military_Grid_Reference_System). The accuracy can also be adapted. --- - A2G LL DMS: Lattitude Longitude [Degrees Minutes Seconds](https://en.wikipedia.org/wiki/Geographic_coordinate_conversion). The accuracy can also be adapted. --- - A2G LL DDM: Lattitude Longitude [Decimal Degrees Minutes](https://en.wikipedia.org/wiki/Decimal_degrees). The accuracy can also be adapted. +-- - A2G LL DMS: Latitude Longitude [Degrees Minutes Seconds](https://en.wikipedia.org/wiki/Geographic_coordinate_conversion). The accuracy can also be adapted. +-- - A2G LL DDM: Latitude Longitude [Decimal Degrees Minutes](https://en.wikipedia.org/wiki/Decimal_degrees). The accuracy can also be adapted. -- -- ### 3.1.2) A2G coordinates setting **menu** -- @@ -10261,7 +13923,7 @@ end -- The settings can be changed by using the **Default settings menu** on the Command Center or the **Player settings menu** on the Player Slot. -- -- Each Message Type has specific timings that will be applied when the message is displayed. --- The Settings Menu will provide for each Message Type a selection of proposed durations from which can be choosen. +-- The Settings Menu will provide for each Message Type a selection of proposed durations from which can be chosen. -- So the player can choose its own amount of seconds how long a message should be displayed of a certain type. -- Note that **Update** messages can be chosen not to be displayed at all! -- @@ -10274,7 +13936,7 @@ end -- -- ## 3.5) **Era** of the battle -- --- The threat level metric is scaled according the era of the battle. A target that is AAA, will pose a much greather threat in WWII than on modern warfare. +-- The threat level metric is scaled according the era of the battle. A target that is AAA, will pose a much greater threat in WWII than on modern warfare. -- Therefore, there are 4 era that are defined within the settings: -- -- - **WWII** era: Use for warfare with equipment during the world war II time. @@ -10291,8 +13953,8 @@ end SETTINGS = { ClassName = "SETTINGS", ShowPlayerMenu = true, - MenuShort = false, - MenuStatic = false, + MenuShort = false, + MenuStatic = false, } SETTINGS.__Enum = {} @@ -10309,7 +13971,6 @@ SETTINGS.__Enum.Era = { Modern = 4, } - do -- SETTINGS --- SETTINGS constructor. @@ -10331,6 +13992,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 ) @@ -10346,14 +14008,14 @@ do -- SETTINGS -- Short text are better suited for, e.g., VR. -- @param #SETTINGS self -- @param #boolean onoff If *true* use short menu texts. If *false* long ones (default). - function SETTINGS:SetMenutextShort(onoff) + function SETTINGS:SetMenutextShort( onoff ) _SETTINGS.MenuShort = onoff end --- Set menu to be static. -- @param #SETTINGS self -- @param #boolean onoff If *true* menu is static. If *false* menu will be updated after changes (default). - function SETTINGS:SetMenuStatic(onoff) + function SETTINGS:SetMenuStatic( onoff ) _SETTINGS.MenuStatic = onoff end @@ -10362,12 +14024,26 @@ 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. function SETTINGS:IsMetric() - return ( self.Metric ~= nil and self.Metric == true ) or ( self.Metric == nil and _SETTINGS:IsMetric() ) + return (self.Metric ~= nil and self.Metric == true) or (self.Metric == nil and _SETTINGS:IsMetric()) end --- Sets the SETTINGS imperial. @@ -10380,7 +14056,7 @@ do -- SETTINGS -- @param #SETTINGS self -- @return #boolean true if imperial. function SETTINGS:IsImperial() - return ( self.Metric ~= nil and self.Metric == false ) or ( self.Metric == nil and _SETTINGS:IsMetric() ) + return (self.Metric ~= nil and self.Metric == false) or (self.Metric == nil and _SETTINGS:IsImperial()) end --- Sets the SETTINGS LL accuracy. @@ -10400,7 +14076,7 @@ do -- SETTINGS --- Sets the SETTINGS MGRS accuracy. -- @param #SETTINGS self - -- @param #number MGRS_Accuracy + -- @param #number MGRS_Accuracy 0 to 5 -- @return #SETTINGS function SETTINGS:SetMGRS_Accuracy( MGRS_Accuracy ) self.MGRS_Accuracy = MGRS_Accuracy @@ -10422,13 +14098,12 @@ do -- SETTINGS self.MessageTypeTimings[MessageType] = MessageTime end - --- Gets the SETTINGS Message Display Timing of a MessageType -- @param #SETTINGS self -- @param Core.Message#MESSAGE MessageType The type of the message. -- @return #number function SETTINGS:GetMessageTime( MessageType ) - return ( self.MessageTypeTimings and self.MessageTypeTimings[MessageType] ) or _SETTINGS:GetMessageTime( MessageType ) + return (self.MessageTypeTimings and self.MessageTypeTimings[MessageType]) or _SETTINGS:GetMessageTime( MessageType ) end --- Sets A2G LL DMS @@ -10449,14 +14124,14 @@ do -- SETTINGS -- @param #SETTINGS self -- @return #boolean true if LL DMS function SETTINGS:IsA2G_LL_DMS() - return ( self.A2GSystem and self.A2GSystem == "LL DMS" ) or ( not self.A2GSystem and _SETTINGS:IsA2G_LL_DMS() ) + return (self.A2GSystem and self.A2GSystem == "LL DMS") or (not self.A2GSystem and _SETTINGS:IsA2G_LL_DMS()) end --- Is LL DDM -- @param #SETTINGS self -- @return #boolean true if LL DDM function SETTINGS:IsA2G_LL_DDM() - return ( self.A2GSystem and self.A2GSystem == "LL DDM" ) or ( not self.A2GSystem and _SETTINGS:IsA2G_LL_DDM() ) + return (self.A2GSystem and self.A2GSystem == "LL DDM") or (not self.A2GSystem and _SETTINGS:IsA2G_LL_DDM()) end --- Sets A2G MGRS @@ -10470,7 +14145,7 @@ do -- SETTINGS -- @param #SETTINGS self -- @return #boolean true if MGRS function SETTINGS:IsA2G_MGRS() - return ( self.A2GSystem and self.A2GSystem == "MGRS" ) or ( not self.A2GSystem and _SETTINGS:IsA2G_MGRS() ) + return (self.A2GSystem and self.A2GSystem == "MGRS") or (not self.A2GSystem and _SETTINGS:IsA2G_MGRS()) end --- Sets A2G BRA @@ -10484,7 +14159,7 @@ do -- SETTINGS -- @param #SETTINGS self -- @return #boolean true if BRA function SETTINGS:IsA2G_BR() - return ( self.A2GSystem and self.A2GSystem == "BR" ) or ( not self.A2GSystem and _SETTINGS:IsA2G_BR() ) + return (self.A2GSystem and self.A2GSystem == "BR") or (not self.A2GSystem and _SETTINGS:IsA2G_BR()) end --- Sets A2A BRA @@ -10498,7 +14173,7 @@ do -- SETTINGS -- @param #SETTINGS self -- @return #boolean true if BRA function SETTINGS:IsA2A_BRAA() - return ( self.A2ASystem and self.A2ASystem == "BRAA" ) or ( not self.A2ASystem and _SETTINGS:IsA2A_BRAA() ) + return (self.A2ASystem and self.A2ASystem == "BRAA") or (not self.A2ASystem and _SETTINGS:IsA2A_BRAA()) end --- Sets A2A BULLS @@ -10512,7 +14187,7 @@ do -- SETTINGS -- @param #SETTINGS self -- @return #boolean true if BULLS function SETTINGS:IsA2A_BULLS() - return ( self.A2ASystem and self.A2ASystem == "BULLS" ) or ( not self.A2ASystem and _SETTINGS:IsA2A_BULLS() ) + return (self.A2ASystem and self.A2ASystem == "BULLS") or (not self.A2ASystem and _SETTINGS:IsA2A_BULLS()) end --- Sets A2A LL DMS @@ -10533,14 +14208,14 @@ do -- SETTINGS -- @param #SETTINGS self -- @return #boolean true if LL DMS function SETTINGS:IsA2A_LL_DMS() - return ( self.A2ASystem and self.A2ASystem == "LL DMS" ) or ( not self.A2ASystem and _SETTINGS:IsA2A_LL_DMS() ) + return (self.A2ASystem and self.A2ASystem == "LL DMS") or (not self.A2ASystem and _SETTINGS:IsA2A_LL_DMS()) end --- Is LL DDM -- @param #SETTINGS self -- @return #boolean true if LL DDM function SETTINGS:IsA2A_LL_DDM() - return ( self.A2ASystem and self.A2ASystem == "LL DDM" ) or ( not self.A2ASystem and _SETTINGS:IsA2A_LL_DDM() ) + return (self.A2ASystem and self.A2ASystem == "LL DDM") or (not self.A2ASystem and _SETTINGS:IsA2A_LL_DDM()) end --- Sets A2A MGRS @@ -10554,7 +14229,7 @@ do -- SETTINGS -- @param #SETTINGS self -- @return #boolean true if MGRS function SETTINGS:IsA2A_MGRS() - return ( self.A2ASystem and self.A2ASystem == "MGRS" ) or ( not self.A2ASystem and _SETTINGS:IsA2A_MGRS() ) + return (self.A2ASystem and self.A2ASystem == "MGRS") or (not self.A2ASystem and _SETTINGS:IsA2A_MGRS()) end --- @param #SETTINGS self @@ -10573,37 +14248,37 @@ do -- SETTINGS -- A2G Coordinate System ------- - local text="A2G Coordinate System" + local text = "A2G Coordinate System" if _SETTINGS.MenuShort then - text="A2G Coordinates" + text = "A2G Coordinates" end local A2GCoordinateMenu = MENU_GROUP:New( MenuGroup, text, SettingsMenu ):SetTime( MenuTime ) -- Set LL DMS if not self:IsA2G_LL_DMS() then - local text="Lat/Lon Degree Min Sec (LL DMS)" + local text = "Lat/Lon Degree Min Sec (LL DMS)" if _SETTINGS.MenuShort then - text="LL DMS" + text = "LL DMS" end MENU_GROUP_COMMAND:New( MenuGroup, text, A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "LL DMS" ):SetTime( MenuTime ) end -- Set LL DDM if not self:IsA2G_LL_DDM() then - local text="Lat/Lon Degree Dec Min (LL DDM)" + local text = "Lat/Lon Degree Dec Min (LL DDM)" if _SETTINGS.MenuShort then - text="LL DDM" + text = "LL DDM" end MENU_GROUP_COMMAND:New( MenuGroup, "Lat/Lon Degree Dec Min (LL DDM)", A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "LL DDM" ):SetTime( MenuTime ) end -- Set LL DMS accuracy. if self:IsA2G_LL_DDM() then - local text1="LL DDM Accuracy 1" - local text2="LL DDM Accuracy 2" - local text3="LL DDM Accuracy 3" + local text1 = "LL DDM Accuracy 1" + local text2 = "LL DDM Accuracy 2" + local text3 = "LL DDM Accuracy 3" if _SETTINGS.MenuShort then - text1="LL DDM" + text1 = "LL DDM" end MENU_GROUP_COMMAND:New( MenuGroup, "LL DDM Accuracy 1", A2GCoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 1 ):SetTime( MenuTime ) MENU_GROUP_COMMAND:New( MenuGroup, "LL DDM Accuracy 2", A2GCoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 2 ):SetTime( MenuTime ) @@ -10612,18 +14287,18 @@ do -- SETTINGS -- Set BR. if not self:IsA2G_BR() then - local text="Bearing, Range (BR)" + local text = "Bearing, Range (BR)" if _SETTINGS.MenuShort then - text="BR" + text = "BR" end - MENU_GROUP_COMMAND:New( MenuGroup, text , A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "BR" ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, text, A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "BR" ):SetTime( MenuTime ) end -- Set MGRS. if not self:IsA2G_MGRS() then - local text="Military Grid (MGRS)" + local text = "Military Grid (MGRS)" if _SETTINGS.MenuShort then - text="MGRS" + text = "MGRS" end MENU_GROUP_COMMAND:New( MenuGroup, text, A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "MGRS" ):SetTime( MenuTime ) end @@ -10641,24 +14316,24 @@ do -- SETTINGS -- A2A Coordinate System ------- - local text="A2A Coordinate System" + local text = "A2A Coordinate System" if _SETTINGS.MenuShort then - text="A2A Coordinates" + text = "A2A Coordinates" end local A2ACoordinateMenu = MENU_GROUP:New( MenuGroup, text, SettingsMenu ):SetTime( MenuTime ) if not self:IsA2A_LL_DMS() then - local text="Lat/Lon Degree Min Sec (LL DMS)" + local text = "Lat/Lon Degree Min Sec (LL DMS)" if _SETTINGS.MenuShort then - text="LL DMS" + text = "LL DMS" end MENU_GROUP_COMMAND:New( MenuGroup, text, A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "LL DMS" ):SetTime( MenuTime ) end if not self:IsA2A_LL_DDM() then - local text="Lat/Lon Degree Dec Min (LL DDM)" + local text = "Lat/Lon Degree Dec Min (LL DDM)" if _SETTINGS.MenuShort then - text="LL DDM" + text = "LL DDM" end MENU_GROUP_COMMAND:New( MenuGroup, text, A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "LL DDM" ):SetTime( MenuTime ) end @@ -10671,25 +14346,25 @@ do -- SETTINGS end if not self:IsA2A_BULLS() then - local text="Bullseye (BULLS)" + local text = "Bullseye (BULLS)" if _SETTINGS.MenuShort then - text="Bulls" + text = "Bulls" end MENU_GROUP_COMMAND:New( MenuGroup, text, A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "BULLS" ):SetTime( MenuTime ) end if not self:IsA2A_BRAA() then - local text="Bearing Range Altitude Aspect (BRAA)" + local text = "Bearing Range Altitude Aspect (BRAA)" if _SETTINGS.MenuShort then - text="BRAA" + text = "BRAA" end MENU_GROUP_COMMAND:New( MenuGroup, text, A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "BRAA" ):SetTime( MenuTime ) end if not self:IsA2A_MGRS() then - local text="Military Grid (MGRS)" + local text = "Military Grid (MGRS)" if _SETTINGS.MenuShort then - text="MGRS" + text = "MGRS" end MENU_GROUP_COMMAND:New( MenuGroup, text, A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "MGRS" ):SetTime( MenuTime ) end @@ -10702,31 +14377,31 @@ do -- SETTINGS MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 5", A2ACoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 5 ):SetTime( MenuTime ) end - local text="Measures and Weights System" + local text = "Measures and Weights System" if _SETTINGS.MenuShort then - text="Unit System" + text = "Unit System" end local MetricsMenu = MENU_GROUP:New( MenuGroup, text, SettingsMenu ):SetTime( MenuTime ) if self:IsMetric() then - local text="Imperial (Miles,Feet)" + local text = "Imperial (Miles,Feet)" if _SETTINGS.MenuShort then - text="Imperial" + text = "Imperial" end MENU_GROUP_COMMAND:New( MenuGroup, text, MetricsMenu, self.MenuMWSystem, self, MenuGroup, RootMenu, false ):SetTime( MenuTime ) end if self:IsImperial() then - local text="Metric (Kilometers,Meters)" + local text = "Metric (Kilometers,Meters)" if _SETTINGS.MenuShort then - text="Metric" + text = "Metric" end MENU_GROUP_COMMAND:New( MenuGroup, text, MetricsMenu, self.MenuMWSystem, self, MenuGroup, RootMenu, true ):SetTime( MenuTime ) end - local text="Messages and Reports" + local text = "Messages and Reports" if _SETTINGS.MenuShort then - text="Messages & Reports" + text = "Messages & Reports" end local MessagesMenu = MENU_GROUP:New( MenuGroup, text, SettingsMenu ):SetTime( MenuTime ) @@ -10767,7 +14442,6 @@ do -- SETTINGS MENU_GROUP_COMMAND:New( MenuGroup, "2 minutes", DetailedReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.DetailedReportsMenu, 120 ):SetTime( MenuTime ) MENU_GROUP_COMMAND:New( MenuGroup, "3 minutes", DetailedReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.DetailedReportsMenu, 180 ):SetTime( MenuTime ) - SettingsMenu:Remove( MenuTime ) return self @@ -10811,11 +14485,11 @@ do -- SETTINGS self.PlayerMenu = PlayerMenu - self:I(string.format("Setting menu for player %s", tostring(PlayerName))) + self:I( string.format( "Setting menu for player %s", tostring( PlayerName ) ) ) local submenu = MENU_GROUP:New( PlayerGroup, "LL Accuracy", PlayerMenu ) MENU_GROUP_COMMAND:New( PlayerGroup, "LL 0 Decimals", submenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 0 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "LL 1 Decimal", submenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 1 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "LL 1 Decimal", submenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 1 ) MENU_GROUP_COMMAND:New( PlayerGroup, "LL 2 Decimals", submenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 2 ) MENU_GROUP_COMMAND:New( PlayerGroup, "LL 3 Decimals", submenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 3 ) MENU_GROUP_COMMAND:New( PlayerGroup, "LL 4 Decimals", submenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 4 ) @@ -10832,40 +14506,40 @@ do -- SETTINGS -- A2G Coordinate System ------ - local text="A2G Coordinate System" + local text = "A2G Coordinate System" if _SETTINGS.MenuShort then - text="A2G Coordinates" + text = "A2G Coordinates" end local A2GCoordinateMenu = MENU_GROUP:New( PlayerGroup, text, PlayerMenu ) if not self:IsA2G_LL_DMS() or _SETTINGS.MenuStatic then - local text="Lat/Lon Degree Min Sec (LL DMS)" + local text = "Lat/Lon Degree Min Sec (LL DMS)" if _SETTINGS.MenuShort then - text="A2G LL DMS" + text = "A2G LL DMS" end MENU_GROUP_COMMAND:New( PlayerGroup, text, A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DMS" ) end if not self:IsA2G_LL_DDM() or _SETTINGS.MenuStatic then - local text="Lat/Lon Degree Dec Min (LL DDM)" + local text = "Lat/Lon Degree Dec Min (LL DDM)" if _SETTINGS.MenuShort then - text="A2G LL DDM" + text = "A2G LL DDM" end MENU_GROUP_COMMAND:New( PlayerGroup, text, A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DDM" ) end if not self:IsA2G_BR() or _SETTINGS.MenuStatic then - local text="Bearing, Range (BR)" + local text = "Bearing, Range (BR)" if _SETTINGS.MenuShort then - text="A2G BR" + text = "A2G BR" end MENU_GROUP_COMMAND:New( PlayerGroup, text, A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "BR" ) end if not self:IsA2G_MGRS() or _SETTINGS.MenuStatic then - local text="Military Grid (MGRS)" + local text = "Military Grid (MGRS)" if _SETTINGS.MenuShort then - text="A2G MGRS" + text = "A2G MGRS" end MENU_GROUP_COMMAND:New( PlayerGroup, text, A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "MGRS" ) end @@ -10874,49 +14548,48 @@ do -- SETTINGS -- A2A Coordinates Menu ------ - local text="A2A Coordinate System" + local text = "A2A Coordinate System" if _SETTINGS.MenuShort then - text="A2A Coordinates" + text = "A2A Coordinates" end local A2ACoordinateMenu = MENU_GROUP:New( PlayerGroup, text, PlayerMenu ) - if not self:IsA2A_LL_DMS() or _SETTINGS.MenuStatic then - local text="Lat/Lon Degree Min Sec (LL DMS)" + local text = "Lat/Lon Degree Min Sec (LL DMS)" if _SETTINGS.MenuShort then - text="A2A LL DMS" + text = "A2A LL DMS" end MENU_GROUP_COMMAND:New( PlayerGroup, text, A2ACoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DMS" ) end if not self:IsA2A_LL_DDM() or _SETTINGS.MenuStatic then - local text="Lat/Lon Degree Dec Min (LL DDM)" + local text = "Lat/Lon Degree Dec Min (LL DDM)" if _SETTINGS.MenuShort then - text="A2A LL DDM" + text = "A2A LL DDM" end MENU_GROUP_COMMAND:New( PlayerGroup, text, A2ACoordinateMenu, self.MenuGroupA2ASystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DDM" ) end if not self:IsA2A_BULLS() or _SETTINGS.MenuStatic then - local text="Bullseye (BULLS)" + local text = "Bullseye (BULLS)" if _SETTINGS.MenuShort then - text="A2A BULLS" + text = "A2A BULLS" end MENU_GROUP_COMMAND:New( PlayerGroup, text, A2ACoordinateMenu, self.MenuGroupA2ASystem, self, PlayerUnit, PlayerGroup, PlayerName, "BULLS" ) end if not self:IsA2A_BRAA() or _SETTINGS.MenuStatic then - local text="Bearing Range Altitude Aspect (BRAA)" + local text = "Bearing Range Altitude Aspect (BRAA)" if _SETTINGS.MenuShort then - text="A2A BRAA" + text = "A2A BRAA" end MENU_GROUP_COMMAND:New( PlayerGroup, text, A2ACoordinateMenu, self.MenuGroupA2ASystem, self, PlayerUnit, PlayerGroup, PlayerName, "BRAA" ) end if not self:IsA2A_MGRS() or _SETTINGS.MenuStatic then - local text="Military Grid (MGRS)" + local text = "Military Grid (MGRS)" if _SETTINGS.MenuShort then - text="A2A MGRS" + text = "A2A MGRS" end MENU_GROUP_COMMAND:New( PlayerGroup, text, A2ACoordinateMenu, self.MenuGroupA2ASystem, self, PlayerUnit, PlayerGroup, PlayerName, "MGRS" ) end @@ -10925,24 +14598,24 @@ do -- SETTINGS -- Unit system --- - local text="Measures and Weights System" + local text = "Measures and Weights System" if _SETTINGS.MenuShort then - text="Unit System" + text = "Unit System" end local MetricsMenu = MENU_GROUP:New( PlayerGroup, text, PlayerMenu ) if self:IsMetric() or _SETTINGS.MenuStatic then - local text="Imperial (Miles,Feet)" + local text = "Imperial (Miles,Feet)" if _SETTINGS.MenuShort then - text="Imperial" + text = "Imperial" end MENU_GROUP_COMMAND:New( PlayerGroup, text, MetricsMenu, self.MenuGroupMWSystem, self, PlayerUnit, PlayerGroup, PlayerName, false ) end if self:IsImperial() or _SETTINGS.MenuStatic then - local text="Metric (Kilometers,Meters)" + local text = "Metric (Kilometers,Meters)" if _SETTINGS.MenuShort then - text="Metric" + text = "Metric" end MENU_GROUP_COMMAND:New( PlayerGroup, text, MetricsMenu, self.MenuGroupMWSystem, self, PlayerUnit, PlayerGroup, PlayerName, true ) end @@ -10951,9 +14624,9 @@ do -- SETTINGS -- Messages and Reports --- - local text="Messages and Reports" + local text = "Messages and Reports" if _SETTINGS.MenuShort then - text="Messages & Reports" + text = "Messages & Reports" end local MessagesMenu = MENU_GROUP:New( PlayerGroup, text, PlayerMenu ) @@ -11013,39 +14686,38 @@ do -- SETTINGS return self end - --- @param #SETTINGS self function SETTINGS:A2GMenuSystem( MenuGroup, RootMenu, A2GSystem ) self.A2GSystem = A2GSystem - MESSAGE:New( string.format("Settings: Default A2G coordinate system set to %s for all players!", A2GSystem ), 5 ):ToAll() + MESSAGE:New( string.format( "Settings: Default A2G coordinate system set to %s for all players!", A2GSystem ), 5 ):ToAll() self:SetSystemMenu( MenuGroup, RootMenu ) end --- @param #SETTINGS self function SETTINGS:A2AMenuSystem( MenuGroup, RootMenu, A2ASystem ) self.A2ASystem = A2ASystem - MESSAGE:New( string.format("Settings: Default A2A coordinate system set to %s for all players!", A2ASystem ), 5 ):ToAll() + MESSAGE:New( string.format( "Settings: Default A2A coordinate system set to %s for all players!", A2ASystem ), 5 ):ToAll() self:SetSystemMenu( MenuGroup, RootMenu ) end --- @param #SETTINGS self function SETTINGS:MenuLL_DDM_Accuracy( MenuGroup, RootMenu, LL_Accuracy ) self.LL_Accuracy = LL_Accuracy - MESSAGE:New( string.format("Settings: Default LL accuracy set to %s for all players!", LL_Accuracy ), 5 ):ToAll() + MESSAGE:New( string.format( "Settings: Default LL accuracy set to %s for all players!", LL_Accuracy ), 5 ):ToAll() self:SetSystemMenu( MenuGroup, RootMenu ) end --- @param #SETTINGS self function SETTINGS:MenuMGRS_Accuracy( MenuGroup, RootMenu, MGRS_Accuracy ) self.MGRS_Accuracy = MGRS_Accuracy - MESSAGE:New( string.format("Settings: Default MGRS accuracy set to %s for all players!", MGRS_Accuracy ), 5 ):ToAll() + MESSAGE:New( string.format( "Settings: Default MGRS accuracy set to %s for all players!", MGRS_Accuracy ), 5 ):ToAll() self:SetSystemMenu( MenuGroup, RootMenu ) end --- @param #SETTINGS self function SETTINGS:MenuMWSystem( MenuGroup, RootMenu, MW ) self.Metric = MW - MESSAGE:New( string.format("Settings: Default measurement format set to %s for all players!", MW and "Metric" or "Imperial" ), 5 ):ToAll() + MESSAGE:New( string.format( "Settings: Default measurement format set to %s for all players!", MW and "Metric" or "Imperial" ), 5 ):ToAll() self:SetSystemMenu( MenuGroup, RootMenu ) end @@ -11058,12 +14730,12 @@ do -- SETTINGS do --- @param #SETTINGS self function SETTINGS:MenuGroupA2GSystem( PlayerUnit, PlayerGroup, PlayerName, A2GSystem ) - BASE:E( {self, PlayerUnit:GetName(), A2GSystem} ) + BASE:E( { self, PlayerUnit:GetName(), A2GSystem } ) self.A2GSystem = A2GSystem MESSAGE:New( string.format( "Settings: A2G format set to %s for player %s.", A2GSystem, PlayerName ), 5 ):ToGroup( PlayerGroup ) - if _SETTINGS.MenuStatic==false then - self:RemovePlayerMenu(PlayerUnit) - self:SetPlayerMenu(PlayerUnit) + if _SETTINGS.MenuStatic == false then + self:RemovePlayerMenu( PlayerUnit ) + self:SetPlayerMenu( PlayerUnit ) end end @@ -11071,9 +14743,9 @@ do -- SETTINGS function SETTINGS:MenuGroupA2ASystem( PlayerUnit, PlayerGroup, PlayerName, A2ASystem ) self.A2ASystem = A2ASystem MESSAGE:New( string.format( "Settings: A2A format set to %s for player %s.", A2ASystem, PlayerName ), 5 ):ToGroup( PlayerGroup ) - if _SETTINGS.MenuStatic==false then - self:RemovePlayerMenu(PlayerUnit) - self:SetPlayerMenu(PlayerUnit) + if _SETTINGS.MenuStatic == false then + self:RemovePlayerMenu( PlayerUnit ) + self:SetPlayerMenu( PlayerUnit ) end end @@ -11081,9 +14753,9 @@ do -- SETTINGS function SETTINGS:MenuGroupLL_DDM_AccuracySystem( PlayerUnit, PlayerGroup, PlayerName, LL_Accuracy ) self.LL_Accuracy = LL_Accuracy MESSAGE:New( string.format( "Settings: LL format accuracy set to %d decimal places for player %s.", LL_Accuracy, PlayerName ), 5 ):ToGroup( PlayerGroup ) - if _SETTINGS.MenuStatic==false then - self:RemovePlayerMenu(PlayerUnit) - self:SetPlayerMenu(PlayerUnit) + if _SETTINGS.MenuStatic == false then + self:RemovePlayerMenu( PlayerUnit ) + self:SetPlayerMenu( PlayerUnit ) end end @@ -11091,9 +14763,9 @@ do -- SETTINGS function SETTINGS:MenuGroupMGRS_AccuracySystem( PlayerUnit, PlayerGroup, PlayerName, MGRS_Accuracy ) self.MGRS_Accuracy = MGRS_Accuracy MESSAGE:New( string.format( "Settings: MGRS format accuracy set to %d for player %s.", MGRS_Accuracy, PlayerName ), 5 ):ToGroup( PlayerGroup ) - if _SETTINGS.MenuStatic==false then - self:RemovePlayerMenu(PlayerUnit) - self:SetPlayerMenu(PlayerUnit) + if _SETTINGS.MenuStatic == false then + self:RemovePlayerMenu( PlayerUnit ) + self:SetPlayerMenu( PlayerUnit ) end end @@ -11101,9 +14773,9 @@ do -- SETTINGS function SETTINGS:MenuGroupMWSystem( PlayerUnit, PlayerGroup, PlayerName, MW ) self.Metric = MW MESSAGE:New( string.format( "Settings: Measurement format set to %s for player %s.", MW and "Metric" or "Imperial", PlayerName ), 5 ):ToGroup( PlayerGroup ) - if _SETTINGS.MenuStatic==false then - self:RemovePlayerMenu(PlayerUnit) - self:SetPlayerMenu(PlayerUnit) + if _SETTINGS.MenuStatic == false then + self:RemovePlayerMenu( PlayerUnit ) + self:SetPlayerMenu( PlayerUnit ) end end @@ -11133,7 +14805,6 @@ do -- SETTINGS end - --- Configures the era of the mission to be Cold war. -- @param #SETTINGS self -- @return #SETTINGS self @@ -11143,7 +14814,6 @@ do -- SETTINGS end - --- Configures the era of the mission to be Modern war. -- @param #SETTINGS self -- @return #SETTINGS self @@ -11153,9 +14823,6 @@ do -- SETTINGS end - - - end --- **Core** - Manage hierarchical menu structures and commands for players within a mission. -- @@ -11173,7 +14840,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. @@ -11182,7 +14849,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. @@ -11212,7 +14879,6 @@ end -- @module Core.Menu -- @image Core_Menu.JPG - MENU_INDEX = {} MENU_INDEX.MenuMission = {} MENU_INDEX.MenuMission.Menus = {} @@ -11223,10 +14889,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 @@ -11254,20 +14917,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 ) @@ -11278,42 +14937,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() @@ -11321,53 +14964,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 @@ -11375,10 +15001,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 ) @@ -11387,27 +15013,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 {} @@ -11415,7 +15039,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 @@ -11425,7 +15048,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}. @@ -11435,7 +15057,6 @@ do -- MENU_BASE self.MenuRemoveParent = RemoveParent return self end - --- Gets a @{Menu} from a parent @{Menu} -- @param #MENU_BASE self @@ -11444,7 +15065,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 @@ -11482,9 +15103,7 @@ do -- MENU_BASE end end - do -- MENU_COMMAND_BASE - --- @type MENU_COMMAND_BASE -- @field #function MenuCallHandler -- @extends Core.Menu#MENU_BASE @@ -11505,8 +15124,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 ) @@ -11526,7 +15144,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, @@ -11539,7 +15157,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!!! @@ -11550,35 +15167,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 @@ -11591,17 +15203,16 @@ 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! @@ -11625,7 +15236,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 @@ -11646,10 +15256,7 @@ do -- MENU_MISSION return self end - - end - do -- MENU_MISSION_COMMAND --- @type MENU_MISSION_COMMAND @@ -11662,7 +15269,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. @@ -11677,7 +15284,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 ) @@ -11691,17 +15297,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 @@ -11712,7 +15316,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 @@ -11731,13 +15334,8 @@ do -- MENU_MISSION_COMMAND return self end - end - - - do -- MENU_COALITION - --- @type MENU_COALITION -- @extends Core.Menu#MENU_BASE @@ -11793,18 +15391,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 ) @@ -11815,17 +15410,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! @@ -11848,7 +15441,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 @@ -11868,11 +15460,7 @@ do -- MENU_COALITION return self end - end - - - do -- MENU_COALITION_COMMAND --- @type MENU_COALITION_COMMAND @@ -11901,7 +15489,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 ) @@ -11916,20 +15503,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 @@ -11940,7 +15524,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 @@ -11959,20 +15542,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 @@ -12048,16 +15627,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 ) @@ -12065,12 +15641,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 ) @@ -12079,7 +15654,31 @@ do Menu:Refresh() end end + + return self + end + + --- Refreshes a new radio item for a group and submenus, ordering by (numerical) MenuTag + -- @param #MENU_GROUP self + -- @return #MENU_GROUP + function MENU_GROUP:RefreshAndOrderByTag() + do + missionCommands.removeItemForGroup( self.GroupID, self.MenuPath ) + missionCommands.addSubMenuForGroup( self.GroupID, self.MenuText, self.MenuParentPath ) + + local MenuTable = {} + for MenuText, Menu in pairs( self.Menus or {} ) do + local tag = Menu.MenuTag or math.random(1,10000) + MenuTable[#MenuTable+1] = {Tag=tag, Enty=Menu} + end + table.sort(MenuTable, function (k1, k2) return k1.tag < k2.tag end ) + for _, Menu in pairs( MenuTable ) do + Menu.Entry:Refresh() + end + end + + return self end --- Removes the sub menus recursively of this MENU_GROUP. @@ -12088,7 +15687,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 @@ -12097,18 +15695,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 @@ -12152,18 +15747,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 @@ -12174,19 +15766,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. @@ -12195,11 +15785,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 @@ -12218,13 +15806,9 @@ do return self end - end - --- MENU_GROUP_DELAYED - do - --- @type MENU_GROUP_DELAYED -- @extends Core.Menu#MENU_BASE @@ -12252,16 +15836,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 @@ -12275,12 +15856,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 ) @@ -12291,15 +15870,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 ) @@ -12308,7 +15884,8 @@ do Menu:Refresh() end end - + + return self end --- Removes the sub menus recursively of this MENU_GROUP_DELAYED. @@ -12317,7 +15894,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 @@ -12326,18 +15902,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 @@ -12382,18 +15955,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 @@ -12409,33 +15979,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. @@ -12444,11 +16010,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 @@ -12467,9 +16031,7 @@ do return self end - end - --- **Core** - Define zones within your mission of various forms, with various capabilities. -- -- === @@ -12481,7 +16043,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. @@ -12492,10 +16054,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: -- @@ -12518,19 +16080,23 @@ end -- === -- -- ### Author: **FlightControl** --- ### Contributions: +-- ### Contributions: **Applevangelist**, **FunkyFranky** -- -- === -- -- @module Core.Zone -- @image Core_Zones.JPG - --- @type ZONE_BASE -- @field #string ZoneName Name of the zone. -- @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 #table Table of any trigger zone properties from the ME. The key is the Name of the property, and the value is the property's Value. +-- @field #number Surface Type of surface. Only determined at the center of the zone! -- @extends Core.Fsm#FSM @@ -12542,7 +16108,7 @@ end -- * @{#ZONE_BASE.SetName}(): Sets the name of the zone. -- -- --- ## Each zone implements two polymorphic functions defined in @{Core.Zone#ZONE_BASE}: +-- ## Each zone implements two polymorphic functions defined in @{#ZONE_BASE}: -- -- * @{#ZONE_BASE.IsVec2InZone}(): Returns if a 2D vector is within the zone. -- * @{#ZONE_BASE.IsVec3InZone}(): Returns if a 3D vector is within the zone. @@ -12574,16 +16140,23 @@ end -- * @{#ZONE_BASE.SmokeZone}(): Smokes the zone boundaries in a color. -- * @{#ZONE_BASE.FlareZone}(): Flares the zone boundaries in a color. -- +-- ## A zone might have additional Properties created in the DCS Mission Editor, which can be accessed: +-- +-- *@{#ZONE_BASE.GetProperty}(): Returns the Value of the zone with the given PropertyName, or nil if no matching property exists. +-- *@{#ZONE_BASE.GetAllProperties}(): Returns the zone Properties table. +-- -- @field #ZONE_BASE ZONE_BASE = { ClassName = "ZONE_BASE", ZoneName = "", ZoneProbability = 1, DrawID=nil, - Color={} + Color={}, + ZoneID=nil, + Properties={}, + Surface=nil, } - --- The ZONE_BASE.BoundingSquare -- @type ZONE_BASE.BoundingSquare -- @field DCS#Distance x1 The lower x coordinate (left down) @@ -12591,7 +16164,6 @@ ZONE_BASE = { -- @field DCS#Distance x2 The higher x coordinate (right up) -- @field DCS#Distance y2 The higher y coordinate (right up) - --- ZONE_BASE constructor -- @param #ZONE_BASE self -- @param #string ZoneName Name of the zone. @@ -12602,11 +16174,11 @@ function ZONE_BASE:New( ZoneName ) self.ZoneName = ZoneName + --_DATABASE:AddZone(ZoneName,self) + return self end - - --- Returns the name of the zone. -- @param #ZONE_BASE self -- @return #string The name of the zone. @@ -12616,7 +16188,6 @@ function ZONE_BASE:GetName() return self.ZoneName end - --- Sets the name of the zone. -- @param #ZONE_BASE self -- @param #string ZoneName The name of the zone. @@ -12642,6 +16213,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 @@ -12655,12 +16227,12 @@ function ZONE_BASE:IsCoordinateInZone( Coordinate ) return InZone end ---- Returns if a PointVec2 is within the zone. +--- Returns if a PointVec2 is within the zone. (Name is misleading, actually takes a #COORDINATE) -- @param #ZONE_BASE self --- @param Core.Point#POINT_VEC2 PointVec2 The PointVec2 to test. +-- @param Core.Point#COORDINATE PointVec2 The coordinate to test. -- @return #boolean true if the PointVec2 is within the zone. -function ZONE_BASE:IsPointVec2InZone( PointVec2 ) - local InZone = self:IsVec2InZone( PointVec2:GetVec2() ) +function ZONE_BASE:IsPointVec2InZone( Coordinate ) + local InZone = self:IsVec2InZone( Coordinate:GetVec2() ) return InZone end @@ -12673,7 +16245,6 @@ function ZONE_BASE:IsPointVec3InZone( PointVec3 ) return InZone end - --- Returns the @{DCS#Vec2} coordinate of the zone. -- @param #ZONE_BASE self -- @return #nil. @@ -12697,7 +16268,6 @@ function ZONE_BASE:GetPointVec2() return PointVec2 end - --- Returns the @{DCS#Vec3} of the zone. -- @param #ZONE_BASE self -- @param DCS#Distance Height The height to add to the land height where the center of the zone is located. @@ -12760,6 +16330,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 @@ -12786,22 +16373,44 @@ 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 coalition. +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}`. --- @param #number Alpha Transparacy between 0 and 1. Default 0.15. +-- @param #number Alpha Transparency between 0 and 1. Default 0.15. -- @return #ZONE_BASE self function ZONE_BASE:SetColor(RGBcolor, Alpha) @@ -12821,7 +16430,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. @@ -12829,17 +16438,66 @@ 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 ---- Get transperency Alpha value of zone. +--- Get transparency Alpha value of zone. -- @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 transparency 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 @@ -12931,6 +16589,26 @@ function ZONE_BASE:GetZoneMaybe() end end +-- Returns the Value of the zone with the given PropertyName, or nil if no matching property exists. +-- @param #ZONE_BASE self +-- @param #string PropertyName The name of a the TriggerZone Property to be retrieved. +-- @return #string The Value of the TriggerZone Property with the given PropertyName, or nil if absent. +-- @usage +-- +-- local PropertiesZone = ZONE:FindByName("Properties Zone") +-- local Property = "ExampleProperty" +-- local PropertyValue = PropertiesZone:GetProperty(Property) +-- +function ZONE_BASE:GetProperty(PropertyName) + return self.Properties[PropertyName] +end + +-- Returns the zone Properties table. +-- @param #ZONE_BASE self +-- @return #table The Key:Value table of TriggerZone properties of the zone. +function ZONE_BASE:GetAllProperties() + return self.Properties +end --- The ZONE_RADIUS class, defined by a zone name, a location and a radius. -- @type ZONE_RADIUS @@ -12939,7 +16617,7 @@ end -- @extends #ZONE_BASE --- The ZONE_RADIUS class defined by a zone name, a location and a radius. --- This class implements the inherited functions from Core.Zone#ZONE_BASE taking into account the own zone format and properties. +-- This class implements the inherited functions from @{#ZONE_BASE} taking into account the own zone format and properties. -- -- ## ZONE_RADIUS constructor -- @@ -12970,27 +16648,32 @@ end -- -- @field #ZONE_RADIUS ZONE_RADIUS = { - ClassName="ZONE_RADIUS", - } + ClassName="ZONE_RADIUS", + } --- Constructor of @{#ZONE_RADIUS}, taking the zone name, the zone location and a radius. -- @param #ZONE_RADIUS self -- @param #string ZoneName Name of the zone. -- @param DCS#Vec2 Vec2 The location of the zone. -- @param DCS#Distance Radius The radius of the zone. +-- @param DCS#Boolean DoNotRegisterZone Determines if the Zone should not be registered in the _Database Table. Default=false -- @return #ZONE_RADIUS self -function ZONE_RADIUS:New( ZoneName, Vec2, Radius ) +function ZONE_RADIUS:New( ZoneName, Vec2, Radius, DoNotRegisterZone ) -- Inherit ZONE_BASE. - local self = BASE:Inherit( self, ZONE_BASE:New( ZoneName ) ) -- #ZONE_RADIUS - self:F( { ZoneName, Vec2, Radius } ) + local self = BASE:Inherit( self, ZONE_BASE:New( ZoneName ) ) -- #ZONE_RADIUS + self:F( { ZoneName, Vec2, Radius } ) - self.Radius = Radius - self.Vec2 = Vec2 + self.Radius = Radius + self.Vec2 = Vec2 - --self.Coordinate=COORDINATE:NewFromVec2(Vec2) + if not DoNotRegisterZone then + _EVENTDISPATCHER:CreateEventNewZone(self) + end + + --self.Coordinate=COORDINATE:NewFromVec2(Vec2) - return self + return self end --- Update zone from a 2D vector. @@ -13073,7 +16756,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) @@ -13097,7 +16780,6 @@ function ZONE_RADIUS:BoundZone( Points, CountryID, UnBound ) local Angle local RadialBase = math.pi*2 - -- for Angle = 0, 360, (360 / Points ) do local Radial = Angle * RadialBase / 360 Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() @@ -13127,7 +16809,6 @@ function ZONE_RADIUS:BoundZone( Points, CountryID, UnBound ) return self end - --- Smokes the zone boundaries in a color. -- @param #ZONE_RADIUS self -- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. @@ -13159,7 +16840,6 @@ function ZONE_RADIUS:SmokeZone( SmokeColor, Points, AddHeight, AngleOffset ) return self end - --- Flares the zone boundaries in a color. -- @param #ZONE_RADIUS self -- @param Utilities.Utils#FLARECOLOR FlareColor The flare color. @@ -13218,11 +16898,11 @@ end -- @param #ZONE_RADIUS self -- @return DCS#Vec2 The location of the zone. function ZONE_RADIUS:GetVec2() - self:F2( self.ZoneName ) + self:F2( self.ZoneName ) - self:T2( { self.Vec2 } ) + self:T2( { self.Vec2 } ) - return self.Vec2 + return self.Vec2 end --- Sets the @{DCS#Vec2} of the zone. @@ -13256,30 +16936,26 @@ function ZONE_RADIUS:GetVec3( Height ) return Vec3 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. -- * @{ZONE_RADIUS.IsSomeInZoneOfCoalition}(): Scan if there is some presence of units in the zone of the given coalition. -- * @{ZONE_RADIUS.IsNoneInZoneOfCoalition}(): Scan if there isn't any presence of units in the zone of an other coalition than the given one. -- * @{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() --- local IsAttacked = self.Zone:IsSomeInZoneOfCoalition( self.Coalition ) +-- myzone:Scan({Object.Category.UNIT},{Unit.Category.GROUND_UNIT}) +-- local IsAttacked = myzone:IsSomeInZoneOfCoalition( self.Coalition ) function ZONE_RADIUS:Scan( ObjectCategories, UnitCategories ) self.ScanData = {} self.ScanData.Coalitions = {} self.ScanData.Scenery = {} + self.ScanData.SceneryTable = {} self.ScanData.Units = {} local ZoneCoord = self:GetCoordinate() @@ -13342,9 +17018,11 @@ function ZONE_RADIUS:Scan( ObjectCategories, UnitCategories ) if ObjectCategory == Object.Category.SCENERY then local SceneryType = ZoneObject:getTypeName() local SceneryName = ZoneObject:getName() + --BASE:I("SceneryType "..SceneryType.."SceneryName"..SceneryName) self.ScanData.Scenery[SceneryType] = self.ScanData.Scenery[SceneryType] or {} self.ScanData.Scenery[SceneryType][SceneryName] = SCENERY:Register( SceneryName, ZoneObject ) - self:F2( { SCENERY = self.ScanData.Scenery[SceneryType][SceneryName] } ) + table.insert(self.ScanData.SceneryTable,self.ScanData.Scenery[SceneryType][SceneryName] ) + self:T( { SCENERY = self.ScanData.Scenery[SceneryType][SceneryName] } ) end end @@ -13365,7 +17043,6 @@ function ZONE_RADIUS:GetScannedUnits() return self.ScanData.Units end - --- Get a set of scanned units. -- @param #ZONE_RADIUS self -- @return Core.Set#SET_UNIT Set of units and statics inside the zone. @@ -13419,7 +17096,6 @@ function ZONE_RADIUS:GetScannedSetGroup() return self.ScanSetGroup end - --- Count the number of different coalitions inside the zone. -- @param #ZONE_RADIUS self -- @return #number Counted coalitions. @@ -13472,7 +17148,6 @@ function ZONE_RADIUS:GetScannedCoalition( Coalition ) end end - --- Get scanned scenery type -- @param #ZONE_RADIUS self -- @return #table Table of DCS scenery type objects. @@ -13480,17 +17155,34 @@ function ZONE_RADIUS:GetScannedSceneryType( SceneryType ) return self.ScanData.Scenery[SceneryType] end - --- Get scanned scenery table -- @param #ZONE_RADIUS self --- @return #table Table of DCS scenery objects. +-- @return #table Structured object table: [type].[name].SCENERY function ZONE_RADIUS:GetScannedScenery() return self.ScanData.Scenery end +--- Get table of scanned scenery objects +-- @param #ZONE_RADIUS self +-- @return #table Table of SCENERY objects. +function ZONE_RADIUS:GetScannedSceneryObjects() + return self.ScanData.SceneryTable +end + +--- Get set of scanned scenery objects +-- @param #ZONE_RADIUS self +-- @return #table Table of Wrapper.Scenery#SCENERY scenery objects. +function ZONE_RADIUS:GetScannedSetScenery() + local scenery = SET_SCENERY:New() + local objects = self:GetScannedSceneryObjects() + for _,_obj in pairs (objects) do + scenery:AddScenery(_obj) + end + return scenery +end --- Is All in Zone of Coalition? --- Check if only the specifed coalition is inside the zone and noone else. +-- Check if only the specified coalition is inside the zone and no one else. -- @param #ZONE_RADIUS self -- @param #number Coalition Coalition ID of the coalition which is checked to be the only one in the zone. -- @return #boolean True, if **only** that coalition is inside the zone and no one else. @@ -13503,7 +17195,6 @@ function ZONE_RADIUS:IsAllInZoneOfCoalition( Coalition ) return self:CountScannedCoalitions() == 1 and self:GetScannedCoalition( Coalition ) == true end - --- Is All in Zone of Other Coalition? -- Check if only one coalition is inside the zone and the specified coalition is not the one. -- You first need to use the @{#ZONE_RADIUS.Scan} method to scan the zone before it can be evaluated! @@ -13520,13 +17211,12 @@ function ZONE_RADIUS:IsAllInZoneOfOtherCoalition( Coalition ) return self:CountScannedCoalitions() == 1 and self:GetScannedCoalition( Coalition ) == nil end - --- Is Some in Zone of Coalition? --- Check if more than one coaltion is inside the zone and the specifed coalition is one of them. +-- Check if more than one coalition is inside the zone and the specified coalition is one of them. -- You first need to use the @{#ZONE_RADIUS.Scan} method to scan the zone before it can be evaluated! -- Note that once a zone has been scanned, multiple evaluations can be done on the scan result set. -- @param #ZONE_RADIUS self --- @param #number Coalition ID of the coaliton which is checked to be inside the zone. +-- @param #number Coalition ID of the coalition which is checked to be inside the zone. -- @return #boolean True if more than one coalition is inside the zone and the specified coalition is one of them. -- @usage -- self.Zone:Scan() @@ -13536,7 +17226,6 @@ function ZONE_RADIUS:IsSomeInZoneOfCoalition( Coalition ) return self:CountScannedCoalitions() > 1 and self:GetScannedCoalition( Coalition ) == true end - --- Is None in Zone of Coalition? -- You first need to use the @{#ZONE_RADIUS.Scan} method to scan the zone before it can be evaluated! -- Note that once a zone has been scanned, multiple evaluations can be done on the scan result set. @@ -13551,7 +17240,6 @@ function ZONE_RADIUS:IsNoneInZoneOfCoalition( Coalition ) return self:GetScannedCoalition( Coalition ) == nil end - --- Is None in Zone? -- You first need to use the @{#ZONE_RADIUS.Scan} method to scan the zone before it can be evaluated! -- Note that once a zone has been scanned, multiple evaluations can be done on the scan result set. @@ -13565,9 +17253,6 @@ function ZONE_RADIUS:IsNoneInZone() return self:CountScannedCoalitions() == 0 end - - - --- Searches the zone -- @param #ZONE_RADIUS self -- @param ObjectCategories A list of categories, which are members of Object.Category @@ -13608,6 +17293,8 @@ end function ZONE_RADIUS:IsVec2InZone( Vec2 ) self:F2( Vec2 ) + if not Vec2 then return false end + local ZoneVec2 = self:GetVec2() if ZoneVec2 then @@ -13625,7 +17312,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 @@ -13633,24 +17320,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. @@ -13702,20 +17419,98 @@ 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 100 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 +--- Returns a @{Core.Point#COORDINATE} object reflecting a random location within the zone where there are no **map objects** of type "Building". +-- Does not find statics you might have placed there. **Note** This might be quite CPU intensive, use with care. +-- @param #ZONE_RADIUS self +-- @param #number inner (Optional) Minimal distance from the center of the zone in meters. Default is 0m. +-- @param #number outer (Optional) Maximal distance from the outer edge of the zone in meters. Default is the radius of the zone. +-- @param #number distance (Optional) Minimum distance from any building coordinate. Defaults to 100m. +-- @param #boolean markbuildings (Optional) Place markers on found buildings (if any). +-- @param #boolean markfinal (Optional) Place marker on the final coordinate (if any). +-- @return Core.Point#COORDINATE The random coordinate or `nil` if cannot be found in 1000 iterations. +function ZONE_RADIUS:GetRandomCoordinateWithoutBuildings(inner,outer,distance,markbuildings,markfinal) + + local dist = distance or 100 + + local objects = {} + + if self.ScanData and self.ScanData.Scenery then + objects = self:GetScannedScenery() + else + self:Scan({Object.Category.SCENERY}) + objects = self:GetScannedScenery() + end + + local T0 = timer.getTime() + local T1 = timer.getTime() + local buildings = {} + if self.ScanData and self.ScanData.BuildingCoordinates then + buildings = self.ScanData.BuildingCoordinates + else + -- build table of buildings coordinates + for _,_object in pairs (objects) do + for _,_scen in pairs (_object) do + local scenery = _scen -- Wrapper.Scenery#SCENERY + local description=scenery:GetDesc() + if description and description.attributes and description.attributes.Buildings then + if markbuildings then + MARKER:New(scenery:GetCoordinate(),"Building"):ToAll() + end + buildings[#buildings+1] = scenery:GetCoordinate() + end + end + end + self.ScanData.BuildingCoordinates = buildings + end + + -- max 1000 tries + local rcoord = nil + local found = false + local iterations = 0 + + for i=1,1000 do + iterations = iterations + 1 + rcoord = self:GetRandomCoordinate(inner,outer) + found = false + for _,_coord in pairs (buildings) do + local coord = _coord -- Core.Point#COORDINATE + -- keep >50m dist from buildings + if coord:Get3DDistance(rcoord) > dist then + found = true + else + found = false + end + end + if found then + -- we have a winner! + if markfinal then + MARKER:New(rcoord,"FREE"):ToAll() + end + break + end + end + + T1=timer.getTime() + + self:T(string.format("Found a coordinate: %s | Iterations: %d | Time: %d",tostring(found),iterations,T1-T0)) + + if found then return rcoord else return nil end + +end --- @type ZONE -- @extends #ZONE_RADIUS @@ -13773,12 +17568,12 @@ 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 -- Create a new ZONE_RADIUS. - local self=BASE:Inherit( self, ZONE_RADIUS:New(ZoneName, {x=Zone.point.x, y=Zone.point.z}, Zone.radius)) + local self=BASE:Inherit( self, ZONE_RADIUS:New(ZoneName, {x=Zone.point.x, y=Zone.point.z}, Zone.radius, true)) self:F(ZoneName) -- Color of zone. @@ -13791,9 +17586,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 ) @@ -13807,7 +17602,7 @@ end -- @extends Core.Zone#ZONE_RADIUS ---- # ZONE_UNIT class, extends @{Zone#ZONE_RADIUS} +--- # ZONE_UNIT class, extends @{#ZONE_RADIUS} -- -- The ZONE_UNIT class defined by a zone attached to a @{Wrapper.Unit#UNIT} with a radius and optional offsets. -- This class implements the inherited functions from @{#ZONE_RADIUS} taking into account the own zone format and properties. @@ -13845,7 +17640,7 @@ function ZONE_UNIT:New( ZoneName, ZoneUNIT, Radius, Offset) self.relative_to_unit = Offset.relative_to_unit or false end - local self = BASE:Inherit( self, ZONE_RADIUS:New( ZoneName, ZoneUNIT:GetVec2(), Radius ) ) + local self = BASE:Inherit( self, ZONE_RADIUS:New( ZoneName, ZoneUNIT:GetVec2(), Radius, true ) ) self:F( { ZoneName, ZoneUNIT:GetVec2(), Radius } ) @@ -13947,7 +17742,7 @@ end --- The ZONE_GROUP class defines by a zone around a @{Wrapper.Group#GROUP} with a radius. The current leader of the group defines the center of the zone. --- This class implements the inherited functions from @{Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. +-- This class implements the inherited functions from @{#ZONE_RADIUS} taking into account the own zone format and properties. -- -- @field #ZONE_GROUP ZONE_GROUP = { @@ -13961,7 +17756,7 @@ ZONE_GROUP = { -- @param DCS#Distance Radius The radius of the zone. -- @return #ZONE_GROUP self function ZONE_GROUP:New( ZoneName, ZoneGROUP, Radius ) - local self = BASE:Inherit( self, ZONE_RADIUS:New( ZoneName, ZoneGROUP:GetVec2(), Radius ) ) + local self = BASE:Inherit( self, ZONE_RADIUS:New( ZoneName, ZoneGROUP:GetVec2(), Radius, true ) ) self:F( { ZoneName, ZoneGROUP:GetVec2(), Radius } ) self._.ZoneGROUP = ZoneGROUP @@ -14029,12 +17824,12 @@ 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 --- The ZONE_POLYGON_BASE class defined by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon. --- This class implements the inherited functions from @{Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. +-- This class implements the inherited functions from @{#ZONE_RADIUS} taking into account the own zone format and properties. -- This class is an abstract BASE class for derived classes, and is not meant to be instantiated. -- -- ## Zone point randomization @@ -14266,8 +18061,7 @@ function ZONE_POLYGON_BASE:BoundZone( UnBound ) return self 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. @@ -14279,35 +18073,97 @@ 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]) + if self._.Polygon and #self._.Polygon>=3 then - Color=Color or self:GetColorRGB() - Alpha=Alpha or 1 - FillColor=FillColor or Color - FillAlpha=FillAlpha or self:GetColorAlpha() + local coordinate=COORDINATE:NewFromVec2(self._.Polygon[1]) + Coalition=Coalition or self:GetDrawCoalition() - if #self._.Polygon==4 then + -- Set draw coalition. + self:SetDrawCoalition(Coalition) - local Coord2=COORDINATE:NewFromVec2(self._.Polygon[2]) - local Coord3=COORDINATE:NewFromVec2(self._.Polygon[3]) - local Coord4=COORDINATE:NewFromVec2(self._.Polygon[4]) + Color=Color or self:GetColorRGB() + Alpha=Alpha or 1 - self.DrawID=coordinate:QuadToAll(Coord2, Coord3, Coord4, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) + -- Set color. + self:SetColor(Color, Alpha) - else + 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 - local Coordinates=self:GetVerticiesCoordinates() - table.remove(Coordinates, 1) + -- Set fill color. + self:SetFillColor(FillColor, FillAlpha) - self.DrawID=coordinate:MarkupToAllFreeForm(Coordinates, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) + if #self._.Polygon==4 then - end + 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 + +--- Get the smallest circular zone encompassing all points points of the polygon zone. +-- @param #ZONE_POLYGON_BASE self +-- @param #string ZoneName (Optional) Name of the zone. Default is the name of the polygon zone. +-- @param #boolean DoNotRegisterZone (Optional) If `true`, zone is not registered. +-- @return #ZONE_RADIUS The circular zone. +function ZONE_POLYGON_BASE:GetZoneRadius(ZoneName, DoNotRegisterZone) + + local center=self:GetVec2() + + local radius=0 + + for _,_vec2 in pairs(self._.Polygon) do + local vec2=_vec2 --DCS#Vec2 + + local r=UTILS.VecDist2D(center, vec2) + + if r>radius then + radius=r + end + + end + + local zone=ZONE_RADIUS:New(ZoneName or self.ZoneName, center, radius, DoNotRegisterZone) + + return zone +end + + +--- Get the smallest rectangular zone encompassing all points points of the polygon zone. +-- @param #ZONE_POLYGON_BASE self +-- @param #string ZoneName (Optional) Name of the zone. Default is the name of the polygon zone. +-- @param #boolean DoNotRegisterZone (Optional) If `true`, zone is not registered. +-- @return #ZONE_POLYGON The rectangular zone. +function ZONE_POLYGON_BASE:GetZoneQuad(ZoneName, DoNotRegisterZone) + + local vec1, vec3=self:GetBoundingVec2() + + local vec2={x=vec1.x, y=vec3.y} + local vec4={x=vec3.x, y=vec1.y} + + local zone=ZONE_POLYGON_BASE:New(ZoneName or self.ZoneName, {vec1, vec2, vec3, vec4}) + + return zone +end + --- Smokes the zone boundaries in a color. -- @param #ZONE_POLYGON_BASE self -- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. @@ -14339,7 +18195,6 @@ function ZONE_POLYGON_BASE:SmokeZone( SmokeColor, Segments ) return self end - --- Flare the zone boundaries in a color. -- @param #ZONE_POLYGON_BASE self -- @param Utilities.Utils#FLARECOLOR FlareColor The flare color. @@ -14375,9 +18230,6 @@ function ZONE_POLYGON_BASE:FlareZone( FlareColor, Segments, Azimuth, AddHeight ) return self end - - - --- Returns if a location is within the zone. -- Source learned and taken from: https://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html -- @param #ZONE_POLYGON_BASE self @@ -14385,7 +18237,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 @@ -14409,30 +18261,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 n self._.Polygon[i].x ) and self._.Polygon[i].x or x1 + x2 = ( x2 < self._.Polygon[i].x ) and self._.Polygon[i].x or x2 + y1 = ( y1 > self._.Polygon[i].y ) and self._.Polygon[i].y or y1 + y2 = ( y2 < self._.Polygon[i].y ) and self._.Polygon[i].y or y2 + + end + + local vec1={x=x1, y=y1} + local vec2={x=x2, y=y2} + + return vec1, vec2 +end + --- Draw a frontier on the F10 map with small filled circles. -- @param #ZONE_POLYGON_BASE self -- @param #number Coalition (Optional) Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1= All. @@ -14529,7 +18423,7 @@ function ZONE_POLYGON_BASE:Boundary(Coalition, Color, Radius, Alpha, Segments, C for Segment = 0, Segments do local PointX = self._.Polygon[i].x + ( Segment * DeltaX / Segments ) local PointY = self._.Polygon[i].y + ( Segment * DeltaY / Segments ) - ZONE_RADIUS:New( "Zone", {x = PointX, y = PointY}, Radius ):DrawZone(Coalition, Color, 1, Color, Alpha, nil, true) + --ZONE_RADIUS:New( "Zone", {x = PointX, y = PointY}, Radius ):DrawZone(Coalition, Color, 1, Color, Alpha, nil, true) end end j = i @@ -14543,16 +18437,16 @@ end --- The ZONE_POLYGON class defined by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon. --- This class implements the inherited functions from @{Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. +-- This class implements the inherited functions from @{#ZONE_RADIUS} taking into account the own zone format and properties. -- -- ## Declare a ZONE_POLYGON directly in the DCS mission editor! -- --- You can declare a ZONE_POLYGON using the DCS mission editor by adding the ~ZONE_POLYGON tag in the group name. +-- You can declare a ZONE_POLYGON using the DCS mission editor by adding the #ZONE_POLYGON tag in the group name. -- --- So, imagine you have a group declared in the mission editor, with group name `DefenseZone~ZONE_POLYGON`. +-- So, imagine you have a group declared in the mission editor, with group name `DefenseZone#ZONE_POLYGON`. -- Then during mission startup, when loading Moose.lua, this group will be detected as a ZONE_POLYGON declaration. -- Within the background, a ZONE_POLYGON object will be created within the @{Core.Database} using the properties of the group. --- The ZONE_POLYGON name will be the group name without the ~ZONE_POLYGON tag. +-- The ZONE_POLYGON name will be the group name without the #ZONE_POLYGON tag. -- -- So, you can search yourself for the ZONE_POLYGON by using the @{#ZONE_POLYGON.FindByName}() method. -- In this example, `local PolygonZone = ZONE_POLYGON:FindByName( "DefenseZone" )` would return the ZONE_POLYGON object @@ -14589,6 +18483,21 @@ function ZONE_POLYGON:New( ZoneName, ZoneGroup ) return self end +--- Constructor to create a ZONE_POLYGON instance, taking the zone name and an array of DCS#Vec2, forming a polygon. +-- @param #ZONE_POLYGON self +-- @param #string ZoneName Name of the zone. +-- @param #ZONE_POLYGON_BASE.ListVec2 PointsArray An array of @{DCS#Vec2}, forming a polygon. +-- @return #ZONE_POLYGON self +function ZONE_POLYGON:NewFromPointsArray( ZoneName, PointsArray ) + + local self = BASE:Inherit( self, ZONE_POLYGON_BASE:New( ZoneName, PointsArray ) ) + self:F( { ZoneName, self._.Polygon } ) + + -- Zone objects are added to the _DATABASE and SET_ZONE objects. + _EVENTDISPATCHER:CreateEventNewZone( self ) + + return self +end --- Constructor to create a ZONE_POLYGON instance, taking the zone name and the **name** of the @{Wrapper.Group#GROUP} defined within the Mission Editor. -- The @{Wrapper.Group#GROUP} waypoints define the polygon corners. The first and the last point are automatically connected by ZONE_POLYGON. @@ -14621,14 +18530,568 @@ function ZONE_POLYGON:FindByName( ZoneName ) return ZoneFound end +--- Scan the zone for the presence of units of the given ObjectCategories. Does **not** scan for scenery at the moment. +-- Note that **only after** a zone has been scanned, the zone can be evaluated by: +-- +-- * @{ZONE_POLYGON.IsAllInZoneOfCoalition}(): Scan the presence of units in the zone of a coalition. +-- * @{ZONE_POLYGON.IsAllInZoneOfOtherCoalition}(): Scan the presence of units in the zone of an other coalition. +-- * @{ZONE_POLYGON.IsSomeInZoneOfCoalition}(): Scan if there is some presence of units in the zone of the given coalition. +-- * @{ZONE_POLYGON.IsNoneInZoneOfCoalition}(): Scan if there isn't any presence of units in the zone of an other coalition than the given one. +-- * @{ZONE_POLYGON.IsNoneInZone}(): Scan if the zone is empty. +-- @param #ZONE_POLYGON self +-- @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 +-- myzone:Scan({Object.Category.UNIT},{Unit.Category.GROUND_UNIT}) +-- local IsAttacked = myzone:IsSomeInZoneOfCoalition( self.Coalition ) +function ZONE_POLYGON:Scan( ObjectCategories, UnitCategories ) + + self.ScanData = {} + self.ScanData.Coalitions = {} + self.ScanData.Scenery = {} + self.ScanData.SceneryTable = {} + self.ScanData.Units = {} + + local vectors = self:GetBoundingSquare() + + local minVec3 = {x=vectors.x1, y=0, z=vectors.y1} + local maxVec3 = {x=vectors.x2, y=0, z=vectors.y2} + + local minmarkcoord = COORDINATE:NewFromVec3(minVec3) + local maxmarkcoord = COORDINATE:NewFromVec3(maxVec3) + local ZoneRadius = minmarkcoord:Get2DDistance(maxmarkcoord)/2 + + local CenterVec3 = self:GetCoordinate():GetVec3() + + --[[ this a bit shaky in functionality it seems + local VolumeBox = { + id = world.VolumeType.BOX, + params = { + min = minVec3, + max = maxVec3 + } + } + --]] + + local SphereSearch = { + id = world.VolumeType.SPHERE, + params = { + point = CenterVec3, + radius = ZoneRadius, + } + } + + local function EvaluateZone( ZoneObject ) + + if ZoneObject then + + local ObjectCategory = ZoneObject:getCategory() + + if ( ObjectCategory == Object.Category.UNIT and ZoneObject:isExist() and ZoneObject:isActive() ) or (ObjectCategory == Object.Category.STATIC and ZoneObject:isExist()) then + + local CoalitionDCSUnit = ZoneObject:getCoalition() + + local Include = false + if not UnitCategories then + -- Anything found is included. + Include = true + else + -- Check if found object is in specified categories. + local CategoryDCSUnit = ZoneObject:getDesc().category + + for UnitCategoryID, UnitCategory in pairs( UnitCategories ) do + if UnitCategory == CategoryDCSUnit then + Include = true + break + end + end + + end + + if Include then + + local CoalitionDCSUnit = ZoneObject:getCoalition() + + -- This coalition is inside the zone. + self.ScanData.Coalitions[CoalitionDCSUnit] = true + + self.ScanData.Units[ZoneObject] = ZoneObject + + self:F2( { Name = ZoneObject:getName(), Coalition = CoalitionDCSUnit } ) + end + end + + -- trying with box search + if ObjectCategory == Object.Category.SCENERY and self:IsVec3InZone(ZoneObject:getPoint()) then + local SceneryType = ZoneObject:getTypeName() + local SceneryName = ZoneObject:getName() + self.ScanData.Scenery[SceneryType] = self.ScanData.Scenery[SceneryType] or {} + self.ScanData.Scenery[SceneryType][SceneryName] = SCENERY:Register( SceneryName, ZoneObject ) + table.insert(self.ScanData.SceneryTable,self.ScanData.Scenery[SceneryType][SceneryName]) + self:T( { SCENERY = self.ScanData.Scenery[SceneryType][SceneryName] } ) + end + + end + + return true + end + + -- Search objects. + local inzoneunits = SET_UNIT:New():FilterZones({self}):FilterOnce() + local inzonestatics = SET_STATIC:New():FilterZones({self}):FilterOnce() + + inzoneunits:ForEach( + function(unit) + local Unit = unit --Wrapper.Unit#UNIT + local DCS = Unit:GetDCSObject() + EvaluateZone(DCS) + end + ) + + inzonestatics:ForEach( + function(static) + local Static = static --Wrapper.Static#STATIC + local DCS = Static:GetDCSObject() + EvaluateZone(DCS) + end + ) + + local searchscenery = false + for _,_type in pairs(ObjectCategories) do + if _type == Object.Category.SCENERY then + searchscenery = true + end + end + + if searchscenery then + -- Search objects. + world.searchObjects({Object.Category.SCENERY}, SphereSearch, EvaluateZone ) + end + +end + +--- Count the number of different coalitions inside the zone. +-- @param #ZONE_POLYGON self +-- @return #table Table of DCS units and DCS statics inside the zone. +function ZONE_POLYGON:GetScannedUnits() + return self.ScanData.Units +end + +--- Get a set of scanned units. +-- @param #ZONE_POLYGON self +-- @return Core.Set#SET_UNIT Set of units and statics inside the zone. +function ZONE_POLYGON:GetScannedSetUnit() + + local SetUnit = SET_UNIT:New() + + if self.ScanData then + for ObjectID, UnitObject in pairs( self.ScanData.Units ) do + local UnitObject = UnitObject -- DCS#Unit + if UnitObject:isExist() then + local FoundUnit = UNIT:FindByName( UnitObject:getName() ) + if FoundUnit then + SetUnit:AddUnit( FoundUnit ) + else + local FoundStatic = STATIC:FindByName( UnitObject:getName() ) + if FoundStatic then + SetUnit:AddUnit( FoundStatic ) + end + end + end + end + end + + return SetUnit +end + +--- Get a set of scanned units. +-- @param #ZONE_POLYGON self +-- @return Core.Set#SET_GROUP Set of groups. +function ZONE_POLYGON:GetScannedSetGroup() + + self.ScanSetGroup=self.ScanSetGroup or SET_GROUP:New() --Core.Set#SET_GROUP + + self.ScanSetGroup.Set={} + + if self.ScanData then + for ObjectID, UnitObject in pairs( self.ScanData.Units ) do + local UnitObject = UnitObject -- DCS#Unit + if UnitObject:isExist() then + + local FoundUnit=UNIT:FindByName(UnitObject:getName()) + if FoundUnit then + local group=FoundUnit:GetGroup() + self.ScanSetGroup:AddGroup(group) + end + end + end + end + + return self.ScanSetGroup +end + +--- Count the number of different coalitions inside the zone. +-- @param #ZONE_POLYGON self +-- @return #number Counted coalitions. +function ZONE_POLYGON:CountScannedCoalitions() + + local Count = 0 + + for CoalitionID, Coalition in pairs( self.ScanData.Coalitions ) do + Count = Count + 1 + end + + return Count +end + +--- Check if a certain coalition is inside a scanned zone. +-- @param #ZONE_POLYGON self +-- @param #number Coalition The coalition id, e.g. coalition.side.BLUE. +-- @return #boolean If true, the coalition is inside the zone. +function ZONE_POLYGON:CheckScannedCoalition( Coalition ) + if Coalition then + return self.ScanData.Coalitions[Coalition] + end + return nil +end + +--- Get Coalitions of the units in the Zone, or Check if there are units of the given Coalition in the Zone. +-- Returns nil if there are none to two Coalitions in the zone! +-- Returns one Coalition if there are only Units of one Coalition in the Zone. +-- Returns the Coalition for the given Coalition if there are units of the Coalition in the Zone. +-- @param #ZONE_POLYGON self +-- @return #table +function ZONE_POLYGON:GetScannedCoalition( Coalition ) + + if Coalition then + return self.ScanData.Coalitions[Coalition] + else + local Count = 0 + local ReturnCoalition = nil + + for CoalitionID, Coalition in pairs( self.ScanData.Coalitions ) do + Count = Count + 1 + ReturnCoalition = CoalitionID + end + + if Count ~= 1 then + ReturnCoalition = nil + end + + return ReturnCoalition + end +end + +--- Get scanned scenery types +-- @param #ZONE_POLYGON self +-- @return #table Table of DCS scenery type objects. +function ZONE_POLYGON:GetScannedSceneryType( SceneryType ) + return self.ScanData.Scenery[SceneryType] +end + +--- Get scanned scenery table +-- @param #ZONE_POLYGON self +-- @return #table Table of Wrapper.Scenery#SCENERY scenery objects. +function ZONE_POLYGON:GetScannedSceneryObjects() + return self.ScanData.SceneryTable +end + +--- Get scanned scenery table +-- @param #ZONE_POLYGON self +-- @return #table Structured table of [type].[name].Wrapper.Scenery#SCENERY scenery objects. +function ZONE_POLYGON:GetScannedScenery() + return self.ScanData.Scenery +end + +--- Get scanned set of scenery objects +-- @param #ZONE_POLYGON self +-- @return #table Table of Wrapper.Scenery#SCENERY scenery objects. +function ZONE_POLYGON:GetScannedSetScenery() + local scenery = SET_SCENERY:New() + local objects = self:GetScannedSceneryObjects() + for _,_obj in pairs (objects) do + scenery:AddScenery(_obj) + end + return scenery +end + +--- Is All in Zone of Coalition? +-- Check if only the specified coalition is inside the zone and noone else. +-- @param #ZONE_POLYGON self +-- @param #number Coalition Coalition ID of the coalition which is checked to be the only one in the zone. +-- @return #boolean True, if **only** that coalition is inside the zone and no one else. +-- @usage +-- self.Zone:Scan() +-- local IsGuarded = self.Zone:IsAllInZoneOfCoalition( self.Coalition ) +function ZONE_POLYGON:IsAllInZoneOfCoalition( Coalition ) + return self:CountScannedCoalitions() == 1 and self:GetScannedCoalition( Coalition ) == true +end + +--- Is All in Zone of Other Coalition? +-- Check if only one coalition is inside the zone and the specified coalition is not the one. +-- You first need to use the @{#ZONE_POLYGON.Scan} method to scan the zone before it can be evaluated! +-- Note that once a zone has been scanned, multiple evaluations can be done on the scan result set. +-- @param #ZONE_POLYGON self +-- @param #number Coalition Coalition ID of the coalition which is not supposed to be in the zone. +-- @return #boolean True, if and only if only one coalition is inside the zone and the specified coalition is not it. +-- @usage +-- self.Zone:Scan() +-- local IsCaptured = self.Zone:IsAllInZoneOfOtherCoalition( self.Coalition ) +function ZONE_POLYGON:IsAllInZoneOfOtherCoalition( Coalition ) + return self:CountScannedCoalitions() == 1 and self:GetScannedCoalition( Coalition ) == nil +end + +--- Is Some in Zone of Coalition? +-- Check if more than one coalition is inside the zone and the specified coalition is one of them. +-- You first need to use the @{#ZONE_POLYGON.Scan} method to scan the zone before it can be evaluated! +-- Note that once a zone has been scanned, multiple evaluations can be done on the scan result set. +-- @param #ZONE_POLYGON self +-- @param #number Coalition ID of the coalition which is checked to be inside the zone. +-- @return #boolean True if more than one coalition is inside the zone and the specified coalition is one of them. +-- @usage +-- self.Zone:Scan() +-- local IsAttacked = self.Zone:IsSomeInZoneOfCoalition( self.Coalition ) +function ZONE_POLYGON:IsSomeInZoneOfCoalition( Coalition ) + return self:CountScannedCoalitions() > 1 and self:GetScannedCoalition( Coalition ) == true +end + +--- Is None in Zone of Coalition? +-- You first need to use the @{#ZONE_POLYGON.Scan} method to scan the zone before it can be evaluated! +-- Note that once a zone has been scanned, multiple evaluations can be done on the scan result set. +-- @param #ZONE_POLYGON self +-- @param Coalition +-- @return #boolean +-- @usage +-- self.Zone:Scan() +-- local IsOccupied = self.Zone:IsNoneInZoneOfCoalition( self.Coalition ) +function ZONE_POLYGON:IsNoneInZoneOfCoalition( Coalition ) + return self:GetScannedCoalition( Coalition ) == nil +end + +--- Is None in Zone? +-- You first need to use the @{#ZONE_POLYGON.Scan} method to scan the zone before it can be evaluated! +-- Note that once a zone has been scanned, multiple evaluations can be done on the scan result set. +-- @param #ZONE_POLYGON self +-- @return #boolean +-- @usage +-- self.Zone:Scan() +-- local IsEmpty = self.Zone:IsNoneInZone() +function ZONE_POLYGON:IsNoneInZone() + return self:CountScannedCoalitions() == 0 +end + + +do -- ZONE_ELASTIC + + --- @type ZONE_ELASTIC + -- @field #table points Points in 2D. + -- @field #table setGroups Set of GROUPs. + -- @field #table setOpsGroups Set of OPSGROUPS. + -- @field #table setUnits Set of UNITs. + -- @field #number updateID Scheduler ID for updating. + -- @extends #ZONE_POLYGON_BASE + + --- The ZONE_ELASTIC class defines a dynamic polygon zone, where only the convex hull is used. + -- + -- @field #ZONE_ELASTIC + ZONE_ELASTIC = { + ClassName="ZONE_ELASTIC", + points={}, + setGroups={} + } + + --- Constructor to create a ZONE_ELASTIC instance. + -- @param #ZONE_ELASTIC self + -- @param #string ZoneName Name of the zone. + -- @param DCS#Vec2 Points (Optional) Fixed points. + -- @return #ZONE_ELASTIC self + function ZONE_ELASTIC:New(ZoneName, Points) + + local self=BASE:Inherit(self, ZONE_POLYGON_BASE:New(ZoneName, Points)) --#ZONE_ELASTIC + + -- Zone objects are added to the _DATABASE and SET_ZONE objects. + _EVENTDISPATCHER:CreateEventNewZone( self ) + + if Points then + self.points=Points + end + + return self + end + + --- Add a vertex (point) to the polygon. + -- @param #ZONE_ELASTIC self + -- @param DCS#Vec2 Vec2 Point in 2D (with x and y coordinates). + -- @return #ZONE_ELASTIC self + function ZONE_ELASTIC:AddVertex2D(Vec2) + + -- Add vec2 to points. + table.insert(self.points, Vec2) + + return self + end + + + --- Add a vertex (point) to the polygon. + -- @param #ZONE_ELASTIC self + -- @param DCS#Vec3 Vec3 Point in 3D (with x, y and z coordinates). Only the x and z coordinates are used. + -- @return #ZONE_ELASTIC self + function ZONE_ELASTIC:AddVertex3D(Vec3) + + -- Add vec2 from vec3 to points. + table.insert(self.points, {x=Vec3.x, y=Vec3.z}) + + return self + end + + + --- Add a set of groups. Positions of the group will be considered as polygon vertices when contructing the convex hull. + -- @param #ZONE_ELASTIC self + -- @param Core.Set#SET_GROUP SetGroup Set of groups. + -- @return #ZONE_ELASTIC self + function ZONE_ELASTIC:AddSetGroup(GroupSet) + + -- Add set to table. + table.insert(self.setGroups, GroupSet) + + return self + end + + + --- Update the convex hull of the polygon. + -- This uses the [Graham scan](https://en.wikipedia.org/wiki/Graham_scan). + -- @param #ZONE_ELASTIC self + -- @param #number Delay Delay in seconds before the zone is updated. Default 0. + -- @param #boolean Draw Draw the zone. Default `nil`. + -- @return #ZONE_ELASTIC self + function ZONE_ELASTIC:Update(Delay, Draw) + + -- Debug info. + self:T(string.format("Updating ZONE_ELASTIC %s", tostring(self.ZoneName))) + + -- Copy all points. + local points=UTILS.DeepCopy(self.points or {}) + + if self.setGroups then + for _,_setGroup in pairs(self.setGroups) do + local setGroup=_setGroup --Core.Set#SET_GROUP + for _,_group in pairs(setGroup.Set) do + local group=_group --Wrapper.Group#GROUP + if group and group:IsAlive() then + table.insert(points, group:GetVec2()) + end + end + end + end + + -- Update polygon verticies from points. + self._.Polygon=self:_ConvexHull(points) + + if Draw~=false then + if self.DrawID or Draw==true then + self:UndrawZone() + self:DrawZone() + end + end + + return self + end + + --- Start the updating scheduler. + -- @param #ZONE_ELASTIC self + -- @param #number Tstart Time in seconds before the updating starts. + -- @param #number dT Time interval in seconds between updates. Default 60 sec. + -- @param #number Tstop Time in seconds after which the updating stops. Default `nil`. + -- @param #boolean Draw Draw the zone. Default `nil`. + -- @return #ZONE_ELASTIC self + function ZONE_ELASTIC:StartUpdate(Tstart, dT, Tstop, Draw) + + self.updateID=self:ScheduleRepeat(Tstart, dT, 0, Tstop, ZONE_ELASTIC.Update, self, 0, Draw) + + return self + end + + --- Stop the updating scheduler. + -- @param #ZONE_ELASTIC self + -- @param #number Delay Delay in seconds before the scheduler will be stopped. Default 0. + -- @return #ZONE_ELASTIC self + function ZONE_ELASTIC:StopUpdate(Delay) + + if Delay and Delay>0 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 --- The ZONE_AIRBASE class defines by a zone around a @{Wrapper.Airbase#AIRBASE} with a radius. - -- This class implements the inherited functions from @{Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. + -- This class implements the inherited functions from @{#ZONE_RADIUS} taking into account the own zone format and properties. -- -- @field #ZONE_AIRBASE ZONE_AIRBASE = { @@ -14648,10 +19111,24 @@ do -- ZONE_AIRBASE local Airbase = AIRBASE:FindByName( AirbaseName ) - local self = BASE:Inherit( self, ZONE_RADIUS:New( AirbaseName, Airbase:GetVec2(), Radius ) ) + local self = BASE:Inherit( self, ZONE_RADIUS:New( AirbaseName, Airbase:GetVec2(), Radius, true ) ) 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 ) @@ -14666,9 +19143,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 ) @@ -14686,24 +19163,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. @@ -14719,11 +19178,12 @@ do -- ZONE_AIRBASE return PointVec2 end - end +--- **Core** - The ZONE_DETECTION class, defined by a zone name, a detection object and a radius. +-- @module Core.Zone_Detection +-- @image MOOSE.JPG ---- The ZONE_DETECTION class, defined by a zone name, a detection object and a radius. --- @type ZONE_DETECTION +--- @type ZONE_DETECTION -- @field DCS#Vec2 Vec2 The current location of the zone. -- @field DCS#Distance Radius The radius of the zone. -- @extends #ZONE_BASE @@ -14752,7 +19212,7 @@ function ZONE_DETECTION:New( ZoneName, Detection, Radius ) self.Detection = Detection self.Radius = Radius - + return self end @@ -14771,15 +19231,14 @@ function ZONE_DETECTION:BoundZone( Points, CountryID, UnBound ) local Angle local RadialBase = math.pi*2 - - -- + for Angle = 0, 360, (360 / Points ) do local Radial = Angle * RadialBase / 360 Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() - + local CountryName = _DATABASE.COUNTRY_NAME[CountryID] - + local Tire = { ["country"] = CountryName, ["category"] = "Fortifications", @@ -14977,7 +19436,7 @@ end -- * PLAYERS -- * CARGOS -- --- On top, for internal MOOSE administration purposes, the DATBASE administers the Unit and Group TEMPLATES as defined within the Mission Editor. +-- On top, for internal MOOSE administration purposes, the DATABASE administers the Unit and Group TEMPLATES as defined within the Mission Editor. -- -- The singleton object **_DATABASE** is automatically created by MOOSE, that administers all objects within the mission. -- Moose refers to **_DATABASE** within the framework extensively, but you can also refer to the _DATABASE object within your missions if required. @@ -15050,6 +19509,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 ) @@ -15062,8 +19522,8 @@ function DATABASE:New() self:_RegisterGroupsAndUnits() self:_RegisterClients() self:_RegisterStatics() - self:_RegisterAirbases() --self:_RegisterPlayers() + --self:_RegisterAirbases() self.UNITS_Position = 0 @@ -15088,17 +19548,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] @@ -15108,7 +19562,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 @@ -15143,16 +19596,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. @@ -15188,7 +19631,7 @@ end do -- Zones - --- Finds a @{Zone} based on the zone name. + --- Finds a @{Core.Zone} based on the zone name. -- @param #DATABASE self -- @param #string ZoneName The name of the zone. -- @return Core.Zone#ZONE_BASE The found ZONE. @@ -15198,7 +19641,7 @@ do -- Zones return ZoneFound end - --- Adds a @{Zone} based on the zone name in the DATABASE. + --- Adds a @{Core.Zone} based on the zone name in the DATABASE. -- @param #DATABASE self -- @param #string ZoneName The name of the zone. -- @param Core.Zone#ZONE_BASE Zone The zone. @@ -15210,7 +19653,7 @@ do -- Zones end - --- Deletes a @{Zone} from the DATABASE based on the zone name. + --- Deletes a @{Core.Zone} from the DATABASE based on the zone name. -- @param #DATABASE self -- @param #string ZoneName The name of the zone. function DATABASE:DeleteZone( ZoneName ) @@ -15226,23 +19669,23 @@ do -- Zones for ZoneID, ZoneData in pairs(env.mission.triggers.zones) do local ZoneName = ZoneData.name - + -- Color local color=ZoneData.color or {1, 0, 0, 0.15} - + -- Create new Zone local Zone=nil --Core.Zone#ZONE_BASE - + if ZoneData.type==0 then - + --- -- Circular zone --- - + self:I(string.format("Register ZONE: %s (Circular)", ZoneName)) - + Zone=ZONE:New(ZoneName) - + else --- @@ -15250,64 +19693,127 @@ do -- Zones --- self:I(string.format("Register ZONE: %s (Polygon, Quad)", ZoneName)) - - Zone=ZONE_POLYGON_BASE:New(ZoneName, ZoneData.verticies) - + + Zone=ZONE_POLYGON:NewFromPointsArray(ZoneName, ZoneData.verticies) + --for i,vec2 in pairs(ZoneData.verticies) do -- local coord=COORDINATE:NewFromVec2(vec2) -- coord:MarkToAll(string.format("%s Point %d", ZoneName, i)) --end - + end - + if Zone then - -- Store color of zone. + -- Store color of zone. Zone.Color=color - + + -- Store zone ID. + Zone.ZoneID=ZoneData.zoneId + + -- Store zone properties (if any) + local ZoneProperties = ZoneData.properties or nil + Zone.Properties = {} + if ZoneName and ZoneProperties then + for _,ZoneProp in ipairs(ZoneProperties) do + if ZoneProp.key then + Zone.Properties[ZoneProp.key] = ZoneProp.value + end + end + end + -- Store in DB. self.ZONENAMES[ZoneName] = ZoneName - + -- Add zone. self:AddZone(ZoneName, Zone) - + end - + end -- Polygon zones defined by late activated groups. for ZoneGroupName, ZoneGroup in pairs( self.GROUPS ) do if ZoneGroupName:match("#ZONE_POLYGON") then - + local ZoneName1 = ZoneGroupName:match("(.*)#ZONE_POLYGON") local ZoneName2 = ZoneGroupName:match(".*#ZONE_POLYGON(.*)") local ZoneName = ZoneName1 .. ( ZoneName2 or "" ) -- Debug output self:I(string.format("Register ZONE: %s (Polygon)", ZoneName)) - + -- Create a new polygon zone. local Zone_Polygon = ZONE_POLYGON:New( ZoneName, ZoneGroup ) - + -- Set color. Zone_Polygon:SetColor({1, 0, 0}, 0.15) - + -- Store name in DB. self.ZONENAMES[ZoneName] = ZoneName - + -- Add zone to DB. self:AddZone( ZoneName, Zone_Polygon ) end end - end - + -- Drawings as zones + if env.mission.drawings and env.mission.drawings.layers then + + -- Loop over layers. + for layerID, layerData in pairs(env.mission.drawings.layers or {}) do + + -- Loop over objects in layers. + for objectID, objectData in pairs(layerData.objects or {}) do + + -- Check for polygon which has at least 4 points (we would need 3 but the origin seems to be there twice) + if objectData.polygonMode=="free" and objectData.points and #objectData.points>=4 then + + -- Name of the zone. + local ZoneName=objectData.name or "Unknown Drawing Zone" + + -- Reference point. All other points need to be translated by this. + local vec2={x=objectData.mapX, y=objectData.mapY} + + -- Copy points array. + local points=UTILS.DeepCopy(objectData.points) + + -- Translate points. + for i,_point in pairs(points) do + local point=_point --DCS#Vec2 + points[i]=UTILS.Vec2Add(point, vec2) + end + + -- Remove last point. + table.remove(points, #points) + + -- Debug output + self:I(string.format("Register ZONE: %s (Polygon drawing with %d verticies)", ZoneName, #points)) + + -- Create new polygon zone. + local Zone=ZONE_POLYGON:NewFromPointsArray(ZoneName, points) + + -- Set color. + Zone:SetColor({1, 0, 0}, 0.15) + + -- Store in DB. + self.ZONENAMES[ZoneName] = ZoneName + + -- Add zone. + self:AddZone(ZoneName, Zone) + + end + end + end + + end + end end -- zone do -- Zone_Goal - --- Finds a @{Zone} based on the zone name. + --- Finds a @{Core.Zone} based on the zone name. -- @param #DATABASE self -- @param #string ZoneName The name of the zone. -- @return Core.Zone#ZONE_BASE The found ZONE. @@ -15317,7 +19823,7 @@ do -- Zone_Goal return ZoneFound end - --- Adds a @{Zone} based on the zone name in the DATABASE. + --- Adds a @{Core.Zone} based on the zone name in the DATABASE. -- @param #DATABASE self -- @param #string ZoneName The name of the zone. -- @param Core.Zone#ZONE_BASE Zone The zone. @@ -15329,7 +19835,7 @@ do -- Zone_Goal end - --- Deletes a @{Zone} from the DATABASE based on the zone name. + --- Deletes a @{Core.Zone} from the DATABASE based on the zone name. -- @param #DATABASE self -- @param #string ZoneName The name of the zone. function DATABASE:DeleteZoneGoal( ZoneName ) @@ -15704,7 +20210,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 {} @@ -15712,7 +20220,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 @@ -15756,7 +20264,7 @@ function DATABASE:GetStaticUnitTemplate( StaticName ) return UnitTemplate, self.Templates.Statics[StaticName].CoalitionID, self.Templates.Statics[StaticName].CategoryID, self.Templates.Statics[StaticName].CountryID else self:E("ERROR: Static unit template does NOT exist for static "..tostring(StaticName)) - return nil + return nil end end @@ -15782,10 +20290,24 @@ function DATABASE:GetGroupTemplateFromUnitName( UnitName ) return self.Templates.Units[UnitName].GroupTemplate else self:E("ERROR: Unit template does not exist for unit "..tostring(UnitName)) - return nil + return nil 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. @@ -15860,13 +20382,13 @@ end function DATABASE:_RegisterGroupsAndUnits() local CoalitionsData = { GroupsRed = coalition.getGroups( coalition.side.RED ), GroupsBlue = coalition.getGroups( coalition.side.BLUE ), GroupsNeutral = coalition.getGroups( coalition.side.NEUTRAL ) } - + for CoalitionId, CoalitionData in pairs( CoalitionsData ) do - + for DCSGroupId, DCSGroup in pairs( CoalitionData ) do if DCSGroup:isExist() then - + -- Group name. local DCSGroupName = DCSGroup:getName() @@ -15879,11 +20401,11 @@ function DATABASE:_RegisterGroupsAndUnits() -- Get unit name. local DCSUnitName = DCSUnit:getName() - + -- Add unit. self:I(string.format("Register Unit: %s", tostring(DCSUnitName))) self:AddUnit( DCSUnitName ) - + end else self:E({"Group does not exist: ", DCSGroup}) @@ -15902,13 +20424,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)} @@ -15936,34 +20460,44 @@ end function DATABASE:_RegisterAirbases() for DCSAirbaseId, DCSAirbase in pairs(world.getAirbases()) do - + + self:_RegisterAirbase(DCSAirbase) + + end + + return self +end + +--- Register a DCS airbase. +-- @param #DATABASE self +-- @param DCS#Airbase airbase Airbase. +-- @return #DATABASE self +function DATABASE:_RegisterAirbase(airbase) + + if airbase then + -- Get the airbase name. - local DCSAirbaseName = DCSAirbase:getName() + local DCSAirbaseName = airbase:getName() -- This gave the incorrect value to be inserted into the airdromeID for DCS 2.5.6. Is fixed now. - local airbaseID=DCSAirbase:getID() + local airbaseID=airbase:getID() -- Add and register airbase. local airbase=self:AddAirbase( DCSAirbaseName ) - + -- Unique ID. - local airbaseUID=airbase:GetID(true) + 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]) end end - text=text.."]" + 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 @@ -15979,74 +20513,77 @@ function DATABASE:_EventOnBirth( Event ) self:F( { Event } ) if Event.IniDCSUnit then - - if Event.IniObjectCategory == 3 then - + + if Event.IniObjectCategory == Object.Category.STATIC then + + -- Add static object to DB. self:AddStatic( Event.IniDCSUnitName ) - + else - - if Event.IniObjectCategory == 1 then - + + if Event.IniObjectCategory == Object.Category.UNIT then + + -- Add unit and group to DB. self:AddUnit( Event.IniDCSUnitName ) self:AddGroup( Event.IniDCSGroupName ) - - -- Add airbase if it was spawned later in the mission. + + -- A unit can also be an airbase (e.g. ships). local DCSAirbase = Airbase.getByName(Event.IniDCSUnitName) if DCSAirbase then + -- Add airbase if it was spawned later in the mission. self:I(string.format("Adding airbase %s", tostring(Event.IniDCSUnitName))) self:AddAirbase(Event.IniDCSUnitName) end - + end end - - if Event.IniObjectCategory == 1 then - + + if Event.IniObjectCategory == Object.Category.UNIT then + Event.IniUnit = self:FindUnit( Event.IniDCSUnitName ) Event.IniGroup = self:FindGroup( Event.IniDCSGroupName ) - + -- Client local client=self.CLIENTS[Event.IniDCSUnitName] --Wrapper.Client#CLIENT - + if client then -- TODO: create event ClientAlive - end - - -- Get player name. + end + + -- Get player name. local PlayerName = Event.IniUnit:GetPlayerName() - + if PlayerName then - + -- Debug info. self:I(string.format("Player '%s' joint unit '%s' of group '%s'", tostring(PlayerName), tostring(Event.IniDCSUnitName), tostring(Event.IniDCSGroupName))) - + -- Add client in case it does not exist already. if not client then client=self:AddClient(Event.IniDCSUnitName) end - + -- Add player. client:AddPlayer(PlayerName) - + -- Add player. if not self.PLAYERS[PlayerName] then self:AddPlayer( Event.IniUnitName, PlayerName ) end - + -- Player settings. local Settings = SETTINGS:Set( PlayerName ) Settings:SetPlayerMenu(Event.IniUnit) - + -- Create an event. self:CreateEventPlayerEnterAircraft(Event.IniUnit) - + end - + end - + end - + end @@ -16056,48 +20593,64 @@ end function DATABASE:_EventOnDeadOrCrash( Event ) if Event.IniDCSUnit then - + local name=Event.IniDCSUnitName - + if Event.IniObjectCategory == 3 then - + --- -- STATICS --- - + 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 - + if Event.IniObjectCategory == 1 then - + --- -- UNITS --- - + -- Delete unit. if self.UNITS[Event.IniDCSUnitName] then self:DeleteUnit(Event.IniDCSUnitName) end - + -- Remove client players. local client=self.CLIENTS[name] --Wrapper.Client#CLIENT - + if client then client:RemovePlayers() end - + end end - + -- Add airbase if it was spawned later in the mission. local airbase=self.AIRBASES[Event.IniDCSUnitName] --Wrapper.Airbase#AIRBASE if airbase and (airbase:IsHelipad() or airbase:IsShip()) then self:DeleteAirbase(Event.IniDCSUnitName) end - + end -- Account destroys. @@ -16113,28 +20666,28 @@ function DATABASE:_EventOnPlayerEnterUnit( Event ) if Event.IniDCSUnit then if Event.IniObjectCategory == 1 then - + -- Add unit. self:AddUnit( Event.IniDCSUnitName ) - + -- Ini unit. Event.IniUnit = self:FindUnit( Event.IniDCSUnitName ) - + -- Add group. self:AddGroup( Event.IniDCSGroupName ) - + -- Get player unit. local PlayerName = Event.IniDCSUnit:getPlayerName() - + if PlayerName then - + if not self.PLAYERS[PlayerName] then self:AddPlayer( Event.IniDCSUnitName, PlayerName ) end - + local Settings = SETTINGS:Set( PlayerName ) Settings:SetPlayerMenu( Event.IniUnit ) - + else self:E("ERROR: getPlayerName() returned nil for event PlayerEnterUnit") end @@ -16150,30 +20703,30 @@ function DATABASE:_EventOnPlayerLeaveUnit( Event ) self:F2( { Event } ) if Event.IniUnit then - + if Event.IniObjectCategory == 1 then - + -- Try to get the player name. This can be buggy for multicrew aircraft! local PlayerName = Event.IniUnit:GetPlayerName() - + if PlayerName then --and self.PLAYERS[PlayerName] then - + -- Debug info. self:I(string.format("Player '%s' left unit %s", tostring(PlayerName), tostring(Event.IniUnitName))) - + -- Remove player menu. local Settings = SETTINGS:Set( PlayerName ) Settings:RemovePlayerMenu(Event.IniUnit) - + -- Delete player. self:DeletePlayer(Event.IniUnit, PlayerName) - + -- Client stuff. local client=self.CLIENTS[Event.IniDCSUnitName] --Wrapper.Client#CLIENT if client then client:RemovePlayer(PlayerName) end - + end end end @@ -16400,19 +20953,35 @@ 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.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 an OPS group (FLIGHTGROUP, ARMYGROUP, NAVYGROUP) from the data base. -- @param #DATABASE self --- @param Ops.FlightGroup#FLIGHTGROUP flightgroup -function DATABASE:AddFlightGroup(flightgroup) - self:I({NewFlightGroup=flightgroup.groupname}) - self.FLIGHTGROUPS[flightgroup.groupname]=flightgroup +-- @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 + else + groupname=groupname:GetName() + end + + --env.info("Getting OPSGROUP "..tostring(groupname)) + return self.FLIGHTGROUPS[groupname] end ---- Get a flight group from the data base. +--- Find an OPSGROUP (FLIGHTGROUP, ARMYGROUP, NAVYGROUP) in 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:FindOpsGroup(groupname) -- Get group and group name. if type(groupname)=="string" then @@ -16420,9 +20989,37 @@ 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 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 @@ -16506,13 +21103,13 @@ function DATABASE:_RegisterTemplates() for group_num, Template in pairs(obj_type_data.group) do 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 - + self:_RegisterStaticTemplate(Template, CoalitionSide, _DATABASECategory[string.lower(CategoryName)], CountryID) - + end --if GroupTemplate and GroupTemplate.units then end --for group_num, GroupTemplate in pairs(obj_type_data.group) do end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then @@ -16636,12 +21233,13 @@ 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 @{Wrapper.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. +-- * @{#SET_SCENERY}: Defines a collection of @{Wrapper.Scenery}s added via a filtered @{#SET_ZONE}. -- -- These classes are derived from @{#SET_BASE}, which contains the main methods to manage the collections. -- @@ -16653,27 +21251,26 @@ end -- === -- -- ### Author: **FlightControl** --- ### Contributions: **funkyfranky** +-- ### Contributions: **funkyfranky**, **applevangelist** -- -- === -- -- @module Core.Set -- @image Core_Sets.JPG - 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 + -- @field #SET_BASE.Filters Filter Filters -- @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. @@ -16684,7 +21281,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 @@ -16695,11 +21292,15 @@ do -- SET_BASE List = {}, Index = {}, Database = nil, - CallScheduler=nil, - TimeInterval=nil, - YieldInterval=nil, + CallScheduler = nil, + TimeInterval = nil, + YieldInterval = nil, } + --- Filters + -- @type SET_BASE.Filters + -- @field #table Coalition Coalitions + -- @field #table Prefix Prefixes. --- Creates a new SET_BASE object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. -- @param #SET_BASE self @@ -16725,8 +21326,7 @@ do -- SET_BASE -- @param #string ObjectName The name of the object. -- @param Object The object. - - self:AddTransition( "*", "Added", "*" ) + self:AddTransition( "*", "Added", "*" ) --- Removed Handler OnAfter for SET_BASE -- @function [parent=#SET_BASE] OnAfterRemoved @@ -16737,7 +21337,7 @@ do -- SET_BASE -- @param #string ObjectName The name of the object. -- @param Object The object. - self:AddTransition( "*", "Removed", "*" ) + self:AddTransition( "*", "Removed", "*" ) self.YieldInterval = 10 self.TimeInterval = 0.001 @@ -16754,18 +21354,17 @@ do -- SET_BASE --- Clear the Objects in the Set. -- @param #SET_BASE self + -- @param #boolean TriggerEvent If `true`, an event remove is triggered for each group that is removed from the set. -- @return #SET_BASE self - function SET_BASE:Clear() + function SET_BASE:Clear(TriggerEvent) for Name, Object in pairs( self.Set ) do - self:Remove( Name ) + self:Remove( Name, not TriggerEvent ) end return self end - - --- Finds an @{Core.Base#BASE} object based on the object Name. -- @param #SET_BASE self -- @param #string ObjectName @@ -16776,7 +21375,6 @@ do -- SET_BASE return ObjectFound end - --- Gets the Set. -- @param #SET_BASE self -- @return #SET_BASE self @@ -16788,8 +21386,8 @@ do -- SET_BASE --- Gets a list of the Names of the Objects in the Set. -- @param #SET_BASE self - -- @return #SET_BASE self - function SET_BASE:GetSetNames() -- R2.3 + -- @return #table Table of names. + function SET_BASE:GetSetNames() -- R2.3 self:F2() local Names = {} @@ -16801,11 +21399,10 @@ do -- SET_BASE return Names end - - --- Gets a list of the Objects in the Set. + --- Returns a table of the Objects in the Set. -- @param #SET_BASE self - -- @return #SET_BASE self - function SET_BASE:GetSetObjects() -- R2.3 + -- @return #table Table of objects. + function SET_BASE:GetSetObjects() -- R2.3 self:F2() local Objects = {} @@ -16817,17 +21414,24 @@ do -- SET_BASE return Objects end - --- Removes a @{Core.Base#BASE} object from the @{Core.Set#SET_BASE} and derived classes, based on the Object Name. -- @param #SET_BASE self -- @param #string ObjectName - -- @param NoTriggerEvent (optional) When `true`, the :Remove() method will not trigger a **Removed** event. + -- @param #boolean 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 + else + TriggerEvent = true + end + local Object = self.Set[ObjectName] if Object then + for Index, Key in ipairs( self.Index ) do if Key == ObjectName then table.remove( self.Index, Index ) @@ -16835,21 +21439,23 @@ do -- SET_BASE break 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 end - --- Adds a @{Core.Base#BASE} object in the @{Core.Set#SET_BASE}, using a given ObjectName as the index. -- @param #SET_BASE self -- @param #string ObjectName The name of the object. -- @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 @@ -16879,21 +21485,49 @@ 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 a= Limit then - break - end - -- if Count % self.YieldInterval == 0 then - -- coroutine.yield( false ) - -- end + else + IteratorFunction( Object, unpack( arg ) ) + end + Count = Count + 1 + if Count >= Limit then + break + end + -- if Count % self.YieldInterval == 0 then + -- coroutine.yield( false ) + -- end end return true end - -- local co = coroutine.create( CoRoutine ) + -- local co = coroutine.create( CoRoutine ) local co = CoRoutine local function Schedule() - -- local status, res = coroutine.resume( co ) + -- local status, res = coroutine.resume( co ) local status, res = co() self:T3( { status, res } ) @@ -17368,50 +22001,49 @@ do -- SET_BASE return false end - --self.CallScheduler:Schedule( self, Schedule, {}, self.TimeInterval, self.TimeInterval, 0 ) + -- self.CallScheduler:Schedule( self, Schedule, {}, self.TimeInterval, self.TimeInterval, 0 ) Schedule() return self end - ----- Iterate the SET_BASE and call an interator function for each **alive** unit, providing the Unit and optional parameters. + ----- Iterate the SET_BASE and call an iterator function for each **alive** unit, providing the Unit and optional parameters. ---- @param #SET_BASE self ---- @param #function IteratorFunction The function that will be called when there is an alive unit in the SET_BASE. The function needs to accept a UNIT parameter. ---- @return #SET_BASE self - --function SET_BASE:ForEachDCSUnitAlive( IteratorFunction, ... ) + -- function SET_BASE:ForEachDCSUnitAlive( IteratorFunction, ... ) -- self:F3( arg ) -- -- self:ForEach( IteratorFunction, arg, self.DCSUnitsAlive ) -- -- return self - --end + -- end -- - ----- Iterate the SET_BASE and call an interator function for each **alive** player, providing the Unit of the player and optional parameters. + ----- Iterate the SET_BASE and call an iterator function for each **alive** player, providing the Unit of the player and optional parameters. ---- @param #SET_BASE self ---- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_BASE. The function needs to accept a UNIT parameter. ---- @return #SET_BASE self - --function SET_BASE:ForEachPlayer( IteratorFunction, ... ) + -- function SET_BASE:ForEachPlayer( IteratorFunction, ... ) -- self:F3( arg ) -- -- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) -- -- return self - --end + -- end -- -- - ----- Iterate the SET_BASE and call an interator function for each client, providing the Client to the function and optional parameters. + ----- Iterate the SET_BASE and call an iterator function for each client, providing the Client to the function and optional parameters. ---- @param #SET_BASE self ---- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_BASE. The function needs to accept a CLIENT parameter. ---- @return #SET_BASE self - --function SET_BASE:ForEachClient( IteratorFunction, ... ) + -- function SET_BASE:ForEachClient( IteratorFunction, ... ) -- self:F3( arg ) -- -- self:ForEach( IteratorFunction, arg, self.Clients ) -- -- return self - --end - + -- end --- Decides whether to include the Object. -- @param #SET_BASE self @@ -17423,14 +22055,31 @@ do -- SET_BASE return true end - --- Decides whether to include the Object. + --- Decides whether an object is in the SET + -- @param #SET_BASE self + -- @param #table Object + -- @return #boolean `true` if object is in set and `false` otherwise. + function SET_BASE:IsInSet( Object ) + self:F3( Object ) + local outcome = false + local name = Object:GetName() + self:ForEach( + function(object) + if object:GetName() == name then + outcome = true + end + end + ) + return outcome + end + + --- Decides whether an object is **not** in the SET -- @param #SET_BASE self -- @param #table Object -- @return #SET_BASE self - function SET_BASE:IsInSet(ObjectName) + function SET_BASE:IsNotInSet( Object ) self:F3( Object ) - - return true + return not self:IsInSet(Object) end --- Gets a string with all the object names. @@ -17449,7 +22098,7 @@ do -- SET_BASE --- Flushes the current SET_BASE contents in the log ... (for debugging reasons). -- @param #SET_BASE self - -- @param Core.Base#BASE MasterObject (optional) The master object as a reference. + -- @param Core.Base#BASE MasterObject (Optional) The master object as a reference. -- @return #string A string with the names of the objects. function SET_BASE:Flush( MasterObject ) self:F3() @@ -17465,7 +22114,6 @@ do -- SET_BASE end - do -- SET_GROUP --- @type SET_GROUP @@ -17507,17 +22155,13 @@ do -- SET_GROUP -- * @{#SET_GROUP.FilterCategoryGround}: Builds the SET_GROUP from ground vehicles or infantry. -- * @{#SET_GROUP.FilterCategoryShip}: Builds the SET_GROUP from ships. -- * @{#SET_GROUP.FilterCategoryStructure}: Builds the SET_GROUP from structures. - -- + -- * @{#SET_GROUP.FilterZones}: Builds the SET_GROUP with the groups within a @{Core.Zone#ZONE}. -- -- Once the filter criteria have been set for the SET_GROUP, you can start filtering using: -- -- * @{#SET_GROUP.FilterStart}: Starts the filtering of the groups within the SET_GROUP and add or remove GROUP objects **dynamically**. -- * @{#SET_GROUP.FilterOnce}: Filters of the groups **once**. -- - -- Planned filter criteria within development are (so these are not yet available): - -- - -- * @{#SET_GROUP.FilterZones}: Builds the SET_GROUP with the groups within a @{Core.Zone#ZONE}. - -- -- ## SET_GROUP iterators -- -- Once the filters have been defined and the SET_GROUP has been built, you can iterate the SET_GROUP with the available iterator methods. @@ -17525,9 +22169,9 @@ do -- SET_GROUP -- The following iterator methods are currently available within the SET_GROUP: -- -- * @{#SET_GROUP.ForEachGroup}: Calls a function for each alive group it finds within the SET_GROUP. - -- * @{#SET_GROUP.ForEachGroupCompletelyInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence completely in a @{Zone}, providing the GROUP and optional parameters to the called function. - -- * @{#SET_GROUP.ForEachGroupPartlyInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence partly in a @{Zone}, providing the GROUP and optional parameters to the called function. - -- * @{#SET_GROUP.ForEachGroupNotInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence not in a @{Zone}, providing the GROUP and optional parameters to the called function. + -- * @{#SET_GROUP.ForEachGroupCompletelyInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence completely in a @{Core.Zone}, providing the GROUP and optional parameters to the called function. + -- * @{#SET_GROUP.ForEachGroupPartlyInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence partly in a @{Core.Zone}, providing the GROUP and optional parameters to the called function. + -- * @{#SET_GROUP.ForEachGroupNotInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence not in a @{Core.Zone}, providing the GROUP and optional parameters to the called function. -- -- -- ## SET_GROUP trigger events on the GROUP objects. @@ -17537,7 +22181,7 @@ do -- SET_GROUP -- ### When a GROUP object crashes or is dead, the SET_GROUP will trigger a **Dead** event. -- -- You can handle the event using the OnBefore and OnAfter event handlers. - -- The event handlers need to have the paramters From, Event, To, GroupObject. + -- The event handlers need to have the parameters From, Event, To, GroupObject. -- The GroupObject is the GROUP object that is dead and within the SET_GROUP, and is passed as a parameter to the event handler. -- See the following example: -- @@ -17552,7 +22196,7 @@ do -- SET_GROUP -- end -- -- While this is a good example, there is a catch. - -- Imageine you want to execute the code above, the the self would need to be from the object declared outside (above) the OnAfterDead method. + -- Imagine you want to execute the code above, the the self would need to be from the object declared outside (above) the OnAfterDead method. -- So, the self would need to contain another object. Fortunately, this can be done, but you must use then the **`.`** notation for the method. -- See the modified example: -- @@ -17584,6 +22228,7 @@ do -- SET_GROUP Categories = nil, Countries = nil, GroupPrefixes = nil, + Zones = nil, }, FilterMeta = { Coalitions = { @@ -17601,7 +22246,6 @@ do -- SET_GROUP }, } - --- Creates a new SET_GROUP object, building a set of groups belonging to a coalitions, categories, countries, types or with defined prefix names. -- @param #SET_GROUP self -- @return #SET_GROUP @@ -17618,9 +22262,9 @@ do -- SET_GROUP return self end - --- Gets the Set. + --- Get a *new* set that only contains alive groups. -- @param #SET_GROUP self - -- @return #SET_GROUP self + -- @return #SET_GROUP Set of alive groups. function SET_GROUP:GetAliveSet() self:F2() @@ -17628,7 +22272,7 @@ do -- SET_GROUP -- Clean the Set before returning with only the alive Groups. for GroupName, GroupObject in pairs( self.Set ) do - local GroupObject=GroupObject --Wrapper.Group#GROUP + local GroupObject = GroupObject -- Wrapper.Group#GROUP if GroupObject then if GroupObject:IsAlive() then AliveSet:Add( GroupName, GroupObject ) @@ -17676,16 +22320,22 @@ do -- SET_GROUP -- Note that for each unit in the group that is set, a default cargo bay limit is initialized. -- @param Core.Set#SET_GROUP self -- @param Wrapper.Group#GROUP group The group which should be added to the set. + -- @param #boolean DontSetCargoBayLimit If true, do not attempt to auto-add the cargo bay limit per unit in this group. -- @return Core.Set#SET_GROUP self - function SET_GROUP:AddGroup( group ) + function SET_GROUP:AddGroup( group, DontSetCargoBayLimit ) self:Add( group:GetName(), group ) - - -- I set the default cargo bay weight limit each time a new group is added to the set. - for UnitID, UnitData in pairs( group:GetUnits() ) do - UnitData:SetCargoBayWeightLimit() + + if not DontSetCargoBayLimit then + -- I set the default cargo bay weight limit each time a new group is added to the set. + -- TODO Why is this here in the first place? + for UnitID, UnitData in pairs( group:GetUnits() ) do + if UnitData and UnitData:IsAlive() then + UnitData:SetCargoBayWeightLimit() + end + end end - + return self end @@ -17695,7 +22345,7 @@ do -- SET_GROUP -- @return Core.Set#SET_GROUP self function SET_GROUP:AddGroupsByName( AddGroupNames ) - local AddGroupNamesArray = ( type( AddGroupNames ) == "table" ) and AddGroupNames or { AddGroupNames } + local AddGroupNamesArray = (type( AddGroupNames ) == "table") and AddGroupNames or { AddGroupNames } for AddGroupID, AddGroupName in pairs( AddGroupNamesArray ) do self:Add( AddGroupName, GROUP:FindByName( AddGroupName ) ) @@ -17710,7 +22360,7 @@ do -- SET_GROUP -- @return Core.Set#SET_GROUP self function SET_GROUP:RemoveGroupsByName( RemoveGroupNames ) - local RemoveGroupNamesArray = ( type( RemoveGroupNames ) == "table" ) and RemoveGroupNames or { RemoveGroupNames } + local RemoveGroupNamesArray = (type( RemoveGroupNames ) == "table") and RemoveGroupNames or { RemoveGroupNames } for RemoveGroupID, RemoveGroupName in pairs( RemoveGroupNamesArray ) do self:Remove( RemoveGroupName ) @@ -17719,9 +22369,6 @@ do -- SET_GROUP return self end - - - --- Finds a Group based on the Group Name. -- @param #SET_GROUP self -- @param #string GroupName @@ -17739,10 +22386,12 @@ do -- SET_GROUP function SET_GROUP:FindNearestGroupFromPointVec2( PointVec2 ) self:F2( PointVec2 ) - local NearestGroup = nil --Wrapper.Group#GROUP + local NearestGroup = nil -- Wrapper.Group#GROUP local ClosestDistance = nil - - for ObjectID, ObjectData in pairs( self.Set ) do + + local Set = self:GetAliveSet() + + for ObjectID, ObjectData in pairs( Set ) do if NearestGroup == nil then NearestGroup = ObjectData ClosestDistance = PointVec2:DistanceFromPointVec2( ObjectData:GetCoordinate() ) @@ -17758,41 +22407,77 @@ do -- SET_GROUP return NearestGroup end + --- Builds a set of groups in zones. + -- @param #SET_GROUP self + -- @param #table Zones Table of Core.Zone#ZONE Zone objects, or a Core.Set#SET_ZONE + -- @param #boolean Clear If `true`, clear any previously defined filters. + -- @return #SET_GROUP self + function SET_GROUP:FilterZones( Zones, Clear ) + + if Clear or not self.Filter.Zones then + self.Filter.Zones = {} + end + + local zones = {} + if Zones.ClassName and Zones.ClassName == "SET_ZONE" then + zones = Zones.Set + elseif type( Zones ) ~= "table" or (type( Zones ) == "table" and Zones.ClassName) then + self:E( "***** FilterZones needs either a table of ZONE Objects or a SET_ZONE as parameter!" ) + return self + else + zones = Zones + end + + for _, Zone in pairs( zones ) do + local zonename = Zone:GetName() + self.Filter.Zones[zonename] = Zone + end + + return self + end --- Builds a set of groups of coalitions. -- Possible current coalitions are red, blue and neutral. -- @param #SET_GROUP self -- @param #string Coalitions Can take the following values: "red", "blue", "neutral". + -- @param #boolean Clear If `true`, clear any previously defined filters. -- @return #SET_GROUP self - function SET_GROUP:FilterCoalitions( Coalitions ) - if not self.Filter.Coalitions then + function SET_GROUP:FilterCoalitions( Coalitions, Clear ) + + if Clear or (not self.Filter.Coalitions) then self.Filter.Coalitions = {} end - if type( Coalitions ) ~= "table" then - Coalitions = { Coalitions } - end + + -- Ensure table. + Coalitions = UTILS.EnsureTable(Coalitions, false) + for CoalitionID, Coalition in pairs( Coalitions ) do self.Filter.Coalitions[Coalition] = Coalition end + return self end - --- Builds a set of groups out of categories. -- Possible current categories are plane, helicopter, ground, ship. -- @param #SET_GROUP self -- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship". + -- @param #boolean Clear If `true`, clear any previously defined filters. -- @return #SET_GROUP self - function SET_GROUP:FilterCategories( Categories ) - if not self.Filter.Categories then + function SET_GROUP:FilterCategories( Categories, Clear ) + + if Clear or not self.Filter.Categories then self.Filter.Categories = {} end + if type( Categories ) ~= "table" then Categories = { Categories } end + for CategoryID, Category in pairs( Categories ) do self.Filter.Categories[Category] = Category end + return self end @@ -17836,8 +22521,6 @@ do -- SET_GROUP return self end - - --- Builds a set of groups of defined countries. -- Possible current countries are those known within DCS world. -- @param #SET_GROUP self @@ -17856,9 +22539,8 @@ do -- SET_GROUP return self end - --- Builds a set of groups that contain the given string in their group name. - -- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all groups that **contain** the string. + -- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all groups that **contain** the string. -- @param #SET_GROUP self -- @param #string Prefixes The string pattern(s) that needs to be contained in the group name. Can also be passed as a `#table` of strings. -- @return #SET_GROUP self @@ -17878,7 +22560,7 @@ do -- SET_GROUP --- Builds a set of groups that are only active. -- Only the groups that are active will be included within the set. -- @param #SET_GROUP self - -- @param #boolean Active (optional) Include only active groups to the set. + -- @param #boolean Active (Optional) Include only active groups to the set. -- Include inactive groups if you provide false. -- @return #SET_GROUP self -- @usage @@ -17896,12 +22578,11 @@ do -- SET_GROUP -- GroupSet = SET_GROUP:New():FilterActive( false ):FilterCoalition( "blue" ):FilterOnce() -- function SET_GROUP:FilterActive( Active ) - Active = Active or not ( Active == false ) + Active = Active or not (Active == false) self.Filter.Active = Active return self end - --- Starts the filtering. -- @param #SET_GROUP self -- @return #SET_GROUP self @@ -17915,8 +22596,6 @@ do -- SET_GROUP self:HandleEvent( EVENTS.RemoveUnit, self._EventOnDeadOrCrash ) end - - return self end @@ -17930,7 +22609,11 @@ do -- SET_GROUP if Event.IniDCSUnit then local ObjectName, Object = self:FindInDatabase( Event ) if ObjectName then - if Event.IniDCSGroup:getSize() == 1 then -- Only remove if the last unit of the group was destroyed. + local size = 1 + if Event.IniDCSGroup then + size = Event.IniDCSGroup:getSize() + end + if size == 1 then -- Only remove if the last unit of the group was destroyed. self:Remove( ObjectName ) end end @@ -18016,7 +22699,23 @@ do -- SET_GROUP return self end - --- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence completely in a @{Zone}, providing the GROUP and optional parameters to the called function. + --- Activate late activated groups. + -- @param #SET_GROUP self + -- @param #number Delay Delay in seconds. + -- @return #SET_GROUP self + function SET_GROUP:Activate(Delay) + local Set = self:GetSet() + for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP + local group=GroupData --Wrapper.Group#GROUP + if group and group:IsAlive()==false then + group:Activate(Delay) + end + end + return self + end + + + --- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence completely in a @{Core.Zone}, providing the GROUP and optional parameters to the called function. -- @param #SET_GROUP self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. -- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. @@ -18038,7 +22737,7 @@ do -- SET_GROUP return self end - --- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence partly in a @{Zone}, providing the GROUP and optional parameters to the called function. + --- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence partly in a @{Core.Zone}, providing the GROUP and optional parameters to the called function. -- @param #SET_GROUP self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. -- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. @@ -18060,7 +22759,7 @@ do -- SET_GROUP return self end - --- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence not in a @{Zone}, providing the GROUP and optional parameters to the called function. + --- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence not in a @{Core.Zone}, providing the GROUP and optional parameters to the called function. -- @param #SET_GROUP self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. -- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. @@ -18085,7 +22784,7 @@ do -- SET_GROUP --- Iterate the SET_GROUP and return true if all the @{Wrapper.Group#GROUP} are completely in the @{Core.Zone#ZONE} -- @param #SET_GROUP self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. - -- @return #boolean true if all the @{Wrapper.Group#GROUP} are completly in the @{Core.Zone#ZONE}, false otherwise + -- @return #boolean true if all the @{Wrapper.Group#GROUP} are completely in the @{Core.Zone#ZONE}, false otherwise -- @usage -- local MyZone = ZONE:New("Zone1") -- local MySetGroup = SET_GROUP:New() @@ -18096,11 +22795,11 @@ do -- SET_GROUP -- else -- MESSAGE:New("Some or all SET's GROUP are outside zone !", 10):ToAll() -- end - function SET_GROUP:AllCompletelyInZone(Zone) - self:F2(Zone) + function SET_GROUP:AllCompletelyInZone( Zone ) + self:F2( Zone ) local Set = self:GetSet() - for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP - if not GroupData:IsCompletelyInZone(Zone) then + for GroupID, GroupData in pairs( Set ) do -- For each GROUP in SET_GROUP + if not GroupData:IsCompletelyInZone( Zone ) then return false end end @@ -18129,11 +22828,10 @@ do -- SET_GROUP return self end - --- Iterate the SET_GROUP and return true if at least one of the @{Wrapper.Group#GROUP} is completely inside the @{Core.Zone#ZONE} -- @param #SET_GROUP self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. - -- @return #boolean true if at least one of the @{Wrapper.Group#GROUP} is completly inside the @{Core.Zone#ZONE}, false otherwise. + -- @return #boolean true if at least one of the @{Wrapper.Group#GROUP} is completely inside the @{Core.Zone#ZONE}, false otherwise. -- @usage -- local MyZone = ZONE:New("Zone1") -- local MySetGroup = SET_GROUP:New() @@ -18144,21 +22842,21 @@ do -- SET_GROUP -- else -- MESSAGE:New("No GROUP is completely in zone !", 10):ToAll() -- end - function SET_GROUP:AnyCompletelyInZone(Zone) - self:F2(Zone) + function SET_GROUP:AnyCompletelyInZone( Zone ) + self:F2( Zone ) local Set = self:GetSet() - for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP - if GroupData:IsCompletelyInZone(Zone) then + for GroupID, GroupData in pairs( Set ) do -- For each GROUP in SET_GROUP + if GroupData:IsCompletelyInZone( Zone ) then return true end end return false end - --- Iterate the SET_GROUP and return true if at least one @{#UNIT} of one @{GROUP} of the @{SET_GROUP} is in @{ZONE} + --- Iterate the SET_GROUP and return true if at least one @{#UNIT} of one @{Wrapper.Group#GROUP} of the @{#SET_GROUP} is in @{Core.Zone} -- @param #SET_GROUP self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. - -- @return #boolean true if at least one of the @{Wrapper.Group#GROUP} is partly or completly inside the @{Core.Zone#ZONE}, false otherwise. + -- @return #boolean true if at least one of the @{Wrapper.Group#GROUP} is partly or completely inside the @{Core.Zone#ZONE}, false otherwise. -- @usage -- local MyZone = ZONE:New("Zone1") -- local MySetGroup = SET_GROUP:New() @@ -18169,22 +22867,22 @@ do -- SET_GROUP -- else -- MESSAGE:New("No UNIT of any GROUP is in zone !", 10):ToAll() -- end - function SET_GROUP:AnyInZone(Zone) - self:F2(Zone) + function SET_GROUP:AnyInZone( Zone ) + self:F2( Zone ) local Set = self:GetSet() - for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP - if GroupData:IsPartlyInZone(Zone) or GroupData:IsCompletelyInZone(Zone) then + for GroupID, GroupData in pairs( Set ) do -- For each GROUP in SET_GROUP + if GroupData:IsPartlyInZone( Zone ) or GroupData:IsCompletelyInZone( Zone ) then return true end end return false end - --- Iterate the SET_GROUP and return true if at least one @{GROUP} of the @{SET_GROUP} is partly in @{ZONE}. - -- Will return false if a @{GROUP} is fully in the @{ZONE} + --- Iterate the SET_GROUP and return true if at least one @{Wrapper.Group#GROUP} of the @{#SET_GROUP} is partly in @{Core.Zone}. + -- Will return false if a @{Wrapper.Group#GROUP} is fully in the @{Core.Zone} -- @param #SET_GROUP self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. - -- @return #boolean true if at least one of the @{Wrapper.Group#GROUP} is partly or completly inside the @{Core.Zone#ZONE}, false otherwise. + -- @return #boolean true if at least one of the @{Wrapper.Group#GROUP} is partly or completely inside the @{Core.Zone#ZONE}, false otherwise. -- @usage -- local MyZone = ZONE:New("Zone1") -- local MySetGroup = SET_GROUP:New() @@ -18195,14 +22893,14 @@ do -- SET_GROUP -- else -- MESSAGE:New("No GROUP are in zone, or one (or more) GROUP is completely in it !", 10):ToAll() -- end - function SET_GROUP:AnyPartlyInZone(Zone) - self:F2(Zone) + function SET_GROUP:AnyPartlyInZone( Zone ) + self:F2( Zone ) local IsPartlyInZone = false local Set = self:GetSet() - for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP - if GroupData:IsCompletelyInZone(Zone) then + for GroupID, GroupData in pairs( Set ) do -- For each GROUP in SET_GROUP + if GroupData:IsCompletelyInZone( Zone ) then return false - elseif GroupData:IsPartlyInZone(Zone) then + elseif GroupData:IsPartlyInZone( Zone ) then IsPartlyInZone = true -- at least one GROUP is partly in zone end end @@ -18214,7 +22912,7 @@ do -- SET_GROUP end end - --- Iterate the SET_GROUP and return true if no @{GROUP} of the @{SET_GROUP} is in @{ZONE} + --- Iterate the SET_GROUP and return true if no @{Wrapper.Group#GROUP} of the @{#SET_GROUP} is in @{Core.Zone} -- This could also be achieved with `not SET_GROUP:AnyPartlyInZone(Zone)`, but it's easier for the -- mission designer to add a dedicated method -- @param #SET_GROUP self @@ -18230,11 +22928,11 @@ do -- SET_GROUP -- else -- MESSAGE:New("No UNIT of any GROUP is in zone !", 10):ToAll() -- end - function SET_GROUP:NoneInZone(Zone) - self:F2(Zone) + function SET_GROUP:NoneInZone( Zone ) + self:F2( Zone ) local Set = self:GetSet() - for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP - if not GroupData:IsNotInZone(Zone) then -- If the GROUP is in Zone in any way + for GroupID, GroupData in pairs( Set ) do -- For each GROUP in SET_GROUP + if not GroupData:IsNotInZone( Zone ) then -- If the GROUP is in Zone in any way return false end end @@ -18253,12 +22951,12 @@ do -- SET_GROUP -- MySetGroup:AddGroupsByName({"Group1", "Group2"}) -- -- MESSAGE:New("There are " .. MySetGroup:CountInZone(MyZone) .. " GROUPs in the Zone !", 10):ToAll() - function SET_GROUP:CountInZone(Zone) - self:F2(Zone) + function SET_GROUP:CountInZone( Zone ) + self:F2( Zone ) local Count = 0 local Set = self:GetSet() - for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP - if GroupData:IsCompletelyInZone(Zone) then + for GroupID, GroupData in pairs( Set ) do -- For each GROUP in SET_GROUP + if GroupData:IsCompletelyInZone( Zone ) then Count = Count + 1 end end @@ -18275,12 +22973,12 @@ do -- SET_GROUP -- MySetGroup:AddGroupsByName({"Group1", "Group2"}) -- -- MESSAGE:New("There are " .. MySetGroup:CountUnitInZone(MyZone) .. " UNITs in the Zone !", 10):ToAll() - function SET_GROUP:CountUnitInZone(Zone) - self:F2(Zone) + function SET_GROUP:CountUnitInZone( Zone ) + self:F2( Zone ) local Count = 0 local Set = self:GetSet() - for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP - Count = Count + GroupData:CountInZone(Zone) + for GroupID, GroupData in pairs( Set ) do -- For each GROUP in SET_GROUP + Count = Count + GroupData:CountInZone( Zone ) end return Count end @@ -18295,50 +22993,49 @@ do -- SET_GROUP local Set = self:GetSet() - for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP + for GroupID, GroupData in pairs( Set ) do -- For each GROUP in SET_GROUP if GroupData and GroupData:IsAlive() then CountG = CountG + 1 - --Count Units. - for _,_unit in pairs(GroupData:GetUnits()) do - local unit=_unit --Wrapper.Unit#UNIT + -- Count Units. + for _, _unit in pairs( GroupData:GetUnits() ) do + local unit = _unit -- Wrapper.Unit#UNIT if unit and unit:IsAlive() then - CountU=CountU+1 + CountU = CountU + 1 end end end end - return CountG,CountU + return CountG, CountU end - ----- Iterate the SET_GROUP and call an interator function for each **alive** player, providing the Group of the player and optional parameters. + ----- Iterate the SET_GROUP and call an iterator function for each **alive** player, providing the Group of the player and optional parameters. ---- @param #SET_GROUP self ---- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_GROUP. The function needs to accept a GROUP parameter. ---- @return #SET_GROUP self - --function SET_GROUP:ForEachPlayer( IteratorFunction, ... ) + -- function SET_GROUP:ForEachPlayer( IteratorFunction, ... ) -- self:F2( arg ) -- -- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) -- -- return self - --end + -- end -- -- - ----- Iterate the SET_GROUP and call an interator function for each client, providing the Client to the function and optional parameters. + ----- Iterate the SET_GROUP and call an iterator function for each client, providing the Client to the function and optional parameters. ---- @param #SET_GROUP self ---- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_GROUP. The function needs to accept a CLIENT parameter. ---- @return #SET_GROUP self - --function SET_GROUP:ForEachClient( IteratorFunction, ... ) + -- function SET_GROUP:ForEachClient( IteratorFunction, ... ) -- self:F2( arg ) -- -- self:ForEach( IteratorFunction, arg, self.Clients ) -- -- return self - --end - + -- end --- -- @param #SET_GROUP self @@ -18351,7 +23048,7 @@ do -- SET_GROUP if self.Filter.Active ~= nil then local MGroupActive = false self:F( { Active = self.Filter.Active } ) - if self.Filter.Active == false or ( self.Filter.Active == true and MGroup:IsActive() == true ) then + if self.Filter.Active == false or (self.Filter.Active == true and MGroup:IsActive() == true) then MGroupActive = true end MGroupInclude = MGroupInclude and MGroupActive @@ -18394,18 +23091,28 @@ do -- SET_GROUP local MGroupPrefix = false for GroupPrefixId, GroupPrefix in pairs( self.Filter.GroupPrefixes ) do self:T3( { "Prefix:", string.find( MGroup:GetName(), GroupPrefix, 1 ), GroupPrefix } ) - if string.find( MGroup:GetName(), GroupPrefix:gsub ("-", "%%-"), 1 ) then + if string.find( MGroup:GetName(), GroupPrefix:gsub( "-", "%%-" ), 1 ) then MGroupPrefix = true end end MGroupInclude = MGroupInclude and MGroupPrefix end - + + if self.Filter.Zones then + local MGroupZone = false + for ZoneName, Zone in pairs( self.Filter.Zones ) do + self:T3( "Zone:", ZoneName ) + if MGroup:IsInZone(Zone) then + MGroupZone = true + end + end + MGroupInclude = MGroupInclude and MGroupZone + end + self:T2( MGroupInclude ) return MGroupInclude end - --- Iterate the SET_GROUP and set for each unit the default cargo bay weight limit. -- Because within a group, the type of carriers can differ, each cargo bay weight limit is set on @{Wrapper.Unit} level. -- @param #SET_GROUP self @@ -18417,14 +23124,48 @@ do -- SET_GROUP local Set = self:GetSet() for GroupID, GroupData in pairs( Set ) do -- For each GROUP in SET_GROUP for UnitName, UnitData in pairs( GroupData:GetUnits() ) do - --local UnitData = UnitData -- Wrapper.Unit#UNIT + -- local UnitData = UnitData -- Wrapper.Unit#UNIT UnitData:SetCargoBayWeightLimit() end end end -end + --- Get the closest group of the set with respect to a given reference coordinate. Optionally, only groups of given coalitions are considered in the search. + -- @param #SET_GROUP self + -- @param Core.Point#COORDINATE Coordinate Reference Coordinate from which the closest group is determined. + -- @return Wrapper.Group#GROUP The closest group (if any). + -- @return #number Distance in meters to the closest group. + function SET_GROUP:GetClosestGroup(Coordinate, Coalitions) + + local Set = self:GetSet() + + local dmin=math.huge + local gmin=nil + + for GroupID, GroupData in pairs( Set ) do -- For each GROUP in SET_GROUP + local group=GroupData --Wrapper.Group#GROUP + + if group and group:IsAlive() and (Coalitions==nil or UTILS.IsAnyInTable(Coalitions, group:GetCoalition())) then + + local coord=group:GetCoord() + + -- Distance between ref. coordinate and group coordinate. + local d=UTILS.VecDist3D(Coordinate, coord) + + if d x2 ) and Coordinate.x or x2 - y1 = ( Coordinate.y < y1 ) and Coordinate.y or y1 - y2 = ( Coordinate.y > y2 ) and Coordinate.y or y2 - z1 = ( Coordinate.y < z1 ) and Coordinate.z or z1 - z2 = ( Coordinate.y > z2 ) and Coordinate.z or z2 + x1 = (Coordinate.x < x1) and Coordinate.x or x1 + x2 = (Coordinate.x > x2) and Coordinate.x or x2 + y1 = (Coordinate.y < y1) and Coordinate.y or y1 + y2 = (Coordinate.y > y2) and Coordinate.y or y2 + z1 = (Coordinate.y < z1) and Coordinate.z or z1 + z2 = (Coordinate.y > z2) and Coordinate.z or z2 local Velocity = Coordinate:GetVelocity() - if Velocity ~= 0 then - MaxVelocity = ( MaxVelocity < Velocity ) and Velocity or MaxVelocity + if Velocity ~= 0 then + MaxVelocity = (MaxVelocity < Velocity) and Velocity or MaxVelocity local Heading = Coordinate:GetHeading() - AvgHeading = AvgHeading and ( AvgHeading + Heading ) or Heading + AvgHeading = AvgHeading and (AvgHeading + Heading) or Heading MovingCount = MovingCount + 1 end end - AvgHeading = AvgHeading and ( AvgHeading / MovingCount ) + AvgHeading = AvgHeading and (AvgHeading / MovingCount) - Coordinate.x = ( x2 - x1 ) / 2 + x1 - Coordinate.y = ( y2 - y1 ) / 2 + y1 - Coordinate.z = ( z2 - z1 ) / 2 + z1 + Coordinate.x = (x2 - x1) / 2 + x1 + Coordinate.y = (y2 - y1) / 2 + y1 + Coordinate.z = (z2 - z1) / 2 + z1 Coordinate:SetHeading( AvgHeading ) Coordinate:SetVelocity( MaxVelocity ) @@ -19225,8 +23964,8 @@ do -- SET_UNIT local Coordinate = Unit:GetCoordinate() local Velocity = Coordinate:GetVelocity() - if Velocity ~= 0 then - MaxVelocity = ( MaxVelocity < Velocity ) and Velocity or MaxVelocity + if Velocity ~= 0 then + MaxVelocity = (MaxVelocity < Velocity) and Velocity or MaxVelocity end end @@ -19249,12 +23988,12 @@ do -- SET_UNIT local Coordinate = Unit:GetCoordinate() local Velocity = Coordinate:GetVelocity() - if Velocity ~= 0 then + if Velocity ~= 0 then local Heading = Coordinate:GetHeading() if HeadingSet == nil then HeadingSet = Heading else - local HeadingDiff = ( HeadingSet - Heading + 180 + 360 ) % 360 - 180 + local HeadingDiff = (HeadingSet - Heading + 180 + 360) % 360 - 180 HeadingDiff = math.abs( HeadingDiff ) if HeadingDiff > 5 then HeadingSet = nil @@ -19268,9 +24007,7 @@ do -- SET_UNIT end - - - --- Returns if the @{Set} has targets having a radar (of a given type). + --- Returns if the @{Core.Set} has targets having a radar (of a given type). -- @param #SET_UNIT self -- @param DCS#Unit.RadarType RadarType -- @return #number The amount of radars in the Set with the given type @@ -19278,7 +24015,7 @@ do -- SET_UNIT self:F2( RadarType ) local RadarCount = 0 - for UnitID, UnitData in pairs( self:GetSet()) do + for UnitID, UnitData in pairs( self:GetSet() ) do local UnitSensorTest = UnitData -- Wrapper.Unit#UNIT local HasSensors if RadarType then @@ -19286,7 +24023,7 @@ do -- SET_UNIT else HasSensors = UnitSensorTest:HasSensors( Unit.SensorType.RADAR ) end - self:T3(HasSensors) + self:T3( HasSensors ) if HasSensors then RadarCount = RadarCount + 1 end @@ -19295,21 +24032,21 @@ do -- SET_UNIT return RadarCount end - --- Returns if the @{Set} has targets that can be SEADed. + --- Returns if the @{Core.Set} has targets that can be SEADed. -- @param #SET_UNIT self -- @return #number The amount of SEADable units in the Set function SET_UNIT:HasSEAD() self:F2() local SEADCount = 0 - for UnitID, UnitData in pairs( self:GetSet()) do + for UnitID, UnitData in pairs( self:GetSet() ) do local UnitSEAD = UnitData -- Wrapper.Unit#UNIT if UnitSEAD:IsAlive() then local UnitSEADAttributes = UnitSEAD:GetDesc().attributes local HasSEAD = UnitSEAD:HasSEAD() - self:T3(HasSEAD) + self:T3( HasSEAD ) if HasSEAD then SEADCount = SEADCount + 1 end @@ -19319,14 +24056,14 @@ do -- SET_UNIT return SEADCount end - --- Returns if the @{Set} has ground targets. + --- Returns if the @{Core.Set} has ground targets. -- @param #SET_UNIT self -- @return #number The amount of ground targets in the Set. function SET_UNIT:HasGroundUnits() self:F2() local GroundUnitCount = 0 - for UnitID, UnitData in pairs( self:GetSet()) do + for UnitID, UnitData in pairs( self:GetSet() ) do local UnitTest = UnitData -- Wrapper.Unit#UNIT if UnitTest:IsGround() then GroundUnitCount = GroundUnitCount + 1 @@ -19336,7 +24073,7 @@ do -- SET_UNIT return GroundUnitCount end - --- Returns if the @{Set} has air targets. + --- Returns if the @{Core.Set} has air targets. -- @param #SET_UNIT self -- @return #number The amount of air targets in the Set. function SET_UNIT:HasAirUnits() @@ -19353,14 +24090,14 @@ do -- SET_UNIT return AirUnitCount end - --- Returns if the @{Set} has friendly ground units. + --- Returns if the @{Core.Set} has friendly ground units. -- @param #SET_UNIT self -- @return #number The amount of ground targets in the Set. function SET_UNIT:HasFriendlyUnits( FriendlyCoalition ) self:F2() local FriendlyUnitCount = 0 - for UnitID, UnitData in pairs( self:GetSet()) do + for UnitID, UnitData in pairs( self:GetSet() ) do local UnitTest = UnitData -- Wrapper.Unit#UNIT if UnitTest:IsFriendly( FriendlyCoalition ) then FriendlyUnitCount = FriendlyUnitCount + 1 @@ -19372,31 +24109,30 @@ do -- SET_UNIT - ----- Iterate the SET_UNIT and call an interator function for each **alive** player, providing the Unit of the player and optional parameters. + ----- Iterate the SET_UNIT and call an iterator function for each **alive** player, providing the Unit of the player and optional parameters. ---- @param #SET_UNIT self ---- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_UNIT. The function needs to accept a UNIT parameter. ---- @return #SET_UNIT self - --function SET_UNIT:ForEachPlayer( IteratorFunction, ... ) + -- function SET_UNIT:ForEachPlayer( IteratorFunction, ... ) -- self:F2( arg ) -- -- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) -- -- return self - --end + -- end -- -- - ----- Iterate the SET_UNIT and call an interator function for each client, providing the Client to the function and optional parameters. + ----- Iterate the SET_UNIT and call an iterator function for each client, providing the Client to the function and optional parameters. ---- @param #SET_UNIT self ---- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_UNIT. The function needs to accept a CLIENT parameter. ---- @return #SET_UNIT self - --function SET_UNIT:ForEachClient( IteratorFunction, ... ) + -- function SET_UNIT:ForEachClient( IteratorFunction, ... ) -- self:F2( arg ) -- -- self:ForEach( IteratorFunction, arg, self.Clients ) -- -- return self - --end - + -- end --- -- @param #SET_UNIT self @@ -19413,7 +24149,7 @@ do -- SET_UNIT if self.Filter.Active ~= nil then local MUnitActive = false - if self.Filter.Active == false or ( self.Filter.Active == true and MUnit:IsActive() == true ) then + if self.Filter.Active == false or (self.Filter.Active == true and MUnit:IsActive() == true) then MUnitActive = true end MUnitInclude = MUnitInclude and MUnitActive @@ -19497,15 +24233,25 @@ do -- SET_UNIT MUnitInclude = MUnitInclude and MUnitSEAD end end - + + if self.Filter.Zones then + local MGroupZone = false + for ZoneName, Zone in pairs( self.Filter.Zones ) do + self:T3( "Zone:", ZoneName ) + if MUnit:IsInZone(Zone) then + MGroupZone = true + end + end + MUnitInclude = MUnitInclude and MGroupZone + end + self:T2( MUnitInclude ) return MUnitInclude end - --- Retrieve the type names of the @{Wrapper.Unit}s in the SET, delimited by an optional delimiter. -- @param #SET_UNIT self - -- @param #string Delimiter (optional) The delimiter, which is default a comma. + -- @param #string Delimiter (Optional) The delimiter, which is default a comma. -- @return #string The types of the @{Wrapper.Unit}s delimited. function SET_UNIT:GetTypeNames( Delimiter ) @@ -19536,16 +24282,13 @@ do -- SET_UNIT function SET_UNIT:SetCargoBayWeightLimit() local Set = self:GetSet() for UnitID, UnitData in pairs( Set ) do -- For each UNIT in SET_UNIT - --local UnitData = UnitData -- Wrapper.Unit#UNIT + -- local UnitData = UnitData -- Wrapper.Unit#UNIT UnitData:SetCargoBayWeightLimit() end end - - end - do -- SET_STATIC --- @type SET_STATIC @@ -19580,15 +24323,12 @@ do -- SET_STATIC -- * @{#SET_STATIC.FilterTypes}: Builds the SET_STATIC with the units belonging to the unit type(s). -- * @{#SET_STATIC.FilterCountries}: Builds the SET_STATIC with the units belonging to the country(ies). -- * @{#SET_STATIC.FilterPrefixes}: Builds the SET_STATIC with the units containing the same string(s) in their name. **ATTENTION** bad naming convention as this *does not** only filter *prefixes*. - -- + -- * @{#SET_STATIC.FilterZones}: Builds the SET_STATIC with the units within a @{Core.Zone#ZONE}. + -- -- Once the filter criteria have been set for the SET_STATIC, you can start filtering using: -- -- * @{#SET_STATIC.FilterStart}: Starts the filtering of the units within the SET_STATIC. -- - -- Planned filter criteria within development are (so these are not yet available): - -- - -- * @{#SET_STATIC.FilterZones}: Builds the SET_STATIC with the units within a @{Core.Zone#ZONE}. - -- -- ## SET_STATIC iterators -- -- Once the filters have been defined and the SET_STATIC has been built, you can iterate the SET_STATIC with the available iterator methods. @@ -19596,20 +24336,15 @@ do -- SET_STATIC -- The following iterator methods are currently available within the SET_STATIC: -- -- * @{#SET_STATIC.ForEachStatic}: Calls a function for each alive unit it finds within the SET_STATIC. - -- * @{#SET_GROUP.ForEachGroupCompletelyInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence completely in a @{Zone}, providing the GROUP and optional parameters to the called function. - -- * @{#SET_GROUP.ForEachGroupNotInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence not in a @{Zone}, providing the GROUP and optional parameters to the called function. - -- - -- Planned iterators methods in development are (so these are not yet available): - -- - -- * @{#SET_STATIC.ForEachStaticInZone}: Calls a function for each unit contained within the SET_STATIC. - -- * @{#SET_STATIC.ForEachStaticCompletelyInZone}: Iterate and call an iterator function for each **alive** STATIC presence completely in a @{Zone}, providing the STATIC and optional parameters to the called function. - -- * @{#SET_STATIC.ForEachStaticNotInZone}: Iterate and call an iterator function for each **alive** STATIC presence not in a @{Zone}, providing the STATIC and optional parameters to the called function. + -- * @{#SET_STATIC.ForEachStaticCompletelyInZone}: Iterate the SET_STATIC and call an iterator function for each **alive** STATIC presence completely in a @{Core.Zone}, providing the STATIC and optional parameters to the called function. + -- * @{#SET_STATIC.ForEachStaticInZone}: Iterate the SET_STATIC and call an iterator function for each **alive** STATIC presence completely in a @{Core.Zone}, providing the STATIC and optional parameters to the called function. + -- * @{#SET_STATIC.ForEachStaticNotInZone}: Iterate the SET_STATIC and call an iterator function for each **alive** STATIC presence not in a @{Core.Zone}, providing the STATIC and optional parameters to the called function. -- -- ## SET_STATIC atomic methods -- -- Various methods exist for a SET_STATIC to perform actions or calculations and retrieve results from the SET_STATIC: -- - -- * @{#SET_STATIC.GetTypeNames}(): Retrieve the type names of the @{Static}s in the SET, delimited by a comma. + -- * @{#SET_STATIC.GetTypeNames}(): Retrieve the type names of the @{Wrapper.Static}s in the SET, delimited by a comma. -- -- === -- @field #SET_STATIC SET_STATIC @@ -19622,6 +24357,7 @@ do -- SET_STATIC Types = nil, Countries = nil, StaticPrefixes = nil, + Zones = nil, }, FilterMeta = { Coalitions = { @@ -19671,14 +24407,13 @@ do -- SET_STATIC return self end - --- Add STATIC(s) to SET_STATIC. -- @param #SET_STATIC self -- @param #string AddStaticNames A single name or an array of STATIC names. -- @return #SET_STATIC self function SET_STATIC:AddStaticsByName( AddStaticNames ) - local AddStaticNamesArray = ( type( AddStaticNames ) == "table" ) and AddStaticNames or { AddStaticNames } + local AddStaticNamesArray = (type( AddStaticNames ) == "table") and AddStaticNames or { AddStaticNames } self:T( AddStaticNamesArray ) for AddStaticID, AddStaticName in pairs( AddStaticNamesArray ) do @@ -19694,7 +24429,7 @@ do -- SET_STATIC -- @return self function SET_STATIC:RemoveStaticsByName( RemoveStaticNames ) - local RemoveStaticNamesArray = ( type( RemoveStaticNames ) == "table" ) and RemoveStaticNames or { RemoveStaticNames } + local RemoveStaticNamesArray = (type( RemoveStaticNames ) == "table") and RemoveStaticNames or { RemoveStaticNames } for RemoveStaticID, RemoveStaticName in pairs( RemoveStaticNamesArray ) do self:Remove( RemoveStaticName ) @@ -19703,7 +24438,6 @@ do -- SET_STATIC return self end - --- Finds a Static based on the Static Name. -- @param #SET_STATIC self -- @param #string StaticName @@ -19714,8 +24448,6 @@ do -- SET_STATIC return StaticFound end - - --- Builds a set of units of coalitions. -- Possible current coalitions are red, blue and neutral. -- @param #SET_STATIC self @@ -19733,7 +24465,31 @@ do -- SET_STATIC end return self end - + + + --- Builds a set of statics in zones. + -- @param #SET_STATIC self + -- @param #table Zones Table of Core.Zone#ZONE Zone objects, or a Core.Set#SET_ZONE + -- @return #SET_STATIC self + function SET_STATIC:FilterZones( Zones ) + if not self.Filter.Zones then + self.Filter.Zones = {} + end + local zones = {} + if Zones.ClassName and Zones.ClassName == "SET_ZONE" then + zones = Zones.Set + elseif type( Zones ) ~= "table" or (type( Zones ) == "table" and Zones.ClassName ) then + self:E("***** FilterZones needs either a table of ZONE Objects or a SET_ZONE as parameter!") + return self + else + zones = Zones + end + for _,Zone in pairs( zones ) do + local zonename = Zone:GetName() + self.Filter.Zones[zonename] = Zone + end + return self + end --- Builds a set of units out of categories. -- Possible current categories are plane, helicopter, ground, ship. @@ -19753,7 +24509,6 @@ do -- SET_STATIC return self end - --- Builds a set of units of defined unit types. -- Possible current types are those types known within DCS world. -- @param #SET_STATIC self @@ -19772,7 +24527,6 @@ do -- SET_STATIC return self end - --- Builds a set of units of defined countries. -- Possible current countries are those known within DCS world. -- @param #SET_STATIC self @@ -19791,7 +24545,6 @@ do -- SET_STATIC return self end - --- Builds a set of STATICs that contain the given string in their name. -- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all statics that **contain** the string. -- @param #SET_STATIC self @@ -19810,7 +24563,6 @@ do -- SET_STATIC return self end - --- Starts the filtering. -- @param #SET_STATIC self -- @return #SET_STATIC self @@ -19834,7 +24586,7 @@ do -- SET_STATIC local Set = self:GetSet() local CountU = 0 - for UnitID, UnitData in pairs(Set) do + for UnitID, UnitData in pairs( Set ) do if UnitData and UnitData:IsAlive() then CountU = CountU + 1 end @@ -19872,11 +24624,9 @@ do -- SET_STATIC function SET_STATIC:FindInDatabase( Event ) self:F2( { Event.IniDCSUnitName, self.Set[Event.IniDCSUnitName], Event } ) - return Event.IniDCSUnitName, self.Set[Event.IniDCSUnitName] end - do -- Is Zone methods --- Check if minimal one element of the SET_STATIC is in the Zone. @@ -19889,7 +24639,7 @@ do -- SET_STATIC local function EvaluateZone( ZoneStatic ) - local ZoneStaticName = ZoneStatic:GetName() + local ZoneStaticName = ZoneStatic:GetName() if self:FindStatic( ZoneStaticName ) then IsPartiallyInZone = true return false @@ -19901,7 +24651,6 @@ do -- SET_STATIC return IsPartiallyInZone end - --- Check if no element of the SET_STATIC is in the Zone. -- @param #SET_STATIC self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. @@ -19912,7 +24661,7 @@ do -- SET_STATIC local function EvaluateZone( ZoneStatic ) - local ZoneStaticName = ZoneStatic:GetName() + local ZoneStaticName = ZoneStatic:GetName() if self:FindStatic( ZoneStaticName ) then IsNotInZone = false return false @@ -19926,7 +24675,6 @@ do -- SET_STATIC return IsNotInZone end - --- Check if minimal one element of the SET_STATIC is in the Zone. -- @param #SET_STATIC self -- @param #function IteratorFunction The function that will be called when there is an alive STATIC in the SET_STATIC. The function needs to accept a STATIC parameter. @@ -19939,11 +24687,10 @@ do -- SET_STATIC return self end - end - --- Iterate the SET_STATIC and call an interator function for each **alive** STATIC, providing the STATIC and optional parameters. + --- Iterate the SET_STATIC and call an iterator function for each **alive** STATIC, providing the STATIC and optional parameters. -- @param #SET_STATIC self -- @param #function IteratorFunction The function that will be called when there is an alive STATIC in the SET_STATIC. The function needs to accept a STATIC parameter. -- @return #SET_STATIC self @@ -19955,8 +24702,7 @@ do -- SET_STATIC return self end - - --- Iterate the SET_STATIC and call an iterator function for each **alive** STATIC presence completely in a @{Zone}, providing the STATIC and optional parameters to the called function. + --- Iterate the SET_STATIC and call an iterator function for each **alive** STATIC presence completely in a @{Core.Zone}, providing the STATIC and optional parameters to the called function. -- @param #SET_STATIC self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. -- @param #function IteratorFunction The function that will be called when there is an alive STATIC in the SET_STATIC. The function needs to accept a STATIC parameter. @@ -19978,7 +24724,7 @@ do -- SET_STATIC return self end - --- Iterate the SET_STATIC and call an iterator function for each **alive** STATIC presence not in a @{Zone}, providing the STATIC and optional parameters to the called function. + --- Iterate the SET_STATIC and call an iterator function for each **alive** STATIC presence not in a @{Core.Zone}, providing the STATIC and optional parameters to the called function. -- @param #SET_STATIC self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. -- @param #function IteratorFunction The function that will be called when there is an alive STATIC in the SET_STATIC. The function needs to accept a STATIC parameter. @@ -20023,14 +24769,13 @@ do -- SET_STATIC end for StaticTypeID, StaticType in pairs( StaticTypes ) do - MT[#MT+1] = StaticType .. " of " .. StaticTypeID + MT[#MT + 1] = StaticType .. " of " .. StaticTypeID end return StaticTypes end - - --- Returns a comma separated string of the unit types with a count in the @{Set}. + --- Returns a comma separated string of the unit types with a count in the @{Core.Set}. -- @param #SET_STATIC self -- @return #string The unit types string function SET_STATIC:GetStaticTypesText() @@ -20040,7 +24785,7 @@ do -- SET_STATIC local StaticTypes = self:GetStaticTypes() for StaticTypeID, StaticType in pairs( StaticTypes ) do - MT[#MT+1] = StaticType .. " of " .. StaticTypeID + MT[#MT + 1] = StaticType .. " of " .. StaticTypeID end return table.concat( MT, ", " ) @@ -20068,27 +24813,27 @@ do -- SET_STATIC local Static = StaticData -- Wrapper.Static#STATIC local Coordinate = Static:GetCoordinate() - x1 = ( Coordinate.x < x1 ) and Coordinate.x or x1 - x2 = ( Coordinate.x > x2 ) and Coordinate.x or x2 - y1 = ( Coordinate.y < y1 ) and Coordinate.y or y1 - y2 = ( Coordinate.y > y2 ) and Coordinate.y or y2 - z1 = ( Coordinate.y < z1 ) and Coordinate.z or z1 - z2 = ( Coordinate.y > z2 ) and Coordinate.z or z2 + x1 = (Coordinate.x < x1) and Coordinate.x or x1 + x2 = (Coordinate.x > x2) and Coordinate.x or x2 + y1 = (Coordinate.y < y1) and Coordinate.y or y1 + y2 = (Coordinate.y > y2) and Coordinate.y or y2 + z1 = (Coordinate.y < z1) and Coordinate.z or z1 + z2 = (Coordinate.y > z2) and Coordinate.z or z2 local Velocity = Coordinate:GetVelocity() - if Velocity ~= 0 then - MaxVelocity = ( MaxVelocity < Velocity ) and Velocity or MaxVelocity + if Velocity ~= 0 then + MaxVelocity = (MaxVelocity < Velocity) and Velocity or MaxVelocity local Heading = Coordinate:GetHeading() - AvgHeading = AvgHeading and ( AvgHeading + Heading ) or Heading + AvgHeading = AvgHeading and (AvgHeading + Heading) or Heading MovingCount = MovingCount + 1 end end - AvgHeading = AvgHeading and ( AvgHeading / MovingCount ) + AvgHeading = AvgHeading and (AvgHeading / MovingCount) - Coordinate.x = ( x2 - x1 ) / 2 + x1 - Coordinate.y = ( y2 - y1 ) / 2 + y1 - Coordinate.z = ( z2 - z1 ) / 2 + z1 + Coordinate.x = (x2 - x1) / 2 + x1 + Coordinate.y = (y2 - y1) / 2 + y1 + Coordinate.z = (z2 - z1) / 2 + z1 Coordinate:SetHeading( AvgHeading ) Coordinate:SetVelocity( MaxVelocity ) @@ -20120,12 +24865,12 @@ do -- SET_STATIC local Coordinate = Static:GetCoordinate() local Velocity = Coordinate:GetVelocity() - if Velocity ~= 0 then + if Velocity ~= 0 then local Heading = Coordinate:GetHeading() if HeadingSet == nil then HeadingSet = Heading else - local HeadingDiff = ( HeadingSet - Heading + 180 + 360 ) % 360 - 180 + local HeadingDiff = (HeadingSet - Heading + 180 + 360) % 360 - 180 HeadingDiff = math.abs( HeadingDiff ) if HeadingDiff > 5 then HeadingSet = nil @@ -20139,24 +24884,24 @@ do -- SET_STATIC end - --- Calculate the maxium A2G threat level of the SET_STATIC. + --- Calculate the maximum A2G threat level of the SET_STATIC. -- @param #SET_STATIC self -- @return #number The maximum threatlevel function SET_STATIC:CalculateThreatLevelA2G() - local MaxThreatLevelA2G = 0 - local MaxThreatText = "" - for StaticName, StaticData in pairs( self:GetSet() ) do - local ThreatStatic = StaticData -- Wrapper.Static#STATIC - local ThreatLevelA2G, ThreatText = ThreatStatic:GetThreatLevel() - if ThreatLevelA2G > MaxThreatLevelA2G then - MaxThreatLevelA2G = ThreatLevelA2G - MaxThreatText = ThreatText + local MaxThreatLevelA2G = 0 + local MaxThreatText = "" + for StaticName, StaticData in pairs( self:GetSet() ) do + local ThreatStatic = StaticData -- Wrapper.Static#STATIC + local ThreatLevelA2G, ThreatText = ThreatStatic:GetThreatLevel() + if ThreatLevelA2G > MaxThreatLevelA2G then + MaxThreatLevelA2G = ThreatLevelA2G + MaxThreatText = ThreatText + end end - end - self:F( { MaxThreatLevelA2G = MaxThreatLevelA2G, MaxThreatText = MaxThreatText } ) - return MaxThreatLevelA2G, MaxThreatText + self:F( { MaxThreatLevelA2G = MaxThreatLevelA2G, MaxThreatText = MaxThreatText } ) + return MaxThreatLevelA2G, MaxThreatText end @@ -20222,16 +24967,26 @@ do -- SET_STATIC end MStaticInclude = MStaticInclude and MStaticPrefix end - + + if self.Filter.Zones then + local MStaticZone = false + for ZoneName, Zone in pairs( self.Filter.Zones ) do + self:T3( "Zone:", ZoneName ) + if MStatic and MStatic:IsInZone(Zone) then + MStaticZone = true + end + end + MStaticInclude = MStaticInclude and MStaticZone + end + self:T2( MStaticInclude ) return MStaticInclude end - - --- Retrieve the type names of the @{Static}s in the SET, delimited by an optional delimiter. + --- Retrieve the type names of the @{Wrapper.Static}s in the SET, delimited by an optional delimiter. -- @param #SET_STATIC self - -- @param #string Delimiter (optional) The delimiter, which is default a comma. - -- @return #string The types of the @{Static}s delimited. + -- @param #string Delimiter (Optional) The delimiter, which is default a comma. + -- @return #string The types of the @{Wrapper.Static}s delimited. function SET_STATIC:GetTypeNames( Delimiter ) Delimiter = Delimiter or ", " @@ -20254,15 +25009,11 @@ do -- SET_STATIC end - do -- SET_CLIENT - --- @type SET_CLIENT -- @extends Core.Set#SET_BASE - - --- Mission designers can use the @{Core.Set#SET_CLIENT} class to build sets of units belonging to certain: -- -- * Coalitions @@ -20293,16 +25044,13 @@ do -- SET_CLIENT -- * @{#SET_CLIENT.FilterCountries}: Builds the SET_CLIENT with the clients belonging to the country(ies). -- * @{#SET_CLIENT.FilterPrefixes}: Builds the SET_CLIENT with the clients containing the same string(s) in their unit/pilot name. **ATTENTION!** Bad naming convention as this *does not* only filter *prefixes*. -- * @{#SET_CLIENT.FilterActive}: Builds the SET_CLIENT with the units that are only active. Units that are inactive (late activation) won't be included in the set! - -- + -- * @{#SET_CLIENT.FilterZones}: Builds the SET_CLIENT with the clients within a @{Core.Zone#ZONE}. + -- -- Once the filter criteria have been set for the SET_CLIENT, you can start filtering using: -- -- * @{#SET_CLIENT.FilterStart}: Starts the filtering of the clients **dynamically**. -- * @{#SET_CLIENT.FilterOnce}: Filters the clients **once**. -- - -- Planned filter criteria within development are (so these are not yet available): - -- - -- * @{#SET_CLIENT.FilterZones}: Builds the SET_CLIENT with the clients within a @{Core.Zone#ZONE}. - -- -- ## 4) SET_CLIENT iterators -- -- Once the filters have been defined and the SET_CLIENT has been built, you can iterate the SET_CLIENT with the available iterator methods. @@ -20322,6 +25070,9 @@ do -- SET_CLIENT Types = nil, Countries = nil, ClientPrefixes = nil, + Zones = nil, + Playernames = nil, + Callsigns = nil, }, FilterMeta = { Coalitions = { @@ -20339,7 +25090,6 @@ do -- SET_CLIENT }, } - --- Creates a new SET_CLIENT object, building a set of clients belonging to a coalitions, categories, countries, types or with defined prefix names. -- @param #SET_CLIENT self -- @return #SET_CLIENT @@ -20361,7 +25111,7 @@ do -- SET_CLIENT -- @return self function SET_CLIENT:AddClientsByName( AddClientNames ) - local AddClientNamesArray = ( type( AddClientNames ) == "table" ) and AddClientNames or { AddClientNames } + local AddClientNamesArray = (type( AddClientNames ) == "table") and AddClientNames or { AddClientNames } for AddClientID, AddClientName in pairs( AddClientNamesArray ) do self:Add( AddClientName, CLIENT:FindByName( AddClientName ) ) @@ -20376,7 +25126,7 @@ do -- SET_CLIENT -- @return self function SET_CLIENT:RemoveClientsByName( RemoveClientNames ) - local RemoveClientNamesArray = ( type( RemoveClientNames ) == "table" ) and RemoveClientNames or { RemoveClientNames } + local RemoveClientNamesArray = (type( RemoveClientNames ) == "table") and RemoveClientNames or { RemoveClientNames } for RemoveClientID, RemoveClientName in pairs( RemoveClientNamesArray ) do self:Remove( RemoveClientName.ClientName ) @@ -20385,7 +25135,6 @@ do -- SET_CLIENT return self end - --- Finds a Client based on the Client Name. -- @param #SET_CLIENT self -- @param #string ClientName @@ -20396,7 +25145,39 @@ do -- SET_CLIENT return ClientFound end + --- Builds a set of clients of certain callsigns. + -- @param #SET_CLIENT self + -- @param #string Callsigns Can be a single string e.g. "Ford", or a table of strings e.g. {"Uzi","Enfield","Chevy"}. Refers to the callsigns as they can be set in the mission editor. + -- @return #SET_CLIENT self + function SET_CLIENT:FilterCallsigns( Callsigns ) + if not self.Filter.Callsigns then + self.Filter.Callsigns = {} + end + if type( Callsigns ) ~= "table" then + Callsigns = { Callsigns } + end + for callsignID, callsign in pairs( Callsigns ) do + self.Filter.Callsigns[callsign] = callsign + end + return self + end + --- Builds a set of clients of certain playernames. + -- @param #SET_CLIENT self + -- @param #string Playernames Can be a single string e.g. "Apple", or a table of strings e.g. {"Walter","Hermann","Gonzo"}. Useful if you have e.g. a common squadron prefix. + -- @return #SET_CLIENT self + function SET_CLIENT:FilterPlayernames( Playernames ) + if not self.Filter.Playernames then + self.Filter.Playernames = {} + end + if type( Playernames ) ~= "table" then + Playernames = { Playernames } + end + for PlayernameID, playername in pairs( Playernames ) do + self.Filter.Playernames[playername] = playername + end + return self + end --- Builds a set of clients of coalitions. -- Possible current coalitions are red, blue and neutral. @@ -20416,7 +25197,6 @@ do -- SET_CLIENT return self end - --- Builds a set of clients out of categories. -- Possible current categories are plane, helicopter, ground, ship. -- @param #SET_CLIENT self @@ -20435,7 +25215,6 @@ do -- SET_CLIENT return self end - --- Builds a set of clients of defined client types. -- Possible current types are those types known within DCS world. -- @param #SET_CLIENT self @@ -20454,7 +25233,6 @@ do -- SET_CLIENT return self end - --- Builds a set of clients of defined countries. -- Possible current countries are those known within DCS world. -- @param #SET_CLIENT self @@ -20473,7 +25251,6 @@ do -- SET_CLIENT return self end - --- Builds a set of CLIENTs that contain the given string in their unit/pilot name. -- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all clients that **contain** the string. -- @param #SET_CLIENT self @@ -20495,7 +25272,7 @@ do -- SET_CLIENT --- Builds a set of clients that are only active. -- Only the clients that are active will be included within the set. -- @param #SET_CLIENT self - -- @param #boolean Active (optional) Include only active clients to the set. + -- @param #boolean Active (Optional) Include only active clients to the set. -- Include inactive clients if you provide false. -- @return #SET_CLIENT self -- @usage @@ -20513,12 +25290,34 @@ do -- SET_CLIENT -- ClientSet = SET_CLIENT:New():FilterActive( false ):FilterCoalition( "blue" ):FilterOnce() -- function SET_CLIENT:FilterActive( Active ) - Active = Active or not ( Active == false ) + Active = Active or not (Active == false) self.Filter.Active = Active return self end - + --- Builds a set of clients in zones. + -- @param #SET_CLIENT self + -- @param #table Zones Table of Core.Zone#ZONE Zone objects, or a Core.Set#SET_ZONE + -- @return #SET_CLIENT self + function SET_CLIENT:FilterZones( Zones ) + if not self.Filter.Zones then + self.Filter.Zones = {} + end + local zones = {} + if Zones.ClassName and Zones.ClassName == "SET_ZONE" then + zones = Zones.Set + elseif type( Zones ) ~= "table" or (type( Zones ) == "table" and Zones.ClassName ) then + self:E("***** FilterZones needs either a table of ZONE Objects or a SET_ZONE as parameter!") + return self + else + zones = Zones + end + for _,Zone in pairs( zones ) do + local zonename = Zone:GetName() + self.Filter.Zones[zonename] = Zone + end + return self + end --- Starts the filtering. -- @param #SET_CLIENT self @@ -20559,7 +25358,7 @@ do -- SET_CLIENT return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] end - --- Iterate the SET_CLIENT and call an interator function for each **alive** CLIENT, providing the CLIENT and optional parameters. + --- Iterate the SET_CLIENT and call an iterator function for each **alive** CLIENT, providing the CLIENT and optional parameters. -- @param #SET_CLIENT self -- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_CLIENT. The function needs to accept a CLIENT parameter. -- @return #SET_CLIENT self @@ -20571,7 +25370,7 @@ do -- SET_CLIENT return self end - --- Iterate the SET_CLIENT and call an iterator function for each **alive** CLIENT presence completely in a @{Zone}, providing the CLIENT and optional parameters to the called function. + --- Iterate the SET_CLIENT and call an iterator function for each **alive** CLIENT presence completely in a @{Core.Zone}, providing the CLIENT and optional parameters to the called function. -- @param #SET_CLIENT self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. -- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_CLIENT. The function needs to accept a CLIENT parameter. @@ -20593,7 +25392,7 @@ do -- SET_CLIENT return self end - --- Iterate the SET_CLIENT and call an iterator function for each **alive** CLIENT presence not in a @{Zone}, providing the CLIENT and optional parameters to the called function. + --- Iterate the SET_CLIENT and call an iterator function for each **alive** CLIENT presence not in a @{Core.Zone}, providing the CLIENT and optional parameters to the called function. -- @param #SET_CLIENT self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. -- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_CLIENT. The function needs to accept a CLIENT parameter. @@ -20615,6 +25414,44 @@ do -- SET_CLIENT return self end + --- Iterate the SET_CLIENT and count alive units. + -- @param #SET_CLIENT self + -- @return #number count + function SET_CLIENT:CountAlive() + + local Set = self:GetSet() + + local CountU = 0 + for UnitID, UnitData in pairs( Set ) do -- For each GROUP in SET_GROUP + if UnitData and UnitData:IsAlive() then + CountU = CountU + 1 + end + + end + + return CountU + end + + + --- Gets the alive set. + -- @param #SET_CLIENT self + -- @return #table Table of SET objects + function SET_CLIENT:GetAliveSet() + + local AliveSet = SET_CLIENT:New() + + -- Clean the Set before returning with only the alive Groups. + for GroupName, GroupObject in pairs(self.Set) do + local GroupObject=GroupObject --Wrapper.Client#CLIENT + + if GroupObject and GroupObject:IsAlive() then + AliveSet:Add(GroupName, GroupObject) + end + end + + return AliveSet.Set or {} + end + --- -- @param #SET_CLIENT self -- @param Wrapper.Client#CLIENT MClient @@ -20629,9 +25466,10 @@ do -- SET_CLIENT if self.Filter.Active ~= nil then local MClientActive = false - if self.Filter.Active == false or ( self.Filter.Active == true and MClient:IsActive() == true ) then + if self.Filter.Active == false or (self.Filter.Active == true and MClient:IsActive() == true and MClient:IsAlive() == true) then MClientActive = true end + --self:I( { "Evaluated Active", MClientActive } ) MClientInclude = MClientInclude and MClientActive end @@ -20676,7 +25514,7 @@ do -- SET_CLIENT if self.Filter.Countries then local MClientCountry = false for CountryID, CountryName in pairs( self.Filter.Countries ) do - local ClientCountryID = _DATABASE:GetCountryFromClientTemplate(MClientName) + local ClientCountryID = _DATABASE:GetCountryFromClientTemplate( MClientName ) self:T3( { "Country:", ClientCountryID, country.id[CountryName], CountryName } ) if country.id[CountryName] and country.id[CountryName] == ClientCountryID then MClientCountry = true @@ -20697,22 +25535,57 @@ do -- SET_CLIENT self:T( { "Evaluated Prefix", MClientPrefix } ) MClientInclude = MClientInclude and MClientPrefix end - end + if self.Filter.Zones then + local MClientZone = false + for ZoneName, Zone in pairs( self.Filter.Zones ) do + self:T3( "Zone:", ZoneName ) + local unit = MClient:GetClientGroupUnit() + if unit and unit:IsInZone(Zone) then + MClientZone = true + end + end + MClientInclude = MClientInclude and MClientZone + end + + if self.Filter.Playernames then + local MClientPlayername = false + local playername = MClient:GetPlayerName() or "Unknown" + --self:I(playername) + for _,_Playername in pairs(self.Filter.Playernames) do + if playername and string.find(playername,_Playername) then + MClientPlayername = true + end + end + self:T( { "Evaluated Playername", MClientPlayername } ) + MClientInclude = MClientInclude and MClientPlayername + end + + if self.Filter.Callsigns then + local MClientCallsigns = false + local callsign = MClient:GetCallsign() + --self:I(callsign) + for _,_Callsign in pairs(self.Filter.Callsigns) do + if callsign and string.find(callsign,_Callsign) then + MClientCallsigns = true + end + end + self:T( { "Evaluated Callsign", MClientCallsigns } ) + MClientInclude = MClientInclude and MClientCallsigns + end + + end self:T2( MClientInclude ) return MClientInclude end end - do -- SET_PLAYER --- @type SET_PLAYER -- @extends Core.Set#SET_BASE - - --- Mission designers can use the @{Core.Set#SET_PLAYER} class to build sets of units belonging to alive players: -- -- ## SET_PLAYER constructor @@ -20759,6 +25632,7 @@ do -- SET_PLAYER Types = nil, Countries = nil, ClientPrefixes = nil, + Zones = nil, }, FilterMeta = { Coalitions = { @@ -20776,7 +25650,6 @@ do -- SET_PLAYER }, } - --- Creates a new SET_PLAYER object, building a set of clients belonging to a coalitions, categories, countries, types or with defined prefix names. -- @param #SET_PLAYER self -- @return #SET_PLAYER @@ -20796,7 +25669,7 @@ do -- SET_PLAYER -- @return self function SET_PLAYER:AddClientsByName( AddClientNames ) - local AddClientNamesArray = ( type( AddClientNames ) == "table" ) and AddClientNames or { AddClientNames } + local AddClientNamesArray = (type( AddClientNames ) == "table") and AddClientNames or { AddClientNames } for AddClientID, AddClientName in pairs( AddClientNamesArray ) do self:Add( AddClientName, CLIENT:FindByName( AddClientName ) ) @@ -20811,7 +25684,7 @@ do -- SET_PLAYER -- @return self function SET_PLAYER:RemoveClientsByName( RemoveClientNames ) - local RemoveClientNamesArray = ( type( RemoveClientNames ) == "table" ) and RemoveClientNames or { RemoveClientNames } + local RemoveClientNamesArray = (type( RemoveClientNames ) == "table") and RemoveClientNames or { RemoveClientNames } for RemoveClientID, RemoveClientName in pairs( RemoveClientNamesArray ) do self:Remove( RemoveClientName.ClientName ) @@ -20820,7 +25693,6 @@ do -- SET_PLAYER return self end - --- Finds a Client based on the Player Name. -- @param #SET_PLAYER self -- @param #string PlayerName @@ -20831,8 +25703,6 @@ do -- SET_PLAYER return ClientFound end - - --- Builds a set of clients of coalitions joined by specific players. -- Possible current coalitions are red, blue and neutral. -- @param #SET_PLAYER self @@ -20850,7 +25720,31 @@ do -- SET_PLAYER end return self end - + + --- Builds a set of players in zones. + -- @param #SET_PLAYER self + -- @param #table Zones Table of Core.Zone#ZONE Zone objects, or a Core.Set#SET_ZONE + -- @return #SET_PLAYER self + function SET_PLAYER:FilterZones( Zones ) + if not self.Filter.Zones then + self.Filter.Zones = {} + end + local zones = {} + if Zones.ClassName and Zones.ClassName == "SET_ZONE" then + zones = Zones.Set + elseif type( Zones ) ~= "table" or (type( Zones ) == "table" and Zones.ClassName ) then + self:E("***** FilterZones needs either a table of ZONE Objects or a SET_ZONE as parameter!") + return self + else + zones = Zones + end + for _,Zone in pairs( zones ) do + local zonename = Zone:GetName() + self.Filter.Zones[zonename] = Zone + end + return self + end + --- Builds a set of clients out of categories joined by players. -- Possible current categories are plane, helicopter, ground, ship. @@ -20870,7 +25764,6 @@ do -- SET_PLAYER return self end - --- Builds a set of clients of defined client types joined by players. -- Possible current types are those types known within DCS world. -- @param #SET_PLAYER self @@ -20889,7 +25782,6 @@ do -- SET_PLAYER return self end - --- Builds a set of clients of defined countries. -- Possible current countries are those known within DCS world. -- @param #SET_PLAYER self @@ -20908,7 +25800,6 @@ do -- SET_PLAYER return self end - --- Builds a set of PLAYERs that contain the given string in their unit/pilot name. -- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all player clients that **contain** the string. -- @param #SET_PLAYER self @@ -20927,9 +25818,6 @@ do -- SET_PLAYER return self end - - - --- Starts the filtering. -- @param #SET_PLAYER self -- @return #SET_PLAYER self @@ -20969,7 +25857,7 @@ do -- SET_PLAYER return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] end - --- Iterate the SET_PLAYER and call an interator function for each **alive** CLIENT, providing the CLIENT and optional parameters. + --- Iterate the SET_PLAYER and call an iterator function for each **alive** CLIENT, providing the CLIENT and optional parameters. -- @param #SET_PLAYER self -- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_PLAYER. The function needs to accept a CLIENT parameter. -- @return #SET_PLAYER self @@ -20981,7 +25869,7 @@ do -- SET_PLAYER return self end - --- Iterate the SET_PLAYER and call an iterator function for each **alive** CLIENT presence completely in a @{Zone}, providing the CLIENT and optional parameters to the called function. + --- Iterate the SET_PLAYER and call an iterator function for each **alive** CLIENT presence completely in a @{Core.Zone}, providing the CLIENT and optional parameters to the called function. -- @param #SET_PLAYER self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. -- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_PLAYER. The function needs to accept a CLIENT parameter. @@ -21003,7 +25891,7 @@ do -- SET_PLAYER return self end - --- Iterate the SET_PLAYER and call an iterator function for each **alive** CLIENT presence not in a @{Zone}, providing the CLIENT and optional parameters to the called function. + --- Iterate the SET_PLAYER and call an iterator function for each **alive** CLIENT presence not in a @{Core.Zone}, providing the CLIENT and optional parameters to the called function. -- @param #SET_PLAYER self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. -- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_PLAYER. The function needs to accept a CLIENT parameter. @@ -21078,7 +25966,7 @@ do -- SET_PLAYER if self.Filter.Countries then local MClientCountry = false for CountryID, CountryName in pairs( self.Filter.Countries ) do - local ClientCountryID = _DATABASE:GetCountryFromClientTemplate(MClientName) + local ClientCountryID = _DATABASE:GetCountryFromClientTemplate( MClientName ) self:T3( { "Country:", ClientCountryID, country.id[CountryName], CountryName } ) if country.id[CountryName] and country.id[CountryName] == ClientCountryID then MClientCountry = true @@ -21100,14 +25988,25 @@ do -- SET_PLAYER MClientInclude = MClientInclude and MClientPrefix end end - + + if self.Filter.Zones then + local MClientZone = false + for ZoneName, Zone in pairs( self.Filter.Zones ) do + self:T3( "Zone:", ZoneName ) + local unit = MClient:GetClientGroupUnit() + if unit and unit:IsInZone(Zone) then + MClientZone = true + end + end + MClientInclude = MClientInclude and MClientZone + end + self:T2( MClientInclude ) return MClientInclude end end - do -- SET_AIRBASE --- @type SET_AIRBASE @@ -21169,7 +26068,6 @@ do -- SET_AIRBASE }, } - --- Creates a new SET_AIRBASE object, building a set of airbases belonging to a coalitions and categories. -- @param #SET_AIRBASE self -- @return #SET_AIRBASE self @@ -21200,7 +26098,7 @@ do -- SET_AIRBASE -- @return self function SET_AIRBASE:AddAirbasesByName( AddAirbaseNames ) - local AddAirbaseNamesArray = ( type( AddAirbaseNames ) == "table" ) and AddAirbaseNames or { AddAirbaseNames } + local AddAirbaseNamesArray = (type( AddAirbaseNames ) == "table") and AddAirbaseNames or { AddAirbaseNames } for AddAirbaseID, AddAirbaseName in pairs( AddAirbaseNamesArray ) do self:Add( AddAirbaseName, AIRBASE:FindByName( AddAirbaseName ) ) @@ -21215,7 +26113,7 @@ do -- SET_AIRBASE -- @return self function SET_AIRBASE:RemoveAirbasesByName( RemoveAirbaseNames ) - local RemoveAirbaseNamesArray = ( type( RemoveAirbaseNames ) == "table" ) and RemoveAirbaseNames or { RemoveAirbaseNames } + local RemoveAirbaseNamesArray = (type( RemoveAirbaseNames ) == "table") and RemoveAirbaseNames or { RemoveAirbaseNames } for RemoveAirbaseID, RemoveAirbaseName in pairs( RemoveAirbaseNamesArray ) do self:Remove( RemoveAirbaseName ) @@ -21224,7 +26122,6 @@ do -- SET_AIRBASE return self end - --- Finds a Airbase based on the Airbase Name. -- @param #SET_AIRBASE self -- @param #string AirbaseName @@ -21235,7 +26132,6 @@ do -- SET_AIRBASE return AirbaseFound end - --- Finds an Airbase in range of a coordinate. -- @param #SET_AIRBASE self -- @param Core.Point#COORDINATE Coordinate @@ -21250,7 +26146,7 @@ do -- SET_AIRBASE local AirbaseCoordinate = AirbaseObject:GetCoordinate() local Distance = Coordinate:Get2DDistance( AirbaseCoordinate ) - self:F({Distance=Distance}) + self:F( { Distance = Distance } ) if Distance <= Range then AirbaseFound = AirbaseObject @@ -21262,7 +26158,6 @@ do -- SET_AIRBASE return AirbaseFound end - --- Finds a random Airbase in the set. -- @param #SET_AIRBASE self -- @return Wrapper.Airbase#AIRBASE The found Airbase. @@ -21274,8 +26169,6 @@ do -- SET_AIRBASE return RandomAirbase end - - --- Builds a set of airbases of coalitions. -- Possible current coalitions are red, blue and neutral. -- @param #SET_AIRBASE self @@ -21294,7 +26187,6 @@ do -- SET_AIRBASE return self end - --- Builds a set of airbases out of categories. -- Possible current categories are plane, helicopter, ground, ship. -- @param #SET_AIRBASE self @@ -21321,8 +26213,8 @@ do -- SET_AIRBASE if _DATABASE then -- We use the BaseCaptured event, which is generated by DCS when a base got captured. - self:HandleEvent(EVENTS.BaseCaptured) - self:HandleEvent(EVENTS.Dead) + self:HandleEvent( EVENTS.BaseCaptured ) + self:HandleEvent( EVENTS.Dead ) -- We initialize the first set. for ObjectName, Object in pairs( self.Database ) do @@ -21340,7 +26232,7 @@ do -- SET_AIRBASE --- Base capturing event. -- @param #SET_AIRBASE self -- @param Core.Event#EVENT EventData - function SET_AIRBASE:OnEventBaseCaptured(EventData) + function SET_AIRBASE:OnEventBaseCaptured( EventData ) -- When a base got captured, we reevaluate the set. for ObjectName, Object in pairs( self.Database ) do @@ -21358,17 +26250,16 @@ do -- SET_AIRBASE --- Dead event. -- @param #SET_AIRBASE self -- @param Core.Event#EVENT EventData - function SET_AIRBASE:OnEventDead(EventData) + function SET_AIRBASE:OnEventDead( EventData ) - local airbaseName, airbase=self:FindInDatabase(EventData) + local airbaseName, airbase = self:FindInDatabase( EventData ) - if airbase and airbase:IsShip() or airbase:IsHelipad() then - self:RemoveAirbasesByName(airbaseName) + if airbase and (airbase:IsShip() or airbase:IsHelipad()) then + self:RemoveAirbasesByName( airbaseName ) end end - --- Handles the Database to check on an event (birth) that the Object was added in the Database. -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! -- @param #SET_AIRBASE self @@ -21391,7 +26282,7 @@ do -- SET_AIRBASE return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] end - --- Iterate the SET_AIRBASE and call an interator function for each AIRBASE, providing the AIRBASE and optional parameters. + --- Iterate the SET_AIRBASE and call an iterator function for each AIRBASE, providing the AIRBASE and optional parameters. -- @param #SET_AIRBASE self -- @param #function IteratorFunction The function that will be called when there is an alive AIRBASE in the SET_AIRBASE. The function needs to accept a AIRBASE parameter. -- @return #SET_AIRBASE self @@ -21414,8 +26305,6 @@ do -- SET_AIRBASE return NearestAirbase end - - --- -- @param #SET_AIRBASE self -- @param Wrapper.Airbase#AIRBASE MAirbase @@ -21461,7 +26350,6 @@ do -- SET_AIRBASE end - do -- SET_CARGO --- @type SET_CARGO @@ -21507,7 +26395,6 @@ do -- SET_CARGO -- * @{#SET_CARGO.ForEachCargo}: Calls a function for each cargo it finds within the SET_CARGO. -- -- @field #SET_CARGO SET_CARGO - -- SET_CARGO = { ClassName = "SET_CARGO", Cargos = {}, @@ -21526,40 +26413,37 @@ do -- SET_CARGO }, } - --- Creates a new SET_CARGO object, building a set of cargos belonging to a coalitions and categories. -- @param #SET_CARGO self -- @return #SET_CARGO -- @usage -- -- Define a new SET_CARGO Object. The DatabaseSet will contain a reference to all Cargos. -- DatabaseSet = SET_CARGO:New() - function SET_CARGO:New() --R2.1 + function SET_CARGO:New() -- R2.1 -- Inherits from BASE local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.CARGOS ) ) -- #SET_CARGO return self end - --- (R2.1) Add CARGO to SET_CARGO. -- @param Core.Set#SET_CARGO self -- @param Cargo.Cargo#CARGO Cargo A single cargo. -- @return Core.Set#SET_CARGO self - function SET_CARGO:AddCargo( Cargo ) --R2.4 + function SET_CARGO:AddCargo( Cargo ) -- R2.4 self:Add( Cargo:GetName(), Cargo ) return self end - --- (R2.1) Add CARGOs to SET_CARGO. -- @param Core.Set#SET_CARGO self -- @param #string AddCargoNames A single name or an array of CARGO names. -- @return Core.Set#SET_CARGO self - function SET_CARGO:AddCargosByName( AddCargoNames ) --R2.1 + function SET_CARGO:AddCargosByName( AddCargoNames ) -- R2.1 - local AddCargoNamesArray = ( type( AddCargoNames ) == "table" ) and AddCargoNames or { AddCargoNames } + local AddCargoNamesArray = (type( AddCargoNames ) == "table") and AddCargoNames or { AddCargoNames } for AddCargoID, AddCargoName in pairs( AddCargoNamesArray ) do self:Add( AddCargoName, CARGO:FindByName( AddCargoName ) ) @@ -21572,9 +26456,9 @@ do -- SET_CARGO -- @param Core.Set#SET_CARGO self -- @param Wrapper.Cargo#CARGO RemoveCargoNames A single name or an array of CARGO names. -- @return Core.Set#SET_CARGO self - function SET_CARGO:RemoveCargosByName( RemoveCargoNames ) --R2.1 + function SET_CARGO:RemoveCargosByName( RemoveCargoNames ) -- R2.1 - local RemoveCargoNamesArray = ( type( RemoveCargoNames ) == "table" ) and RemoveCargoNames or { RemoveCargoNames } + local RemoveCargoNamesArray = (type( RemoveCargoNames ) == "table") and RemoveCargoNames or { RemoveCargoNames } for RemoveCargoID, RemoveCargoName in pairs( RemoveCargoNamesArray ) do self:Remove( RemoveCargoName.CargoName ) @@ -21583,25 +26467,22 @@ do -- SET_CARGO return self end - --- (R2.1) Finds a Cargo based on the Cargo Name. -- @param #SET_CARGO self -- @param #string CargoName -- @return Wrapper.Cargo#CARGO The found Cargo. - function SET_CARGO:FindCargo( CargoName ) --R2.1 + function SET_CARGO:FindCargo( CargoName ) -- R2.1 local CargoFound = self.Set[CargoName] return CargoFound end - - --- (R2.1) Builds a set of cargos of coalitions. -- Possible current coalitions are red, blue and neutral. -- @param #SET_CARGO self -- @param #string Coalitions Can take the following values: "red", "blue", "neutral". -- @return #SET_CARGO self - function SET_CARGO:FilterCoalitions( Coalitions ) --R2.1 + function SET_CARGO:FilterCoalitions( Coalitions ) -- R2.1 if not self.Filter.Coalitions then self.Filter.Coalitions = {} end @@ -21619,7 +26500,7 @@ do -- SET_CARGO -- @param #SET_CARGO self -- @param #string Types Can take those type strings known within DCS world. -- @return #SET_CARGO self - function SET_CARGO:FilterTypes( Types ) --R2.1 + function SET_CARGO:FilterTypes( Types ) -- R2.1 if not self.Filter.Types then self.Filter.Types = {} end @@ -21632,13 +26513,12 @@ do -- SET_CARGO return self end - --- (R2.1) Builds a set of cargos of defined countries. -- Possible current countries are those known within DCS world. -- @param #SET_CARGO self -- @param #string Countries Can take those country strings known within DCS world. -- @return #SET_CARGO self - function SET_CARGO:FilterCountries( Countries ) --R2.1 + function SET_CARGO:FilterCountries( Countries ) -- R2.1 if not self.Filter.Countries then self.Filter.Countries = {} end @@ -21651,13 +26531,12 @@ do -- SET_CARGO return self end - --- Builds a set of CARGOs that contain a given string in their name. -- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all cargos that **contain** the string. -- @param #SET_CARGO self -- @param #string Prefixes The string pattern(s) that need to be in the cargo name. Can also be passed as a `#table` of strings. -- @return #SET_CARGO self - function SET_CARGO:FilterPrefixes( Prefixes ) --R2.1 + function SET_CARGO:FilterPrefixes( Prefixes ) -- R2.1 if not self.Filter.CargoPrefixes then self.Filter.CargoPrefixes = {} end @@ -21670,12 +26549,10 @@ do -- SET_CARGO return self end - - --- (R2.1) Starts the filtering. -- @param #SET_CARGO self -- @return #SET_CARGO self - function SET_CARGO:FilterStart() --R2.1 + function SET_CARGO:FilterStart() -- R2.1 if _DATABASE then self:_FilterStart() @@ -21697,14 +26574,13 @@ do -- SET_CARGO return self end - --- (R2.1) Handles the Database to check on an event (birth) that the Object was added in the Database. -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! -- @param #SET_CARGO self -- @param Core.Event#EVENTDATA Event -- @return #string The name of the CARGO -- @return #table The CARGO - function SET_CARGO:AddInDatabase( Event ) --R2.1 + function SET_CARGO:AddInDatabase( Event ) -- R2.1 self:F3( { Event } ) return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] @@ -21716,17 +26592,17 @@ do -- SET_CARGO -- @param Core.Event#EVENTDATA Event -- @return #string The name of the CARGO -- @return #table The CARGO - function SET_CARGO:FindInDatabase( Event ) --R2.1 + function SET_CARGO:FindInDatabase( Event ) -- R2.1 self:F3( { Event } ) return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] end - --- (R2.1) Iterate the SET_CARGO and call an interator function for each CARGO, providing the CARGO and optional parameters. + --- (R2.1) Iterate the SET_CARGO and call an iterator function for each CARGO, providing the CARGO and optional parameters. -- @param #SET_CARGO self -- @param #function IteratorFunction The function that will be called when there is an alive CARGO in the SET_CARGO. The function needs to accept a CARGO parameter. -- @return #SET_CARGO self - function SET_CARGO:ForEachCargo( IteratorFunction, ... ) --R2.1 + function SET_CARGO:ForEachCargo( IteratorFunction, ... ) -- R2.1 self:F2( arg ) self:ForEach( IteratorFunction, arg, self:GetSet() ) @@ -21738,7 +26614,7 @@ do -- SET_CARGO -- @param #SET_CARGO self -- @param Core.Point#POINT_VEC2 PointVec2 A @{Core.Point#POINT_VEC2} object from where to evaluate the closest @{Cargo.Cargo#CARGO}. -- @return Wrapper.Cargo#CARGO The closest @{Cargo.Cargo#CARGO}. - function SET_CARGO:FindNearestCargoFromPointVec2( PointVec2 ) --R2.1 + function SET_CARGO:FindNearestCargoFromPointVec2( PointVec2 ) -- R2.1 self:F2( PointVec2 ) local NearestCargo = self:FindNearestObjectFromPointVec2( PointVec2 ) @@ -21773,7 +26649,6 @@ do -- SET_CARGO return FirstCargo end - --- Iterate the SET_CARGO while identifying the first @{Cargo.Cargo#CARGO} that is UnLoaded. -- @param #SET_CARGO self -- @return Cargo.Cargo#CARGO The first @{Cargo.Cargo#CARGO}. @@ -21782,7 +26657,6 @@ do -- SET_CARGO return FirstCargo end - --- Iterate the SET_CARGO while identifying the first @{Cargo.Cargo#CARGO} that is UnLoaded and not Deployed. -- @param #SET_CARGO self -- @return Cargo.Cargo#CARGO The first @{Cargo.Cargo#CARGO}. @@ -21791,7 +26665,6 @@ do -- SET_CARGO return FirstCargo end - --- Iterate the SET_CARGO while identifying the first @{Cargo.Cargo#CARGO} that is Loaded. -- @param #SET_CARGO self -- @return Cargo.Cargo#CARGO The first @{Cargo.Cargo#CARGO}. @@ -21800,7 +26673,6 @@ do -- SET_CARGO return FirstCargo end - --- Iterate the SET_CARGO while identifying the first @{Cargo.Cargo#CARGO} that is Deployed. -- @param #SET_CARGO self -- @return Cargo.Cargo#CARGO The first @{Cargo.Cargo#CARGO}. @@ -21809,14 +26681,11 @@ do -- SET_CARGO return FirstCargo end - - - --- (R2.1) -- @param #SET_CARGO self -- @param AI.AI_Cargo#AI_CARGO MCargo -- @return #SET_CARGO self - function SET_CARGO:IsIncludeObject( MCargo ) --R2.1 + function SET_CARGO:IsIncludeObject( MCargo ) -- R2.1 self:F2( MCargo ) local MCargoInclude = true @@ -21869,13 +26738,13 @@ do -- SET_CARGO --- (R2.1) Handles the OnEventNewCargo event for the Set. -- @param #SET_CARGO self -- @param Core.Event#EVENTDATA EventData - function SET_CARGO:OnEventNewCargo( EventData ) --R2.1 + function SET_CARGO:OnEventNewCargo( EventData ) -- R2.1 self:F( { "New Cargo", EventData } ) if EventData.Cargo then if EventData.Cargo and self:IsIncludeObject( EventData.Cargo ) then - self:Add( EventData.Cargo.Name , EventData.Cargo ) + self:Add( EventData.Cargo.Name, EventData.Cargo ) end end end @@ -21883,20 +26752,20 @@ do -- SET_CARGO --- (R2.1) Handles the OnDead or OnCrash event for alive units set. -- @param #SET_CARGO self -- @param Core.Event#EVENTDATA EventData - function SET_CARGO:OnEventDeleteCargo( EventData ) --R2.1 + function SET_CARGO:OnEventDeleteCargo( EventData ) -- R2.1 self:F3( { EventData } ) if EventData.Cargo then local Cargo = _DATABASE:FindCargo( EventData.Cargo.Name ) if Cargo and Cargo.Name then - -- When cargo was deleted, it may probably be because of an S_EVENT_DEAD. - -- However, in the loading logic, an S_EVENT_DEAD is also generated after a Destroy() call. - -- And this is a problem because it will remove all entries from the SET_CARGOs. - -- To prevent this from happening, the Cargo object has a flag NoDestroy. - -- When true, the SET_CARGO won't Remove the Cargo object from the set. - -- This flag is switched off after the event handlers have been called in the EVENT class. - self:F( { CargoNoDestroy=Cargo.NoDestroy } ) + -- When cargo was deleted, it may probably be because of an S_EVENT_DEAD. + -- However, in the loading logic, an S_EVENT_DEAD is also generated after a Destroy() call. + -- And this is a problem because it will remove all entries from the SET_CARGOs. + -- To prevent this from happening, the Cargo object has a flag NoDestroy. + -- When true, the SET_CARGO won't Remove the Cargo object from the set. + -- This flag is switched off after the event handlers have been called in the EVENT class. + self:F( { CargoNoDestroy = Cargo.NoDestroy } ) if Cargo.NoDestroy then else self:Remove( Cargo.Name ) @@ -21907,7 +26776,6 @@ do -- SET_CARGO end - do -- SET_ZONE --- @type SET_ZONE @@ -21953,11 +26821,10 @@ do -- SET_ZONE Filter = { Prefixes = nil, }, - FilterMeta = { + FilterMeta = { }, } - --- Creates a new SET_ZONE object, building a set of zones. -- @param #SET_ZONE self -- @return #SET_ZONE self @@ -21977,7 +26844,7 @@ do -- SET_ZONE -- @return self function SET_ZONE:AddZonesByName( AddZoneNames ) - local AddZoneNamesArray = ( type( AddZoneNames ) == "table" ) and AddZoneNames or { AddZoneNames } + local AddZoneNamesArray = (type( AddZoneNames ) == "table") and AddZoneNames or { AddZoneNames } for AddAirbaseID, AddZoneName in pairs( AddZoneNamesArray ) do self:Add( AddZoneName, ZONE:FindByName( AddZoneName ) ) @@ -21997,14 +26864,13 @@ do -- SET_ZONE return self end - --- Remove ZONEs from SET_ZONE. -- @param Core.Set#SET_ZONE self -- @param Core.Zone#ZONE_BASE RemoveZoneNames A single name or an array of ZONE_BASE names. -- @return self function SET_ZONE:RemoveZonesByName( RemoveZoneNames ) - local RemoveZoneNamesArray = ( type( RemoveZoneNames ) == "table" ) and RemoveZoneNames or { RemoveZoneNames } + local RemoveZoneNamesArray = (type( RemoveZoneNames ) == "table") and RemoveZoneNames or { RemoveZoneNames } for RemoveZoneID, RemoveZoneName in pairs( RemoveZoneNamesArray ) do self:Remove( RemoveZoneName ) @@ -22013,7 +26879,6 @@ do -- SET_ZONE return self end - --- Finds a Zone based on the Zone Name. -- @param #SET_ZONE self -- @param #string ZoneName @@ -22024,14 +26889,13 @@ do -- SET_ZONE return ZoneFound end - --- Get a random zone from the set. -- @param #SET_ZONE self -- @param #number margin Number of tries to find a zone -- @return Core.Zone#ZONE_BASE The random Zone. -- @return #nil if no zone in the collection. - function SET_ZONE:GetRandomZone(margin) - + function SET_ZONE:GetRandomZone( margin ) + local margin = margin or 100 if self:Count() ~= 0 then @@ -22054,7 +26918,6 @@ do -- SET_ZONE return nil end - --- Set a zone probability. -- @param #SET_ZONE self -- @param #string ZoneName The name of the zone. @@ -22063,9 +26926,6 @@ do -- SET_ZONE Zone:SetZoneProbability( ZoneProbability ) end - - - --- Builds a set of ZONEs that contain the given string in their name. -- **ATTENTION!** Bad naming convention as this **does not** filter only **prefixes** but all zones that **contain** the string. -- @param #SET_ZONE self @@ -22084,7 +26944,6 @@ do -- SET_ZONE return self end - --- Starts the filtering. -- @param #SET_ZONE self -- @return #SET_ZONE self @@ -22143,7 +27002,7 @@ do -- SET_ZONE return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] end - --- Iterate the SET_ZONE and call an interator function for each ZONE, providing the ZONE and optional parameters. + --- Iterate the SET_ZONE and call an iterator function for each ZONE, providing the ZONE and optional parameters. -- @param #SET_ZONE self -- @param #function IteratorFunction The function that will be called when there is an alive ZONE in the SET_ZONE. The function needs to accept a AIRBASE parameter. -- @return #SET_ZONE self @@ -22155,8 +27014,28 @@ do -- SET_ZONE return self end + --- Draw all zones in the set on the F10 map. + -- @param #SET_ZONE 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. + -- @param #number Alpha Transparency [0,1]. Default 1. + -- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. + -- @param #number FillAlpha Transparency [0,1]. Default 0.15. + -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. + -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. + -- @return #SET_ZONE self + function SET_ZONE:DrawZone(Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) + + for _,_zone in pairs(self.Set) do + local zone=_zone --Core.Zone#ZONE + zone:DrawZone(Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) + end + + return self + end + - --- + --- Private function. -- @param #SET_ZONE self -- @param Core.Zone#ZONE_BASE MZone -- @return #SET_ZONE self @@ -22171,7 +27050,7 @@ do -- SET_ZONE if self.Filter.Prefixes then local MZonePrefix = false for ZonePrefixId, ZonePrefix in pairs( self.Filter.Prefixes ) do - self:T3( { "Prefix:", string.find( MZoneName, ZonePrefix, 1 ), ZonePrefix } ) + self:T2( { "Prefix:", string.find( MZoneName, ZonePrefix, 1 ), ZonePrefix } ) if string.find( MZoneName, ZonePrefix, 1 ) then MZonePrefix = true end @@ -22188,13 +27067,13 @@ do -- SET_ZONE --- Handles the OnEventNewZone event for the Set. -- @param #SET_ZONE self -- @param Core.Event#EVENTDATA EventData - function SET_ZONE:OnEventNewZone( EventData ) --R2.1 + function SET_ZONE:OnEventNewZone( EventData ) -- R2.1 self:F( { "New Zone", EventData } ) if EventData.Zone then if EventData.Zone and self:IsIncludeObject( EventData.Zone ) then - self:Add( EventData.Zone.ZoneName , EventData.Zone ) + self:Add( EventData.Zone.ZoneName, EventData.Zone ) end end end @@ -22202,20 +27081,20 @@ do -- SET_ZONE --- Handles the OnDead or OnCrash event for alive units set. -- @param #SET_ZONE self -- @param Core.Event#EVENTDATA EventData - function SET_ZONE:OnEventDeleteZone( EventData ) --R2.1 + function SET_ZONE:OnEventDeleteZone( EventData ) -- R2.1 self:F3( { EventData } ) if EventData.Zone then local Zone = _DATABASE:FindZone( EventData.Zone.ZoneName ) if Zone and Zone.ZoneName then - -- When cargo was deleted, it may probably be because of an S_EVENT_DEAD. - -- However, in the loading logic, an S_EVENT_DEAD is also generated after a Destroy() call. - -- And this is a problem because it will remove all entries from the SET_ZONEs. - -- To prevent this from happening, the Zone object has a flag NoDestroy. - -- When true, the SET_ZONE won't Remove the Zone object from the set. - -- This flag is switched off after the event handlers have been called in the EVENT class. - self:F( { ZoneNoDestroy=Zone.NoDestroy } ) + -- When cargo was deleted, it may probably be because of an S_EVENT_DEAD. + -- However, in the loading logic, an S_EVENT_DEAD is also generated after a Destroy() call. + -- And this is a problem because it will remove all entries from the SET_ZONEs. + -- To prevent this from happening, the Zone object has a flag NoDestroy. + -- When true, the SET_ZONE won't Remove the Zone object from the set. + -- This flag is switched off after the event handlers have been called in the EVENT class. + self:F( { ZoneNoDestroy = Zone.NoDestroy } ) if Zone.NoDestroy then else self:Remove( Zone.ZoneName ) @@ -22225,12 +27104,11 @@ do -- SET_ZONE end --- Validate if a coordinate is in one of the zones in the set. - -- Returns the ZONE object where the coordiante is located. + -- Returns the ZONE object where the coordinate is located. -- If zones overlap, the first zone that validates the test is returned. -- @param #SET_ZONE self -- @param Core.Point#COORDINATE Coordinate The coordinate to be searched. - -- @return Core.Zone#ZONE_BASE The zone that validates the coordinate location. - -- @return #nil No zone has been found. + -- @return Core.Zone#ZONE_BASE The zone (if any) that validates the coordinate location. function SET_ZONE:IsCoordinateInZone( Coordinate ) for _, Zone in pairs( self:GetSet() ) do @@ -22242,6 +27120,27 @@ do -- SET_ZONE return nil end + + --- Get the closest zone to a given coordinate. + -- @param #SET_ZONE self + -- @param Core.Point#COORDINATE Coordinate The reference coordinate from which the closest zone is determined. + -- @return Core.Zone#ZONE_BASE The closest zone (if any). + -- @return #number Distance to ref coordinate in meters. + function SET_ZONE:GetClosestZone( Coordinate ) + + local dmin=math.huge + local zmin=nil + for _, Zone in pairs( self:GetSet() ) do + local Zone = Zone -- Core.Zone#ZONE_BASE + local d=Zone:Get2DDistance(Coordinate) + if d x2 ) and Coordinate.x or x2 + y1 = ( Coordinate.y < y1 ) and Coordinate.y or y1 + y2 = ( Coordinate.y > y2 ) and Coordinate.y or y2 + z1 = ( Coordinate.y < z1 ) and Coordinate.z or z1 + z2 = ( Coordinate.y > z2 ) and Coordinate.z or z2 + + end + + Coordinate.x = ( x2 - x1 ) / 2 + x1 + Coordinate.y = ( y2 - y1 ) / 2 + y1 + Coordinate.z = ( z2 - z1 ) / 2 + z1 + + self:F( { Coordinate = Coordinate } ) + return Coordinate + + end + + --- + -- @param #SET_SCENERY self + -- @param Wrapper.Scenery#SCENERY MScenery + -- @return #SET_SCENERY self + function SET_SCENERY:IsIncludeObject( MScenery ) + self:F2( MScenery ) + return true + end +end --- **Core** - Defines an extensive API to manage 3D points in the DCS World 3D simulation space. -- -- ## Features: --- +-- -- * Provides a COORDINATE class, which allows to manage points in 3D space and perform various operations on it. -- * Provides a POINT\_VEC2 class, which is derived from COORDINATE, and allows to manage points in 3D space, but from a Lat/Lon and Altitude perspective. -- * Provides a POINT\_VEC3 class, which is derived from COORDINATE, and allows to manage points in 3D space, but from a X, Z and Y vector perspective. --- +-- -- === -- -- # Demo Missions @@ -22592,22 +28844,31 @@ end -- -- ### Authors: -- --- * FlightControl : Design & Programming +-- * FlightControl (Design & Programming) -- -- ### Contributions: +-- +-- * funkyfranky +-- * Applevangelist +-- +-- === -- -- @module Core.Point -- @image Core_Coordinate.JPG - - do -- COORDINATE --- @type COORDINATE + -- @field #string ClassName Name of the class + -- @field #number x Component of the 3D vector. + -- @field #number y Component of the 3D vector. + -- @field #number z Component of the 3D vector. + -- @field #number Heading Heading in degrees. Needs to be set first. + -- @field #number Velocity Velocity in meters per second. Needs to be set first. -- @extends Core.Base#BASE - - + + --- Defines a 3D point in the simulator and with its methods, you can use or manipulate the point in 3D space. -- -- # 1) Create a COORDINATE object. @@ -22650,17 +28911,17 @@ do -- COORDINATE -- -- -- # 3) Create markings on the map. - -- - -- Place markers (text boxes with clarifications for briefings, target locations or any other reference point) + -- + -- Place markers (text boxes with clarifications for briefings, target locations or any other reference point) -- on the map for all players, coalitions or specific groups: - -- + -- -- * @{#COORDINATE.MarkToAll}(): Place a mark to all players. -- * @{#COORDINATE.MarkToCoalition}(): Place a mark to a coalition. -- * @{#COORDINATE.MarkToCoalitionRed}(): Place a mark to the red coalition. -- * @{#COORDINATE.MarkToCoalitionBlue}(): Place a mark to the blue coalition. -- * @{#COORDINATE.MarkToGroup}(): Place a mark to a group (needs to have a client in it or a CA group (CA group is bugged)). -- * @{#COORDINATE.RemoveMark}(): Removes a mark from the map. - -- + -- -- # 4) Coordinate calculation methods. -- -- Various calculation methods exist to use or manipulate 3D space. Find below a short description of each method: @@ -22690,37 +28951,37 @@ do -- COORDINATE -- -- * @{#COORDINATE.GetRandomVec2InRadius}(): Provides a random 2D vector around the current 3D point, in the given inner to outer band. -- * @{#COORDINATE.GetRandomVec3InRadius}(): Provides a random 3D vector around the current 3D point, in the given inner to outer band. - -- + -- -- ## 4.6) LOS between coordinates. - -- + -- -- Calculate if the coordinate has Line of Sight (LOS) with the other given coordinate. -- Mountains, trees and other objects can be positioned between the two 3D points, preventing visibilty in a straight continuous line. - -- The method @{#COORDINATE.IsLOS}() returns if the two coodinates have LOS. - -- + -- The method @{#COORDINATE.IsLOS}() returns if the two coordinates have LOS. + -- -- ## 4.7) Check the coordinate position. - -- + -- -- Various methods are available that allow to check if a coordinate is: - -- + -- -- * @{#COORDINATE.IsInRadius}(): in a give radius. -- * @{#COORDINATE.IsInSphere}(): is in a given sphere. -- * @{#COORDINATE.IsAtCoordinate2D}(): is in a given coordinate within a specific precision. - -- - -- + -- + -- -- -- # 5) Measure the simulation environment at the coordinate. - -- + -- -- ## 5.1) Weather specific. - -- + -- -- Within the DCS simulator, a coordinate has specific environmental properties, like wind, temperature, humidity etc. - -- + -- -- * @{#COORDINATE.GetWind}(): Retrieve the wind at the specific coordinate within the DCS simulator. -- * @{#COORDINATE.GetTemperature}(): Retrieve the temperature at the specific height within the DCS simulator. -- * @{#COORDINATE.GetPressure}(): Retrieve the pressure at the specific height within the DCS simulator. - -- + -- -- ## 5.2) Surface specific. - -- + -- -- Within the DCS simulator, the surface can have various objects placed at the coordinate, and the surface height will vary. - -- + -- -- * @{#COORDINATE.GetLandHeight}(): Retrieve the height of the surface (on the ground) within the DCS simulator. -- * @{#COORDINATE.GetSurfaceType}(): Retrieve the surface type (on the ground) within the DCS simulator. -- @@ -22735,13 +28996,13 @@ do -- COORDINATE -- Route points can be used in the Route methods of the @{Wrapper.Group#GROUP} class. -- -- ## 7) Manage the roads. - -- + -- -- Important for ground vehicle transportation and movement, the method @{#COORDINATE.GetClosestPointToRoad}() will calculate -- the closest point on the nearest road. - -- + -- -- In order to use the most optimal road system to transport vehicles, the method @{#COORDINATE.GetPathOnRoad}() will calculate -- the most optimal path following the road between two coordinates. - -- + -- -- ## 8) Metric or imperial system -- -- * @{#COORDINATE.IsMetric}(): Returns if the 3D point is Metric or Nautical Miles. @@ -22749,12 +29010,17 @@ do -- COORDINATE -- -- -- ## 9) Coordinate text generation - -- + -- -- * @{#COORDINATE.ToStringBR}(): Generates a Bearing & Range text in the format of DDD for DI where DDD is degrees and DI is distance. - -- * @{#COORDINATE.ToStringLL}(): Generates a Latutude & Longutude text. + -- * @{#COORDINATE.ToStringBRA}(): Generates a Bearing, Range & Altitude text. + -- * @{#COORDINATE.ToStringBRAANATO}(): Generates a Generates a Bearing, Range, Aspect & Altitude text in NATOPS. + -- * @{#COORDINATE.ToStringLL}(): Generates a Latutide & Longitude text. + -- * @{#COORDINATE.ToStringLLDMS}(): Generates a Lat, Lon, Degree, Minute, Second text. + -- * @{#COORDINATE.ToStringLLDDM}(): Generates a Lat, Lon, Degree, decimal Minute text. + -- * @{#COORDINATE.ToStringMGRS}(): Generates a MGRS grid coordinate text. -- -- ## 10) Drawings on F10 map - -- + -- -- * @{#COORDINATE.CircleToAll}(): Draw a circle on the F10 map. -- * @{#COORDINATE.LineToAll}(): Draw a line on the F10 map. -- * @{#COORDINATE.RectToAll}(): Draw a rectangle on the F10 map. @@ -22767,81 +29033,104 @@ do -- COORDINATE ClassName = "COORDINATE", } - --- @field COORDINATE.WaypointAltType + --- Waypoint altitude types. + -- @type COORDINATE.WaypointAltType + -- @field #string BARO Barometric altitude. + -- @field #string RADIO Radio altitude. COORDINATE.WaypointAltType = { BARO = "BARO", RADIO = "RADIO", } - - --- @field COORDINATE.WaypointAction + + --- Waypoint actions. + -- @type COORDINATE.WaypointAction + -- @field #string TurningPoint Turning point. + -- @field #string FlyoverPoint Fly over point. + -- @field #string FromParkingArea From parking area. + -- @field #string FromParkingAreaHot From parking area hot. + -- @field #string FromGroundAreaHot From ground area hot. + -- @field #string FromGroundArea From ground area. + -- @field #string FromRunway From runway. + -- @field #string Landing Landing. + -- @field #string LandingReFuAr Landing and refuel and rearm. COORDINATE.WaypointAction = { TurningPoint = "Turning Point", FlyoverPoint = "Fly Over Point", FromParkingArea = "From Parking Area", FromParkingAreaHot = "From Parking Area Hot", + FromGroundAreaHot = "From Ground Area Hot", + FromGroundArea = "From Ground Area", FromRunway = "From Runway", Landing = "Landing", LandingReFuAr = "LandingReFuAr", } - --- @field COORDINATE.WaypointType + --- Waypoint types. + -- @type COORDINATE.WaypointType + -- @field #string TakeOffParking Take of parking. + -- @field #string TakeOffParkingHot Take of parking hot. + -- @field #string TakeOff Take off parking hot. + -- @field #string TakeOffGroundHot Take of from ground hot. + -- @field #string TurningPoint Turning point. + -- @field #string Land Landing point. + -- @field #string LandingReFuAr Landing and refuel and rearm. COORDINATE.WaypointType = { TakeOffParking = "TakeOffParking", TakeOffParkingHot = "TakeOffParkingHot", TakeOff = "TakeOffParkingHot", + TakeOffGroundHot = "TakeOffGroundHot", + TakeOffGround = "TakeOffGround", TurningPoint = "Turning Point", Land = "Land", - LandingReFuAr = "LandingReFuAr", + LandingReFuAr = "LandingReFuAr", } --- COORDINATE constructor. -- @param #COORDINATE self -- @param DCS#Distance x The x coordinate of the Vec3 point, pointing to the North. - -- @param DCS#Distance y The y coordinate of the Vec3 point, pointing to the Right. - -- @param DCS#Distance z The z coordinate of the Vec3 point, pointing to the Right. - -- @return #COORDINATE - function COORDINATE:New( x, y, z ) + -- @param DCS#Distance y The y coordinate of the Vec3 point, pointing to up. + -- @param DCS#Distance z The z coordinate of the Vec3 point, pointing to the right. + -- @return #COORDINATE self + function COORDINATE:New( x, y, z ) - --env.info("FF COORDINATE New") - local self = BASE:Inherit( self, BASE:New() ) -- #COORDINATE + local self=BASE:Inherit(self, BASE:New()) -- #COORDINATE + self.x = x self.y = y self.z = z - + return self end --- COORDINATE constructor. -- @param #COORDINATE self -- @param #COORDINATE Coordinate. - -- @return #COORDINATE - function COORDINATE:NewFromCoordinate( Coordinate ) + -- @return #COORDINATE self + function COORDINATE:NewFromCoordinate( Coordinate ) local self = BASE:Inherit( self, BASE:New() ) -- #COORDINATE self.x = Coordinate.x self.y = Coordinate.y self.z = Coordinate.z - + return self end --- Create a new COORDINATE object from Vec2 coordinates. -- @param #COORDINATE self -- @param DCS#Vec2 Vec2 The Vec2 point. - -- @param DCS#Distance LandHeightAdd (optional) The default height if required to be evaluated will be the land height of the x, y coordinate. You can specify an extra height to be added to the land height. - -- @return #COORDINATE - function COORDINATE:NewFromVec2( Vec2, LandHeightAdd ) + -- @param DCS#Distance LandHeightAdd (Optional) The default height if required to be evaluated will be the land height of the x, y coordinate. You can specify an extra height to be added to the land height. + -- @return #COORDINATE self + function COORDINATE:NewFromVec2( Vec2, LandHeightAdd ) local LandHeight = land.getHeight( Vec2 ) - + LandHeightAdd = LandHeightAdd or 0 LandHeight = LandHeight + LandHeightAdd local self = self:New( Vec2.x, LandHeight, Vec2.y ) -- #COORDINATE - self:F2( self ) - return self end @@ -22849,8 +29138,8 @@ do -- COORDINATE --- Create a new COORDINATE object from Vec3 coordinates. -- @param #COORDINATE self -- @param DCS#Vec3 Vec3 The Vec3 point. - -- @return #COORDINATE - function COORDINATE:NewFromVec3( Vec3 ) + -- @return #COORDINATE self + function COORDINATE:NewFromVec3( Vec3 ) local self = self:New( Vec3.x, Vec3.y, Vec3.z ) -- #COORDINATE @@ -22859,6 +29148,28 @@ do -- COORDINATE return self end + --- Create a new COORDINATE object from a waypoint. This uses the components + -- + -- * `waypoint.x` + -- * `waypoint.alt` + -- * `waypoint.y` + -- + -- @param #COORDINATE self + -- @param DCS#Waypoint Waypoint The waypoint. + -- @return #COORDINATE self + function COORDINATE:NewFromWaypoint(Waypoint) + + local self=self:New(Waypoint.x, Waypoint.alt, Waypoint.y) -- #COORDINATE + + return self + end + + --- Return the coordinates itself. Sounds stupid but can be useful for compatibility. + -- @param #COORDINATE self + -- @return #COORDINATE self + function COORDINATE:GetCoordinate() + return self + end --- Return the coordinates of the COORDINATE in Vec3 format. -- @param #COORDINATE self @@ -22880,39 +29191,75 @@ do -- COORDINATE -- @param DCS#Vec3 Vec3 The 3D vector with x,y,z components. -- @return #COORDINATE The modified COORDINATE itself. function COORDINATE:UpdateFromVec3(Vec3) - + self.x=Vec3.x self.y=Vec3.y self.z=Vec3.z - + return self end - + --- Update x,y,z coordinates from another given COORDINATE. -- @param #COORDINATE self -- @param #COORDINATE Coordinate The coordinate with the new x,y,z positions. -- @return #COORDINATE The modified COORDINATE itself. function COORDINATE:UpdateFromCoordinate(Coordinate) - + self.x=Coordinate.x self.y=Coordinate.y self.z=Coordinate.z - + return self - end + end --- Update x and z coordinates from a given 2D vector. -- @param #COORDINATE self -- @param DCS#Vec2 Vec2 The 2D vector with x,y components. x is overwriting COORDINATE.x while y is overwriting COORDINATE.z. -- @return #COORDINATE The modified COORDINATE itself. function COORDINATE:UpdateFromVec2(Vec2) - + self.x=Vec2.x self.z=Vec2.y - + return self end + + --- Returns the magnetic declination at the given coordinate. + -- NOTE that this needs `require` to be available so you need to desanitize the `MissionScripting.lua` file in your DCS/Scrips folder. + -- If `require` is not available, a constant value for the whole map. + -- @param #COORDINATE self + -- @param #number Month (Optional) The month at which the declination is calculated. Default is the mission month. + -- @param #number Year (Optional) The year at which the declination is calculated. Default is the mission year. + -- @return #number Magnetic declination in degrees. + function COORDINATE:GetMagneticDeclination(Month, Year) + + local decl=UTILS.GetMagneticDeclination() + + if require then + + local magvar = require('magvar') + + if magvar then + + local date, year, month, day=UTILS.GetDCSMissionDate() + + magvar.init(Month or month, Year or year) + + local lat, lon=self:GetLLDDM() + + decl=magvar.get_mag_decl(lat, lon) + + if decl then + decl=math.deg(decl) + end + + end + else + self:T("The require package is not available. Using constant value for magnetic declination") + end + return decl + end --- Returns the coordinate from the latitude and longitude given in decimal degrees. -- @param #COORDINATE self @@ -22921,13 +29268,13 @@ do -- COORDINATE -- @param #number altitude (Optional) Altitude in meters. Default is the land height at the coordinate. -- @return #COORDINATE function COORDINATE:NewFromLLDD( latitude, longitude, altitude) - + -- Returns a point from latitude and longitude in the vec3 format. local vec3=coord.LLtoLO(latitude, longitude) - + -- Convert vec3 to coordinate object. local _coord=self:NewFromVec3(vec3) - + -- Adjust height if altitude==nil then _coord.y=self:GetLandHeight() @@ -22938,23 +29285,23 @@ do -- COORDINATE return _coord end - + --- Returns if the 2 coordinates are at the same 2D position. -- @param #COORDINATE self -- @param #COORDINATE Coordinate -- @param #number Precision -- @return #boolean true if at the same position. function COORDINATE:IsAtCoordinate2D( Coordinate, Precision ) - + self:F( { Coordinate = Coordinate:GetVec2() } ) self:F( { self = self:GetVec2() } ) - + local x = Coordinate.x local z = Coordinate.z - - return x - Precision <= self.x and x + Precision >= self.x and z - Precision <= self.z and z + Precision >= self.z + + return x - Precision <= self.x and x + Precision >= self.x and z - Precision <= self.z and z + Precision >= self.z end - + --- Scan/find objects (units, statics, scenery) within a certain radius around the coordinate using the world.searchObjects() DCS API function. -- @param #COORDINATE self -- @param #number radius (Optional) Scan radius in meters. Default 100 m. @@ -22964,7 +29311,7 @@ do -- COORDINATE -- @return #boolean True if units were found. -- @return #boolean True if statics were found. -- @return #boolean True if scenery objects were found. - -- @return #table Table of MOOSE @[#Wrapper.Unit#UNIT} objects found. + -- @return #table Table of MOOSE @{Wrapper.Unit#UNIT} objects found. -- @return #table Table of DCS static objects found. -- @return #table Table of DCS scenery objects found. function COORDINATE:ScanObjects(radius, scanunits, scanstatics, scanscenery) @@ -22989,7 +29336,7 @@ do -- COORDINATE if scanscenery==nil then scanscenery=false end - + --{Object.Category.UNIT, Object.Category.STATIC, Object.Category.SCENERY} local scanobjects={} if scanunits then @@ -23001,7 +29348,7 @@ do -- COORDINATE if scanscenery then table.insert(scanobjects, Object.Category.SCENERY) end - + -- Found stuff. local Units = {} local Statics = {} @@ -23009,40 +29356,40 @@ do -- COORDINATE local gotstatics=false local gotunits=false local gotscenery=false - + local function EvaluateZone(ZoneObject) - + BASE:T({ZoneObject}) if ZoneObject then - + -- Get category of scanned object. local ObjectCategory = ZoneObject:getCategory() - + -- Check for unit or static objects if ObjectCategory==Object.Category.UNIT and ZoneObject:isExist() then - + table.insert(Units, UNIT:Find(ZoneObject)) gotunits=true - + elseif ObjectCategory==Object.Category.STATIC and ZoneObject:isExist() then - + table.insert(Statics, ZoneObject) gotstatics=true - + elseif ObjectCategory==Object.Category.SCENERY then - + table.insert(Scenery, ZoneObject) gotscenery=true - + end - + end - + return true end - + -- Search the world. world.searchObjects(scanobjects, SphereSearch, EvaluateZone) - + for _,unit in pairs(Units) do self:T(string.format("Scan found unit %s", unit:GetName())) end @@ -23052,37 +29399,37 @@ do -- COORDINATE end for _,scenery in pairs(Scenery) do self:T(string.format("Scan found scenery %s typename=%s", scenery:getName(), scenery:getTypeName())) - SCENERY:Register(scenery:getName(), scenery) + --SCENERY:Register(scenery:getName(), scenery) end - + return gotunits, gotstatics, gotscenery, Units, Statics, Scenery end - + --- Scan/find UNITS within a certain radius around the coordinate using the world.searchObjects() DCS API function. -- @param #COORDINATE self -- @param #number radius (Optional) Scan radius in meters. Default 100 m. -- @return Core.Set#SET_UNIT Set of units. function COORDINATE:ScanUnits(radius) - + local _,_,_,units=self:ScanObjects(radius, true, false, false) - + local set=SET_UNIT:New() - + for _,unit in pairs(units) do set:AddUnit(unit) end - + return set end - - --- Find the closest unit to the COORDINATE within a certain radius. + + --- Find the closest unit to the COORDINATE within a certain radius. -- @param #COORDINATE self -- @param #number radius Scan radius in meters. Default 100 m. -- @return Wrapper.Unit#UNIT The closest unit or #nil if no unit is inside the given radius. function COORDINATE:FindClosestUnit(radius) - + local units=self:ScanUnits(radius) - + local umin=nil --Wrapper.Unit#UNIT local dmin=math.huge for _,_unit in pairs(units.Set) do @@ -23093,12 +29440,56 @@ do -- COORDINATE dmin=d umin=unit end - end - + end + return umin - end - - + end + + --- Scan/find SCENERY objects within a certain radius around the coordinate using the world.searchObjects() DCS API function. + -- @param #COORDINATE self + -- @param #number radius (Optional) Scan radius in meters. Default 100 m. + -- @return table Table of SCENERY objects. + function COORDINATE:ScanScenery(radius) + + local _,_,_,_,_,scenerys=self:ScanObjects(radius, false, false, true) + + local set={} + + for _,_scenery in pairs(scenerys) do + local scenery=_scenery --DCS#Object + + local name=scenery:getName() + local s=SCENERY:Register(name, scenery) + table.insert(set, s) + + end + + return set + end + + --- Find the closest scenery to the COORDINATE within a certain radius. + -- @param #COORDINATE self + -- @param #number radius Scan radius in meters. Default 100 m. + -- @return Wrapper.Scenery#SCENERY The closest scenery or #nil if no object is inside the given radius. + function COORDINATE:FindClosestScenery(radius) + + local sceneries=self:ScanScenery(radius) + + local umin=nil --Wrapper.Scenery#SCENERY + local dmin=math.huge + for _,_scenery in pairs(sceneries) do + local scenery=_scenery --Wrapper.Scenery#SCENERY + local coordinate=scenery:GetCoordinate() + local d=self:Get2DDistance(coordinate) + if d1 then + local norm=UTILS.VecNorm(vec) + f=Fraction/norm + end + -- Scale the vector. vec.x=f*vec.x vec.y=f*vec.y vec.z=f*vec.z - + -- 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 @@ -23346,15 +29757,12 @@ 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 d=UTILS.VecNorm(a) - - return d - + local norm=UTILS.VecNorm(a) + return norm end - + --- Returns the temperature in Degrees Celsius. -- @param #COORDINATE self -- @param height (Optional) parameter specifying the height ASL. @@ -23369,36 +29777,36 @@ do -- COORDINATE return T-273.15 end - --- Returns a text of the temperature according the measurement system @{Settings}. + --- Returns a text of the temperature according the measurement system @{Core.Settings}. -- 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) - -- - -- A text containing a pressure will look like this: - -- - -- - `Temperature: %n.d °C` + -- - For American aircraft we link to the imperial system - Degrees Fahrenheit (°F) + -- + -- A text containing a pressure will look like this: + -- + -- - `Temperature: %n.d °C` -- - `Temperature: %n.d °F` - -- + -- -- @param #COORDINATE self -- @param height (Optional) parameter specifying the height ASL. - -- @return #string Temperature according the measurement system @{Settings}. + -- @return #string Temperature according the measurement system @{Core.Settings}. function COORDINATE:GetTemperatureText( height, Settings ) - + local DegreesCelcius = self:GetTemperature( height ) - + local Settings = Settings or _SETTINGS if DegreesCelcius then 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" end - + return nil end @@ -23414,27 +29822,27 @@ do -- COORDINATE -- Return Pressure in hPa. return P/100 end - - --- Returns a text of the pressure according the measurement system @{Settings}. + + --- Returns a text of the pressure according the measurement system @{Core.Settings}. -- 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 - -- - -- A text containing a pressure will look like this: - -- - -- - `QFE: x hPa (y mmHg)` + -- - For American and European aircraft we link to the imperial system - hPa and inHg + -- + -- A text containing a pressure will look like this: + -- + -- - `QFE: x hPa (y mmHg)` -- - `QFE: x hPa (y inHg)` - -- + -- -- @param #COORDINATE self -- @param height (Optional) parameter specifying the height ASL. E.g. set height=0 for QNH. - -- @return #string Pressure in hPa and mmHg or inHg depending on the measurement system @{Settings}. + -- @return #string Pressure in hPa and mmHg or inHg depending on the measurement system @{Core.Settings}. function COORDINATE:GetPressureText( height, Settings ) local Pressure_hPa = self:GetPressure( height ) local Pressure_mmHg = Pressure_hPa * 0.7500615613030 local Pressure_inHg = Pressure_hPa * 0.0295299830714 - + local Settings = Settings or _SETTINGS if Pressure_hPa then @@ -23446,14 +29854,14 @@ do -- COORDINATE else return " no pressure" end - + return nil end - + --- Returns the heading from this to another coordinate. -- @param #COORDINATE self -- @param #COORDINATE ToCoordinate - -- @return #number Heading in degrees. + -- @return #number Heading in degrees. function COORDINATE:HeadingTo(ToCoordinate) local dz=ToCoordinate.z-self.z local dx=ToCoordinate.x-self.x @@ -23463,7 +29871,7 @@ do -- COORDINATE end return heading end - + --- Returns the wind direction (from) and strength. -- @param #COORDINATE self -- @param height (Optional) parameter specifying the height ASL. The minimum height will be always be the land height since the wind is zero below the ground. @@ -23473,12 +29881,12 @@ do -- COORDINATE local landheight=self:GetLandHeight()+0.1 -- we at 0.1 meters to be sure to be above ground since wind is zero below ground level. local point={x=self.x, y=math.max(height or self.y, landheight), z=self.z} -- get wind velocity vector - local wind = atmosphere.getWind(point) + local wind = atmosphere.getWind(point) local direction = math.deg(math.atan2(wind.z, wind.x)) if direction < 0 then direction = 360 + direction end - -- Convert to direction to from direction + -- Convert to direction to from direction if direction > 180 then direction = direction-180 else @@ -23488,40 +29896,40 @@ do -- COORDINATE -- Return wind direction and strength km/h. return direction, strength end - + --- Returns the wind direction (from) and strength. -- @param #COORDINATE self -- @param height (Optional) parameter specifying the height ASL. The minimum height will be always be the land height since the wind is zero below the ground. -- @return Direction the wind is blowing from in degrees. function COORDINATE:GetWindWithTurbulenceVec3(height) - - -- AGL height if + + -- AGL height if local landheight=self:GetLandHeight()+0.1 -- we at 0.1 meters to be sure to be above ground since wind is zero below ground level. - - -- Point at which the wind is evaluated. + + -- Point at which the wind is evaluated. local point={x=self.x, y=math.max(height or self.y, landheight), z=self.z} - + -- Get wind velocity vector including turbulences. local vec3 = atmosphere.getWindWithTurbulence(point) - + return vec3 - end + end - --- Returns a text documenting the wind direction (from) and strength according the measurement system @{Settings}. + --- Returns a text documenting the wind direction (from) and strength according the measurement system @{Core.Settings}. -- 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). - -- - -- A text containing a pressure will look like this: - -- - -- - `Wind: %n ° at n.d mps` + -- - 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: + -- + -- - `Wind: %n ° at n.d mps` -- - `Wind: %n ° at n.d kps` - -- + -- -- @param #COORDINATE self -- @param height (Optional) parameter specifying the height ASL. The minimum height will be always be the land height since the wind is zero below the ground. - -- @return #string Wind direction and strength according the measurement system @{Settings}. + -- @return #string Wind direction and strength according the measurement system @{Core.Settings}. function COORDINATE:GetWindText( height, Settings ) local Direction, Strength = self:GetWind( height ) @@ -23537,7 +29945,7 @@ do -- COORDINATE else return " no wind" end - + return nil end @@ -23557,14 +29965,24 @@ do -- COORDINATE -- @param #number AngleRadians The angle in randians. -- @param #number Precision The precision. -- @param Core.Settings#SETTINGS Settings + -- @param #boolean MagVar If true, include magentic degrees -- @return #string The bearing text in degrees. - function COORDINATE:GetBearingText( AngleRadians, Precision, Settings, Language ) + function COORDINATE:GetBearingText( AngleRadians, Precision, Settings, MagVar ) local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS local AngleDegrees = UTILS.Round( UTILS.ToDegree( AngleRadians ), Precision ) - - local s = string.format( '%03d°', AngleDegrees ) + + local s = string.format( '%03d°', AngleDegrees ) + + if MagVar then + local variation = UTILS.GetMagneticDeclination() or 0 + local AngleMagnetic = AngleDegrees - variation + + if AngleMagnetic < 0 then AngleMagnetic = 360-AngleMagnetic end + + s = string.format( '%03d°M|%03d°', AngleMagnetic,AngleDegrees ) + end return s end @@ -23573,28 +29991,32 @@ 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 Language = Language or Settings.Locale or _SETTINGS.Locale or "EN" + Language = string.lower(Language) + local Precision = Precision or 0 + local DistanceText if Settings:IsMetric() then - if Language == "EN" then - DistanceText = " for " .. UTILS.Round( Distance / 1000, 2 ) .. " km" - elseif Language == "RU" then - DistanceText = " за " .. UTILS.Round( Distance / 1000, 2 ) .. " километров" + if Language == "en" then + DistanceText = " for " .. UTILS.Round( Distance / 1000, Precision ) .. " km" + elseif Language == "ru" then + DistanceText = " за " .. UTILS.Round( Distance / 1000, Precision ) .. " километров" end else - if Language == "EN" then - DistanceText = " for " .. UTILS.Round( UTILS.MetersToNM( Distance ), 2 ) .. " miles" - elseif Language == "RU" then - DistanceText = " за " .. UTILS.Round( UTILS.MetersToNM( Distance ), 2 ) .. " миль" + if Language == "en" then + DistanceText = " for " .. UTILS.Round( UTILS.MetersToNM( Distance ), Precision ) .. " miles" + elseif Language == "ru" then + DistanceText = " за " .. UTILS.Round( UTILS.MetersToNM( Distance ), Precision ) .. " миль" end end - + return DistanceText end @@ -23604,19 +30026,21 @@ do -- COORDINATE function COORDINATE:GetAltitudeText( Settings, Language ) local Altitude = self.y local Settings = Settings or _SETTINGS - local Language = Language or "EN" + local Language = Language or Settings.Locale or _SETTINGS.Locale or "EN" + + Language = string.lower(Language) if Altitude ~= 0 then if Settings:IsMetric() then - if Language == "EN" then + if Language == "en" then return " at " .. UTILS.Round( self.y, -3 ) .. " meters" - elseif Language == "RU" then + elseif Language == "ru" then return " в " .. UTILS.Round( self.y, -3 ) .. " метры" end else - if Language == "EN" then + if Language == "en" then return " at " .. UTILS.Round( UTILS.MetersToFeet( self.y ), -3 ) .. " feet" - elseif Language == "RU" then + elseif Language == "ru" then return " в " .. UTILS.Round( self.y, -3 ) .. " ноги" end end @@ -23663,14 +30087,16 @@ do -- COORDINATE -- @param #number AngleRadians The angle in randians -- @param #number Distance The distance -- @param Core.Settings#SETTINGS Settings + -- @param #string Language (Optional) Language "en" or "ru" + -- @param #boolean MagVar If true, also state angle in magnetic -- @return #string The BR Text - function COORDINATE:GetBRText( AngleRadians, Distance, Settings, Language ) + function COORDINATE:GetBRText( AngleRadians, Distance, Settings, Language, MagVar ) local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS - local BearingText = self:GetBearingText( AngleRadians, 0, Settings, Language ) - local DistanceText = self:GetDistanceText( Distance, Settings, Language ) - + local BearingText = self:GetBearingText( AngleRadians, 0, Settings, MagVar ) + local DistanceText = self:GetDistanceText( Distance, Settings, Language, 0 ) + local BRText = BearingText .. DistanceText return BRText @@ -23681,13 +30107,15 @@ do -- COORDINATE -- @param #number AngleRadians The angle in randians -- @param #number Distance The distance -- @param Core.Settings#SETTINGS Settings + -- @param #string Language (Optional) Language "en" or "ru" + -- @param #boolean MagVar If true, also state angle in magnetic -- @return #string The BRA Text - function COORDINATE:GetBRAText( AngleRadians, Distance, Settings, Language ) + function COORDINATE:GetBRAText( AngleRadians, Distance, Settings, Language, MagVar ) local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS - local BearingText = self:GetBearingText( AngleRadians, 0, Settings, Language ) - local DistanceText = self:GetDistanceText( Distance, Settings, Language ) + local BearingText = self:GetBearingText( AngleRadians, 0, Settings, MagVar ) + 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. @@ -23711,7 +30139,15 @@ do -- COORDINATE self.y=alt return self end - + + --- Set altitude to be at land height (i.e. on the ground!) + -- @param #COORDINATE self + function COORDINATE:SetAtLandheight() + local alt=self:GetLandHeight() + self.y=alt + return self + end + --- Build an air type route point. -- @param #COORDINATE self -- @param #COORDINATE.WaypointAltType AltType The altitude type. @@ -23726,44 +30162,44 @@ do -- COORDINATE -- @return #table The route point. function COORDINATE:WaypointAir( AltType, Type, Action, Speed, SpeedLocked, airbase, DCSTasks, description, timeReFuAr ) self:F2( { AltType, Type, Action, Speed, SpeedLocked } ) - + -- Set alttype or "RADIO" which is AGL. AltType=AltType or "RADIO" - + -- Speedlocked by default if SpeedLocked==nil then SpeedLocked=true end - + -- Speed or default 500 km/h. Speed=Speed or 500 - + -- Waypoint array. local RoutePoint = {} - + -- Coordinates. RoutePoint.x = self.x RoutePoint.y = self.z - + -- Altitude. RoutePoint.alt = self.y RoutePoint.alt_type = AltType - + -- Waypoint type. RoutePoint.type = Type or nil - RoutePoint.action = Action or nil - + RoutePoint.action = Action or nil + -- Speed. RoutePoint.speed = Speed/3.6 RoutePoint.speed_locked = SpeedLocked - + -- ETA. RoutePoint.ETA=0 RoutePoint.ETA_locked=false - + -- Waypoint description. RoutePoint.name=description - + -- Airbase parameters for takeoff and landing points. if airbase then local AirbaseID = airbase:GetID() @@ -23777,26 +30213,26 @@ do -- COORDINATE self:E("ERROR: Unknown airbase category in COORDINATE:WaypointAir()!") end end - - -- Time in minutes to stay at the airbase before resuming route. + + -- Time in minutes to stay at the airbase before resuming route. if Type==COORDINATE.WaypointType.LandingReFuAr then RoutePoint.timeReFuAr=timeReFuAr or 10 end - + -- Waypoint tasks. RoutePoint.task = {} RoutePoint.task.id = "ComboTask" RoutePoint.task.params = {} RoutePoint.task.params.tasks = DCSTasks or {} - + --RoutePoint.properties={} --RoutePoint.properties.addopt={} - + --RoutePoint.formation_template="" -- Debug. self:T({RoutePoint=RoutePoint}) - + -- Return waypoint. return RoutePoint end @@ -23813,7 +30249,7 @@ do -- COORDINATE return self:WaypointAir( AltType, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, Speed, true, nil, DCSTasks, description ) end - + --- Build a Waypoint Air "Fly Over Point". -- @param #COORDINATE self -- @param #COORDINATE.WaypointAltType AltType The altitude type. @@ -23822,8 +30258,8 @@ do -- COORDINATE function COORDINATE:WaypointAirFlyOverPoint( AltType, Speed ) return self:WaypointAir( AltType, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.FlyoverPoint, Speed ) end - - + + --- Build a Waypoint Air "Take Off Parking Hot". -- @param #COORDINATE self -- @param #COORDINATE.WaypointAltType AltType The altitude type. @@ -23832,7 +30268,7 @@ do -- COORDINATE function COORDINATE:WaypointAirTakeOffParkingHot( AltType, Speed ) return self:WaypointAir( AltType, COORDINATE.WaypointType.TakeOffParkingHot, COORDINATE.WaypointAction.FromParkingAreaHot, Speed ) end - + --- Build a Waypoint Air "Take Off Parking". -- @param #COORDINATE self @@ -23842,8 +30278,8 @@ do -- COORDINATE function COORDINATE:WaypointAirTakeOffParking( AltType, Speed ) return self:WaypointAir( AltType, COORDINATE.WaypointType.TakeOffParking, COORDINATE.WaypointAction.FromParkingArea, Speed ) end - - + + --- Build a Waypoint Air "Take Off Runway". -- @param #COORDINATE self -- @param #COORDINATE.WaypointAltType AltType The altitude type. @@ -23852,8 +30288,8 @@ do -- COORDINATE function COORDINATE:WaypointAirTakeOffRunway( AltType, Speed ) return self:WaypointAir( AltType, COORDINATE.WaypointType.TakeOff, COORDINATE.WaypointAction.FromRunway, Speed ) end - - + + --- Build a Waypoint Air "Landing". -- @param #COORDINATE self -- @param DCS#Speed Speed Airspeed in km/h. @@ -23862,17 +30298,17 @@ do -- COORDINATE -- @param #string description A text description of the waypoint, which will be shown on the F10 map. -- @return #table The route point. -- @usage - -- + -- -- LandingZone = ZONE:New( "LandingZone" ) -- LandingCoord = LandingZone:GetCoordinate() -- LandingWaypoint = LandingCoord:WaypointAirLanding( 60 ) -- HeliGroup:Route( { LandWaypoint }, 1 ) -- Start landing the helicopter in one second. - -- + -- function COORDINATE:WaypointAirLanding( Speed, airbase, DCSTasks, description ) return self:WaypointAir(nil, COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, Speed, false, airbase, DCSTasks, description) end - - --- Build a Waypoint Air "LandingReFuAr". Mimics the aircraft ReFueling and ReArming. + + --- Build a Waypoint Air "LandingReFuAr". Mimics the aircraft ReFueling and ReArming. -- @param #COORDINATE self -- @param DCS#Speed Speed Airspeed in km/h. -- @param Wrapper.Airbase#AIRBASE airbase The airbase for takeoff and landing points. @@ -23882,9 +30318,9 @@ do -- COORDINATE -- @return #table The route point. function COORDINATE:WaypointAirLandingReFu( Speed, airbase, timeReFuAr, DCSTasks, description ) return self:WaypointAir(nil, COORDINATE.WaypointType.LandingReFuAr, COORDINATE.WaypointAction.LandingReFuAr, Speed, false, airbase, DCSTasks, description, timeReFuAr or 10) - end - - + end + + --- Build an ground type route point. -- @param #COORDINATE self -- @param #number Speed (Optional) Speed in km/h. The default speed is 20 km/h. @@ -23895,18 +30331,18 @@ do -- COORDINATE self:F2( { Speed, Formation, DCSTasks } ) local RoutePoint = {} - + RoutePoint.x = self.x RoutePoint.y = self.z - + RoutePoint.alt = self:GetLandHeight()+1 RoutePoint.alt_type = COORDINATE.WaypointAltType.BARO - + RoutePoint.type = "Turning Point" - + RoutePoint.action = Formation or "Off Road" RoutePoint.formation_template="" - + RoutePoint.ETA=0 RoutePoint.ETA_locked=false @@ -23917,10 +30353,10 @@ do -- COORDINATE RoutePoint.task.id = "ComboTask" RoutePoint.task.params = {} RoutePoint.task.params.tasks = DCSTasks or {} - + return RoutePoint end - + --- Build route waypoint point for Naval units. -- @param #COORDINATE self -- @param #number Speed (Optional) Speed in km/h. The default speed is 20 km/h. @@ -23931,10 +30367,10 @@ do -- COORDINATE self:F2( { Speed, Depth, DCSTasks } ) local RoutePoint = {} - + RoutePoint.x = self.x RoutePoint.y = self.z - + RoutePoint.alt = Depth or self.y -- Depth is for submarines only. Ships should have alt=0. RoutePoint.alt_type = "BARO" @@ -23962,11 +30398,11 @@ do -- COORDINATE -- @param #number Coalition (Optional) Coalition of the airbase. -- @return Wrapper.Airbase#AIRBASE Closest Airbase to the given coordinate. -- @return #number Distance to the closest airbase in meters. - function COORDINATE:GetClosestAirbase2(Category, Coalition) - + function COORDINATE:GetClosestAirbase(Category, Coalition) + -- Get all airbases of the map. local airbases=AIRBASE.GetAllAirbases(Coalition) - + local closest=nil local distmin=nil -- Loop over all airbases. @@ -23976,9 +30412,9 @@ do -- COORDINATE local category=airbase:GetAirbaseCategory() if Category and Category==category or Category==nil then - -- Distance to airbase. + -- Distance to airbase. local dist=self:Get2DDistance(airbase:GetCoordinate()) - + if closest==nil then distmin=dist closest=airbase @@ -23986,46 +30422,27 @@ do -- COORDINATE if dist=2 then for i=1,#Path-1 do @@ -24204,8 +30625,8 @@ do -- COORDINATE else -- There are cases where no path on road can be found. return nil,nil,false - end - + end + return Path, Way, GotPath end @@ -24225,7 +30646,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() @@ -24265,26 +30686,31 @@ 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 - SCHEDULER:New(nil, self.Explosion, {self,ExplosionIntensity}, Delay) + self:ScheduleOnce(Delay, self.Explosion, self, ExplosionIntensity) else - trigger.action.explosion( self:GetVec3(), ExplosionIntensity ) + trigger.action.explosion(self:GetVec3(), ExplosionIntensity) end return self end --- 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 @@ -24333,85 +30759,103 @@ do -- COORDINATE --- Big smoke and fire at the coordinate. -- @param #COORDINATE self - -- @param Utilities.Utils#BIGSMOKEPRESET preset Smoke preset (0=small smoke and fire, 1=medium smoke and fire, 2=large smoke and fire, 3=huge smoke and fire, 4=small smoke, 5=medium smoke, 6=large smoke, 7=huge smoke). + -- @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 ) + 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) - end + self:BigSmokeAndFire(BIGSMOKEPRESET.HugeSmoke, density,name) + end --- Flares the point in a color. -- @param #COORDINATE self @@ -24452,9 +30896,9 @@ do -- COORDINATE self:F2( Azimuth ) self:Flare( FLARECOLOR.Red, Azimuth ) end - + do -- Markings - + --- Mark to All -- @param #COORDINATE self -- @param #string MarkText Free format text that shows the marking clarification. @@ -24540,7 +30984,7 @@ do -- COORDINATE trigger.action.markToGroup( MarkID, MarkText, self:GetVec3(), MarkGroup:GetID(), ReadOnly, text ) return MarkID end - + --- Remove a mark -- @param #COORDINATE self -- @param #number MarkID The ID of the mark to be removed. @@ -24553,18 +30997,18 @@ do -- COORDINATE function COORDINATE:RemoveMark( MarkID ) trigger.action.removeMark( MarkID ) end - + --- 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. - -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. + -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. -- @param #string Text (Optional) Text displayed when mark is added. Default none. - -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. + -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. function COORDINATE:LineToAll(Endpoint, Coalition, Color, Alpha, LineType, ReadOnly, Text) local MarkID = UTILS.GetMarkID() if ReadOnly==nil then @@ -24578,11 +31022,11 @@ do -- COORDINATE trigger.action.lineToAll(Coalition, MarkID, self:GetVec3(), vec3, Color, LineType, ReadOnly, Text or "") return MarkID end - + --- 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. @@ -24591,90 +31035,109 @@ do -- COORDINATE -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. -- @param #string Text (Optional) Text displayed when mark is added. Default none. - -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. + -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. function COORDINATE:CircleToAll(Radius, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly, Text) local MarkID = UTILS.GetMarkID() 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 - + end + end -- Markings --- 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. -- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. -- @param #number FillAlpha Transparency [0,1]. Default 0.15. - -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. + -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. -- @param #string Text (Optional) Text displayed when mark is added. Default none. - -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. + -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. function COORDINATE:RectToAll(Endpoint, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly, Text) local MarkID = UTILS.GetMarkID() 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[4]=FillAlpha or 0.15 + + 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. -- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. -- @param #number FillAlpha Transparency [0,1]. Default 0.15. - -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. + -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. -- @param #string Text (Optional) Text displayed when mark is added. Default none. - -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. + -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. function COORDINATE:QuadToAll(Coord2, Coord3, Coord4, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly, Text) local MarkID = UTILS.GetMarkID() 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. @@ -24682,27 +31145,27 @@ do -- COORDINATE -- @param #number Alpha Transparency [0,1]. Default 1. -- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. -- @param #number FillAlpha Transparency [0,1]. Default 0.15. - -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. + -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. -- @param #string Text (Optional) Text displayed when mark is added. Default none. - -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. + -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. function COORDINATE:MarkupToAllFreeForm(Coordinates, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly, Text) - + local MarkID = UTILS.GetMarkID() if ReadOnly==nil then ReadOnly=false end - + Coalition=Coalition or -1 - + Color=Color or {1,0,0} Color[4]=Alpha or 1.0 - + LineType=LineType or 1 - - FillColor=FillColor or UTILS.DeepCopy(Color) + + FillColor=FillColor or UTILS.DeepCopy(Color) FillColor[4]=FillAlpha or 0.15 - + local vecs={} vecs[1]=self:GetVec3() for i,coord in ipairs(Coordinates) do @@ -24718,7 +31181,7 @@ do -- COORDINATE elseif #vecs==5 then trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], Color, FillColor, LineType, ReadOnly, Text or "") elseif #vecs==6 then - trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], Color, FillColor, LineType, Text or "") + trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], Color, FillColor, LineType, ReadOnly, Text or "") elseif #vecs==7 then trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], vecs[7], Color, FillColor, LineType, ReadOnly, Text or "") elseif #vecs==8 then @@ -24726,16 +31189,36 @@ do -- COORDINATE elseif #vecs==9 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], 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 "") + 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 - + return MarkID - end - + end + --- Text to all. Creates a text imposed on the map at the COORDINATE. Text scales with the map. -- @param #COORDINATE self -- @param #string Text Text displayed on the F10 map. @@ -24746,22 +31229,26 @@ do -- COORDINATE -- @param #number FillAlpha Transparency [0,1]. Default 0.3. -- @param #number FontSize Font size. Default 14. -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. - -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. + -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. function COORDINATE:TextToAll(Text, Coalition, Color, Alpha, FillColor, FillAlpha, FontSize, ReadOnly) local MarkID = UTILS.GetMarkID() if ReadOnly==nil then 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 - + --- Arrow to all. Creates an arrow from the COORDINATE to the endpoint COORDINATE on the F10 map. There is no control over other dimensions of the arrow. -- @param #COORDINATE self -- @param #COORDINATE Endpoint COORDINATE where the tip of the arrow is pointing at. @@ -24773,23 +31260,29 @@ do -- COORDINATE -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. -- @param #string Text (Optional) Text displayed when mark is added. Default none. - -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. + -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. function COORDINATE:ArrowToAll(Endpoint, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly, Text) local MarkID = UTILS.GetMarkID() 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 "") + trigger.action.arrowToAll(Coalition, MarkID, vec3, self:GetVec3(), Color, FillColor, LineType, ReadOnly, Text or "") return MarkID - end + end --- Returns if a Coordinate has Line of Sight (LOS) with the ToCoordinate. -- @param #COORDINATE self @@ -24797,7 +31290,7 @@ do -- COORDINATE -- @param #number Offset Height offset in meters. Default 2 m. -- @return #boolean true If the ToCoordinate has LOS with the Coordinate, otherwise false. function COORDINATE:IsLOS( ToCoordinate, Offset ) - + Offset=Offset or 2 -- Measurement of visibility should not be from the ground, so Adding a hypotethical 2 meters to each Coordinate. @@ -24822,7 +31315,7 @@ do -- COORDINATE local InVec2 = self:GetVec2() local Vec2 = Coordinate:GetVec2() - + local InRadius = UTILS.IsInRadius( InVec2, Vec2, Radius) return InRadius @@ -24839,7 +31332,7 @@ do -- COORDINATE local InVec3 = self:GetVec3() local Vec3 = Coordinate:GetVec3() - + local InSphere = UTILS.IsInSphere( InVec3, Vec3, Radius) return InSphere @@ -24850,17 +31343,17 @@ do -- COORDINATE -- @param #number Day The day. -- @param #number Month The month. -- @param #number Year The year. - -- @param #boolean InSeconds If true, return the sun rise time in seconds. + -- @param #boolean InSeconds If true, return the sun rise time in seconds. -- @return #string Sunrise time, e.g. "05:41". function COORDINATE:GetSunriseAtDate(Day, Month, Year, InSeconds) - - -- Day of the year. + + -- Day of the year. local DayOfYear=UTILS.GetDayOfYear(Year, Month, Day) - + local Latitude, Longitude=self:GetLLDDM() - + local Tdiff=UTILS.GMTToLocalTimeDifference() - + local sunrise=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, true, Tdiff) if InSeconds then @@ -24868,20 +31361,20 @@ do -- COORDINATE else return UTILS.SecondsToClock(sunrise, true) end - + end - + --- Get sun rise time for a specific day of the year at the coordinate. -- @param #COORDINATE self -- @param #number DayOfYear The day of the year. - -- @param #boolean InSeconds If true, return the sun rise time in seconds. + -- @param #boolean InSeconds If true, return the sun rise time in seconds. -- @return #string Sunrise time, e.g. "05:41". function COORDINATE:GetSunriseAtDayOfYear(DayOfYear, InSeconds) - + local Latitude, Longitude=self:GetLLDDM() - + local Tdiff=UTILS.GMTToLocalTimeDifference() - + local sunrise=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, true, Tdiff) if InSeconds then @@ -24889,38 +31382,38 @@ do -- COORDINATE else return UTILS.SecondsToClock(sunrise, true) end - + end - + --- Get todays sun rise time. -- @param #COORDINATE self - -- @param #boolean InSeconds If true, return the sun rise time in seconds. + -- @param #boolean InSeconds If true, return the sun rise time in seconds. -- @return #string Sunrise time, e.g. "05:41". function COORDINATE:GetSunrise(InSeconds) - - -- Get current day of the year. + + -- Get current day of the year. local DayOfYear=UTILS.GetMissionDayOfYear() - + -- Lat and long at this point. local Latitude, Longitude=self:GetLLDDM() - + -- GMT time diff. local Tdiff=UTILS.GMTToLocalTimeDifference() - + -- Sunrise in seconds of the day. local sunrise=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, true, Tdiff) - + local date=UTILS.GetDCSMissionDate() - + -- Debug output. --self:I(string.format("Sun rise at lat=%.3f long=%.3f on %s (DayOfYear=%d): %s (%d sec of the day) (GMT %d)", Latitude, Longitude, date, DayOfYear, tostring(UTILS.SecondsToClock(sunrise)), sunrise, Tdiff)) - + if InSeconds then return sunrise else return UTILS.SecondsToClock(sunrise, true) end - + end --- Get minutes until the next sun rise at this coordinate. @@ -24928,103 +31421,103 @@ do -- COORDINATE -- @param OnlyToday If true, only calculate the sun rise of today. If sun has already risen, the time in negative minutes since sunrise is reported. -- @return #number Minutes to the next sunrise. function COORDINATE:GetMinutesToSunrise(OnlyToday) - + -- Seconds of today local time=UTILS.SecondsOfToday() -- Next Sunrise in seconds. local sunrise=nil - + -- Time to sunrise. local delta=nil - + if OnlyToday then - + --- -- Sunrise of today --- - + sunrise=self:GetSunrise(true) - + delta=sunrise-time - + else --- -- Sunrise of tomorrow --- - + -- Tomorrows day of the year. local DayOfYear=UTILS.GetMissionDayOfYear()+1 local Latitude, Longitude=self:GetLLDDM() - + local Tdiff=UTILS.GMTToLocalTimeDifference() - + sunrise=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, true, Tdiff) - + delta=sunrise+UTILS.SecondsToMidnight() end return delta/60 end - + --- Check if it is day, i.e. if the sun has risen about the horizon at this coordinate. -- @param #COORDINATE self -- @param #string Clock (Optional) Time in format "HH:MM:SS+D", e.g. "05:40:00+3" to check if is day at 5:40 at third day after mission start. Default is to check right now. -- @return #boolean If true, it is day. If false, it is night time. function COORDINATE:IsDay(Clock) - + if Clock then - + local Time=UTILS.ClockToSeconds(Clock) - + local clock=UTILS.Split(Clock, "+")[1] - + -- Tomorrows day of the year. local DayOfYear=UTILS.GetMissionDayOfYear(Time) local Latitude, Longitude=self:GetLLDDM() - + local Tdiff=UTILS.GMTToLocalTimeDifference() - + local sunrise=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, true, Tdiff) local sunset=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, false, Tdiff) - + local time=UTILS.ClockToSeconds(clock) - + -- Check if time is between sunrise and sunset. if time>sunrise and time<=sunset then return true else return false - end - + end + else - + -- Todays sun rise in sec. local sunrise=self:GetSunrise(true) - + -- Todays sun set in sec. local sunset=self:GetSunset(true) - + -- Seconds passed since midnight. local time=UTILS.SecondsOfToday() - + -- Check if time is between sunrise and sunset. if time>sunrise and time<=sunset then return true else return false end - - end - + + end + end - + --- Check if it is night, i.e. if the sun has set below the horizon at this coordinate. - -- @param #COORDINATE self + -- @param #COORDINATE self -- @param #string Clock (Optional) Time in format "HH:MM:SS+D", e.g. "05:40:00+3" to check if is night at 5:40 at third day after mission start. Default is to check right now. -- @return #boolean If true, it is night. If false, it is day time. function COORDINATE:IsNight(Clock) @@ -25036,17 +31529,17 @@ do -- COORDINATE -- @param #number Day The day. -- @param #number Month The month. -- @param #number Year The year. - -- @param #boolean InSeconds If true, return the sun rise time in seconds. + -- @param #boolean InSeconds If true, return the sun rise time in seconds. -- @return #string Sunset time, e.g. "20:41". function COORDINATE:GetSunsetAtDate(Day, Month, Year, InSeconds) - - -- Day of the year. + + -- Day of the year. local DayOfYear=UTILS.GetDayOfYear(Year, Month, Day) - + local Latitude, Longitude=self:GetLLDDM() - + local Tdiff=UTILS.GMTToLocalTimeDifference() - + local sunset=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, false, Tdiff) if InSeconds then @@ -25054,80 +31547,80 @@ do -- COORDINATE else return UTILS.SecondsToClock(sunset, true) end - + end --- Get todays sun set time. -- @param #COORDINATE self - -- @param #boolean InSeconds If true, return the sun set time in seconds. + -- @param #boolean InSeconds If true, return the sun set time in seconds. -- @return #string Sunrise time, e.g. "20:41". function COORDINATE:GetSunset(InSeconds) - - -- Get current day of the year. + + -- Get current day of the year. local DayOfYear=UTILS.GetMissionDayOfYear() - + -- Lat and long at this point. local Latitude, Longitude=self:GetLLDDM() - + -- GMT time diff. local Tdiff=UTILS.GMTToLocalTimeDifference() - + -- Sunrise in seconds of the day. local sunrise=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, false, Tdiff) - + local date=UTILS.GetDCSMissionDate() - + -- Debug output. --self:I(string.format("Sun set at lat=%.3f long=%.3f on %s (DayOfYear=%d): %s (%d sec of the day) (GMT %d)", Latitude, Longitude, date, DayOfYear, tostring(UTILS.SecondsToClock(sunrise)), sunrise, Tdiff)) - + if InSeconds then return sunrise else return UTILS.SecondsToClock(sunrise, true) end - + end - + --- Get minutes until the next sun set at this coordinate. -- @param #COORDINATE self -- @param OnlyToday If true, only calculate the sun set of today. If sun has already set, the time in negative minutes since sunset is reported. -- @return #number Minutes to the next sunrise. function COORDINATE:GetMinutesToSunset(OnlyToday) - + -- Seconds of today local time=UTILS.SecondsOfToday() -- Next Sunset in seconds. local sunset=nil - + -- Time to sunrise. local delta=nil - + if OnlyToday then - + --- -- Sunset of today --- - + sunset=self:GetSunset(true) - + delta=sunset-time - + else --- -- Sunset of tomorrow --- - + -- Tomorrows day of the year. local DayOfYear=UTILS.GetMissionDayOfYear()+1 local Latitude, Longitude=self:GetLLDDM() - + local Tdiff=UTILS.GMTToLocalTimeDifference() - + sunset=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, false, Tdiff) - + delta=sunset+UTILS.SecondsToMidnight() end @@ -25140,39 +31633,130 @@ do -- 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. + -- @param #boolean MagVar If true, also get angle in MagVar for BR/BRA -- @return #string The BR text. - function COORDINATE:ToStringBR( FromCoordinate, Settings ) + function COORDINATE:ToStringBR( FromCoordinate, Settings, MagVar ) local DirectionVec3 = FromCoordinate:GetDirectionVec3( self ) local AngleRadians = self:GetAngleRadians( DirectionVec3 ) local Distance = self:Get2DDistance( FromCoordinate ) - return "BR, " .. self:GetBRText( AngleRadians, Distance, Settings ) + return "BR, " .. self:GetBRText( AngleRadians, Distance, Settings, nil, MagVar ) 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. + -- @param #boolean MagVar If true, also get angle in MagVar for BR/BRA -- @return #string The BR text. - function COORDINATE:ToStringBRA( FromCoordinate, Settings, Language ) + function COORDINATE:ToStringBRA( FromCoordinate, Settings, MagVar ) local DirectionVec3 = FromCoordinate:GetDirectionVec3( self ) local AngleRadians = self:GetAngleRadians( DirectionVec3 ) local Distance = FromCoordinate:Get2DDistance( self ) local Altitude = self:GetAltitudeText() - return "BRA, " .. self:GetBRAText( AngleRadians, Distance, Settings, Language ) + return "BRA, " .. self:GetBRAText( AngleRadians, Distance, Settings, nil, MagVar ) 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. -- @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. + -- @param #boolean MagVar If true, als get angle in magnetic -- @return #string The BR text. - function COORDINATE:ToStringBULLS( Coalition, Settings ) + function COORDINATE:ToStringBULLS( Coalition, Settings, MagVar ) local BullsCoordinate = COORDINATE:NewFromVec3( coalition.getMainRefPoint( Coalition ) ) local DirectionVec3 = BullsCoordinate:GetDirectionVec3( self ) local AngleRadians = self:GetAngleRadians( DirectionVec3 ) local Distance = self:Get2DDistance( BullsCoordinate ) local Altitude = self:GetAltitudeText() - return "BULLS, " .. self:GetBRText( AngleRadians, Distance, Settings ) + return "BULLS, " .. self:GetBRText( AngleRadians, Distance, Settings, nil, MagVar ) end --- Return an aspect string from a COORDINATE to the Angle of the object. @@ -25183,7 +31767,7 @@ do -- COORDINATE local Heading = self.Heading local DirectionVec3 = self:GetDirectionVec3( TargetCoordinate ) local Angle = self:GetAngleDegrees( DirectionVec3 ) - + if Heading then local Aspect = Angle - Heading if Aspect > -135 and Aspect <= -45 then @@ -25206,7 +31790,7 @@ do -- COORDINATE -- @param #COORDINATE self -- @return #number Latitude in DDM. -- @return #number Lontitude in DDM. - function COORDINATE:GetLLDDM() + function COORDINATE:GetLLDDM() return coord.LOtoLL( self:GetVec3() ) end @@ -25214,7 +31798,7 @@ do -- COORDINATE -- @param #COORDINATE self -- @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. -- @return #string The LL DMS Text - function COORDINATE:ToStringLLDMS( Settings ) + function COORDINATE:ToStringLLDMS( Settings ) local LL_Accuracy = Settings and Settings.LL_Accuracy or _SETTINGS.LL_Accuracy local lat, lon = coord.LOtoLL( self:GetVec3() ) @@ -25236,7 +31820,7 @@ do -- COORDINATE -- @param #COORDINATE self -- @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. -- @return #string The MGRS Text - function COORDINATE:ToStringMGRS( Settings ) --R2.1 Fixes issue #424. + function COORDINATE:ToStringMGRS( Settings ) local MGRS_Accuracy = Settings and Settings.MGRS_Accuracy or _SETTINGS.MGRS_Accuracy local lat, lon = coord.LOtoLL( self:GetVec3() ) @@ -25248,42 +31832,78 @@ do -- COORDINATE -- * Uses default settings in COORDINATE. -- * Can be overridden if for a GROUP containing x clients, a menu was selected to override the default. -- @param #COORDINATE self - -- @param #COORDINATE ReferenceCoord The refrence coordinate. - -- @param #string ReferenceName The refrence name. + -- @param #COORDINATE ReferenceCoord The reference coordinate. + -- @param #string ReferenceName The reference name. -- @param Wrapper.Controllable#CONTROLLABLE Controllable -- @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. + -- @param #boolean MagVar If true also show angle in magnetic -- @return #string The coordinate Text in the configured coordinate system. - function COORDINATE:ToStringFromRP( ReferenceCoord, ReferenceName, Controllable, Settings ) - + function COORDINATE:ToStringFromRP( ReferenceCoord, ReferenceName, Controllable, Settings, MagVar ) + self:F2( { ReferenceCoord = ReferenceCoord, ReferenceName = ReferenceName } ) local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS - + local IsAir = Controllable and Controllable:IsAirPlane() or false if IsAir then local DirectionVec3 = ReferenceCoord:GetDirectionVec3( self ) local AngleRadians = self:GetAngleRadians( DirectionVec3 ) local Distance = self:Get2DDistance( ReferenceCoord ) - return "Targets are the last seen " .. self:GetBRText( AngleRadians, Distance, Settings ) .. " from " .. ReferenceName + return "Targets are the last seen " .. self:GetBRText( AngleRadians, Distance, Settings, nil, MagVar ) .. " from " .. ReferenceName else local DirectionVec3 = ReferenceCoord:GetDirectionVec3( self ) local AngleRadians = self:GetAngleRadians( DirectionVec3 ) local Distance = self:Get2DDistance( ReferenceCoord ) - return "Target are located " .. self:GetBRText( AngleRadians, Distance, Settings ) .. " from " .. ReferenceName + return "Target are located " .. self:GetBRText( AngleRadians, Distance, Settings, nil, MagVar ) .. " from " .. ReferenceName end - + return nil end + + --- Provides a coordinate string of the point, based on a coordinate format system: + -- * Uses default settings in COORDINATE. + -- * Can be overridden if for a GROUP containing x clients, a menu was selected to override the default. + -- @param #COORDINATE self + -- @param #COORDINATE ReferenceCoord The reference coordinate. + -- @param #string ReferenceName The reference name. + -- @param Wrapper.Controllable#CONTROLLABLE Controllable + -- @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. + -- @param #boolean MagVar If true also get the angle as magnetic + -- @return #string The coordinate Text in the configured coordinate system. + function COORDINATE:ToStringFromRPShort( ReferenceCoord, ReferenceName, Controllable, Settings, MagVar ) + + self:F2( { ReferenceCoord = ReferenceCoord, ReferenceName = ReferenceName } ) + local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS + + local IsAir = Controllable and Controllable:IsAirPlane() or false + + if IsAir then + local DirectionVec3 = ReferenceCoord:GetDirectionVec3( self ) + local AngleRadians = self:GetAngleRadians( DirectionVec3 ) + local Distance = self:Get2DDistance( ReferenceCoord ) + return self:GetBRText( AngleRadians, Distance, Settings, nil, MagVar ) .. " from " .. ReferenceName + else + local DirectionVec3 = ReferenceCoord:GetDirectionVec3( self ) + local AngleRadians = self:GetAngleRadians( DirectionVec3 ) + local Distance = self:Get2DDistance( ReferenceCoord ) + return self:GetBRText( AngleRadians, Distance, Settings, nil, MagVar ) .. " from " .. ReferenceName + end + + return nil + + end + --- Provides a coordinate string of the point, based on the A2G coordinate format system. -- @param #COORDINATE self -- @param Wrapper.Controllable#CONTROLLABLE Controllable -- @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. + -- @param #boolean MagVar If true, also get angle in MagVar for BR/BRA -- @return #string The coordinate Text in the configured coordinate system. - function COORDINATE:ToStringA2G( Controllable, Settings ) - + function COORDINATE:ToStringA2G( Controllable, Settings, MagVar ) + self:F2( { Controllable = Controllable and Controllable:GetName() } ) local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS @@ -25292,7 +31912,7 @@ do -- COORDINATE -- If no Controllable is given to calculate the BR from, then MGRS will be used!!! if Controllable then local Coordinate = Controllable:GetCoordinate() - return Controllable and self:ToStringBR( Coordinate, Settings ) or self:ToStringMGRS( Settings ) + return Controllable and self:ToStringBR( Coordinate, Settings, MagVar ) or self:ToStringMGRS( Settings ) else return self:ToStringMGRS( Settings ) end @@ -25316,33 +31936,34 @@ do -- COORDINATE -- @param #COORDINATE self -- @param Wrapper.Controllable#CONTROLLABLE Controllable -- @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. + -- @param #boolean MagVar If true, also get angle in MagVar for BR/BRA -- @return #string The coordinate Text in the configured coordinate system. - function COORDINATE:ToStringA2A( Controllable, Settings, Language ) -- R2.2 - + function COORDINATE:ToStringA2A( Controllable, Settings, MagVar ) + self:F2( { Controllable = Controllable and Controllable:GetName() } ) local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS - if Settings:IsA2A_BRAA() then + if Settings:IsA2A_BRAA() then if Controllable then local Coordinate = Controllable:GetCoordinate() - return self:ToStringBRA( Coordinate, Settings, Language ) + return self:ToStringBRA( Coordinate, Settings, MagVar ) else - return self:ToStringMGRS( Settings, Language ) + return self:ToStringMGRS( Settings ) end end if Settings:IsA2A_BULLS() then local Coalition = Controllable:GetCoalition() - return self:ToStringBULLS( Coalition, Settings, Language ) + return self:ToStringBULLS( Coalition, Settings, MagVar ) end if Settings:IsA2A_LL_DMS() then - return self:ToStringLLDMS( Settings, Language ) + return self:ToStringLLDMS( Settings ) end if Settings:IsA2A_LL_DDM() then - return self:ToStringLLDDM( Settings, Language ) + return self:ToStringLLDDM( Settings ) end if Settings:IsA2A_MGRS() then - return self:ToStringMGRS( Settings, Language ) + return self:ToStringMGRS( Settings ) end return nil @@ -25358,13 +31979,13 @@ do -- COORDINATE -- @param Tasking.Task#TASK Task The task for which coordinates need to be calculated. -- @return #string The coordinate Text in the configured coordinate system. function COORDINATE:ToString( Controllable, Settings, Task ) - + -- self:E( { Controllable = Controllable and Controllable:GetName() } ) local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS local ModeA2A = nil - + if Task then if Task:IsInstanceOf( TASK_A2A ) then ModeA2A = true @@ -25381,8 +32002,8 @@ do -- COORDINATE end end end - - + + if ModeA2A == nil then local IsAir = Controllable and ( Controllable:IsAirPlane() or Controllable:IsHelicopter() ) or false if IsAir then @@ -25391,14 +32012,14 @@ do -- COORDINATE ModeA2A = false end end - + if ModeA2A == true then return self:ToStringA2A( Controllable, Settings ) else return self:ToStringA2G( Controllable, Settings ) end - + return nil end @@ -25410,8 +32031,8 @@ do -- COORDINATE -- @param Wrapper.Controllable#CONTROLLABLE Controllable -- @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. -- @return #string The pressure text in the configured measurement system. - function COORDINATE:ToStringPressure( Controllable, Settings ) -- R2.3 - + function COORDINATE:ToStringPressure( Controllable, Settings ) + self:F2( { Controllable = Controllable and Controllable:GetName() } ) local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS @@ -25427,7 +32048,7 @@ do -- COORDINATE -- @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. -- @return #string The wind text in the configured measurement system. function COORDINATE:ToStringWind( Controllable, Settings ) - + self:F2( { Controllable = Controllable and Controllable:GetName() } ) local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS @@ -25440,10 +32061,10 @@ do -- COORDINATE -- * Can be overridden if for a GROUP containing x clients, a menu was selected to override the default. -- @param #COORDINATE self -- @param Wrapper.Controllable#CONTROLLABLE Controllable - -- @param Core.Settings#SETTINGS + -- @param Core.Settings#SETTINGS -- @return #string The temperature text in the configured measurement system. function COORDINATE:ToStringTemperature( Controllable, Settings ) - + self:F2( { Controllable = Controllable and Controllable:GetName() } ) local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS @@ -25459,15 +32080,15 @@ 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 -- @field #POINT_VEC3.RoutePointType RoutePointType -- @field #POINT_VEC3.RoutePointAction RoutePointAction -- @extends #COORDINATE - - + + --- Defines a 3D point in the simulator and with its methods, you can use or manipulate the point in 3D space. -- -- **Important Note:** Most of the functions in this section were taken from MIST, and reworked to OO concepts. @@ -25553,7 +32174,7 @@ do -- POINT_VEC3 local self = BASE:Inherit( self, COORDINATE:New( x, y, z ) ) -- Core.Point#POINT_VEC3 self:F2( self ) - + return self end @@ -25579,7 +32200,7 @@ do -- POINT_VEC3 local self = BASE:Inherit( self, COORDINATE:NewFromVec3( Vec3 ) ) -- Core.Point#POINT_VEC3 self:F2( self ) - + return self end @@ -25587,21 +32208,21 @@ do -- POINT_VEC3 --- Return the x coordinate of the POINT_VEC3. -- @param #POINT_VEC3 self - -- @return #number The x coodinate. + -- @return #number The x coordinate. function POINT_VEC3:GetX() return self.x end --- Return the y coordinate of the POINT_VEC3. -- @param #POINT_VEC3 self - -- @return #number The y coodinate. + -- @return #number The y coordinate. function POINT_VEC3:GetY() return self.y end --- Return the z coordinate of the POINT_VEC3. -- @param #POINT_VEC3 self - -- @return #number The z coodinate. + -- @return #number The z coordinate. function POINT_VEC3:GetZ() return self.z end @@ -25635,7 +32256,7 @@ do -- POINT_VEC3 --- Add to the x coordinate of the POINT_VEC3. -- @param #POINT_VEC3 self - -- @param #number x The x coordinate value to add to the current x coodinate. + -- @param #number x The x coordinate value to add to the current x coordinate. -- @return #POINT_VEC3 function POINT_VEC3:AddX( x ) self.x = self.x + x @@ -25644,7 +32265,7 @@ do -- POINT_VEC3 --- Add to the y coordinate of the POINT_VEC3. -- @param #POINT_VEC3 self - -- @param #number y The y coordinate value to add to the current y coodinate. + -- @param #number y The y coordinate value to add to the current y coordinate. -- @return #POINT_VEC3 function POINT_VEC3:AddY( y ) self.y = self.y + y @@ -25653,7 +32274,7 @@ do -- POINT_VEC3 --- Add to the z coordinate of the POINT_VEC3. -- @param #POINT_VEC3 self - -- @param #number z The z coordinate value to add to the current z coodinate. + -- @param #number z The z coordinate value to add to the current z coordinate. -- @return #POINT_VEC3 function POINT_VEC3:AddZ( z ) self.z = self.z +z @@ -25678,7 +32299,7 @@ do -- POINT_VEC2 -- @field DCS#Distance x The x coordinate in meters. -- @field DCS#Distance y the y coordinate in meters. -- @extends Core.Point#COORDINATE - + --- Defines a 2D point in the simulator. The height coordinate (if needed) will be the land height + an optional added height specified. -- -- ## POINT_VEC2 constructor @@ -25706,7 +32327,7 @@ do -- POINT_VEC2 POINT_VEC2 = { ClassName = "POINT_VEC2", } - + --- POINT_VEC2 constructor. @@ -25759,14 +32380,14 @@ do -- POINT_VEC2 --- Return the x coordinate of the POINT_VEC2. -- @param #POINT_VEC2 self - -- @return #number The x coodinate. + -- @return #number The x coordinate. function POINT_VEC2:GetX() return self.x end --- Return the y coordinate of the POINT_VEC2. -- @param #POINT_VEC2 self - -- @return #number The y coodinate. + -- @return #number The y coordinate. function POINT_VEC2:GetY() return self.z end @@ -25791,7 +32412,7 @@ do -- POINT_VEC2 --- Return Return the Lat(itude) coordinate of the POINT_VEC2 (ie: (parent)POINT_VEC3.x). -- @param #POINT_VEC2 self - -- @return #number The x coodinate. + -- @return #number The x coordinate. function POINT_VEC2:GetLat() return self.x end @@ -25807,7 +32428,7 @@ do -- POINT_VEC2 --- Return the Lon(gitude) coordinate of the POINT_VEC2 (ie: (parent)POINT_VEC3.z). -- @param #POINT_VEC2 self - -- @return #number The y coodinate. + -- @return #number The y coordinate. function POINT_VEC2:GetLon() return self.z end @@ -25891,8 +32512,6 @@ do -- POINT_VEC2 end end - - --- **Core** - Models a velocity or speed, which can be expressed in various formats according the settings. -- -- === @@ -25949,7 +32568,7 @@ do -- Velocity self.Velocity = VelocityMps return self end - + --- Get the velocity in Mps (meters per second). -- @param #VELOCITY self -- @return #number The velocity in meters per second. @@ -25965,12 +32584,12 @@ do -- Velocity self.Velocity = UTILS.KmphToMps( VelocityKmph ) return self end - + --- Get the velocity in Kmph (kilometers per hour). -- @param #VELOCITY self -- @return #number The velocity in kilometers per hour. function VELOCITY:GetKmph() - + return UTILS.MpsToKmph( self.Velocity ) end @@ -25982,7 +32601,7 @@ do -- Velocity self.Velocity = UTILS.MiphToMps( VelocityMiph ) return self end - + --- Get the velocity in Miph (miles per hour). -- @param #VELOCITY self -- @return #number The velocity in miles per hour. @@ -25990,8 +32609,7 @@ do -- Velocity return UTILS.MpsToMiph( self.Velocity ) end - - --- Get the velocity in text, according the player @{Settings}. + --- Get the velocity in text, according the player @{Core.Settings}. -- @param #VELOCITY self -- @param Core.Settings#SETTINGS Settings -- @return #string The velocity in text. @@ -26008,11 +32626,11 @@ do -- Velocity end end - --- Get the velocity in text, according the player or default @{Settings}. + --- Get the velocity in text, according the player or default @{Core.Settings}. -- @param #VELOCITY self -- @param Wrapper.Controllable#CONTROLLABLE Controllable -- @param Core.Settings#SETTINGS Settings - -- @return #string The velocity in text according the player or default @{Settings} + -- @return #string The velocity in text according the player or default @{Core.Settings} function VELOCITY:ToString( VelocityGroup, Settings ) -- R2.3 self:F( { Group = VelocityGroup and VelocityGroup:GetName() } ) local Settings = Settings or ( VelocityGroup and _DATABASE:GetPlayerSettings( VelocityGroup:GetPlayerName() ) ) or _SETTINGS @@ -26029,7 +32647,7 @@ do -- VELOCITY_POSITIONABLE --- # VELOCITY_POSITIONABLE class, extends @{Core.Base#BASE} -- - -- VELOCITY_POSITIONABLE monitors the speed of an @{Positionable} in the simulation, which can be expressed in various formats according the Settings. + -- @{#VELOCITY_POSITIONABLE} monitors the speed of a @{Wrapper.Positionable#POSITIONABLE} in the simulation, which can be expressed in various formats according the Settings. -- -- ## 1. VELOCITY_POSITIONABLE constructor -- @@ -26062,7 +32680,7 @@ do -- VELOCITY_POSITIONABLE -- @param #VELOCITY_POSITIONABLE self -- @return #number The velocity in kilometers per hour. function VELOCITY_POSITIONABLE:GetKmph() - + return UTILS.MpsToKmph( self.Positionable:GetVelocityMPS() or 0) end @@ -26073,9 +32691,9 @@ do -- VELOCITY_POSITIONABLE return UTILS.MpsToMiph( self.Positionable:GetVelocityMPS() or 0 ) end - --- Get the velocity in text, according the player or default @{Settings}. + --- Get the velocity in text, according the player or default @{Core.Settings}. -- @param #VELOCITY_POSITIONABLE self - -- @return #string The velocity in text according the player or default @{Settings} + -- @return #string The velocity in text according the player or default @{Core.Settings} function VELOCITY_POSITIONABLE:ToString() -- R2.3 self:F( { Group = self.Positionable and self.Positionable:GetName() } ) local Settings = Settings or ( self.Positionable and _DATABASE:GetPlayerSettings( self.Positionable:GetPlayerName() ) ) or _SETTINGS @@ -26084,20 +32702,21 @@ do -- VELOCITY_POSITIONABLE end end--- **Core** - Informs the players using messages during a simulation. --- +-- -- === --- +-- -- ## Features: --- +-- -- * A more advanced messaging system using the DCS message system. -- * Time messages. -- * Send messages based on a message type, which has a pre-defined duration that can be tweaked in SETTINGS. -- * Send message to all players. -- * Send messages to a coalition. -- * Send messages to a specific group. +-- * Send messages to a specific unit or client. -- -- === --- +-- -- @module Core.Message -- @image Core_Message.JPG @@ -26108,42 +32727,43 @@ end--- **Core** - Informs the players using messages during a simulation. --- Message System to display Messages to Clients, Coalitions or All. -- Messages are shown on the display panel for an amount of seconds, and will then disappear. -- Messages can contain a category which is indicating the category of the message. --- +-- -- ## MESSAGE construction --- +-- -- Messages are created with @{#MESSAGE.New}. Note that when the MESSAGE object is created, no message is sent yet. -- To send messages, you need to use the To functions. --- +-- -- ## Send messages to an audience --- +-- -- Messages are sent: -- --- * To a @{Client} using @{#MESSAGE.ToClient}(). +-- * To a @{Wrapper.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}(). -- * To all Players using @{#MESSAGE.ToAll}(). --- +-- -- ## Send conditionally to an audience --- +-- -- Messages can be sent conditionally to an audience (when a condition is true): --- +-- -- * To all players using @{#MESSAGE.ToAllIf}(). -- * To a coalition using @{#MESSAGE.ToCoalitionIf}(). --- +-- -- === --- +-- -- ### Author: **FlightControl** --- ### Contributions: --- +-- ### Contributions: +-- -- === --- +-- -- @field #MESSAGE MESSAGE = { - ClassName = "MESSAGE", - MessageCategory = 0, - MessageID = 0, + ClassName = "MESSAGE", + MessageCategory = 0, + MessageID = 0, } --- Message Types @@ -26153,10 +32773,9 @@ MESSAGE.Type = { Information = "Information", Briefing = "Briefing Report", Overview = "Overview Report", - Detailed = "Detailed Report" + Detailed = "Detailed Report", } - --- Creates a new MESSAGE object. Note that these MESSAGE objects are not yet displayed on the display panel. You must use the functions @{ToClient} or @{ToCoalition} or @{ToAll} to send these Messages to the respective recipients. -- @param self -- @param #string MessageText is the text of the Message. @@ -26165,212 +32784,283 @@ MESSAGE.Type = { -- @param #boolean ClearScreen (optional) Clear all previous messages if true. -- @return #MESSAGE -- @usage --- -- Create a series of new Messages. --- -- MessageAll is meant to be sent to all players, for 25 seconds, and is classified as "Score". --- -- MessageRED is meant to be sent to the RED players only, for 10 seconds, and is classified as "End of Mission", with ID "Win". --- -- MessageClient1 is meant to be sent to a Client, for 25 seconds, and is classified as "Score", with ID "Score". --- -- MessageClient1 is meant to be sent to a Client, for 25 seconds, and is classified as "Score", with ID "Score". --- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", 25, "End of Mission" ) --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", 25, "Penalty" ) --- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", 25, "Score" ) --- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", 25, "Score") +-- +-- -- Create a series of new Messages. +-- -- MessageAll is meant to be sent to all players, for 25 seconds, and is classified as "Score". +-- -- MessageRED is meant to be sent to the RED players only, for 10 seconds, and is classified as "End of Mission", with ID "Win". +-- -- MessageClient1 is meant to be sent to a Client, for 25 seconds, and is classified as "Score", with ID "Score". +-- -- MessageClient1 is meant to be sent to a Client, for 25 seconds, and is classified as "Score", with ID "Score". +-- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", 25, "End of Mission" ) +-- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", 25, "Penalty" ) +-- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", 25, "Score" ) +-- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", 25, "Score") +-- function MESSAGE:New( MessageText, MessageDuration, MessageCategory, ClearScreen ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( { MessageText, MessageDuration, MessageCategory } ) - + local self = BASE:Inherit( self, BASE:New() ) + self:F( { MessageText, MessageDuration, MessageCategory } ) self.MessageType = nil - - -- When no MessageCategory is given, we don't show it as a title... - if MessageCategory and MessageCategory ~= "" then - if MessageCategory:sub(-1) ~= "\n" then + + -- When no MessageCategory is given, we don't show it as a title... + if MessageCategory and MessageCategory ~= "" then + if MessageCategory:sub( -1 ) ~= "\n" then self.MessageCategory = MessageCategory .. ": " else - self.MessageCategory = MessageCategory:sub( 1, -2 ) .. ":\n" + self.MessageCategory = MessageCategory:sub( 1, -2 ) .. ":\n" end else self.MessageCategory = "" end - - self.ClearScreen=false - if ClearScreen~=nil then - self.ClearScreen=ClearScreen + + self.ClearScreen = false + if ClearScreen ~= nil then + self.ClearScreen = ClearScreen end - self.MessageDuration = MessageDuration or 5 - self.MessageTime = timer.getTime() - self.MessageText = MessageText:gsub("^\n","",1):gsub("\n$","",1) - - self.MessageSent = false - self.MessageGroup = false - self.MessageCoalition = false + self.MessageDuration = MessageDuration or 5 + self.MessageTime = timer.getTime() + self.MessageText = MessageText:gsub( "^\n", "", 1 ):gsub( "\n$", "", 1 ) - return self -end + self.MessageSent = false + self.MessageGroup = false + self.MessageCoalition = false + return self +end ---- Creates a new MESSAGE object of a certain type. --- Note that these MESSAGE objects are not yet displayed on the display panel. +--- Creates a new MESSAGE object of a certain type. +-- Note that these MESSAGE objects are not yet displayed on the display panel. -- You must use the functions @{ToClient} or @{ToCoalition} or @{ToAll} to send these Messages to the respective recipients. --- The message display times are automatically defined based on the timing settings in the @{Settings} menu. +-- The message display times are automatically defined based on the timing settings in the @{Core.Settings} menu. -- @param self -- @param #string MessageText is the text of the Message. -- @param #MESSAGE.Type MessageType The type of the message. -- @param #boolean ClearScreen (optional) Clear all previous messages. -- @return #MESSAGE -- @usage +-- -- MessageAll = MESSAGE:NewType( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", MESSAGE.Type.Information ) -- MessageRED = MESSAGE:NewType( "To the RED Players: You receive a penalty because you've killed one of your own units", MESSAGE.Type.Information ) -- MessageClient1 = MESSAGE:NewType( "Congratulations, you've just hit a target", MESSAGE.Type.Update ) -- MessageClient2 = MESSAGE:NewType( "Congratulations, you've just killed a target", MESSAGE.Type.Update ) +-- function MESSAGE:NewType( MessageText, MessageType, ClearScreen ) local self = BASE:Inherit( self, BASE:New() ) self:F( { MessageText } ) - + self.MessageType = MessageType - - self.ClearScreen=false - if ClearScreen~=nil then - self.ClearScreen=ClearScreen + + self.ClearScreen = false + if ClearScreen ~= nil then + self.ClearScreen = ClearScreen end self.MessageTime = timer.getTime() - self.MessageText = MessageText:gsub("^\n","",1):gsub("\n$","",1) - + self.MessageText = MessageText:gsub( "^\n", "", 1 ):gsub( "\n$", "", 1 ) + return self end - - ---- Clears all previous messages from the screen before the new message is displayed. Not that this must come before all functions starting with ToX(), e.g. ToAll(), ToGroup() etc. +--- Clears all previous messages from the screen before the new message is displayed. Not that this must come before all functions starting with ToX(), e.g. ToAll(), ToGroup() etc. -- @param #MESSAGE self -- @return #MESSAGE function MESSAGE:Clear() self:F() - self.ClearScreen=true + self.ClearScreen = true return self end - - --- Sends a MESSAGE to a Client Group. Note that the Group needs to be defined within the ME with the skillset "Client" or "Player". -- @param #MESSAGE self -- @param Wrapper.Client#CLIENT Client is the Group of the Client. --- @param Core.Settings#SETTINGS Settings Settings used to display the message. +-- @param Core.Settings#SETTINGS Settings used to display the message. -- @return #MESSAGE -- @usage --- -- Send the 2 messages created with the @{New} method to the Client Group. --- -- Note that the Message of MessageClient2 is overwriting the Message of MessageClient1. --- ClientGroup = Group.getByName( "ClientGroup" ) -- --- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ):ToClient( ClientGroup ) --- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ):ToClient( ClientGroup ) --- or --- MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ):ToClient( ClientGroup ) --- MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ):ToClient( ClientGroup ) --- or --- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ) --- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ) --- MessageClient1:ToClient( ClientGroup ) --- MessageClient2:ToClient( ClientGroup ) +-- -- Send the 2 messages created with the @{New} method to the Client Group. +-- -- Note that the Message of MessageClient2 is overwriting the Message of MessageClient1. +-- ClientGroup = Group.getByName( "ClientGroup" ) +-- +-- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ):ToClient( ClientGroup ) +-- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ):ToClient( ClientGroup ) +-- or +-- MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25 ):ToClient( ClientGroup ) +-- MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25 ):ToClient( ClientGroup ) +-- or +-- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25 ) +-- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25 ) +-- MessageClient1:ToClient( ClientGroup ) +-- MessageClient2:ToClient( ClientGroup ) +-- function MESSAGE:ToClient( Client, Settings ) - self:F( Client ) + self:F( Client ) - if Client and Client:GetClientGroupID() then + if Client and Client:GetClientGroupID() then if self.MessageType then local Settings = Settings or ( Client and _DATABASE:GetPlayerSettings( Client:GetPlayerName() ) ) or _SETTINGS -- Core.Settings#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) - end - end - - return self + 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.outTextForUnit( Unit:GetID(), self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration , self.ClearScreen) + end + end + + return self end ---- Sends a MESSAGE to a Group. +--- Sends a MESSAGE to a Group. -- @param #MESSAGE self -- @param Wrapper.Group#GROUP Group to which the message is displayed. +-- @param Core.Settings#Settings Settings (Optional) Settings for message display. -- @return #MESSAGE Message object. function MESSAGE:ToGroup( Group, Settings ) self:F( Group.GroupName ) if Group then + + if self.MessageType then + local Settings = Settings or (Group and _DATABASE:GetPlayerSettings( Group: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.outTextForGroup( Group:GetID(), self.MessageCategory .. self.MessageText:gsub( "\n$", "" ):gsub( "\n$", "" ), self.MessageDuration, self.ClearScreen ) + end + end + + return self +end + +--- Sends a MESSAGE to a Unit. +-- @param #MESSAGE self +-- @param Wrapper.Unit#UNIT Unit to which the message is displayed. +-- @param Core.Settings#Settings Settings (Optional) Settings for message display. +-- @return #MESSAGE Message object. +function MESSAGE:ToUnit( Unit, Settings ) + self:F( Unit.IdentifiableName ) + + if Unit then if self.MessageType then - local Settings = Settings or ( Group and _DATABASE:GetPlayerSettings( Group:GetPlayerName() ) ) or _SETTINGS -- Core.Settings#SETTINGS + 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.outTextForGroup( Group:GetID(), 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 return self end + +--- Sends a MESSAGE to a Country. +-- @param #MESSAGE self +-- @param #number Country to which the message is displayed, e.g. country.id.GERMANY. For all country numbers see here: [Hoggit Wiki](https://wiki.hoggitworld.com/view/DCS_enum_country) +-- @param Core.Settings#Settings Settings (Optional) Settings for message display. +-- @return #MESSAGE Message object. +function MESSAGE:ToCountry( Country, Settings ) + self:F(Country ) + if Country then + if self.MessageType then + local Settings = Settings 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.outTextForCountry( Country, self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration, self.ClearScreen ) + end + end + return self +end + +--- Sends a MESSAGE to a Country. +-- @param #MESSAGE self +-- @param #number Country to which the message is displayed, , e.g. country.id.GERMANY. For all country numbers see here: [Hoggit Wiki](https://wiki.hoggitworld.com/view/DCS_enum_country) +-- @param #boolean Condition Sends the message only if the condition is true. +-- @param Core.Settings#Settings Settings (Optional) Settings for message display. +-- @return #MESSAGE Message object. +function MESSAGE:ToCountryIf( Country, Condition, Settings ) + self:F(Country ) + if Country and Condition == true then + self:ToCountry( Country, Settings ) + end + return self +end + --- Sends a MESSAGE to the Blue coalition. --- @param #MESSAGE self +-- @param #MESSAGE self -- @return #MESSAGE -- @usage --- -- Send a message created with the @{New} method to the BLUE coalition. --- MessageBLUE = MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToBlue() --- or --- MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToBlue() --- or --- MessageBLUE = MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) --- MessageBLUE:ToBlue() +-- +-- -- Send a message created with the @{New} method to the BLUE coalition. +-- MessageBLUE = MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25):ToBlue() +-- or +-- MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25 ):ToBlue() +-- or +-- MessageBLUE = MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25 ) +-- MessageBLUE:ToBlue() +-- function MESSAGE:ToBlue() - self:F() + self:F() - self:ToCoalition( coalition.side.BLUE ) - - return self + self:ToCoalition( coalition.side.BLUE ) + + return self end ---- Sends a MESSAGE to the Red Coalition. +--- Sends a MESSAGE to the Red Coalition. -- @param #MESSAGE self -- @return #MESSAGE -- @usage --- -- Send a message created with the @{New} method to the RED coalition. --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToRed() --- or --- MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToRed() --- or --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) --- MessageRED:ToRed() -function MESSAGE:ToRed( ) - self:F() +-- +-- -- Send a message created with the @{New} method to the RED coalition. +-- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25 ):ToRed() +-- or +-- MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25 ):ToRed() +-- or +-- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25 ) +-- MessageRED:ToRed() +-- +function MESSAGE:ToRed() + self:F() - self:ToCoalition( coalition.side.RED ) - - return self + self:ToCoalition( coalition.side.RED ) + + return self end ---- Sends a MESSAGE to a Coalition. +--- Sends a MESSAGE to a Coalition. -- @param #MESSAGE self -- @param #DCS.coalition.side CoalitionSide @{#DCS.coalition.side} to which the message is displayed. -- @param Core.Settings#SETTINGS Settings (Optional) Settings for message display. -- @return #MESSAGE Message object. -- @usage --- -- Send a message created with the @{New} method to the RED coalition. --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToCoalition( coalition.side.RED ) --- or --- MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToCoalition( coalition.side.RED ) --- or --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) --- MessageRED:ToCoalition( coalition.side.RED ) +-- +-- -- Send a message created with the @{New} method to the RED coalition. +-- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25 ):ToCoalition( coalition.side.RED ) +-- or +-- MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25 ):ToCoalition( coalition.side.RED ) +-- or +-- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25 ) +-- MessageRED:ToCoalition( coalition.side.RED ) +-- function MESSAGE:ToCoalition( CoalitionSide, Settings ) - self:F( CoalitionSide ) + self:F( CoalitionSide ) if self.MessageType then local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS @@ -26378,20 +33068,20 @@ function MESSAGE:ToCoalition( CoalitionSide, Settings ) self.MessageCategory = "" -- self.MessageType .. ": " end - if CoalitionSide then + if CoalitionSide then if self.MessageDuration ~= 0 then - self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) - trigger.action.outTextForCoalition( CoalitionSide, self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration, self.ClearScreen ) + self:T( self.MessageCategory .. self.MessageText:gsub( "\n$", "" ):gsub( "\n$", "" ) .. " / " .. self.MessageDuration ) + trigger.action.outTextForCoalition( CoalitionSide, self.MessageText:gsub( "\n$", "" ):gsub( "\n$", "" ), self.MessageDuration, self.ClearScreen ) end - end - - return self + end + + return self end ---- Sends a MESSAGE to a Coalition if the given Condition is true. +--- Sends a MESSAGE to a Coalition if the given Condition is true. -- @param #MESSAGE self -- @param CoalitionSide needs to be filled out by the defined structure of the standard scripting engine @{coalition.side}. --- @param #boolean Condition Sends the message only if the condition is true. +-- @param #boolean Condition Sends the message only if the condition is true. -- @return #MESSAGE self function MESSAGE:ToCoalitionIf( CoalitionSide, Condition ) self:F( CoalitionSide ) @@ -26399,7 +33089,7 @@ function MESSAGE:ToCoalitionIf( CoalitionSide, Condition ) if Condition and Condition == true then self:ToCoalition( CoalitionSide ) end - + return self end @@ -26408,14 +33098,16 @@ end -- @param Core.Settings#Settings Settings (Optional) Settings for message display. -- @return #MESSAGE -- @usage --- -- Send a message created to all players. --- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ):ToAll() --- or --- MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ):ToAll() --- or --- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ) --- MessageAll:ToAll() -function MESSAGE:ToAll(Settings) +-- +-- -- Send a message created to all players. +-- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25 ):ToAll() +-- or +-- MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25 ):ToAll() +-- or +-- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25 ) +-- MessageAll:ToAll() +-- +function MESSAGE:ToAll( Settings ) self:F() if self.MessageType then @@ -26425,16 +33117,16 @@ function MESSAGE:ToAll(Settings) end if self.MessageDuration ~= 0 then - self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) - trigger.action.outText( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration, self.ClearScreen ) + self:T( self.MessageCategory .. self.MessageText:gsub( "\n$", "" ):gsub( "\n$", "" ) .. " / " .. self.MessageDuration ) + trigger.action.outText( self.MessageCategory .. self.MessageText:gsub( "\n$", "" ):gsub( "\n$", "" ), self.MessageDuration, self.ClearScreen ) end return self end - --- Sends a MESSAGE to all players if the given Condition is true. -- @param #MESSAGE self +-- @param #boolean Condition -- @return #MESSAGE function MESSAGE:ToAllIf( Condition ) @@ -26442,14 +33134,35 @@ function MESSAGE:ToAllIf( Condition ) self:ToAll() end - return self + return self +end + +--- Sends a MESSAGE to DCS log file. +-- @param #MESSAGE self +-- @return #MESSAGE self +function MESSAGE:ToLog() + + env.info(self.MessageCategory .. self.MessageText:gsub( "\n$", "" ):gsub( "\n$", "" )) + + return self +end + +--- Sends a MESSAGE to DCS log file if the given Condition is true. +-- @param #MESSAGE self +-- @return #MESSAGE self +function MESSAGE:ToLogIf( Condition ) + + if Condition and Condition == true then + env.info(self.MessageCategory .. self.MessageText:gsub( "\n$", "" ):gsub( "\n$", "" )) + end + return self end --- **Core** - FSM (Finite State Machine) are objects that model and control long lasting business processes and workflow. --- +-- -- === --- +-- -- ## Features: --- +-- -- * Provide a base class to model your own state machines. -- * Trigger events synchronously. -- * Trigger events asynchronously. @@ -26459,66 +33172,65 @@ end -- - to handle controllables (groups and units). -- - to handle tasks. -- - to handle processes. --- +-- -- === --- +-- -- A Finite State Machine (FSM) models a process flow that transitions between various **States** through triggered **Events**. --- --- A FSM can only be in one of a finite number of states. --- The machine is in only one state at a time; the state it is in at any given time is called the **current state**. --- It can change from one state to another when initiated by an **__internal__ or __external__ triggering event**, which is called a **transition**. --- An **FSM implementation** is defined by **a list of its states**, **its initial state**, and **the triggering events** for **each possible transition**. --- An FSM implementation is composed out of **two parts**, a set of **state transition rules**, and an implementation set of **state transition handlers**, implementing those transitions. --- --- The FSM class supports a **hierarchical implementation of a Finite State Machine**, +-- +-- A FSM can only be in one of a finite number of states. +-- The machine is in only one state at a time; the state it is in at any given time is called the **current state**. +-- It can change from one state to another when initiated by an **__internal__ or __external__ triggering event**, which is called a **transition**. +-- A **FSM implementation** is defined by **a list of its states**, **its initial state**, and **the triggering events** for **each possible transition**. +-- A FSM implementation is composed out of **two parts**, a set of **state transition rules**, and an implementation set of **state transition handlers**, implementing those transitions. +-- +-- The FSM class supports a **hierarchical implementation of a Finite State Machine**, -- that is, it allows to **embed existing FSM implementations in a master FSM**. -- FSM hierarchies allow for efficient FSM re-use, **not having to re-invent the wheel every time again** when designing complex processes. --- +-- -- ![Workflow Example](..\Presentations\FSM\Dia2.JPG) --- +-- -- The above diagram shows a graphical representation of a FSM implementation for a **Task**, which guides a Human towards a Zone, -- orders him to destroy x targets and account the results. --- Other examples of ready made FSM could be: --- --- * route a plane to a zone flown by a human --- * detect targets by an AI and report to humans --- * account for destroyed targets by human players --- * handle AI infantry to deploy from or embark to a helicopter or airplane or vehicle --- * let an AI patrol a zone --- --- The **MOOSE framework** uses extensively the FSM class and derived FSM\_ classes, +-- Other examples of ready made FSM could be: +-- +-- * Route a plane to a zone flown by a human. +-- * Detect targets by an AI and report to humans. +-- * Account for destroyed targets by human players. +-- * Handle AI infantry to deploy from or embark to a helicopter or airplane or vehicle. +-- * Let an AI patrol a zone. +-- +-- The **MOOSE framework** extensively uses the FSM class and derived FSM\_ classes, -- because **the goal of MOOSE is to simplify mission design complexity for mission building**. -- By efficiently utilizing the FSM class and derived classes, MOOSE allows mission designers to quickly build processes. --- **Ready made FSM-based implementations classes** exist within the MOOSE framework that **can easily be re-used, +-- **Ready made FSM-based implementations classes** exist within the MOOSE framework that **can easily be re-used, -- and tailored** by mission designers through **the implementation of Transition Handlers**. -- Each of these FSM implementation classes start either with: --- --- * an acronym **AI\_**, which indicates an FSM implementation directing **AI controlled** @{GROUP} and/or @{UNIT}. These AI\_ classes derive the @{#FSM_CONTROLLABLE} class. --- * an acronym **TASK\_**, which indicates an FSM implementation executing a @{TASK} executed by Groups of players. These TASK\_ classes derive the @{#FSM_TASK} class. --- * an acronym **ACT\_**, which indicates an Sub-FSM implementation, directing **Humans actions** that need to be done in a @{TASK}, seated in a @{CLIENT} (slot) or a @{UNIT} (CA join). These ACT\_ classes derive the @{#FSM_PROCESS} class. --- +-- +-- * an acronym **AI\_**, which indicates a FSM implementation directing **AI controlled** @{Wrapper.Group#GROUP} and/or @{Wrapper.Unit#UNIT}. These AI\_ classes derive the @{#FSM_CONTROLLABLE} class. +-- * an acronym **TASK\_**, which indicates a FSM implementation executing a @{Tasking.Task#TASK} executed by Groups of players. These TASK\_ classes derive the @{#FSM_TASK} class. +-- * an acronym **ACT\_**, which indicates an Sub-FSM implementation, directing **Humans actions** that need to be done in a @{Tasking.Task#TASK}, seated in a @{Wrapper.Client#CLIENT} (slot) or a @{Wrapper.Unit#UNIT} (CA join). These ACT\_ classes derive the @{#FSM_PROCESS} class. +-- -- Detailed explanations and API specifics are further below clarified and FSM derived class specifics are described in those class documentation sections. --- --- ##__Dislaimer:__ +-- +-- ##__Disclaimer:__ -- The FSM class development is based on a finite state machine implementation made by Conroy Kyle. -- The state machine can be found on [github](https://github.com/kyleconroy/lua-state-machine) -- I've reworked this development (taken the concept), and created a **hierarchical state machine** out of it, embedded within the DCS simulator. -- Additionally, I've added extendability and created an API that allows seamless FSM implementation. --- --- The following derived classes are available in the MOOSE framework, that implement a specialised form of a FSM: --- --- * @{#FSM_TASK}: Models Finite State Machines for @{Task}s. --- * @{#FSM_PROCESS}: Models Finite State Machines for @{Task} actions, which control @{Client}s. --- * @{#FSM_CONTROLLABLE}: Models Finite State Machines for @{Wrapper.Controllable}s, which are @{Wrapper.Group}s, @{Wrapper.Unit}s, @{Client}s. --- * @{#FSM_SET}: Models Finite State Machines for @{Set}s. Note that these FSMs control multiple objects!!! So State concerns here +-- +-- The following derived classes are available in the MOOSE framework, that implement a specialized form of a FSM: +-- +-- * @{#FSM_TASK}: Models Finite State Machines for @{Tasking.Task}s. +-- * @{#FSM_PROCESS}: Models Finite State Machines for @{Tasking.Task} actions, which control @{Wrapper.Client}s. +-- * @{#FSM_CONTROLLABLE}: Models Finite State Machines for @{Wrapper.Controllable}s, which are @{Wrapper.Group}s, @{Wrapper.Unit}s, @{Wrapper.Client}s. +-- * @{#FSM_SET}: Models Finite State Machines for @{Core.Set}s. Note that these FSMs control multiple objects!!! So State concerns here -- for multiple objects or the position of the state machine in the process. --- +-- -- === --- --- +-- -- ### Author: **FlightControl** -- ### Contributions: **funkyfranky** --- +-- -- === -- -- @module Core.Fsm @@ -26534,195 +33246,194 @@ do -- FSM -- @field #table Scores Scores. -- @field #string current Current state name. -- @extends Core.Base#BASE - - + --- A Finite State Machine (FSM) models a process flow that transitions between various **States** through triggered **Events**. - -- + -- -- A FSM can only be in one of a finite number of states. - -- The machine is in only one state at a time; the state it is in at any given time is called the **current state**. - -- It can change from one state to another when initiated by an **__internal__ or __external__ triggering event**, which is called a **transition**. + -- The machine is in only one state at a time; the state it is in at any given time is called the **current state**. + -- It can change from one state to another when initiated by an **__internal__ or __external__ triggering event**, which is called a **transition**. -- An **FSM implementation** is defined by **a list of its states**, **its initial state**, and **the triggering events** for **each possible transition**. -- An FSM implementation is composed out of **two parts**, a set of **state transition rules**, and an implementation set of **state transition handlers**, implementing those transitions. - -- - -- The FSM class supports a **hierarchical implementation of a Finite State Machine**, + -- + -- The FSM class supports a **hierarchical implementation of a Finite State Machine**, -- that is, it allows to **embed existing FSM implementations in a master FSM**. -- FSM hierarchies allow for efficient FSM re-use, **not having to re-invent the wheel every time again** when designing complex processes. - -- + -- -- ![Workflow Example](..\Presentations\FSM\Dia2.JPG) - -- + -- -- The above diagram shows a graphical representation of a FSM implementation for a **Task**, which guides a Human towards a Zone, -- orders him to destroy x targets and account the results. - -- Other examples of ready made FSM could be: - -- + -- Other examples of ready made FSM could be: + -- -- * route a plane to a zone flown by a human -- * detect targets by an AI and report to humans -- * account for destroyed targets by human players - -- * handle AI infantry to deploy from or embark to a helicopter or airplane or vehicle + -- * handle AI infantry to deploy from or embark to a helicopter or airplane or vehicle -- * let an AI patrol a zone - -- - -- The **MOOSE framework** uses extensively the FSM class and derived FSM\_ classes, + -- + -- The **MOOSE framework** uses extensively the FSM class and derived FSM\_ classes, -- because **the goal of MOOSE is to simplify mission design complexity for mission building**. -- By efficiently utilizing the FSM class and derived classes, MOOSE allows mission designers to quickly build processes. - -- **Ready made FSM-based implementations classes** exist within the MOOSE framework that **can easily be re-used, + -- **Ready made FSM-based implementations classes** exist within the MOOSE framework that **can easily be re-used, -- and tailored** by mission designers through **the implementation of Transition Handlers**. -- Each of these FSM implementation classes start either with: - -- - -- * an acronym **AI\_**, which indicates an FSM implementation directing **AI controlled** @{GROUP} and/or @{UNIT}. These AI\_ classes derive the @{#FSM_CONTROLLABLE} class. - -- * an acronym **TASK\_**, which indicates an FSM implementation executing a @{TASK} executed by Groups of players. These TASK\_ classes derive the @{#FSM_TASK} class. - -- * an acronym **ACT\_**, which indicates an Sub-FSM implementation, directing **Humans actions** that need to be done in a @{TASK}, seated in a @{CLIENT} (slot) or a @{UNIT} (CA join). These ACT\_ classes derive the @{#FSM_PROCESS} class. - -- + -- + -- * an acronym **AI\_**, which indicates an FSM implementation directing **AI controlled** @{Wrapper.Group#GROUP} and/or @{Wrapper.Unit#UNIT}. These AI\_ classes derive the @{#FSM_CONTROLLABLE} class. + -- * an acronym **TASK\_**, which indicates an FSM implementation executing a @{Tasking.Task#TASK} executed by Groups of players. These TASK\_ classes derive the @{#FSM_TASK} class. + -- * an acronym **ACT\_**, which indicates an Sub-FSM implementation, directing **Humans actions** that need to be done in a @{Tasking.Task#TASK}, seated in a @{Wrapper.Client#CLIENT} (slot) or a @{Wrapper.Unit#UNIT} (CA join). These ACT\_ classes derive the @{#FSM_PROCESS} class. + -- -- ![Transition Rules and Transition Handlers and Event Triggers](..\Presentations\FSM\Dia3.JPG) - -- + -- -- The FSM class is the base class of all FSM\_ derived classes. It implements the main functionality to define and execute Finite State Machines. -- The derived FSM\_ classes extend the Finite State Machine functionality to run a workflow process for a specific purpose or component. - -- + -- -- Finite State Machines have **Transition Rules**, **Transition Handlers** and **Event Triggers**. - -- - -- The **Transition Rules** define the "Process Flow Boundaries", that is, + -- + -- The **Transition Rules** define the "Process Flow Boundaries", that is, -- the path that can be followed hopping from state to state upon triggered events. - -- If an event is triggered, and there is no valid path found for that event, + -- If an event is triggered, and there is no valid path found for that event, -- an error will be raised and the FSM will stop functioning. - -- + -- -- The **Transition Handlers** are special methods that can be defined by the mission designer, following a defined syntax. -- If the FSM object finds a method of such a handler, then the method will be called by the FSM, passing specific parameters. -- The method can then define its own custom logic to implement the FSM workflow, and to conduct other actions. - -- + -- -- The **Event Triggers** are methods that are defined by the FSM, which the mission designer can use to implement the workflow. -- Most of the time, these Event Triggers are used within the Transition Handler methods, so that a workflow is created running through the state machine. - -- + -- -- As explained above, a FSM supports **Linear State Transitions** and **Hierarchical State Transitions**, and both can be mixed to make a comprehensive FSM implementation. - -- The below documentation has a seperate chapter explaining both transition modes, taking into account the **Transition Rules**, **Transition Handlers** and **Event Triggers**. - -- + -- The below documentation has a separate chapter explaining both transition modes, taking into account the **Transition Rules**, **Transition Handlers** and **Event Triggers**. + -- -- ## FSM Linear Transitions - -- + -- -- Linear Transitions are Transition Rules allowing an FSM to transition from one or multiple possible **From** state(s) towards a **To** state upon a Triggered **Event**. - -- The Lineair transition rule evaluation will always be done from the **current state** of the FSM. + -- The Linear transition rule evaluation will always be done from the **current state** of the FSM. -- If no valid Transition Rule can be found in the FSM, the FSM will log an error and stop. - -- + -- -- ### FSM Transition Rules - -- - -- The FSM has transition rules that it follows and validates, as it walks the process. + -- + -- The FSM has transition rules that it follows and validates, as it walks the process. -- These rules define when an FSM can transition from a specific state towards an other specific state upon a triggered event. - -- - -- The method @{#FSM.AddTransition}() specifies a new possible Transition Rule for the FSM. - -- + -- + -- The method @{#FSM.AddTransition}() specifies a new possible Transition Rule for the FSM. + -- -- The initial state can be defined using the method @{#FSM.SetStartState}(). The default start state of an FSM is "None". - -- + -- -- Find below an example of a Linear Transition Rule definition for an FSM. - -- + -- -- local Fsm3Switch = FSM:New() -- #FsmDemo -- FsmSwitch:SetStartState( "Off" ) -- FsmSwitch:AddTransition( "Off", "SwitchOn", "On" ) -- FsmSwitch:AddTransition( "Off", "SwitchMiddle", "Middle" ) -- FsmSwitch:AddTransition( "On", "SwitchOff", "Off" ) -- FsmSwitch:AddTransition( "Middle", "SwitchOff", "Off" ) - -- + -- -- The above code snippet models a 3-way switch Linear Transition: - -- + -- -- * It can be switched **On** by triggering event **SwitchOn**. -- * It can be switched to the **Middle** position, by triggering event **SwitchMiddle**. -- * It can be switched **Off** by triggering event **SwitchOff**. -- * Note that once the Switch is **On** or **Middle**, it can only be switched **Off**. - -- + -- -- #### Some additional comments: - -- + -- -- Note that Linear Transition Rules **can be declared in a few variations**: - -- + -- -- * The From states can be **a table of strings**, indicating that the transition rule will be valid **if the current state** of the FSM will be **one of the given From states**. -- * The From state can be a **"*"**, indicating that **the transition rule will always be valid**, regardless of the current state of the FSM. - -- - -- The below code snippet shows how the two last lines can be rewritten and consensed. - -- + -- + -- The below code snippet shows how the two last lines can be rewritten and condensed. + -- -- FsmSwitch:AddTransition( { "On", "Middle" }, "SwitchOff", "Off" ) - -- + -- -- ### Transition Handling - -- + -- -- ![Transition Handlers](..\Presentations\FSM\Dia4.JPG) - -- - -- An FSM transitions in **4 moments** when an Event is being triggered and processed. - -- The mission designer can define for each moment specific logic within methods implementations following a defined API syntax. + -- + -- An FSM transitions in **4 moments** when an Event is being triggered and processed. + -- The mission designer can define for each moment specific logic within methods implementations following a defined API syntax. -- These methods define the flow of the FSM process; because in those methods the FSM Internal Events will be triggered. -- -- * To handle **State** transition moments, create methods starting with OnLeave or OnEnter concatenated with the State name. -- * To handle **Event** transition moments, create methods starting with OnBefore or OnAfter concatenated with the Event name. - -- + -- -- **The OnLeave and OnBefore transition methods may return false, which will cancel the transition!** - -- + -- -- Transition Handler methods need to follow the above specified naming convention, but are also passed parameters from the FSM. -- These parameters are on the correct order: From, Event, To: - -- + -- -- * From = A string containing the From state. -- * Event = A string containing the Event name that was triggered. -- * To = A string containing the To state. - -- + -- -- On top, each of these methods can have a variable amount of parameters passed. See the example in section [1.1.3](#1.1.3\)-event-triggers). - -- + -- -- ### Event Triggers - -- + -- -- ![Event Triggers](..\Presentations\FSM\Dia5.JPG) - -- - -- The FSM creates for each Event two **Event Trigger methods**. + -- + -- The FSM creates for each Event two **Event Trigger methods**. -- There are two modes how Events can be triggered, which is **synchronous** and **asynchronous**: - -- + -- -- * The method **FSM:Event()** triggers an Event that will be processed **synchronously** or **immediately**. -- * The method **FSM:__Event( __seconds__ )** triggers an Event that will be processed **asynchronously** over time, waiting __x seconds__. - -- - -- The destinction between these 2 Event Trigger methods are important to understand. An asynchronous call will "log" the Event Trigger to be executed at a later time. + -- + -- The distinction between these 2 Event Trigger methods are important to understand. An asynchronous call will "log" the Event Trigger to be executed at a later time. -- Processing will just continue. Synchronous Event Trigger methods are useful to change states of the FSM immediately, but may have a larger processing impact. - -- + -- -- The following example provides a little demonstration on the difference between synchronous and asynchronous Event Triggering. - -- + -- -- function FSM:OnAfterEvent( From, Event, To, Amount ) - -- self:T( { Amount = Amount } ) + -- self:T( { Amount = Amount } ) -- end - -- + -- -- local Amount = 1 - -- FSM:__Event( 5, Amount ) - -- + -- FSM:__Event( 5, Amount ) + -- -- Amount = Amount + 1 -- FSM:Event( Text, Amount ) - -- + -- -- In this example, the **:OnAfterEvent**() Transition Handler implementation will get called when **Event** is being triggered. - -- Before we go into more detail, let's look at the last 4 lines of the example. + -- Before we go into more detail, let's look at the last 4 lines of the example. -- The last line triggers synchronously the **Event**, and passes Amount as a parameter. - -- The 3rd last line of the example triggers asynchronously **Event**. + -- The 3rd last line of the example triggers asynchronously **Event**. -- Event will be processed after 5 seconds, and Amount is given as a parameter. - -- + -- -- The output of this little code fragment will be: - -- + -- -- * Amount = 2 -- * Amount = 2 - -- + -- -- Because ... When Event was asynchronously processed after 5 seconds, Amount was set to 2. So be careful when processing and passing values and objects in asynchronous processing! - -- + -- -- ### Linear Transition Example - -- + -- -- This example is fully implemented in the MOOSE test mission on GITHUB: [FSM-100 - Transition Explanation](https://github.com/FlightControl-Master/MOOSE/blob/master/Moose%20Test%20Missions/FSM%20-%20Finite%20State%20Machine/FSM-100%20-%20Transition%20Explanation/FSM-100%20-%20Transition%20Explanation.lua) - -- + -- -- It models a unit standing still near Batumi, and flaring every 5 seconds while switching between a Green flare and a Red flare. -- The purpose of this example is not to show how exciting flaring is, but it demonstrates how a Linear Transition FSM can be build. -- Have a look at the source code. The source code is also further explained below in this section. - -- + -- -- The example creates a new FsmDemo object from class FSM. -- It will set the start state of FsmDemo to state **Green**. -- Two Linear Transition Rules are created, where upon the event **Switch**, -- the FsmDemo will transition from state **Green** to **Red** and from **Red** back to **Green**. - -- + -- -- ![Transition Example](..\Presentations\FSM\Dia6.JPG) - -- + -- -- local FsmDemo = FSM:New() -- #FsmDemo -- FsmDemo:SetStartState( "Green" ) -- FsmDemo:AddTransition( "Green", "Switch", "Red" ) -- FsmDemo:AddTransition( "Red", "Switch", "Green" ) - -- + -- -- In the above example, the FsmDemo could flare every 5 seconds a Green or a Red flare into the air. -- The next code implements this through the event handling method **OnAfterSwitch**. - -- + -- -- ![Transition Flow](..\Presentations\FSM\Dia7.JPG) - -- + -- -- function FsmDemo:OnAfterSwitch( From, Event, To, FsmUnit ) -- self:T( { From, Event, To, FsmUnit } ) - -- + -- -- if From == "Green" then -- FsmUnit:Flare(FLARECOLOR.Green) -- else @@ -26732,22 +33443,22 @@ do -- FSM -- end -- self:__Switch( 5, FsmUnit ) -- Trigger the next Switch event to happen in 5 seconds. -- end - -- + -- -- FsmDemo:__Switch( 5, FsmUnit ) -- Trigger the first Switch event to happen in 5 seconds. - -- + -- -- The OnAfterSwitch implements a loop. The last line of the code fragment triggers the Switch Event within 5 seconds. -- Upon the event execution (after 5 seconds), the OnAfterSwitch method is called of FsmDemo (cfr. the double point notation!!! ":"). - -- The OnAfterSwitch method receives from the FSM the 3 transition parameter details ( From, Event, To ), + -- The OnAfterSwitch method receives from the FSM the 3 transition parameter details ( From, Event, To ), -- and one additional parameter that was given when the event was triggered, which is in this case the Unit that is used within OnSwitchAfter. - -- + -- -- function FsmDemo:OnAfterSwitch( From, Event, To, FsmUnit ) - -- + -- -- For debugging reasons the received parameters are traced within the DCS.log. - -- + -- -- self:T( { From, Event, To, FsmUnit } ) - -- + -- -- The method will check if the From state received is either "Green" or "Red" and will flare the respective color from the FsmUnit. - -- + -- -- if From == "Green" then -- FsmUnit:Flare(FLARECOLOR.Green) -- else @@ -26755,77 +33466,75 @@ do -- FSM -- FsmUnit:Flare(FLARECOLOR.Red) -- end -- end - -- + -- -- It is important that the Switch event is again triggered, otherwise, the FsmDemo would stop working after having the first Event being handled. - -- + -- -- FsmDemo:__Switch( 5, FsmUnit ) -- Trigger the next Switch event to happen in 5 seconds. - -- + -- -- The below code fragment extends the FsmDemo, demonstrating multiple **From states declared as a table**, adding a **Linear Transition Rule**. -- The new event **Stop** will cancel the Switching process. -- The transition for event Stop can be executed if the current state of the FSM is either "Red" or "Green". - -- + -- -- local FsmDemo = FSM:New() -- #FsmDemo -- FsmDemo:SetStartState( "Green" ) -- FsmDemo:AddTransition( "Green", "Switch", "Red" ) -- FsmDemo:AddTransition( "Red", "Switch", "Green" ) -- FsmDemo:AddTransition( { "Red", "Green" }, "Stop", "Stopped" ) - -- + -- -- The transition for event Stop can also be simplified, as any current state of the FSM is valid. - -- + -- -- FsmDemo:AddTransition( "*", "Stop", "Stopped" ) - -- + -- -- So... When FsmDemo:Stop() is being triggered, the state of FsmDemo will transition from Red or Green to Stopped. -- And there is no transition handling method defined for that transition, thus, no new event is being triggered causing the FsmDemo process flow to halt. - -- + -- -- ## FSM Hierarchical Transitions - -- + -- -- Hierarchical Transitions allow to re-use readily available and implemented FSMs. - -- This becomes in very useful for mission building, where mission designers build complex processes and workflows, + -- This becomes in very useful for mission building, where mission designers build complex processes and workflows, -- combining smaller FSMs to one single FSM. - -- - -- The FSM can embed **Sub-FSMs** that will execute and return **multiple possible Return (End) States**. + -- + -- The FSM can embed **Sub-FSMs** that will execute and return **multiple possible Return (End) States**. -- Depending upon **which state is returned**, the main FSM can continue the flow **triggering specific events**. - -- - -- The method @{#FSM.AddProcess}() adds a new Sub-FSM to the FSM. + -- + -- The method @{#FSM.AddProcess}() adds a new Sub-FSM to the FSM. -- -- === - -- + -- -- @field #FSM - -- FSM = { ClassName = "FSM", } - + --- Creates a new FSM object. -- @param #FSM self -- @return #FSM function FSM:New() - + -- Inherits from BASE self = BASE:Inherit( self, BASE:New() ) - + self.options = options or {} self.options.subs = self.options.subs or {} self.current = self.options.initial or 'none' self.Events = {} self.subs = {} self.endstates = {} - + self.Scores = {} - + self._StartState = "none" self._Transitions = {} self._Processes = {} self._EndStates = {} self._Scores = {} self._EventSchedules = {} - + self.CallScheduler = SCHEDULER:New( self ) - + return self end - - + --- Sets the start state of the FSM. -- @param #FSM self -- @param #string State A string defining the start state. @@ -26833,15 +33542,14 @@ do -- FSM self._StartState = State self.current = State end - - + --- Returns the start state of the FSM. -- @param #FSM self -- @return #string A string containing the start state. function FSM:GetStartState() return self._StartState or {} end - + --- Add a new transition rule to the FSM. -- A transition rule defines when and if the FSM can transition from a state towards another state upon a triggered event. -- @param #FSM self @@ -26849,28 +33557,27 @@ do -- FSM -- @param #string Event The Event name. -- @param #string To The To state. function FSM:AddTransition( From, Event, To ) - + local Transition = {} Transition.From = From Transition.Event = Event Transition.To = To - + -- Debug message. - self:T2( Transition ) + --self:T3( Transition ) self._Transitions[Transition] = Transition self:_eventmap( self.Events, Transition ) end - --- Returns a table of the transition rules defined within the FSM. -- @param #FSM self -- @return #table Transitions. - function FSM:GetTransitions() + function FSM:GetTransitions() return self._Transitions or {} end - - --- Set the default @{Process} template with key ProcessName providing the ProcessClass and the process object when it is assigned to a @{Wrapper.Controllable} by the task. + + --- Set the default @{#FSM_PROCESS} template with key ProcessName providing the ProcessClass and the process object when it is assigned to a @{Wrapper.Controllable} by the task. -- @param #FSM self -- @param #table From Can contain a string indicating the From state or a table of strings containing multiple From states. -- @param #string Event The Event name. @@ -26878,7 +33585,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 @@ -26886,66 +33593,64 @@ do -- FSM Sub.fsm = Process Sub.StartEvent = "Start" Sub.ReturnEvents = ReturnEvents - + self._Processes[Sub] = Sub - + self:_submap( self.subs, Sub, nil ) - + self:AddTransition( From, Event, From ) - + return Process end - - + --- Returns a table of the SubFSM rules defined within the FSM. -- @param #FSM self -- @return #table Sub processes. function FSM:GetProcesses() - + self:F( { Processes = self._Processes } ) - + return self._Processes or {} end - + function FSM:GetProcess( From, Event ) - + for ProcessID, Process in pairs( self:GetProcesses() ) do if Process.From == From and Process.Event == Event then return Process.fsm end end - + error( "Sub-Process from state " .. From .. " with event " .. Event .. " not found!" ) end - + function FSM:SetProcess( From, Event, Fsm ) - + for ProcessID, Process in pairs( self:GetProcesses() ) do if Process.From == From and Process.Event == Event then Process.fsm = Fsm return true end end - + error( "Sub-Process from state " .. From .. " with event " .. Event .. " not found!" ) end - + --- Adds an End state. -- @param #FSM self -- @param #string State The FSM state. - function FSM:AddEndState( State ) + function FSM:AddEndState( State ) self._EndStates[State] = State self.endstates[State] = State end - + --- Returns the End states. -- @param #FSM self -- @return #table End states. - function FSM:GetEndStates() + function FSM:GetEndStates() return self._EndStates or {} end - - + --- Adds a score for the FSM to be achieved. -- @param #FSM self -- @param #string State is the state of the process when the score needs to be given. (See the relevant state descriptions of the process). @@ -26954,14 +33659,14 @@ do -- FSM -- @return #FSM self function FSM:AddScore( State, ScoreText, Score ) self:F( { State, ScoreText, Score } ) - + self._Scores[State] = self._Scores[State] or {} self._Scores[State].ScoreText = ScoreText self._Scores[State].Score = Score - + return self end - + --- Adds a score for the FSM_PROCESS to be achieved. -- @param #FSM self -- @param #string From is the From State of the main process. @@ -26972,44 +33677,44 @@ do -- FSM -- @return #FSM self function FSM:AddScoreProcess( From, Event, State, ScoreText, Score ) self:F( { From, Event, State, ScoreText, Score } ) - + local Process = self:GetProcess( From, Event ) - + Process._Scores[State] = Process._Scores[State] or {} Process._Scores[State].ScoreText = ScoreText Process._Scores[State].Score = Score - self:T( Process._Scores ) + --self:T3( Process._Scores ) return Process end - + --- Returns a table with the scores defined. -- @param #FSM self -- @return #table Scores. - function FSM:GetScores() + function FSM:GetScores() return self._Scores or {} end - + --- Returns a table with the Subs defined. -- @param #FSM self -- @return #table Sub processes. - function FSM:GetSubs() + function FSM:GetSubs() return self.options.subs end - + --- Load call backs. -- @param #FSM self - -- @param #table CallBackTable Table of call backs. + -- @param #table CallBackTable Table of call backs. function FSM:LoadCallBacks( CallBackTable ) - + for name, callback in pairs( CallBackTable or {} ) do self[name] = callback end - + end - - --- Event map. + + --- Event map. -- @param #FSM self -- @param #table Events Events. -- @param #table EventStructure Event structure. @@ -27022,35 +33727,35 @@ 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 ) end - --- Sub maps. + --- Sub maps. -- @param #FSM self -- @param #table subs Subs. -- @param #table sub Sub. - -- @param #string name Name. + -- @param #string name Name. function FSM:_submap( subs, sub, name ) - + subs[sub.From] = subs[sub.From] or {} subs[sub.From][sub.Event] = subs[sub.From][sub.Event] or {} - + -- Make the reference table weak. -- setmetatable( subs[sub.From][sub.Event], { __mode = "k" } ) - + subs[sub.From][sub.Event][sub] = {} subs[sub.From][sub.Event][sub].fsm = sub.fsm subs[sub.From][sub.Event][sub].StartEvent = sub.StartEvent subs[sub.From][sub.Event][sub].ReturnEvents = sub.ReturnEvents or {} -- these events need to be given to find the correct continue event ... if none given, the processing will stop. subs[sub.From][sub.Event][sub].name = name subs[sub.From][sub.Event][sub].fsmparent = self - + end - + --- Call handler. -- @param #FSM self -- @param #string step Step "onafter", "onbefore", "onenter", "onleave". @@ -27059,12 +33764,12 @@ do -- FSM -- @param #string EventName Event name. -- @return Value. function FSM:_call_handler( step, trigger, params, EventName ) - --env.info(string.format("FF T=%.3f _call_handler step=%s, trigger=%s, event=%s", timer.getTime(), step, trigger, EventName)) + -- env.info(string.format("FF T=%.3f _call_handler step=%s, trigger=%s, event=%s", timer.getTime(), step, trigger, EventName)) local handler = step .. trigger - + if self[handler] then - + --[[ if step == "onafter" or step == "OnAfter" then self:T( ":::>" .. step .. params[2] .. " : " .. params[1] .. " >> " .. params[2] .. ">" .. step .. params[2] .. "()" .. " >> " .. params[3] ) @@ -27078,7 +33783,7 @@ do -- FSM self:T( ":::>" .. step .. " : " .. params[1] .. " >> " .. params[2] .. " >> " .. params[3] ) end ]] - + self._EventSchedules[EventName] = nil -- Error handler. @@ -27086,49 +33791,50 @@ do -- FSM env.info( "Error in SCHEDULER function:" .. errmsg ) if BASE.Debug ~= nil then env.info( BASE.Debug.traceback() ) - end + end return errmsg end - - --return self[handler](self, unpack( params )) - + + -- return self[handler](self, unpack( params )) + -- Protected call. - local Result, Value = xpcall( function() return self[handler]( self, unpack( params ) ) end, ErrorHandler ) - return Value + local Result, Value = xpcall( function() + return self[handler]( self, unpack( params ) ) + end, ErrorHandler ) + return Value end - + end - + --- Handler. -- @param #FSM self -- @param #string EventName Event name. -- @param ... Arguments. function FSM._handler( self, EventName, ... ) - + local Can, To = self:can( EventName ) - + if To == "*" then To = self.current end - + if Can then - + -- From state. local From = self.current - + -- Parameters. - local Params = { From, EventName, To, ... } + local Params = { From, EventName, To, ... } + if self["onleave" .. From] or + self["OnLeave" .. From] or + self["onbefore" .. EventName] or + self["OnBefore" .. EventName] or + self["onafter" .. EventName] or + self["OnAfter" .. EventName] or + self["onenter" .. To] or + self["OnEnter" .. To] then - if self["onleave".. From] or - self["OnLeave".. From] or - self["onbefore".. EventName] or - self["OnBefore".. EventName] or - self["onafter".. EventName] or - self["OnAfter".. EventName] or - self["onenter".. To] or - self["OnEnter".. To] then - if self:_call_handler( "onbefore", EventName, Params, EventName ) == false then self:T( "*** FSM *** Cancel" .. " *** " .. self.current .. " --> " .. EventName .. " --> " .. To .. " *** onbefore" .. EventName ) return false @@ -27137,7 +33843,7 @@ do -- FSM self:T( "*** FSM *** Cancel" .. " *** " .. self.current .. " --> " .. EventName .. " --> " .. To .. " *** OnBefore" .. EventName ) return false else - if self:_call_handler( "onleave", From, Params, EventName ) == false then + if self:_call_handler( "onleave", From, Params, EventName ) == false then self:T( "*** FSM *** Cancel" .. " *** " .. self.current .. " --> " .. EventName .. " --> " .. To .. " *** onleave" .. From ) return false else @@ -27145,154 +33851,156 @@ do -- FSM self:T( "*** FSM *** Cancel" .. " *** " .. self.current .. " --> " .. EventName .. " --> " .. To .. " *** OnLeave" .. From ) return false end - end + end end end - + else - + local ClassName = self:GetClassName() - + if ClassName == "FSM" then self:T( "*** FSM *** Transit *** " .. self.current .. " --> " .. EventName .. " --> " .. To ) end - + if ClassName == "FSM_TASK" then self:T( "*** FSM *** Transit *** " .. self.current .. " --> " .. EventName .. " --> " .. To .. " *** Task: " .. self.TaskName ) end - + if ClassName == "FSM_CONTROLLABLE" then - self:T( "*** FSM *** Transit *** " .. self.current .. " --> " .. EventName .. " --> " .. To .. " *** TaskUnit: " .. self.Controllable.ControllableName .. " *** " ) - end - + self:T( "*** FSM *** Transit *** " .. self.current .. " --> " .. EventName .. " --> " .. To .. " *** TaskUnit: " .. self.Controllable.ControllableName .. " *** " ) + end + if ClassName == "FSM_PROCESS" then - self:T( "*** FSM *** Transit *** " .. self.current .. " --> " .. EventName .. " --> " .. To .. " *** Task: " .. self.Task:GetName() .. ", TaskUnit: " .. self.Controllable.ControllableName .. " *** " ) - end + self:T( "*** FSM *** Transit *** " .. self.current .. " --> " .. EventName .. " --> " .. To .. " *** Task: " .. self.Task:GetName() .. ", TaskUnit: " .. self.Controllable.ControllableName .. " *** " ) + end end - + -- New current state. self.current = To - + local execute = true - + local subtable = self:_gosub( From, EventName ) - + for _, sub in pairs( subtable ) do - - --if sub.nextevent then + + -- if sub.nextevent then -- self:F2( "nextevent = " .. sub.nextevent ) -- self[sub.nextevent]( self ) - --end - + -- end + self:T( "*** FSM *** Sub *** " .. sub.StartEvent ) - + sub.fsm.fsmparent = self sub.fsm.ReturnEvents = sub.ReturnEvents sub.fsm[sub.StartEvent]( sub.fsm ) - + execute = false end - + local fsmparent, Event = self:_isendstate( To ) - + if fsmparent and Event then - + self:T( "*** FSM *** End *** " .. Event ) - - self:_call_handler("onenter", To, Params, EventName ) - self:_call_handler("OnEnter", To, Params, EventName ) - self:_call_handler("onafter", EventName, Params, EventName ) - self:_call_handler("OnAfter", EventName, Params, EventName ) - self:_call_handler("onstate", "change", Params, EventName ) - + + self:_call_handler( "onenter", To, Params, EventName ) + self:_call_handler( "OnEnter", To, Params, EventName ) + self:_call_handler( "onafter", EventName, Params, EventName ) + self:_call_handler( "OnAfter", EventName, Params, EventName ) + self:_call_handler( "onstate", "change", Params, EventName ) + fsmparent[Event]( fsmparent ) - + execute = false end - + if execute then - - self:_call_handler("onafter", EventName, Params, EventName ) - self:_call_handler("OnAfter", EventName, Params, EventName ) - - self:_call_handler("onenter", To, Params, EventName ) - self:_call_handler("OnEnter", To, Params, EventName ) - - self:_call_handler("onstate", "change", Params, EventName ) - + + self:_call_handler( "onafter", EventName, Params, EventName ) + self:_call_handler( "OnAfter", EventName, Params, EventName ) + + self:_call_handler( "onenter", To, Params, EventName ) + self:_call_handler( "OnEnter", To, Params, EventName ) + + self:_call_handler( "onstate", "change", Params, EventName ) + end else self:T( "*** FSM *** NO Transition *** " .. self.current .. " --> " .. EventName .. " --> ? " ) end - + return nil end --- Delayed transition. -- @param #FSM self - -- @param #string EventName Event name. + -- @param #string EventName Event name. -- @return #function Function. function FSM:_delayed_transition( EventName ) - + return function( self, DelaySeconds, ... ) - + -- Debug. - self:T2( "Delayed Event: " .. EventName ) + self:T3( "Delayed Event: " .. EventName ) local CallID = 0 if DelaySeconds ~= nil then - + if DelaySeconds < 0 then -- Only call the event ONCE! - + DelaySeconds = math.abs( DelaySeconds ) - + if not self._EventSchedules[EventName] then - + -- Call _handler. CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1, nil, nil, nil, 4, true ) - + -- Set call ID. 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 --- Create transition. -- @param #FSM self - -- @param #string EventName Event name. - -- @return #function Function. + -- @param #string EventName Event name. + -- @return #function Function. function FSM:_create_transition( EventName ) - return function( self, ... ) return self._handler( self, EventName , ... ) end + return function( self, ... ) + return self._handler( self, EventName, ... ) + end end - + --- Go sub. - -- @param #FSM self + -- @param #FSM self -- @param #string ParentFrom Parent from state. -- @param #string ParentEvent Parent event name. -- @return #table Subs. 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 {} @@ -27306,21 +34014,21 @@ do -- FSM -- @return #string Event name. function FSM:_isendstate( Current ) local FSMParent = self.fsmparent - + if FSMParent and self.endstates[Current] then - --self:T( { state = Current, endstates = self.endstates, endstate = self.endstates[Current] } ) + -- self:T( { state = Current, endstates = self.endstates, endstate = self.endstates[Current] } ) FSMParent.current = Current local ParentFrom = FSMParent.current - --self:T( { ParentFrom, self.ReturnEvents } ) + -- self:T( { ParentFrom, self.ReturnEvents } ) local Event = self.ReturnEvents[Current] - --self:T( { Event } ) + -- self:T( { Event } ) if Event then return FSMParent, Event else - --self:T( { "Could not find parent event name for state ", ParentFrom } ) + -- self:T( { "Could not find parent event name for state ", ParentFrom } ) end end - + return nil end @@ -27329,17 +34037,17 @@ do -- FSM -- @param #table Map Map. -- @param #table Event Event table. function FSM:_add_to_map( Map, Event ) - self:F3( { Map, Event } ) - - if type(Event.From) == 'string' then - Map[Event.From] = Event.To + self:F3( { Map, Event } ) + + if type( Event.From ) == 'string' then + Map[Event.From] = Event.To else - for _, From in ipairs(Event.From) do - Map[From] = Event.To + for _, From in ipairs( Event.From ) do + Map[From] = Event.To end end - self:T3( { Map, Event } ) + --self:T3( { Map, Event } ) end --- Get current state. @@ -27351,15 +34059,15 @@ do -- FSM --- Get current state. -- @param #FSM self - -- @return #string Current FSM state. + -- @return #string Current FSM state. function FSM:GetCurrentState() return self.current end - + --- Check if FSM is in state. -- @param #FSM self -- @param #string State State name. - -- @param #boolean If true, FSM is in this state. + -- @return #boolean If true, FSM is in this state. function FSM:Is( State ) return self.current == State end @@ -27367,7 +34075,7 @@ do -- FSM --- Check if FSM is in state. -- @param #FSM self -- @param #string State State name. - -- @param #boolean If true, FSM is in this state. + -- @return #boolean If true, FSM is in this state. function FSM:is(state) return self.current == state end @@ -27377,14 +34085,14 @@ do -- FSM -- @param #string e Event name. -- @return #boolean If true, FSM can do the event. -- @return #string To state. - function FSM:can(e) - + function FSM:can( e ) + local Event = self.Events[e] - - --self:F3( { self.current, Event } ) - + + -- self:F3( { self.current, Event } ) + local To = Event and Event.map[self.current] or Event.map['*'] - + return To ~= nil, To end @@ -27392,8 +34100,8 @@ do -- FSM -- @param #FSM self -- @param #string e Event name. -- @return #boolean If true, FSM cannot do the event. - function FSM:cannot(e) - return not self:can(e) + function FSM:cannot( e ) + return not self:can( e ) end end @@ -27403,32 +34111,32 @@ do -- FSM_CONTROLLABLE --- @type FSM_CONTROLLABLE -- @field Wrapper.Controllable#CONTROLLABLE Controllable -- @extends Core.Fsm#FSM - - --- Models Finite State Machines for @{Wrapper.Controllable}s, which are @{Wrapper.Group}s, @{Wrapper.Unit}s, @{Client}s. - -- + + --- Models Finite State Machines for @{Wrapper.Controllable}s, which are @{Wrapper.Group}s, @{Wrapper.Unit}s, @{Wrapper.Client}s. + -- -- === - -- + -- -- @field #FSM_CONTROLLABLE FSM_CONTROLLABLE = { ClassName = "FSM_CONTROLLABLE", } - + --- Creates a new FSM_CONTROLLABLE object. -- @param #FSM_CONTROLLABLE self -- @param #table FSMT Finite State Machine Table -- @param Wrapper.Controllable#CONTROLLABLE Controllable (optional) The CONTROLLABLE object that the FSM_CONTROLLABLE governs. -- @return #FSM_CONTROLLABLE function FSM_CONTROLLABLE:New( Controllable ) - + -- Inherits from BASE local self = BASE:Inherit( self, FSM:New() ) -- Core.Fsm#FSM_CONTROLLABLE - + if Controllable then self:SetControllable( Controllable ) end - + self:AddTransition( "*", "Stop", "Stopped" ) - + --- OnBefore Transition Handler for Event Stop. -- @function [parent=#FSM_CONTROLLABLE] OnBeforeStop -- @param #FSM_CONTROLLABLE self @@ -27437,7 +34145,7 @@ do -- FSM_CONTROLLABLE -- @param #string Event The Event string. -- @param #string To The To State string. -- @return #boolean Return false to cancel Transition. - + --- OnAfter Transition Handler for Event Stop. -- @function [parent=#FSM_CONTROLLABLE] OnAfterStop -- @param #FSM_CONTROLLABLE self @@ -27445,16 +34153,16 @@ do -- FSM_CONTROLLABLE -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. - + --- Synchronous Event Trigger for Event Stop. -- @function [parent=#FSM_CONTROLLABLE] Stop -- @param #FSM_CONTROLLABLE self - + --- Asynchronous Event Trigger for Event Stop. -- @function [parent=#FSM_CONTROLLABLE] __Stop -- @param #FSM_CONTROLLABLE self -- @param #number Delay The delay in seconds. - + --- OnLeave Transition Handler for State Stopped. -- @function [parent=#FSM_CONTROLLABLE] OnLeaveStopped -- @param #FSM_CONTROLLABLE self @@ -27463,7 +34171,7 @@ do -- FSM_CONTROLLABLE -- @param #string Event The Event string. -- @param #string To The To State string. -- @return #boolean Return false to cancel Transition. - + --- OnEnter Transition Handler for State Stopped. -- @function [parent=#FSM_CONTROLLABLE] OnEnterStopped -- @param #FSM_CONTROLLABLE self @@ -27482,51 +34190,53 @@ do -- FSM_CONTROLLABLE -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. - function FSM_CONTROLLABLE:OnAfterStop(Controllable,From,Event,To) - + function FSM_CONTROLLABLE:OnAfterStop( Controllable, From, Event, To ) + -- Clear all pending schedules self.CallScheduler:Clear() end - + --- Sets the CONTROLLABLE object that the FSM_CONTROLLABLE governs. -- @param #FSM_CONTROLLABLE self -- @param Wrapper.Controllable#CONTROLLABLE FSMControllable -- @return #FSM_CONTROLLABLE function FSM_CONTROLLABLE:SetControllable( FSMControllable ) - --self:F( FSMControllable:GetName() ) + -- self:F( FSMControllable:GetName() ) self.Controllable = FSMControllable end - + --- Gets the CONTROLLABLE object that the FSM_CONTROLLABLE governs. -- @param #FSM_CONTROLLABLE self -- @return Wrapper.Controllable#CONTROLLABLE function FSM_CONTROLLABLE:GetControllable() return self.Controllable end - + function FSM_CONTROLLABLE:_call_handler( step, trigger, params, EventName ) - + local handler = step .. trigger - + local ErrorHandler = function( errmsg ) - + env.info( "Error in SCHEDULER function:" .. errmsg ) if BASE.Debug ~= nil then env.info( BASE.Debug.traceback() ) end - + return errmsg end - + if self[handler] then self:T( "*** FSM *** " .. step .. " *** " .. params[1] .. " --> " .. params[2] .. " --> " .. params[3] .. " *** TaskUnit: " .. self.Controllable:GetName() ) self._EventSchedules[EventName] = nil - local Result, Value = xpcall( function() return self[handler]( self, self.Controllable, unpack( params ) ) end, ErrorHandler ) + local Result, Value = xpcall( function() + return self[handler]( self, self.Controllable, unpack( params ) ) + end, ErrorHandler ) return Value - --return self[handler]( self, self.Controllable, unpack( params ) ) + -- return self[handler]( self, self.Controllable, unpack( params ) ) end end - + end do -- FSM_PROCESS @@ -27534,50 +34244,47 @@ do -- FSM_PROCESS --- @type FSM_PROCESS -- @field Tasking.Task#TASK Task -- @extends Core.Fsm#FSM_CONTROLLABLE - - - --- FSM_PROCESS class models Finite State Machines for @{Task} actions, which control @{Client}s. + + --- FSM_PROCESS class models Finite State Machines for @{Tasking.Task} actions, which control @{Wrapper.Client}s. -- -- === -- -- @field #FSM_PROCESS FSM_PROCESS -- - FSM_PROCESS = { - ClassName = "FSM_PROCESS", - } - + FSM_PROCESS = { ClassName = "FSM_PROCESS" } + --- Creates a new FSM_PROCESS object. -- @param #FSM_PROCESS self -- @return #FSM_PROCESS function FSM_PROCESS:New( Controllable, Task ) - + local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- Core.Fsm#FSM_PROCESS - --self:F( Controllable ) - + -- self:F( Controllable ) + self:Assign( Controllable, Task ) - + return self end - + function FSM_PROCESS:Init( FsmProcess ) self:T( "No Initialisation" ) - end + end function FSM_PROCESS:_call_handler( step, trigger, params, EventName ) - + local handler = step .. trigger - + local ErrorHandler = function( errmsg ) - + env.info( "Error in FSM_PROCESS call handler:" .. errmsg ) if BASE.Debug ~= nil then env.info( BASE.Debug.traceback() ) end - + return errmsg end - + if self[handler] then if handler ~= "onstatechange" then self:T( "*** FSM *** " .. step .. " *** " .. params[1] .. " --> " .. params[2] .. " --> " .. params[3] .. " *** Task: " .. self.Task:GetName() .. ", TaskUnit: " .. self.Controllable:GetName() ) @@ -27585,53 +34292,54 @@ do -- FSM_PROCESS self._EventSchedules[EventName] = nil local Result, Value if self.Controllable and self.Controllable:IsAlive() == true then - Result, Value = xpcall( function() return self[handler]( self, self.Controllable, self.Task, unpack( params ) ) end, ErrorHandler ) + Result, Value = xpcall( function() + return self[handler]( self, self.Controllable, self.Task, unpack( params ) ) + end, ErrorHandler ) end return Value - --return self[handler]( self, self.Controllable, unpack( params ) ) + -- return self[handler]( self, self.Controllable, unpack( params ) ) end end - + --- Creates a new FSM_PROCESS object based on this 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 - + NewFsm:Assign( Controllable, Task ) -- Polymorphic call to initialize the new FSM_PROCESS based on self FSM_PROCESS NewFsm:Init( self ) - + -- Set Start State NewFsm:SetStartState( self:GetStartState() ) - + -- Copy Transitions for TransitionID, Transition in pairs( self:GetTransitions() ) do NewFsm:AddTransition( Transition.From, Transition.Event, Transition.To ) end - + -- Copy Processes for ProcessID, Process in pairs( self:GetProcesses() ) do - --self:E( { Process:GetName() } ) + -- self:E( { Process:GetName() } ) local FsmProcess = NewFsm:AddProcess( Process.From, Process.Event, Process.fsm:Copy( Controllable, Task ), Process.ReturnEvents ) end - + -- 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 - + return NewFsm end @@ -27643,7 +34351,7 @@ do -- FSM_PROCESS self:F( "Clearing Schedules" ) self.CallScheduler:Clear() - + -- Copy Processes for ProcessID, Process in pairs( self:GetProcesses() ) do if Process.fsm then @@ -27651,98 +34359,94 @@ do -- FSM_PROCESS Process.fsm = nil end end - + return self end - + --- Sets the task of the process. -- @param #FSM_PROCESS self -- @param Tasking.Task#TASK Task -- @return #FSM_PROCESS function FSM_PROCESS:SetTask( Task ) - + self.Task = Task - + return self end - + --- Gets the task of the process. -- @param #FSM_PROCESS self -- @return Tasking.Task#TASK function FSM_PROCESS:GetTask() - + return self.Task end - + --- Gets the mission of the process. -- @param #FSM_PROCESS self -- @return Tasking.Mission#MISSION function FSM_PROCESS:GetMission() - + return self.Task.Mission end - + --- Gets the mission of the process. -- @param #FSM_PROCESS self -- @return Tasking.CommandCenter#COMMANDCENTER function FSM_PROCESS:GetCommandCenter() - + return self:GetTask():GetMission():GetCommandCenter() end - --- TODO: Need to check and fix that an FSM_PROCESS is only for a UNIT. Not for a GROUP. - - --- Send a message of the @{Task} to the Group of the Unit. + + -- TODO: Need to check and fix that an FSM_PROCESS is only for a UNIT. Not for a GROUP. + + --- Send a message of the @{Tasking.Task} to the Group of the Unit. -- @param #FSM_PROCESS self function FSM_PROCESS:Message( Message ) self:F( { Message = Message } ) - + local CC = self:GetCommandCenter() local TaskGroup = self.Controllable:GetGroup() - + local PlayerName = self.Controllable:GetPlayerName() -- Only for a unit PlayerName = PlayerName and " (" .. PlayerName .. ")" or "" -- If PlayerName is nil, then keep it nil, otherwise add brackets. local Callsign = self.Controllable:GetCallsign() local Prefix = Callsign and " @ " .. Callsign .. PlayerName or "" - + Message = Prefix .. ": " .. Message CC:MessageToGroup( Message, TaskGroup ) end - - - --- Assign the process to a @{Wrapper.Unit} and activate the process. -- @param #FSM_PROCESS self -- @param Task.Tasking#TASK Task -- @param Wrapper.Unit#UNIT ProcessUnit -- @return #FSM_PROCESS self function FSM_PROCESS:Assign( ProcessUnit, Task ) - --self:T( { Task:GetName(), ProcessUnit:GetName() } ) - + -- self:T( { Task:GetName(), ProcessUnit:GetName() } ) + self:SetControllable( ProcessUnit ) self:SetTask( Task ) - - --self.ProcessGroup = ProcessUnit:GetGroup() - + + -- self.ProcessGroup = ProcessUnit:GetGroup() + return self end - --- function FSM_PROCESS:onenterAssigned( ProcessUnit, Task, From, Event, To ) --- --- if From( "Planned" ) then --- self:T( "*** FSM *** Assign *** " .. Task:GetName() .. "/" .. ProcessUnit:GetName() .. " *** " .. From .. " --> " .. Event .. " --> " .. To ) --- self.Task:Assign() --- end --- end - + + -- function FSM_PROCESS:onenterAssigned( ProcessUnit, Task, From, Event, To ) + -- + -- if From( "Planned" ) then + -- self:T( "*** FSM *** Assign *** " .. Task:GetName() .. "/" .. ProcessUnit:GetName() .. " *** " .. From .. " --> " .. Event .. " --> " .. To ) + -- self.Task:Assign() + -- end + -- end + function FSM_PROCESS:onenterFailed( ProcessUnit, Task, From, Event, To ) self:T( "*** FSM *** Failed *** " .. Task:GetName() .. "/" .. ProcessUnit:GetName() .. " *** " .. From .. " --> " .. Event .. " --> " .. To ) - + self.Task:Fail() end - --- StateMachine callback function for a FSM_PROCESS -- @param #FSM_PROCESS self -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit @@ -27750,20 +34454,20 @@ do -- FSM_PROCESS -- @param #string From -- @param #string To function FSM_PROCESS:onstatechange( ProcessUnit, Task, From, Event, To ) - + if From ~= To then self:T( "*** FSM *** Change *** " .. Task:GetName() .. "/" .. ProcessUnit:GetName() .. " *** " .. From .. " --> " .. Event .. " --> " .. To ) end - --- if self:IsTrace() then --- MESSAGE:New( "@ Process " .. self:GetClassNameAndID() .. " : " .. Event .. " changed to state " .. To, 2 ):ToAll() --- self:F2( { Scores = self._Scores, To = To } ) --- end - + + -- if self:IsTrace() then + -- MESSAGE:New( "@ Process " .. self:GetClassNameAndID() .. " : " .. Event .. " changed to state " .. To, 2 ):ToAll() + -- self:F2( { Scores = self._Scores, To = To } ) + -- end + -- TODO: This needs to be reworked with a callback functions allocated within Task, and set within the mission script from the Task Objects... if self._Scores[To] then - - local Task = self.Task + + local Task = self.Task local Scoring = Task:GetScoring() if Scoring then Scoring:_AddMissionTaskScore( Task.Mission, ProcessUnit, self._Scores[To].ScoreText, self._Scores[To].Score ) @@ -27779,49 +34483,51 @@ do -- FSM_TASK -- @type FSM_TASK -- @field Tasking.Task#TASK Task -- @extends #FSM - + --- Models Finite State Machines for @{Tasking.Task}s. - -- + -- -- === - -- + -- -- @field #FSM_TASK FSM_TASK - -- + -- FSM_TASK = { ClassName = "FSM_TASK", } - + --- Creates a new FSM_TASK object. -- @param #FSM_TASK self -- @param #string TaskName The name of the task. -- @return #FSM_TASK function FSM_TASK:New( TaskName ) - + local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- Core.Fsm#FSM_TASK - + self["onstatechange"] = self.OnStateChange self.TaskName = TaskName - + return self end - + function FSM_TASK:_call_handler( step, trigger, params, EventName ) local handler = step .. trigger - + local ErrorHandler = function( errmsg ) - + env.info( "Error in SCHEDULER function:" .. errmsg ) if BASE.Debug ~= nil then env.info( BASE.Debug.traceback() ) end - + return errmsg end if self[handler] then self:T( "*** FSM *** " .. step .. " *** " .. params[1] .. " --> " .. params[2] .. " --> " .. params[3] .. " *** Task: " .. self.TaskName ) self._EventSchedules[EventName] = nil - --return self[handler]( self, unpack( params ) ) - local Result, Value = xpcall( function() return self[handler]( self, unpack( params ) ) end, ErrorHandler ) + -- return self[handler]( self, unpack( params ) ) + local Result, Value = xpcall( function() + return self[handler]( self, unpack( params ) ) + end, ErrorHandler ) return Value end end @@ -27835,35 +34541,33 @@ do -- FSM_SET -- @field Core.Set#SET_BASE Set -- @extends Core.Fsm#FSM - - --- FSM_SET class models Finite State Machines for @{Set}s. Note that these FSMs control multiple objects!!! So State concerns here + --- FSM_SET class models Finite State Machines for @{Core.Set}s. Note that these FSMs control multiple objects!!! So State concerns here -- for multiple objects or the position of the state machine in the process. - -- + -- -- === - -- + -- -- @field #FSM_SET FSM_SET - -- FSM_SET = { ClassName = "FSM_SET", } - + --- Creates a new FSM_SET object. -- @param #FSM_SET self -- @param #table FSMT Finite State Machine Table -- @param Set_SET_BASE FSMSet (optional) The Set object that the FSM_SET governs. -- @return #FSM_SET function FSM_SET:New( FSMSet ) - + -- Inherits from BASE self = BASE:Inherit( self, FSM:New() ) -- Core.Fsm#FSM_SET - + if FSMSet then self:Set( FSMSet ) end - + return self end - + --- Sets the SET_BASE object that the FSM_SET governs. -- @param #FSM_SET self -- @param Core.Set#SET_BASE FSMSet @@ -27872,16 +34576,16 @@ do -- FSM_SET self:F( FSMSet ) self.Set = FSMSet end - + --- Gets the SET_BASE object that the FSM_SET governs. -- @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 ) - local handler = step .. trigger + + function FSM_SET:_call_handler( step, trigger, params, EventName ) + local handler = step .. trigger if self[handler] then self:T( "*** FSM *** " .. step .. " *** " .. params[1] .. " --> " .. params[2] .. " --> " .. params[3] ) self._EventSchedules[EventName] = nil @@ -27892,18 +34596,18 @@ do -- FSM_SET end -- FSM_SET --- **Core** - Spawn dynamically new groups of units in running missions. --- +-- -- === --- +-- -- ## Features: --- +-- -- * Spawn new groups in running missions. -- * Schedule spawning of new groups. -- * Put limits on the amount of groups that can be spawned, and the amount of units that can be alive at the same time. -- * Randomize the spawning location between different zones. -- * Randomize the initial positions within the zones. -- * Spawn in array formation. --- * Spawn uncontrolled (for planes or helos only). +-- * Spawn uncontrolled (for planes or helicopters only). -- * Clean up inactive helicopters that "crashed". -- * Place a hook to capture a spawn event, and tailor with customer code. -- * Spawn late activated. @@ -27920,26 +34624,25 @@ end -- FSM_SET -- * Spawn and keep the unit names. -- * Spawn with a different coalition and country. -- * Enquiry methods to check on spawn status. --- +-- -- === -- --- ### [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) -- -- === --- +-- -- ### [YouTube Playlist](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl1jirWIo4t4YxqN-HxjqRkL) --- +-- -- === --- +-- -- ### Author: **FlightControl** -- ### Contributions: A lot of people within this community! --- +-- -- === --- +-- -- @module Core.Spawn -- @image Core_Spawn.JPG - --- SPAWN Class -- @type SPAWN -- @field ClassName @@ -27952,81 +34655,80 @@ end -- FSM_SET -- @field #SPAWN.SpawnZoneTable SpawnZoneTable -- @extends Core.Base#BASE - ---- Allows to spawn dynamically new @{Core.Group}s. --- +--- Allows to spawn dynamically new @{Core.Group}s. +-- -- Each SPAWN object needs to be have related **template groups** setup in the Mission Editor (ME), --- which is a normal group with the **Late Activation** flag set. --- This template group will never be activated in your mission. --- SPAWN uses that **template group** to reference to all the characteristics --- (air, ground, livery, unit composition, formation, skill level etc) of each new group to be spawned. --- +-- which is a normal group with the **Late Activation** flag set. +-- This template group will never be activated in your mission. +-- SPAWN uses that **template group** to reference to all the characteristics +-- (air, ground, livery, unit composition, formation, skill level etc) of each new group to be spawned. +-- -- Therefore, when creating a SPAWN object, the @{#SPAWN.New} and @{#SPAWN.NewWithAlias} require --- **the name of the template group** to be given as a string to those constructor methods. --- --- Initialization settings can be applied on the SPAWN object, --- which modify the behaviour or the way groups are spawned. +-- **the name of the template group** to be given as a string to those constructor methods. +-- +-- Initialization settings can be applied on the SPAWN object, +-- which modify the behavior or the way groups are spawned. -- These initialization methods have the prefix **Init**. -- There are also spawn methods with the prefix **Spawn** and will spawn new groups in various ways. --- --- ### IMPORTANT! The methods with prefix **Init** must be used before any methods with prefix **Spawn** method are used, or unexpected results may appear!!! --- --- Because SPAWN can spawn multiple groups of a template group, --- SPAWN has an **internal index** that keeps track --- which was the latest group that was spawned. --- --- **Limits** can be set on how many groups can be spawn in each SPAWN object, +-- +-- ### IMPORTANT! The methods with prefix **Init** must be used before any methods with prefix **Spawn** method are used, or unexpected results may appear!!! +-- +-- Because SPAWN can spawn multiple groups of a template group, +-- SPAWN has an **internal index** that keeps track +-- which was the latest group that was spawned. +-- +-- **Limits** can be set on how many groups can be spawn in each SPAWN object, -- using the method @{#SPAWN.InitLimit}. SPAWN has 2 kind of limits: --- --- * The maximum amount of @{Wrapper.Unit}s that can be **alive** at the same time... +-- +-- * The maximum amount of @{Wrapper.Unit}s that can be **alive** at the same time... -- * The maximum amount of @{Wrapper.Group}s that can be **spawned**... This is more of a **resource**-type of limit. --- --- When new groups get spawned using the **Spawn** methods, +-- +-- When new groups get spawned using the **Spawn** methods, -- it will be evaluated whether any limits have been reached. --- When no spawn limit is reached, a new group will be created by the spawning methods, --- and the internal index will be increased with 1. --- --- These limits ensure that your mission does not accidentally get flooded with spawned groups. --- Additionally, it also guarantees that independent of the group composition, +-- When no spawn limit is reached, a new group will be created by the spawning methods, +-- and the internal index will be increased with 1. +-- +-- These limits ensure that your mission does not accidentally get flooded with spawned groups. +-- Additionally, it also guarantees that independent of the group composition, -- at any time, the most optimal amount of groups are alive in your mission. -- For example, if your template group has a group composition of 10 units, and you specify a limit of 100 units alive at the same time, -- with unlimited resources = :InitLimit( 100, 0 ) and 10 groups are alive, but two groups have only one unit alive in the group, -- then a sequent Spawn(Scheduled) will allow a new group to be spawned!!! --- --- ### IMPORTANT!! If a limit has been reached, it is possible that a **Spawn** method returns **nil**, meaning, no @{Wrapper.Group} had been spawned!!! --- --- Spawned groups get **the same name** as the name of the template group. --- Spawned units in those groups keep _by default_ **the same name** as the name of the template group. --- However, because multiple groups and units are created from the template group, +-- +-- ### IMPORTANT!! If a limit has been reached, it is possible that a **Spawn** method returns **nil**, meaning, no @{Wrapper.Group} had been spawned!!! +-- +-- Spawned groups get **the same name** as the name of the template group. +-- Spawned units in those groups keep _by default_ **the same name** as the name of the template group. +-- However, because multiple groups and units are created from the template group, -- a suffix is added to each spawned group and unit. --- +-- -- Newly spawned groups will get the following naming structure at run-time: --- --- 1. Spawned groups will have the name _GroupName_#_nnn_, where _GroupName_ is the name of the **template group**, +-- +-- 1. Spawned groups will have the name _GroupName_#_nnn_, where _GroupName_ is the name of the **template group**, -- and _nnn_ is a **counter from 0 to 999**. --- 2. Spawned units will have the name _GroupName_#_nnn_-_uu_, +-- 2. Spawned units will have the name _GroupName_#_nnn_-_uu_, -- where _uu_ is a **counter from 0 to 99** for each new spawned unit belonging to the group. --- --- That being said, there is a way to keep the same unit names! +-- +-- That being said, there is a way to keep the same unit names! -- The method @{#SPAWN.InitKeepUnitNames}() will keep the same unit names as defined within the template group, thus: --- --- 3. Spawned units will have the name _UnitName_#_nnn_-_uu_, --- where _UnitName_ is the **unit name as defined in the template group*, +-- +-- 3. Spawned units will have the name _UnitName_#_nnn_-_uu_, +-- where _UnitName_ is the **unit name as defined in the template group*, -- and _uu_ is a **counter from 0 to 99** for each new spawned unit belonging to the group. --- +-- -- Some **additional notes that need to be considered!!**: --- --- * templates are actually groups defined within the mission editor, with the flag "Late Activation" set. +-- +-- * templates are actually groups defined within the mission editor, with the flag "Late Activation" set. -- As such, these groups are never used within the mission, but are used by the @{#SPAWN} module. -- * It is important to defined BEFORE you spawn new groups, -- a proper initialization of the SPAWN instance is done with the options you want to use. --- * When designing a mission, NEVER name groups using a "#" within the name of the group Spawn template(s), +-- * When designing a mission, NEVER name groups using a "#" within the name of the group Spawn template(s), -- or the SPAWN module logic won't work anymore. --- +-- -- ## SPAWN construction methods --- +-- -- Create a new SPAWN object with the @{#SPAWN.New}() or the @{#SPAWN.NewWithAlias}() methods: --- +-- -- * @{#SPAWN.New}(): Creates a new SPAWN object taking the name of the group that represents the GROUP template (definition). -- * @{#SPAWN.NewWithAlias}(): Creates a new SPAWN object taking the name of the group that represents the GROUP template (definition), and gives each spawned @{Wrapper.Group} an different name. -- @@ -28035,135 +34737,130 @@ end -- FSM_SET -- So in principle, the group list will contain all parameters and configurations after initialization, and when groups get actually spawned, this spawning can be done quickly and efficient. -- -- ## SPAWN **Init**ialization methods --- --- A spawn object will behave differently based on the usage of **initialization** methods, which all start with the **Init** prefix: --- +-- +-- A spawn object will behave differently based on the usage of **initialization** methods, which all start with the **Init** prefix: +-- -- ### Unit Names --- +-- -- * @{#SPAWN.InitKeepUnitNames}(): Keeps the unit names as defined within the mission editor, but note that anything after a # mark is ignored, and any spaces before and after the resulting name are removed. IMPORTANT! This method MUST be the first used after :New !!! --- +-- -- ### Route randomization --- +-- -- * @{#SPAWN.InitRandomizeRoute}(): Randomize the routes of spawned groups, and for air groups also optionally the height. --- --- ### Group composition randomization --- +-- +-- ### Group composition randomization +-- -- * @{#SPAWN.InitRandomizeTemplate}(): Randomize the group templates so that when a new group is spawned, a random group template is selected from one of the templates defined. --- +-- -- ### Uncontrolled --- +-- -- * @{#SPAWN.InitUnControlled}(): Spawn plane groups uncontrolled. --- +-- -- ### Array formation --- --- * @{#SPAWN.InitArray}(): Make groups visible before they are actually activated, and order these groups like a batallion in an array. --- +-- +-- * @{#SPAWN.InitArray}(): Make groups visible before they are actually activated, and order these groups like a battalion in an array. +-- -- ### Position randomization --- +-- -- * @{#SPAWN.InitRandomizePosition}(): Randomizes the position of @{Wrapper.Group}s that are spawned within a **radius band**, given an Outer and Inner radius, from the point that the spawn happens. -- * @{#SPAWN.InitRandomizeUnits}(): Randomizes the @{Wrapper.Unit}s in the @{Wrapper.Group} that is spawned within a **radius band**, given an Outer and Inner radius. --- * @{#SPAWN.InitRandomizeZones}(): Randomizes the spawning between a predefined list of @{Zone}s that are declared using this function. Each zone can be given a probability factor. --- +-- * @{#SPAWN.InitRandomizeZones}(): Randomizes the spawning between a predefined list of @{Core.Zone}s that are declared using this function. Each zone can be given a probability factor. +-- -- ### Enable / Disable AI when spawning a new @{Wrapper.Group} --- +-- -- * @{#SPAWN.InitAIOn}(): Turns the AI On when spawning the new @{Wrapper.Group} object. -- * @{#SPAWN.InitAIOff}(): Turns the AI Off when spawning the new @{Wrapper.Group} object. -- * @{#SPAWN.InitAIOnOff}(): Turns the AI On or Off when spawning the new @{Wrapper.Group} object. --- --- ### Limit scheduled spawning --- +-- +-- ### Limit scheduled spawning +-- -- * @{#SPAWN.InitLimit}(): Limits the amount of groups that can be alive at the same time and that can be dynamically spawned. --- +-- -- ### Delay initial scheduled spawn --- --- * @{#SPAWN.InitDelayOnOff}(): Turns the inital delay On/Off when scheduled spawning the first @{Wrapper.Group} object. --- * @{#SPAWN.InitDelayOn}(): Turns the inital delay On when scheduled spawning the first @{Wrapper.Group} object. --- * @{#SPAWN.InitDelayOff}(): Turns the inital delay Off when scheduled spawning the first @{Wrapper.Group} object. --- +-- +-- * @{#SPAWN.InitDelayOnOff}(): Turns the initial delay On/Off when scheduled spawning the first @{Wrapper.Group} object. +-- * @{#SPAWN.InitDelayOn}(): Turns the initial delay On when scheduled spawning the first @{Wrapper.Group} object. +-- * @{#SPAWN.InitDelayOff}(): Turns the initial delay Off when scheduled spawning the first @{Wrapper.Group} object. +-- -- ### Repeat spawned @{Wrapper.Group}s upon landing --- +-- -- * @{#SPAWN.InitRepeat}() or @{#SPAWN.InitRepeatOnLanding}(): This method is used to re-spawn automatically the same group after it has landed. -- * @{#SPAWN.InitRepeatOnEngineShutDown}(): This method is used to re-spawn automatically the same group after it has landed and it shuts down the engines at the ramp. --- --- +-- -- ## SPAWN **Spawn** methods --- +-- -- Groups can be spawned at different times and methods: --- +-- -- ### **Single** spawning methods --- +-- -- * @{#SPAWN.Spawn}(): Spawn one new group based on the last spawned index. -- * @{#SPAWN.ReSpawn}(): Re-spawn a group based on a given index. -- * @{#SPAWN.SpawnFromVec3}(): Spawn a new group from a Vec3 coordinate. (The group will can be spawned at a point in the air). -- * @{#SPAWN.SpawnFromVec2}(): Spawn a new group from a Vec2 coordinate. (The group will be spawned at land height ). --- * @{#SPAWN.SpawnFromStatic}(): Spawn a new group from a structure, taking the position of a @{Static}. +-- * @{#SPAWN.SpawnFromStatic}(): Spawn a new group from a structure, taking the position of a @{Wrapper.Static}. -- * @{#SPAWN.SpawnFromUnit}(): Spawn a new group taking the position of a @{Wrapper.Unit}. --- * @{#SPAWN.SpawnInZone}(): Spawn a new group in a @{Zone}. +-- * @{#SPAWN.SpawnInZone}(): Spawn a new group in a @{Core.Zone}. -- * @{#SPAWN.SpawnAtAirbase}(): Spawn a new group at an @{Wrapper.Airbase}, which can be an airdrome, ship or helipad. --- --- Note that @{#SPAWN.Spawn} and @{#SPAWN.ReSpawn} return a @{Wrapper.Group#GROUP.New} object, that contains a reference to the DCSGroup object. --- You can use the @{GROUP} object to do further actions with the DCSGroup. --- +-- +-- Note that @{#SPAWN.Spawn} and @{#SPAWN.ReSpawn} return a @{Wrapper.Group#GROUP.New} object, that contains a reference to the DCSGroup object. +-- You can use the @{Wrapper.Group#GROUP} object to do further actions with the DCSGroup. +-- -- ### **Scheduled** spawning methods --- --- * @{#SPAWN.SpawnScheduled}(): Spawn groups at scheduled but randomized intervals. ---- * @{#SPAWN.SpawnScheduleStart}(): Start or continue to spawn groups at scheduled time intervals. --- * @{#SPAWN.SpawnScheduleStop}(): Stop the spawning of groups at scheduled time intervals. --- --- +-- +-- * @{#SPAWN.SpawnScheduled}(): Spawn groups at scheduled but randomized intervals. +--- * @{#SPAWN.SpawnScheduleStart}(): Start or continue to spawn groups at scheduled time intervals. +-- * @{#SPAWN.SpawnScheduleStop}(): Stop the spawning of groups at scheduled time intervals. +-- -- ## Retrieve alive GROUPs spawned by the SPAWN object --- +-- -- The SPAWN class administers which GROUPS it has reserved (in stock) or has created during mission execution. -- Every time a SPAWN object spawns a new GROUP object, a reference to the GROUP object is added to an internal table of GROUPS. -- SPAWN provides methods to iterate through that internal GROUP object reference table: --- +-- -- * @{#SPAWN.GetFirstAliveGroup}(): Will find the first alive GROUP it has spawned, and return the alive GROUP object and the first Index where the first alive GROUP object has been found. -- * @{#SPAWN.GetNextAliveGroup}(): Will find the next alive GROUP object from a given Index, and return a reference to the alive GROUP object and the next Index where the alive GROUP has been found. -- * @{#SPAWN.GetLastAliveGroup}(): Will find the last alive GROUP object, and will return a reference to the last live GROUP object and the last Index where the last alive GROUP object has been found. --- +-- -- You can use the methods @{#SPAWN.GetFirstAliveGroup}() and sequently @{#SPAWN.GetNextAliveGroup}() to iterate through the alive GROUPS within the SPAWN object, and to actions... See the respective methods for an example. -- The method @{#SPAWN.GetGroupFromIndex}() will return the GROUP object reference from the given Index, dead or alive... --- +-- -- ## Spawned cleaning of inactive groups --- --- Sometimes, it will occur during a mission run-time, that ground or especially air objects get damaged, and will while being damged stop their activities, while remaining alive. --- In such cases, the SPAWN object will just sit there and wait until that group gets destroyed, but most of the time it won't, +-- +-- Sometimes, it will occur during a mission run-time, that ground or especially air objects get damaged, and will while being damaged stop their activities, while remaining alive. +-- In such cases, the SPAWN object will just sit there and wait until that group gets destroyed, but most of the time it won't, -- and it may occur that no new groups are or can be spawned as limits are reached. -- To prevent this, a @{#SPAWN.InitCleanUp}() initialization method has been defined that will silently monitor the status of each spawned group. --- Once a group has a velocity = 0, and has been waiting for a defined interval, that group will be cleaned or removed from run-time. --- There is a catch however :-) If a damaged group has returned to an airbase within the coalition, that group will not be considered as "lost"... --- In such a case, when the inactive group is cleaned, a new group will Re-spawned automatically. --- This models AI that has succesfully returned to their airbase, to restart their combat activities. +-- Once a group has a velocity = 0, and has been waiting for a defined interval, that group will be cleaned or removed from run-time. +-- There is a catch however :-) If a damaged group has returned to an airbase within the coalition, that group will not be considered as "lost"... +-- In such a case, when the inactive group is cleaned, a new group will Re-spawned automatically. +-- This models AI that has successfully returned to their airbase, to restart their combat activities. -- Check the @{#SPAWN.InitCleanUp}() for further info. --- +-- -- ## Catch the @{Wrapper.Group} Spawn Event in a callback function! --- +-- -- When using the @{#SPAWN.SpawnScheduled)() method, new @{Wrapper.Group}s are created following the spawn time interval parameters. -- When a new @{Wrapper.Group} is spawned, you maybe want to execute actions with that group spawned at the spawn event. --- The SPAWN class supports this functionality through the method @{#SPAWN.OnSpawnGroup}( **function( SpawnedGroup ) end ** ), --- which takes a function as a parameter that you can define locally. +-- The SPAWN class supports this functionality through the method @{#SPAWN.OnSpawnGroup}( **function( SpawnedGroup ) end ** ), +-- which takes a function as a parameter that you can define locally. -- Whenever a new @{Wrapper.Group} is spawned, the given function is called, and the @{Wrapper.Group} that was just spawned, is given as a parameter. --- As a result, your spawn event handling function requires one parameter to be declared, which will contain the spawned @{Wrapper.Group} object. +-- As a result, your spawn event handling function requires one parameter to be declared, which will contain the spawned @{Wrapper.Group} object. -- A coding example is provided at the description of the @{#SPAWN.OnSpawnGroup}( **function( SpawnedGroup ) end ** ) method. --- +-- -- ## Delay the initial spawning --- --- When using the @{#SPAWN.SpawnScheduled)() method, the default behaviour of this method will be that it will spawn the initial (first) @{Wrapper.Group} +-- +-- When using the @{#SPAWN.SpawnScheduled)() method, the default behavior of this method will be that it will spawn the initial (first) @{Wrapper.Group} -- immediately when :SpawnScheduled() is initiated. The methods @{#SPAWN.InitDelayOnOff}() and @{#SPAWN.InitDelayOn}() can be used to -- activate a delay before the first @{Wrapper.Group} is spawned. For completeness, a method @{#SPAWN.InitDelayOff}() is also available, that --- can be used to switch off the initial delay. Because there is no delay by default, this method would only be used when a +-- can be used to switch off the initial delay. Because there is no delay by default, this method would only be used when a -- @{#SPAWN.SpawnScheduleStop}() ; @{#SPAWN.SpawnScheduleStart}() sequence would have been used. --- --- +-- -- @field #SPAWN SPAWN --- SPAWN = { ClassName = "SPAWN", SpawnTemplatePrefix = nil, SpawnAliasPrefix = nil, } - --- Enumerator for spawns at airbases -- @type SPAWN.Takeoff -- @extends Wrapper.Group#GROUP.Takeoff @@ -28179,7 +34876,6 @@ SPAWN.Takeoff = { --- @type SPAWN.SpawnZoneTable -- @list SpawnZone - --- Creates the main object to spawn a @{Wrapper.Group} defined in the DCS ME. -- @param #SPAWN self -- @param #string SpawnTemplatePrefix is the name of the Group in the ME that defines the Template. Each new group will have the name starting with SpawnTemplatePrefix. @@ -28189,47 +34885,47 @@ SPAWN.Takeoff = { -- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ) -- @usage local Plane = SPAWN:New( "Plane" ) -- Creates a new local variable that can initiate new planes with the name "Plane#ddd" using the template "Plane" as defined within the ME. function SPAWN:New( SpawnTemplatePrefix ) - local self = BASE:Inherit( self, BASE:New() ) -- #SPAWN - self:F( { SpawnTemplatePrefix } ) - - local TemplateGroup = GROUP:FindByName( SpawnTemplatePrefix ) - if TemplateGroup then - self.SpawnTemplatePrefix = SpawnTemplatePrefix - self.SpawnIndex = 0 - self.SpawnCount = 0 -- The internal counter of the amount of spawning the has happened since SpawnStart. - self.AliveUnits = 0 -- Contains the counter how many units are currently alive - self.SpawnIsScheduled = false -- Reflects if the spawning for this SpawnTemplatePrefix is going to be scheduled or not. - self.SpawnTemplate = self._GetTemplate( self, SpawnTemplatePrefix ) -- Contains the template structure for a Group Spawn from the Mission Editor. Note that this group must have lateActivation always on!!! - self.Repeat = false -- Don't repeat the group from Take-Off till Landing and back Take-Off by ReSpawning. - self.UnControlled = false -- When working in UnControlled mode, all planes are Spawned in UnControlled mode before the scheduler starts. - self.SpawnInitLimit = false -- By default, no InitLimit - self.SpawnMaxUnitsAlive = 0 -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. - self.SpawnMaxGroups = 0 -- The maximum amount of groups that can be spawned. - self.SpawnRandomize = false -- Sets the randomization flag of new Spawned units to false. - self.SpawnVisible = false -- Flag that indicates if all the Groups of the SpawnGroup need to be visible when Spawned. - self.AIOnOff = true -- The AI is on by default when spawning a group. + local self = BASE:Inherit( self, BASE:New() ) -- #SPAWN + self:F( { SpawnTemplatePrefix } ) + + local TemplateGroup = GROUP:FindByName( SpawnTemplatePrefix ) + if TemplateGroup then + self.SpawnTemplatePrefix = SpawnTemplatePrefix + self.SpawnIndex = 0 + self.SpawnCount = 0 -- The internal counter of the amount of spawning the has happened since SpawnStart. + self.AliveUnits = 0 -- Contains the counter how many units are currently alive + self.SpawnIsScheduled = false -- Reflects if the spawning for this SpawnTemplatePrefix is going to be scheduled or not. + self.SpawnTemplate = self._GetTemplate( self, SpawnTemplatePrefix ) -- Contains the template structure for a Group Spawn from the Mission Editor. Note that this group must have lateActivation always on!!! + self.Repeat = false -- Don't repeat the group from Take-Off till Landing and back Take-Off by ReSpawning. + self.UnControlled = false -- When working in UnControlled mode, all planes are Spawned in UnControlled mode before the scheduler starts. + self.SpawnInitLimit = false -- By default, no InitLimit + self.SpawnMaxUnitsAlive = 0 -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. + self.SpawnMaxGroups = 0 -- The maximum amount of groups that can be spawned. + self.SpawnRandomize = false -- Sets the randomization flag of new Spawned units to false. + self.SpawnVisible = false -- Flag that indicates if all the Groups of the SpawnGroup need to be visible when Spawned. + self.AIOnOff = true -- The AI is on by default when spawning a group. self.SpawnUnControlled = false - self.SpawnInitKeepUnitNames = false -- Overwrite unit names by default with group name. - self.DelayOnOff = false -- No intial delay when spawning the first group. - self.SpawnGrouping = nil -- No grouping. - self.SpawnInitLivery = nil -- No special livery. - self.SpawnInitSkill = nil -- No special skill. - self.SpawnInitFreq = nil -- No special frequency. - self.SpawnInitModu = nil -- No special modulation. - self.SpawnInitRadio = nil -- No radio comms setting. + self.SpawnInitKeepUnitNames = false -- Overwrite unit names by default with group name. + self.DelayOnOff = false -- No intial delay when spawning the first group. + self.SpawnGrouping = nil -- No grouping. + self.SpawnInitLivery = nil -- No special livery. + self.SpawnInitSkill = nil -- No special skill. + self.SpawnInitFreq = nil -- No special frequency. + self.SpawnInitModu = nil -- No special modulation. + self.SpawnInitRadio = nil -- No radio comms setting. self.SpawnInitModex = nil self.SpawnInitAirbase = nil - self.TweakedTemplate = false -- Check if the user is using self made template. + self.TweakedTemplate = false -- Check if the user is using self made template. - self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. - else - error( "SPAWN:New: There is no group declared in the mission editor with SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" ) - end + self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. + else + error( "SPAWN:New: There is no group declared in the mission editor with SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" ) + end self:SetEventPriority( 5 ) self.SpawnHookScheduler = SCHEDULER:New( nil ) - return self + return self end --- Creates a new SPAWN instance to create new groups based on the defined template and using a new alias for each new group. @@ -28242,140 +34938,142 @@ end -- Spawn_BE_KA50 = SPAWN:NewWithAlias( 'BE KA-50@RAMP-Ground Defense', 'Helicopter Attacking a City' ) -- @usage local PlaneWithAlias = SPAWN:NewWithAlias( "Plane", "Bomber" ) -- Creates a new local variable that can instantiate new planes with the name "Bomber#ddd" using the template "Plane" as defined within the ME. function SPAWN:NewWithAlias( SpawnTemplatePrefix, SpawnAliasPrefix ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( { SpawnTemplatePrefix, SpawnAliasPrefix } ) - - local TemplateGroup = GROUP:FindByName( SpawnTemplatePrefix ) - if TemplateGroup then - self.SpawnTemplatePrefix = SpawnTemplatePrefix - self.SpawnAliasPrefix = SpawnAliasPrefix - self.SpawnIndex = 0 - self.SpawnCount = 0 -- The internal counter of the amount of spawning the has happened since SpawnStart. - self.AliveUnits = 0 -- Contains the counter how many units are currently alive - self.SpawnIsScheduled = false -- Reflects if the spawning for this SpawnTemplatePrefix is going to be scheduled or not. - self.SpawnTemplate = self._GetTemplate( self, SpawnTemplatePrefix ) -- Contains the template structure for a Group Spawn from the Mission Editor. Note that this group must have lateActivation always on!!! - self.Repeat = false -- Don't repeat the group from Take-Off till Landing and back Take-Off by ReSpawning. - self.UnControlled = false -- When working in UnControlled mode, all planes are Spawned in UnControlled mode before the scheduler starts. - self.SpawnInitLimit = false -- By default, no InitLimit - self.SpawnMaxUnitsAlive = 0 -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. - self.SpawnMaxGroups = 0 -- The maximum amount of groups that can be spawned. - self.SpawnRandomize = false -- Sets the randomization flag of new Spawned units to false. - self.SpawnVisible = false -- Flag that indicates if all the Groups of the SpawnGroup need to be visible when Spawned. - self.AIOnOff = true -- The AI is on by default when spawning a group. + local self = BASE:Inherit( self, BASE:New() ) + self:F( { SpawnTemplatePrefix, SpawnAliasPrefix } ) + + local TemplateGroup = GROUP:FindByName( SpawnTemplatePrefix ) + if TemplateGroup then + self.SpawnTemplatePrefix = SpawnTemplatePrefix + self.SpawnAliasPrefix = SpawnAliasPrefix + self.SpawnIndex = 0 + self.SpawnCount = 0 -- The internal counter of the amount of spawning the has happened since SpawnStart. + self.AliveUnits = 0 -- Contains the counter how many units are currently alive + self.SpawnIsScheduled = false -- Reflects if the spawning for this SpawnTemplatePrefix is going to be scheduled or not. + self.SpawnTemplate = self._GetTemplate( self, SpawnTemplatePrefix ) -- Contains the template structure for a Group Spawn from the Mission Editor. Note that this group must have lateActivation always on!!! + self.Repeat = false -- Don't repeat the group from Take-Off till Landing and back Take-Off by ReSpawning. + self.UnControlled = false -- When working in UnControlled mode, all planes are Spawned in UnControlled mode before the scheduler starts. + self.SpawnInitLimit = false -- By default, no InitLimit + self.SpawnMaxUnitsAlive = 0 -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. + self.SpawnMaxGroups = 0 -- The maximum amount of groups that can be spawned. + self.SpawnRandomize = false -- Sets the randomization flag of new Spawned units to false. + self.SpawnVisible = false -- Flag that indicates if all the Groups of the SpawnGroup need to be visible when Spawned. + self.AIOnOff = true -- The AI is on by default when spawning a group. self.SpawnUnControlled = false - self.SpawnInitKeepUnitNames = false -- Overwrite unit names by default with group name. - self.DelayOnOff = false -- No intial delay when spawning the first group. - self.SpawnGrouping = nil -- No grouping. - self.SpawnInitLivery = nil -- No special livery. - self.SpawnInitSkill = nil -- No special skill. - self.SpawnInitFreq = nil -- No special frequency. - self.SpawnInitModu = nil -- No special modulation. - self.SpawnInitRadio = nil -- No radio comms setting. + self.SpawnInitKeepUnitNames = false -- Overwrite unit names by default with group name. + self.DelayOnOff = false -- No initial delay when spawning the first group. + self.SpawnGrouping = nil -- No grouping. + self.SpawnInitLivery = nil -- No special livery. + self.SpawnInitSkill = nil -- No special skill. + self.SpawnInitFreq = nil -- No special frequency. + self.SpawnInitModu = nil -- No special modulation. + self.SpawnInitRadio = nil -- No radio communication setting. self.SpawnInitModex = nil self.SpawnInitAirbase = nil - self.TweakedTemplate = false -- Check if the user is using self made template. + self.TweakedTemplate = false -- Check if the user is using self made template. + + self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. + else + error( "SPAWN:New: There is no group declared in the mission editor with SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" ) + end - self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. - else - error( "SPAWN:New: There is no group declared in the mission editor with SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" ) - end - self:SetEventPriority( 5 ) self.SpawnHookScheduler = SCHEDULER:New( nil ) - - return self -end + return self +end --- Creates a new SPAWN instance to create new groups based on the provided template. -- @param #SPAWN self -- @param #table SpawnTemplate is the Template of the Group. This must be a valid Group Template structure! -- @param #string SpawnTemplatePrefix is the name of the Group that will be given at each spawn. --- @param #string SpawnAliasPrefix (optional) is the name that will be given to the Group at runtime. +-- @param #string SpawnAliasPrefix is the name that will be given to the Group at runtime. -- @return #SPAWN -- @usage -- -- Create a new SPAWN object based on a Group Template defined from scratch. -- Spawn_BE_KA50 = SPAWN:NewWithAlias( 'BE KA-50@RAMP-Ground Defense', 'Helicopter Attacking a City' ) --- @usage --- -- Create a new CSAR_Spawn object based on a normal Group Template to spawn a soldier. --- local CSAR_Spawn = SPAWN:NewWithFromTemplate( Template, "CSAR", "Pilot" ) +-- @usage +-- +-- -- Create a new CSAR_Spawn object based on a normal Group Template to spawn a soldier. +-- local CSAR_Spawn = SPAWN:NewWithFromTemplate( Template, "CSAR", "Pilot" ) +-- function SPAWN:NewFromTemplate( SpawnTemplate, SpawnTemplatePrefix, SpawnAliasPrefix ) local self = BASE:Inherit( self, BASE:New() ) self:F( { SpawnTemplate, SpawnTemplatePrefix, SpawnAliasPrefix } ) if SpawnAliasPrefix == nil or SpawnAliasPrefix == "" then - BASE:I("ERROR: in function NewFromTemplate, required paramter SpawnAliasPrefix is not set") + BASE:I( "ERROR: in function NewFromTemplate, required parameter SpawnAliasPrefix is not set" ) return nil end if SpawnTemplate then - self.SpawnTemplate = SpawnTemplate -- Contains the template structure for a Group Spawn from the Mission Editor. Note that this group must have lateActivation always on!!! + self.SpawnTemplate = SpawnTemplate -- Contains the template structure for a Group Spawn from the Mission Editor. Note that this group must have lateActivation always on!!! self.SpawnTemplatePrefix = SpawnTemplatePrefix self.SpawnAliasPrefix = SpawnAliasPrefix self.SpawnIndex = 0 - self.SpawnCount = 0 -- The internal counter of the amount of spawning the has happened since SpawnStart. - self.AliveUnits = 0 -- Contains the counter how many units are currently alive - self.SpawnIsScheduled = false -- Reflects if the spawning for this SpawnTemplatePrefix is going to be scheduled or not. - self.Repeat = false -- Don't repeat the group from Take-Off till Landing and back Take-Off by ReSpawning. - self.UnControlled = false -- When working in UnControlled mode, all planes are Spawned in UnControlled mode before the scheduler starts. - self.SpawnInitLimit = false -- By default, no InitLimit. - self.SpawnMaxUnitsAlive = 0 -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. - self.SpawnMaxGroups = 0 -- The maximum amount of groups that can be spawned. - self.SpawnRandomize = false -- Sets the randomization flag of new Spawned units to false. - self.SpawnVisible = false -- Flag that indicates if all the Groups of the SpawnGroup need to be visible when Spawned. - self.AIOnOff = true -- The AI is on by default when spawning a group. + self.SpawnCount = 0 -- The internal counter of the amount of spawning the has happened since SpawnStart. + self.AliveUnits = 0 -- Contains the counter how many units are currently alive + self.SpawnIsScheduled = false -- Reflects if the spawning for this SpawnTemplatePrefix is going to be scheduled or not. + self.Repeat = false -- Don't repeat the group from Take-Off till Landing and back Take-Off by ReSpawning. + self.UnControlled = false -- When working in UnControlled mode, all planes are Spawned in UnControlled mode before the scheduler starts. + self.SpawnInitLimit = false -- By default, no InitLimit. + self.SpawnMaxUnitsAlive = 0 -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. + self.SpawnMaxGroups = 0 -- The maximum amount of groups that can be spawned. + self.SpawnRandomize = false -- Sets the randomization flag of new Spawned units to false. + self.SpawnVisible = false -- Flag that indicates if all the Groups of the SpawnGroup need to be visible when Spawned. + self.AIOnOff = true -- The AI is on by default when spawning a group. self.SpawnUnControlled = false - self.SpawnInitKeepUnitNames = false -- Overwrite unit names by default with group name. - self.DelayOnOff = false -- No intial delay when spawning the first group. - self.Grouping = nil -- No grouping. - self.SpawnInitLivery = nil -- No special livery. - self.SpawnInitSkill = nil -- No special skill. - self.SpawnInitFreq = nil -- No special frequency. - self.SpawnInitModu = nil -- No special modulation. - self.SpawnInitRadio = nil -- No radio comms setting. + self.SpawnInitKeepUnitNames = false -- Overwrite unit names by default with group name. + self.DelayOnOff = false -- No initial delay when spawning the first group. + self.Grouping = nil -- No grouping. + self.SpawnInitLivery = nil -- No special livery. + self.SpawnInitSkill = nil -- No special skill. + self.SpawnInitFreq = nil -- No special frequency. + self.SpawnInitModu = nil -- No special modulation. + self.SpawnInitRadio = nil -- No radio communication setting. self.SpawnInitModex = nil self.SpawnInitAirbase = nil - self.TweakedTemplate = true -- Check if the user is using self made template. - - self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. + self.TweakedTemplate = true -- Check if the user is using self made template. + + self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. else error( "There is no template provided for SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" ) end - + self:SetEventPriority( 5 ) self.SpawnHookScheduler = SCHEDULER:New( nil ) - + return self end - --- Stops any more repeat spawns from happening once the UNIT count of Alive units, spawned by the same SPAWN object, exceeds the first parameter. Also can stop spawns from happening once a total GROUP still alive is met. -- Exceptionally powerful when combined with SpawnSchedule for Respawning. -- Note that this method is exceptionally important to balance the performance of the mission. Depending on the machine etc, a mission can only process a maximum amount of units. -- If the time interval must be short, but there should not be more Units or Groups alive than a maximum amount of units, then this method should be used... -- When a @{#SPAWN.New} is executed and the limit of the amount of units alive is reached, then no new spawn will happen of the group, until some of these units of the spawn object will be destroyed. -- @param #SPAWN self --- @param #number SpawnMaxUnitsAlive The maximum amount of units that can be alive at runtime. --- @param #number SpawnMaxGroups The maximum amount of groups that can be spawned. When the limit is reached, then no more actual spawns will happen of the group. --- This parameter is useful to define a maximum amount of airplanes, ground troops, helicopters, ships etc within a supply area. +-- @param #number SpawnMaxUnitsAlive The maximum amount of units that can be alive at runtime. +-- @param #number SpawnMaxGroups The maximum amount of groups that can be spawned. When the limit is reached, then no more actual spawns will happen of the group. +-- This parameter is useful to define a maximum amount of airplanes, ground troops, helicopters, ships etc within a supply area. -- This parameter accepts the value 0, which defines that there are no maximum group limits, but there are limits on the maximum of units that can be alive at the same time. -- @return #SPAWN self -- @usage --- -- NATO helicopters engaging in the battle field. --- -- This helicopter group consists of one Unit. So, this group will SPAWN maximum 2 groups simultaneously within the DCSRTE. --- -- There will be maximum 24 groups spawned during the whole mission lifetime. --- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):InitLimit( 2, 24 ) +-- +-- -- NATO helicopters engaging in the battle field. +-- -- This helicopter group consists of one Unit. So, this group will SPAWN maximum 2 groups simultaneously within the DCSRTE. +-- -- There will be maximum 24 groups spawned during the whole mission lifetime. +-- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):InitLimit( 2, 24 ) +-- function SPAWN:InitLimit( SpawnMaxUnitsAlive, SpawnMaxGroups ) - self:F( { self.SpawnTemplatePrefix, SpawnMaxUnitsAlive, SpawnMaxGroups } ) + self:F( { self.SpawnTemplatePrefix, SpawnMaxUnitsAlive, SpawnMaxGroups } ) self.SpawnInitLimit = true - self.SpawnMaxUnitsAlive = SpawnMaxUnitsAlive -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. - self.SpawnMaxGroups = SpawnMaxGroups -- The maximum amount of groups that can be spawned. - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self:_InitializeSpawnGroups( SpawnGroupID ) - end + self.SpawnMaxUnitsAlive = SpawnMaxUnitsAlive -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. + self.SpawnMaxGroups = SpawnMaxGroups -- The maximum amount of groups that can be spawned. - return self + for SpawnGroupID = 1, self.SpawnMaxGroups do + self:_InitializeSpawnGroups( SpawnGroupID ) + end + + return self end --- Keeps the unit names as defined within the mission editor, @@ -28386,23 +35084,22 @@ end -- @param #boolean KeepUnitNames (optional) If true, the unit names are kept, false or not provided to make new unit names. -- @return #SPAWN self function SPAWN:InitKeepUnitNames( KeepUnitNames ) - self:F( ) + self:F() self.SpawnInitKeepUnitNames = KeepUnitNames or true - + return self end - --- Flags that the spawned groups must be spawned late activated. -- @param #SPAWN self -- @param #boolean LateActivated (optional) If true, the spawned groups are late activated. -- @return #SPAWN self function SPAWN:InitLateActivated( LateActivated ) - self:F( ) + self:F() self.LateActivated = LateActivated or true - + return self end @@ -28410,47 +35107,45 @@ end -- @param #SPAWN self -- @param #string AirbaseName Name of the airbase. -- @param #number Takeoff (Optional) Takeoff type. Can be SPAWN.Takeoff.Hot (default), SPAWN.Takeoff.Cold or SPAWN.Takeoff.Runway. --- @param #number TerminalTyple (Optional) The terminal type. +-- @param #number TerminalType (Optional) The terminal type. -- @return #SPAWN self function SPAWN:InitAirbase( AirbaseName, Takeoff, TerminalType ) - self:F( ) + self:F() + + self.SpawnInitAirbase = AIRBASE:FindByName( AirbaseName ) + + self.SpawnInitTakeoff = Takeoff or SPAWN.Takeoff.Hot + + self.SpawnInitTerminalType = TerminalType - self.SpawnInitAirbase=AIRBASE:FindByName(AirbaseName) - - self.SpawnInitTakeoff=Takeoff or SPAWN.Takeoff.Hot - - self.SpawnInitTerminalType=TerminalType - return self end - ---- Defines the Heading for the new spawned units. +--- Defines the Heading for the new spawned units. -- The heading can be given as one fixed degree, or can be randomized between minimum and maximum degrees. -- @param #SPAWN self -- @param #number HeadingMin The minimum or fixed heading in degrees. -- @param #number HeadingMax (optional) The maximum heading in degrees. This there is no maximum heading, then the heading will be fixed for all units using minimum heading. -- @return #SPAWN self -- @usage --- --- Spawn = SPAWN:New( ... ) --- --- -- Spawn the units pointing to 100 degrees. --- Spawn:InitHeading( 100 ) --- --- -- Spawn the units pointing between 100 and 150 degrees. --- Spawn:InitHeading( 100, 150 ) --- +-- +-- Spawn = SPAWN:New( ... ) +-- +-- -- Spawn the units pointing to 100 degrees. +-- Spawn:InitHeading( 100 ) +-- +-- -- Spawn the units pointing between 100 and 150 degrees. +-- Spawn:InitHeading( 100, 150 ) +-- function SPAWN:InitHeading( HeadingMin, HeadingMax ) - self:F( ) + self:F() self.SpawnInitHeadingMin = HeadingMin self.SpawnInitHeadingMax = HeadingMax - + return self end - --- Defines the heading of the overall formation of the new spawned group. -- The heading can be given as one fixed degree, or can be randomized between minimum and maximum degrees. -- The Group's formation as laid out in its template will be rotated around the first unit in the group @@ -28462,116 +35157,114 @@ end -- @param #number unitVar (optional) Individual units within the group will have their heading randomized by +/- unitVar degrees. Default is zero. -- @return #SPAWN self -- @usage --- +-- -- mySpawner = SPAWN:New( ... ) --- --- -- Spawn the Group with the formation rotated +100 degrees around unit #1, compared to the mission template. --- mySpawner:InitGroupHeading( 100 ) --- --- Spawn the Group with the formation rotated units between +100 and +150 degrees around unit #1, compared to the mission template, and with individual units varying by +/- 10 degrees from their templated facing. --- mySpawner:InitGroupHeading( 100, 150, 10 ) --- --- Spawn the Group with the formation rotated -60 degrees around unit #1, compared to the mission template, but with all units facing due north regardless of how they were laid out in the template. --- mySpawner:InitGroupHeading(-60):InitHeading(0) --- or --- mySpawner:InitHeading(0):InitGroupHeading(-60) --- +-- +-- -- Spawn the Group with the formation rotated +100 degrees around unit #1, compared to the mission template. +-- mySpawner:InitGroupHeading( 100 ) +-- +-- -- Spawn the Group with the formation rotated units between +100 and +150 degrees around unit #1, compared to the mission template, and with individual units varying by +/- 10 degrees from their templated facing. +-- mySpawner:InitGroupHeading( 100, 150, 10 ) +-- +-- -- Spawn the Group with the formation rotated -60 degrees around unit #1, compared to the mission template, but with all units facing due north regardless of how they were laid out in the template. +-- mySpawner:InitGroupHeading(-60):InitHeading(0) +-- -- or +-- mySpawner:InitHeading(0):InitGroupHeading(-60) +-- function SPAWN:InitGroupHeading( HeadingMin, HeadingMax, unitVar ) - self:F({HeadingMin=HeadingMin, HeadingMax=HeadingMax, unitVar=unitVar}) + self:F( { HeadingMin = HeadingMin, HeadingMax = HeadingMax, unitVar = unitVar } ) self.SpawnInitGroupHeadingMin = HeadingMin self.SpawnInitGroupHeadingMax = HeadingMax - self.SpawnInitGroupUnitVar = unitVar + self.SpawnInitGroupUnitVar = unitVar return self end - --- Sets the coalition of the spawned group. Note that it might be necessary to also set the country explicitly! --- @param #SPAWN self +-- @param #SPAWN self -- @param DCS#coalition.side Coalition Coalition of the group as number of enumerator: --- --- * @{DCS#coaliton.side.NEUTRAL} --- * @{DCS#coaliton.side.RED} +-- +-- * @{DCS#coalition.side.NEUTRAL} +-- * @{DCS#coalition.side.RED} -- * @{DCS#coalition.side.BLUE} --- +-- -- @return #SPAWN self function SPAWN:InitCoalition( Coalition ) - self:F({coalition=Coalition}) + self:F( { coalition = Coalition } ) self.SpawnInitCoalition = Coalition - + return self end ---- Sets the country of the spawn group. Note that the country determins the coalition of the group depending on which country is defined to be on which side for each specific mission! --- @param #SPAWN self +--- Sets the country of the spawn group. Note that the country determines the coalition of the group depending on which country is defined to be on which side for each specific mission! +-- @param #SPAWN self -- @param #number Country Country id as number or enumerator: --- +-- -- * @{DCS#country.id.RUSSIA} -- * @{DCS#county.id.USA} --- +-- -- @return #SPAWN self function SPAWN:InitCountry( Country ) - self:F( ) + self:F() self.SpawnInitCountry = Country - + return self end - --- Sets category ID of the group. --- @param #SPAWN self +-- @param #SPAWN self -- @param #number Category Category id. -- @return #SPAWN self function SPAWN:InitCategory( Category ) - self:F( ) + self:F() self.SpawnInitCategory = Category - + return self end --- Sets livery of the group. --- @param #SPAWN self --- @param #string Livery Livery name. Note that this is not necessarily the same name as displayed in the mission edior. +-- @param #SPAWN self +-- @param #string Livery Livery name. Note that this is not necessarily the same name as displayed in the mission editor. -- @return #SPAWN self function SPAWN:InitLivery( Livery ) - self:F({livery=Livery} ) + self:F( { livery = Livery } ) self.SpawnInitLivery = Livery - + return self end --- Sets skill of the group. --- @param #SPAWN self +-- @param #SPAWN self -- @param #string Skill Skill, possible values "Average", "Good", "High", "Excellent" or "Random". -- @return #SPAWN self function SPAWN:InitSkill( Skill ) - self:F({skill=Skill}) - if Skill:lower()=="average" then - self.SpawnInitSkill="Average" - elseif Skill:lower()=="good" then - self.SpawnInitSkill="Good" - elseif Skill:lower()=="excellent" then - self.SpawnInitSkill="Excellent" - elseif Skill:lower()=="random" then - self.SpawnInitSkill="Random" + self:F( { skill = Skill } ) + if Skill:lower() == "average" then + self.SpawnInitSkill = "Average" + elseif Skill:lower() == "good" then + self.SpawnInitSkill = "Good" + elseif Skill:lower() == "excellent" then + self.SpawnInitSkill = "Excellent" + elseif Skill:lower() == "random" then + self.SpawnInitSkill = "Random" else - self.SpawnInitSkill="High" + self.SpawnInitSkill = "High" end - + return self end ---- Sets the radio comms on or off. Same as checking/unchecking the COMM box in the mission editor. --- @param #SPAWN self --- @param #number switch If true (or nil), enables the radio comms. If false, disables the radio for the spawned group. +--- Sets the radio communication on or off. Same as checking/unchecking the COMM box in the mission editor. +-- @param #SPAWN self +-- @param #number switch If true (or nil), enables the radio communication. If false, disables the radio for the spawned group. -- @return #SPAWN self -function SPAWN:InitRadioCommsOnOff(switch) - self:F({switch=switch} ) - self.SpawnInitRadio=switch or true +function SPAWN:InitRadioCommsOnOff( switch ) + self:F( { switch = switch } ) + self.SpawnInitRadio = switch or true return self end @@ -28579,11 +35272,11 @@ end -- @param #SPAWN self -- @param #number frequency The frequency in MHz. -- @return #SPAWN self -function SPAWN:InitRadioFrequency(frequency) - self:F({frequency=frequency} ) +function SPAWN:InitRadioFrequency( frequency ) + self:F( { frequency = frequency } ) + + self.SpawnInitFreq = frequency - self.SpawnInitFreq=frequency - return self end @@ -28591,64 +35284,65 @@ end -- @param #SPAWN self -- @param #string modulation Either "FM" or "AM". If no value is given, modulation is set to AM. -- @return #SPAWN self -function SPAWN:InitRadioModulation(modulation) - self:F({modulation=modulation}) - if modulation and modulation:lower()=="fm" then - self.SpawnInitModu=radio.modulation.FM +function SPAWN:InitRadioModulation( modulation ) + self:F( { modulation = modulation } ) + if modulation and modulation:lower() == "fm" then + self.SpawnInitModu = radio.modulation.FM else - self.SpawnInitModu=radio.modulation.AM + self.SpawnInitModu = radio.modulation.AM end return self end --- Sets the modex of the first unit of the group. If more units are in the group, the number is increased by one with every unit. --- @param #SPAWN self +-- @param #SPAWN self -- @param #number modex Modex of the first unit. -- @return #SPAWN self -function SPAWN:InitModex(modex) +function SPAWN:InitModex( modex ) if modex then - self.SpawnInitModex=tonumber(modex) + self.SpawnInitModex = tonumber( modex ) end - + return self end - ---- Randomizes the defined route of the SpawnTemplatePrefix group in the ME. This is very useful to define extra variation of the behaviour of groups. +--- Randomizes the defined route of the SpawnTemplatePrefix group in the ME. This is very useful to define extra variation of the behavior of groups. -- @param #SPAWN self --- @param #number SpawnStartPoint is the waypoint where the randomization begins. +-- @param #number SpawnStartPoint is the waypoint where the randomization begins. -- Note that the StartPoint = 0 equaling the point where the group is spawned. --- @param #number SpawnEndPoint is the waypoint where the randomization ends counting backwards. +-- @param #number SpawnEndPoint is the waypoint where the randomization ends counting backwards. -- This parameter is useful to avoid randomization to end at a waypoint earlier than the last waypoint on the route. -- @param #number SpawnRadius is the radius in meters in which the randomization of the new waypoints, with the original waypoint of the original template located in the middle ... -- @param #number SpawnHeight (optional) Specifies the **additional** height in meters that can be added to the base height specified at each waypoint in the ME. -- @return #SPAWN -- @usage --- -- NATO helicopters engaging in the battle field. --- -- The KA-50 has waypoints Start point ( =0 or SP ), 1, 2, 3, 4, End point (= 5 or DP). --- -- Waypoints 2 and 3 will only be randomized. The others will remain on their original position with each new spawn of the helicopter. --- -- The randomization of waypoint 2 and 3 will take place within a radius of 2000 meters. --- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):InitRandomizeRoute( 2, 2, 2000 ) +-- +-- -- NATO helicopters engaging in the battle field. +-- -- The KA-50 has waypoints Start point ( =0 or SP ), 1, 2, 3, 4, End point (= 5 or DP). +-- -- Waypoints 2 and 3 will only be randomized. The others will remain on their original position with each new spawn of the helicopter. +-- -- The randomization of waypoint 2 and 3 will take place within a radius of 2000 meters. +-- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):InitRandomizeRoute( 2, 2, 2000 ) +-- function SPAWN:InitRandomizeRoute( SpawnStartPoint, SpawnEndPoint, SpawnRadius, SpawnHeight ) - self:F( { self.SpawnTemplatePrefix, SpawnStartPoint, SpawnEndPoint, SpawnRadius, SpawnHeight } ) + self:F( { self.SpawnTemplatePrefix, SpawnStartPoint, SpawnEndPoint, SpawnRadius, SpawnHeight } ) - self.SpawnRandomizeRoute = true - self.SpawnRandomizeRouteStartPoint = SpawnStartPoint - self.SpawnRandomizeRouteEndPoint = SpawnEndPoint - self.SpawnRandomizeRouteRadius = SpawnRadius - self.SpawnRandomizeRouteHeight = SpawnHeight + self.SpawnRandomizeRoute = true + self.SpawnRandomizeRouteStartPoint = SpawnStartPoint + self.SpawnRandomizeRouteEndPoint = SpawnEndPoint + self.SpawnRandomizeRouteRadius = SpawnRadius + self.SpawnRandomizeRouteHeight = SpawnHeight - for GroupID = 1, self.SpawnMaxGroups do - self:_RandomizeRoute( GroupID ) - end - - return self + for GroupID = 1, self.SpawnMaxGroups do + self:_RandomizeRoute( GroupID ) + end + + return self end --- Randomizes the position of @{Wrapper.Group}s that are spawned within a **radius band**, given an Outer and Inner radius, from the point that the spawn happens. -- @param #SPAWN self --- @param #boolean RandomizePosition If true, SPAWN will perform the randomization of the @{Wrapper.Group}s position between a given outer and inner radius. +-- @param #boolean RandomizePosition If true, SPAWN will perform the randomization of the @{Wrapper.Group}s position between a given outer and inner radius. -- @param DCS#Distance OuterRadius (optional) The outer radius in meters where the new group will be spawned. -- @param DCS#Distance InnerRadius (optional) The inner radius in meters where the new group will NOT be spawned. -- @return #SPAWN @@ -28662,23 +35356,24 @@ function SPAWN:InitRandomizePosition( RandomizePosition, OuterRadius, InnerRadiu for GroupID = 1, self.SpawnMaxGroups do self:_RandomizeRoute( GroupID ) end - + return self end - --- Randomizes the UNITs that are spawned within a radius band given an Outer and Inner radius. -- @param #SPAWN self --- @param #boolean RandomizeUnits If true, SPAWN will perform the randomization of the @{UNIT}s position within the group between a given outer and inner radius. +-- @param #boolean RandomizeUnits If true, SPAWN will perform the randomization of the @{Wrapper.Unit#UNIT}s position within the group between a given outer and inner radius. -- @param DCS#Distance OuterRadius (optional) The outer radius in meters where the new group will be spawned. -- @param DCS#Distance InnerRadius (optional) The inner radius in meters where the new group will NOT be spawned. -- @return #SPAWN -- @usage --- -- NATO helicopters engaging in the battle field. --- -- The KA-50 has waypoints Start point ( =0 or SP ), 1, 2, 3, 4, End point (= 5 or DP). --- -- Waypoints 2 and 3 will only be randomized. The others will remain on their original position with each new spawn of the helicopter. --- -- The randomization of waypoint 2 and 3 will take place within a radius of 2000 meters. --- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):InitRandomizeRoute( 2, 2, 2000 ) +-- +-- -- NATO helicopters engaging in the battle field. +-- -- The KA-50 has waypoints Start point ( =0 or SP ), 1, 2, 3, 4, End point (= 5 or DP). +-- -- Waypoints 2 and 3 will only be randomized. The others will remain on their original position with each new spawn of the helicopter. +-- -- The randomization of waypoint 2 and 3 will take place within a radius of 2000 meters. +-- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):InitRandomizeRoute( 2, 2, 2000 ) +-- function SPAWN:InitRandomizeUnits( RandomizeUnits, OuterRadius, InnerRadius ) self:F( { self.SpawnTemplatePrefix, RandomizeUnits, OuterRadius, InnerRadius } ) @@ -28689,112 +35384,115 @@ function SPAWN:InitRandomizeUnits( RandomizeUnits, OuterRadius, InnerRadius ) for GroupID = 1, self.SpawnMaxGroups do self:_RandomizeRoute( GroupID ) end - + return self end --- This method is rather complicated to understand. But I'll try to explain. --- This method becomes useful when you need to spawn groups with random templates of groups defined within the mission editor, +-- This method becomes useful when you need to spawn groups with random templates of groups defined within the mission editor, -- but they will all follow the same Template route and have the same prefix name. -- In other words, this method randomizes between a defined set of groups the template to be used for each new spawn of a group. -- @param #SPAWN self --- @param #string SpawnTemplatePrefixTable A table with the names of the groups defined within the mission editor, from which one will be choosen when a new group will be spawned. +-- @param #string SpawnTemplatePrefixTable A table with the names of the groups defined within the mission editor, from which one will be chosen when a new group will be spawned. -- @return #SPAWN -- @usage --- -- NATO Tank Platoons invading Gori. --- -- Choose between 13 different 'US Tank Platoon' configurations for each new SPAWN the Group to be spawned for the --- -- 'US Tank Platoon Left', 'US Tank Platoon Middle' and 'US Tank Platoon Right' SpawnTemplatePrefixes. --- -- Each new SPAWN will randomize the route, with a defined time interval of 200 seconds with 40% time variation (randomization) and --- -- with a limit set of maximum 12 Units alive simulteneously and 150 Groups to be spawned during the whole mission. --- Spawn_US_Platoon = { 'US Tank Platoon 1', 'US Tank Platoon 2', 'US Tank Platoon 3', 'US Tank Platoon 4', 'US Tank Platoon 5', --- 'US Tank Platoon 6', 'US Tank Platoon 7', 'US Tank Platoon 8', 'US Tank Platoon 9', 'US Tank Platoon 10', --- 'US Tank Platoon 11', 'US Tank Platoon 12', 'US Tank Platoon 13' } --- Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) --- Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) --- Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) +-- +-- -- NATO Tank Platoons invading Gori. +-- -- Choose between 13 different 'US Tank Platoon' configurations for each new SPAWN the Group to be spawned for the +-- -- 'US Tank Platoon Left', 'US Tank Platoon Middle' and 'US Tank Platoon Right' SpawnTemplatePrefixes. +-- -- Each new SPAWN will randomize the route, with a defined time interval of 200 seconds with 40% time variation (randomization) and +-- -- with a limit set of maximum 12 Units alive simultaneously and 150 Groups to be spawned during the whole mission. +-- Spawn_US_Platoon = { 'US Tank Platoon 1', 'US Tank Platoon 2', 'US Tank Platoon 3', 'US Tank Platoon 4', 'US Tank Platoon 5', +-- 'US Tank Platoon 6', 'US Tank Platoon 7', 'US Tank Platoon 8', 'US Tank Platoon 9', 'US Tank Platoon 10', +-- 'US Tank Platoon 11', 'US Tank Platoon 12', 'US Tank Platoon 13' } +-- Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) function SPAWN:InitRandomizeTemplate( SpawnTemplatePrefixTable ) - self:F( { self.SpawnTemplatePrefix, SpawnTemplatePrefixTable } ) + self:F( { self.SpawnTemplatePrefix, SpawnTemplatePrefixTable } ) + + local temptable = {} + for _,_temp in pairs(SpawnTemplatePrefixTable) do + temptable[#temptable+1] = _temp + end + + self.SpawnTemplatePrefixTable = UTILS.ShuffleTable(temptable) + self.SpawnRandomizeTemplate = true - self.SpawnTemplatePrefixTable = SpawnTemplatePrefixTable - self.SpawnRandomizeTemplate = true + for SpawnGroupID = 1, self.SpawnMaxGroups do + self:_RandomizeTemplate( SpawnGroupID ) + end - for SpawnGroupID = 1, self.SpawnMaxGroups do - self:_RandomizeTemplate( SpawnGroupID ) - end - - return self + return self end - --- Randomize templates to be used as the unit representatives for the Spawned group, defined using a SET_GROUP object. --- This method becomes useful when you need to spawn groups with random templates of groups defined within the mission editor, +-- This method becomes useful when you need to spawn groups with random templates of groups defined within the mission editor, -- but they will all follow the same Template route and have the same prefix name. -- In other words, this method randomizes between a defined set of groups the template to be used for each new spawn of a group. -- @param #SPAWN self --- @param Core.Set#SET_GROUP SpawnTemplateSet A SET_GROUP object set, that contains the groups that are possible unit representatives of the group to be spawned. +-- @param Core.Set#SET_GROUP SpawnTemplateSet A SET_GROUP object set, that contains the groups that are possible unit representatives of the group to be spawned. -- @return #SPAWN -- @usage --- -- NATO Tank Platoons invading Gori. --- --- -- Choose between different 'US Tank Platoon Template' configurations to be spawned for the --- -- 'US Tank Platoon Left', 'US Tank Platoon Middle' and 'US Tank Platoon Right' SPAWN objects. --- --- -- Each new SPAWN will randomize the route, with a defined time interval of 200 seconds with 40% time variation (randomization) and --- -- with a limit set of maximum 12 Units alive simulteneously and 150 Groups to be spawned during the whole mission. --- --- Spawn_US_PlatoonSet = SET_GROUP:New():FilterPrefixes( "US Tank Platoon Templates" ):FilterOnce() --- --- --- Now use the Spawn_US_PlatoonSet to define the templates using InitRandomizeTemplateSet. --- Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplateSet( Spawn_US_PlatoonSet ):InitRandomizeRoute( 3, 3, 2000 ) --- Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplateSet( Spawn_US_PlatoonSet ):InitRandomizeRoute( 3, 3, 2000 ) --- Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplateSet( Spawn_US_PlatoonSet ):InitRandomizeRoute( 3, 3, 2000 ) -function SPAWN:InitRandomizeTemplateSet( SpawnTemplateSet ) -- R2.3 +-- +-- -- NATO Tank Platoons invading Gori. +-- +-- -- Choose between different 'US Tank Platoon Template' configurations to be spawned for the +-- -- 'US Tank Platoon Left', 'US Tank Platoon Middle' and 'US Tank Platoon Right' SPAWN objects. +-- +-- -- Each new SPAWN will randomize the route, with a defined time interval of 200 seconds with 40% time variation (randomization) and +-- -- with a limit set of maximum 12 Units alive simultaneously and 150 Groups to be spawned during the whole mission. +-- +-- Spawn_US_PlatoonSet = SET_GROUP:New():FilterPrefixes( "US Tank Platoon Templates" ):FilterOnce() +-- +-- -- Now use the Spawn_US_PlatoonSet to define the templates using InitRandomizeTemplateSet. +-- Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplateSet( Spawn_US_PlatoonSet ):InitRandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplateSet( Spawn_US_PlatoonSet ):InitRandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplateSet( Spawn_US_PlatoonSet ):InitRandomizeRoute( 3, 3, 2000 ) +-- +function SPAWN:InitRandomizeTemplateSet( SpawnTemplateSet ) self:F( { self.SpawnTemplatePrefix } ) - self.SpawnTemplatePrefixTable = SpawnTemplateSet:GetSetNames() - self.SpawnRandomizeTemplate = true - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self:_RandomizeTemplate( SpawnGroupID ) - end + local setnames = SpawnTemplateSet:GetSetNames() + self:InitRandomizeTemplate(setnames) return self end - --- Randomize templates to be used as the unit representatives for the Spawned group, defined by specifying the prefix names. --- This method becomes useful when you need to spawn groups with random templates of groups defined within the mission editor, +-- This method becomes useful when you need to spawn groups with random templates of groups defined within the mission editor, -- but they will all follow the same Template route and have the same prefix name. -- In other words, this method randomizes between a defined set of groups the template to be used for each new spawn of a group. -- @param #SPAWN self -- @param #string SpawnTemplatePrefixes A string or a list of string that contains the prefixes of the groups that are possible unit representatives of the group to be spawned. -- @return #SPAWN -- @usage --- -- NATO Tank Platoons invading Gori. --- --- -- Choose between different 'US Tank Platoon Templates' configurations to be spawned for the --- -- 'US Tank Platoon Left', 'US Tank Platoon Middle' and 'US Tank Platoon Right' SPAWN objects. --- --- -- Each new SPAWN will randomize the route, with a defined time interval of 200 seconds with 40% time variation (randomization) and --- -- with a limit set of maximum 12 Units alive simulteneously and 150 Groups to be spawned during the whole mission. --- --- Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplatePrefixes( "US Tank Platoon Templates" ):InitRandomizeRoute( 3, 3, 2000 ) --- Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplatePrefixes( "US Tank Platoon Templates" ):InitRandomizeRoute( 3, 3, 2000 ) --- Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplatePrefixes( "US Tank Platoon Templates" ):InitRandomizeRoute( 3, 3, 2000 ) -function SPAWN:InitRandomizeTemplatePrefixes( SpawnTemplatePrefixes ) --R2.3 +-- +-- -- NATO Tank Platoons invading Gori. +-- +-- -- Choose between different 'US Tank Platoon Templates' configurations to be spawned for the +-- -- 'US Tank Platoon Left', 'US Tank Platoon Middle' and 'US Tank Platoon Right' SPAWN objects. +-- +-- -- Each new SPAWN will randomize the route, with a defined time interval of 200 seconds with 40% time variation (randomization) and +-- -- with a limit set of maximum 12 Units alive simultaneously and 150 Groups to be spawned during the whole mission. +-- +-- Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplatePrefixes( "US Tank Platoon Templates" ):InitRandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplatePrefixes( "US Tank Platoon Templates" ):InitRandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplatePrefixes( "US Tank Platoon Templates" ):InitRandomizeRoute( 3, 3, 2000 ) +-- +function SPAWN:InitRandomizeTemplatePrefixes( SpawnTemplatePrefixes ) -- R2.3 self:F( { self.SpawnTemplatePrefix } ) local SpawnTemplateSet = SET_GROUP:New():FilterPrefixes( SpawnTemplatePrefixes ):FilterOnce() self:InitRandomizeTemplateSet( SpawnTemplateSet ) - + return self end - --- When spawning a new group, make the grouping of the units according the InitGrouping setting. -- @param #SPAWN self --- @param #number Grouping Indicates the maximum amount of units in the group. +-- @param #number Grouping Indicates the maximum amount of units in the group. -- @return #SPAWN function SPAWN:InitGrouping( Grouping ) -- R2.2 self:F( { self.SpawnTemplatePrefix, Grouping } ) @@ -28804,110 +35502,109 @@ function SPAWN:InitGrouping( Grouping ) -- R2.2 return self end - - --- This method provides the functionality to randomize the spawning of the Groups at a given list of zones of different types. -- @param #SPAWN self --- @param #table SpawnZoneTable A table with @{Zone} objects. If this table is given, then each spawn will be executed within the given list of @{Zone}s objects. +-- @param #table SpawnZoneTable A table with @{Core.Zone} objects. If this table is given, then each spawn will be executed within the given list of @{Core.Zone}s objects. -- @return #SPAWN -- @usage --- -- Create a zone table of the 2 zones. --- ZoneTable = { ZONE:New( "Zone1" ), ZONE:New( "Zone2" ) } --- --- Spawn_Vehicle_1 = SPAWN:New( "Spawn Vehicle 1" ) --- :InitLimit( 10, 10 ) --- :InitRandomizeRoute( 1, 1, 200 ) --- :InitRandomizeZones( ZoneTable ) --- :SpawnScheduled( 5, .5 ) --- +-- +-- -- Create a zone table of the 2 zones. +-- ZoneTable = { ZONE:New( "Zone1" ), ZONE:New( "Zone2" ) } +-- +-- Spawn_Vehicle_1 = SPAWN:New( "Spawn Vehicle 1" ) +-- :InitLimit( 10, 10 ) +-- :InitRandomizeRoute( 1, 1, 200 ) +-- :InitRandomizeZones( ZoneTable ) +-- :SpawnScheduled( 5, .5 ) +-- function SPAWN:InitRandomizeZones( SpawnZoneTable ) self:F( { self.SpawnTemplatePrefix, SpawnZoneTable } ) - - self.SpawnZoneTable = SpawnZoneTable + + local temptable = {} + for _,_temp in pairs(SpawnZoneTable) do + temptable[#temptable+1] = _temp + end + + self.SpawnZoneTable = UTILS.ShuffleTable(temptable) self.SpawnRandomizeZones = true for SpawnGroupID = 1, self.SpawnMaxGroups do self:_RandomizeZones( SpawnGroupID ) end - + return self end - - - - ---- For planes and helicopters, when these groups go home and land on their home airbases and farps, they normally would taxi to the parking spot, shut-down their engines and wait forever until the Group is removed by the runtime environment. --- This method is used to re-spawn automatically (so no extra call is needed anymore) the same group after it has landed. +--- For planes and helicopters, when these groups go home and land on their home airbases and FARPs, they normally would taxi to the parking spot, shut-down their engines and wait forever until the Group is removed by the runtime environment. +-- This method is used to re-spawn automatically (so no extra call is needed anymore) the same group after it has landed. -- This will enable a spawned group to be re-spawned after it lands, until it is destroyed... --- Note: When the group is respawned, it will re-spawn from the original airbase where it took off. +-- Note: When the group is respawned, it will re-spawn from the original airbase where it took off. -- So ensure that the routes for groups that respawn, always return to the original airbase, or players may get confused ... -- @param #SPAWN self -- @return #SPAWN self -- @usage --- -- RU Su-34 - AI Ship Attack --- -- Re-SPAWN the Group(s) after each landing and Engine Shut-Down automatically. --- SpawnRU_SU34 = SPAWN --- :New( 'Su-34' ) --- :Schedule( 2, 3, 1800, 0.4 ) --- :SpawnUncontrolled() --- :InitRandomizeRoute( 1, 1, 3000 ) --- :InitRepeatOnEngineShutDown() --- +-- +-- -- RU Su-34 - AI Ship Attack +-- -- Re-SPAWN the Group(s) after each landing and Engine Shut-Down automatically. +-- SpawnRU_SU34 = SPAWN:New( 'Su-34' ) +-- :Schedule( 2, 3, 1800, 0.4 ) +-- :SpawnUncontrolled() +-- :InitRandomizeRoute( 1, 1, 3000 ) +-- :InitRepeatOnEngineShutDown() +-- function SPAWN:InitRepeat() - self:F( { self.SpawnTemplatePrefix, self.SpawnIndex } ) + self:F( { self.SpawnTemplatePrefix, self.SpawnIndex } ) - self.Repeat = true - self.RepeatOnEngineShutDown = false - self.RepeatOnLanding = true + self.Repeat = true + self.RepeatOnEngineShutDown = false + self.RepeatOnLanding = true - return self + return self end --- Respawn group after landing. -- @param #SPAWN self -- @return #SPAWN self -- @usage --- -- RU Su-34 - AI Ship Attack --- -- Re-SPAWN the Group(s) after each landing and Engine Shut-Down automatically. --- SpawnRU_SU34 = SPAWN --- :New( 'Su-34' ) --- :InitRandomizeRoute( 1, 1, 3000 ) --- :InitRepeatOnLanding() --- :Spawn() +-- +-- -- RU Su-34 - AI Ship Attack +-- -- Re-SPAWN the Group(s) after each landing and Engine Shut-Down automatically. +-- SpawnRU_SU34 = SPAWN:New( 'Su-34' ) +-- :InitRandomizeRoute( 1, 1, 3000 ) +-- :InitRepeatOnLanding() +-- :Spawn() +-- function SPAWN:InitRepeatOnLanding() - self:F( { self.SpawnTemplatePrefix } ) + self:F( { self.SpawnTemplatePrefix } ) - self:InitRepeat() - self.RepeatOnEngineShutDown = false - self.RepeatOnLanding = true - - return self -end + self:InitRepeat() + self.RepeatOnEngineShutDown = false + self.RepeatOnLanding = true + return self +end --- Respawn after landing when its engines have shut down. -- @param #SPAWN self -- @return #SPAWN self -- @usage --- -- RU Su-34 - AI Ship Attack --- -- Re-SPAWN the Group(s) after each landing and Engine Shut-Down automatically. --- SpawnRU_SU34 = SPAWN --- :New( 'Su-34' ) --- :SpawnUncontrolled() --- :InitRandomizeRoute( 1, 1, 3000 ) --- :InitRepeatOnEngineShutDown() --- :Spawn() +-- +-- -- RU Su-34 - AI Ship Attack +-- -- Re-SPAWN the Group(s) after each landing and Engine Shut-Down automatically. +-- SpawnRU_SU34 = SPAWN:New( 'Su-34' ) +-- :SpawnUncontrolled() +-- :InitRandomizeRoute( 1, 1, 3000 ) +-- :InitRepeatOnEngineShutDown() +-- :Spawn() function SPAWN:InitRepeatOnEngineShutDown() - self:F( { self.SpawnTemplatePrefix } ) + self:F( { self.SpawnTemplatePrefix } ) - self:InitRepeat() - self.RepeatOnEngineShutDown = true - self.RepeatOnLanding = false - - return self -end + self:InitRepeat() + self.RepeatOnEngineShutDown = true + self.RepeatOnLanding = false + return self +end --- Delete groups that have not moved for X seconds - AIR ONLY!!! -- DO NOT USE ON GROUPS THAT DO NOT MOVE OR YOUR SERVER WILL BURN IN HELL (Pikes - April 2020) @@ -28915,25 +35612,25 @@ end -- @param #SPAWN self -- @param #string SpawnCleanUpInterval The interval to check for inactive groups within seconds. -- @return #SPAWN self --- @usage --- Spawn_Helicopter:InitCleanUp( 20 ) -- CleanUp the spawning of the helicopters every 20 seconds when they become inactive. +-- @usage +-- +-- Spawn_Helicopter:InitCleanUp( 20 ) -- CleanUp the spawning of the helicopters every 20 seconds when they become inactive. +-- function SPAWN:InitCleanUp( SpawnCleanUpInterval ) - self:F( { self.SpawnTemplatePrefix, SpawnCleanUpInterval } ) + self:F( { self.SpawnTemplatePrefix, SpawnCleanUpInterval } ) - self.SpawnCleanUpInterval = SpawnCleanUpInterval - self.SpawnCleanUpTimeStamps = {} + self.SpawnCleanUpInterval = SpawnCleanUpInterval + self.SpawnCleanUpTimeStamps = {} local SpawnGroup, SpawnCursor = self:GetFirstAliveGroup() self:T( { "CleanUp Scheduler:", SpawnGroup } ) - - --self.CleanUpFunction = routines.scheduleFunction( self._SpawnCleanUpScheduler, { self }, timer.getTime() + 1, SpawnCleanUpInterval ) - self.CleanUpScheduler = SCHEDULER:New( self, self._SpawnCleanUpScheduler, {}, 1, SpawnCleanUpInterval, 0.2 ) - return self -end - + -- self.CleanUpFunction = routines.scheduleFunction( self._SpawnCleanUpScheduler, { self }, timer.getTime() + 1, SpawnCleanUpInterval ) + self.CleanUpScheduler = SCHEDULER:New( self, self._SpawnCleanUpScheduler, {}, 1, SpawnCleanUpInterval, 0.2 ) + return self +end ---- Makes the groups visible before start (like a batallion). +--- Makes the groups visible before start (like a battalion). -- The method will take the position of the group as the first position in the array. -- CAUTION: this directive will NOT work with OnSpawnGroup function. -- @param #SPAWN self @@ -28943,45 +35640,45 @@ end -- @param #number SpawnDeltaY The space between each Group on the Y-axis. -- @return #SPAWN self -- @usage --- -- Define an array of Groups. --- Spawn_BE_Ground = SPAWN --- :New( 'BE Ground' ) --- :InitLimit( 2, 24 ) --- :InitArray( 90, 10, 100, 50 ) --- +-- +-- -- Define an array of Groups. +-- Spawn_BE_Ground = SPAWN:New( 'BE Ground' ) +-- :InitLimit( 2, 24 ) +-- :InitArray( 90, 10, 100, 50 ) +-- function SPAWN:InitArray( SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY ) - self:F( { self.SpawnTemplatePrefix, SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY } ) + self:F( { self.SpawnTemplatePrefix, SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY } ) - self.SpawnVisible = true -- When the first Spawn executes, all the Groups need to be made visible before start. - - local SpawnX = 0 - local SpawnY = 0 - local SpawnXIndex = 0 - local SpawnYIndex = 0 - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self:T( { SpawnX, SpawnY, SpawnXIndex, SpawnYIndex } ) + self.SpawnVisible = true -- When the first Spawn executes, all the Groups need to be made visible before start. - self.SpawnGroups[SpawnGroupID].Visible = true - self.SpawnGroups[SpawnGroupID].Spawned = false - - SpawnXIndex = SpawnXIndex + 1 - if SpawnWidth and SpawnWidth ~= 0 then - if SpawnXIndex >= SpawnWidth then - SpawnXIndex = 0 - SpawnYIndex = SpawnYIndex + 1 - end - end + local SpawnX = 0 + local SpawnY = 0 + local SpawnXIndex = 0 + local SpawnYIndex = 0 - local SpawnRootX = self.SpawnGroups[SpawnGroupID].SpawnTemplate.x - local SpawnRootY = self.SpawnGroups[SpawnGroupID].SpawnTemplate.y - - self:_TranslateRotate( SpawnGroupID, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle ) - - self.SpawnGroups[SpawnGroupID].SpawnTemplate.lateActivation = true - self.SpawnGroups[SpawnGroupID].SpawnTemplate.visible = true - - self.SpawnGroups[SpawnGroupID].Visible = true + for SpawnGroupID = 1, self.SpawnMaxGroups do + self:T( { SpawnX, SpawnY, SpawnXIndex, SpawnYIndex } ) + + self.SpawnGroups[SpawnGroupID].Visible = true + self.SpawnGroups[SpawnGroupID].Spawned = false + + SpawnXIndex = SpawnXIndex + 1 + if SpawnWidth and SpawnWidth ~= 0 then + if SpawnXIndex >= SpawnWidth then + SpawnXIndex = 0 + SpawnYIndex = SpawnYIndex + 1 + end + end + + local SpawnRootX = self.SpawnGroups[SpawnGroupID].SpawnTemplate.x + local SpawnRootY = self.SpawnGroups[SpawnGroupID].SpawnTemplate.y + + self:_TranslateRotate( SpawnGroupID, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle ) + + self.SpawnGroups[SpawnGroupID].SpawnTemplate.lateActivation = true + self.SpawnGroups[SpawnGroupID].SpawnTemplate.visible = true + + self.SpawnGroups[SpawnGroupID].Visible = true self:HandleEvent( EVENTS.Birth, self._OnBirth ) self:HandleEvent( EVENTS.Dead, self._OnDeadOrCrash ) @@ -28994,40 +35691,41 @@ function SPAWN:InitArray( SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY ) if self.RepeatOnEngineShutDown then self:HandleEvent( EVENTS.EngineShutdown, self._OnEngineShutDown ) end - - self.SpawnGroups[SpawnGroupID].Group = _DATABASE:Spawn( self.SpawnGroups[SpawnGroupID].SpawnTemplate ) - SpawnX = SpawnXIndex * SpawnDeltaX - SpawnY = SpawnYIndex * SpawnDeltaY - end - - return self + self.SpawnGroups[SpawnGroupID].Group = _DATABASE:Spawn( self.SpawnGroups[SpawnGroupID].SpawnTemplate ) + + SpawnX = SpawnXIndex * SpawnDeltaX + SpawnY = SpawnYIndex * SpawnDeltaY + end + + return self end do -- AI methods + --- Turns the AI On or Off for the @{Wrapper.Group} when spawning. -- @param #SPAWN self -- @param #boolean AIOnOff A value of true sets the AI On, a value of false sets the AI Off. -- @return #SPAWN The SPAWN object function SPAWN:InitAIOnOff( AIOnOff ) - + self.AIOnOff = AIOnOff return self end - + --- Turns the AI On for the @{Wrapper.Group} when spawning. -- @param #SPAWN self -- @return #SPAWN The SPAWN object function SPAWN:InitAIOn() - + return self:InitAIOnOff( true ) end - + --- Turns the AI Off for the @{Wrapper.Group} when spawning. -- @param #SPAWN self -- @return #SPAWN The SPAWN object function SPAWN:InitAIOff() - + return self:InitAIOnOff( false ) end @@ -29040,82 +35738,81 @@ do -- Delay methods -- @param #boolean DelayOnOff A value of true sets the Delay On, a value of false sets the Delay Off. -- @return #SPAWN The SPAWN object function SPAWN:InitDelayOnOff( DelayOnOff ) - + self.DelayOnOff = DelayOnOff return self end - + --- Turns the Delay On for the @{Wrapper.Group} when spawning. -- @param #SPAWN self -- @return #SPAWN The SPAWN object function SPAWN:InitDelayOn() - + return self:InitDelayOnOff( true ) end - + --- Turns the Delay Off for the @{Wrapper.Group} when spawning. -- @param #SPAWN self -- @return #SPAWN The SPAWN object function SPAWN:InitDelayOff() - + return self:InitDelayOnOff( false ) end end -- Delay methods --- Will spawn a group based on the internal index. --- Note: Uses @{DATABASE} module defined in MOOSE. +-- Note: This method uses the global _DATABASE object (an instance of @{Core.Database#DATABASE}), which contains ALL initial and new spawned objects in MOOSE. -- @param #SPAWN self -- @return Wrapper.Group#GROUP The group that was spawned. You can use this group for further actions. function SPAWN:Spawn() - self:F( { self.SpawnTemplatePrefix, self.SpawnIndex, self.AliveUnits } ) + self:F( { self.SpawnTemplatePrefix, self.SpawnIndex, self.AliveUnits } ) if self.SpawnInitAirbase then - return self:SpawnAtAirbase(self.SpawnInitAirbase, self.SpawnInitTakeoff, nil, self.SpawnInitTerminalType) + return self:SpawnAtAirbase( self.SpawnInitAirbase, self.SpawnInitTakeoff, nil, self.SpawnInitTerminalType ) else - return self:SpawnWithIndex( self.SpawnIndex + 1 ) - end - + return self:SpawnWithIndex( self.SpawnIndex + 1 ) + end + end --- Will re-spawn a group based on a given index. --- Note: Uses @{DATABASE} module defined in MOOSE. +-- Note: This method uses the global _DATABASE object (an instance of @{Core.Database#DATABASE}), which contains ALL initial and new spawned objects in MOOSE. -- @param #SPAWN self -- @param #string SpawnIndex The index of the group to be spawned. -- @return Wrapper.Group#GROUP The group that was spawned. You can use this group for further actions. function SPAWN:ReSpawn( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) - - if not SpawnIndex then - SpawnIndex = 1 - end + self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) + + if not SpawnIndex then + SpawnIndex = 1 + end --- TODO: This logic makes DCS crash and i don't know why (yet). -- ED (Pikes -- not in the least bit scary to see this, right?) - local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) - local WayPoints = SpawnGroup and SpawnGroup.WayPoints or nil - if SpawnGroup then + -- TODO: This logic makes DCS crash and i don't know why (yet). -- ED (Pikes -- not in the least bit scary to see this, right?) + local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) + local WayPoints = SpawnGroup and SpawnGroup.WayPoints or nil + if SpawnGroup then local SpawnDCSGroup = SpawnGroup:GetDCSObject() - if SpawnDCSGroup then + if SpawnDCSGroup then SpawnGroup:Destroy() - end + end end - local SpawnGroup = self:SpawnWithIndex( SpawnIndex ) - if SpawnGroup and WayPoints then - -- If there were WayPoints set, then Re-Execute those WayPoints! - SpawnGroup:WayPointInitialize( WayPoints ) - SpawnGroup:WayPointExecute( 1, 5 ) - end - - if SpawnGroup.ReSpawnFunction then - SpawnGroup:ReSpawnFunction() - end - - SpawnGroup:ResetEvents() - - return SpawnGroup -end + local SpawnGroup = self:SpawnWithIndex( SpawnIndex ) + if SpawnGroup and WayPoints then + -- If there were WayPoints set, then Re-Execute those WayPoints! + SpawnGroup:WayPointInitialize( WayPoints ) + SpawnGroup:WayPointExecute( 1, 5 ) + end + if SpawnGroup.ReSpawnFunction then + SpawnGroup:ReSpawnFunction() + end + + SpawnGroup:ResetEvents() + + return SpawnGroup +end --- Set the spawn index to a specified index number. -- This method can be used to "reset" the spawn counter to a specific index number. @@ -29127,23 +35824,22 @@ function SPAWN:SetSpawnIndex( SpawnIndex ) self.SpawnIndex = SpawnIndex or 0 end - --- Will spawn a group with a specified index number. --- Uses @{DATABASE} global object defined in MOOSE. +-- Note: This method uses the global _DATABASE object (an instance of @{Core.Database#DATABASE}), which contains ALL initial and new spawned objects in MOOSE. -- @param #SPAWN self -- @param #string SpawnIndex The index of the group to be spawned. -- @return Wrapper.Group#GROUP The group that was spawned. You can use this group for further actions. function SPAWN:SpawnWithIndex( SpawnIndex, NoBirth ) - self:F2( { SpawnTemplatePrefix = self.SpawnTemplatePrefix, SpawnIndex = SpawnIndex, AliveUnits = self.AliveUnits, SpawnMaxGroups = self.SpawnMaxGroups } ) - - if self:_GetSpawnIndex( SpawnIndex ) then - - if self.SpawnGroups[self.SpawnIndex].Visible then - self.SpawnGroups[self.SpawnIndex].Group:Activate() - else + self:F2( { SpawnTemplatePrefix = self.SpawnTemplatePrefix, SpawnIndex = SpawnIndex, AliveUnits = self.AliveUnits, SpawnMaxGroups = self.SpawnMaxGroups } ) + + if self:_GetSpawnIndex( SpawnIndex ) then + + if self.SpawnGroups[self.SpawnIndex].Visible then + self.SpawnGroups[self.SpawnIndex].Group:Activate() + else - local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate - self:T( SpawnTemplate.name ) + local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate + self:T( SpawnTemplate.name ) if SpawnTemplate then @@ -29158,46 +35854,46 @@ function SPAWN:SpawnWithIndex( SpawnIndex, NoBirth ) SpawnTemplate.x = RandomVec2.x SpawnTemplate.y = RandomVec2.y for UnitID = 1, #SpawnTemplate.units do - SpawnTemplate.units[UnitID].x = SpawnTemplate.units[UnitID].x + ( RandomVec2.x - CurrentX ) - SpawnTemplate.units[UnitID].y = SpawnTemplate.units[UnitID].y + ( RandomVec2.y - CurrentY ) - self:T( 'SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) + SpawnTemplate.units[UnitID].x = SpawnTemplate.units[UnitID].x + (RandomVec2.x - CurrentX) + SpawnTemplate.units[UnitID].y = SpawnTemplate.units[UnitID].y + (RandomVec2.y - CurrentY) + self:T( 'SpawnTemplate.units[' .. UnitID .. '].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units[' .. UnitID .. '].y = ' .. SpawnTemplate.units[UnitID].y ) end end - + -- If RandomizeUnits, then Randomize the formation at the start point. if self.SpawnRandomizeUnits then for UnitID = 1, #SpawnTemplate.units do local RandomVec2 = PointVec3:GetRandomVec2InRadius( self.SpawnOuterRadius, self.SpawnInnerRadius ) SpawnTemplate.units[UnitID].x = RandomVec2.x SpawnTemplate.units[UnitID].y = RandomVec2.y - self:T( 'SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) + self:T( 'SpawnTemplate.units[' .. UnitID .. '].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units[' .. UnitID .. '].y = ' .. SpawnTemplate.units[UnitID].y ) end end - + -- Get correct heading in Radians. - local function _Heading(courseDeg) + local function _Heading( courseDeg ) local h - if courseDeg<=180 then - h=math.rad(courseDeg) + if courseDeg <= 180 then + h = math.rad( courseDeg ) else - h=-math.rad(360-courseDeg) + h = -math.rad( 360 - courseDeg ) end - return h - end + return h + end - local Rad180 = math.rad(180) - local function _HeadingRad(courseRad) - if courseRad<=Rad180 then + local Rad180 = math.rad( 180 ) + local function _HeadingRad( courseRad ) + if courseRad <= Rad180 then return courseRad else - return -((2*Rad180)-courseRad) + return -((2 * Rad180) - courseRad) end - end + end -- Generate a random value somewhere between two floating point values. - local function _RandomInRange ( min, max ) + local function _RandomInRange( min, max ) if min and max then - return min + ( math.random()*(max-min) ) + return min + (math.random() * (max - min)) else return min end @@ -29211,39 +35907,39 @@ function SPAWN:SpawnWithIndex( SpawnIndex, NoBirth ) local pivotX = SpawnTemplate.units[1].x -- unit #1 is the pivot point local pivotY = SpawnTemplate.units[1].y - local headingRad = math.rad(_RandomInRange(self.SpawnInitGroupHeadingMin or 0,self.SpawnInitGroupHeadingMax)) - local cosHeading = math.cos(headingRad) - local sinHeading = math.sin(headingRad) - - local unitVarRad = math.rad(self.SpawnInitGroupUnitVar or 0) + local headingRad = math.rad( _RandomInRange( self.SpawnInitGroupHeadingMin or 0, self.SpawnInitGroupHeadingMax ) ) + local cosHeading = math.cos( headingRad ) + local sinHeading = math.sin( headingRad ) + + local unitVarRad = math.rad( self.SpawnInitGroupUnitVar or 0 ) for UnitID = 1, #SpawnTemplate.units do - + if UnitID > 1 then -- don't rotate position of unit #1 local unitXOff = SpawnTemplate.units[UnitID].x - pivotX -- rotate position offset around unit #1 local unitYOff = SpawnTemplate.units[UnitID].y - pivotY - SpawnTemplate.units[UnitID].x = pivotX + (unitXOff*cosHeading) - (unitYOff*sinHeading) - SpawnTemplate.units[UnitID].y = pivotY + (unitYOff*cosHeading) + (unitXOff*sinHeading) + SpawnTemplate.units[UnitID].x = pivotX + (unitXOff * cosHeading) - (unitYOff * sinHeading) + SpawnTemplate.units[UnitID].y = pivotY + (unitYOff * cosHeading) + (unitXOff * sinHeading) end - + -- adjust heading of all units, including unit #1 local unitHeading = SpawnTemplate.units[UnitID].heading + headingRad -- add group rotation to units default rotation - SpawnTemplate.units[UnitID].heading = _HeadingRad(_RandomInRange(unitHeading-unitVarRad, unitHeading+unitVarRad)) + SpawnTemplate.units[UnitID].heading = _HeadingRad( _RandomInRange( unitHeading - unitVarRad, unitHeading + unitVarRad ) ) SpawnTemplate.units[UnitID].psi = -SpawnTemplate.units[UnitID].heading - + end end - + -- If Heading is given, point all the units towards the given Heading. Overrides any heading set in InitGroupHeading above. if self.SpawnInitHeadingMin then for UnitID = 1, #SpawnTemplate.units do - SpawnTemplate.units[UnitID].heading = _Heading(_RandomInRange(self.SpawnInitHeadingMin, self.SpawnInitHeadingMax)) + SpawnTemplate.units[UnitID].heading = _Heading( _RandomInRange( self.SpawnInitHeadingMin, self.SpawnInitHeadingMax ) ) SpawnTemplate.units[UnitID].psi = -SpawnTemplate.units[UnitID].heading end end - + -- Set livery. if self.SpawnInitLivery then for UnitID = 1, #SpawnTemplate.units do @@ -29261,39 +35957,38 @@ function SPAWN:SpawnWithIndex( SpawnIndex, NoBirth ) -- Set tail number. if self.SpawnInitModex then for UnitID = 1, #SpawnTemplate.units do - SpawnTemplate.units[UnitID].onboard_num = string.format("%03d", self.SpawnInitModex+(UnitID-1)) + SpawnTemplate.units[UnitID].onboard_num = string.format( "%03d", self.SpawnInitModex + (UnitID - 1) ) end end - + -- Set radio comms on/off. if self.SpawnInitRadio then - SpawnTemplate.communication=self.SpawnInitRadio - end - + SpawnTemplate.communication = self.SpawnInitRadio + end + -- Set radio frequency. if self.SpawnInitFreq then - SpawnTemplate.frequency=self.SpawnInitFreq + SpawnTemplate.frequency = self.SpawnInitFreq end - + -- Set radio modulation. if self.SpawnInitModu then - SpawnTemplate.modulation=self.SpawnInitModu - end - - -- Set country, coaliton and categroy. - SpawnTemplate.CategoryID = self.SpawnInitCategory or SpawnTemplate.CategoryID - SpawnTemplate.CountryID = self.SpawnInitCountry or SpawnTemplate.CountryID - SpawnTemplate.CoalitionID = self.SpawnInitCoalition or SpawnTemplate.CoalitionID - - --- if SpawnTemplate.CategoryID == Group.Category.HELICOPTER or SpawnTemplate.CategoryID == Group.Category.AIRPLANE then --- if SpawnTemplate.route.points[1].type == "TakeOffParking" then --- SpawnTemplate.uncontrolled = self.SpawnUnControlled --- end --- end + SpawnTemplate.modulation = self.SpawnInitModu + end + + -- Set country, coalition and category. + SpawnTemplate.CategoryID = self.SpawnInitCategory or SpawnTemplate.CategoryID + SpawnTemplate.CountryID = self.SpawnInitCountry or SpawnTemplate.CountryID + SpawnTemplate.CoalitionID = self.SpawnInitCoalition or SpawnTemplate.CoalitionID + + -- if SpawnTemplate.CategoryID == Group.Category.HELICOPTER or SpawnTemplate.CategoryID == Group.Category.AIRPLANE then + -- if SpawnTemplate.route.points[1].type == "TakeOffParking" then + -- SpawnTemplate.uncontrolled = self.SpawnUnControlled + -- end + -- end end - - if not NoBirth then + + if not NoBirth then self:HandleEvent( EVENTS.Birth, self._OnBirth ) end self:HandleEvent( EVENTS.Dead, self._OnDeadOrCrash ) @@ -29307,37 +36002,36 @@ function SPAWN:SpawnWithIndex( SpawnIndex, NoBirth ) self:HandleEvent( EVENTS.EngineShutdown, self._OnEngineShutDown ) end - self.SpawnGroups[self.SpawnIndex].Group = _DATABASE:Spawn( SpawnTemplate ) - - local SpawnGroup = self.SpawnGroups[self.SpawnIndex].Group -- Wrapper.Group#GROUP - - --TODO: Need to check if this function doesn't need to be scheduled, as the group may not be immediately there! + self.SpawnGroups[self.SpawnIndex].Group = _DATABASE:Spawn( SpawnTemplate ) + + local SpawnGroup = self.SpawnGroups[self.SpawnIndex].Group -- Wrapper.Group#GROUP + + -- TODO: Need to check if this function doesn't need to be scheduled, as the group may not be immediately there! if SpawnGroup then - - SpawnGroup:SetAIOnOff( self.AIOnOff ) - end + + SpawnGroup:SetAIOnOff( self.AIOnOff ) + end self:T3( SpawnTemplate.name ) - - -- If there is a SpawnFunction hook defined, call it. - if self.SpawnFunctionHook then - -- delay calling this for .1 seconds so that it hopefully comes after the BIRTH event of the group. - self.SpawnHookScheduler:Schedule( nil, self.SpawnFunctionHook, { self.SpawnGroups[self.SpawnIndex].Group, unpack( self.SpawnFunctionArguments)}, 0.1 ) - end - -- TODO: Need to fix this by putting an "R" in the name of the group when the group repeats. - --if self.Repeat then - -- _DATABASE:SetStatusGroup( SpawnTemplate.name, "ReSpawn" ) - --end - end - - - self.SpawnGroups[self.SpawnIndex].Spawned = true - return self.SpawnGroups[self.SpawnIndex].Group - else - --self:E( { self.SpawnTemplatePrefix, "No more Groups to Spawn:", SpawnIndex, self.SpawnMaxGroups } ) - end - return nil + -- If there is a SpawnFunction hook defined, call it. + if self.SpawnFunctionHook then + -- delay calling this for .1 seconds so that it hopefully comes after the BIRTH event of the group. + self.SpawnHookScheduler:Schedule( nil, self.SpawnFunctionHook, { self.SpawnGroups[self.SpawnIndex].Group, unpack( self.SpawnFunctionArguments ) }, 0.1 ) + end + -- TODO: Need to fix this by putting an "R" in the name of the group when the group repeats. + -- if self.Repeat then + -- _DATABASE:SetStatusGroup( SpawnTemplate.name, "ReSpawn" ) + -- end + end + + self.SpawnGroups[self.SpawnIndex].Spawned = true + return self.SpawnGroups[self.SpawnIndex].Group + else + -- self:E( { self.SpawnTemplatePrefix, "No more Groups to Spawn:", SpawnIndex, self.SpawnMaxGroups } ) + end + + return nil end --- Spawns new groups at varying time intervals. @@ -29345,29 +36039,29 @@ end -- @param #SPAWN self -- @param #number SpawnTime The time interval defined in seconds between each new spawn of new groups. -- @param #number SpawnTimeVariation The variation to be applied on the defined time interval between each new spawn. --- The variation is a number between 0 and 1, representing the %-tage of variation to be applied on the time interval. +-- The variation is a number between 0 and 1, representing the % of variation to be applied on the time interval. -- @return #SPAWN self -- @usage -- -- NATO helicopters engaging in the battle field. -- -- The time interval is set to SPAWN new helicopters between each 600 seconds, with a time variation of 50%. --- -- The time variation in this case will be between 450 seconds and 750 seconds. --- -- This is calculated as follows: --- -- Low limit: 600 * ( 1 - 0.5 / 2 ) = 450 +-- -- The time variation in this case will be between 450 seconds and 750 seconds. +-- -- This is calculated as follows: +-- -- Low limit: 600 * ( 1 - 0.5 / 2 ) = 450 -- -- High limit: 600 * ( 1 + 0.5 / 2 ) = 750 --- -- Between these two values, a random amount of seconds will be choosen for each new spawn of the helicopters. +-- -- Between these two values, a random amount of seconds will be chosen for each new spawn of the helicopters. -- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):SpawnScheduled( 600, 0.5 ) function SPAWN:SpawnScheduled( SpawnTime, SpawnTimeVariation ) - self:F( { SpawnTime, SpawnTimeVariation } ) + self:F( { SpawnTime, SpawnTimeVariation } ) - if SpawnTime ~= nil and SpawnTimeVariation ~= nil then - local InitialDelay = 0 - if self.DelayOnOff == true then - InitialDelay = math.random( SpawnTime - SpawnTime * SpawnTimeVariation, SpawnTime + SpawnTime * SpawnTimeVariation ) - end + if SpawnTime ~= nil and SpawnTimeVariation ~= nil then + local InitialDelay = 0 + if self.DelayOnOff == true then + InitialDelay = math.random( SpawnTime - SpawnTime * SpawnTimeVariation, SpawnTime + SpawnTime * SpawnTimeVariation ) + end self.SpawnScheduler = SCHEDULER:New( self, self._Scheduler, {}, InitialDelay, SpawnTime, SpawnTimeVariation ) - end + end - return self + return self end --- Will re-start the spawning scheduler. @@ -29386,12 +36080,11 @@ end -- @return #SPAWN function SPAWN:SpawnScheduleStop() self:F( { self.SpawnTemplatePrefix } ) - + self.SpawnScheduler:Stop() return self end - --- Allows to place a CallFunction hook when a new group spawns. -- The provided method will be called when a new group is spawned, including its given parameters. -- The first parameter of the SpawnFunction is the @{Wrapper.Group#GROUP} that was spawned. @@ -29400,17 +36093,16 @@ end -- @param SpawnFunctionArguments A random amount of arguments to be provided to the function when the group spawns. -- @return #SPAWN -- @usage --- -- Declare SpawnObject and call a function when a new Group is spawned. --- local SpawnObject = SPAWN --- :New( "SpawnObject" ) --- :InitLimit( 2, 10 ) --- :OnSpawnGroup( --- function( SpawnGroup ) --- SpawnGroup:E( "I am spawned" ) --- end --- ) --- :SpawnScheduled( 300, 0.3 ) --- +-- +-- -- Declare SpawnObject and call a function when a new Group is spawned. +-- local SpawnObject = SPAWN:New( "SpawnObject" ) +-- :InitLimit( 2, 10 ) +-- :OnSpawnGroup( function( SpawnGroup ) +-- SpawnGroup:E( "I am spawned" ) +-- end +-- ) +-- :SpawnScheduled( 300, 0.3 ) +-- function SPAWN:OnSpawnGroup( SpawnCallBackFunction, ... ) self:F( "OnSpawnGroup" ) @@ -29418,109 +36110,110 @@ function SPAWN:OnSpawnGroup( SpawnCallBackFunction, ... ) self.SpawnFunctionArguments = {} if arg then self.SpawnFunctionArguments = arg - end + end return self end ---- Will spawn a group at an @{Wrapper.Airbase}. +--- Will spawn a group at an @{Wrapper.Airbase}. -- This method is mostly advisable to be used if you want to simulate spawning units at an airbase. -- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. -- You can use the returned group to further define the route to be followed. --- +-- -- The @{Wrapper.Airbase#AIRBASE} object must refer to a valid airbase known in the sim. -- You can use the following enumerations to search for the pre-defined airbases on the current known maps of DCS: --- --- * @{Wrapper.Airbase#AIRBASE.Caucasus}: The airbases on the Caucasus map. --- * @{Wrapper.Airbase#AIRBASE.Nevada}: The airbases on the Nevada (NTTR) map. --- * @{Wrapper.Airbase#AIRBASE.Normandy}: The airbases on the Normandy map. --- --- Use the method @{Wrapper.Airbase#AIRBASE.FindByName}() to retrieve the airbase object. +-- +-- * @{Wrapper.Airbase#AIRBASE.Caucasus}: The airbases on the Caucasus map. +-- * @{Wrapper.Airbase#AIRBASE.Nevada}: The airbases on the Nevada (NTTR) map. +-- * @{Wrapper.Airbase#AIRBASE.Normandy}: The airbases on the Normandy map. +-- +-- Use the method @{Wrapper.Airbase#AIRBASE.FindByName}() to retrieve the airbase object. -- The known AIRBASE objects are automatically imported at mission start by MOOSE. -- Therefore, there isn't any New() constructor defined for AIRBASE objects. --- --- Ships and Farps are added within the mission, and are therefore not known. +-- +-- Ships and FARPs are added within the mission, and are therefore not known. -- For these AIRBASE objects, there isn't an @{Wrapper.Airbase#AIRBASE} enumeration defined. -- You need to provide the **exact name** of the airbase as the parameter to the @{Wrapper.Airbase#AIRBASE.FindByName}() method! --- +-- -- @param #SPAWN self -- @param Wrapper.Airbase#AIRBASE SpawnAirbase The @{Wrapper.Airbase} where to spawn the group. -- @param #SPAWN.Takeoff Takeoff (optional) The location and takeoff method. Default is Hot. -- @param #number TakeoffAltitude (optional) The altitude above the ground. -- @param Wrapper.Airbase#AIRBASE.TerminalType TerminalType (optional) The terminal type the aircraft should be spawned at. See @{Wrapper.Airbase#AIRBASE.TerminalType}. -- @param #boolean EmergencyAirSpawn (optional) If true (default), groups are spawned in air if there is no parking spot at the airbase. If false, nothing is spawned if no parking spot is available. --- @param #table Parkingdata (optional) Table holding the coordinates and terminal ids for all units of the group. Spawning will be forced to happen at exactily these spots! +-- @param #table Parkingdata (optional) Table holding the coordinates and terminal ids for all units of the group. Spawning will be forced to happen at exactly these spots! -- @return Wrapper.Group#GROUP The group that was spawned or nil when nothing was spawned. -- @usage +-- -- Spawn_Plane = SPAWN:New( "Plane" ) -- Spawn_Plane:SpawnAtAirbase( AIRBASE:FindByName( AIRBASE.Caucasus.Krymsk ), SPAWN.Takeoff.Cold ) -- Spawn_Plane:SpawnAtAirbase( AIRBASE:FindByName( AIRBASE.Caucasus.Krymsk ), SPAWN.Takeoff.Hot ) -- Spawn_Plane:SpawnAtAirbase( AIRBASE:FindByName( AIRBASE.Caucasus.Krymsk ), SPAWN.Takeoff.Runway ) --- +-- -- Spawn_Plane:SpawnAtAirbase( AIRBASE:FindByName( "Carrier" ), SPAWN.Takeoff.Cold ) --- +-- -- Spawn_Heli = SPAWN:New( "Heli") --- +-- -- Spawn_Heli:SpawnAtAirbase( AIRBASE:FindByName( "FARP Cold" ), SPAWN.Takeoff.Cold ) -- Spawn_Heli:SpawnAtAirbase( AIRBASE:FindByName( "FARP Hot" ), SPAWN.Takeoff.Hot ) -- Spawn_Heli:SpawnAtAirbase( AIRBASE:FindByName( "FARP Runway" ), SPAWN.Takeoff.Runway ) -- Spawn_Heli:SpawnAtAirbase( AIRBASE:FindByName( "FARP Air" ), SPAWN.Takeoff.Air ) --- +-- -- Spawn_Heli:SpawnAtAirbase( AIRBASE:FindByName( "Carrier" ), SPAWN.Takeoff.Cold ) --- +-- -- Spawn_Plane:SpawnAtAirbase( AIRBASE:FindByName( AIRBASE.Caucasus.Krymsk ), SPAWN.Takeoff.Cold, nil, AIRBASE.TerminalType.OpenBig ) --- +-- function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalType, EmergencyAirSpawn, Parkingdata ) -- R2.2, R2.4 self:F( { self.SpawnTemplatePrefix, SpawnAirbase, Takeoff, TakeoffAltitude, TerminalType } ) -- Get position of airbase. local PointVec3 = SpawnAirbase:GetCoordinate() - self:T2(PointVec3) + self:T2( PointVec3 ) -- Set take off type. Default is hot. Takeoff = Takeoff or SPAWN.Takeoff.Hot - + -- By default, groups are spawned in air if no parking spot is available. - if EmergencyAirSpawn==nil then - EmergencyAirSpawn=true + if EmergencyAirSpawn == nil then + EmergencyAirSpawn = true end - + self:F( { SpawnIndex = self.SpawnIndex } ) - + if self:_GetSpawnIndex( self.SpawnIndex + 1 ) then - + -- Get group template. local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate - + self:F( { SpawnTemplate = SpawnTemplate } ) - + if SpawnTemplate then - + -- Check if the aircraft with the specified SpawnIndex is already spawned. -- If yes, ensure that the aircraft is spawned at the same aircraft spot. - + local GroupAlive = self:GetGroupFromIndex( self.SpawnIndex ) - + self:F( { GroupAlive = GroupAlive } ) -- Debug output self:T( { "Current point of ", self.SpawnTemplatePrefix, SpawnAirbase } ) - + -- Template group, unit and its attributes. - local TemplateGroup = GROUP:FindByName(self.SpawnTemplatePrefix) - local TemplateUnit=TemplateGroup:GetUnit(1) - + local TemplateGroup = GROUP:FindByName( self.SpawnTemplatePrefix ) + local TemplateUnit = TemplateGroup:GetUnit( 1 ) + -- General category of spawned group. - local group=TemplateGroup - local istransport=group:HasAttribute("Transports") and group:HasAttribute("Planes") - local isawacs=group:HasAttribute("AWACS") - local isfighter=group:HasAttribute("Fighters") or group:HasAttribute("Interceptors") or group:HasAttribute("Multirole fighters") or (group:HasAttribute("Bombers") and not group:HasAttribute("Strategic bombers")) - local isbomber=group:HasAttribute("Strategic bombers") - local istanker=group:HasAttribute("Tankers") - local ishelo=TemplateUnit:HasAttribute("Helicopters") - + local group = TemplateGroup + local istransport = group:HasAttribute( "Transports" ) and group:HasAttribute( "Planes" ) + local isawacs = group:HasAttribute( "AWACS" ) + local isfighter = group:HasAttribute( "Fighters" ) or group:HasAttribute( "Interceptors" ) or group:HasAttribute( "Multirole fighters" ) or (group:HasAttribute( "Bombers" ) and not group:HasAttribute( "Strategic bombers" )) + local isbomber = group:HasAttribute( "Strategic bombers" ) + local istanker = group:HasAttribute( "Tankers" ) + local ishelo = TemplateUnit:HasAttribute( "Helicopters" ) + -- Number of units in the group. With grouping this can actually differ from the template group size! - local nunits=#SpawnTemplate.units + local nunits = #SpawnTemplate.units -- First waypoint of the group. local SpawnPoint = SpawnTemplate.route.points[1] @@ -29534,7 +36227,7 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT local AirbaseID = SpawnAirbase:GetID() local AirbaseCategory = SpawnAirbase:GetAirbaseCategory() self:F( { AirbaseCategory = AirbaseCategory } ) - + -- Set airdromeId. if AirbaseCategory == Airbase.Category.SHIP then SpawnPoint.linkUnit = AirbaseID @@ -29547,71 +36240,70 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT end -- Set waypoint type/action. - SpawnPoint.alt = 0 - SpawnPoint.type = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type + SpawnPoint.alt = 0 + SpawnPoint.type = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type SpawnPoint.action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action - + -- Check if we spawn on ground. - local spawnonground=not (Takeoff==SPAWN.Takeoff.Air) - self:T({spawnonground=spawnonground, TOtype=Takeoff, TOair=Takeoff==SPAWN.Takeoff.Air}) - + local spawnonground = not (Takeoff == SPAWN.Takeoff.Air) + self:T( { spawnonground = spawnonground, TOtype = Takeoff, TOair = Takeoff == SPAWN.Takeoff.Air } ) + -- Check where we actually spawn if we spawn on ground. - local spawnonship=false - local spawnonfarp=false - local spawnonrunway=false - local spawnonairport=false - if spawnonground then + local spawnonship = false + local spawnonfarp = false + local spawnonrunway = false + local spawnonairport = false + if spawnonground then if AirbaseCategory == Airbase.Category.SHIP then - spawnonship=true + spawnonship = true elseif AirbaseCategory == Airbase.Category.HELIPAD then - spawnonfarp=true + spawnonfarp = true elseif AirbaseCategory == Airbase.Category.AIRDROME then - spawnonairport=true + spawnonairport = true end - spawnonrunway=Takeoff==SPAWN.Takeoff.Runway + spawnonrunway = Takeoff == SPAWN.Takeoff.Runway end - + -- Array with parking spots coordinates. - local parkingspots={} - local parkingindex={} + local parkingspots = {} + local parkingindex = {} local spots - + -- Spawn happens on ground, i.e. at an airbase, a FARP or a ship. if spawnonground and not SpawnTemplate.parked then - - + -- Number of free parking spots. - local nfree=0 - + local nfree = 0 + -- Set terminal type. - local termtype=TerminalType - if spawnonrunway then + local termtype = TerminalType + if spawnonrunway then if spawnonship then -- Looks like there are no runway spawn spots on the stennis! if ishelo then - termtype=AIRBASE.TerminalType.HelicopterUsable + termtype = AIRBASE.TerminalType.HelicopterUsable else - termtype=AIRBASE.TerminalType.OpenMedOrBig + termtype = AIRBASE.TerminalType.OpenMedOrBig end else - termtype=AIRBASE.TerminalType.Runway - end + termtype = AIRBASE.TerminalType.Runway + end end - + -- Scan options. Might make that input somehow. - local scanradius=50 - local scanunits=true - local scanstatics=true - local scanscenery=false - local verysafe=false - + local scanradius = 50 + local scanunits = true + local scanstatics = true + local scanscenery = false + local verysafe = false + -- Number of free parking spots at the airbase. if spawnonship or spawnonfarp or spawnonrunway then -- These places work procedural and have some kind of build in queue ==> Less effort. - self:T(string.format("Group %s is spawned on farp/ship/runway %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) - nfree=SpawnAirbase:GetFreeParkingSpotsNumber(termtype, true) - spots=SpawnAirbase:GetFreeParkingSpotsTable(termtype, true) - --[[ + self:T( string.format( "Group %s is spawned on farp/ship/runway %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName() ) ) + nfree = SpawnAirbase:GetFreeParkingSpotsNumber( termtype, true ) + spots = SpawnAirbase:GetFreeParkingSpotsTable( termtype, true ) + --[[ elseif Parkingdata~=nil then -- Parking data explicitly set by user as input parameter. nfree=#Parkingdata @@ -29619,146 +36311,146 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT ]] else if ishelo then - if termtype==nil then + if termtype == nil then -- Helo is spawned. Try exclusive helo spots first. - self:T(string.format("Helo group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), AIRBASE.TerminalType.HelicopterOnly)) - spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.HelicopterOnly, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, Parkingdata) - nfree=#spots - if nfree=1 then - + + -- On free spot required in these cases. + if nfree >= 1 then + -- All units get the same spot. DCS takes care of the rest. - for i=1,nunits do - table.insert(parkingspots, spots[1].Coordinate) - table.insert(parkingindex, spots[1].TerminalID) + for i = 1, nunits do + table.insert( parkingspots, spots[1].Coordinate ) + table.insert( parkingindex, spots[1].TerminalID ) end -- This is actually used... - PointVec3=spots[1].Coordinate - + PointVec3 = spots[1].Coordinate + else -- If there is absolutely no spot ==> air start! - _notenough=true + _notenough = true end - + elseif spawnonairport then - - if nfree>=nunits then - - for i=1,nunits do - table.insert(parkingspots, spots[i].Coordinate) - table.insert(parkingindex, spots[i].TerminalID) + + if nfree >= nunits then + + for i = 1, nunits do + table.insert( parkingspots, spots[i].Coordinate ) + table.insert( parkingindex, spots[i].TerminalID ) end - + else -- Not enough spots for the whole group ==> air start! - _notenough=true - end + _notenough = true + end end - + -- Not enough spots ==> Prepare airstart. if _notenough then - - if EmergencyAirSpawn and not self.SpawnUnControlled then - self:E(string.format("WARNING: Group %s has no parking spots at %s ==> air start!", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) - + + if EmergencyAirSpawn and not self.SpawnUnControlled then + self:E( string.format( "WARNING: Group %s has no parking spots at %s ==> air start!", self.SpawnTemplatePrefix, SpawnAirbase:GetName() ) ) + -- Not enough parking spots at the airport ==> Spawn in air. - spawnonground=false - spawnonship=false - spawnonfarp=false - spawnonrunway=false - + spawnonground = false + spawnonship = false + spawnonfarp = false + spawnonrunway = false + -- Set waypoint type/action to turning point. - SpawnPoint.type = GROUPTEMPLATE.Takeoff[GROUP.Takeoff.Air][1] -- type = Turning Point + SpawnPoint.type = GROUPTEMPLATE.Takeoff[GROUP.Takeoff.Air][1] -- type = Turning Point SpawnPoint.action = GROUPTEMPLATE.Takeoff[GROUP.Takeoff.Air][2] -- action = Turning Point - + -- Adjust altitude to be 500-1000 m above the airbase. - PointVec3.x=PointVec3.x+math.random(-500,500) - PointVec3.z=PointVec3.z+math.random(-500,500) + PointVec3.x = PointVec3.x + math.random( -500, 500 ) + PointVec3.z = PointVec3.z + math.random( -500, 500 ) if ishelo then - PointVec3.y=PointVec3:GetLandHeight()+math.random(100,1000) + PointVec3.y = PointVec3:GetLandHeight() + math.random( 100, 1000 ) else -- Randomize position so that multiple AC wont be spawned on top even in air. - PointVec3.y=PointVec3:GetLandHeight()+math.random(500,2500) + PointVec3.y = PointVec3:GetLandHeight() + math.random( 500, 2500 ) end - - Takeoff=GROUP.Takeoff.Air + + Takeoff = GROUP.Takeoff.Air else - self:E(string.format("WARNING: Group %s has no parking spots at %s ==> No emergency air start or uncontrolled spawning ==> No spawn!", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) + self:E( string.format( "WARNING: Group %s has no parking spots at %s ==> No emergency air start or uncontrolled spawning ==> No spawn!", self.SpawnTemplatePrefix, SpawnAirbase:GetName() ) ) return nil end end - + else - + -- Air start requested initially ==> Set altitude. if TakeoffAltitude then - PointVec3.y=TakeoffAltitude + PointVec3.y = TakeoffAltitude else if ishelo then - PointVec3.y=PointVec3:GetLandHeight()+math.random(100,1000) + PointVec3.y = PointVec3:GetLandHeight() + math.random( 100, 1000 ) else -- Randomize position so that multiple AC wont be spawned on top even in air. - PointVec3.y=PointVec3:GetLandHeight()+math.random(500,2500) + PointVec3.y = PointVec3:GetLandHeight() + math.random( 500, 2500 ) end end - + end if not SpawnTemplate.parked then @@ -29767,163 +36459,162 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT SpawnTemplate.parked = true for UnitID = 1, nunits do - self:T2('Before Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) - + self:T2( 'Before Translation SpawnTemplate.units[' .. UnitID .. '].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units[' .. UnitID .. '].y = ' .. SpawnTemplate.units[UnitID].y ) + -- Template of the current unit. local UnitTemplate = SpawnTemplate.units[UnitID] - + -- Tranlate position and preserve the relative position/formation of all aircraft. local SX = UnitTemplate.x - local SY = UnitTemplate.y + local SY = UnitTemplate.y local BX = SpawnTemplate.route.points[1].x local BY = SpawnTemplate.route.points[1].y - local TX = PointVec3.x + (SX-BX) - local TY = PointVec3.z + (SY-BY) - + local TX = PointVec3.x + (SX - BX) + local TY = PointVec3.z + (SY - BY) + if spawnonground then - + -- Ships and FARPS seem to have a build in queue. if spawnonship or spawnonfarp or spawnonrunway then - - self:T(string.format("Group %s spawning at farp, ship or runway %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) - + + self:T( string.format( "Group %s spawning at farp, ship or runway %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName() ) ) + -- Spawn on ship. We take only the position of the ship. - SpawnTemplate.units[UnitID].x = PointVec3.x --TX - SpawnTemplate.units[UnitID].y = PointVec3.z --TY + SpawnTemplate.units[UnitID].x = PointVec3.x -- TX + SpawnTemplate.units[UnitID].y = PointVec3.z -- TY SpawnTemplate.units[UnitID].alt = PointVec3.y - + else - - self:T(string.format("Group %s spawning at airbase %s on parking spot id %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), parkingindex[UnitID])) - + + self:T( string.format( "Group %s spawning at airbase %s on parking spot id %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), parkingindex[UnitID] ) ) + -- Get coordinates of parking spot. - SpawnTemplate.units[UnitID].x = parkingspots[UnitID].x - SpawnTemplate.units[UnitID].y = parkingspots[UnitID].z + SpawnTemplate.units[UnitID].x = parkingspots[UnitID].x + SpawnTemplate.units[UnitID].y = parkingspots[UnitID].z SpawnTemplate.units[UnitID].alt = parkingspots[UnitID].y - - --parkingspots[UnitID]:MarkToAll(string.format("Group %s spawning at airbase %s on parking spot id %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), parkingindex[UnitID])) + + -- parkingspots[UnitID]:MarkToAll(string.format("Group %s spawning at airbase %s on parking spot id %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), parkingindex[UnitID])) end - + else - - self:T(string.format("Group %s spawning in air at %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) - + + self:T( string.format( "Group %s spawning in air at %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName() ) ) + -- Spawn in air as requested initially. Original template orientation is perserved, altitude is already correctly set. - SpawnTemplate.units[UnitID].x = TX - SpawnTemplate.units[UnitID].y = TY + SpawnTemplate.units[UnitID].x = TX + SpawnTemplate.units[UnitID].y = TY SpawnTemplate.units[UnitID].alt = PointVec3.y - + end - + -- Parking spot id. UnitTemplate.parking = nil UnitTemplate.parking_id = nil if parkingindex[UnitID] then UnitTemplate.parking = parkingindex[UnitID] end - + -- Debug output. - self:T(string.format("Group %s unit number %d: Parking = %s",self.SpawnTemplatePrefix, UnitID, tostring(UnitTemplate.parking))) - self:T(string.format("Group %s unit number %d: Parking ID = %s",self.SpawnTemplatePrefix, UnitID, tostring(UnitTemplate.parking_id))) - self:T2('After Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) + self:T( string.format( "Group %s unit number %d: Parking = %s", self.SpawnTemplatePrefix, UnitID, tostring( UnitTemplate.parking ) ) ) + self:T( string.format( "Group %s unit number %d: Parking ID = %s", self.SpawnTemplatePrefix, UnitID, tostring( UnitTemplate.parking_id ) ) ) + self:T2( 'After Translation SpawnTemplate.units[' .. UnitID .. '].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units[' .. UnitID .. '].y = ' .. SpawnTemplate.units[UnitID].y ) end end - + -- Set gereral spawnpoint position. - SpawnPoint.x = PointVec3.x - SpawnPoint.y = PointVec3.z + SpawnPoint.x = PointVec3.x + SpawnPoint.y = PointVec3.z SpawnPoint.alt = PointVec3.y - + SpawnTemplate.x = PointVec3.x SpawnTemplate.y = PointVec3.z - + SpawnTemplate.uncontrolled = self.SpawnUnControlled - + -- Spawn group. local GroupSpawned = self:SpawnWithIndex( self.SpawnIndex ) - + -- When spawned in the air, we need to generate a Takeoff Event. if Takeoff == GROUP.Takeoff.Air then for UnitID, UnitSpawned in pairs( GroupSpawned:GetUnits() ) do - SCHEDULER:New( nil, BASE.CreateEventTakeoff, { GroupSpawned, timer.getTime(), UnitSpawned:GetDCSObject() } , 5 ) + SCHEDULER:New( nil, BASE.CreateEventTakeoff, { GroupSpawned, timer.getTime(), UnitSpawned:GetDCSObject() }, 5 ) end end - + -- Check if we accidentally spawned on the runway. Needs to be schedules, because group is not immidiately alive. - if Takeoff~=SPAWN.Takeoff.Runway and Takeoff~=SPAWN.Takeoff.Air and spawnonairport then - SCHEDULER:New(nil, AIRBASE.CheckOnRunWay, {SpawnAirbase, GroupSpawned, 75, true} , 1.0) + if Takeoff ~= SPAWN.Takeoff.Runway and Takeoff ~= SPAWN.Takeoff.Air and spawnonairport then + SCHEDULER:New( nil, AIRBASE.CheckOnRunWay, { SpawnAirbase, GroupSpawned, 75, true }, 1.0 ) end - - return GroupSpawned + + return GroupSpawned end end - + return nil end ---- Spawn a group on an @{Wrapper.Airbase} at a specific parking spot. +--- Spawn a group on an @{Wrapper.Airbase} at a specific parking spot. -- @param #SPAWN self -- @param Wrapper.Airbase#AIRBASE Airbase The @{Wrapper.Airbase} where to spawn the group. -- @param #table Spots Table of parking spot IDs. Note that these in general are different from the numbering in the mission editor! -- @param #SPAWN.Takeoff Takeoff (Optional) Takeoff type, i.e. either SPAWN.Takeoff.Cold or SPAWN.Takeoff.Hot. Default is Hot. -- @return Wrapper.Group#GROUP The group that was spawned or nil when nothing was spawned. -function SPAWN:SpawnAtParkingSpot(Airbase, Spots, Takeoff) -- R2.5 - self:F({Airbase=Airbase, Spots=Spots, Takeoff=Takeoff}) +function SPAWN:SpawnAtParkingSpot( Airbase, Spots, Takeoff ) -- R2.5 + self:F( { Airbase = Airbase, Spots = Spots, Takeoff = Takeoff } ) -- Ensure that Spots parameter is a table. - if type(Spots)~="table" then - Spots={Spots} + if type( Spots ) ~= "table" then + Spots = { Spots } end -- Get template group. - local group=GROUP:FindByName(self.SpawnTemplatePrefix) - + local group = GROUP:FindByName( self.SpawnTemplatePrefix ) + -- Get number of units in group. - local nunits=self.SpawnGrouping or #group:GetUnits() + local nunits = self.SpawnGrouping or #group:GetUnits() -- Quick check. if nunits then - + -- Check that number of provided parking spots is large enough. - if #Spots=nunits then - return self:SpawnAtAirbase(Airbase, Takeoff, nil, nil, nil, Parkingdata) + + if #Parkingdata >= nunits then + return self:SpawnAtAirbase( Airbase, Takeoff, nil, nil, nil, Parkingdata ) else - self:E("ERROR: Could not find enough free parking spots!") + self:E( "ERROR: Could not find enough free parking spots!" ) end - - + else - self:E("ERROR: Could not get number of units in group!") + self:E( "ERROR: Could not get number of units in group!" ) end return nil end ---- Will park a group at an @{Wrapper.Airbase}. --- +--- Will park a group at an @{Wrapper.Airbase}. +-- -- @param #SPAWN self -- @param Wrapper.Airbase#AIRBASE SpawnAirbase The @{Wrapper.Airbase} where to spawn the group. -- @param Wrapper.Airbase#AIRBASE.TerminalType TerminalType (optional) The terminal type the aircraft should be spawned at. See @{Wrapper.Airbase#AIRBASE.TerminalType}. @@ -29935,34 +36626,34 @@ function SPAWN:ParkAircraft( SpawnAirbase, TerminalType, Parkingdata, SpawnIndex -- Get position of airbase. local PointVec3 = SpawnAirbase:GetCoordinate() - self:T2(PointVec3) + self:T2( PointVec3 ) -- Set take off type. Default is hot. local Takeoff = SPAWN.Takeoff.Cold - + -- Get group template. local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate if SpawnTemplate then - + -- Check if the aircraft with the specified SpawnIndex is already spawned. -- If yes, ensure that the aircraft is spawned at the same aircraft spot. - + local GroupAlive = self:GetGroupFromIndex( SpawnIndex ) -- Debug output self:T( { "Current point of ", self.SpawnTemplatePrefix, SpawnAirbase } ) - + -- Template group, unit and its attributes. - local TemplateGroup = GROUP:FindByName(self.SpawnTemplatePrefix) - local TemplateUnit=TemplateGroup:GetUnit(1) - local ishelo=TemplateUnit:HasAttribute("Helicopters") - local isbomber=TemplateUnit:HasAttribute("Bombers") - local istransport=TemplateUnit:HasAttribute("Transports") - local isfighter=TemplateUnit:HasAttribute("Battleplanes") - + local TemplateGroup = GROUP:FindByName( self.SpawnTemplatePrefix ) + local TemplateUnit = TemplateGroup:GetUnit( 1 ) + local ishelo = TemplateUnit:HasAttribute( "Helicopters" ) + local isbomber = TemplateUnit:HasAttribute( "Bombers" ) + local istransport = TemplateUnit:HasAttribute( "Transports" ) + local isfighter = TemplateUnit:HasAttribute( "Battleplanes" ) + -- Number of units in the group. With grouping this can actually differ from the template group size! - local nunits=#SpawnTemplate.units + local nunits = #SpawnTemplate.units -- First waypoint of the group. local SpawnPoint = SpawnTemplate.route.points[1] @@ -29976,7 +36667,7 @@ function SPAWN:ParkAircraft( SpawnAirbase, TerminalType, Parkingdata, SpawnIndex local AirbaseID = SpawnAirbase:GetID() local AirbaseCategory = SpawnAirbase:GetAirbaseCategory() self:F( { AirbaseCategory = AirbaseCategory } ) - + -- Set airdromeId. if AirbaseCategory == Airbase.Category.SHIP then SpawnPoint.linkUnit = AirbaseID @@ -29989,59 +36680,58 @@ function SPAWN:ParkAircraft( SpawnAirbase, TerminalType, Parkingdata, SpawnIndex end -- Set waypoint type/action. - SpawnPoint.alt = 0 - SpawnPoint.type = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type + SpawnPoint.alt = 0 + SpawnPoint.type = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type SpawnPoint.action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action - + -- Check if we spawn on ground. - local spawnonground=not (Takeoff==SPAWN.Takeoff.Air) - self:T({spawnonground=spawnonground, TOtype=Takeoff, TOair=Takeoff==SPAWN.Takeoff.Air}) - + local spawnonground = not (Takeoff == SPAWN.Takeoff.Air) + self:T( { spawnonground = spawnonground, TOtype = Takeoff, TOair = Takeoff == SPAWN.Takeoff.Air } ) + -- Check where we actually spawn if we spawn on ground. - local spawnonship=false - local spawnonfarp=false - local spawnonrunway=false - local spawnonairport=false - if spawnonground then + local spawnonship = false + local spawnonfarp = false + local spawnonrunway = false + local spawnonairport = false + if spawnonground then if AirbaseCategory == Airbase.Category.SHIP then - spawnonship=true + spawnonship = true elseif AirbaseCategory == Airbase.Category.HELIPAD then - spawnonfarp=true + spawnonfarp = true elseif AirbaseCategory == Airbase.Category.AIRDROME then - spawnonairport=true + spawnonairport = true end - spawnonrunway=Takeoff==SPAWN.Takeoff.Runway + spawnonrunway = Takeoff == SPAWN.Takeoff.Runway end - + -- Array with parking spots coordinates. - local parkingspots={} - local parkingindex={} + local parkingspots = {} + local parkingindex = {} local spots - + -- Spawn happens on ground, i.e. at an airbase, a FARP or a ship. if spawnonground and not SpawnTemplate.parked then - - + -- Number of free parking spots. - local nfree=0 - + local nfree = 0 + -- Set terminal type. - local termtype=TerminalType + local termtype = TerminalType -- Scan options. Might make that input somehow. - local scanradius=50 - local scanunits=true - local scanstatics=true - local scanscenery=false - local verysafe=false - + local scanradius = 50 + local scanunits = true + local scanstatics = true + local scanscenery = false + local verysafe = false + -- Number of free parking spots at the airbase. if spawnonship or spawnonfarp or spawnonrunway then -- These places work procedural and have some kind of build in queue ==> Less effort. - self:T(string.format("Group %s is spawned on farp/ship/runway %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) - nfree=SpawnAirbase:GetFreeParkingSpotsNumber(termtype, true) - spots=SpawnAirbase:GetFreeParkingSpotsTable(termtype, true) - --[[ + self:T( string.format( "Group %s is spawned on farp/ship/runway %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName() ) ) + nfree = SpawnAirbase:GetFreeParkingSpotsNumber( termtype, true ) + spots = SpawnAirbase:GetFreeParkingSpotsTable( termtype, true ) + --[[ elseif Parkingdata~=nil then -- Parking data explicitly set by user as input parameter. nfree=#Parkingdata @@ -30049,114 +36739,114 @@ function SPAWN:ParkAircraft( SpawnAirbase, TerminalType, Parkingdata, SpawnIndex ]] else if ishelo then - if termtype==nil then + if termtype == nil then -- Helo is spawned. Try exclusive helo spots first. - self:T(string.format("Helo group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), AIRBASE.TerminalType.HelicopterOnly)) - spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.HelicopterOnly, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, Parkingdata) - nfree=#spots - if nfree=1 then - + if nfree >= 1 then + -- All units get the same spot. DCS takes care of the rest. - for i=1,nunits do - table.insert(parkingspots, spots[1].Coordinate) - table.insert(parkingindex, spots[1].TerminalID) + for i = 1, nunits do + table.insert( parkingspots, spots[1].Coordinate ) + table.insert( parkingindex, spots[1].TerminalID ) end -- This is actually used... - PointVec3=spots[1].Coordinate - + PointVec3 = spots[1].Coordinate + else -- If there is absolutely no spot ==> air start! - _notenough=true + _notenough = true end - + elseif spawnonairport then - - if nfree>=nunits then - - for i=1,nunits do - table.insert(parkingspots, spots[i].Coordinate) - table.insert(parkingindex, spots[i].TerminalID) + + if nfree >= nunits then + + for i = 1, nunits do + table.insert( parkingspots, spots[i].Coordinate ) + table.insert( parkingindex, spots[i].TerminalID ) end - + else -- Not enough spots for the whole group ==> air start! - _notenough=true - end + _notenough = true + end end - + -- Not enough spots ==> Prepare airstart. if _notenough then - - if not self.SpawnUnControlled then + + if not self.SpawnUnControlled then else - self:E(string.format("WARNING: Group %s has no parking spots at %s ==> No emergency air start or uncontrolled spawning ==> No spawn!", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) + self:E( string.format( "WARNING: Group %s has no parking spots at %s ==> No emergency air start or uncontrolled spawning ==> No spawn!", self.SpawnTemplatePrefix, SpawnAirbase:GetName() ) ) return nil end end - + else - + end if not SpawnTemplate.parked then @@ -30165,118 +36855,118 @@ function SPAWN:ParkAircraft( SpawnAirbase, TerminalType, Parkingdata, SpawnIndex SpawnTemplate.parked = true for UnitID = 1, nunits do - self:F('Before Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) - + self:F( 'Before Translation SpawnTemplate.units[' .. UnitID .. '].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units[' .. UnitID .. '].y = ' .. SpawnTemplate.units[UnitID].y ) + -- Template of the current unit. local UnitTemplate = SpawnTemplate.units[UnitID] - + -- Tranlate position and preserve the relative position/formation of all aircraft. local SX = UnitTemplate.x - local SY = UnitTemplate.y + local SY = UnitTemplate.y local BX = SpawnTemplate.route.points[1].x local BY = SpawnTemplate.route.points[1].y - local TX = PointVec3.x + (SX-BX) - local TY = PointVec3.z + (SY-BY) - + local TX = PointVec3.x + (SX - BX) + local TY = PointVec3.z + (SY - BY) + if spawnonground then - + -- Ships and FARPS seem to have a build in queue. if spawnonship or spawnonfarp or spawnonrunway then - - self:T(string.format("Group %s spawning at farp, ship or runway %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) + + self:T( string.format( "Group %s spawning at farp, ship or runway %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName() ) ) -- Spawn on ship. We take only the position of the ship. - SpawnTemplate.units[UnitID].x = PointVec3.x --TX - SpawnTemplate.units[UnitID].y = PointVec3.z --TY + SpawnTemplate.units[UnitID].x = PointVec3.x -- TX + SpawnTemplate.units[UnitID].y = PointVec3.z -- TY SpawnTemplate.units[UnitID].alt = PointVec3.y - + else - self:T(string.format("Group %s spawning at airbase %s on parking spot id %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), parkingindex[UnitID])) - + self:T( string.format( "Group %s spawning at airbase %s on parking spot id %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), parkingindex[UnitID] ) ) + -- Get coordinates of parking spot. - SpawnTemplate.units[UnitID].x = parkingspots[UnitID].x - SpawnTemplate.units[UnitID].y = parkingspots[UnitID].z + SpawnTemplate.units[UnitID].x = parkingspots[UnitID].x + SpawnTemplate.units[UnitID].y = parkingspots[UnitID].z SpawnTemplate.units[UnitID].alt = parkingspots[UnitID].y - - --parkingspots[UnitID]:MarkToAll(string.format("Group %s spawning at airbase %s on parking spot id %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), parkingindex[UnitID])) + + -- parkingspots[UnitID]:MarkToAll(string.format("Group %s spawning at airbase %s on parking spot id %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), parkingindex[UnitID])) end - + else - - self:T(string.format("Group %s spawning in air at %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) - + + self:T( string.format( "Group %s spawning in air at %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName() ) ) + -- Spawn in air as requested initially. Original template orientation is perserved, altitude is already correctly set. - SpawnTemplate.units[UnitID].x = TX - SpawnTemplate.units[UnitID].y = TY + SpawnTemplate.units[UnitID].x = TX + SpawnTemplate.units[UnitID].y = TY SpawnTemplate.units[UnitID].alt = PointVec3.y - + end - + -- Parking spot id. UnitTemplate.parking = nil UnitTemplate.parking_id = nil if parkingindex[UnitID] then UnitTemplate.parking = parkingindex[UnitID] end - + -- Debug output. - self:T2(string.format("Group %s unit number %d: Parking = %s",self.SpawnTemplatePrefix, UnitID, tostring(UnitTemplate.parking))) - self:T2(string.format("Group %s unit number %d: Parking ID = %s",self.SpawnTemplatePrefix, UnitID, tostring(UnitTemplate.parking_id))) - self:T2('After Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) + self:T2( string.format( "Group %s unit number %d: Parking = %s", self.SpawnTemplatePrefix, UnitID, tostring( UnitTemplate.parking ) ) ) + self:T2( string.format( "Group %s unit number %d: Parking ID = %s", self.SpawnTemplatePrefix, UnitID, tostring( UnitTemplate.parking_id ) ) ) + self:T2( 'After Translation SpawnTemplate.units[' .. UnitID .. '].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units[' .. UnitID .. '].y = ' .. SpawnTemplate.units[UnitID].y ) end end - + -- Set general spawnpoint position. - SpawnPoint.x = PointVec3.x - SpawnPoint.y = PointVec3.z + SpawnPoint.x = PointVec3.x + SpawnPoint.y = PointVec3.z SpawnPoint.alt = PointVec3.y - + SpawnTemplate.x = PointVec3.x SpawnTemplate.y = PointVec3.z - + SpawnTemplate.uncontrolled = true - + -- Spawn group. local GroupSpawned = self:SpawnWithIndex( SpawnIndex, true ) - + -- When spawned in the air, we need to generate a Takeoff Event. if Takeoff == GROUP.Takeoff.Air then for UnitID, UnitSpawned in pairs( GroupSpawned:GetUnits() ) do - SCHEDULER:New( nil, BASE.CreateEventTakeoff, { GroupSpawned, timer.getTime(), UnitSpawned:GetDCSObject() } , 5 ) + SCHEDULER:New( nil, BASE.CreateEventTakeoff, { GroupSpawned, timer.getTime(), UnitSpawned:GetDCSObject() }, 5 ) end end - + -- Check if we accidentally spawned on the runway. Needs to be schedules, because group is not immidiately alive. - if Takeoff~=SPAWN.Takeoff.Runway and Takeoff~=SPAWN.Takeoff.Air and spawnonairport then - SCHEDULER:New(nil, AIRBASE.CheckOnRunWay, {SpawnAirbase, GroupSpawned, 75, true} , 1.0) + if Takeoff ~= SPAWN.Takeoff.Runway and Takeoff ~= SPAWN.Takeoff.Air and spawnonairport then + SCHEDULER:New( nil, AIRBASE.CheckOnRunWay, { SpawnAirbase, GroupSpawned, 75, true }, 1.0 ) end - + end end ---- Will park a group at an @{Wrapper.Airbase}. +--- Will park a group at an @{Wrapper.Airbase}. -- This method is mostly advisable to be used if you want to simulate parking units at an airbase and be visible. -- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. --- +-- -- All groups that are in the spawn collection and that are alive, and not in the air, are parked. --- +-- -- The @{Wrapper.Airbase#AIRBASE} object must refer to a valid airbase known in the sim. -- You can use the following enumerations to search for the pre-defined airbases on the current known maps of DCS: --- --- * @{Wrapper.Airbase#AIRBASE.Caucasus}: The airbases on the Caucasus map. --- * @{Wrapper.Airbase#AIRBASE.Nevada}: The airbases on the Nevada (NTTR) map. --- * @{Wrapper.Airbase#AIRBASE.Normandy}: The airbases on the Normandy map. --- --- Use the method @{Wrapper.Airbase#AIRBASE.FindByName}() to retrieve the airbase object. +-- +-- * @{Wrapper.Airbase#AIRBASE.Caucasus}: The airbases on the Caucasus map. +-- * @{Wrapper.Airbase#AIRBASE.Nevada}: The airbases on the Nevada (NTTR) map. +-- * @{Wrapper.Airbase#AIRBASE.Normandy}: The airbases on the Normandy map. +-- +-- Use the method @{Wrapper.Airbase#AIRBASE.FindByName}() to retrieve the airbase object. -- The known AIRBASE objects are automatically imported at mission start by MOOSE. -- Therefore, there isn't any New() constructor defined for AIRBASE objects. --- --- Ships and Farps are added within the mission, and are therefore not known. +-- +-- Ships and FARPs are added within the mission, and are therefore not known. -- For these AIRBASE objects, there isn't an @{Wrapper.Airbase#AIRBASE} enumeration defined. -- You need to provide the **exact name** of the airbase as the parameter to the @{Wrapper.Airbase#AIRBASE.FindByName}() method! --- +-- -- @param #SPAWN self -- @param Wrapper.Airbase#AIRBASE SpawnAirbase The @{Wrapper.Airbase} where to spawn the group. -- @param Wrapper.Airbase#AIRBASE.TerminalType TerminalType (optional) The terminal type the aircraft should be spawned at. See @{Wrapper.Airbase#AIRBASE.TerminalType}. @@ -30285,15 +36975,15 @@ end -- @usage -- Spawn_Plane = SPAWN:New( "Plane" ) -- Spawn_Plane:ParkAtAirbase( AIRBASE:FindByName( AIRBASE.Caucasus.Krymsk ) ) --- +-- -- Spawn_Heli = SPAWN:New( "Heli") --- +-- -- Spawn_Heli:ParkAtAirbase( AIRBASE:FindByName( "FARP Cold" ) ) --- +-- -- Spawn_Heli:ParkAtAirbase( AIRBASE:FindByName( "Carrier" ) ) --- +-- -- Spawn_Plane:ParkAtAirbase( AIRBASE:FindByName( AIRBASE.Caucasus.Krymsk ), AIRBASE.TerminalType.OpenBig ) --- +-- function SPAWN:ParkAtAirbase( SpawnAirbase, TerminalType, Parkingdata ) -- R2.2, R2.4, R2.5 self:F( { self.SpawnTemplatePrefix, SpawnAirbase, TerminalType } ) @@ -30301,42 +36991,41 @@ function SPAWN:ParkAtAirbase( SpawnAirbase, TerminalType, Parkingdata ) -- R2.2, for SpawnIndex = 2, self.SpawnMaxGroups do self:ParkAircraft( SpawnAirbase, TerminalType, Parkingdata, SpawnIndex ) - --self:ScheduleOnce( SpawnIndex * 0.1, SPAWN.ParkAircraft, self, SpawnAirbase, TerminalType, Parkingdata, SpawnIndex ) + -- self:ScheduleOnce( SpawnIndex * 0.1, SPAWN.ParkAircraft, self, SpawnAirbase, TerminalType, Parkingdata, SpawnIndex ) end - + self:SetSpawnIndex( 0 ) - + return nil end ---- Will spawn a group from a Vec3 in 3D space. +--- Will spawn a group from a Vec3 in 3D space. -- This method is mostly advisable to be used if you want to simulate spawning units in the air, like helicopters or airplanes. -- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. -- You can use the returned group to further define the route to be followed. -- @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 } ) local PointVec3 = POINT_VEC3:NewFromVec3( Vec3 ) - self:T2(PointVec3) + self:T2( PointVec3 ) if SpawnIndex then else SpawnIndex = self.SpawnIndex + 1 end - + if self:_GetSpawnIndex( SpawnIndex ) then - + local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate - + if SpawnTemplate then self:T( { "Current point of ", self.SpawnTemplatePrefix, Vec3 } ) - + local TemplateHeight = SpawnTemplate.route and SpawnTemplate.route.points[1].alt or nil SpawnTemplate.route = SpawnTemplate.route or {} @@ -30347,20 +37036,20 @@ function SPAWN:SpawnFromVec3( Vec3, SpawnIndex ) -- Translate the position of the Group Template to the Vec3. for UnitID = 1, #SpawnTemplate.units do - --self:T( 'Before Translation SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) + -- self:T( 'Before Translation SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) local UnitTemplate = SpawnTemplate.units[UnitID] local SX = UnitTemplate.x or 0 - local SY = UnitTemplate.y or 0 + local SY = UnitTemplate.y or 0 local BX = SpawnTemplate.route.points[1].x local BY = SpawnTemplate.route.points[1].y - local TX = Vec3.x + ( SX - BX ) - local TY = Vec3.z + ( SY - BY ) + local TX = Vec3.x + (SX - BX) + local TY = Vec3.z + (SY - BY) SpawnTemplate.units[UnitID].x = TX SpawnTemplate.units[UnitID].y = TY if SpawnTemplate.CategoryID ~= Group.Category.SHIP then SpawnTemplate.units[UnitID].alt = Vec3.y or TemplateHeight end - self:T( 'After Translation SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) + self:T( 'After Translation SpawnTemplate.units[' .. UnitID .. '].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units[' .. UnitID .. '].y = ' .. SpawnTemplate.units[UnitID].y ) end SpawnTemplate.route.points[1].x = Vec3.x SpawnTemplate.route.points[1].y = Vec3.z @@ -30370,32 +37059,28 @@ function SPAWN:SpawnFromVec3( Vec3, SpawnIndex ) SpawnTemplate.x = Vec3.x SpawnTemplate.y = Vec3.z SpawnTemplate.alt = Vec3.y or TemplateHeight - + return self:SpawnWithIndex( self.SpawnIndex ) end end - + return nil end - ---- Will spawn a group from a Coordinate in 3D space. +--- Will spawn a group from a Coordinate in 3D space. -- This method is mostly advisable to be used if you want to simulate spawning units in the air, like helicopters or airplanes. -- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. -- You can use the returned group to further define the route to be followed. -- @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 } ) return self:SpawnFromVec3( Coordinate:GetVec3(), SpawnIndex ) end - - --- Will spawn a group from a PointVec3 in 3D space. -- This method is mostly advisable to be used if you want to simulate spawning units in the air, like helicopters or airplanes. -- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. @@ -30403,22 +37088,20 @@ 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. --- +-- -- -- Spawn at the zone center position at 2000 meters from the ground! --- SpawnAirplanes:SpawnFromPointVec3( SpawnPointVec3 ) --- +-- SpawnAirplanes:SpawnFromPointVec3( SpawnPointVec3 ) +-- function SPAWN:SpawnFromPointVec3( PointVec3, SpawnIndex ) self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) return self:SpawnFromVec3( PointVec3:GetVec3(), SpawnIndex ) end - --- Will spawn a group from a Vec2 in 3D space. -- This method is mostly advisable to be used if you want to simulate spawning groups on the ground from air units, like vehicles. -- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. @@ -30428,32 +37111,30 @@ 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() --- +-- -- -- Spawn at the zone center position at the height specified in the ME of the group template! --- SpawnAirplanes:SpawnFromVec2( SpawnVec2 ) --- +-- SpawnAirplanes:SpawnFromVec2( SpawnVec2 ) +-- -- -- Spawn from the static position at the height randomized between 2000 and 4000 meters. --- SpawnAirplanes:SpawnFromVec2( SpawnVec2, 2000, 4000 ) --- +-- SpawnAirplanes:SpawnFromVec2( SpawnVec2, 2000, 4000 ) +-- function SPAWN:SpawnFromVec2( Vec2, MinHeight, MaxHeight, SpawnIndex ) self:F( { self.SpawnTemplatePrefix, self.SpawnIndex, Vec2, MinHeight, MaxHeight, SpawnIndex } ) local Height = nil if MinHeight and MaxHeight then - Height = math.random( MinHeight, MaxHeight) + Height = math.random( MinHeight, MaxHeight ) end - + return self:SpawnFromVec3( { x = Vec2.x, y = Height, z = Vec2.y }, SpawnIndex ) -- y can be nil. In this case, spawn on the ground for vehicles, and in the template altitude for air. end - ---- Will spawn a group from a POINT_VEC2 in 3D space. +--- Will spawn a group from a POINT_VEC2 in 3D space. -- This method is mostly advisable to be used if you want to simulate spawning groups on the ground from air units, like vehicles. -- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. -- You can use the returned group to further define the route to be followed. @@ -30462,26 +37143,23 @@ 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() --- +-- -- -- Spawn at the zone center position at the height specified in the ME of the group template! --- SpawnAirplanes:SpawnFromPointVec2( SpawnPointVec2 ) --- +-- SpawnAirplanes:SpawnFromPointVec2( SpawnPointVec2 ) +-- -- -- Spawn from the static position at the height randomized between 2000 and 4000 meters. --- SpawnAirplanes:SpawnFromPointVec2( SpawnPointVec2, 2000, 4000 ) --- +-- SpawnAirplanes:SpawnFromPointVec2( SpawnPointVec2, 2000, 4000 ) +-- function SPAWN:SpawnFromPointVec2( PointVec2, MinHeight, MaxHeight, SpawnIndex ) self:F( { self.SpawnTemplatePrefix, self.SpawnIndex } ) return self:SpawnFromVec2( PointVec2:GetVec2(), MinHeight, MaxHeight, SpawnIndex ) end - - --- Will spawn a group from a hosting unit. This method is mostly advisable to be used if you want to simulate spawning from air units, like helicopters, which are dropping infantry into a defined Landing Zone. -- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. -- You can use the returned group to further define the route to be followed. @@ -30493,22 +37171,22 @@ end -- @return Wrapper.Group#GROUP that was spawned. -- @return #nil Nothing was spawned. -- @usage --- +-- -- local SpawnStatic = STATIC:FindByName( StaticName ) --- +-- -- -- Spawn from the static position at the height specified in the ME of the group template! --- SpawnAirplanes:SpawnFromUnit( SpawnStatic ) --- +-- SpawnAirplanes:SpawnFromUnit( SpawnStatic ) +-- -- -- Spawn from the static position at the height randomized between 2000 and 4000 meters. --- SpawnAirplanes:SpawnFromUnit( SpawnStatic, 2000, 4000 ) --- +-- SpawnAirplanes:SpawnFromUnit( SpawnStatic, 2000, 4000 ) +-- function SPAWN:SpawnFromUnit( HostUnit, MinHeight, MaxHeight, SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, HostUnit, MinHeight, MaxHeight, SpawnIndex } ) + self:F( { self.SpawnTemplatePrefix, HostUnit, MinHeight, MaxHeight, SpawnIndex } ) if HostUnit and HostUnit:IsAlive() ~= nil then -- and HostUnit:getUnit(1):inAir() == false then return self:SpawnFromVec2( HostUnit:GetVec2(), MinHeight, MaxHeight, SpawnIndex ) end - + return nil end @@ -30519,30 +37197,29 @@ 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 ) --- +-- -- -- Spawn from the static position at the height specified in the ME of the group template! --- SpawnAirplanes:SpawnFromStatic( SpawnStatic ) --- +-- SpawnAirplanes:SpawnFromStatic( SpawnStatic ) +-- -- -- Spawn from the static position at the height randomized between 2000 and 4000 meters. --- SpawnAirplanes:SpawnFromStatic( SpawnStatic, 2000, 4000 ) --- +-- SpawnAirplanes:SpawnFromStatic( SpawnStatic, 2000, 4000 ) +-- function SPAWN:SpawnFromStatic( HostStatic, MinHeight, MaxHeight, SpawnIndex ) self:F( { self.SpawnTemplatePrefix, HostStatic, MinHeight, MaxHeight, SpawnIndex } ) if HostStatic and HostStatic:IsAlive() then return self:SpawnFromVec2( HostStatic:GetVec2(), MinHeight, MaxHeight, SpawnIndex ) end - + return nil end ---- Will spawn a Group within a given @{Zone}. --- The @{Zone} can be of any type derived from @{Core.Zone#ZONE_BASE}. +--- Will spawn a Group within a given @{Core.Zone}. +-- The @{Core.Zone} can be of any type derived from @{Core.Zone#ZONE_BASE}. -- Once the @{Wrapper.Group} is spawned within the zone, the @{Wrapper.Group} will continue on its route. -- The **first waypoint** (where the group is spawned) is replaced with the zone location coordinates. -- @param #SPAWN self @@ -30551,30 +37228,29 @@ 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 ) --- +-- -- -- Spawn at the zone center position at the height specified in the ME of the group template! --- SpawnAirplanes:SpawnInZone( SpawnZone ) --- +-- SpawnAirplanes:SpawnInZone( SpawnZone ) +-- -- -- Spawn in the zone at a random position at the height specified in the Me of the group template. --- SpawnAirplanes:SpawnInZone( SpawnZone, true ) --- +-- SpawnAirplanes:SpawnInZone( SpawnZone, true ) +-- -- -- Spawn in the zone at a random position at the height randomized between 2000 and 4000 meters. --- SpawnAirplanes:SpawnInZone( SpawnZone, true, 2000, 4000 ) --- +-- SpawnAirplanes:SpawnInZone( SpawnZone, true, 2000, 4000 ) +-- -- -- Spawn at the zone center position at the height randomized between 2000 and 4000 meters. --- SpawnAirplanes:SpawnInZone( SpawnZone, false, 2000, 4000 ) --- +-- SpawnAirplanes:SpawnInZone( SpawnZone, false, 2000, 4000 ) +-- -- -- Spawn at the zone center position at the height randomized between 2000 and 4000 meters. --- SpawnAirplanes:SpawnInZone( SpawnZone, nil, 2000, 4000 ) --- +-- SpawnAirplanes:SpawnInZone( SpawnZone, nil, 2000, 4000 ) +-- function SPAWN:SpawnInZone( Zone, RandomizeGroup, MinHeight, MaxHeight, SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, Zone, RandomizeGroup, MinHeight, MaxHeight, SpawnIndex } ) - + self:F( { self.SpawnTemplatePrefix, Zone, RandomizeGroup, MinHeight, MaxHeight, SpawnIndex } ) + if Zone then if RandomizeGroup then return self:SpawnFromVec2( Zone:GetRandomVec2(), MinHeight, MaxHeight, SpawnIndex ) @@ -30582,11 +37258,11 @@ function SPAWN:SpawnInZone( Zone, RandomizeGroup, MinHeight, MaxHeight, SpawnInd return self:SpawnFromVec2( Zone:GetVec2(), MinHeight, MaxHeight, SpawnIndex ) end end - + return nil end ---- (**AIR**) Will spawn a plane group in UnControlled or Controlled mode... +--- (**AIR**) Will spawn a plane group in UnControlled or Controlled mode... -- This will be similar to the uncontrolled flag setting in the ME. -- You can use UnControlled mode to simulate planes startup and ready for take-off but aren't moving (yet). -- ReSpawn the plane in Controlled mode, and the plane will move... @@ -30594,17 +37270,16 @@ end -- @param #boolean UnControlled true if UnControlled, false if Controlled. -- @return #SPAWN self function SPAWN:InitUnControlled( UnControlled ) - self:F2( { self.SpawnTemplatePrefix, UnControlled } ) - - self.SpawnUnControlled = ( UnControlled == true ) and true or nil - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self.SpawnGroups[SpawnGroupID].UnControlled = self.SpawnUnControlled - end - - return self -end + self:F2( { self.SpawnTemplatePrefix, UnControlled } ) + + self.SpawnUnControlled = (UnControlled == true) and true or nil + for SpawnGroupID = 1, self.SpawnMaxGroups do + self.SpawnGroups[SpawnGroupID].UnControlled = self.SpawnUnControlled + end + + return self +end --- Get the Coordinate of the Group that is Late Activated as the template for the SPAWN object. -- @param #SPAWN self @@ -30615,32 +37290,31 @@ function SPAWN:GetCoordinate() if LateGroup then return LateGroup:GetCoordinate() end - + return nil end - --- Will return the SpawnGroupName either with with a specific count number or without any count. -- @param #SPAWN self -- @param #number SpawnIndex Is the number of the Group that is to be spawned. -- @return #string SpawnGroupName function SPAWN:SpawnGroupName( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) - local SpawnPrefix = self.SpawnTemplatePrefix - if self.SpawnAliasPrefix then - SpawnPrefix = self.SpawnAliasPrefix - end + local SpawnPrefix = self.SpawnTemplatePrefix + if self.SpawnAliasPrefix then + SpawnPrefix = self.SpawnAliasPrefix + end + + if SpawnIndex then + local SpawnName = string.format( '%s#%03d', SpawnPrefix, SpawnIndex ) + self:T( SpawnName ) + return SpawnName + else + self:T( SpawnPrefix ) + return SpawnPrefix + end - if SpawnIndex then - local SpawnName = string.format( '%s#%03d', SpawnPrefix, SpawnIndex ) - self:T( SpawnName ) - return SpawnName - else - self:T( SpawnPrefix ) - return SpawnPrefix - end - end --- Will find the first alive @{Wrapper.Group} it has spawned, and return the alive @{Wrapper.Group} object and the first Index where the first alive @{Wrapper.Group} object has been found. @@ -30648,14 +37322,16 @@ end -- @return Wrapper.Group#GROUP, #number The @{Wrapper.Group} object found, the new Index where the group was found. -- @return #nil, #nil When no group is found, #nil is returned. -- @usage --- -- Find the first alive @{Wrapper.Group} object of the SpawnPlanes SPAWN object @{Wrapper.Group} collection that it has spawned during the mission. --- local GroupPlane, Index = SpawnPlanes:GetFirstAliveGroup() --- while GroupPlane ~= nil do --- -- Do actions with the GroupPlane object. --- GroupPlane, Index = SpawnPlanes:GetNextAliveGroup( Index ) --- end +-- +-- -- Find the first alive @{Wrapper.Group} object of the SpawnPlanes SPAWN object @{Wrapper.Group} collection that it has spawned during the mission. +-- local GroupPlane, Index = SpawnPlanes:GetFirstAliveGroup() +-- while GroupPlane ~= nil do +-- -- Do actions with the GroupPlane object. +-- GroupPlane, Index = SpawnPlanes:GetNextAliveGroup( Index ) +-- end +-- function SPAWN:GetFirstAliveGroup() - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) for SpawnIndex = 1, self.SpawnCount do local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) @@ -30663,25 +37339,26 @@ function SPAWN:GetFirstAliveGroup() return SpawnGroup, SpawnIndex end end - + return nil, nil end - --- Will find the next alive @{Wrapper.Group} object from a given Index, and return a reference to the alive @{Wrapper.Group} object and the next Index where the alive @{Wrapper.Group} has been found. -- @param #SPAWN self -- @param #number SpawnIndexStart A Index holding the start position to search from. This method can also be used to find the first alive @{Wrapper.Group} object from the given Index. -- @return Wrapper.Group#GROUP, #number The next alive @{Wrapper.Group} object found, the next Index where the next alive @{Wrapper.Group} object was found. -- @return #nil, #nil When no alive @{Wrapper.Group} object is found from the start Index position, #nil is returned. -- @usage --- -- Find the first alive @{Wrapper.Group} object of the SpawnPlanes SPAWN object @{Wrapper.Group} collection that it has spawned during the mission. --- local GroupPlane, Index = SpawnPlanes:GetFirstAliveGroup() --- while GroupPlane ~= nil do --- -- Do actions with the GroupPlane object. --- GroupPlane, Index = SpawnPlanes:GetNextAliveGroup( Index ) --- end +-- +-- -- Find the first alive @{Wrapper.Group} object of the SpawnPlanes SPAWN object @{Wrapper.Group} collection that it has spawned during the mission. +-- local GroupPlane, Index = SpawnPlanes:GetFirstAliveGroup() +-- while GroupPlane ~= nil do +-- -- Do actions with the GroupPlane object. +-- GroupPlane, Index = SpawnPlanes:GetNextAliveGroup( Index ) +-- end +-- function SPAWN:GetNextAliveGroup( SpawnIndexStart ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndexStart } ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndexStart } ) SpawnIndexStart = SpawnIndexStart + 1 for SpawnIndex = SpawnIndexStart, self.SpawnCount do @@ -30690,7 +37367,7 @@ function SPAWN:GetNextAliveGroup( SpawnIndexStart ) return SpawnGroup, SpawnIndex end end - + return nil, nil end @@ -30699,14 +37376,16 @@ end -- @return Wrapper.Group#GROUP, #number The last alive @{Wrapper.Group} object found, the last Index where the last alive @{Wrapper.Group} object was found. -- @return #nil, #nil When no alive @{Wrapper.Group} object is found, #nil is returned. -- @usage --- -- Find the last alive @{Wrapper.Group} object of the SpawnPlanes SPAWN object @{Wrapper.Group} collection that it has spawned during the mission. --- local GroupPlane, Index = SpawnPlanes:GetLastAliveGroup() --- if GroupPlane then -- GroupPlane can be nil!!! --- -- Do actions with the GroupPlane object. --- end +-- +-- -- Find the last alive @{Wrapper.Group} object of the SpawnPlanes SPAWN object @{Wrapper.Group} collection that it has spawned during the mission. +-- local GroupPlane, Index = SpawnPlanes:GetLastAliveGroup() +-- if GroupPlane then -- GroupPlane can be nil!!! +-- -- Do actions with the GroupPlane object. +-- end +-- function SPAWN:GetLastAliveGroup() - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) - + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) + for SpawnIndex = self.SpawnCount, 1, -1 do -- Added local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) if SpawnGroup and SpawnGroup:IsAlive() then @@ -30719,8 +37398,6 @@ function SPAWN:GetLastAliveGroup() return nil end - - --- Get the group from an index. -- Returns the group from the SpawnGroups list. -- If no index is given, it will return the first group in the list. @@ -30728,150 +37405,165 @@ end -- @param #number SpawnIndex The index of the group to return. -- @return Wrapper.Group#GROUP self function SPAWN:GetGroupFromIndex( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndex } ) - - if not SpawnIndex then + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndex } ) + + if not SpawnIndex then SpawnIndex = 1 - end - - if self.SpawnGroups and self.SpawnGroups[SpawnIndex] then - local SpawnGroup = self.SpawnGroups[SpawnIndex].Group - return SpawnGroup - else + end + + if self.SpawnGroups and self.SpawnGroups[SpawnIndex] then + local SpawnGroup = self.SpawnGroups[SpawnIndex].Group + return SpawnGroup + else return nil - end + end end - --- Return the prefix of a SpawnUnit. -- 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 return nil end - --- Get the index from a given group. -- The function will search the name of the group for a #, and will return the number behind the #-mark. function SPAWN:GetSpawnIndexFromGroup( SpawnGroup ) - self:F2( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnGroup } ) - - local IndexString = string.match( SpawnGroup:GetName(), "#(%d*)$" ):sub( 2 ) - local Index = tonumber( IndexString ) - - self:T3( IndexString, Index ) - return Index - + self:F2( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnGroup } ) + + local IndexString = string.match( SpawnGroup:GetName(), "#(%d*)$" ):sub( 2 ) + local Index = tonumber( IndexString ) + + self:T3( IndexString, Index ) + return Index + end --- Return the last maximum index that can be used. function SPAWN:_GetLastIndex() - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) - return self.SpawnMaxGroups + return self.SpawnMaxGroups end --- Initalize the SpawnGroups collection. -- @param #SPAWN self function SPAWN:_InitializeSpawnGroups( SpawnIndex ) - self:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndex } ) - - if not self.SpawnGroups[SpawnIndex] then - self.SpawnGroups[SpawnIndex] = {} - self.SpawnGroups[SpawnIndex].Visible = false - self.SpawnGroups[SpawnIndex].Spawned = false - self.SpawnGroups[SpawnIndex].UnControlled = false - self.SpawnGroups[SpawnIndex].SpawnTime = 0 - - self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix = self.SpawnTemplatePrefix - self.SpawnGroups[SpawnIndex].SpawnTemplate = self:_Prepare( self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix, SpawnIndex ) - end - - self:_RandomizeTemplate( SpawnIndex ) - self:_RandomizeRoute( SpawnIndex ) - --self:_TranslateRotate( SpawnIndex ) - - return self.SpawnGroups[SpawnIndex] -end + self:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndex } ) + + if not self.SpawnGroups[SpawnIndex] then + self.SpawnGroups[SpawnIndex] = {} + self.SpawnGroups[SpawnIndex].Visible = false + self.SpawnGroups[SpawnIndex].Spawned = false + self.SpawnGroups[SpawnIndex].UnControlled = false + self.SpawnGroups[SpawnIndex].SpawnTime = 0 + + self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix = self.SpawnTemplatePrefix + self.SpawnGroups[SpawnIndex].SpawnTemplate = self:_Prepare( self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix, SpawnIndex ) + end + self:_RandomizeTemplate( SpawnIndex ) + self:_RandomizeRoute( SpawnIndex ) + -- self:_TranslateRotate( SpawnIndex ) + return self.SpawnGroups[SpawnIndex] +end --- Gets the CategoryID of the Group with the given SpawnPrefix function SPAWN:_GetGroupCategoryID( SpawnPrefix ) - local TemplateGroup = Group.getByName( SpawnPrefix ) - - if TemplateGroup then - return TemplateGroup:getCategory() - else - return nil - end + local TemplateGroup = Group.getByName( SpawnPrefix ) + + if TemplateGroup then + return TemplateGroup:getCategory() + else + return nil + end end --- Gets the CoalitionID of the Group with the given SpawnPrefix function SPAWN:_GetGroupCoalitionID( SpawnPrefix ) - local TemplateGroup = Group.getByName( SpawnPrefix ) - - if TemplateGroup then - return TemplateGroup:getCoalition() - else - return nil - end + local TemplateGroup = Group.getByName( SpawnPrefix ) + + if TemplateGroup then + return TemplateGroup:getCoalition() + else + return nil + end end --- Gets the CountryID of the Group with the given SpawnPrefix function SPAWN:_GetGroupCountryID( SpawnPrefix ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnPrefix } ) - - local TemplateGroup = Group.getByName( SpawnPrefix ) - - if TemplateGroup then - local TemplateUnits = TemplateGroup:getUnits() - return TemplateUnits[1]:getCountry() - else - return nil - end + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnPrefix } ) + + local TemplateGroup = Group.getByName( SpawnPrefix ) + + if TemplateGroup then + local TemplateUnits = TemplateGroup:getUnits() + return TemplateUnits[1]:getCountry() + else + return nil + end end --- Gets the Group Template from the ME environment definition. --- This method used the @{DATABASE} object, which contains ALL initial and new spawned object in MOOSE. +-- Note: This method uses the global _DATABASE object (an instance of @{Core.Database#DATABASE}), which contains ALL initial and new spawned objects in MOOSE. -- @param #SPAWN self -- @param #string SpawnTemplatePrefix -- @return @SPAWN self function SPAWN:_GetTemplate( SpawnTemplatePrefix ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnTemplatePrefix } ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnTemplatePrefix } ) - local SpawnTemplate = nil + local SpawnTemplate = nil local Template = _DATABASE.Templates.Groups[SpawnTemplatePrefix].Template self:F( { Template = Template } ) - SpawnTemplate = UTILS.DeepCopy( _DATABASE.Templates.Groups[SpawnTemplatePrefix].Template ) - - if SpawnTemplate == nil then - error( 'No Template returned for SpawnTemplatePrefix = ' .. SpawnTemplatePrefix ) - end + SpawnTemplate = UTILS.DeepCopy( _DATABASE.Templates.Groups[SpawnTemplatePrefix].Template ) - --SpawnTemplate.SpawnCoalitionID = self:_GetGroupCoalitionID( SpawnTemplatePrefix ) - --SpawnTemplate.SpawnCategoryID = self:_GetGroupCategoryID( SpawnTemplatePrefix ) - --SpawnTemplate.SpawnCountryID = self:_GetGroupCountryID( SpawnTemplatePrefix ) - - self:T3( { SpawnTemplate } ) - return SpawnTemplate + if SpawnTemplate == nil then + error( 'No Template returned for SpawnTemplatePrefix = ' .. SpawnTemplatePrefix ) + end + + -- SpawnTemplate.SpawnCoalitionID = self:_GetGroupCoalitionID( SpawnTemplatePrefix ) + -- SpawnTemplate.SpawnCategoryID = self:_GetGroupCategoryID( SpawnTemplatePrefix ) + -- SpawnTemplate.SpawnCountryID = self:_GetGroupCountryID( SpawnTemplatePrefix ) + + self:T3( { SpawnTemplate } ) + return SpawnTemplate end --- Prepares the new Group Template. @@ -30879,36 +37571,35 @@ end -- @param #string SpawnTemplatePrefix -- @param #number SpawnIndex -- @return #SPAWN self -function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) --R2.2 - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) - --- if not self.SpawnTemplate then --- self.SpawnTemplate = self:_GetTemplate( SpawnTemplatePrefix ) --- end - +function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) -- R2.2 + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) + + -- if not self.SpawnTemplate then + -- self.SpawnTemplate = self:_GetTemplate( SpawnTemplatePrefix ) + -- end + local SpawnTemplate if self.TweakedTemplate ~= nil and self.TweakedTemplate == true then - BASE:I("WARNING: You are using a tweaked template.") + BASE:I( "WARNING: You are using a tweaked template." ) SpawnTemplate = self.SpawnTemplate else SpawnTemplate = self:_GetTemplate( SpawnTemplatePrefix ) SpawnTemplate.name = self:SpawnGroupName( SpawnIndex ) end - - SpawnTemplate.groupId = nil - --SpawnTemplate.lateActivation = false - SpawnTemplate.lateActivation = self.LateActivated or false + SpawnTemplate.groupId = nil + -- SpawnTemplate.lateActivation = false + SpawnTemplate.lateActivation = self.LateActivated or false - if SpawnTemplate.CategoryID == Group.Category.GROUND then - self:T3( "For ground units, visible needs to be false..." ) - SpawnTemplate.visible = false - end - - if self.SpawnGrouping then - local UnitAmount = #SpawnTemplate.units - self:F( { UnitAmount = UnitAmount, SpawnGrouping = self.SpawnGrouping } ) - if UnitAmount > self.SpawnGrouping then + if SpawnTemplate.CategoryID == Group.Category.GROUND then + self:T3( "For ground units, visible needs to be false..." ) + SpawnTemplate.visible = false + end + + if self.SpawnGrouping then + local UnitAmount = #SpawnTemplate.units + self:F( { UnitAmount = UnitAmount, SpawnGrouping = self.SpawnGrouping } ) + if UnitAmount > self.SpawnGrouping then for UnitID = self.SpawnGrouping + 1, UnitAmount do SpawnTemplate.units[UnitID] = nil end @@ -30921,17 +37612,17 @@ function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) --R2.2 end end end - + if self.SpawnInitKeepUnitNames == false then - for UnitID = 1, #SpawnTemplate.units do - SpawnTemplate.units[UnitID].name = string.format( SpawnTemplate.name .. '-%02d', UnitID ) - SpawnTemplate.units[UnitID].unitId = nil - end + for UnitID = 1, #SpawnTemplate.units do + SpawnTemplate.units[UnitID].name = string.format( SpawnTemplate.name .. '-%02d', UnitID ) + SpawnTemplate.units[UnitID].unitId = nil + end else for UnitID = 1, #SpawnTemplate.units do local UnitPrefix, Rest = string.match( SpawnTemplate.units[UnitID].name, "^([^#]+)#?" ):gsub( "^%s*(.-)%s*$", "%1" ) self:T( { UnitPrefix, Rest } ) - + SpawnTemplate.units[UnitID].name = string.format( '%s#%03d-%02d', UnitPrefix, SpawnIndex, UnitID ) SpawnTemplate.units[UnitID].unitId = nil end @@ -30941,20 +37632,21 @@ function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) --R2.2 for UnitID = 1, #SpawnTemplate.units do local Callsign = SpawnTemplate.units[UnitID].callsign if Callsign then - if type(Callsign) ~= "number" then -- blue callsign - Callsign[2] = ( ( SpawnIndex - 1 ) % 10 ) + 1 + if type( Callsign ) ~= "number" then -- blue callsign + Callsign[2] = ((SpawnIndex - 1) % 10) + 1 local CallsignName = SpawnTemplate.units[UnitID].callsign["name"] -- #string + CallsignName = string.match(CallsignName,"^(%a+)") -- 2.8 - only the part w/o numbers local CallsignLen = CallsignName:len() - SpawnTemplate.units[UnitID].callsign["name"] = CallsignName:sub(1,CallsignLen) .. SpawnTemplate.units[UnitID].callsign[2] .. SpawnTemplate.units[UnitID].callsign[3] + SpawnTemplate.units[UnitID].callsign["name"] = CallsignName:sub( 1, CallsignLen ) .. SpawnTemplate.units[UnitID].callsign[2] .. SpawnTemplate.units[UnitID].callsign[3] else SpawnTemplate.units[UnitID].callsign = Callsign + SpawnIndex end end end - - self:T3( { "Template:", SpawnTemplate } ) - return SpawnTemplate - + + self:T3( { "Template:", SpawnTemplate } ) + return SpawnTemplate + end --- Private method randomizing the routes. @@ -30962,17 +37654,17 @@ end -- @param #number SpawnIndex The index of the group to be spawned. -- @return #SPAWN function SPAWN:_RandomizeRoute( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeRoute, self.SpawnRandomizeRouteStartPoint, self.SpawnRandomizeRouteEndPoint, self.SpawnRandomizeRouteRadius } ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeRoute, self.SpawnRandomizeRouteStartPoint, self.SpawnRandomizeRouteEndPoint, self.SpawnRandomizeRouteRadius } ) if self.SpawnRandomizeRoute then local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate local RouteCount = #SpawnTemplate.route.points - - for t = self.SpawnRandomizeRouteStartPoint + 1, ( RouteCount - self.SpawnRandomizeRouteEndPoint ) do - + + for t = self.SpawnRandomizeRouteStartPoint + 1, (RouteCount - self.SpawnRandomizeRouteEndPoint) do + SpawnTemplate.route.points[t].x = SpawnTemplate.route.points[t].x + math.random( self.SpawnRandomizeRouteRadius * -1, self.SpawnRandomizeRouteRadius ) SpawnTemplate.route.points[t].y = SpawnTemplate.route.points[t].y + math.random( self.SpawnRandomizeRouteRadius * -1, self.SpawnRandomizeRouteRadius ) - + -- Manage randomization of altitude for airborne units ... if SpawnTemplate.CategoryID == Group.Category.AIRPLANE or SpawnTemplate.CategoryID == Group.Category.HELICOPTER then if SpawnTemplate.route.points[t].alt and self.SpawnRandomizeRouteHeight then @@ -30981,13 +37673,13 @@ function SPAWN:_RandomizeRoute( SpawnIndex ) else SpawnTemplate.route.points[t].alt = nil end - + self:T( 'SpawnTemplate.route.points[' .. t .. '].x = ' .. SpawnTemplate.route.points[t].x .. ', SpawnTemplate.route.points[' .. t .. '].y = ' .. SpawnTemplate.route.points[t].y ) end end - + self:_RandomizeZones( SpawnIndex ) - + return self end @@ -30996,10 +37688,10 @@ end -- @param #number SpawnIndex -- @return #SPAWN self function SPAWN:_RandomizeTemplate( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeTemplate } ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeTemplate } ) if self.SpawnRandomizeTemplate then - self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix = self.SpawnTemplatePrefixTable[ math.random( 1, #self.SpawnTemplatePrefixTable ) ] + self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix = self.SpawnTemplatePrefixTable[math.random( 1, #self.SpawnTemplatePrefixTable )] self.SpawnGroups[SpawnIndex].SpawnTemplate = self:_Prepare( self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix, SpawnIndex ) self.SpawnGroups[SpawnIndex].SpawnTemplate.route = UTILS.DeepCopy( self.SpawnTemplate.route ) self.SpawnGroups[SpawnIndex].SpawnTemplate.x = self.SpawnTemplate.x @@ -31009,18 +37701,18 @@ function SPAWN:_RandomizeTemplate( SpawnIndex ) local OldY = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[1].y for UnitID = 1, #self.SpawnGroups[SpawnIndex].SpawnTemplate.units do self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].heading = self.SpawnTemplate.units[1].heading - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].x = self.SpawnTemplate.units[1].x + ( self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].x - OldX ) - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].y = self.SpawnTemplate.units[1].y + ( self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].y - OldY ) + self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].x = self.SpawnTemplate.units[1].x + (self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].x - OldX) + self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].y = self.SpawnTemplate.units[1].y + (self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].y - OldY) self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].alt = self.SpawnTemplate.units[1].alt end end - + self:_RandomizeRoute( SpawnIndex ) - + return self end ---- Private method that randomizes the @{Zone}s where the Group will be spawned. +--- Private method that randomizes the @{Core.Zone}s where the Group will be spawned. -- @param #SPAWN self -- @param #number SpawnIndex -- @return #SPAWN self @@ -31033,84 +37725,79 @@ function SPAWN:_RandomizeZones( SpawnIndex ) self:T( { SpawnZoneTableCount = #self.SpawnZoneTable, self.SpawnZoneTable } ) local ZoneID = math.random( #self.SpawnZoneTable ) self:T( ZoneID ) - SpawnZone = self.SpawnZoneTable[ ZoneID ]:GetZoneMaybe() + SpawnZone = self.SpawnZoneTable[ZoneID]:GetZoneMaybe() end - + self:T( "Preparing Spawn in Zone", SpawnZone:GetName() ) - + local SpawnVec2 = SpawnZone:GetRandomVec2() - + self:T( { SpawnVec2 = SpawnVec2 } ) - + local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate - + self:T( { Route = SpawnTemplate.route } ) - + for UnitID = 1, #SpawnTemplate.units do local UnitTemplate = SpawnTemplate.units[UnitID] - self:T( 'Before Translation SpawnTemplate.units['..UnitID..'].x = ' .. UnitTemplate.x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. UnitTemplate.y ) + self:T( 'Before Translation SpawnTemplate.units[' .. UnitID .. '].x = ' .. UnitTemplate.x .. ', SpawnTemplate.units[' .. UnitID .. '].y = ' .. UnitTemplate.y ) local SX = UnitTemplate.x - local SY = UnitTemplate.y + local SY = UnitTemplate.y local BX = SpawnTemplate.route.points[1].x local BY = SpawnTemplate.route.points[1].y - local TX = SpawnVec2.x + ( SX - BX ) - local TY = SpawnVec2.y + ( SY - BY ) + local TX = SpawnVec2.x + (SX - BX) + local TY = SpawnVec2.y + (SY - BY) UnitTemplate.x = TX UnitTemplate.y = TY -- TODO: Manage altitude based on landheight... - --SpawnTemplate.units[UnitID].alt = SpawnVec2: - self:T( 'After Translation SpawnTemplate.units['..UnitID..'].x = ' .. UnitTemplate.x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. UnitTemplate.y ) + -- SpawnTemplate.units[UnitID].alt = SpawnVec2: + self:T( 'After Translation SpawnTemplate.units[' .. UnitID .. '].x = ' .. UnitTemplate.x .. ', SpawnTemplate.units[' .. UnitID .. '].y = ' .. UnitTemplate.y ) end SpawnTemplate.x = SpawnVec2.x SpawnTemplate.y = SpawnVec2.y SpawnTemplate.route.points[1].x = SpawnVec2.x SpawnTemplate.route.points[1].y = SpawnVec2.y end - + return self - + end function SPAWN:_TranslateRotate( SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle } ) - + self:F( { self.SpawnTemplatePrefix, SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle } ) + -- Translate local TranslatedX = SpawnX local TranslatedY = SpawnY - + -- Rotate -- From Wikipedia: https://en.wikipedia.org/wiki/Rotation_matrix#Common_rotations -- x' = x \cos \theta - y \sin \theta\ -- y' = x \sin \theta + y \cos \theta\ - local RotatedX = - TranslatedX * math.cos( math.rad( SpawnAngle ) ) - + TranslatedY * math.sin( math.rad( SpawnAngle ) ) - local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) - + TranslatedY * math.cos( math.rad( SpawnAngle ) ) - + local RotatedX = -TranslatedX * math.cos( math.rad( SpawnAngle ) ) + TranslatedY * math.sin( math.rad( SpawnAngle ) ) + local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) + TranslatedY * math.cos( math.rad( SpawnAngle ) ) + -- Assign self.SpawnGroups[SpawnIndex].SpawnTemplate.x = SpawnRootX - RotatedX self.SpawnGroups[SpawnIndex].SpawnTemplate.y = SpawnRootY + RotatedY - local SpawnUnitCount = table.getn( self.SpawnGroups[SpawnIndex].SpawnTemplate.units ) for u = 1, SpawnUnitCount do - + -- Translate - local TranslatedX = SpawnX - local TranslatedY = SpawnY - 10 * ( u - 1 ) - + local TranslatedX = SpawnX + local TranslatedY = SpawnY - 10 * (u - 1) + -- Rotate - local RotatedX = - TranslatedX * math.cos( math.rad( SpawnAngle ) ) - + TranslatedY * math.sin( math.rad( SpawnAngle ) ) - local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) - + TranslatedY * math.cos( math.rad( SpawnAngle ) ) - + local RotatedX = -TranslatedX * math.cos( math.rad( SpawnAngle ) ) + TranslatedY * math.sin( math.rad( SpawnAngle ) ) + local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) + TranslatedY * math.cos( math.rad( SpawnAngle ) ) + -- Assign self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].x = SpawnRootX - RotatedX self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].y = SpawnRootY + RotatedY self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].heading = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].heading + math.rad( SpawnAngle ) end - + return self end @@ -31119,10 +37806,10 @@ end -- @param #number SpawnIndex Spawn index. -- @return #number self.SpawnIndex function SPAWN:_GetSpawnIndex( SpawnIndex ) - self:F2( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive, self.AliveUnits, #self.SpawnTemplate.units } ) - - if ( self.SpawnMaxGroups == 0 ) or ( SpawnIndex <= self.SpawnMaxGroups ) then - if ( self.SpawnMaxUnitsAlive == 0 ) or ( self.AliveUnits + #self.SpawnTemplate.units <= self.SpawnMaxUnitsAlive ) or self.UnControlled == true then + self:F2( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive, self.AliveUnits, #self.SpawnTemplate.units } ) + + if (self.SpawnMaxGroups == 0) or (SpawnIndex <= self.SpawnMaxGroups) then + if (self.SpawnMaxUnitsAlive == 0) or (self.AliveUnits + #self.SpawnTemplate.units <= self.SpawnMaxUnitsAlive) or self.UnControlled == true then self:F( { SpawnCount = self.SpawnCount, SpawnIndex = SpawnIndex } ) if SpawnIndex and SpawnIndex >= self.SpawnCount + 1 then self.SpawnCount = self.SpawnCount + 1 @@ -31138,11 +37825,10 @@ function SPAWN:_GetSpawnIndex( SpawnIndex ) else return nil end - + return self.SpawnIndex end - -- TODO Need to delete this... _DATABASE does this now ... --- @param #SPAWN self @@ -31151,40 +37837,43 @@ function SPAWN:_OnBirth( 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! - self:T( { "Birth Event:", EventPrefix, self.SpawnTemplatePrefix } ) - if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then - self.AliveUnits = self.AliveUnits + 1 - self:T( "Alive Units: " .. self.AliveUnits ) - end + self:T( { "Birth Event:", EventPrefix, self.SpawnTemplatePrefix } ) + 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 ---- Obscolete --- @todo Need to delete this... _DATABASE does this now ... - --- @param #SPAWN self -- @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... @@ -31199,12 +37888,12 @@ function SPAWN:_OnTakeOff( EventData ) local EventPrefix = self:_GetPrefixFromGroup( SpawnGroup ) if EventPrefix then -- EventPrefix can be nil if no # is found, which means, no spawnable group! self:T( { "TakeOff event: " .. EventPrefix } ) - if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then - self:T( "self.Landed = false" ) - SpawnGroup:SetState( SpawnGroup, "Spawn_Landed", false ) + if EventPrefix == self.SpawnTemplatePrefix or (self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix) then + self:T( "self.Landed = false" ) + SpawnGroup:SetState( SpawnGroup, "Spawn_Landed", false ) end end - end + end end --- Will detect AIR Units landing... When the event takes place, the spawned Group is registered as landed. @@ -31219,20 +37908,20 @@ function SPAWN:_OnLand( EventData ) local EventPrefix = self:_GetPrefixFromGroup( SpawnGroup ) if EventPrefix then -- EventPrefix can be nil if no # is found, which means, no spawnable group! self:T( { "Land event: " .. EventPrefix } ) - if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then - -- TODO: Check if this is the last unit of the group that lands. - SpawnGroup:SetState( SpawnGroup, "Spawn_Landed", true ) - if self.RepeatOnLanding then - local SpawnGroupIndex = self:GetSpawnIndexFromGroup( SpawnGroup ) - self:T( { "Landed:", "ReSpawn:", SpawnGroup:GetName(), SpawnGroupIndex } ) - --self:ReSpawn( SpawnGroupIndex ) - -- Delay respawn by three seconds due to DCS 2.5.4.26368 OB bug https://github.com/FlightControl-Master/MOOSE/issues/1076 - -- Bug was initially only for engine shutdown event but after ED "fixed" it, it now happens on landing events. - SCHEDULER:New(nil, self.ReSpawn, {self, SpawnGroupIndex}, 3) - end - end + if EventPrefix == self.SpawnTemplatePrefix or (self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix) then + -- TODO: Check if this is the last unit of the group that lands. + SpawnGroup:SetState( SpawnGroup, "Spawn_Landed", true ) + if self.RepeatOnLanding then + local SpawnGroupIndex = self:GetSpawnIndexFromGroup( SpawnGroup ) + self:T( { "Landed:", "ReSpawn:", SpawnGroup:GetName(), SpawnGroupIndex } ) + -- self:ReSpawn( SpawnGroupIndex ) + -- Delay respawn by three seconds due to DCS 2.5.4.26368 OB bug https://github.com/FlightControl-Master/MOOSE/issues/1076 + -- Bug was initially only for engine shutdown event but after ED "fixed" it, it now happens on landing events. + SCHEDULER:New( nil, self.ReSpawn, { self, SpawnGroupIndex }, 3 ) + end + end end - end + end end --- Will detect AIR Units shutting down their engines ... @@ -31248,78 +37937,81 @@ function SPAWN:_OnEngineShutDown( EventData ) local EventPrefix = self:_GetPrefixFromGroup( SpawnGroup ) if EventPrefix then -- EventPrefix can be nil if no # is found, which means, no spawnable group! self:T( { "EngineShutdown event: " .. EventPrefix } ) - if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then - -- todo: test if on the runway - local Landed = SpawnGroup:GetState( SpawnGroup, "Spawn_Landed" ) - if Landed and self.RepeatOnEngineShutDown then - local SpawnGroupIndex = self:GetSpawnIndexFromGroup( SpawnGroup ) - self:T( { "EngineShutDown: ", "ReSpawn:", SpawnGroup:GetName(), SpawnGroupIndex } ) - --self:ReSpawn( SpawnGroupIndex ) - -- Delay respawn by three seconds due to DCS 2.5.4 OB bug https://github.com/FlightControl-Master/MOOSE/issues/1076 - SCHEDULER:New(nil, self.ReSpawn, {self, SpawnGroupIndex}, 3) - end - end + if EventPrefix == self.SpawnTemplatePrefix or (self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix) then + -- todo: test if on the runway + local Landed = SpawnGroup:GetState( SpawnGroup, "Spawn_Landed" ) + if Landed and self.RepeatOnEngineShutDown then + local SpawnGroupIndex = self:GetSpawnIndexFromGroup( SpawnGroup ) + self:T( { "EngineShutDown: ", "ReSpawn:", SpawnGroup:GetName(), SpawnGroupIndex } ) + -- self:ReSpawn( SpawnGroupIndex ) + -- Delay respawn by three seconds due to DCS 2.5.4 OB bug https://github.com/FlightControl-Master/MOOSE/issues/1076 + SCHEDULER:New( nil, self.ReSpawn, { self, SpawnGroupIndex }, 3 ) + end + end end - end + end end --- This function is called automatically by the Spawning scheduler. -- It is the internal worker method SPAWNing new Groups on the defined time intervals. -- @param #SPAWN self function SPAWN:_Scheduler() - self:F2( { "_Scheduler", self.SpawnTemplatePrefix, self.SpawnAliasPrefix, self.SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive } ) - - -- Validate if there are still groups left in the batch... - self:Spawn() - - return true + self:F2( { "_Scheduler", self.SpawnTemplatePrefix, self.SpawnAliasPrefix, self.SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive } ) + + -- Validate if there are still groups left in the batch... + self:Spawn() + + return true end --- Schedules the CleanUp of Groups -- @param #SPAWN self -- @return #boolean True = Continue Scheduler function SPAWN:_SpawnCleanUpScheduler() - self:F( { "CleanUp Scheduler:", self.SpawnTemplatePrefix } ) + self:F( { "CleanUp Scheduler:", self.SpawnTemplatePrefix } ) + + local SpawnGroup, SpawnCursor = self:GetFirstAliveGroup() + self:T( { "CleanUp Scheduler:", SpawnGroup, SpawnCursor } ) - local SpawnGroup, SpawnCursor = self:GetFirstAliveGroup() - self:T( { "CleanUp Scheduler:", SpawnGroup, SpawnCursor } ) + local IsHelo = false + + while SpawnGroup do + + IsHelo = SpawnGroup:IsHelicopter() + + local SpawnUnits = SpawnGroup:GetUnits() - while SpawnGroup do + for UnitID, UnitData in pairs( SpawnUnits ) do - local SpawnUnits = SpawnGroup:GetUnits() - - for UnitID, UnitData in pairs( SpawnUnits ) do - - local SpawnUnit = UnitData -- Wrapper.Unit#UNIT - local SpawnUnitName = SpawnUnit:GetName() - - - self.SpawnCleanUpTimeStamps[SpawnUnitName] = self.SpawnCleanUpTimeStamps[SpawnUnitName] or {} - local Stamp = self.SpawnCleanUpTimeStamps[SpawnUnitName] + local SpawnUnit = UnitData -- Wrapper.Unit#UNIT + local SpawnUnitName = SpawnUnit:GetName() + + self.SpawnCleanUpTimeStamps[SpawnUnitName] = self.SpawnCleanUpTimeStamps[SpawnUnitName] or {} + local Stamp = self.SpawnCleanUpTimeStamps[SpawnUnitName] self:T( { SpawnUnitName, Stamp } ) - - if Stamp.Vec2 then - if SpawnUnit:InAir() == false and SpawnUnit:GetVelocityKMH() < 1 then - local NewVec2 = SpawnUnit:GetVec2() - if (Stamp.Vec2.x == NewVec2.x and Stamp.Vec2.y == NewVec2.y) or (SpawnUnit:GetLife() <= 1) then - -- If the plane is not moving or dead , and is on the ground, assign it with a timestamp... - if Stamp.Time + self.SpawnCleanUpInterval < timer.getTime() then - self:T( { "CleanUp Scheduler:", "ReSpawning:", SpawnGroup:GetName() } ) - self:ReSpawn( SpawnCursor ) + + if Stamp.Vec2 then + if (SpawnUnit:InAir() == false and SpawnUnit:GetVelocityKMH() < 1) or IsHelo then + local NewVec2 = SpawnUnit:GetVec2() or {x=0, y=0} + if (Stamp.Vec2.x == NewVec2.x and Stamp.Vec2.y == NewVec2.y) or (SpawnUnit:GetLife() <= 1) then + -- If the plane is not moving or dead , and is on the ground, assign it with a timestamp... + if Stamp.Time + self.SpawnCleanUpInterval < timer.getTime() then + self:T( { "CleanUp Scheduler:", "ReSpawning:", SpawnGroup:GetName() } ) + self:ReSpawn( SpawnCursor ) Stamp.Vec2 = nil Stamp.Time = nil - end - else - Stamp.Time = timer.getTime() + end + else + Stamp.Time = timer.getTime() Stamp.Vec2 = SpawnUnit:GetVec2() - end - else - Stamp.Vec2 = nil - Stamp.Time = nil - end - else - if SpawnUnit:InAir() == false then - Stamp.Vec2 = SpawnUnit:GetVec2() + end + else + Stamp.Vec2 = nil + Stamp.Time = nil + end + else + if SpawnUnit:InAir() == false or (IsHelo and SpawnUnit:GetLife() <= 1) then + Stamp.Vec2 = SpawnUnit:GetVec2() or {x=0, y=0} if (SpawnUnit:GetVelocityKMH() < 1) then Stamp.Time = timer.getTime() end @@ -31329,15 +38021,15 @@ function SPAWN:_SpawnCleanUpScheduler() end end end - - SpawnGroup, SpawnCursor = self:GetNextAliveGroup( SpawnCursor ) - - self:T( { "CleanUp Scheduler:", SpawnGroup, SpawnCursor } ) - - end - - return true -- Repeat - + + SpawnGroup, SpawnCursor = self:GetNextAliveGroup( SpawnCursor ) + + self:T( { "CleanUp Scheduler:", SpawnGroup, SpawnCursor } ) + + end + + return true -- Repeat + end --- **Core** - Spawn statics. -- @@ -31387,26 +38079,26 @@ 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 ---- Allows to spawn dynamically new @{Static}s into your mission. +--- Allows to spawn dynamically new @{Wrapper.Static}s into your mission. -- -- Through creating a copy of an existing static object template as defined in the Mission Editor (ME), SPAWNSTATIC can retireve the properties of the defined static object template (like type, category etc), -- and "copy" these properties to create a new static object and place it at the desired coordinate. -- --- New spawned @{Static}s get **the same name** as the name of the template Static, or gets the given name when a new name is provided at the Spawn method. --- By default, spawned @{Static}s will follow a naming convention at run-time: +-- New spawned @{Wrapper.Static}s get **the same name** as the name of the template Static, or gets the given name when a new name is provided at the Spawn method. +-- By default, spawned @{Wrapper.Static}s will follow a naming convention at run-time: -- --- * Spawned @{Static}s will have the name _StaticName_#_nnn_, where _StaticName_ is the name of the **Template Static**, and _nnn_ is a **counter from 0 to 99999**. +-- * Spawned @{Wrapper.Static}s will have the name _StaticName_#_nnn_, where _StaticName_ is the name of the **Template Static**, and _nnn_ is a **counter from 0 to 99999**. -- -- # SPAWNSTATIC Constructors -- @@ -31447,7 +38139,7 @@ end -- * @{#SPAWNSTATIC.Spawn}(Heading, NewName) spawns the static with the set parameters. Optionally, heading and name can be given. The name **must be unique**! -- * @{#SPAWNSTATIC.SpawnFromCoordinate}(Coordinate, Heading, NewName) spawn the static at the given coordinate. Optionally, heading and name can be given. The name **must be unique**! -- * @{#SPAWNSTATIC.SpawnFromPointVec2}(PointVec2, Heading, NewName) spawns the static at a POINT_VEC2 coordinate. Optionally, heading and name can be given. The name **must be unique**! --- * @{#SPAWNSTATIC.SpawnFromZone}(Zone, Heading, NewName) spawns the static at the center of a @{Zone}. Optionally, heading and name can be given. The name **must be unique**! +-- * @{#SPAWNSTATIC.SpawnFromZone}(Zone, Heading, NewName) spawns the static at the center of a @{Core.Zone}. Optionally, heading and name can be given. The name **must be unique**! -- -- @field #SPAWNSTATIC SPAWNSTATIC -- @@ -31472,34 +38164,34 @@ SPAWNSTATIC = { -- @field #number mass Cargo mass in kg. -- @field #boolean canCargo Static can be a cargo. ---- Creates the main object to spawn a @{Static} defined in the mission editor (ME). +--- Creates the main object to spawn a @{Wrapper.Static} defined in the mission editor (ME). -- @param #SPAWNSTATIC self -- @param #string SpawnTemplateName Name of the static object in the ME. Each new static will have the name starting with this prefix. -- @param DCS#country.id SpawnCountryID (Optional) The ID of the country. -- @return #SPAWNSTATIC self function SPAWNSTATIC:NewFromStatic(SpawnTemplateName, SpawnCountryID) - local self = BASE:Inherit( self, BASE:New() ) -- #SPAWNSTATIC + local self = BASE:Inherit( self, BASE:New() ) -- #SPAWNSTATIC - local TemplateStatic, CoalitionID, CategoryID, CountryID = _DATABASE:GetStaticGroupTemplate(SpawnTemplateName) - - if TemplateStatic then - self.SpawnTemplatePrefix = SpawnTemplateName - self.TemplateStaticUnit = UTILS.DeepCopy(TemplateStatic.units[1]) - self.CountryID = SpawnCountryID or CountryID - self.CategoryID = CategoryID - self.CoalitionID = CoalitionID - self.SpawnIndex = 0 - else - error( "SPAWNSTATIC:New: There is no static declared in the mission editor with SpawnTemplatePrefix = '" .. tostring(SpawnTemplateName) .. "'" ) - end + local TemplateStatic, CoalitionID, CategoryID, CountryID = _DATABASE:GetStaticGroupTemplate(SpawnTemplateName) + + if TemplateStatic then + self.SpawnTemplatePrefix = SpawnTemplateName + self.TemplateStaticUnit = UTILS.DeepCopy(TemplateStatic.units[1]) + self.CountryID = SpawnCountryID or CountryID + self.CategoryID = CategoryID + self.CoalitionID = CoalitionID + self.SpawnIndex = 0 + else + error( "SPAWNSTATIC:New: There is no static declared in the mission editor with SpawnTemplatePrefix = '" .. tostring(SpawnTemplateName) .. "'" ) + end self:SetEventPriority( 5 ) - return self + return self end ---- Creates the main object to spawn a @{Static} given a template table. +--- Creates the main object to spawn a @{Wrapper.Static} given a template table. -- @param #SPAWNSTATIC self -- @param #table SpawnTemplate Template used for spawning. -- @param DCS#country.id CountryID The ID of the country. Default `country.id.USA`. @@ -31515,7 +38207,7 @@ function SPAWNSTATIC:NewFromTemplate(SpawnTemplate, CountryID) return self end ---- Creates the main object to spawn a @{Static} from a given type. +--- Creates the main object to spawn a @{Wrapper.Static} from a given type. -- NOTE that you have to init many other parameters as spawn coordinate etc. -- @param #SPAWNSTATIC self -- @param #string StaticType Type of the static. @@ -31601,7 +38293,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 @@ -31610,7 +38302,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 IsDead If true, this static is dead. +-- @return #SPAWNSTATIC self +function SPAWNSTATIC:InitDead(IsDead) + self.InitStaticDead=IsDead return self end @@ -31668,7 +38369,7 @@ function SPAWNSTATIC:Spawn(Heading, NewName) end ---- Creates a new @{Static} from a POINT_VEC2. +--- Creates a new @{Wrapper.Static} from a POINT_VEC2. -- @param #SPAWNSTATIC self -- @param Core.Point#POINT_VEC2 PointVec2 The 2D coordinate where to spawn the static. -- @param #number Heading The heading of the static, which is a number in degrees from 0 to 360. @@ -31684,7 +38385,7 @@ function SPAWNSTATIC:SpawnFromPointVec2(PointVec2, Heading, NewName) end ---- Creates a new @{Static} from a COORDINATE. +--- Creates a new @{Wrapper.Static} from a COORDINATE. -- @param #SPAWNSTATIC self -- @param Core.Point#COORDINATE Coordinate The 3D coordinate where to spawn the static. -- @param #number Heading (Optional) Heading The heading of the static in degrees. Default is 0 degrees. @@ -31707,7 +38408,7 @@ function SPAWNSTATIC:SpawnFromCoordinate(Coordinate, Heading, NewName) end ---- Creates a new @{Static} from a @{Zone}. +--- Creates a new @{Wrapper.Static} from a @{Core.Zone}. -- @param #SPAWNSTATIC self -- @param Core.Zone#ZONE_BASE Zone The Zone where to spawn the static. -- @param #number Heading (Optional)The heading of the static in degrees. Default is the heading of the template. @@ -31758,12 +38459,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.isCargo=self.InitCargo + if self.InitStaticCargo~=nil then + Template.canCargo=self.InitStaticCargo + end + + if self.InitStaticCargoMass~=nil then + Template.mass=self.InitStaticCargoMass end if self.InitLinkUnit then @@ -31795,7 +38500,7 @@ function SPAWNSTATIC:_SpawnStatic(Template, CountryID) self:T(Template) -- Add static to the game. - local Static=nil + local Static=nil --DCS#StaticObject if self.InitFarp then @@ -31815,7 +38520,20 @@ function SPAWNSTATIC:_SpawnStatic(Template, CountryID) -- ED's dirty way to spawn FARPS. Static=coalition.addGroup(CountryID, -1, TemplateGroup) + + -- Currently DCS 2.8 does not trigger birth events if FAPRS are spawned! + -- We create such an event. The airbase is registered in Core.Event + local Event = { + id = EVENTS.Birth, + time = timer.getTime(), + initiator = Static + } + -- Create BIRTH event. + world.onEvent(Event) + else + self:T("Spawning Static") + self:T2({Template=Template}) Static=coalition.addStaticObject(CountryID, Template) end @@ -31857,8 +38575,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. @@ -31930,19 +38646,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 @@ -31979,13 +38693,10 @@ 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 ---- Create a new TIMER object. +--- Start TIMER object. -- @param #TIMER self -- @param #number Tstart Relative start time in seconds. -- @param #number dT Interval between function calls in seconds. If not specified `nil`, the function is called only once. @@ -31997,7 +38708,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 @@ -32022,6 +38733,20 @@ function TIMER:Start(Tstart, dT, Duration) return self end +--- Start TIMER object if a condition is met. Useful for e.g. debugging. +-- @param #TIMER self +-- @param #boolean Condition Must be true for the TIMER to start +-- @param #number Tstart Relative start time in seconds. +-- @param #number dT Interval between function calls in seconds. If not specified `nil`, the function is called only once. +-- @param #number Duration Time in seconds for how long the timer is running. If not specified `nil`, the timer runs forever or until stopped manually by the `TIMER:Stop()` function. +-- @return #TIMER self +function TIMER:StartIf(Condition,Tstart, dT, Duration) + if Condition then + self:Start(Tstart, dT, Duration) + end + return self +end + --- Stop the timer by removing the timer function. -- @param #TIMER self -- @param #number Delay (Optional) Delay in seconds, before the timer is stopped. @@ -32042,10 +38767,7 @@ function TIMER:Stop(Delay) -- Not running any more. self.isrunning=false - - -- Remove DB entry. - --_TIMERDB[self.uid]=nil - + end end @@ -32062,6 +38784,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. @@ -32115,89 +38846,87 @@ end ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- **Core** - Models the process to achieve goal(s). -- -- === --- +-- -- ## Features: --- +-- -- * Define the goal. -- * Monitor the goal achievement. -- * Manage goal contribution by players. --- +-- -- === --- +-- -- Classes that implement a goal achievement, will derive from GOAL to implement the ways how the achievements can be realized. --- +-- -- === --- +-- -- ### Author: **FlightControl** -- ### Contributions: **funkyfranky** --- +-- -- === --- +-- -- @module Core.Goal -- @image Core_Goal.JPG - do -- Goal --- @type GOAL -- @extends Core.Fsm#FSM - --- Models processes that have an objective with a defined achievement. Derived classes implement the ways how the achievements can be realized. - -- + -- -- # 1. GOAL constructor - -- + -- -- * @{#GOAL.New}(): Creates a new GOAL object. - -- + -- -- # 2. GOAL is a finite state machine (FSM). - -- + -- -- ## 2.1. GOAL States - -- + -- -- * **Pending**: The goal object is in progress. -- * **Achieved**: The goal objective is Achieved. - -- + -- -- ## 2.2. GOAL Events - -- + -- -- * **Achieved**: Set the goal objective to Achieved. - -- + -- -- # 3. Player contributions. - -- + -- -- Goals are most of the time achieved by players. These player achievements can be registered as part of the goal achievement. -- Use @{#GOAL.AddPlayerContribution}() to add a player contribution to the goal. -- The player contributions are based on a points system, an internal counter per player. - -- So once the goal has been achieved, the player contributions can be queried using @{#GOAL.GetPlayerContributions}(), + -- So once the goal has been achieved, the player contributions can be queried using @{#GOAL.GetPlayerContributions}(), -- that retrieves all contributions done by the players. For one player, the contribution can be queried using @{#GOAL.GetPlayerContribution}(). -- The total amount of player contributions can be queried using @{#GOAL.GetTotalContributions}(). - -- + -- -- # 4. Goal achievement. - -- + -- -- Once the goal is achieved, the mission designer will need to trigger the goal achievement using the **Achieved** event. -- The underlying 2 examples will achieve the goals for the `Goal` object: - -- + -- -- Goal:Achieved() -- Achieve the goal immediately. -- Goal:__Achieved( 30 ) -- Achieve the goal within 30 seconds. - -- + -- -- # 5. Check goal achievement. - -- + -- -- The method @{#GOAL.IsAchieved}() will return true if the goal is achieved (the trigger **Achieved** was executed). -- You can use this method to check asynchronously if a goal has been achieved, for example using a scheduler. - -- + -- -- @field #GOAL GOAL = { ClassName = "GOAL", } - + --- @field #table GOAL.Players GOAL.Players = {} --- @field #number GOAL.TotalContributions GOAL.TotalContributions = 0 - + --- GOAL Constructor. -- @param #GOAL self -- @return #GOAL function GOAL:New() - + local self = BASE:Inherit( self, FSM:New() ) -- #GOAL self:F( {} ) @@ -32218,11 +38947,10 @@ do -- Goal -- @param #string From -- @param #string Event -- @param #string To - - + self:SetStartState( "Pending" ) - self:AddTransition( "*", "Achieved", "Achieved" ) - + self:AddTransition( "*", "Achieved", "Achieved" ) + --- Achieved Handler OnBefore for GOAL -- @function [parent=#GOAL] OnBeforeAchieved -- @param #GOAL self @@ -32230,47 +38958,44 @@ do -- Goal -- @param #string Event -- @param #string To -- @return #boolean - + --- Achieved Handler OnAfter for GOAL -- @function [parent=#GOAL] OnAfterAchieved -- @param #GOAL self -- @param #string From -- @param #string Event -- @param #string To - + --- Achieved Trigger for GOAL -- @function [parent=#GOAL] Achieved -- @param #GOAL self - + --- Achieved Asynchronous Trigger for GOAL -- @function [parent=#GOAL] __Achieved -- @param #GOAL self -- @param #number Delay - + self:SetEventPriority( 5 ) return self end - - + --- Add a new contribution by a player. -- @param #GOAL self -- @param #string PlayerName The name of the player. function GOAL:AddPlayerContribution( PlayerName ) - self:F({PlayerName}) + self:F( { PlayerName } ) self.Players[PlayerName] = self.Players[PlayerName] or 0 self.Players[PlayerName] = self.Players[PlayerName] + 1 self.TotalContributions = self.TotalContributions + 1 end - - + --- @param #GOAL self -- @param #number Player contribution. function GOAL:GetPlayerContribution( PlayerName ) - return self.Players[PlayerName] or 0 + return self.Players[PlayerName] or 0 end - --- Get the players who contributed to achieve the goal. -- The result is a list of players, sorted by the name of the players. -- @param #GOAL self @@ -32279,7 +39004,6 @@ do -- Goal return self.Players or {} end - --- Gets the total contributions that happened to achieve the goal. -- The result is a number. -- @param #GOAL self @@ -32287,9 +39011,7 @@ do -- Goal function GOAL:GetTotalContributions() return self.TotalContributions or 0 end - - - + --- Validates if the goal is achieved. -- @param #GOAL self -- @return #boolean true if the goal is achieved. @@ -32297,7 +39019,8 @@ do -- Goal return self:Is( "Achieved" ) end -end--- **Core** - Management of spotting logistics, that can be activated and deactivated upon command. +end +--- **Core** - Management of spotting logistics, that can be activated and deactivated upon command. -- -- === -- @@ -32548,8 +39271,10 @@ do local RecceDcsUnit = self.Recce:GetDCSObject() - self.SpotIR = Spot.createInfraRed( RecceDcsUnit, { x = 0, y = 2, z = 0 }, Target:GetPointVec3():AddY(1):GetVec3() ) - self.SpotLaser = Spot.createLaser( RecceDcsUnit, { x = 0, y = 2, z = 0 }, Target:GetPointVec3():AddY(1):GetVec3(), LaserCode ) + local relativespot = self.relstartpos or { x = 0, y = 2, z = 0 } + + self.SpotIR = Spot.createInfraRed( RecceDcsUnit, relativespot, Target:GetPointVec3():AddY(1):GetVec3() ) + self.SpotLaser = Spot.createLaser( RecceDcsUnit, relativespot, Target:GetPointVec3():AddY(1):GetVec3(), LaserCode ) if Duration then self.ScheduleID = self.LaseScheduler:Schedule( self, StopLase, {self}, Duration ) @@ -32667,7 +39392,495 @@ do return self.Lasing end -end--- **Wrapper** -- OBJECT wraps the DCS Object derived objects. + --- Set laser start position relative to the lasing unit. + -- @param #SPOT self + -- @param #table position Start position of the laser relative to the lasing unit. Default is { x = 0, y = 2, z = 0 } + -- @return #SPOT self + -- @usage + -- -- Set lasing position to be the position of the optics of the Gazelle M: + -- myspot:SetRelativeStartPosition({ x = 1.7, y = 1.2, z = 0 }) + function SPOT:SetRelativeStartPosition(position) + self.relstartpos = position or { x = 0, y = 2, z = 0 } + return self + end + +end +--- **Core** - Tap into markers added to the F10 map by users. +-- +-- **Main Features:** +-- +-- * Create an easy way to tap into markers added to the F10 map by users. +-- * Recognize own tag and list of keywords. +-- * Matched keywords are handed down to functions. +-- ##Listen for your tag +-- myMarker = MARKEROPS_BASE:New("tag", {}, false) +-- function myMarker:OnAfterMarkChanged(From, Event, To, Text, Keywords, Coord, idx) +-- +-- end +-- Make sure to use the "MarkChanged" event as "MarkAdded" comes in right after the user places a blank marker and your callback will never be called. +-- +-- === +-- +-- ### Author: **Applevangelist** +-- +-- Date: 5 May 2021 +-- Last Update: Sep 2022 +-- +-- === +--- +-- @module Core.MarkerOps_Base +-- @image MOOSE_Core.JPG + +-------------------------------------------------------------------------- +-- MARKEROPS_BASE Class Definition. +-------------------------------------------------------------------------- + +--- MARKEROPS_BASE class. +-- @type MARKEROPS_BASE +-- @field #string ClassName Name of the class. +-- @field #string Tag Tag to identify commands. +-- @field #table Keywords Table of keywords to recognize. +-- @field #string version Version of #MARKEROPS_BASE. +-- @field #boolean Casesensitive Enforce case when identifying the Tag, i.e. tag ~= Tag +-- @extends Core.Fsm#FSM + +--- *Fiat lux.* -- Latin proverb. +-- +-- === +-- +-- # The MARKEROPS_BASE Concept +-- +-- This class enable scripting text-based actions from markers. +-- +-- @field #MARKEROPS_BASE +MARKEROPS_BASE = { + ClassName = "MARKEROPS", + Tag = "mytag", + Keywords = {}, + version = "0.1.0", + debug = false, + Casesensitive = true, +} + +--- Function to instantiate a new #MARKEROPS_BASE object. +-- @param #MARKEROPS_BASE self +-- @param #string Tagname Name to identify us from the event text. +-- @param #table Keywords Table of keywords recognized from the event text. +-- @param #boolean Casesensitive (Optional) Switch case sensitive identification of Tagname. Defaults to true. +-- @return #MARKEROPS_BASE self +function MARKEROPS_BASE:New(Tagname,Keywords,Casesensitive) + -- Inherit FSM + local self=BASE:Inherit(self, FSM:New()) -- #MARKEROPS_BASE + + -- Set some string id for output to DCS.log file. + self.lid=string.format("MARKEROPS_BASE %s | ", tostring(self.version)) + + self.Tag = Tagname or "mytag"-- #string + self.Keywords = Keywords or {} -- #table - might want to use lua regex here, too + self.debug = false + self.Casesensitive = true + + if Casesensitive and Casesensitive == false then + self.Casesensitive = false + end + + ----------------------- + --- FSM Transitions --- + ----------------------- + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start the FSM. + self:AddTransition("*", "MarkAdded", "*") -- Start the FSM. + self:AddTransition("*", "MarkChanged", "*") -- Start the FSM. + self:AddTransition("*", "MarkDeleted", "*") -- Start the FSM. + self:AddTransition("Running", "Stop", "Stopped") -- Stop the FSM. + + self:HandleEvent(EVENTS.MarkAdded, self.OnEventMark) + self:HandleEvent(EVENTS.MarkChange, self.OnEventMark) + self:HandleEvent(EVENTS.MarkRemoved, self.OnEventMark) + + -- start + self:I(self.lid..string.format("started for %s",self.Tag)) + self:__Start(1) + return self + + ------------------- + -- PSEUDO Functions + ------------------- + + --- On after "MarkAdded" event. Triggered when a Marker is added to the F10 map. + -- @function [parent=#MARKEROPS_BASE] OnAfterMarkAdded + -- @param #MARKEROPS_BASE self + -- @param #string From The From state + -- @param #string Event The Event called + -- @param #string To The To state + -- @param #string Text The text on the marker + -- @param #table Keywords Table of matching keywords found in the Event text + -- @param Core.Point#COORDINATE Coord Coordinate of the marker. + + --- On after "MarkChanged" event. Triggered when a Marker is changed on the F10 map. + -- @function [parent=#MARKEROPS_BASE] OnAfterMarkChanged + -- @param #MARKEROPS_BASE self + -- @param #string From The From state + -- @param #string Event The Event called + -- @param #string To The To state + -- @param #string Text The text on the marker + -- @param #table Keywords Table of matching keywords found in the Event text + -- @param Core.Point#COORDINATE Coord Coordinate of the marker. + + --- On after "MarkDeleted" event. Triggered when a Marker is deleted from the F10 map. + -- @function [parent=#MARKEROPS_BASE] OnAfterMarkDeleted + -- @param #MARKEROPS_BASE self + -- @param #string From The From state + -- @param #string Event The Event called + -- @param #string To The To state + + --- "Stop" trigger. Used to stop the function an unhandle events + -- @function [parent=#MARKEROPS_BASE] Stop + +end + +--- (internal) Handle events. +-- @param #MARKEROPS_BASE self +-- @param Core.Event#EVENTDATA Event +function MARKEROPS_BASE:OnEventMark(Event) + self:T({Event}) + if Event == nil or Event.idx == nil then + self:E("Skipping onEvent. Event or Event.idx unknown.") + return true + end + --position + local vec3={y=Event.pos.y, x=Event.pos.x, z=Event.pos.z} + local coord=COORDINATE:NewFromVec3(vec3) + if self.debug then + local coordtext = coord:ToStringLLDDM() + local text = tostring(Event.text) + local m = MESSAGE:New(string.format("Mark added at %s with text: %s",coordtext,text),10,"Info",false):ToAll() + end + -- decision + if Event.id==world.event.S_EVENT_MARK_ADDED then + self:T({event="S_EVENT_MARK_ADDED", carrier=self.groupname, vec3=Event.pos}) + -- Handle event + local Eventtext = tostring(Event.text) + if Eventtext~=nil then + if self:_MatchTag(Eventtext) then + local matchtable = self:_MatchKeywords(Eventtext) + self:MarkAdded(Eventtext,matchtable,coord) + end + end + elseif Event.id==world.event.S_EVENT_MARK_CHANGE then + self:T({event="S_EVENT_MARK_CHANGE", carrier=self.groupname, vec3=Event.pos}) + -- Handle event. + local Eventtext = tostring(Event.text) + if Eventtext~=nil then + if self:_MatchTag(Eventtext) then + local matchtable = self:_MatchKeywords(Eventtext) + self:MarkChanged(Eventtext,matchtable,coord) + end + end + elseif Event.id==world.event.S_EVENT_MARK_REMOVED then + self:T({event="S_EVENT_MARK_REMOVED", carrier=self.groupname, vec3=Event.pos}) + -- Hande event. + local Eventtext = tostring(Event.text) + if Eventtext~=nil then + if self:_MatchTag(Eventtext) then + self:MarkDeleted() + end + end + end +end + +--- (internal) Match tag. +-- @param #MARKEROPS_BASE self +-- @param #string Eventtext Text added to the marker. +-- @return #boolean +function MARKEROPS_BASE:_MatchTag(Eventtext) + local matches = false + if not self.Casesensitive then + local type = string.lower(self.Tag) -- #string + if string.find(string.lower(Eventtext),type) then + matches = true --event text contains tag + end + else + local type = self.Tag -- #string + if string.find(Eventtext,type) then + matches = true --event text contains tag + end + end + return matches +end + +--- (internal) Match keywords table. +-- @param #MARKEROPS_BASE self +-- @param #string Eventtext Text added to the marker. +-- @return #table +function MARKEROPS_BASE:_MatchKeywords(Eventtext) + local matchtable = {} + local keytable = self.Keywords + for _index,_word in pairs (keytable) do + if string.find(string.lower(Eventtext),string.lower(_word))then + table.insert(matchtable,_word) + end + end + return matchtable +end + +--- On before "MarkAdded" event. Triggered when a Marker is added to the F10 map. + -- @param #MARKEROPS_BASE self + -- @param #string From The From state + -- @param #string Event The Event called + -- @param #string To The To state + -- @param #string Text The text on the marker + -- @param #table Keywords Table of matching keywords found in the Event text + -- @param Core.Point#COORDINATE Coord Coordinate of the marker. +function MARKEROPS_BASE:onbeforeMarkAdded(From,Event,To,Text,Keywords,Coord) + self:T({self.lid,From,Event,To,Text,Keywords,Coord:ToStringLLDDM()}) +end + +--- On before "MarkChanged" event. Triggered when a Marker is changed on the F10 map. + -- @param #MARKEROPS_BASE self + -- @param #string From The From state + -- @param #string Event The Event called + -- @param #string To The To state + -- @param #string Text The text on the marker + -- @param #table Keywords Table of matching keywords found in the Event text + -- @param Core.Point#COORDINATE Coord Coordinate of the marker. +function MARKEROPS_BASE:onbeforeMarkChanged(From,Event,To,Text,Keywords,Coord) + self:T({self.lid,From,Event,To,Text,Keywords,Coord:ToStringLLDDM()}) +end + +--- On before "MarkDeleted" event. Triggered when a Marker is removed from the F10 map. + -- @param #MARKEROPS_BASE self + -- @param #string From The From state + -- @param #string Event The Event called + -- @param #string To The To state +function MARKEROPS_BASE:onbeforeMarkDeleted(From,Event,To) + self:T({self.lid,From,Event,To}) +end + +--- On enter "Stopped" event. Unsubscribe events. + -- @param #MARKEROPS_BASE self + -- @param #string From The From state + -- @param #string Event The Event called + -- @param #string To The To state +function MARKEROPS_BASE:onenterStopped(From,Event,To) + self:T({self.lid,From,Event,To}) + -- unsubscribe from events + self:UnHandleEvent(EVENTS.MarkAdded) + self:UnHandleEvent(EVENTS.MarkChange) + self:UnHandleEvent(EVENTS.MarkRemoved) +end + +-------------------------------------------------------------------------- +-- MARKEROPS_BASE Class Definition End. +-------------------------------------------------------------------------- +--- **Core** - A 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 +---------------------------------------------------------------- +--- **Wrapper** - OBJECT wraps the DCS Object derived objects. -- -- === -- @@ -32762,7 +39975,7 @@ end ---- **Wrapper** -- IDENTIFIABLE is an intermediate class wrapping DCS Object class derived Objects. +--- **Wrapper** - IDENTIFIABLE is an intermediate class wrapping DCS Object class derived Objects. -- -- === -- @@ -32799,7 +40012,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", @@ -32820,8 +40033,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 ) @@ -32841,11 +40053,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 @@ -32912,8 +40121,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 ) @@ -32940,7 +40148,7 @@ function IDENTIFIABLE:GetCoalitionName() if DCSIdentifiable then - -- Get coaliton ID. + -- Get coalition ID. local IdentifiableCoalition = DCSIdentifiable:getCoalition() self:T3( IdentifiableCoalition ) @@ -32954,8 +40162,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 ) @@ -32986,8 +40193,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 ) @@ -33006,8 +40212,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 ) @@ -33030,12 +40235,14 @@ 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". +--- **Wrapper** - POSITIONABLE wraps DCS classes that are "positionable". -- -- === -- @@ -33048,7 +40255,7 @@ end -- @module Wrapper.Positionable -- @image Wrapper_Positionable.JPG ---- @type POSITIONABLE.__ Methods which are not intended for mission designers, but which are used interally by the moose designer :-) +--- @type POSITIONABLE.__ Methods which are not intended for mission designers, but which are used internally by the moose designer :-) -- @extends Wrapper.Identifiable#IDENTIFIABLE --- @type POSITIONABLE @@ -33094,7 +40301,6 @@ POSITIONABLE.__ = {} --- @field #POSITIONABLE.__.Cargo POSITIONABLE.__.Cargo = {} - --- A DCSPositionable -- @type DCSPositionable -- @field id_ The ID of the controllable in DCS @@ -33112,16 +40318,19 @@ end --- Destroys the POSITIONABLE. -- @param #POSITIONABLE self --- @param #boolean GenerateEvent (Optional) true if you want to generate a crash or dead event for the unit. +-- @param #boolean GenerateEvent (Optional) If true, generates a crash or dead event for the unit. If false, no event generated. If nil, a remove event is generated. -- @return #nil The DCS Unit is not existing or alive. -- @usage --- -- Air unit example: destroy the Helicopter and generate a S_EVENT_CRASH for each unit in the Helicopter group. +-- +-- Air unit example: destroy the Helicopter and generate a S_EVENT_CRASH for each unit in the Helicopter group. -- Helicopter = UNIT:FindByName( "Helicopter" ) -- Helicopter:Destroy( true ) +-- -- @usage -- -- Ground unit example: destroy the Tanks and generate a S_EVENT_DEAD for each unit in the Tanks group. -- Tanks = UNIT:FindByName( "Tanks" ) -- Tanks:Destroy( true ) +-- -- @usage -- -- Ship unit example: destroy the Ship silently. -- Ship = STATIC:FindByName( "Ship" ) @@ -33171,7 +40380,7 @@ end --- Returns a pos3 table of the objects current position and orientation in 3D space. X, Y, Z values are unit vectors defining the objects orientation. -- Coordinates are dependent on the position of the maps origin. --- @param Wrapper.Positionable#POSITIONABLE self +-- @param #POSITIONABLE self -- @return DCS#Position3 Table consisting of the point and orientation tables. function POSITIONABLE:GetPosition() self:F2( self.PositionableName ) @@ -33184,18 +40393,19 @@ function POSITIONABLE:GetPosition() return PositionablePosition end - BASE:E( { "Cannot GetPositionVec3", Positionable = self, Alive = self:IsAlive() } ) + BASE:E( { "Cannot GetPosition", Positionable = self, Alive = self:IsAlive() } ) return nil end --- Returns a {@DCS#Vec3} table of the objects current orientation in 3D space. X, Y, Z values are unit vectors defining the objects orientation. -- X is the orientation parallel to the movement of the object, Z perpendicular and Y vertical orientation. --- @param Wrapper.Positionable#POSITIONABLE self +-- @param #POSITIONABLE self -- @return DCS#Vec3 X orientation, i.e. parallel to the direction of movement. -- @return DCS#Vec3 Y orientation, i.e. vertical. -- @return DCS#Vec3 Z orientation, i.e. perpendicular to the direction of movement. +-- @return #nil The POSITIONABLE is not existing or alive. function POSITIONABLE:GetOrientation() - local position=self:GetPosition() + local position = self:GetPosition() if position then return position.x, position.y, position.z else @@ -33205,10 +40415,11 @@ function POSITIONABLE:GetOrientation() end --- Returns a {@DCS#Vec3} table of the objects current X orientation in 3D space, i.e. along the direction of movement. --- @param Wrapper.Positionable#POSITIONABLE self +-- @param #POSITIONABLE self -- @return DCS#Vec3 X orientation, i.e. parallel to the direction of movement. +-- @return #nil The POSITIONABLE is not existing or alive. function POSITIONABLE:GetOrientationX() - local position=self:GetPosition() + local position = self:GetPosition() if position then return position.x else @@ -33218,10 +40429,11 @@ function POSITIONABLE:GetOrientationX() end --- Returns a {@DCS#Vec3} table of the objects current Y orientation in 3D space, i.e. vertical orientation. --- @param Wrapper.Positionable#POSITIONABLE self +-- @param #POSITIONABLE self -- @return DCS#Vec3 Y orientation, i.e. vertical. +-- @return #nil The POSITIONABLE is not existing or alive. function POSITIONABLE:GetOrientationY() - local position=self:GetPosition() + local position = self:GetPosition() if position then return position.y else @@ -33231,10 +40443,11 @@ function POSITIONABLE:GetOrientationY() end --- Returns a {@DCS#Vec3} table of the objects current Z orientation in 3D space, i.e. perpendicular to direction of movement. --- @param Wrapper.Positionable#POSITIONABLE self +-- @param #POSITIONABLE self -- @return DCS#Vec3 Z orientation, i.e. perpendicular to movement. +-- @return #nil The POSITIONABLE is not existing or alive. function POSITIONABLE:GetOrientationZ() - local position=self:GetPosition() + local position = self:GetPosition() if position then return position.z else @@ -33244,7 +40457,7 @@ function POSITIONABLE:GetOrientationZ() 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 +-- @param #POSITIONABLE self -- @return DCS#Position The 3D position vectors of the POSITIONABLE. -- @return #nil The POSITIONABLE is not existing or alive. function POSITIONABLE:GetPositionVec3() @@ -33264,20 +40477,21 @@ function POSITIONABLE:GetPositionVec3() end --- Returns the @{DCS#Vec3} vector indicating the 3D vector of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return DCS#Vec3 The 3D point vector of the POSITIONABLE or `nil` if it is not existing or alive. +-- @param #POSITIONABLE self +-- @return DCS#Vec3 The 3D point vector of the POSITIONABLE. +-- @return #nil The POSITIONABLE is not existing or alive. function POSITIONABLE:GetVec3() local DCSPositionable = self:GetDCSObject() if DCSPositionable then - local vec3=DCSPositionable:getPoint() + local vec3 = DCSPositionable:getPoint() if vec3 then return vec3 else - self:E("ERROR: Cannot get vec3!") + self:E( "ERROR: Cannot get vec3!" ) end end @@ -33287,17 +40501,18 @@ function POSITIONABLE:GetVec3() end --- Returns the @{DCS#Vec2} vector indicating the point in 2D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return DCS#Vec2 The 2D point vector of the POSITIONABLE or #nil if it is not existing or alive. +-- @param #POSITIONABLE self +-- @return DCS#Vec2 The 2D point vector of the POSITIONABLE. +-- @return #nil The POSITIONABLE is not existing or alive. function POSITIONABLE:GetVec2() local DCSPositionable = self:GetDCSObject() if DCSPositionable then - local Vec3=DCSPositionable:getPoint() --DCS#Vec3 + local Vec3 = DCSPositionable:getPoint() -- DCS#Vec3 - return {x=Vec3.x, y=Vec3.z} + return { x = Vec3.x, y = Vec3.z } end self:E( { "Cannot GetVec2", Positionable = self, Alive = self:IsAlive() } ) @@ -33306,7 +40521,7 @@ function POSITIONABLE:GetVec2() end --- Returns a POINT_VEC2 object indicating the point in 2D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self +-- @param #POSITIONABLE self -- @return Core.Point#POINT_VEC2 The 2D point vector of the POSITIONABLE. -- @return #nil The POSITIONABLE is not existing or alive. function POSITIONABLE:GetPointVec2() @@ -33319,7 +40534,7 @@ function POSITIONABLE:GetPointVec2() local PositionablePointVec2 = POINT_VEC2:NewFromVec3( PositionableVec3 ) - --self:F( PositionablePointVec2 ) + -- self:F( PositionablePointVec2 ) return PositionablePointVec2 end @@ -33329,7 +40544,7 @@ function POSITIONABLE:GetPointVec2() end --- Returns a POINT_VEC3 object indicating the point in 3D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self +-- @param #POSITIONABLE self -- @return Core.Point#POINT_VEC3 The 3D point vector of the POSITIONABLE. -- @return #nil The POSITIONABLE is not existing or alive. function POSITIONABLE:GetPointVec3() @@ -33344,14 +40559,14 @@ function POSITIONABLE:GetPointVec3() if false and self.pointvec3 then -- Update vector. - self.pointvec3.x=PositionableVec3.x - self.pointvec3.y=PositionableVec3.y - self.pointvec3.z=PositionableVec3.z + self.pointvec3.x = PositionableVec3.x + self.pointvec3.y = PositionableVec3.y + self.pointvec3.z = PositionableVec3.z else -- Create a new POINT_VEC3 object. - self.pointvec3=POINT_VEC3:NewFromVec3(PositionableVec3) + self.pointvec3 = POINT_VEC3:NewFromVec3( PositionableVec3 ) end @@ -33363,9 +40578,13 @@ function POSITIONABLE:GetPointVec3() return nil end ---- Returns a COORDINATE object indicating the point in 3D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Core.Point#COORDINATE The COORDINATE of the POSITIONABLE. +--- Returns a reference to a COORDINATE object indicating the point in 3D of the POSITIONABLE within the mission. +-- This function works similar to POSITIONABLE.GetCoordinate(), however, this function caches, updates and re-uses the same COORDINATE object stored +-- within the POSITIONABLE. This has higher performance, but comes with all considerations associated with the possible referencing to the same COORDINATE object. +-- This should only be used when performance is critical and there is sufficient awareness of the possible pitfalls. However, in most instances, GetCoordinate() is +-- preferred as it will return a fresh new COORDINATE and thus avoid potentially unexpected issues. +-- @param #POSITIONABLE self +-- @return Core.Point#COORDINATE A reference to the COORDINATE object of the POSITIONABLE. function POSITIONABLE:GetCoord() -- Get DCS object. @@ -33374,20 +40593,14 @@ function POSITIONABLE:GetCoord() if DCSPositionable then -- Get the current position. - local Vec3 = self:GetVec3() + local PositionableVec3 = self:GetVec3() if self.coordinate then - - -- Update vector. - self.coordinate.x=Vec3.x - self.coordinate.y=Vec3.y - self.coordinate.z=Vec3.z - + -- Update COORDINATE from 3D vector. + self.coordinate:UpdateFromVec3( PositionableVec3 ) else - -- New COORDINATE. - self.coordinate=COORDINATE:NewFromVec3(Vec3) - + self.coordinate = COORDINATE:NewFromVec3( PositionableVec3 ) end return self.coordinate @@ -33399,9 +40612,9 @@ function POSITIONABLE:GetCoord() return nil end ---- Returns a COORDINATE object indicating the point in 3D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Core.Point#COORDINATE The COORDINATE of the POSITIONABLE. +--- Returns a new COORDINATE object indicating the point in 3D of the POSITIONABLE within the mission. +-- @param #POSITIONABLE self +-- @return Core.Point#COORDINATE A new COORDINATE object of the POSITIONABLE. function POSITIONABLE:GetCoordinate() -- Get DCS object. @@ -33413,7 +40626,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 @@ -33425,48 +40639,48 @@ function POSITIONABLE:GetCoordinate() end --- Returns a COORDINATE object, which is offset with respect to the orientation of the POSITIONABLE. --- @param Wrapper.Positionable#POSITIONABLE self +-- @param #POSITIONABLE self -- @param #number x Offset in the direction "the nose" of the unit is pointing in meters. Default 0 m. -- @param #number y Offset "above" the unit in meters. Default 0 m. -- @param #number z Offset in the direction "the wing" of the unit is pointing in meters. z>0 starboard, z<0 port. Default 0 m. -- @return Core.Point#COORDINATE The COORDINATE of the offset with respect to the orientation of the POSITIONABLE. -function POSITIONABLE:GetOffsetCoordinate(x,y,z) +function POSITIONABLE:GetOffsetCoordinate( x, y, z ) -- Default if nil. - x=x or 0 - y=y or 0 - z=z or 0 + x = x or 0 + y = y or 0 + z = z or 0 -- Vectors making up the coordinate system. - local X=self:GetOrientationX() - local Y=self:GetOrientationY() - local Z=self:GetOrientationZ() + local X = self:GetOrientationX() + local Y = self:GetOrientationY() + local Z = self:GetOrientationZ() -- Offset vector: x meters ahead, z meters starboard, y meters above. - local A={x=x, y=y, z=z} + local A = { x = x, y = y, z = z } -- Scale components of orthonormal coordinate vectors. - local x={x=X.x*A.x, y=X.y*A.x, z=X.z*A.x} - local y={x=Y.x*A.y, y=Y.y*A.y, z=Y.z*A.y} - local z={x=Z.x*A.z, y=Z.y*A.z, z=Z.z*A.z} + local x = { x = X.x * A.x, y = X.y * A.x, z = X.z * A.x } + local y = { x = Y.x * A.y, y = Y.y * A.y, z = Y.z * A.y } + local z = { x = Z.x * A.z, y = Z.y * A.z, z = Z.z * A.z } -- Add up vectors in the unit coordinate system ==> this gives the offset vector relative the the origin of the map. - local a={x=x.x+y.x+z.x, y=x.y+y.y+z.y, z=x.z+y.z+z.z} + local a = { x = x.x + y.x + z.x, y = x.y + y.y + z.y, z = x.z + y.z + z.z } -- Vector from the origin of the map to the unit. - local u=self:GetVec3() + local u = self:GetVec3() -- Translate offset vector from map origin to the unit: v=u+a. - local v={x=a.x+u.x, y=a.y+u.y, z=a.z+u.z} + local v = { x = a.x + u.x, y = a.y + u.y, z = a.z + u.z } - local coord=COORDINATE:NewFromVec3(v) + local coord = COORDINATE:NewFromVec3( v ) -- Return the offset coordinate. return coord end --- Returns a random @{DCS#Vec3} vector within a range, indicating the point in 3D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self +-- @param #POSITIONABLE self -- @param #number Radius -- @return DCS#Vec3 The 3D point vector of the POSITIONABLE. -- @return #nil The POSITIONABLE is not existing or alive. @@ -33482,7 +40696,7 @@ function POSITIONABLE:GetRandomVec3( Radius ) if Radius then local PositionableRandomVec3 = {} - local angle = math.random() * math.pi*2; + local angle = math.random() * math.pi * 2; PositionableRandomVec3.x = PositionablePointVec3.x + math.cos( angle ) * math.random() * Radius; PositionableRandomVec3.y = PositionablePointVec3.y PositionableRandomVec3.z = PositionablePointVec3.z + math.sin( angle ) * math.random() * Radius; @@ -33490,7 +40704,7 @@ function POSITIONABLE:GetRandomVec3( Radius ) self:T3( PositionableRandomVec3 ) return PositionableRandomVec3 else - self:F("Radius is nil, returning the PointVec3 of the POSITIONABLE", PositionablePointVec3) + self:F( "Radius is nil, returning the PointVec3 of the POSITIONABLE", PositionablePointVec3 ) return PositionablePointVec3 end end @@ -33500,18 +40714,17 @@ function POSITIONABLE:GetRandomVec3( Radius ) return nil end - --- Get the bounding box of the underlying POSITIONABLE DCS Object. -- @param #POSITIONABLE self -- @return DCS#Box3 The bounding box of the POSITIONABLE. -- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetBoundingBox() --R2.1 +function POSITIONABLE:GetBoundingBox() self:F2() local DCSPositionable = self:GetDCSObject() if DCSPositionable then - local PositionableDesc = DCSPositionable:getDesc() --DCS#Desc + local PositionableDesc = DCSPositionable:getDesc() -- DCS#Desc if PositionableDesc then local PositionableBox = PositionableDesc.box return PositionableBox @@ -33523,7 +40736,6 @@ function POSITIONABLE:GetBoundingBox() --R2.1 return nil end - --- Get the object size. -- @param #POSITIONABLE self -- @return DCS#Distance Max size of object in x, z or 0 if bounding box could not be obtained. @@ -33533,28 +40745,29 @@ end function POSITIONABLE:GetObjectSize() -- Get bounding box. - local box=self:GetBoundingBox() + local box = self:GetBoundingBox() if box then - local x=box.max.x+math.abs(box.min.x) --length - local y=box.max.y+math.abs(box.min.y) --height - local z=box.max.z+math.abs(box.min.z) --width - return math.max(x,z), x , y, z + local x = box.max.x + math.abs( box.min.x ) -- length + local y = box.max.y + math.abs( box.min.y ) -- height + local z = box.max.z + math.abs( box.min.z ) -- width + return math.max( x, z ), x, y, z end - return 0,0,0,0 + return 0, 0, 0, 0 end --- Get the bounding radius of the underlying POSITIONABLE DCS Object. -- @param #POSITIONABLE self --- @param #number mindist (Optional) If bounding box is smaller than this value, mindist is returned. --- @return DCS#Distance The bounding radius of the POSITIONABLE or #nil if the POSITIONABLE is not existing or alive. -function POSITIONABLE:GetBoundingRadius(mindist) +-- @param #number MinDist (Optional) If bounding box is smaller than this value, MinDist is returned. +-- @return DCS#Distance The bounding radius of the POSITIONABLE +-- @return #nil The POSITIONABLE is not existing or alive. +function POSITIONABLE:GetBoundingRadius( MinDist ) self:F2() local Box = self:GetBoundingBox() - local boxmin=mindist or 0 + local boxmin = MinDist or 0 if Box then local X = Box.max.x - Box.min.x local Z = Box.max.z - Box.min.z @@ -33568,8 +40781,8 @@ function POSITIONABLE:GetBoundingRadius(mindist) return nil end ---- Returns the altitude of the POSITIONABLE. --- @param Wrapper.Positionable#POSITIONABLE self +--- Returns the altitude above sea level of the POSITIONABLE. +-- @param #POSITIONABLE self -- @return DCS#Distance The altitude of the POSITIONABLE. -- @return #nil The POSITIONABLE is not existing or alive. function POSITIONABLE:GetAltitude() @@ -33578,7 +40791,7 @@ function POSITIONABLE:GetAltitude() local DCSPositionable = self:GetDCSObject() if DCSPositionable then - local PositionablePointVec3 = DCSPositionable:getPoint() --DCS#Vec3 + local PositionablePointVec3 = DCSPositionable:getPoint() -- DCS#Vec3 return PositionablePointVec3.y end @@ -33588,7 +40801,7 @@ function POSITIONABLE:GetAltitude() end --- Returns if the Positionable is located above a runway. --- @param Wrapper.Positionable#POSITIONABLE self +-- @param #POSITIONABLE self -- @return #boolean true if Positionable is above a runway. -- @return #nil The POSITIONABLE is not existing or alive. function POSITIONABLE:IsAboveRunway() @@ -33611,7 +40824,6 @@ function POSITIONABLE:IsAboveRunway() return nil end - function POSITIONABLE:GetSize() local DCSObject = self:GetDCSObject() @@ -33623,11 +40835,10 @@ function POSITIONABLE:GetSize() end end - - --- Returns the POSITIONABLE heading in degrees. --- @param Wrapper.Positionable#POSITIONABLE self --- @return #number The POSITIONABLE heading in degrees or `nil` if not existing or alive. +-- @param #POSITIONABLE self +-- @return #number The POSITIONABLE heading in degrees. +-- @return #nil The POSITIONABLE is not existing or alive. function POSITIONABLE:GetHeading() local DCSPositionable = self:GetDCSObject() @@ -33635,22 +40846,21 @@ function POSITIONABLE:GetHeading() if DCSPositionable then local PositionablePosition = DCSPositionable:getPosition() - + if PositionablePosition then local PositionableHeading = math.atan2( PositionablePosition.x.z, PositionablePosition.x.x ) - + if PositionableHeading < 0 then PositionableHeading = PositionableHeading + 2 * math.pi end - + PositionableHeading = PositionableHeading * 180 / math.pi - + return PositionableHeading end end - self:E({"Cannot GetHeading", Positionable = self, Alive = self:IsAlive()}) - + self:E( { "Cannot GetHeading", Positionable = self, Alive = self:IsAlive() } ) return nil end @@ -33660,6 +40870,7 @@ end -- If the unit is a helicopter or a plane, then this method will return true, otherwise false. -- @param #POSITIONABLE self -- @return #boolean Air category evaluation result. +-- @return #nil The POSITIONABLE is not existing or alive. function POSITIONABLE:IsAir() self:F2() @@ -33675,6 +40886,7 @@ function POSITIONABLE:IsAir() return IsAirResult end + self:E( { "Cannot check IsAir", Positionable = self, Alive = self:IsAlive() } ) return nil end @@ -33682,6 +40894,7 @@ end -- If the unit is a ground vehicle or infantry, this method will return true, otherwise false. -- @param #POSITIONABLE self -- @return #boolean Ground category evaluation result. +-- @return #nil The POSITIONABLE is not existing or alive. function POSITIONABLE:IsGround() self:F2() @@ -33691,19 +40904,20 @@ function POSITIONABLE:IsGround() local UnitDescriptor = DCSUnit:getDesc() self:T3( { UnitDescriptor.category, Unit.Category.GROUND_UNIT } ) - local IsGroundResult = ( UnitDescriptor.category == Unit.Category.GROUND_UNIT ) + local IsGroundResult = (UnitDescriptor.category == Unit.Category.GROUND_UNIT) self:T3( IsGroundResult ) return IsGroundResult end + self:E( { "Cannot check IsGround", Positionable = self, Alive = self:IsAlive() } ) return nil end - --- Returns if the unit is of ship category. -- @param #POSITIONABLE self -- @return #boolean Ship category evaluation result. +-- @return #nil The POSITIONABLE is not existing or alive. function POSITIONABLE:IsShip() self:F2() @@ -33711,19 +40925,42 @@ function POSITIONABLE:IsShip() if DCSUnit then local UnitDescriptor = DCSUnit:getDesc() + self:T3( { UnitDescriptor.category, Unit.Category.SHIP } ) - local IsShip = ( UnitDescriptor.category == Unit.Category.SHIP ) + local IsShipResult = (UnitDescriptor.category == Unit.Category.SHIP) - return IsShip + self:T3( IsShipResult ) + return IsShipResult end + self:E( { "Cannot check IsShip", Positionable = self, Alive = self:IsAlive() } ) return nil end +--- Returns if the unit is a submarine. +-- @param #POSITIONABLE self +-- @return #boolean Submarines attributes result. +function POSITIONABLE:IsSubmarine() + self:F2() + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitDescriptor = DCSUnit:getDesc() + if UnitDescriptor.attributes["Submarines"] == true then + return true + else + return false + end + end + + self:E( { "Cannot check IsSubmarine", Positionable = self, Alive = self:IsAlive() } ) + return nil +end --- Returns true if the POSITIONABLE is in the air. -- Polymorphic, is overridden in GROUP and UNIT. --- @param Wrapper.Positionable#POSITIONABLE self +-- @param #POSITIONABLE self -- @return #boolean true if in the air. -- @return #nil The POSITIONABLE is not existing or alive. function POSITIONABLE:InAir() @@ -33732,9 +40969,8 @@ function POSITIONABLE:InAir() return nil end - ---- Returns the a @{Velocity} object from the positionable. --- @param Wrapper.Positionable#POSITIONABLE self +--- Returns the @{Core.Velocity} object from the POSITIONABLE. +-- @param #POSITIONABLE self -- @return Core.Velocity#VELOCITY Velocity The Velocity object. -- @return #nil The POSITIONABLE is not existing or alive. function POSITIONABLE:GetVelocity() @@ -33752,10 +40988,8 @@ function POSITIONABLE:GetVelocity() return nil end - - --- Returns the POSITIONABLE velocity Vec3 vector. --- @param Wrapper.Positionable#POSITIONABLE self +-- @param #POSITIONABLE self -- @return DCS#Vec3 The velocity Vec3 vector -- @return #nil The POSITIONABLE is not existing or alive. function POSITIONABLE:GetVelocityVec3() @@ -33776,30 +41010,29 @@ end --- Get relative velocity with respect to another POSITIONABLE. -- @param #POSITIONABLE self --- @param #POSITIONABLE positionable Other positionable. +-- @param #POSITIONABLE Positionable Other POSITIONABLE. -- @return #number Relative velocity in m/s. -function POSITIONABLE:GetRelativeVelocity(positionable) +function POSITIONABLE:GetRelativeVelocity( Positionable ) self:F2( self.PositionableName ) - local v1=self:GetVelocityVec3() - local v2=positionable:GetVelocityVec3() + local v1 = self:GetVelocityVec3() + local v2 = Positionable:GetVelocityVec3() - local vtot=UTILS.VecAdd(v1,v2) + local vtot = UTILS.VecAdd( v1, v2 ) - return UTILS.VecNorm(vtot) + return UTILS.VecNorm( vtot ) end ---- Returns the POSITIONABLE height in meters. --- @param Wrapper.Positionable#POSITIONABLE self --- @return DCS#Vec3 The height of the positionable. --- @return #nil The POSITIONABLE is not existing or alive. +--- Returns the POSITIONABLE height above sea level in meters. +-- @param #POSITIONABLE self +-- @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 ) local DCSPositionable = self:GetDCSObject() - if DCSPositionable then + if DCSPositionable and DCSPositionable:isExist() then local PositionablePosition = DCSPositionable:getPosition() if PositionablePosition then local PositionableHeight = PositionablePosition.p.y @@ -33811,10 +41044,9 @@ function POSITIONABLE:GetHeight() --R2.1 return nil end - --- Returns the POSITIONABLE velocity in km/h. --- @param Wrapper.Positionable#POSITIONABLE self --- @return #number The velocity in km/h +-- @param #POSITIONABLE self +-- @return #number The velocity in km/h. function POSITIONABLE:GetVelocityKMH() self:F2( self.PositionableName ) @@ -33832,7 +41064,7 @@ function POSITIONABLE:GetVelocityKMH() end --- Returns the POSITIONABLE velocity in meters per second. --- @param Wrapper.Positionable#POSITIONABLE self +-- @param #POSITIONABLE self -- @return #number The velocity in meters per second. function POSITIONABLE:GetVelocityMPS() self:F2( self.PositionableName ) @@ -33850,16 +41082,17 @@ function POSITIONABLE:GetVelocityMPS() end --- Returns the POSITIONABLE velocity in knots. --- @param Wrapper.Positionable#POSITIONABLE self +-- @param #POSITIONABLE self -- @return #number The velocity in knots. function POSITIONABLE:GetVelocityKNOTS() self:F2( self.PositionableName ) - return UTILS.MpsToKnots(self:GetVelocityMPS()) + return UTILS.MpsToKnots( self:GetVelocityMPS() ) end ---- Returns the Angle of Attack of a positionable. --- @param Wrapper.Positionable#POSITIONABLE self +--- Returns the Angle of Attack of a POSITIONABLE. +-- @param #POSITIONABLE self -- @return #number Angle of attack in degrees. +-- @return #nil The POSITIONABLE is not existing or alive. function POSITIONABLE:GetAoA() -- Get position of the unit. @@ -33870,34 +41103,34 @@ function POSITIONABLE:GetAoA() -- Get velocity vector of the unit. local unitvel = self:GetVelocityVec3() - if unitvel and UTILS.VecNorm(unitvel)~=0 then + if unitvel and UTILS.VecNorm( unitvel ) ~= 0 then -- Get wind vector including turbulences. - local wind=self:GetCoordinate():GetWindWithTurbulenceVec3() + local wind = self:GetCoordinate():GetWindWithTurbulenceVec3() -- Include wind vector. - unitvel.x=unitvel.x-wind.x - unitvel.y=unitvel.y-wind.y - unitvel.z=unitvel.z-wind.z + unitvel.x = unitvel.x - wind.x + unitvel.y = unitvel.y - wind.y + unitvel.z = unitvel.z - wind.z -- Unit velocity transformed into aircraft axes directions. local AxialVel = {} -- Transform velocity components in direction of aircraft axes. - AxialVel.x = UTILS.VecDot(unitpos.x, unitvel) - AxialVel.y = UTILS.VecDot(unitpos.y, unitvel) - AxialVel.z = UTILS.VecDot(unitpos.z, unitvel) + AxialVel.x = UTILS.VecDot( unitpos.x, unitvel ) + AxialVel.y = UTILS.VecDot( unitpos.y, unitvel ) + AxialVel.z = UTILS.VecDot( unitpos.z, unitvel ) -- AoA is angle between unitpos.x and the x and y velocities. - local AoA = math.acos(UTILS.VecDot({x = 1, y = 0, z = 0}, {x = AxialVel.x, y = AxialVel.y, z = 0})/UTILS.VecNorm({x = AxialVel.x, y = AxialVel.y, z = 0})) + local AoA = math.acos( UTILS.VecDot( { x = 1, y = 0, z = 0 }, { x = AxialVel.x, y = AxialVel.y, z = 0 } ) / UTILS.VecNorm( { x = AxialVel.x, y = AxialVel.y, z = 0 } ) ) - --Set correct direction: + -- Set correct direction: if AxialVel.y > 0 then AoA = -AoA end -- Return AoA value in degrees. - return math.deg(AoA) + return math.deg( AoA ) end end @@ -33905,9 +41138,10 @@ function POSITIONABLE:GetAoA() return nil end ---- Returns the unit's climb or descent angle. --- @param Wrapper.Positionable#POSITIONABLE self --- @return #number Climb or descent angle in degrees. Or 0 if velocity vector norm is zero (or nil). Or nil, if the position of the POSITIONABLE returns nil. +--- Returns the climb or descent angle of the POSITIONABLE. +-- @param #POSITIONABLE self +-- @return #number Climb or descent angle in degrees. Or 0 if velocity vector norm is zero. +-- @return #nil The POSITIONABLE is not existing or alive. function POSITIONABLE:GetClimbAngle() -- Get position of the unit. @@ -33918,39 +41152,42 @@ function POSITIONABLE:GetClimbAngle() -- Get velocity vector of the unit. local unitvel = self:GetVelocityVec3() - if unitvel and UTILS.VecNorm(unitvel)~=0 then + if unitvel and UTILS.VecNorm( unitvel ) ~= 0 then -- Calculate climb angle. - local angle=math.asin(unitvel.y/UTILS.VecNorm(unitvel)) + local angle = math.asin( unitvel.y / UTILS.VecNorm( unitvel ) ) -- Return angle in degrees. - return math.deg(angle) + return math.deg( angle ) else return 0 end + end return nil end ---- Returns the pitch angle of a unit. --- @param Wrapper.Positionable#POSITIONABLE self --- @return #number Pitch ange in degrees. +--- Returns the pitch angle of a POSITIONABLE. +-- @param #POSITIONABLE self +-- @return #number Pitch angle in degrees. +-- @return #nil The POSITIONABLE is not existing or alive. function POSITIONABLE:GetPitch() -- Get position of the unit. local unitpos = self:GetPosition() if unitpos then - return math.deg(math.asin(unitpos.x.y)) + return math.deg( math.asin( unitpos.x.y ) ) end return nil end --- Returns the roll angle of a unit. --- @param Wrapper.Positionable#POSITIONABLE self --- @return #number Pitch ange in degrees. +-- @param #POSITIONABLE self +-- @return #number Pitch angle in degrees. +-- @return #nil The POSITIONABLE is not existing or alive. function POSITIONABLE:GetRoll() -- Get position of the unit. @@ -33958,87 +41195,98 @@ function POSITIONABLE:GetRoll() if unitpos then - --first, make a vector that is perpendicular to y and unitpos.x with cross product - local cp = UTILS.VecCross(unitpos.x, {x = 0, y = 1, z = 0}) + -- First, make a vector that is perpendicular to y and unitpos.x with cross product + local cp = UTILS.VecCross( unitpos.x, { x = 0, y = 1, z = 0 } ) - --now, get dot product of of this cross product with unitpos.z - local dp = UTILS.VecDot(cp, unitpos.z) + -- Now, get dot product of of this cross product with unitpos.z + local dp = UTILS.VecDot( cp, unitpos.z ) - --now get the magnitude of the roll (magnitude of the angle between two vectors is acos(vec1.vec2/|vec1||vec2|) - local Roll = math.acos(dp/(UTILS.VecNorm(cp)*UTILS.VecNorm(unitpos.z))) + -- Now get the magnitude of the roll (magnitude of the angle between two vectors is acos(vec1.vec2/|vec1||vec2|) + local Roll = math.acos( dp / (UTILS.VecNorm( cp ) * UTILS.VecNorm( unitpos.z )) ) - --now, have to get sign of roll. - -- by convention, making right roll positive - -- to get sign of roll, use the y component of unitpos.z. For right roll, y component is negative. + -- Now, have to get sign of roll. By convention, making right roll positive + -- To get sign of roll, use the y component of unitpos.z. For right roll, y component is negative. if unitpos.z.y > 0 then -- left roll, flip the sign of the roll Roll = -Roll end - return math.deg(Roll) + return math.deg( Roll ) + end + + return nil end ---- Returns the yaw angle of a unit. --- @param Wrapper.Positionable#POSITIONABLE self --- @return #number Yaw ange in degrees. +--- Returns the yaw angle of a POSITIONABLE. +-- @param #POSITIONABLE self +-- @return #number Yaw angle in degrees. +-- @return #nil The POSITIONABLE is not existing or alive. function POSITIONABLE:GetYaw() + -- Get position of the unit. local unitpos = self:GetPosition() + if unitpos then + -- get unit velocity local unitvel = self:GetVelocityVec3() - if unitvel and UTILS.VecNorm(unitvel) ~= 0 then --must have non-zero velocity! - local AxialVel = {} --unit velocity transformed into aircraft axes directions + if unitvel and UTILS.VecNorm( unitvel ) ~= 0 then -- must have non-zero velocity! + local AxialVel = {} -- unit velocity transformed into aircraft axes directions - --transform velocity components in direction of aircraft axes. - AxialVel.x = UTILS.VecDot(unitpos.x, unitvel) - AxialVel.y = UTILS.VecDot(unitpos.y, unitvel) - AxialVel.z = UTILS.VecDot(unitpos.z, unitvel) + -- transform velocity components in direction of aircraft axes. + AxialVel.x = UTILS.VecDot( unitpos.x, unitvel ) + AxialVel.y = UTILS.VecDot( unitpos.y, unitvel ) + AxialVel.z = UTILS.VecDot( unitpos.z, unitvel ) - --Yaw is the angle between unitpos.x and the x and z velocities - --define right yaw as positive - local Yaw = math.acos(UTILS.VecDot({x = 1, y = 0, z = 0}, {x = AxialVel.x, y = 0, z = AxialVel.z})/UTILS.VecNorm({x = AxialVel.x, y = 0, z = AxialVel.z})) + -- Yaw is the angle between unitpos.x and the x and z velocities + -- define right yaw as positive + local Yaw = math.acos( UTILS.VecDot( { x = 1, y = 0, z = 0 }, { x = AxialVel.x, y = 0, z = AxialVel.z } ) / UTILS.VecNorm( { x = AxialVel.x, y = 0, z = AxialVel.z } ) ) - --now set correct direction: + -- now set correct direction: if AxialVel.z > 0 then Yaw = -Yaw end return Yaw end + end + return nil end - --- Returns the message text with the callsign embedded (if there is one). -- @param #POSITIONABLE self --- @param #string Message The message text --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. --- @return #string The message text +-- @param #string Message The message text. +-- @param #string Name (Optional) The Name of the sender. If not provided, Name is set to the type of the POSITIONABLE. +-- @return #string The message text. +-- @return #nil The POSITIONABLE is not existing or alive. function POSITIONABLE:GetMessageText( Message, Name ) local DCSObject = self:GetDCSObject() + if DCSObject then - local Callsign = string.format( "%s", ( ( Name ~= "" and Name ) or self:GetCallsign() ~= "" and self:GetCallsign() ) or self:GetName() ) - local MessageText = string.format("%s - %s", Callsign, Message ) + + local Callsign = string.format( "%s", ((Name ~= "" and Name) or self:GetCallsign() ~= "" and self:GetCallsign()) or self:GetName() ) + local MessageText = string.format( "%s - %s", Callsign, Message ) return MessageText + end return nil end - --- Returns a message with the callsign embedded (if there is one). -- @param #POSITIONABLE self -- @param #string Message The message text -- @param DCS#Duration Duration The duration of the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +-- @param #string Name (Optional) The Name of the sender. If not provided, Name is set to the type of the POSITIONABLE. -- @return Core.Message#MESSAGE -function POSITIONABLE:GetMessage( Message, Duration, Name ) --R2.1 changed callsign and name and using GetMessageText +function POSITIONABLE:GetMessage( Message, Duration, Name ) local DCSObject = self:GetDCSObject() + if DCSObject then local MessageText = self:GetMessageText( Message, Name ) return MESSAGE:New( MessageText, Duration ) @@ -34051,7 +41299,7 @@ end -- @param #POSITIONABLE self -- @param #string Message The message text -- @param Core.Message#MESSAGE MessageType MessageType The message type. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +-- @param #string Name (Optional) The Name of the sender. If not provided, Name is set to the type of the POSITIONABLE. -- @return Core.Message#MESSAGE function POSITIONABLE:GetMessageType( Message, MessageType, Name ) -- R2.2 changed callsign and name and using GetMessageText @@ -34069,7 +41317,7 @@ end -- @param #POSITIONABLE self -- @param #string Message The message text -- @param DCS#Duration Duration The duration of the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +-- @param #string Name (Optional) The Name of the sender. If not provided, Name is set to the type of the POSITIONABLE. function POSITIONABLE:MessageToAll( Message, Duration, Name ) self:F2( { Message, Duration } ) @@ -34087,7 +41335,7 @@ end -- @param #string Message The message text -- @param DCS#Duration Duration The duration of the message. -- @param DCS#coalition MessageCoalition The Coalition receiving the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +-- @param #string Name (Optional) The Name of the sender. If not provided, Name is set to the type of the POSITIONABLE. function POSITIONABLE:MessageToCoalition( Message, Duration, MessageCoalition, Name ) self:F2( { Message, Duration } ) @@ -34101,14 +41349,13 @@ function POSITIONABLE:MessageToCoalition( Message, Duration, MessageCoalition, N return nil end - --- Send a message to a coalition. -- 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 Core.Message#MESSAGE.Type MessageType The message type that determines the duration. -- @param DCS#coalition MessageCoalition The Coalition receiving the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +-- @param #string Name (Optional) The Name of the sender. If not provided, Name is set to the type of the POSITIONABLE. function POSITIONABLE:MessageTypeToCoalition( Message, MessageType, MessageCoalition, Name ) self:F2( { Message, MessageType } ) @@ -34122,13 +41369,12 @@ function POSITIONABLE:MessageTypeToCoalition( Message, MessageType, MessageCoali return nil end - --- Send a message to the red coalition. -- 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 #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +-- @param #string Name (Optional) The Name of the sender. If not provided, Name is set to the type of the POSITIONABLE. function POSITIONABLE:MessageToRed( Message, Duration, Name ) self:F2( { Message, Duration } ) @@ -34145,7 +41391,7 @@ end -- @param #POSITIONABLE self -- @param #string Message The message text -- @param DCS#Duration Duration The duration of the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +-- @param #string Name (Optional) The Name of the sender. If not provided, Name is set to the type of the POSITIONABLE. function POSITIONABLE:MessageToBlue( Message, Duration, Name ) self:F2( { Message, Duration } ) @@ -34163,7 +41409,7 @@ end -- @param #string Message The message text -- @param DCS#Duration Duration The duration of the message. -- @param Wrapper.Client#CLIENT Client The client object receiving the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +-- @param #string Name (Optional) The Name of the sender. If not provided, Name is set to the type of the POSITIONABLE. function POSITIONABLE:MessageToClient( Message, Duration, Client, Name ) self:F2( { Message, Duration } ) @@ -34175,13 +41421,37 @@ function POSITIONABLE:MessageToClient( Message, Duration, Client, 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 +end + --- Send a message 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 -- @param #string Message The message text -- @param DCS#Duration Duration The duration of the message. -- @param Wrapper.Group#GROUP MessageGroup The GROUP object receiving the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +-- @param #string Name (Optional) The Name of the sender. If not provided, Name is set to the type of the POSITIONABLE. function POSITIONABLE:MessageToGroup( Message, Duration, MessageGroup, Name ) self:F2( { Message, Duration } ) @@ -34194,7 +41464,38 @@ function POSITIONABLE:MessageToGroup( Message, Duration, MessageGroup, Name ) BASE:E( { "Message not sent to Group; Group is not alive...", Message = Message, MessageGroup = MessageGroup } ) end else - BASE:E( { "Message not sent to Group; Positionable is not alive ...", Message = Message, Positionable = self, MessageGroup = MessageGroup } ) + BASE:E( { + "Message not sent to Group; Positionable is not alive ...", + Message = Message, + Positionable = self, + MessageGroup = MessageGroup + } ) + end + end + + 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 @@ -34208,7 +41509,7 @@ end -- @param #string Message The message text -- @param Core.Message#MESSAGE.Type MessageType The message type that determines the duration. -- @param Wrapper.Group#GROUP MessageGroup The GROUP object receiving the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +-- @param #string Name (Optional) The Name of the sender. If not provided, the Name is the type of the POSITIONABLE. function POSITIONABLE:MessageTypeToGroup( Message, MessageType, MessageGroup, Name ) self:F2( { Message, MessageType } ) @@ -34228,16 +41529,38 @@ end -- @param #string Message The message text -- @param DCS#Duration Duration The duration of the message. -- @param Core.Set#SET_GROUP MessageSetGroup The SET_GROUP collection receiving the message. +-- @param #string Name (Optional) The Name of the sender. If not provided, Name is set to the type of the POSITIONABLE. +function POSITIONABLE:MessageToSetGroup( Message, Duration, MessageSetGroup, Name ) + self:F2( { Message, Duration } ) + + local DCSObject = self:GetDCSObject() + if DCSObject then + if DCSObject:isExist() then + MessageSetGroup:ForEachGroupAlive( function( MessageGroup ) + self:GetMessage( Message, Duration, Name ):ToGroup( MessageGroup ) + end ) + end + end + + 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:MessageToSetGroup( Message, Duration, MessageSetGroup, Name ) --R2.1 +function POSITIONABLE:MessageToSetUnit( Message, Duration, MessageSetUnit, Name ) self:F2( { Message, Duration } ) local DCSObject = self:GetDCSObject() if DCSObject then if DCSObject:isExist() then - MessageSetGroup:ForEachGroupAlive( + MessageSetUnit:ForEachUnit( function( MessageGroup ) - self:GetMessage( Message, Duration, Name ):ToGroup( MessageGroup ) + self:GetMessage( Message, Duration, Name ):ToUnit( MessageGroup ) end ) end @@ -34246,12 +41569,36 @@ function POSITIONABLE:MessageToSetGroup( Message, Duration, MessageSetGroup, Nam return nil end ---- Send a message to the players in the @{Wrapper.Group}. +--- 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 +-- @param #string Message The message text +-- @param DCS#Duration Duration The duration of the message. +-- @param #string Name (Optional) The Name of the sender. If not provided, Name is set to the type of the POSITIONABLE. function POSITIONABLE:Message( Message, Duration, Name ) self:F2( { Message, Duration } ) @@ -34263,30 +41610,30 @@ function POSITIONABLE:Message( Message, Duration, Name ) return nil end ---- Create a @{Core.Radio#RADIO}, to allow radio transmission for this POSITIONABLE. +--- Create a @{Sound.Radio#RADIO}, to allow radio transmission for this POSITIONABLE. -- Set parameters with the methods provided, then use RADIO:Broadcast() to actually broadcast the message -- @param #POSITIONABLE self --- @return Core.Radio#RADIO Radio -function POSITIONABLE:GetRadio() --R2.1 - self:F2(self) - return RADIO:New(self) +-- @return Sound.Radio#RADIO Radio +function POSITIONABLE:GetRadio() + self:F2( self ) + return RADIO:New( self ) end ---- Create a @{Core.Radio#BEACON}, to allow this POSITIONABLE to broadcast beacon signals +--- Create a @{Core.Beacon#BEACON}, to allow this POSITIONABLE to broadcast beacon signals. -- @param #POSITIONABLE self --- @return Core.Radio#RADIO Radio -function POSITIONABLE:GetBeacon() --R2.1 - self:F2(self) - return BEACON:New(self) +-- @return Core.Beacon#BEACON Beacon +function POSITIONABLE:GetBeacon() + self:F2( self ) + return BEACON:New( self ) end ---- Start Lasing a POSITIONABLE +--- Start Lasing a POSITIONABLE. -- @param #POSITIONABLE self -- @param #POSITIONABLE Target The target to lase. -- @param #number LaserCode Laser code or random number in [1000, 9999]. -- @param #number Duration Duration of lasing in seconds. -- @return Core.Spot#SPOT -function POSITIONABLE:LaseUnit( Target, LaserCode, Duration ) --R2.1 +function POSITIONABLE:LaseUnit( Target, LaserCode, Duration ) self:F2() LaserCode = LaserCode or math.random( 1000, 9999 ) @@ -34294,9 +41641,9 @@ function POSITIONABLE:LaseUnit( Target, LaserCode, Duration ) --R2.1 local RecceDcsUnit = self:GetDCSObject() local TargetVec3 = Target:GetVec3() - self:F("bulding spot") + self:F( "building spot" ) self.Spot = SPOT:New( self ) -- Core.Spot#SPOT - self.Spot:LaseOn( Target, LaserCode, Duration) + self.Spot:LaseOn( Target, LaserCode, Duration ) self.LaserCode = LaserCode return self.Spot @@ -34305,26 +41652,26 @@ end --- Start Lasing a COORDINATE. -- @param #POSITIONABLE self --- @param Core.Point#COORDIUNATE Coordinate The coordinate where the lase is pointing at. +-- @param Core.Point#COORDINATE Coordinate The coordinate where the lase is pointing at. -- @param #number LaserCode Laser code or random number in [1000, 9999]. -- @param #number Duration Duration of lasing in seconds. -- @return Core.Spot#SPOT -function POSITIONABLE:LaseCoordinate(Coordinate, LaserCode, Duration) +function POSITIONABLE:LaseCoordinate( Coordinate, LaserCode, Duration ) self:F2() - LaserCode = LaserCode or math.random(1000, 9999) + LaserCode = LaserCode or math.random( 1000, 9999 ) - self.Spot = SPOT:New(self) -- Core.Spot#SPOT - self.Spot:LaseOnCoordinate(Coordinate, LaserCode, Duration) + self.Spot = SPOT:New( self ) -- Core.Spot#SPOT + self.Spot:LaseOnCoordinate( Coordinate, LaserCode, Duration ) self.LaserCode = LaserCode return self.Spot end ---- Stop Lasing a POSITIONABLE +--- Stop Lasing a POSITIONABLE. -- @param #POSITIONABLE self -- @return #POSITIONABLE -function POSITIONABLE:LaseOff() --R2.1 +function POSITIONABLE:LaseOff() self:F2() if self.Spot then @@ -34335,10 +41682,10 @@ function POSITIONABLE:LaseOff() --R2.1 return self end ---- Check if the POSITIONABLE is lasing a target +--- Check if the POSITIONABLE is lasing a target. -- @param #POSITIONABLE self -- @return #boolean true if it is lasing a target -function POSITIONABLE:IsLasing() --R2.1 +function POSITIONABLE:IsLasing() self:F2() local Lasing = false @@ -34353,7 +41700,7 @@ end --- Get the Spot -- @param #POSITIONABLE self -- @return Core.Spot#SPOT The Spot -function POSITIONABLE:GetSpot() --R2.1 +function POSITIONABLE:GetSpot() return self.Spot end @@ -34361,7 +41708,7 @@ end --- Get the last assigned laser code -- @param #POSITIONABLE self -- @return #number The laser code -function POSITIONABLE:GetLaserCode() --R2.1 +function POSITIONABLE:GetLaserCode() return self.LaserCode end @@ -34384,8 +41731,6 @@ do -- Cargo return self.__.Cargo end - - --- Remove cargo. -- @param #POSITIONABLE self -- @param Core.Cargo#CARGO Cargo @@ -34430,24 +41775,22 @@ 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 -- @return #number CargoBayFreeWeight function POSITIONABLE:GetCargoBayFreeWeight() - -- When there is no cargo bay weight limit set, then calculate this for this positionable! + -- When there is no cargo bay weight limit set, then calculate this for this POSITIONABLE! if not self.__.CargoBayWeightLimit then self:SetCargoBayWeightLimit() end @@ -34459,67 +41802,109 @@ 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 + 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. + --- - -- When an airplane or helicopter, we calculate the weightlimit based on the descriptor. + -- 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 ["Blitz_36-6700A"] = 10, -- new by kappa - ["BMD-1"] = 9, -- IRL should be 4 passengers + ["BMD-1"] = 9, -- IRL should be 4 passengers ["BMP-1"] = 8, ["BMP-2"] = 7, - ["BMP-3"] = 8, -- IRL should be 7+2 passengers + ["BMP-3"] = 8, -- IRL should be 7+2 passengers ["Boman"] = 25, ["BTR-80"] = 9, -- IRL should be 7 passengers ["BTR-82A"] = 9, -- new by kappa -- IRL should be 7 passengers - ["BTR_D"] = 12, -- IRL should be 10 passengers + ["BTR_D"] = 12, -- IRL should be 10 passengers ["Cobra"] = 8, ["Land_Rover_101_FC"] = 11, -- new by kappa ["Land_Rover_109_S3"] = 7, -- new by kappa @@ -34530,11 +41915,11 @@ do -- Cargo ["M1126 Stryker ICV"] = 9, ["M1134 Stryker ATGM"] = 9, ["M2A1_halftrack"] = 9, - ["M-113"] = 9, -- IRL should be 11 passengers + ["M-113"] = 9, -- IRL should be 11 passengers ["Marder"] = 6, ["MCV-80"] = 9, -- IRL should be 7 passengers ["MLRS FDDM"] = 4, - ["MTLB"] = 25, -- IRL should be 11 passengers + ["MTLB"] = 25, -- IRL should be 11 passengers ["GAZ-66"] = 8, ["GAZ-3307"] = 12, ["GAZ-3308"] = 14, @@ -34543,21 +41928,42 @@ 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, ["Ural-4320 APA-5D"] = 10, ["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. @@ -34565,28 +41971,28 @@ end --- Cargo -- @param Utilities.Utils#FLARECOLOR FlareColor function POSITIONABLE:Flare( FlareColor ) self:F2() - trigger.action.signalFlare( self:GetVec3(), FlareColor , 0 ) + trigger.action.signalFlare( self:GetVec3(), FlareColor, 0 ) end --- Signal a white flare at the position of the POSITIONABLE. -- @param #POSITIONABLE self function POSITIONABLE:FlareWhite() self:F2() - trigger.action.signalFlare( self:GetVec3(), trigger.flareColor.White , 0 ) + trigger.action.signalFlare( self:GetVec3(), trigger.flareColor.White, 0 ) end --- Signal a yellow flare at the position of the POSITIONABLE. -- @param #POSITIONABLE self function POSITIONABLE:FlareYellow() self:F2() - trigger.action.signalFlare( self:GetVec3(), trigger.flareColor.Yellow , 0 ) + trigger.action.signalFlare( self:GetVec3(), trigger.flareColor.Yellow, 0 ) end --- Signal a green flare at the position of the POSITIONABLE. -- @param #POSITIONABLE self function POSITIONABLE:FlareGreen() self:F2() - trigger.action.signalFlare( self:GetVec3(), trigger.flareColor.Green , 0 ) + trigger.action.signalFlare( self:GetVec3(), trigger.flareColor.Green, 0 ) end --- Signal a red flare at the position of the POSITIONABLE. @@ -34601,9 +42007,9 @@ end --- Smoke the POSITIONABLE. -- @param #POSITIONABLE self --- @param Utilities.Utils#SMOKECOLOR SmokeColor The color to smoke to positionable. --- @param #number Range The range in meters to randomize the smoking around the positionable. --- @param #number AddHeight The height in meters to add to the altitude of the positionable. +-- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. +-- @param #number Range The range in meters to randomize the smoking around the POSITIONABLE. +-- @param #number AddHeight The height in meters to add to the altitude of the POSITIONABLE. function POSITIONABLE:Smoke( SmokeColor, Range, AddHeight ) self:F2() if Range then @@ -34653,8 +42059,7 @@ function POSITIONABLE:SmokeBlue() trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Blue ) end - ---- Returns true if the unit is within a @{Zone}. +--- Returns true if the unit is within a @{Core.Zone}. -- @param #POSITIONABLE self -- @param Core.Zone#ZONE_BASE Zone The zone to test. -- @return #boolean Returns true if the unit is within the @{Core.Zone#ZONE_BASE} @@ -34669,7 +42074,7 @@ function POSITIONABLE:IsInZone( Zone ) return false end ---- Returns true if the unit is not within a @{Zone}. +--- Returns true if the unit is not within a @{Core.Zone}. -- @param #POSITIONABLE self -- @param Core.Zone#ZONE_BASE Zone The zone to test. -- @return #boolean Returns true if the unit is not within the @{Core.Zone#ZONE_BASE} @@ -34684,7 +42089,7 @@ function POSITIONABLE:IsNotInZone( Zone ) return false end end - --- **Wrapper** -- CONTROLLABLE is an intermediate class wrapping Group and Unit classes "controllers". +--- **Wrapper** - CONTROLLABLE is an intermediate class wrapping Group and Unit classes "controllers". -- -- === -- @@ -34697,14 +42102,11 @@ end -- @module Wrapper.Controllable -- @image Wrapper_Controllable.JPG - --- @type CONTROLLABLE -- @field DCS#Controllable DCSControllable The DCS controllable class. -- @field #string ControllableName The name of the controllable. -- @extends Wrapper.Positionable#POSITIONABLE - - --- Wrapper class to handle the "DCS Controllable objects", which are Groups and Units: -- -- * Support all DCS Controllable APIs. @@ -34721,7 +42123,7 @@ end -- # 2) CONTROLLABLE Task methods -- -- Several controllable task methods are available that help you to prepare tasks. --- These methods return a string consisting of the task description, which can then be given to either a @{Wrapper.Controllable#CONTROLLABLE.PushTask} or @{Wrapper.Controllable#SetTask} method to assign the task to the CONTROLLABLE. +-- These methods return a string consisting of the task description, which can then be given to either a @{#CONTROLLABLE.PushTask}() or @{#CONTROLLABLE.SetTask}() method to assign the task to the CONTROLLABLE. -- Tasks are specific for the category of the CONTROLLABLE, more specific, for AIR, GROUND or AIR and GROUND. -- Each task description where applicable indicates for which controllable category the task is valid. -- There are 2 main subdivisions of tasks: Assigned tasks and EnRoute tasks. @@ -34748,14 +42150,14 @@ end -- * @{#CONTROLLABLE.TaskHoldPosition}: (AIR) Hold position at the current position of the first unit of the controllable. -- * @{#CONTROLLABLE.TaskLand}: (AIR HELICOPTER) Landing at the ground. For helicopters only. -- * @{#CONTROLLABLE.TaskLandAtZone}: (AIR) Land the controllable at a @{Core.Zone#ZONE_RADIUS). --- * @{#CONTROLLABLE.TaskOrbitCircle}: (AIR) Orbit at the current position of the first unit of the controllable at a specified alititude. --- * @{#CONTROLLABLE.TaskOrbitCircleAtVec2}: (AIR) Orbit at a specified position at a specified alititude during a specified duration with a specified speed. +-- * @{#CONTROLLABLE.TaskOrbitCircle}: (AIR) Orbit at the current position of the first unit of the controllable at a specified altitude. +-- * @{#CONTROLLABLE.TaskOrbitCircleAtVec2}: (AIR) Orbit at a specified position at a specified altitude during a specified duration with a specified speed. -- * @{#CONTROLLABLE.TaskRefueling}: (AIR) Refueling from the nearest tanker. No parameters. --- * @{#CONTROLLABLE.TaskRoute}: (AIR + GROUND) Return a Misson task to follow a given route defined by Points. +-- * @{#CONTROLLABLE.TaskRecoveryTanker}: (AIR) Set group to act as recovery tanker for a naval group. +-- * @{#CONTROLLABLE.TaskRoute}: (AIR + GROUND) Return a Mission task to follow a given route defined by Points. -- * @{#CONTROLLABLE.TaskRouteToVec2}: (AIR + GROUND) Make the Controllable move to a given point. -- * @{#CONTROLLABLE.TaskRouteToVec3}: (AIR + GROUND) Make the Controllable move to a given point. -- * @{#CONTROLLABLE.TaskRouteToZone}: (AIR + GROUND) Route the controllable to a given zone. --- * @{#CONTROLLABLE.TaskReturnToBase}: (AIR) Route the controllable to an airbase. -- -- ## 2.2) EnRoute assignment -- @@ -34772,7 +42174,7 @@ end -- -- ## 2.3) Task preparation -- --- There are certain task methods that allow to tailor the task behaviour: +-- There are certain task methods that allow to tailor the task behavior: -- -- * @{#CONTROLLABLE.TaskWrappedAction}: Return a WrappedAction Task taking a Command. -- * @{#CONTROLLABLE.TaskCombo}: Return a Combo Task taking an array of Tasks. @@ -34819,7 +42221,7 @@ end -- -- # 5) Option methods -- --- Controllable **Option methods** change the behaviour of the Controllable while being alive. +-- Controllable **Option methods** change the behavior of the Controllable while being alive. -- -- ## 5.1) Rule of Engagement: -- @@ -34876,7 +42278,7 @@ CONTROLLABLE = { -- @return #CONTROLLABLE self function CONTROLLABLE:New( ControllableName ) local self = BASE:Inherit( self, POSITIONABLE:New( ControllableName ) ) -- #CONTROLLABLE - --self:F( ControllableName ) + -- self:F( ControllableName ) self.ControllableName = ControllableName self.TaskScheduler = SCHEDULER:New( self ) @@ -34901,7 +42303,6 @@ end -- Get methods - --- Returns the health. Dead controllables have health <= 1.0. -- @param #CONTROLLABLE self -- @return #number The controllable health value (unit or group average). @@ -34960,7 +42361,7 @@ function CONTROLLABLE:GetLife0() end --- Returns relative minimum amount of fuel (from 0.0 to 1.0) a unit or group has in its internal tanks. --- This method returns nil to ensure polymorphic behaviour! This method needs to be overridden by GROUP or UNIT. +-- This method returns nil to ensure polymorphic behavior! This method needs to be overridden by GROUP or UNIT. -- @param #CONTROLLABLE self -- @return #nil The CONTROLLABLE is not existing or alive. function CONTROLLABLE:GetFuelMin() @@ -34970,7 +42371,7 @@ function CONTROLLABLE:GetFuelMin() end --- Returns relative average amount of fuel (from 0.0 to 1.0) a unit or group has in its internal tanks. --- This method returns nil to ensure polymorphic behaviour! This method needs to be overridden by GROUP or UNIT. +-- This method returns nil to ensure polymorphic behavior! This method needs to be overridden by GROUP or UNIT. -- @param #CONTROLLABLE self -- @return #nil The CONTROLLABLE is not existing or alive. function CONTROLLABLE:GetFuelAve() @@ -34980,7 +42381,7 @@ function CONTROLLABLE:GetFuelAve() end --- Returns relative amount of fuel (from 0.0 to 1.0) the unit has in its internal tanks. --- This method returns nil to ensure polymorphic behaviour! This method needs to be overridden by GROUP or UNIT. +-- This method returns nil to ensure polymorphic behavior! This method needs to be overridden by GROUP or UNIT. -- @param #CONTROLLABLE self -- @return #nil The CONTROLLABLE is not existing or alive. function CONTROLLABLE:GetFuel() @@ -34988,7 +42389,6 @@ function CONTROLLABLE:GetFuel() return nil end - -- Tasks --- Clear all tasks from the controllable. @@ -35007,10 +42407,9 @@ function CONTROLLABLE:ClearTasks() return nil end - --- Popping current Task from the controllable. -- @param #CONTROLLABLE self --- @return Wrapper.Controllable#CONTROLLABLE self +-- @return #CONTROLLABLE self function CONTROLLABLE:PopCurrentTask() self:F2() @@ -35027,7 +42426,7 @@ end --- Pushing Task on the queue from the controllable. -- @param #CONTROLLABLE self --- @return Wrapper.Controllable#CONTROLLABLE self +-- @return #CONTROLLABLE self function CONTROLLABLE:PushTask( DCSTask, WaitTime ) self:F2() @@ -35066,7 +42465,7 @@ end -- @param #CONTROLLABLE self -- @param DCS#Task DCSTask DCS Task array. -- @param #number WaitTime Time in seconds, before the task is set. --- @return Wrapper.Controllable#CONTROLLABLE self +-- @return #CONTROLLABLE self function CONTROLLABLE:SetTask( DCSTask, WaitTime ) self:F( { "SetTask", WaitTime, DCSTask = DCSTask } ) @@ -35085,7 +42484,7 @@ function CONTROLLABLE:SetTask( DCSTask, WaitTime ) local function SetTask( Controller, DCSTask ) if self and self:IsAlive() then local Controller = self:_GetController() - --self:I( "Before SetTask" ) + -- self:I( "Before SetTask" ) Controller:setTask( DCSTask ) -- AI_FORMATION class (used by RESCUEHELO) calls SetTask twice per second! hence spamming the DCS log file ==> setting this to trace. self:T( { ControllableName = self:GetName(), DCSTask = DCSTask } ) @@ -35110,8 +42509,8 @@ end --- Checking the Task Queue of the controllable. Returns false if no task is on the queue. true if there is a task. -- @param #CONTROLLABLE self --- @return Wrapper.Controllable#CONTROLLABLE self -function CONTROLLABLE:HasTask() --R2.2 +-- @return #CONTROLLABLE self +function CONTROLLABLE:HasTask() -- R2.2 local HasTaskResult = false @@ -35126,7 +42525,6 @@ function CONTROLLABLE:HasTask() --R2.2 return HasTaskResult end - --- Return a condition section for a controlled task. -- @param #CONTROLLABLE self -- @param DCS#Time time DCS mission time. @@ -35138,7 +42536,7 @@ end -- return DCS#Task function CONTROLLABLE:TaskCondition( time, userFlag, userFlagValue, condition, duration, lastWayPoint ) ---[[ + --[[ StopCondition = { time = Time, userFlag = string, @@ -35171,8 +42569,8 @@ function CONTROLLABLE:TaskControlled( DCSTask, DCSStopCondition ) id = 'ControlledTask', params = { task = DCSTask, - stopCondition = DCSStopCondition - } + stopCondition = DCSStopCondition, + }, } return DCSTaskControlled @@ -35187,8 +42585,8 @@ function CONTROLLABLE:TaskCombo( DCSTasks ) local DCSTaskCombo = { id = 'ComboTask', params = { - tasks = DCSTasks - } + tasks = DCSTasks, + }, } return DCSTaskCombo @@ -35226,9 +42624,6 @@ function CONTROLLABLE:SetTaskWaypoint( Waypoint, Task ) return Waypoint.task end - - - --- Executes a command action for the CONTROLLABLE. -- @param #CONTROLLABLE self -- @param DCS#Command DCSCommand The command to be executed. @@ -35253,17 +42648,18 @@ end -- @param #number ToWayPoint -- @return DCS#Task -- @usage --- --- This test demonstrates the use(s) of the SwitchWayPoint method of the GROUP class. --- HeliGroup = GROUP:FindByName( "Helicopter" ) --- --- --- Route the helicopter back to the FARP after 60 seconds. --- -- We use the SCHEDULER class to do this. --- SCHEDULER:New( nil, --- function( HeliGroup ) --- local CommandRTB = HeliGroup:CommandSwitchWayPoint( 2, 8 ) --- HeliGroup:SetCommand( CommandRTB ) --- end, { HeliGroup }, 90 --- ) +-- +-- -- This test demonstrates the use(s) of the SwitchWayPoint method of the GROUP class. +-- HeliGroup = GROUP:FindByName( "Helicopter" ) +-- +-- -- Route the helicopter back to the FARP after 60 seconds. +-- -- We use the SCHEDULER class to do this. +-- SCHEDULER:New( nil, +-- function( HeliGroup ) +-- local CommandRTB = HeliGroup:CommandSwitchWayPoint( 2, 8 ) +-- HeliGroup:SetCommand( CommandRTB ) +-- end, { HeliGroup }, 90 +-- ) function CONTROLLABLE:CommandSwitchWayPoint( FromWayPoint, ToWayPoint ) self:F2( { FromWayPoint, ToWayPoint } ) @@ -35305,16 +42701,15 @@ function CONTROLLABLE:CommandStopRoute( StopRoute ) return CommandStopRoute end - --- Give an uncontrolled air controllable the start command. -- @param #CONTROLLABLE self -- @param #number delay (Optional) Delay before start command in seconds. -- @return #CONTROLLABLE self -function CONTROLLABLE:StartUncontrolled(delay) - if delay and delay>0 then - SCHEDULER:New(nil, CONTROLLABLE.StartUncontrolled, {self}, delay) +function CONTROLLABLE:StartUncontrolled( delay ) + if delay and delay > 0 then + SCHEDULER:New( nil, CONTROLLABLE.StartUncontrolled, { self }, delay ) else - self:SetCommand({id='Start', params={}}) + self:SetCommand( { id = 'Start', params = {} } ) end return self end @@ -35323,10 +42718,10 @@ end -- For specific beacons like TACAN use the more convenient @{#BEACON} class. -- Note that a controllable can only have one beacon activated at a time with the execption of ICLS. -- @param #CONTROLLABLE self --- @param Core.Radio#BEACON.Type Type Beacon type (VOR, DME, TACAN, RSBN, ILS etc). --- @param Core.Radio#BEACON.System System Beacon system (VOR, DME, TACAN, RSBN, ILS etc). +-- @param Core.Beacon#BEACON.Type Type Beacon type (VOR, DME, TACAN, RSBN, ILS etc). +-- @param Core.Beacon#BEACON.System System Beacon system (VOR, DME, TACAN, RSBN, ILS etc). -- @param #number Frequency Frequency in Hz the beacon is running on. Use @{#UTILS.TACANToFrequency} to generate a frequency for TACAN beacons. --- @param #number UnitID The ID of the unit the beacon is attached to. Usefull if more units are in one group. +-- @param #number UnitID The ID of the unit the beacon is attached to. Useful if more units are in one group. -- @param #number Channel Channel the beacon is using. For, e.g. TACAN beacons. -- @param #string ModeChannel The TACAN mode of the beacon, i.e. "X" or "Y". -- @param #boolean AA If true, create and Air-Air beacon. IF nil, automatically set if CONTROLLABLE depending on whether unit is and aircraft or not. @@ -35334,13 +42729,13 @@ end -- @param #boolean Bearing If true, beacon provides bearing information - if supported by the unit the beacon is attached to. -- @param #number Delay (Optional) Delay in seconds before the beacon is activated. -- @return #CONTROLLABLE self -function CONTROLLABLE:CommandActivateBeacon(Type, System, Frequency, UnitID, Channel, ModeChannel, AA, Callsign, Bearing, Delay) +function CONTROLLABLE:CommandActivateBeacon( Type, System, Frequency, UnitID, Channel, ModeChannel, AA, Callsign, Bearing, Delay ) - AA=AA or self:IsAir() - UnitID=UnitID or self:GetID() + AA = AA or self:IsAir() + UnitID = UnitID or self:GetID() -- Command - local CommandActivateBeacon= { + local CommandActivateBeacon = { id = "ActivateBeacon", params = { ["type"] = Type, @@ -35352,13 +42747,62 @@ function CONTROLLABLE:CommandActivateBeacon(Type, System, Frequency, UnitID, Cha ["AA"] = AA, ["callsign"] = Callsign, ["bearing"] = Bearing, - } + }, } - if Delay and Delay>0 then - SCHEDULER:New(nil, self.CommandActivateBeacon, {self, Type, System, Frequency, UnitID, Channel, ModeChannel, AA, Callsign, Bearing}, Delay) + if Delay and Delay > 0 then + SCHEDULER:New( nil, self.CommandActivateBeacon, { self, Type, System, Frequency, UnitID, Channel, ModeChannel, AA, Callsign, Bearing }, Delay ) + else + self:SetCommand( CommandActivateBeacon ) + end + + return self +end + +--- Activate ACLS system of the CONTROLLABLE. The controllable should be an aircraft carrier! Also needs Link4 to work. +-- @param #CONTROLLABLE self +-- @param #number UnitID (Optional) The DCS UNIT ID of the unit the ACLS system is attached to. Defaults to the UNIT itself. +-- @param #string Name (Optional) Name of the ACLS Beacon +-- @param #number Delay (Optional) Delay in seconds before the ICLS is activated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandActivateACLS( UnitID, Name, Delay ) + + -- Command to activate ACLS system. + local CommandActivateACLS= { + id = 'ActivateACLS', + params = { + unitId = UnitID or self:GetID(), + name = Name or "ACL", + } +} + + self:T({CommandActivateACLS}) + + if Delay and Delay > 0 then + SCHEDULER:New( nil, self.CommandActivateACLS, { self, UnitID, Name }, Delay ) + else + self:SetCommand( CommandActivateACLS ) + end + + return self +end + +--- Deactivate ACLS system of the CONTROLLABLE. The controllable should be an aircraft carrier! +-- @param #CONTROLLABLE self +-- @param #number Delay (Optional) Delay in seconds before the ICLS is deactivated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandDeactivateACLS( Delay ) + + -- Command to activate ACLS system. + local CommandDeactivateACLS= { + id = 'DeactivateACLS', + params = { } +} + + if Delay and Delay > 0 then + SCHEDULER:New( nil, self.CommandDeactivateACLS, { self }, Delay ) else - self:SetCommand(CommandActivateBeacon) + self:SetCommand( CommandDeactivateACLS ) end return self @@ -35367,46 +42811,97 @@ 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. +-- @param #number Delay (Optional) Delay in seconds before the ICLS is activated. -- @return #CONTROLLABLE self -function CONTROLLABLE:CommandActivateICLS(Channel, UnitID, Callsign, Delay) +function CONTROLLABLE:CommandActivateICLS( Channel, UnitID, Callsign, Delay ) -- Command to activate ICLS system. - local CommandActivateICLS= { + local CommandActivateICLS = { id = "ActivateICLS", - params= { + params = { ["type"] = BEACON.Type.ICLS, ["channel"] = Channel, - ["unitId"] = UnitID, + ["unitId"] = UnitID or self:GetID(), ["callsign"] = Callsign, - } + }, } - if Delay and Delay>0 then - SCHEDULER:New(nil, self.CommandActivateICLS, {self}, Delay) + if Delay and Delay > 0 then + SCHEDULER:New( nil, self.CommandActivateICLS, { self, Channel, UnitID, Callsign }, Delay ) else - self:SetCommand(CommandActivateICLS) + self:SetCommand( CommandActivateICLS ) end 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 (defaults to 336 MHz) +-- @param #number UnitID (Optional) The DCS UNIT ID of the unit the LINK4 system is attached to. Defaults to the UNIT itself. +-- @param #string Callsign (Optional) Morse code identification callsign. +-- @param #number Delay (Optional) Delay in seconds before the LINK4 is activated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandActivateLink4(Frequency, UnitID, Callsign, Delay) + + local freq = Frequency or 336 + + -- Command to activate Link4 system. + local CommandActivateLink4= { + id = "ActivateLink4", + params= { + ["frequency "] = freq*1000000, + ["unitId"] = UnitID or self:GetID(), + ["name"] = Callsign or "LNK", + } + } + + self:T({CommandActivateLink4}) + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandActivateLink4, {self, Frequency, UnitID, Callsign}, Delay) + else + self:SetCommand(CommandActivateLink4) + end + + return self +end --- Deactivate the active beacon of the CONTROLLABLE. -- @param #CONTROLLABLE self -- @param #number Delay (Optional) Delay in seconds before the beacon is deactivated. -- @return #CONTROLLABLE self -function CONTROLLABLE:CommandDeactivateBeacon(Delay) +function CONTROLLABLE:CommandDeactivateBeacon( Delay ) -- Command to deactivate + local CommandDeactivateBeacon = { id = 'DeactivateBeacon', params = {} } + 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) + self:SetCommand( CommandDeactivateBeacon ) + end + + 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 @@ -35416,15 +42911,15 @@ end -- @param #CONTROLLABLE self -- @param #number Delay (Optional) Delay in seconds before the ICLS is deactivated. -- @return #CONTROLLABLE self -function CONTROLLABLE:CommandDeactivateICLS(Delay) +function CONTROLLABLE:CommandDeactivateICLS( Delay ) -- Command to deactivate - local CommandDeactivateICLS={id='DeactivateICLS', params={}} + local CommandDeactivateICLS = { id = 'DeactivateICLS', params = {} } - if Delay and Delay>0 then - SCHEDULER:New(nil, self.CommandDeactivateICLS, {self}, Delay) + if Delay and Delay > 0 then + SCHEDULER:New( nil, self.CommandDeactivateICLS, { self }, Delay ) else - self:SetCommand(CommandDeactivateICLS) + self:SetCommand( CommandDeactivateICLS ) end return self @@ -35436,15 +42931,15 @@ end -- @param #number CallNumber The number value the group will be referred to as. Only valid numbers are 1-9. For example Uzi **5**-1. Default 1. -- @param #number Delay (Optional) Delay in seconds before the callsign is set. Default is immediately. -- @return #CONTROLLABLE self -function CONTROLLABLE:CommandSetCallsign(CallName, CallNumber, Delay) +function CONTROLLABLE:CommandSetCallsign( CallName, CallNumber, Delay ) -- Command to set the callsign. - local CommandSetCallsign={id='SetCallsign', params={callname=CallName, number=CallNumber or 1}} + local CommandSetCallsign = { id = 'SetCallsign', params = { callname = CallName, number = CallNumber or 1 } } - if Delay and Delay>0 then - SCHEDULER:New(nil, self.CommandSetCallsign, {self, CallName, CallNumber}, Delay) + if Delay and Delay > 0 then + SCHEDULER:New( nil, self.CommandSetCallsign, { self, CallName, CallNumber }, Delay ) else - self:SetCommand(CommandSetCallsign) + self:SetCommand( CommandSetCallsign ) end return self @@ -35455,26 +42950,26 @@ end -- @param #boolean SwitchOnOff If true (or nil) switch EPLRS on. If false switch off. -- @param #number Delay (Optional) Delay in seconds before the callsign is set. Default is immediately. -- @return #CONTROLLABLE self -function CONTROLLABLE:CommandEPLRS(SwitchOnOff, Delay) +function CONTROLLABLE:CommandEPLRS( SwitchOnOff, Delay ) - if SwitchOnOff==nil then - SwitchOnOff=true + if SwitchOnOff == nil then + SwitchOnOff = true end -- Command to set the callsign. - local CommandEPLRS={ - id='EPLRS', - params={ - value=SwitchOnOff, - groupId=self:GetID() - } + local CommandEPLRS = { + id = 'EPLRS', + params = { + value = SwitchOnOff, + groupId = self:GetID(), + }, } - if Delay and Delay>0 then - SCHEDULER:New(nil, self.CommandEPLRS, {self, SwitchOnOff}, Delay) + if Delay and Delay > 0 then + SCHEDULER:New( nil, self.CommandEPLRS, { self, SwitchOnOff }, Delay ) else - self:T(string.format("EPLRS=%s for controllable %s (id=%s)", tostring(SwitchOnOff), tostring(self:GetName()), tostring(self:GetID()))) - self:SetCommand(CommandEPLRS) + self:T( string.format( "EPLRS=%s for controllable %s (id=%s)", tostring( SwitchOnOff ), tostring( self:GetName() ), tostring( self:GetID() ) ) ) + self:SetCommand( CommandEPLRS ) end return self @@ -35484,52 +42979,50 @@ end -- @param #CONTROLLABLE self -- @param #number Frequency Radio frequency in MHz. -- @param #number Modulation Radio modulation. Default `radio.modulation.AM`. --- @param #number Delay (Optional) Delay in seconds before the frequncy is set. Default is immediately. +-- @param #number Delay (Optional) Delay in seconds before the frequency is set. Default is immediately. -- @return #CONTROLLABLE self -function CONTROLLABLE:CommandSetFrequency(Frequency, Modulation, Delay) +function CONTROLLABLE:CommandSetFrequency( Frequency, Modulation, Delay ) local CommandSetFrequency = { id = 'SetFrequency', params = { - frequency = Frequency*1000000, + frequency = Frequency * 1000000, modulation = Modulation or radio.modulation.AM, - } + }, } - if Delay and Delay>0 then - SCHEDULER:New(nil, self.CommandSetFrequency, {self, Frequency, Modulation}, Delay) + if Delay and Delay > 0 then + SCHEDULER:New( nil, self.CommandSetFrequency, { self, Frequency, Modulation }, Delay ) else - self:SetCommand(CommandSetFrequency) + self:SetCommand( CommandSetFrequency ) end return self end - --- Set EPLRS data link on/off. -- @param #CONTROLLABLE self -- @param #boolean SwitchOnOff If true (or nil) switch EPLRS on. If false switch off. -- @param #number idx Task index. Default 1. -- @return #table Task wrapped action. -function CONTROLLABLE:TaskEPLRS(SwitchOnOff, idx) +function CONTROLLABLE:TaskEPLRS( SwitchOnOff, idx ) - if SwitchOnOff==nil then - SwitchOnOff=true + if SwitchOnOff == nil then + SwitchOnOff = true end -- Command to set the callsign. - local CommandEPLRS={ - id='EPLRS', - params={ - value=SwitchOnOff, - groupId=self:GetID() - } + local CommandEPLRS = { + id = 'EPLRS', + params = { + value = SwitchOnOff, + groupId = self:GetID(), + }, } - return self:TaskWrappedAction(CommandEPLRS, idx or 1) + return self:TaskWrappedAction( CommandEPLRS, idx or 1 ) end - -- TASKS FOR AIR CONTROLLABLES --- (AIR) Attack a Controllable. @@ -35537,14 +43030,14 @@ end -- @param Wrapper.Group#GROUP AttackGroup The Group to be attacked. -- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. -- @param DCS#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. +-- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aircraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aircraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. -- @param DCS#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. -- @param DCS#Distance Altitude (optional) Desired attack start altitude. Controllable/aircraft will make its attacks from the altitude. If the altitude is too low or too high to use weapon aircraft/controllable will choose closest altitude to the desired attack start altitude. If the desired altitude is defined controllable/aircraft will not attack from safe altitude. -- @param #boolean AttackQtyLimit (optional) The flag determines how to interpret attackQty parameter. If the flag is true then attackQty is a limit on maximal attack quantity for "AttackGroup" and "AttackUnit" tasks. If the flag is false then attackQty is a desired attack quantity for "Bombing" and "BombingRunway" tasks. -- @param #boolean GroupAttack (Optional) If true, attack as group. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskAttackGroup( AttackGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit, GroupAttack ) - --self:F2( { self.ControllableName, AttackGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit } ) + -- self:F2( { self.ControllableName, AttackGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit } ) -- AttackGroup = { -- id = 'AttackGroup', @@ -35585,12 +43078,12 @@ end -- @param Wrapper.Unit#UNIT AttackUnit The UNIT to be attacked -- @param #boolean GroupAttack (Optional) If true, all units in the group will attack the Unit when found. Default false. -- @param DCS#AI.Task.WeaponExpend WeaponExpend (Optional) Determines how many weapons will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number AttackQty (Optional) Limits maximal quantity of attack. The aicraft/controllable will not make more attacks than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. +-- @param #number AttackQty (Optional) Limits maximal quantity of attack. The aircraft/controllable will not make more attacks than allowed even if the target controllable not destroyed and the aircraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. -- @param DCS#Azimuth Direction (Optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. -- @param #number Altitude (Optional) The (minimum) altitude in meters from where to attack. Default is altitude of unit to attack but at least 1000 m. -- @param #number WeaponType (optional) The WeaponType. See [DCS Enumerator Weapon Type](https://wiki.hoggitworld.com/view/DCS_enum_weapon_flag) on Hoggit. -- @return DCS#Task The DCS task structure. -function CONTROLLABLE:TaskAttackUnit(AttackUnit, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType) +function CONTROLLABLE:TaskAttackUnit( AttackUnit, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType ) local DCSTask = { id = 'AttackUnit', @@ -35605,19 +43098,18 @@ function CONTROLLABLE:TaskAttackUnit(AttackUnit, GroupAttack, WeaponExpend, Atta attackQtyLimit = AttackQty and true or false, attackQty = AttackQty, weaponType = WeaponType or 1073741822, - } + }, } return DCSTask end - --- (AIR) Delivering weapon at the point on the ground. -- @param #CONTROLLABLE self -- @param DCS#Vec2 Vec2 2D-coordinates of the point to deliver weapon at. -- @param #boolean GroupAttack (optional) If true, all units in the group will attack the Unit when found. -- @param DCS#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. +-- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aircraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aircraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. -- @param DCS#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. -- @param #number Altitude (optional) The altitude from where to attack. -- @param #number WeaponType (optional) The WeaponType. @@ -35652,7 +43144,7 @@ end -- @param DCS#Vec2 Vec2 2D-coordinates of the point to deliver weapon at. -- @param #boolean GroupAttack (Optional) If true, all units in the group will attack the Unit when found. -- @param DCS#AI.Task.WeaponExpend WeaponExpend (Optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit will choose expend on its own discretion. --- @param #number AttackQty (Optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. +-- @param #number AttackQty (Optional) This parameter limits maximal quantity of attack. The aircraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aircraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. -- @param DCS#Azimuth Direction (Optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. -- @param #number Altitude (Optional) The altitude [meters] from where to attack. Default 30 m. -- @param #number WeaponType (Optional) The WeaponType. Default Auto=1073741822. @@ -35680,19 +43172,18 @@ function CONTROLLABLE:TaskAttackMapObject( Vec2, GroupAttack, WeaponExpend, Atta return DCSTask end - --- (AIR) Delivering weapon via CarpetBombing (all bombers in formation release at same time) at the point on the ground. -- @param #CONTROLLABLE self -- @param DCS#Vec2 Vec2 2D-coordinates of the point to deliver weapon at. -- @param #boolean GroupAttack (optional) If true, all units in the group will attack the Unit when found. -- @param DCS#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit will choose expend on its own discretion. --- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. +-- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aircraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aircraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. -- @param DCS#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. -- @param #number Altitude (optional) The altitude from where to attack. -- @param #number WeaponType (optional) The WeaponType. -- @param #number CarpetLength (optional) default to 500 m. -- @return DCS#Task The DCS task structure. -function CONTROLLABLE:TaskCarpetBombing(Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType, CarpetLength) +function CONTROLLABLE:TaskCarpetBombing( Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType, CarpetLength ) -- Build Task Structure local DCSTask = { @@ -35711,14 +43202,12 @@ function CONTROLLABLE:TaskCarpetBombing(Vec2, GroupAttack, WeaponExpend, AttackQ direction = Direction and math.rad(Direction) or 0, altitudeEnabled = Altitude and true or false, altitude = Altitude, - } + }, } return DCSTask end - - --- (AIR) Following another airborne controllable. -- The unit / controllable will follow lead unit of another controllable, wingmens of both controllables will continue following their leaders. -- Used to support CarpetBombing Task @@ -35727,7 +43216,7 @@ end -- @param DCS#Vec3 Vec3 Position of the unit / lead unit of the controllable relative lead unit of another controllable in frame reference oriented by course of lead unit of another controllable. If another controllable is on land the unit / controllable will orbit around. -- @param #number LastWaypointIndex Detach waypoint of another controllable. Once reached the unit / controllable Follow task is finished. -- @return DCS#Task The DCS task structure. -function CONTROLLABLE:TaskFollowBigFormation(FollowControllable, Vec3, LastWaypointIndex ) +function CONTROLLABLE:TaskFollowBigFormation( FollowControllable, Vec3, LastWaypointIndex ) local DCSTask = { id = 'FollowBigFormation', @@ -35735,14 +43224,13 @@ function CONTROLLABLE:TaskFollowBigFormation(FollowControllable, Vec3, LastWaypo groupId = FollowControllable:GetID(), pos = Vec3, lastWptIndexFlag = LastWaypointIndex and true or false, - lastWptIndex = LastWaypointIndex - } + lastWptIndex = LastWaypointIndex, + }, } return DCSTask end - --- (AIR HELICOPTER) Move the controllable to a Vec2 Point, wait for a defined duration and embark infantry groups. -- @param #CONTROLLABLE self -- @param Core.Point#COORDINATE Coordinate The point where to pickup the troops. @@ -35750,29 +43238,29 @@ end -- @param #number Duration (Optional) The maximum duration in seconds to wait until all groups have embarked. -- @param #table Distribution (Optional) Distribution used to put the infantry groups into specific carrier units. -- @return DCS#Task The DCS task structure. -function CONTROLLABLE:TaskEmbarking(Coordinate, GroupSetForEmbarking, Duration, Distribution) +function CONTROLLABLE:TaskEmbarking( Coordinate, GroupSetForEmbarking, Duration, Distribution ) -- Table of group IDs for embarking. - local g4e={} + local g4e = {} if GroupSetForEmbarking then - for _,_group in pairs(GroupSetForEmbarking:GetSet()) do - local group=_group --Wrapper.Group#GROUP - table.insert(g4e, group:GetID()) + for _, _group in pairs( GroupSetForEmbarking:GetSet() ) do + local group = _group -- Wrapper.Group#GROUP + table.insert( g4e, group:GetID() ) end else - self:E("ERROR: No groups for embarking specified!") + self:E( "ERROR: No groups for embarking specified!" ) return nil end -- Table of group IDs for embarking. - --local Distribution={} + -- local Distribution={} -- Distribution - --local distribution={} - --distribution[id]=gids + -- local distribution={} + -- distribution[id]=gids - local groupID=self and self:GetID() + local groupID = self and self:GetID() local DCSTask = { id = 'Embarking', @@ -35785,13 +43273,12 @@ function CONTROLLABLE:TaskEmbarking(Coordinate, GroupSetForEmbarking, Duration, duration = Duration, distributionFlag = Distribution and true or false, distribution = Distribution, - } + }, } return DCSTask end - --- Used in conjunction with the embarking task for a transport helicopter group. The Ground units will move to the specified location and wait to be picked up by a helicopter. -- The helicopter will then fly them to their dropoff point defined by another task for the ground forces; DisembarkFromTransport task. -- The controllable has to be an infantry group! @@ -35800,7 +43287,7 @@ end -- @param #number Radius Radius in meters. Default 200 m. -- @param #string UnitType The unit type name of the carrier, e.g. "UH-1H". Must not be specified. -- @return DCS#Task Embark to transport task. -function CONTROLLABLE:TaskEmbarkToTransport(Coordinate, Radius, UnitType) +function CONTROLLABLE:TaskEmbarkToTransport( Coordinate, Radius, UnitType ) local EmbarkToTransport = { id = "EmbarkToTransport", @@ -35809,46 +43296,44 @@ function CONTROLLABLE:TaskEmbarkToTransport(Coordinate, Radius, UnitType) y = Coordinate.z, zoneRadius = Radius or 200, selectedType = UnitType, - } + }, } return EmbarkToTransport end - --- Specifies the location infantry groups that is being transported by helicopters will be unloaded at. Used in conjunction with the EmbarkToTransport task. -- @param #CONTROLLABLE self -- @param Core.Point#COORDINATE Coordinate Coordinates where AI is expecting to be picked up. -- @return DCS#Task Embark to transport task. -function CONTROLLABLE:TaskDisembarking(Coordinate, GroupSetToDisembark) +function CONTROLLABLE:TaskDisembarking( Coordinate, GroupSetToDisembark ) -- Table of group IDs for disembarking. - local g4e={} + local g4e = {} if GroupSetToDisembark then - for _,_group in pairs(GroupSetToDisembark:GetSet()) do - local group=_group --Wrapper.Group#GROUP - table.insert(g4e, group:GetID()) + for _, _group in pairs( GroupSetToDisembark:GetSet() ) do + local group = _group -- Wrapper.Group#GROUP + table.insert( g4e, group:GetID() ) end else - self:E("ERROR: No groups for disembarking specified!") + self:E( "ERROR: No groups for disembarking specified!" ) return nil end - local Disembarking={ - id = "Disembarking", - params = { - x = Coordinate.x, - y = Coordinate.z, - groupsForEmbarking = g4e, -- This is no bug, the entry is really "groupsForEmbarking" even if we disembark the troops. - } - } + local Disembarking = { + id = "Disembarking", + params = { + x = Coordinate.x, + y = Coordinate.z, + groupsForEmbarking = g4e, -- This is no bug, the entry is really "groupsForEmbarking" even if we disembark the troops. + }, + } return Disembarking end - ---- (AIR) Orbit at a specified position at a specified alititude during a specified duration with a specified speed. +--- (AIR) Orbit at a specified position at a specified altitude during a specified duration with a specified speed. -- @param #CONTROLLABLE self -- @param DCS#Vec2 Point The point to hold the position. -- @param #number Altitude The altitude AGL in meters to hold the position. @@ -35863,8 +43348,8 @@ function CONTROLLABLE:TaskOrbitCircleAtVec2( Point, Altitude, Speed ) pattern = AI.Task.OrbitPattern.CIRCLE, point = Point, speed = Speed, - altitude = Altitude + land.getHeight( Point ) - } + altitude = Altitude + land.getHeight( Point ), + }, } return DCSTask @@ -35877,15 +43362,15 @@ end -- @param #number Speed Speed [m/s] flying the orbit pattern. Default 128 m/s = 250 knots. -- @param Core.Point#COORDINATE CoordRaceTrack (Optional) If this coordinate is specified, the CONTROLLABLE will fly a race-track pattern using this and the initial coordinate. -- @return #CONTROLLABLE self -function CONTROLLABLE:TaskOrbit(Coord, Altitude, Speed, CoordRaceTrack) +function CONTROLLABLE:TaskOrbit( Coord, Altitude, Speed, CoordRaceTrack ) - local Pattern=AI.Task.OrbitPattern.CIRCLE + local Pattern = AI.Task.OrbitPattern.CIRCLE - local P1=Coord:GetVec2() - local P2=nil + local P1 = Coord:GetVec2() + local P2 = nil if CoordRaceTrack then - Pattern=AI.Task.OrbitPattern.RACE_TRACK - P2=CoordRaceTrack:GetVec2() + Pattern = AI.Task.OrbitPattern.RACE_TRACK + P2 = CoordRaceTrack:GetVec2() end local Task = { @@ -35896,13 +43381,13 @@ function CONTROLLABLE:TaskOrbit(Coord, Altitude, Speed, CoordRaceTrack) point2 = P2, speed = Speed or UTILS.KnotsToMps(250), altitude = Altitude or Coord.y, - } + }, } return Task end ---- (AIR) Orbit at the current position of the first unit of the controllable at a specified alititude. +--- (AIR) Orbit at the current position of the first unit of the controllable at a specified altitude. -- @param #CONTROLLABLE self -- @param #number Altitude The altitude [m] to hold the position. -- @param #number Speed The speed [m/s] flying when holding the position. @@ -35921,8 +43406,6 @@ function CONTROLLABLE:TaskOrbitCircle( Altitude, Speed, Coordinate ) return nil end - - --- (AIR) Hold position at the current position of the first unit of the controllable. -- @param #CONTROLLABLE self -- @param #number Duration The maximum duration in seconds to hold the position. @@ -35933,7 +43416,6 @@ function CONTROLLABLE:TaskHoldPosition() return self:TaskOrbitCircle( 30, 10 ) end - --- (AIR) Delivering weapon on the runway. See [hoggit](https://wiki.hoggitworld.com/view/DCS_task_bombingRunway) -- -- Make sure the aircraft has the following role: @@ -35953,7 +43435,7 @@ end -- @param DCS#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. -- @param #boolean GroupAttack (optional) Flag indicates that the target must be engaged by all aircrafts of the controllable. Has effect only if the task is assigned to a group and not to a single aircraft. -- @return DCS#Task The DCS task structure. -function CONTROLLABLE:TaskBombingRunway(Airbase, WeaponType, WeaponExpend, AttackQty, Direction, GroupAttack) +function CONTROLLABLE:TaskBombingRunway( Airbase, WeaponType, WeaponExpend, AttackQty, Direction, GroupAttack ) local DCSTask = { id = 'BombingRunway', @@ -35970,27 +43452,50 @@ function CONTROLLABLE:TaskBombingRunway(Airbase, WeaponType, WeaponExpend, Attac return DCSTask end - --- (AIR) Refueling from the nearest tanker. No parameters. -- @param #CONTROLLABLE self -- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskRefueling() - local DCSTask={ - id='Refueling', - params={} + local DCSTask = { + id = 'Refueling', + params = {}, } return DCSTask end +--- (AIR) Act as Recovery Tanker for a naval/carrier group. +-- @param #CONTROLLABLE self +-- @param Wrapper.Group#GROUP CarrierGroup +-- @param #number Speed Speed in meters per second +-- @param #number Altitude Altitude the tanker orbits at in meters +-- @param #number LastWptNumber (optional) Waypoint of carrier group that when reached, ends the recovery tanker task +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:TaskRecoveryTanker(CarrierGroup, Speed, Altitude, LastWptNumber) + + local LastWptFlag = type(LastWptNumber) == "number" and true or false + + local DCSTask = { + id = "RecoveryTanker", + params = { + groupId = CarrierGroup:GetID(), + speed = Speed, + altitude = Altitude, + lastWptIndexFlag = LastWptFlag, + lastWptIndex = LastWptNumber + } + } + + return DCSTask +end --- (AIR HELICOPTER) Landing at the ground. For helicopters only. -- @param #CONTROLLABLE self -- @param DCS#Vec2 Vec2 The point where to land. -- @param #number Duration The duration in seconds to stay on the ground. -- @return #CONTROLLABLE self -function CONTROLLABLE:TaskLandAtVec2(Vec2, Duration) +function CONTROLLABLE:TaskLandAtVec2( Vec2, Duration ) local DCSTask = { id = 'Land', @@ -36008,39 +43513,37 @@ end -- @param #CONTROLLABLE self -- @param Core.Zone#ZONE Zone The zone where to land. -- @param #number Duration The duration in seconds to stay on the ground. --- @return #CONTROLLABLE self +-- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskLandAtZone( Zone, Duration, RandomPoint ) -- Get landing point - local Point=RandomPoint and Zone:GetRandomVec2() or Zone:GetVec2() + local Point = RandomPoint and Zone:GetRandomVec2() or Zone:GetVec2() local DCSTask = CONTROLLABLE.TaskLandAtVec2( self, Point, Duration ) return DCSTask end - - --- (AIR) Following another airborne controllable. -- The unit / controllable will follow lead unit of another controllable, wingmens of both controllables will continue following their leaders. -- If another controllable is on land the unit / controllable will orbit around. -- @param #CONTROLLABLE self --- @param Wrapper.Controllable#CONTROLLABLE FollowControllable The controllable to be followed. +-- @param #CONTROLLABLE FollowControllable The controllable to be followed. -- @param DCS#Vec3 Vec3 Position of the unit / lead unit of the controllable relative lead unit of another controllable in frame reference oriented by course of lead unit of another controllable. If another controllable is on land the unit / controllable will orbit around. -- @param #number LastWaypointIndex Detach waypoint of another controllable. Once reached the unit / controllable Follow task is finished. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskFollow( FollowControllable, Vec3, LastWaypointIndex ) self:F2( { self.ControllableName, FollowControllable, Vec3, LastWaypointIndex } ) --- Follow = { --- id = 'Follow', --- params = { --- groupId = Group.ID, --- pos = Vec3, --- lastWptIndexFlag = boolean, --- lastWptIndex = number --- } --- } + -- Follow = { + -- id = 'Follow', + -- params = { + -- groupId = Group.ID, + -- pos = Vec3, + -- lastWptIndexFlag = boolean, + -- lastWptIndex = number + -- } + -- } local LastWaypointIndexFlag = false local lastWptIndexFlagChangedManually = false @@ -36057,19 +43560,18 @@ function CONTROLLABLE:TaskFollow( FollowControllable, Vec3, LastWaypointIndex ) lastWptIndexFlag = LastWaypointIndexFlag, lastWptIndex = LastWaypointIndex, lastWptIndexFlagChangedManually = lastWptIndexFlagChangedManually, - } + }, } self:T3( { DCSTask } ) return DCSTask end - --- (AIR) Escort another airborne controllable. -- The unit / controllable will follow lead unit of another controllable, wingmens of both controllables will continue following their leaders. -- The unit / controllable will also protect that controllable from threats of specified types. -- @param #CONTROLLABLE self --- @param Wrapper.Controllable#CONTROLLABLE FollowControllable The controllable to be escorted. +-- @param #CONTROLLABLE FollowControllable The controllable to be escorted. -- @param DCS#Vec3 Vec3 Position of the unit / lead unit of the controllable relative lead unit of another controllable in frame reference oriented by course of lead unit of another controllable. If another controllable is on land the unit / controllable will orbit around. -- @param #number LastWaypointIndex Detach waypoint of another controllable. Once reached the unit / controllable Follow task is finished. -- @param #number EngagementDistance Maximal distance from escorted controllable to threat. If the threat is already engaged by escort escort will disengage if the distance becomes greater than 1.5 * engagementDistMax. @@ -36077,23 +43579,23 @@ end -- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskEscort( FollowControllable, Vec3, LastWaypointIndex, EngagementDistance, TargetTypes ) --- Escort = { --- id = 'Escort', --- params = { --- groupId = Group.ID, --- pos = Vec3, --- lastWptIndexFlag = boolean, --- lastWptIndex = number, --- engagementDistMax = Distance, --- targetTypes = array of AttributeName, --- } --- } + -- Escort = { + -- id = 'Escort', + -- params = { + -- groupId = Group.ID, + -- pos = Vec3, + -- lastWptIndexFlag = boolean, + -- lastWptIndex = number, + -- engagementDistMax = Distance, + -- targetTypes = array of AttributeName, + -- } + -- } local DCSTask DCSTask = { id = 'Escort', params = { - groupId = FollowControllable:GetID(), + groupId = FollowControllable and FollowControllable:GetID() or nil, pos = Vec3, lastWptIndexFlag = LastWaypointIndex and true or false, lastWptIndex = LastWaypointIndex, @@ -36105,7 +43607,6 @@ function CONTROLLABLE:TaskEscort( FollowControllable, Vec3, LastWaypointIndex, E return DCSTask end - -- GROUND TASKS --- (GROUND) Fire at a VEC2 point until ammunition is finished. @@ -36123,14 +43624,14 @@ 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 - } + alt_type = ASL and 0 or 1, + }, } if AmmoCount then @@ -36139,14 +43640,15 @@ function CONTROLLABLE:TaskFireAtPoint( Vec2, Radius, AmmoCount, WeaponType, Alti end if Altitude then - DCSTask.params.altitude=Altitude + DCSTask.params.altitude = Altitude end if WeaponType then - DCSTask.params.weaponType=WeaponType + DCSTask.params.weaponType = WeaponType end - --self:I(DCSTask) + --env.info("FF fireatpoint") + --BASE:I(DCSTask) return DCSTask end @@ -36155,11 +43657,10 @@ end -- @param #CONTROLLABLE self -- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskHold() - local DCSTask = {id = 'Hold', params = {}} + local DCSTask = { id = 'Hold', params = {} } return DCSTask end - -- TASKS FOR AIRBORNE AND GROUND UNITS/CONTROLLABLES --- (AIR + GROUND) The task makes the controllable/unit a FAC and orders the FAC to control the target (enemy ground controllable) destruction. @@ -36189,7 +43690,7 @@ function CONTROLLABLE:TaskFAC_AttackGroup( AttackGroup, WeaponType, Designation, modulation = Modulation or radio.modulation.AM, callname = CallsignName, number = CallsignNumber, - } + }, } return DCSTask @@ -36212,14 +43713,12 @@ function CONTROLLABLE:EnRouteTaskEngageTargets( Distance, TargetTypes, Priority maxDist = Distance, targetTypes = TargetTypes or {"Air"}, priority = Priority or 0, - } + }, } return DCSTask end - - --- (AIR) Engaging a targets of defined types at circle-shaped zone. -- @param #CONTROLLABLE self -- @param DCS#Vec2 Vec2 2D-coordinates of the zone. @@ -36236,20 +43735,61 @@ function CONTROLLABLE:EnRouteTaskEngageTargetsInZone( Vec2, Radius, TargetTypes, zoneRadius = Radius, targetTypes = TargetTypes or {"Air"}, priority = Priority or 0 + }, + } + + 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) Enroute SEAD task. +-- @param #CONTROLLABLE self +-- @param DCS#AttributeNameArray TargetTypes Array of target categories allowed to engage. Default `{"Air Defence"}`. +-- @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:EnRouteTaskSEAD(TargetTypes, Priority) + + local DCSTask = { + id = 'EngageTargets', + key = "SEAD", + --auto = false, + --enabled = true, + params = { + targetTypes = TargetTypes or {"Air Defence"}, + 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 --- @param Wrapper.Controllable#CONTROLLABLE AttackGroup The Controllable to be attacked. +-- @param #CONTROLLABLE AttackGroup The Controllable to be attacked. -- @param #number Priority 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. -- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. -- @param DCS#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. +-- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aircraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aircraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. -- @param DCS#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. -- @param DCS#Distance Altitude (optional) Desired attack start altitude. Controllable/aircraft will make its attacks from the altitude. If the altitude is too low or too high to use weapon aircraft/controllable will choose closest altitude to the desired attack start altitude. If the desired altitude is defined controllable/aircraft will not attack from safe altitude. -- @param #boolean AttackQtyLimit (optional) The flag determines how to interpret attackQty parameter. If the flag is true then attackQty is a limit on maximal attack quantity for "AttackGroup" and "AttackUnit" tasks. If the flag is false then attackQty is a desired attack quantity for "Bombing" and "BombingRunway" tasks. @@ -36291,14 +43831,13 @@ function CONTROLLABLE:EnRouteTaskEngageGroup( AttackGroup, Priority, WeaponType, return DCSTask end - --- (AIR) Search and attack the Unit. -- @param #CONTROLLABLE self -- @param Wrapper.Unit#UNIT EngageUnit The UNIT. -- @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. -- @param #boolean GroupAttack (optional) If true, all units in the group will attack the Unit when found. -- @param DCS#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. +-- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aircraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aircraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. -- @param DCS#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. -- @param DCS#Distance Altitude (optional) Desired altitude to perform the unit engagement. -- @param #boolean Visible (optional) Unit must be visible. @@ -36327,12 +43866,10 @@ function CONTROLLABLE:EnRouteTaskEngageUnit( EngageUnit, Priority, GroupAttack, return DCSTask end - - --- (AIR) Aircraft will act as an AWACS for friendly units (will provide them with information about contacts). No parameters. -- @param #CONTROLLABLE self -- @return DCS#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskAWACS( ) +function CONTROLLABLE:EnRouteTaskAWACS() local DCSTask = { id = 'AWACS', @@ -36342,11 +43879,10 @@ function CONTROLLABLE:EnRouteTaskAWACS( ) return DCSTask end - --- (AIR) Aircraft will act as a tanker for friendly units. No parameters. -- @param #CONTROLLABLE self -- @return DCS#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskTanker( ) +function CONTROLLABLE:EnRouteTaskTanker() local DCSTask = { id = 'Tanker', @@ -36356,13 +43892,12 @@ function CONTROLLABLE:EnRouteTaskTanker( ) return DCSTask end - -- En-route tasks for ground units/controllables --- (GROUND) Ground unit (EW-radar) will act as an EWR for friendly units (will provide them with information about contacts). No parameters. -- @param #CONTROLLABLE self -- @return DCS#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskEWR( ) +function CONTROLLABLE:EnRouteTaskEWR() local DCSTask = { id = 'EWR', @@ -36372,14 +43907,13 @@ function CONTROLLABLE:EnRouteTaskEWR( ) return DCSTask end - -- En-route tasks for airborne and ground units/controllables --- (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose the target (enemy ground controllable) as well as other assigned targets. -- The killer is player-controlled allied CAS-aircraft that is in contact with the FAC. -- If the task is assigned to the controllable lead unit will be a FAC. -- @param #CONTROLLABLE self --- @param Wrapper.Controllable#CONTROLLABLE AttackGroup Target CONTROLLABLE. +-- @param #CONTROLLABLE AttackGroup Target CONTROLLABLE. -- @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 is 0. -- @param #number WeaponType (Optional) Bitmask of weapon types those allowed to use. Default is "Auto". -- @param DCS#AI.Task.Designation Designation (Optional) Designation type. @@ -36395,13 +43929,12 @@ function CONTROLLABLE:EnRouteTaskFAC_EngageGroup( AttackGroup, Priority, WeaponT designation = Designation, datalink = Datalink and Datalink or false, priority = Priority or 0, - } + }, } return DCSTask end - --- (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose a targets (enemy ground controllable) around as well as other assigned targets. -- The killer is player-controlled allied CAS-aircraft that is in contact with the FAC. -- If the task is assigned to the controllable lead unit will be a FAC. @@ -36411,13 +43944,13 @@ end -- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskFAC( Radius, Priority ) --- FAC = { --- id = 'FAC', --- params = { --- radius = Distance, --- priority = number --- } --- } + -- FAC = { + -- id = 'FAC', + -- params = { + -- radius = Distance, + -- priority = number + -- } + -- } local DCSTask = { id = 'FAC', @@ -36430,7 +43963,6 @@ function CONTROLLABLE:EnRouteTaskFAC( Radius, Priority ) return DCSTask end - --- This creates a Task element, with an action to call a function as part of a Wrapped Task. -- This Task can then be embedded at a Waypoint by calling the method @{#CONTROLLABLE.SetTaskWaypoint}. -- @param #CONTROLLABLE self @@ -36483,24 +44015,22 @@ function CONTROLLABLE:TaskFunction( FunctionString, ... ) -- Script local DCSScript = {} - DCSScript[#DCSScript+1] = "local MissionControllable = GROUP:Find( ... ) " + DCSScript[#DCSScript + 1] = "local MissionControllable = GROUP:Find( ... ) " if arg and arg.n > 0 then - local ArgumentKey = '_' .. tostring( arg ):match("table: (.*)") + local ArgumentKey = '_' .. tostring( arg ):match( "table: (.*)" ) self:SetState( self, ArgumentKey, arg ) - DCSScript[#DCSScript+1] = "local Arguments = MissionControllable:GetState( MissionControllable, '" .. ArgumentKey .. "' ) " - DCSScript[#DCSScript+1] = FunctionString .. "( MissionControllable, unpack( Arguments ) )" + DCSScript[#DCSScript + 1] = "local Arguments = MissionControllable:GetState( MissionControllable, '" .. ArgumentKey .. "' ) " + DCSScript[#DCSScript + 1] = FunctionString .. "( MissionControllable, unpack( Arguments ) )" else - DCSScript[#DCSScript+1] = FunctionString .. "( MissionControllable )" + DCSScript[#DCSScript + 1] = FunctionString .. "( MissionControllable )" end -- DCS task. - local DCSTask = self:TaskWrappedAction(self:CommandDoScript(table.concat( DCSScript ))) + local DCSTask = self:TaskWrappedAction( self:CommandDoScript( table.concat( DCSScript ) ) ) return DCSTask end - - --- (AIR + GROUND) Return a mission task from a mission template. -- @param #CONTROLLABLE self -- @param #table TaskMission A table containing the mission task. @@ -36509,16 +44039,17 @@ function CONTROLLABLE:TaskMission( TaskMission ) local DCSTask = { id = 'Mission', - params = { TaskMission, }, + params = { + TaskMission, + }, } return DCSTask end - do -- Patrol methods - --- (GROUND) Patrol iteratively using the waypoints the for the (parent) group. + --- (GROUND) Patrol iteratively using the waypoints of the (parent) group. -- @param #CONTROLLABLE self -- @return #CONTROLLABLE function CONTROLLABLE:PatrolRoute() @@ -36537,32 +44068,32 @@ do -- Patrol methods -- Calculate the new Route. local FromCoord = PatrolGroup:GetCoordinate() - + -- test for submarine local depth = 0 local IsSub = false if PatrolGroup:IsShip() then local navalvec3 = FromCoord:GetVec3() - if navalvec3.y < 0 then + if navalvec3.y < 0 then depth = navalvec3.y IsSub = true end end - - + + local Waypoint = Waypoints[1] local Speed = Waypoint.speed or (20 / 3.6) local From = FromCoord:WaypointGround( Speed ) - - if IsSub then + + if IsSub then From = FromCoord:WaypointNaval( Speed, Waypoint.alt ) end - + table.insert( Waypoints, 1, From ) local TaskRoute = PatrolGroup:TaskFunction( "CONTROLLABLE.PatrolRoute" ) - self:F({Waypoints = Waypoints}) + self:F( { Waypoints = Waypoints } ) local Waypoint = Waypoints[#Waypoints] PatrolGroup:SetTaskWaypoint( Waypoint, TaskRoute ) -- Set for the given Route at Waypoint 2 the TaskRouteToZone. @@ -36602,31 +44133,31 @@ do -- Patrol methods local IsSub = false if PatrolGroup:IsShip() then local navalvec3 = FromCoord:GetVec3() - if navalvec3.y < 0 then + if navalvec3.y < 0 then depth = navalvec3.y IsSub = true end end -- Loop until a waypoint has been found that is not the same as the current waypoint. - -- Otherwise the object zon't move or drive in circles and the algorithm would not do exactly + -- Otherwise the object won't move, or drive in circles, and the algorithm would not do exactly -- what it is supposed to do, which is making groups drive around. local ToWaypoint repeat -- Select a random waypoint and check if it is not the same waypoint as where the object is about. ToWaypoint = math.random( 1, #Waypoints ) - until( ToWaypoint ~= FromWaypoint ) + until (ToWaypoint ~= FromWaypoint) self:F( { FromWaypoint = FromWaypoint, ToWaypoint = ToWaypoint } ) - local Waypoint = Waypoints[ToWaypoint] -- Select random waypoint. + local Waypoint = Waypoints[ToWaypoint] -- Select random waypoint. local ToCoord = COORDINATE:NewFromVec2( { x = Waypoint.x, y = Waypoint.y } ) -- Create a "ground route point", which is a "point" structure that can be given as a parameter to a Task local Route = {} if IsSub then - Route[#Route+1] = FromCoord:WaypointNaval( Speed, depth ) - Route[#Route+1] = ToCoord:WaypointNaval( Speed, Waypoint.alt ) + Route[#Route + 1] = FromCoord:WaypointNaval( Speed, depth ) + Route[#Route + 1] = ToCoord:WaypointNaval( Speed, Waypoint.alt ) else - Route[#Route+1] = FromCoord:WaypointGround( Speed, Formation ) - Route[#Route+1] = ToCoord:WaypointGround( Speed, Formation ) + Route[#Route + 1] = FromCoord:WaypointGround( Speed, Formation ) + Route[#Route + 1] = ToCoord:WaypointGround( Speed, Formation ) end local TaskRouteToZone = PatrolGroup:TaskFunction( "CONTROLLABLE.PatrolRouteRandom", Speed, Formation, ToWaypoint ) @@ -36658,43 +44189,43 @@ do -- Patrol methods PatrolGroup = self:GetGroup() -- Wrapper.Group#GROUP end - DelayMin=DelayMin or 1 - if not DelayMax or DelayMax LengthDirect*10) or (LengthRoad/LengthOnRoad*100<5)) + LongRoad = LengthOnRoad and ((LengthOnRoad > LengthDirect * 10) or (LengthRoad / LengthOnRoad * 100 < 5)) -- Debug info. - self:T(string.format("Length on road = %.3f km", LengthOnRoad/1000)) - self:T(string.format("Length directly = %.3f km", LengthDirect/1000)) - self:T(string.format("Length fraction = %.3f km", LengthOnRoad/LengthDirect)) - self:T(string.format("Length only road = %.3f km", LengthRoad/1000)) - self:T(string.format("Length off road = %.3f km", LengthOffRoad/1000)) - self:T(string.format("Percent on road = %.1f", LengthRoad/LengthOnRoad*100)) + self:T( string.format( "Length on road = %.3f km", LengthOnRoad / 1000 ) ) + self:T( string.format( "Length directly = %.3f km", LengthDirect / 1000 ) ) + self:T( string.format( "Length fraction = %.3f km", LengthOnRoad / LengthDirect ) ) + self:T( string.format( "Length only road = %.3f km", LengthRoad / 1000 ) ) + self:T( string.format( "Length off road = %.3f km", LengthOffRoad / 1000 ) ) + self:T( string.format( "Percent on road = %.1f", LengthRoad / LengthOnRoad * 100 ) ) end -- Route, ground waypoints along road. - local route={} - local canroad=false + local route = {} + local canroad = false -- Check if a valid path on road could be found. if GotPath and LengthRoad and LengthDirect > 2000 then -- if the length of the movement is less than 1 km, drive directly. @@ -37043,43 +44563,43 @@ do -- Route methods -- Road is long ==> we take the short cut. - table.insert(route, FromCoordinate:WaypointGround(Speed, OffRoadFormation)) - table.insert(route, ToCoordinate:WaypointGround(Speed, OffRoadFormation)) + table.insert( route, FromCoordinate:WaypointGround( Speed, OffRoadFormation ) ) + table.insert( route, ToCoordinate:WaypointGround( Speed, OffRoadFormation ) ) else -- Create waypoints. - table.insert(route, FromCoordinate:WaypointGround(Speed, OffRoadFormation)) - table.insert(route, PathOnRoad[2]:WaypointGround(Speed, "On Road")) - table.insert(route, PathOnRoad[#PathOnRoad-1]:WaypointGround(Speed, "On Road")) + table.insert( route, FromCoordinate:WaypointGround( Speed, OffRoadFormation ) ) + table.insert( route, PathOnRoad[2]:WaypointGround( Speed, "On Road" ) ) + table.insert( route, PathOnRoad[#PathOnRoad - 1]:WaypointGround( Speed, "On Road" ) ) -- Add the final coordinate because the final might not be on the road. - local dist=ToCoordinate:Get2DDistance(PathOnRoad[#PathOnRoad-1]) - if dist>10 then - table.insert(route, ToCoordinate:WaypointGround(Speed, OffRoadFormation)) - table.insert(route, ToCoordinate:GetRandomCoordinateInRadius(10,5):WaypointGround(5, OffRoadFormation)) - table.insert(route, ToCoordinate:GetRandomCoordinateInRadius(10,5):WaypointGround(5, OffRoadFormation)) + local dist = ToCoordinate:Get2DDistance( PathOnRoad[#PathOnRoad - 1] ) + if dist > 10 then + table.insert( route, ToCoordinate:WaypointGround( Speed, OffRoadFormation ) ) + table.insert( route, ToCoordinate:GetRandomCoordinateInRadius( 10, 5 ):WaypointGround( 5, OffRoadFormation ) ) + table.insert( route, ToCoordinate:GetRandomCoordinateInRadius( 10, 5 ):WaypointGround( 5, OffRoadFormation ) ) end end - canroad=true + canroad = true else -- No path on road could be found (can happen!) ==> Route group directly from A to B. - table.insert(route, FromCoordinate:WaypointGround(Speed, OffRoadFormation)) - table.insert(route, ToCoordinate:WaypointGround(Speed, OffRoadFormation)) + table.insert( route, FromCoordinate:WaypointGround( Speed, OffRoadFormation ) ) + table.insert( route, ToCoordinate:WaypointGround( Speed, OffRoadFormation ) ) end -- Add passing waypoint function. if WaypointFunction then - local N=#route - for n,waypoint in pairs(route) do + local N = #route + for n, waypoint in pairs( route ) do waypoint.task = {} waypoint.task.id = "ComboTask" waypoint.task.params = {} - waypoint.task.params.tasks = {self:TaskFunction("CONTROLLABLE.___PassingWaypoint", n, N, WaypointFunction, unpack(WaypointFunctionArguments or {}))} + waypoint.task.params.tasks = { self:TaskFunction( "CONTROLLABLE.___PassingWaypoint", n, N, WaypointFunction, unpack( WaypointFunctionArguments or {} ) ) } end end @@ -37090,43 +44610,43 @@ do -- Route methods -- @param #CONTROLLABLE self -- @param Core.Point#COORDINATE ToCoordinate A Coordinate to drive to. -- @param #number Speed (Optional) Speed in km/h. The default speed is 20 km/h. - -- @param #function WaypointFunction (Optional) Function called when passing a waypoint. First parameters of the function are the @{CONTROLLABLE} object, the number of the waypoint and the total number of waypoints. + -- @param #function WaypointFunction (Optional) Function called when passing a waypoint. First parameters of the function are the @{#CONTROLLABLE} object, the number of the waypoint and the total number of waypoints. -- @param #table WaypointFunctionArguments (Optional) List of parameters passed to the *WaypointFunction*. -- @return Task - function CONTROLLABLE:TaskGroundOnRailRoads(ToCoordinate, Speed, WaypointFunction, WaypointFunctionArguments ) - self:F2({ToCoordinate=ToCoordinate, Speed=Speed}) + function CONTROLLABLE:TaskGroundOnRailRoads( ToCoordinate, Speed, WaypointFunction, WaypointFunctionArguments ) + self:F2( { ToCoordinate = ToCoordinate, Speed = Speed } ) -- Defaults. - Speed=Speed or 20 + Speed = Speed or 20 -- Current coordinate. local FromCoordinate = self:GetCoordinate() -- Get path and path length on railroad. - local PathOnRail, LengthOnRail=FromCoordinate:GetPathOnRoad(ToCoordinate, false, true) + local PathOnRail, LengthOnRail = FromCoordinate:GetPathOnRoad( ToCoordinate, false, true ) -- Debug info. - self:T(string.format("Length on railroad = %.3f km", LengthOnRail/1000)) + self:T( string.format( "Length on railroad = %.3f km", LengthOnRail / 1000 ) ) -- Route, ground waypoints along road. - local route={} + local route = {} -- Check if a valid path on railroad could be found. if PathOnRail then - table.insert(route, PathOnRail[1]:WaypointGround(Speed, "On Railroad")) - table.insert(route, PathOnRail[2]:WaypointGround(Speed, "On Railroad")) + table.insert( route, PathOnRail[1]:WaypointGround( Speed, "On Railroad" ) ) + table.insert( route, PathOnRail[2]:WaypointGround( Speed, "On Railroad" ) ) end -- Add passing waypoint function. if WaypointFunction then - local N=#route - for n,waypoint in pairs(route) do + local N = #route + for n, waypoint in pairs( route ) do waypoint.task = {} waypoint.task.id = "ComboTask" waypoint.task.params = {} - waypoint.task.params.tasks = {self:TaskFunction("CONTROLLABLE.___PassingWaypoint", n, N, WaypointFunction, unpack(WaypointFunctionArguments or {}))} + waypoint.task.params.tasks = { self:TaskFunction( "CONTROLLABLE.___PassingWaypoint", n, N, WaypointFunction, unpack( WaypointFunctionArguments or {} ) ) } end end @@ -37138,11 +44658,10 @@ do -- Route methods -- @param #number n Current waypoint number passed. -- @param #number N Total number of waypoints. -- @param #function waypointfunction Function called when a waypoint is passed. - function CONTROLLABLE.___PassingWaypoint(controllable, n, N, waypointfunction, ...) - waypointfunction(controllable, n, N, ...) + function CONTROLLABLE.___PassingWaypoint( controllable, n, N, waypointfunction, ... ) + waypointfunction( controllable, n, N, ... ) end - --- Make the AIR Controllable fly towards a specific point. -- @param #CONTROLLABLE self -- @param Core.Point#COORDINATE ToCoordinate A Coordinate to drive to. @@ -37164,7 +44683,6 @@ do -- Route methods return self end - --- (AIR + GROUND) Route the controllable to a given zone. -- The controllable final destination point can be randomized. -- A speed can be given in km/h. @@ -37190,7 +44708,6 @@ do -- Route methods PointFrom.action = Formation or "Cone" PointFrom.speed = 20 / 3.6 - local PointTo = {} local ZonePoint @@ -37250,7 +44767,6 @@ do -- Route methods PointFrom.action = Formation or "Cone" PointFrom.speed = 20 / 3.6 - local PointTo = {} PointTo.x = Vec2.x @@ -37302,7 +44818,6 @@ function CONTROLLABLE:CommandDoScript( DoScript ) return DCSDoScript end - --- Return the mission template of the controllable. -- @param #CONTROLLABLE self -- @return #table The MissionTemplate @@ -37322,8 +44837,6 @@ function CONTROLLABLE:GetTaskRoute() return routines.utils.deepCopy( _DATABASE.Templates.Controllables[self.ControllableName].Template.route.points ) end - - --- Return the route of a controllable by using the @{Core.Database#DATABASE} class. -- @param #CONTROLLABLE self -- @param #number Begin The route point from where the copy will start. The base route point is 0. @@ -37357,7 +44870,7 @@ function CONTROLLABLE:CopyRoute( Begin, End, Randomize, Radius ) for TPointID = Begin + 1, #Template.route.points - End do if Template.route.points[TPointID] then - Points[#Points+1] = routines.utils.deepCopy( Template.route.points[TPointID] ) + Points[#Points + 1] = routines.utils.deepCopy( Template.route.points[TPointID] ) if Randomize then if not Radius then Radius = 500 @@ -37375,11 +44888,10 @@ function CONTROLLABLE:CopyRoute( Begin, End, Randomize, Radius ) return nil end - --- Return the detected targets of the controllable. -- The optional parametes specify the detection methods that can be applied. -- If no detection method is given, the detection will use all the available methods by default. --- @param Wrapper.Controllable#CONTROLLABLE self +-- @param #CONTROLLABLE self -- @param #boolean DetectVisual (optional) -- @param #boolean DetectOptical (optional) -- @param #boolean DetectRadar (optional) @@ -37394,35 +44906,33 @@ function CONTROLLABLE:GetDetectedTargets( DetectVisual, DetectOptical, DetectRad if DCSControllable then - local DetectionVisual = ( DetectVisual and DetectVisual == true ) and Controller.Detection.VISUAL or nil - local DetectionOptical = ( DetectOptical and DetectOptical == true ) and Controller.Detection.OPTICAL or nil - local DetectionRadar = ( DetectRadar and DetectRadar == true ) and Controller.Detection.RADAR or nil - local DetectionIRST = ( DetectIRST and DetectIRST == true ) and Controller.Detection.IRST or nil - local DetectionRWR = ( DetectRWR and DetectRWR == true ) and Controller.Detection.RWR or nil - local DetectionDLINK = ( DetectDLINK and DetectDLINK == true ) and Controller.Detection.DLINK or nil - + local DetectionVisual = (DetectVisual and DetectVisual == true) and Controller.Detection.VISUAL or nil + local DetectionOptical = (DetectOptical and DetectOptical == true) and Controller.Detection.OPTICAL or nil + local DetectionRadar = (DetectRadar and DetectRadar == true) and Controller.Detection.RADAR or nil + local DetectionIRST = (DetectIRST and DetectIRST == true) and Controller.Detection.IRST or nil + local DetectionRWR = (DetectRWR and DetectRWR == true) and Controller.Detection.RWR or nil + local DetectionDLINK = (DetectDLINK and DetectDLINK == true) and Controller.Detection.DLINK or nil local Params = {} if DetectionVisual then - Params[#Params+1] = DetectionVisual + Params[#Params + 1] = DetectionVisual end if DetectionOptical then - Params[#Params+1] = DetectionOptical + Params[#Params + 1] = DetectionOptical end if DetectionRadar then - Params[#Params+1] = DetectionRadar + Params[#Params + 1] = DetectionRadar end if DetectionIRST then - Params[#Params+1] = DetectionIRST + Params[#Params + 1] = DetectionIRST end if DetectionRWR then - Params[#Params+1] = DetectionRWR + Params[#Params + 1] = DetectionRWR end if DetectionDLINK then - Params[#Params+1] = DetectionDLINK + Params[#Params + 1] = DetectionDLINK end - self:T2( { DetectionVisual, DetectionOptical, DetectionRadar, DetectionIRST, DetectionRWR, DetectionDLINK } ) return self:_GetController():getDetectedTargets( Params[1], Params[2], Params[3], Params[4], Params[5], Params[6] ) @@ -37435,9 +44945,9 @@ end -- The optional parametes specify the detection methods that can be applied. -- If **no** detection method is given, the detection will use **all** the available methods by default. -- If **at least one** detection method is specified, only the methods set to *true* will be used. --- @param Wrapper.Controllable#CONTROLLABLE self +-- @param #CONTROLLABLE self -- @param DCS#Object DCSObject The DCS object that is checked. --- @param Wrapper.Controllable#CONTROLLABLE self +-- @param #CONTROLLABLE self -- @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. @@ -37458,12 +44968,12 @@ function CONTROLLABLE:IsTargetDetected( DCSObject, DetectVisual, DetectOptical, if DCSControllable then - local DetectionVisual = ( DetectVisual and DetectVisual == true ) and Controller.Detection.VISUAL or nil - local DetectionOptical = ( DetectOptical and DetectOptical == true ) and Controller.Detection.OPTICAL or nil - local DetectionRadar = ( DetectRadar and DetectRadar == true ) and Controller.Detection.RADAR or nil - local DetectionIRST = ( DetectIRST and DetectIRST == true ) and Controller.Detection.IRST or nil - local DetectionRWR = ( DetectRWR and DetectRWR == true ) and Controller.Detection.RWR or nil - local DetectionDLINK = ( DetectDLINK and DetectDLINK == true ) and Controller.Detection.DLINK or nil + local DetectionVisual = (DetectVisual and DetectVisual == true) and Controller.Detection.VISUAL or nil + local DetectionOptical = (DetectOptical and DetectOptical == true) and Controller.Detection.OPTICAL or nil + local DetectionRadar = (DetectRadar and DetectRadar == true) and Controller.Detection.RADAR or nil + local DetectionIRST = (DetectIRST and DetectIRST == true) and Controller.Detection.IRST or nil + local DetectionRWR = (DetectRWR and DetectRWR == true) and Controller.Detection.RWR or nil + local DetectionDLINK = (DetectDLINK and DetectDLINK == true) and Controller.Detection.DLINK or nil local Controller = self:_GetController() @@ -37499,7 +45009,7 @@ function CONTROLLABLE:IsUnitDetected( Unit, DetectVisual, DetectOptical, DetectR self:F2( self.ControllableName ) if Unit and Unit:IsAlive() then - return self:IsTargetDetected(Unit:GetDCSObject(), DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) + return self:IsTargetDetected( Unit:GetDCSObject(), DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK ) end return nil @@ -37522,11 +45032,11 @@ function CONTROLLABLE:IsGroupDetected( Group, DetectVisual, DetectOptical, Detec self:F2( self.ControllableName ) if Group and Group:IsAlive() then - for _,_unit in pairs(Group:GetUnits()) do - local unit=_unit --Wrapper.Unit#UNIT + for _, _unit in pairs( Group:GetUnits() ) do + local unit = _unit -- Wrapper.Unit#UNIT if unit and unit:IsAlive() then - local isdetected=self:IsUnitDetected(unit, DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) + local isdetected = self:IsUnitDetected( unit, DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK ) if isdetected then return true @@ -37539,12 +45049,11 @@ function CONTROLLABLE:IsGroupDetected( Group, DetectVisual, DetectOptical, Detec return nil end - --- Return the detected targets of the controllable. -- The optional parametes specify the detection methods that can be applied. -- If **no** detection method is given, the detection will use **all** the available methods by default. -- If **at least one** detection method is specified, only the methods set to *true* will be used. --- @param Wrapper.Controllable#CONTROLLABLE self +-- @param #CONTROLLABLE self -- @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. @@ -37552,23 +45061,23 @@ end -- @param #boolean DetectRWR (Optional) If *false*, do not include targets detected by RWR. -- @param #boolean DetectDLINK (Optional) If *false*, do not include targets detected by data link. -- @return Core.Set#SET_UNIT Set of detected units. -function CONTROLLABLE:GetDetectedUnitSet(DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) +function CONTROLLABLE:GetDetectedUnitSet( DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK ) -- Get detected DCS units. - local detectedtargets=self:GetDetectedTargets(DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) + local detectedtargets = self:GetDetectedTargets( DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK ) - local unitset=SET_UNIT:New() + local unitset = SET_UNIT:New() - for DetectionObjectID, Detection in pairs(detectedtargets or {}) do - local DetectedObject=Detection.object -- DCS#Object + for DetectionObjectID, Detection in pairs( detectedtargets or {} ) do + local DetectedObject = Detection.object -- DCS#Object - if DetectedObject and DetectedObject:isExist() and DetectedObject.id_<50000000 then - local unit=UNIT:Find(DetectedObject) + if DetectedObject and DetectedObject:isExist() and DetectedObject.id_ < 50000000 then + local unit = UNIT:Find( DetectedObject ) if unit and unit:IsAlive() then - if not unitset:FindUnit(unit:GetName()) then - unitset:AddUnit(unit) + if not unitset:FindUnit( unit:GetName() ) then + unitset:AddUnit( unit ) end end @@ -37581,7 +45090,7 @@ end --- Return the detected target groups of the controllable as a @{Core.Set#SET_GROUP}. -- The optional parametes specify the detection methods that can be applied. -- If no detection method is given, the detection will use all the available methods by default. --- @param Wrapper.Controllable#CONTROLLABLE self +-- @param #CONTROLLABLE self -- @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. @@ -37589,24 +45098,24 @@ end -- @param #boolean DetectRWR (Optional) If *false*, do not include targets detected by RWR. -- @param #boolean DetectDLINK (Optional) If *false*, do not include targets detected by data link. -- @return Core.Set#SET_GROUP Set of detected groups. -function CONTROLLABLE:GetDetectedGroupSet(DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) +function CONTROLLABLE:GetDetectedGroupSet( DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK ) -- Get detected DCS units. - local detectedtargets=self:GetDetectedTargets(DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) + local detectedtargets = self:GetDetectedTargets( DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK ) - local groupset=SET_GROUP:New() + local groupset = SET_GROUP:New() - for DetectionObjectID, Detection in pairs(detectedtargets or {}) do - local DetectedObject=Detection.object -- DCS#Object + for DetectionObjectID, Detection in pairs( detectedtargets or {} ) do + local DetectedObject = Detection.object -- DCS#Object - if DetectedObject and DetectedObject:isExist() and DetectedObject.id_<50000000 then - local unit=UNIT:Find(DetectedObject) + if DetectedObject and DetectedObject:isExist() and DetectedObject.id_ < 50000000 then + local unit = UNIT:Find( DetectedObject ) if unit and unit:IsAlive() then - local group=unit:GetGroup() + local group = unit:GetGroup() - if group and not groupset:FindGroup(group:GetName()) then - groupset:AddGroup(group) + if group and not groupset:FindGroup( group:GetName() ) then + groupset:AddGroup( group ) end end @@ -37616,7 +45125,6 @@ function CONTROLLABLE:GetDetectedGroupSet(DetectVisual, DetectOptical, DetectRad return groupset end - -- Options --- Set option. @@ -37624,7 +45132,7 @@ end -- @param #number OptionID ID/Type of the option. -- @param #number OptionValue Value of the option -- @return #CONTROLLABLE self -function CONTROLLABLE:SetOption(OptionID, OptionValue) +function CONTROLLABLE:SetOption( OptionID, OptionValue ) local DCSControllable = self:GetDCSObject() if DCSControllable then @@ -37639,10 +45147,10 @@ function CONTROLLABLE:SetOption(OptionID, OptionValue) end --- Set option for Rules of Engagement (ROE). --- @param Wrapper.Controllable#CONTROLLABLE self +-- @param #CONTROLLABLE self -- @param #number ROEvalue ROE value. See ENUMS.ROE. -- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROE(ROEvalue) +function CONTROLLABLE:OptionROE( ROEvalue ) local DCSControllable = self:GetDCSObject() @@ -37651,11 +45159,11 @@ function CONTROLLABLE:OptionROE(ROEvalue) local Controller = self:_GetController() if self:IsAir() then - Controller:setOption(AI.Option.Air.id.ROE, ROEvalue ) + Controller:setOption( AI.Option.Air.id.ROE, ROEvalue ) elseif self:IsGround() then - Controller:setOption(AI.Option.Ground.id.ROE, ROEvalue ) + Controller:setOption( AI.Option.Ground.id.ROE, ROEvalue ) elseif self:IsShip() then - Controller:setOption(AI.Option.Naval.id.ROE, ROEvalue ) + Controller:setOption( AI.Option.Naval.id.ROE, ROEvalue ) end return self @@ -37885,7 +45393,6 @@ function CONTROLLABLE:OptionROTNoReactionPossible() return nil end - --- No evasion on enemy threats. -- @param #CONTROLLABLE self -- @return #CONTROLLABLE self @@ -37910,7 +45417,7 @@ end -- @param #CONTROLLABLE self -- @param #number ROTvalue ROT value. See ENUMS.ROT. -- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROT(ROTvalue) +function CONTROLLABLE:OptionROT( ROTvalue ) self:F2( { self.ControllableName } ) local DCSControllable = self:GetDCSObject() @@ -37983,7 +45490,6 @@ function CONTROLLABLE:OptionROTEvadeFirePossible() return nil end - --- Evade on fire. -- @param #CONTROLLABLE self -- @return #CONTROLLABLE self @@ -38022,7 +45528,6 @@ function CONTROLLABLE:OptionROTVerticalPossible() return nil end - --- Evade on fire using vertical manoeuvres. -- @param #CONTROLLABLE self -- @return #CONTROLLABLE self @@ -38054,10 +45559,10 @@ function CONTROLLABLE:OptionAlarmStateAuto() local Controller = self:_GetController() if self:IsGround() then - Controller:setOption(AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.AUTO) + Controller:setOption( AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.AUTO ) elseif self:IsShip() then - --Controller:setOption(AI.Option.Naval.id.ALARM_STATE, AI.Option.Naval.val.ALARM_STATE.AUTO) - Controller:setOption(9, 0) + -- Controller:setOption(AI.Option.Naval.id.ALARM_STATE, AI.Option.Naval.val.ALARM_STATE.AUTO) + Controller:setOption( 9, 0 ) end return self @@ -38080,8 +45585,8 @@ function CONTROLLABLE:OptionAlarmStateGreen() Controller:setOption( AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.GREEN ) elseif self:IsShip() then -- AI.Option.Naval.id.ALARM_STATE does not seem to exist! - --Controller:setOption( AI.Option.Naval.id.ALARM_STATE, AI.Option.Naval.val.ALARM_STATE.GREEN ) - Controller:setOption(9, 1) + -- Controller:setOption( AI.Option.Naval.id.ALARM_STATE, AI.Option.Naval.val.ALARM_STATE.GREEN ) + Controller:setOption( 9, 1 ) end return self @@ -38101,10 +45606,10 @@ function CONTROLLABLE:OptionAlarmStateRed() local Controller = self:_GetController() if self:IsGround() then - Controller:setOption(AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.RED) + Controller:setOption( AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.RED ) elseif self:IsShip() then - --Controller:setOption(AI.Option.Naval.id.ALARM_STATE, AI.Option.Naval.val.ALARM_STATE.RED) - Controller:setOption(9, 2) + -- Controller:setOption(AI.Option.Naval.id.ALARM_STATE, AI.Option.Naval.val.ALARM_STATE.RED) + Controller:setOption( 9, 2 ) end return self @@ -38113,18 +45618,17 @@ function CONTROLLABLE:OptionAlarmStateRed() return nil end - --- Set RTB on bingo fuel. -- @param #CONTROLLABLE self -- @param #boolean RTB true if RTB on bingo fuel (default), false if no RTB on bingo fuel. -- Warning! When you switch this option off, the airborne group will continue to fly until all fuel has been consumed, and will crash. -- @return #CONTROLLABLE self -function CONTROLLABLE:OptionRTBBingoFuel( RTB ) --R2.2 +function CONTROLLABLE:OptionRTBBingoFuel( RTB ) -- R2.2 self:F2( { self.ControllableName } ) - --RTB = RTB or true - if RTB==nil then - RTB=true + -- RTB = RTB or true + if RTB == nil then + RTB = true end local DCSControllable = self:GetDCSObject() @@ -38141,7 +45645,6 @@ function CONTROLLABLE:OptionRTBBingoFuel( RTB ) --R2.2 return nil end - --- Set RTB on ammo. -- @param #CONTROLLABLE self -- @param #boolean WeaponsFlag Weapons.flag enumerator. @@ -38163,7 +45666,6 @@ function CONTROLLABLE:OptionRTBAmmo( WeaponsFlag ) return nil end - --- Allow to Jettison of weapons upon threat. -- @param #CONTROLLABLE self -- @return #CONTROLLABLE self @@ -38184,7 +45686,6 @@ function CONTROLLABLE:OptionAllowJettisonWeaponsOnThreat() return nil end - --- Keep weapons upon threat. -- @param #CONTROLLABLE self -- @return #CONTROLLABLE self @@ -38209,15 +45710,15 @@ end -- @param #CONTROLLABLE self -- @param #boolean Prohibit If true or nil, prohibit. If false, do not prohibit. -- @return #CONTROLLABLE self -function CONTROLLABLE:OptionProhibitAfterburner(Prohibit) +function CONTROLLABLE:OptionProhibitAfterburner( Prohibit ) self:F2( { self.ControllableName } ) - if Prohibit==nil then - Prohibit=true + if Prohibit == nil then + Prohibit = true end if self:IsAir() then - self:SetOption(AI.Option.Air.id.PROHIBIT_AB, Prohibit) + self:SetOption( AI.Option.Air.id.PROHIBIT_AB, Prohibit ) end return self @@ -38230,7 +45731,7 @@ function CONTROLLABLE:OptionECM_Never() self:F2( { self.ControllableName } ) if self:IsAir() then - self:SetOption(AI.Option.Air.id.ECM_USING, 0) + self:SetOption( AI.Option.Air.id.ECM_USING, 0 ) end return self @@ -38243,13 +45744,12 @@ function CONTROLLABLE:OptionECM_OnlyLockByRadar() self:F2( { self.ControllableName } ) if self:IsAir() then - self:SetOption(AI.Option.Air.id.ECM_USING, 1) + self:SetOption( AI.Option.Air.id.ECM_USING, 1 ) end return self end - --- Defines the usage of Electronic Counter Measures by airborne forces. If the AI is being detected by a radar they will enable their ECM. -- @param #CONTROLLABLE self -- @return #CONTROLLABLE self @@ -38257,7 +45757,7 @@ function CONTROLLABLE:OptionECM_DetectedLockByRadar() self:F2( { self.ControllableName } ) if self:IsAir() then - self:SetOption(AI.Option.Air.id.ECM_USING, 2) + self:SetOption( AI.Option.Air.id.ECM_USING, 2 ) end return self @@ -38270,15 +45770,15 @@ function CONTROLLABLE:OptionECM_AlwaysOn() self:F2( { self.ControllableName } ) if self:IsAir() then - self:SetOption(AI.Option.Air.id.ECM_USING, 3) + self:SetOption( AI.Option.Air.id.ECM_USING, 3 ) end return self end --- Retrieve the controllable mission and allow to place function hooks within the mission waypoint plan. --- Use the method @{Wrapper.Controllable#CONTROLLABLE:WayPointFunction} to define the hook functions for specific waypoints. --- Use the method @{Controllable@CONTROLLABLE:WayPointExecute) to start the execution of the new mission plan. +-- Use the method @{#CONTROLLABLE.WayPointFunction}() to define the hook functions for specific waypoints. +-- Use the method @{#CONTROLLABLE.WayPointExecute}() to start the execution of the new mission plan. -- Note that when WayPointInitialize is called, the Mission of the controllable is RESTARTED! -- @param #CONTROLLABLE self -- @param #table WayPoints If WayPoints is given, then use the route. @@ -38299,7 +45799,7 @@ end -- @param #CONTROLLABLE self -- @return #table WayPoints If WayPoints is given, then return the WayPoints structure. function CONTROLLABLE:GetWayPoints() - self:F( ) + self:F() if self.WayPoints then return self.WayPoints @@ -38322,7 +45822,6 @@ function CONTROLLABLE:WayPointFunction( WayPoint, WayPointIndex, WayPointFunctio return self end - --- Executes the WayPoint plan. -- The function gets a WayPoint parameter, that you can use to restart the mission at a specific WayPoint. -- Note that when the WayPoint parameter is used, the new start mission waypoint of the controllable will be 1! @@ -38384,8 +45883,8 @@ end --- Sets Controllable Option for Restriction of Afterburner. -- @param #CONTROLLABLE self -- @param #boolean RestrictBurner If true, restrict burner. If false or nil, allow (unrestrict) burner. -function CONTROLLABLE:OptionRestrictBurner(RestrictBurner) - self:F2({self.ControllableName}) +function CONTROLLABLE:OptionRestrictBurner( RestrictBurner ) + self:F2( { self.ControllableName } ) local DCSControllable = self:GetDCSObject() @@ -38397,11 +45896,11 @@ function CONTROLLABLE:OptionRestrictBurner(RestrictBurner) -- Issue https://github.com/FlightControl-Master/MOOSE/issues/1216 if RestrictBurner == true then if self:IsAir() then - Controller:setOption(16, true) + Controller:setOption( 16, true ) end else if self:IsAir() then - Controller:setOption(16, false) + Controller:setOption( 16, false ) end end @@ -38415,20 +45914,20 @@ end -- @param #number range Defines the range -- @return #CONTROLLABLE self -- @usage Range can be one of MAX_RANGE = 0, NEZ_RANGE = 1, HALF_WAY_RMAX_NEZ = 2, TARGET_THREAT_EST = 3, RANDOM_RANGE = 4. Defaults to 3. See: https://wiki.hoggitworld.com/view/DCS_option_missileAttack -function CONTROLLABLE:OptionAAAttackRange(range) +function CONTROLLABLE:OptionAAAttackRange( range ) self:F2( { self.ControllableName } ) -- defaults to 3 local range = range or 3 - if range < 0 or range > 4 then + if range < 0 or range > 4 then range = 3 end local DCSControllable = self:GetDCSObject() if DCSControllable then local Controller = self:_GetController() if Controller then - if self:IsAir() then - self:SetOption(AI.Option.Air.val.MISSILE_ATTACK, range) - end + if self:IsAir() then + self:SetOption( AI.Option.Air.val.MISSILE_ATTACK, range ) + end end return self end @@ -38439,53 +45938,54 @@ end -- @param #CONTROLLABLE self -- @param #number EngageRange Engage range limit in percent (a number between 0 and 100). Default 100. -- @return #CONTROLLABLE self -function CONTROLLABLE:OptionEngageRange(EngageRange) +function CONTROLLABLE:OptionEngageRange( EngageRange ) self:F2( { self.ControllableName } ) -- Set default if not specified. - EngageRange=EngageRange or 100 - if EngageRange < 0 or EngageRange > 100 then + EngageRange = EngageRange or 100 + if EngageRange < 0 or EngageRange > 100 then EngageRange = 100 end local DCSControllable = self:GetDCSObject() if DCSControllable then local Controller = self:_GetController() if Controller then - if self:IsGround() then - self:SetOption(AI.Option.Ground.id.AC_ENGAGEMENT_RANGE_RESTRICTION, EngageRange) - end + if self:IsGround() then + self:SetOption( AI.Option.Ground.id.AC_ENGAGEMENT_RANGE_RESTRICTION, EngageRange ) + end end - return self + return self end return nil end --- (GROUND) Relocate controllable to a random point within a given radius; use e.g.for evasive actions; Note that not all ground controllables can actually drive, also the alarm state of the controllable might stop it from moving. -- @param #CONTROLLABLE self --- @param #number speed Speed of the controllable, default 20 --- @param #number radius Radius of the relocation zone, default 500 --- @param #boolean onroad If true, route on road (less problems with AI way finding), default true --- @param #boolean shortcut If true and onroad is set, take a shorter route - if available - off road, default false +-- @param #number speed Speed of the controllable, default 20 +-- @param #number radius Radius of the relocation zone, default 500 +-- @param #boolean onroad If true, route on road (less problems with AI way finding), default true +-- @param #boolean shortcut If true and onroad is set, take a shorter route - if available - off road, default false +-- @param #string formation Formation string as in the mission editor, e.g. "Vee", "Diamond", "Line abreast", etc. Defaults to "Off Road" -- @return #CONTROLLABLE self -function CONTROLLABLE:RelocateGroundRandomInRadius(speed, radius, onroad, shortcut) +function CONTROLLABLE:RelocateGroundRandomInRadius( speed, radius, onroad, shortcut, formation ) self:F2( { self.ControllableName } ) - local _coord = self:GetCoordinate() - local _radius = radius or 500 - local _speed = speed or 20 - local _tocoord = _coord:GetRandomCoordinateInRadius(_radius,100) - local _onroad = onroad or true - local _grptsk = {} - local _candoroad = false - local _shortcut = shortcut or false - - -- create a DCS Task an push it on the group - -- TaskGroundOnRoad(ToCoordinate,Speed,OffRoadFormation,Shortcut,FromCoordinate,WaypointFunction,WaypointFunctionArguments) - if onroad then - _grptsk, _candoroad = self:TaskGroundOnRoad(_tocoord,_speed,"Off Road",_shortcut) - self:Route(_grptsk,5) - else - self:TaskRouteToVec2(_tocoord:GetVec2(),_speed,"Off Road") - end + local _coord = self:GetCoordinate() + local _radius = radius or 500 + local _speed = speed or 20 + local _tocoord = _coord:GetRandomCoordinateInRadius( _radius, 100 ) + local _onroad = onroad or true + local _grptsk = {} + local _candoroad = false + local _shortcut = shortcut or false + local _formation = formation or "Off Road" + + -- create a DCS Task an push it on the group + if onroad then + _grptsk, _candoroad = self:TaskGroundOnRoad( _tocoord, _speed, _formation, _shortcut ) + self:Route( _grptsk, 5 ) + else + self:TaskRouteToVec2( _tocoord:GetVec2(), _speed, _formation ) + end return self end @@ -38494,7 +45994,7 @@ end -- @param #CONTROLLABLE self -- @param #number Seconds Any positive number: AI will disperse, but only for the specified time before continuing their route. 0: AI will not disperse. -- @return #CONTROLLABLE self -function CONTROLLABLE:OptionDisperseOnAttack(Seconds) +function CONTROLLABLE:OptionDisperseOnAttack( Seconds ) self:F2( { self.ControllableName } ) -- Set default if not specified. local seconds = Seconds or 0 @@ -38502,11 +46002,11 @@ function CONTROLLABLE:OptionDisperseOnAttack(Seconds) if DCSControllable then local Controller = self:_GetController() if Controller then - if self:IsGround() then - self:SetOption(AI.Option.Ground.id.DISPERSE_ON_ATTACK, seconds) - end + if self:IsGround() then + self:SetOption( AI.Option.Ground.id.DISPERSE_ON_ATTACK, seconds ) + end end - return self + return self end return nil end @@ -38514,23 +46014,65 @@ end --- Returns if the unit is a submarine. -- @param #POSITIONABLE self -- @return #boolean Submarines attributes result. -function POSITIONABLE:IsSubmarine() +function CONTROLLABLE:IsSubmarine() self:F2() local DCSUnit = self:GetDCSObject() if DCSUnit then local UnitDescriptor = DCSUnit:getDesc() - if UnitDescriptor.attributes["Submarines"] == true then - return true - else - return false - end + if UnitDescriptor.attributes["Submarines"] == true then + return true + else + return false + end end 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. -- -- === -- @@ -38543,12 +46085,12 @@ end -- * Handle local Group Controller. -- * Manage the "state" of the DCS Group. -- --- **IMPORTANT: ONE SHOULD NEVER SANATIZE these GROUP OBJECT REFERENCES! (make the GROUP object references nil).** +-- **IMPORTANT: ONE SHOULD NEVER SANITIZE these GROUP OBJECT REFERENCES! (make the GROUP object references nil).** -- -- === -- --- For each DCS Group object alive within a running mission, a GROUP wrapper object (instance) will be created within the _@{DATABASE} object. --- This is done at the beginning of the mission (when the mission starts), and dynamically when new DCS Group objects are spawned (using the @{SPAWN} class). +-- For each DCS Group object alive within a running mission, a GROUP wrapper object (instance) will be created within the global _DATABASE object (an instance of @{Core.Database#DATABASE}). +-- This is done at the beginning of the mission (when the mission starts), and dynamically when new DCS Group objects are spawned (using the @{Core.Spawn} class). -- -- The GROUP class does not contain a :New() method, rather it provides :Find() methods to retrieve the object reference -- using the DCS Group or the DCS GroupName. @@ -38579,8 +46121,8 @@ end -- -- The GROUP class provides the following functions to retrieve quickly the relevant GROUP instance: -- --- * @{#GROUP.Find}(): Find a GROUP instance from the _DATABASE object using a DCS Group object. --- * @{#GROUP.FindByName}(): Find a GROUP instance from the _DATABASE object using a DCS Group name. +-- * @{#GROUP.Find}(): Find a GROUP instance from the global _DATABASE object (an instance of @{Core.Database#DATABASE}) using a DCS Group object. +-- * @{#GROUP.FindByName}(): Find a GROUP instance from the global _DATABASE object (an instance of @{Core.Database#DATABASE}) using a DCS Group name. -- -- # 1. Tasking of groups -- @@ -38664,14 +46206,14 @@ end -- -- ## GROUP Zone validation methods -- --- The group can be validated whether it is completely, partly or not within a @{Zone}. +-- The group can be validated whether it is completely, partly or not within a @{Core.Zone}. -- Use the following Zone validation methods on the group: -- --- * @{#GROUP.IsCompletelyInZone}: Returns true if all units of the group are within a @{Zone}. --- * @{#GROUP.IsPartlyInZone}: Returns true if some units of the group are within a @{Zone}. --- * @{#GROUP.IsNotInZone}: Returns true if none of the group units of the group are within a @{Zone}. +-- * @{#GROUP.IsCompletelyInZone}: Returns true if all units of the group are within a @{Core.Zone}. +-- * @{#GROUP.IsPartlyInZone}: Returns true if some units of the group are within a @{Core.Zone}. +-- * @{#GROUP.IsNotInZone}: Returns true if none of the group units of the group are within a @{Core.Zone}. -- --- The zone can be of any @{Zone} class derived from @{Core.Zone#ZONE_BASE}. So, these methods are polymorphic to the zones tested on. +-- The zone can be of any @{Core.Zone} class derived from @{Core.Zone#ZONE_BASE}. So, these methods are polymorphic to the zones tested on. -- -- ## GROUP AI methods -- @@ -38719,6 +46261,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. @@ -38745,6 +46288,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", @@ -38842,8 +46386,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 ) @@ -38871,9 +46414,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 ) @@ -38895,8 +46436,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 ) @@ -38920,7 +46460,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. @@ -38944,7 +46484,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 @@ -39066,26 +46605,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. @@ -39104,12 +46649,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 speed The list of @{Wrapper.Unit} objects of the @{Wrapper.Group}. +-- @return #table of Wrapper.Unit#UNIT objects, indexed by number. function GROUP:GetUnits() self:F2( { self.GroupName } ) local DCSGroup = self:GetDCSObject() if DCSGroup then - local DCSUnits = DCSGroup:getUnits() + local DCSUnits = DCSGroup:getUnits() or {} local Units = {} for Index, UnitData in pairs( DCSUnits ) do Units[#Units+1] = UNIT:Find( UnitData ) @@ -39171,7 +46718,6 @@ function GROUP:GetUnits() return nil end - --- Returns a list of @{Wrapper.Unit} objects of the @{Wrapper.Group} that are occupied by a player. -- @param #GROUP self -- @return #list The list of player occupied @{Wrapper.Unit} objects of the @{Wrapper.Group}. @@ -39195,40 +46741,67 @@ function GROUP:GetPlayerUnits() return nil end +--- Check if an (air) group is a client or player slot. Information is retrieved from the group template. +-- @param #GROUP self +-- @return #boolean If true, group is associated with a client or player slot. +function GROUP:IsPlayer() + return self:GetUnit(1):IsPlayer() +end ---- Returns the UNIT wrapper class with number UnitNumber. --- If the underlying DCS Unit does not exist, the method will return nil. . +--- Returns the UNIT wrapper object with number UnitNumber. If it doesn't exist, tries to return the next available unit. +-- If no underlying DCS Units exist, the method will return nil. -- @param #GROUP self -- @param #number UnitNumber The number of the UNIT wrapper class to be returned. --- @return Wrapper.Unit#UNIT The UNIT wrapper class. +-- @return Wrapper.Unit#UNIT The UNIT object or nil function GROUP:GetUnit( UnitNumber ) - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - - local DCSUnit = DCSGroup:getUnit( UnitNumber ) - - local UnitFound = UNIT:Find(DCSUnit) - - return UnitFound + if DCSGroup then + local UnitFound = nil + -- 2.7.1 dead event bug, return the first alive unit instead + -- Maybe fixed with 2.8? + local units = DCSGroup:getUnits() or {} + if units[UnitNumber] then + local UnitFound = UNIT:Find(units[UnitNumber]) + if UnitFound then + return UnitFound + end + else + for _,_unit in pairs(units) do + local UnitFound = UNIT:Find(_unit) + if UnitFound then + return UnitFound + end + end + end end - - return nil + return nil end + --- Returns the DCS Unit with number UnitNumber. --- If the underlying DCS Unit does not exist, the method will return nil. . +-- If the underlying DCS Unit does not exist, the method will return try to find the next unit. Returns nil if no units are found. -- @param #GROUP self -- @param #number UnitNumber The number of the DCS Unit to be returned. -- @return DCS#Unit The DCS Unit. function GROUP:GetDCSUnit( UnitNumber ) - local DCSGroup=self:GetDCSObject() + local DCSGroup = self:GetDCSObject() if DCSGroup then - local DCSUnitFound=DCSGroup:getUnit( UnitNumber ) - return DCSUnitFound + + if DCSGroup.getUnit and DCSGroup:getUnit( UnitNumber ) then + return DCSGroup:getUnit( UnitNumber ) + else + + -- 2.7.1 dead event bug, return the first alive unit instead + local units = DCSGroup:getUnits() or {} + + for _,_unit in pairs(units) do + if _unit and _unit:isExist() then + return _unit + end + end + end end return nil @@ -39298,12 +46871,24 @@ function GROUP:GetFirstUnitAlive() return nil end +--- Get the first unit of the group. Might be nil! +-- @param #GROUP self +-- @return Wrapper.Unit#UNIT First unit or nil if it does not exist. +function GROUP:GetFirstUnit() + self:F3({self.GroupName}) + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local units=self:GetUnits() + return units[1] + end + return nil +end --- Returns the average velocity Vec3 vector. -- @param Wrapper.Group#GROUP self --- @return DCS#Vec3 The velocity Vec3 vector --- @return #nil The GROUP is not existing or alive. +-- @return DCS#Vec3 The velocity Vec3 vector or `#nil` if the GROUP is not existing or alive. function GROUP:GetVelocityVec3() self:F2( self.GroupName ) @@ -39334,11 +46919,19 @@ function GROUP:GetVelocityVec3() return nil end +--- Returns the average group altitude in meters. +-- @param Wrapper.Group#GROUP 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 altitude of the group or nil if is not existing or alive. +function GROUP:GetAltitude(FromGround) + self:F2( self.GroupName ) + return self:GetHeight(FromGround) +end --- Returns the average group height in meters. -- @param Wrapper.Group#GROUP self --- @param #boolean FromGround Measure from the ground or from sea level. Provide **true** for measuring from the ground. **false** or **nil** if you measure from sea level. --- @return DCS#Vec3 The height of the group or nil if is not existing or alive. +-- @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 GROUP:GetHeight( FromGround ) self:F2( self.GroupName ) @@ -39438,6 +47031,24 @@ function GROUP:GetTypeName() return nil end +--- [AIRPLANE] Get the NATO reporting name (platform, e.g. "Flanker") of a GROUP (note - first unit the group). "Bogey" if not found. Currently airplanes only! +--@param #GROUP self +--@return #string NatoReportingName or "Bogey" if unknown. +function GROUP:GetNatoReportingName() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local GroupTypeName = DCSGroup:getUnit(1):getTypeName() + self:T3( GroupTypeName ) + return UTILS.GetReportingName(GroupTypeName) + end + + return "Bogey" + +end + --- Gets the player name of the group. -- @param #GROUP self -- @return #string The player name of the group. @@ -39489,9 +47100,9 @@ function GROUP:GetVec2() end ---- Returns the current Vec3 vector of the first DCS Unit in the GROUP. +--- Returns the current Vec3 vector of the first Unit in the GROUP. -- @param #GROUP self --- @return DCS#Vec3 Current Vec3 of the first DCS Unit of the GROUP. +-- @return DCS#Vec3 Current Vec3 of the first Unit of the GROUP or nil if cannot be found. function GROUP:GetVec3() -- Get first unit. @@ -39506,6 +47117,37 @@ function GROUP:GetVec3() return nil end +--- Returns the average Vec3 vector of the Units in the GROUP. +-- @param #GROUP self +-- @return DCS#Vec3 Current Vec3 of the GROUP or nil if cannot be found. +function GROUP:GetAverageVec3() + local units = self:GetUnits() or {} + -- Init. + local x=0 ; local y=0 ; local z=0 ; local n=0 + -- Loop over all units. + for _,unit in pairs(units) do + local vec3=nil --DCS#Vec3 + if unit and unit:IsAlive() then + vec3 = unit:GetVec3() + 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 + return Vec3 + end + return nil +end + --- Returns a POINT_VEC2 object indicating the point in 2D of the first UNIT of the GROUP within the mission. -- @param #GROUP self -- @return Core.Point#POINT_VEC2 The 2D point vector of the first DCS Unit of the GROUP. @@ -39526,29 +47168,53 @@ function GROUP:GetPointVec2() return nil end +--- Returns a COORDINATE object indicating the average position of the GROUP within the mission. +-- @param Wrapper.Group#GROUP self +-- @return Core.Point#COORDINATE The COORDINATE of the GROUP. +function GROUP:GetAverageCoordinate() + local vec3 = self:GetAverageVec3() + if vec3 then + local coord = COORDINATE:NewFromVec3(vec3) + local Heading = self:GetHeading() + coord.Heading = Heading + else + BASE:E( { "Cannot GetAverageCoordinate", Group = self, Alive = self:IsAlive() } ) + return nil + end +end + --- Returns a COORDINATE object indicating the point of the first UNIT of the GROUP within the mission. -- @param Wrapper.Group#GROUP self -- @return Core.Point#COORDINATE The COORDINATE of the GROUP. function GROUP:GetCoordinate() - - local FirstUnit = self:GetUnit(1) + + + local Units = self:GetUnits() or {} - if FirstUnit then - local FirstUnitCoordinate = FirstUnit:GetCoordinate() - return FirstUnitCoordinate + for _,_unit in pairs(Units) do + local FirstUnit = _unit -- Wrapper.Unit#UNIT + + if FirstUnit then + + local FirstUnitCoordinate = FirstUnit:GetCoordinate() + + if FirstUnitCoordinate then + local Heading = self:GetHeading() + FirstUnitCoordinate.Heading = Heading + return FirstUnitCoordinate + end + + end end - BASE:E( { "Cannot GetCoordinate", Group = self, Alive = self:IsAlive() } ) - - return nil + end --- Returns a random @{DCS#Vec3} vector (point in 3D of the UNIT within the mission) within a range around the first UNIT of the GROUP. -- @param #GROUP self --- @param #number Radius --- @return DCS#Vec3 The random 3D point vector around the first UNIT of the GROUP. --- @return #nil The GROUP is invalid or empty +-- @param #number Radius Radius in meters. +-- @return DCS#Vec3 The random 3D point vector around the first UNIT of the GROUP or #nil The GROUP is invalid or empty. -- @usage -- -- If Radius is ignored, returns the DCS#Vec3 of first UNIT of the GROUP function GROUP:GetRandomVec3(Radius) @@ -39569,24 +47235,25 @@ end --- Returns the mean heading of every UNIT in the GROUP in degrees -- @param #GROUP self --- @return #number mean heading of the GROUP --- @return #nil The first UNIT is not existing or alive. +-- @return #number Mean heading of the GROUP in degrees or #nil The first UNIT is not existing or alive. function GROUP:GetHeading() self:F2(self.GroupName) + self:F2(self.GroupName) + local GroupSize = self:GetSize() local HeadingAccumulator = 0 - local n=0 + local Units = self:GetUnits() + if GroupSize then - for i = 1, GroupSize do - local unit=self:GetUnit(i) + for _,unit in pairs(Units) do if unit and unit:IsAlive() then HeadingAccumulator = HeadingAccumulator + unit:GetHeading() n=n+1 end end - return math.floor(HeadingAccumulator / n) + return math.floor(HeadingAccumulator / n) end BASE:E( { "Cannot GetHeading", Group = self, Alive = self:IsAlive() } ) @@ -39598,8 +47265,8 @@ end --- Return the fuel state and unit reference for the unit with the least -- amount of fuel in the group. -- @param #GROUP self --- @return #number The fuel state of the unit with the least amount of fuel --- @return #Unit reference to #Unit object for further processing +-- @return #number The fuel state of the unit with the least amount of fuel. +-- @return Wrapper.Unit#UNIT reference to #Unit object for further processing. function GROUP:GetFuelMin() self:F3(self.ControllableName) @@ -39641,7 +47308,7 @@ function GROUP:GetFuelAvg() local TotalFuel = 0 for UnitID, UnitData in pairs( self:GetUnits() ) do local Unit = UnitData -- Wrapper.Unit#UNIT - local UnitFuel = Unit:GetFuel() + local UnitFuel = Unit:GetFuel() or 0 self:F( { Fuel = UnitFuel } ) TotalFuel = TotalFuel + UnitFuel end @@ -39670,6 +47337,7 @@ end -- @return #number Number of rockets left. -- @return #number Number of bombs left. -- @return #number Number of missiles left. +-- @return #number Number of artillery shells left (with explosive mass, included in shells; shells can also be machine gun ammo) function GROUP:GetAmmunition() self:F( self.ControllableName ) @@ -39679,6 +47347,8 @@ function GROUP:GetAmmunition() local Nshells=0 local Nrockets=0 local Nmissiles=0 + local Nbombs=0 + local Narti=0 if DCSControllable then @@ -39687,25 +47357,26 @@ function GROUP:GetAmmunition() local Unit = UnitData -- Wrapper.Unit#UNIT -- Get ammo of the unit - local ntot, nshells, nrockets, nmissiles = Unit:GetAmmunition() + local ntot, nshells, nrockets, nbombs, nmissiles, narti = Unit:GetAmmunition() Ntot=Ntot+ntot Nshells=Nshells+nshells Nrockets=Nrockets+nrockets - Nmissiles=Nmissiles+nmissiles - + Nmissiles=Nmissiles+nmissiles + Nbombs=Nbombs+nbombs + Narti=Narti+narti end end - return Ntot, Nshells, Nrockets, Nmissiles + return Ntot, Nshells, Nrockets, Nbombs, Nmissiles, Narti end do -- Is Zone methods ---- Check if any unit of a group is inside a @{Zone}. +--- Check if any unit of a group is inside a @{Core.Zone}. -- @param #GROUP self -- @param Core.Zone#ZONE_BASE Zone The zone to test. -- @return #boolean Returns `true` if *at least one unit* is inside the zone or `false` if *no* unit is inside. @@ -39716,13 +47387,14 @@ function GROUP:IsInZone( Zone ) for UnitID, UnitData in pairs(self:GetUnits()) do local Unit = UnitData -- Wrapper.Unit#UNIT - -- Get 2D vector. That's all we need for the zone check. - local vec2=Unit:GetVec2() + local vec2 = nil + if Unit then + -- Get 2D vector. That's all we need for the zone check. + vec2=Unit:GetVec2() + end - if Zone:IsVec2InZone(vec2) then + if vec2 and Zone:IsVec2InZone(vec2) then return true -- At least one unit is in the zone. That is enough. - else - -- This one is not but another could be. end end @@ -39733,7 +47405,7 @@ function GROUP:IsInZone( Zone ) return nil end ---- Returns true if all units of the group are within a @{Zone}. +--- Returns true if all units of the group are within a @{Core.Zone}. -- @param #GROUP self -- @param Core.Zone#ZONE_BASE Zone The zone to test. -- @return #boolean Returns true if the Group is completely within the @{Core.Zone#ZONE_BASE} @@ -39753,7 +47425,7 @@ function GROUP:IsCompletelyInZone( Zone ) return true end ---- Returns true if some but NOT ALL units of the group are within a @{Zone}. +--- Returns true if some but NOT ALL units of the group are within a @{Core.Zone}. -- @param #GROUP self -- @param Core.Zone#ZONE_BASE Zone The zone to test. -- @return #boolean Returns true if the Group is partially within the @{Core.Zone#ZONE_BASE} @@ -39781,7 +47453,7 @@ function GROUP:IsPartlyInZone( Zone ) end end ---- Returns true if part or all units of the group are within a @{Zone}. +--- Returns true if part or all units of the group are within a @{Core.Zone}. -- @param #GROUP self -- @param Core.Zone#ZONE_BASE Zone The zone to test. -- @return #boolean Returns true if the Group is partially or completely within the @{Core.Zone#ZONE_BASE}. @@ -39789,7 +47461,7 @@ function GROUP:IsPartlyOrCompletelyInZone( Zone ) return self:IsPartlyInZone(Zone) or self:IsCompletelyInZone(Zone) end ---- Returns true if none of the group units of the group are within a @{Zone}. +--- Returns true if none of the group units of the group are within a @{Core.Zone}. -- @param #GROUP self -- @param Core.Zone#ZONE_BASE Zone The zone to test. -- @return #boolean Returns true if the Group is not within the @{Core.Zone#ZONE_BASE} @@ -39825,10 +47497,10 @@ function GROUP:IsAnyInZone( Zone ) return false end ---- Returns the number of UNITs that are in the @{Zone} +--- Returns the number of UNITs that are in the @{Core.Zone} -- @param #GROUP self -- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #number The number of UNITs that are in the @{Zone} +-- @return #number The number of UNITs that are in the @{Core.Zone} function GROUP:CountInZone( Zone ) self:F2( {self.GroupName, Zone} ) local Count = 0 @@ -40085,7 +47757,7 @@ end -- RESPAWNING ---- Returns the group template from the @{DATABASE} (_DATABASE object). +--- Returns the group template from the global _DATABASE object (an instance of @{Core.Database#DATABASE}). -- @param #GROUP self -- @return #table function GROUP:GetTemplate() @@ -40093,7 +47765,7 @@ function GROUP:GetTemplate() return UTILS.DeepCopy( _DATABASE:GetGroupTemplate( GroupName ) ) end ---- Returns the group template route.points[] (the waypoints) from the @{DATABASE} (_DATABASE object). +--- Returns the group template route.points[] (the waypoints) from the global _DATABASE object (an instance of @{Core.Database#DATABASE}). -- @param #GROUP self -- @return #table function GROUP:GetTemplateRoutePoints() @@ -40151,7 +47823,7 @@ function GROUP:InitHeight( Height ) end ---- Set the respawn @{Zone} for the respawned group. +--- Set the respawn @{Core.Zone} for the respawned group. -- @param #GROUP self -- @param Core.Zone#ZONE Zone The zone in meters. -- @return #GROUP self @@ -40161,7 +47833,7 @@ function GROUP:InitZone( Zone ) end ---- Randomize the positions of the units of the respawned group within the @{Zone}. +--- Randomize the positions of the units of the respawned group within the @{Core.Zone}. -- When a Respawn happens, the units of the group will be placed at random positions within the Zone (selected). -- @param #GROUP self -- @param #boolean PositionZone true will randomize the positions within the Zone. @@ -40261,9 +47933,9 @@ end -- - @{#GROUP.InitHeading}: Set the heading for the units in degrees within the respawned group. -- - @{#GROUP.InitHeight}: Set the height for the units in meters for the respawned group. (This is applicable for air units). -- - @{#GROUP.InitRandomizeHeading}: Randomize the headings for the units within the respawned group. --- - @{#GROUP.InitZone}: Set the respawn @{Zone} for the respawned group. --- - @{#GROUP.InitRandomizeZones}: Randomize the respawn @{Zone} between one of the @{Zone}s given for the respawned group. --- - @{#GROUP.InitRandomizePositionZone}: Randomize the positions of the units of the respawned group within the @{Zone}. +-- - @{#GROUP.InitZone}: Set the respawn @{Core.Zone} for the respawned group. +-- - @{#GROUP.InitRandomizeZones}: Randomize the respawn @{Core.Zone} between one of the @{Core.Zone}s given for the respawned group. +-- - @{#GROUP.InitRandomizePositionZone}: Randomize the positions of the units of the respawned group within the @{Core.Zone}. -- - @{#GROUP.InitRandomizePositionRadius}: Randomize the positions of the units of the respawned group in a circle band. -- - @{#GROUP.InitRandomizeTemplates}: Randomize the Template for the respawned group. -- @@ -40614,7 +48286,7 @@ function GROUP:GetTaskRoute() return routines.utils.deepCopy( _DATABASE.Templates.Groups[self.GroupName].Template.route.points ) end ---- Return the route of a group by using the @{Core.Database#DATABASE} class. +--- Return the route of a group by using the global _DATABASE object (an instance of @{Core.Database#DATABASE}). -- @param #GROUP self -- @param #number Begin The route point from where the copy will start. The base route point is 0. -- @param #number End The route point where the copy will end. The End point is the last point - the End point. The last point has base 0. @@ -40835,14 +48507,15 @@ function GROUP:GetAttribute() --- Ground --- -------------- -- Ground - local apc=self:HasAttribute("Infantry carriers") + local apc=self:HasAttribute("APC") local truck=self:HasAttribute("Trucks") and self:GetCategory()==Group.Category.GROUND local infantry=self:HasAttribute("Infantry") local artillery=self:HasAttribute("Artillery") - local tank=self:HasAttribute("Old Tanks") or self:HasAttribute("Modern Tanks") - local aaa=self:HasAttribute("AAA") + local tank=self:HasAttribute("Old Tanks") or self:HasAttribute("Modern Tanks") or self:HasAttribute("Tanks") + local aaa=self:HasAttribute("AAA") and (not self:HasAttribute("SAM elements")) local ewr=self:HasAttribute("EWR") - local sam=self:HasAttribute("SAM elements") and (not self:HasAttribute("AAA")) + local ifv=self:HasAttribute("IFV") + local sam=self:HasAttribute("SAM elements") or self:HasAttribute("Optical Tracker") -- Train local train=self:GetCategory()==Group.Category.TRAIN @@ -40856,41 +48529,46 @@ function GROUP:GetAttribute() local unarmedship=self:HasAttribute("Unarmed ships") - -- Define attribute. Order is important. - if transportplane then - attribute=GROUP.Attribute.AIR_TRANSPORTPLANE - elseif awacs then - attribute=GROUP.Attribute.AIR_AWACS - elseif fighter then + -- Define attribute. Order of attack is important. + if fighter then attribute=GROUP.Attribute.AIR_FIGHTER elseif bomber then attribute=GROUP.Attribute.AIR_BOMBER + elseif awacs then + attribute=GROUP.Attribute.AIR_AWACS + elseif transportplane then + attribute=GROUP.Attribute.AIR_TRANSPORTPLANE elseif tanker then attribute=GROUP.Attribute.AIR_TANKER + -- helos + elseif attackhelicopter then + attribute=GROUP.Attribute.AIR_ATTACKHELO elseif transporthelo then attribute=GROUP.Attribute.AIR_TRANSPORTHELO - elseif attackhelicopter then - attribute=GROUP.Attribute.AIR_ATTACKHELO elseif uav then attribute=GROUP.Attribute.AIR_UAV - elseif apc then - attribute=GROUP.Attribute.GROUND_APC - elseif infantry then - attribute=GROUP.Attribute.GROUND_INFANTRY - elseif artillery then - attribute=GROUP.Attribute.GROUND_ARTILLERY - elseif tank then - attribute=GROUP.Attribute.GROUND_TANK - elseif aaa then - attribute=GROUP.Attribute.GROUND_AAA + -- ground - order of attack elseif ewr then attribute=GROUP.Attribute.GROUND_EWR elseif sam then attribute=GROUP.Attribute.GROUND_SAM + elseif aaa then + attribute=GROUP.Attribute.GROUND_AAA + elseif artillery then + attribute=GROUP.Attribute.GROUND_ARTILLERY + elseif tank then + attribute=GROUP.Attribute.GROUND_TANK + elseif ifv then + attribute=GROUP.Attribute.GROUND_IFV + elseif apc then + attribute=GROUP.Attribute.GROUND_APC + elseif infantry then + attribute=GROUP.Attribute.GROUND_INFANTRY elseif truck then attribute=GROUP.Attribute.GROUND_TRUCK elseif train then attribute=GROUP.Attribute.GROUND_TRAIN + -- ships elseif aircraftcarrier then attribute=GROUP.Attribute.NAVAL_AIRCRAFTCARRIER elseif warship then @@ -41108,8 +48786,10 @@ end -- @return #GROUP self function GROUP:SetCommandInvisible(switch) self:F2( self.GroupName ) - local switch = switch or false - local SetInvisible = {id = 'SetInvisible', params = {value = true}} + if switch==nil then + switch=false + end + local SetInvisible = {id = 'SetInvisible', params = {value = switch}} self:SetCommand(SetInvisible) return self end @@ -41120,103 +48800,169 @@ end -- @return #GROUP self function GROUP:SetCommandImmortal(switch) self:F2( self.GroupName ) - local switch = switch or false - local SetInvisible = {id = 'SetImmortal', params = {value = true}} - self:SetCommand(SetInvisible) + if switch==nil then + switch=false + end + local SetImmortal = {id = 'SetImmortal', params = {value = switch}} + self:SetCommand(SetImmortal) return self end ---do -- Smoke --- ------ Signal a flare at the position of the GROUP. ----- @param #GROUP self ----- @param Utilities.Utils#FLARECOLOR FlareColor ---function GROUP:Flare( FlareColor ) --- self:F2() --- trigger.action.signalFlare( self:GetVec3(), FlareColor , 0 ) ---end --- ------ Signal a white flare at the position of the GROUP. ----- @param #GROUP self ---function GROUP:FlareWhite() --- self:F2() --- trigger.action.signalFlare( self:GetVec3(), trigger.flareColor.White , 0 ) ---end --- ------ Signal a yellow flare at the position of the GROUP. ----- @param #GROUP self ---function GROUP:FlareYellow() --- self:F2() --- trigger.action.signalFlare( self:GetVec3(), trigger.flareColor.Yellow , 0 ) ---end --- ------ Signal a green flare at the position of the GROUP. ----- @param #GROUP self ---function GROUP:FlareGreen() --- self:F2() --- trigger.action.signalFlare( self:GetVec3(), trigger.flareColor.Green , 0 ) ---end --- ------ Signal a red flare at the position of the GROUP. ----- @param #GROUP self ---function GROUP:FlareRed() --- self:F2() --- local Vec3 = self:GetVec3() --- if Vec3 then --- trigger.action.signalFlare( Vec3, trigger.flareColor.Red, 0 ) --- end ---end --- ------ Smoke the GROUP. ----- @param #GROUP self ---function GROUP:Smoke( SmokeColor, Range ) --- self:F2() --- if Range then --- trigger.action.smoke( self:GetRandomVec3( Range ), SmokeColor ) --- else --- trigger.action.smoke( self:GetVec3(), SmokeColor ) --- end --- ---end --- ------ Smoke the GROUP Green. ----- @param #GROUP self ---function GROUP:SmokeGreen() --- self:F2() --- trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Green ) ---end --- ------ Smoke the GROUP Red. ----- @param #GROUP self ---function GROUP:SmokeRed() --- self:F2() --- trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Red ) ---end --- ------ Smoke the GROUP White. ----- @param #GROUP self ---function GROUP:SmokeWhite() --- self:F2() --- trigger.action.smoke( self:GetVec3(), trigger.smokeColor.White ) ---end --- ------ Smoke the GROUP Orange. ----- @param #GROUP self ---function GROUP:SmokeOrange() --- self:F2() --- trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Orange ) ---end --- ------ Smoke the GROUP Blue. ----- @param #GROUP self ---function GROUP:SmokeBlue() --- self:F2() --- trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Blue ) ---end +--- Get skill from Group. Effectively gets the skill from Unit 1 as the group holds no skill value. +-- @param #GROUP self +-- @return #string Skill String of skill name. +function GROUP:GetSkill() + self:F2( self.GroupName ) + local unit = self:GetUnit(1) + local name = unit:GetName() + local skill = _DATABASE.Templates.Units[name].Template.skill or "Random" + return skill +end + + +--- Get the unit in the group with the highest threat level, which is still alive. +-- @param #GROUP self +-- @return Wrapper.Unit#UNIT The most dangerous unit in the group. +-- @return #number Threat level of the unit. +function GROUP:GetHighestThreat() + + -- Get units of the group. + local units=self:GetUnits() + + if units then + + local threat=nil ; local maxtl=0 + for _,_unit in pairs(units or {}) do + local unit=_unit --Wrapper.Unit#UNIT + + if unit and unit:IsAlive() then + + -- Threat level of group. + local tl=unit:GetThreatLevel() + + -- Check if greater the current threat. + if tl>maxtl then + maxtl=tl + threat=unit + end + end + end + + return threat, maxtl + end + + return nil, nil +end + +--- Get TTS friendly, optionally customized callsign mainly for **player groups**. A customized callsign is taken from the #GROUP name, after an optional '#' sign, e.g. "Aerial 1-1#Ghostrider" resulting in "Ghostrider 9", or, +-- if that isn't available, from the playername, as set in the mission editor main screen under Logbook, after an optional '|' sign (actually, more of a personal call sign), e.g. "Apple|Moose" results in "Moose 9 1". Options see below. +-- @param #GROUP self +-- @param #boolean ShortCallsign Return a shortened customized callsign, i.e. "Ghostrider 9" and not "Ghostrider 9 1" +-- @param #boolean Keepnumber (Player only) Return customized callsign, incl optional numbers at the end, e.g. "Aerial 1-1#Ghostrider 109" results in "Ghostrider 109", if you want to e.g. use historical US Navy Callsigns +-- @param #table CallsignTranslations Table to translate between DCS standard callsigns and bespoke ones. Does not apply if using customized +-- callsigns from playername or group name. +-- @return #string Callsign +-- @usage +-- -- Set Custom CAP Flight Callsigns for use with TTS +-- mygroup:GetCustomCallSign(true,false,{ +-- Devil = 'Bengal', +-- Snake = 'Winder', +-- Colt = 'Camelot', +-- Enfield = 'Victory', +-- Uzi = 'Evil Eye' +-- }) -- +-- results in this outcome if the group has Callsign "Enfield 9 1" on the 1st #UNIT of the group: -- +-- 'Victory 9' -- ---end +-- +function GROUP:GetCustomCallSign(ShortCallsign,Keepnumber,CallsignTranslations) + --self:I("GetCustomCallSign") + + local callsign = "Ghost 1" + if self:IsAlive() then + local IsPlayer = self:IsPlayer() + local shortcallsign = self:GetCallsign() or "unknown91" -- e.g.Uzi91, but we want Uzi 9 1 + local callsignroot = string.match(shortcallsign, '(%a+)') -- Uzi + --self:I("CallSign = " .. callsignroot) + local groupname = self:GetName() + local callnumber = string.match(shortcallsign, "(%d+)$" ) or "91" -- 91 + local callnumbermajor = string.char(string.byte(callnumber,1)) -- 9 + local callnumberminor = string.char(string.byte(callnumber,2)) -- 1 + local personalized = false + if IsPlayer and string.find(groupname,"#") then + -- personalized flight name in group naming + if Keepnumber then + shortcallsign = string.match(groupname,"#(.+)") or "Ghost 111" -- Ghostrider 219 + else + shortcallsign = string.match(groupname,"#%s*([%a]+)") or "Ghost" -- Ghostrider + end + personalized = true + elseif IsPlayer and string.find(self:GetPlayerName(),"|") then + -- personalized flight name in group naming + shortcallsign = string.match(self:GetPlayerName(),"|%s*([%a]+)") or string.match(self:GetPlayerName(),"|%s*([%d]+)") or "Ghost" -- Ghostrider + personalized = true + end + + if (not personalized) and CallsignTranslations and CallsignTranslations[callsignroot] then + callsignroot = CallsignTranslations[callsignroot] + end + + if personalized then + -- player personalized callsign + -- remove trailing/leading spaces + shortcallsign=string.gsub(shortcallsign,"^%s*","") + shortcallsign=string.gsub(shortcallsign,"%s*$","") + if Keepnumber then + return shortcallsign -- Ghostrider 219 + elseif ShortCallsign then + callsign = shortcallsign.." "..callnumbermajor -- Ghostrider 9 + else + callsign = shortcallsign.." "..callnumbermajor.." "..callnumberminor -- Ghostrider 9 1 + end + return callsign + end + + -- AI or not personalized + if ShortCallsign then + callsign = callsignroot.." "..callnumbermajor -- Uzi/Victory 9 + else + callsign = callsignroot.." "..callnumbermajor.." "..callnumberminor -- Uzi/Victory 9 1 + end + + --self:I("Generated Callsign = " .. callsign) + end + + return callsign +end + +--- +-- @param #GROUP self +-- @param Wrapper.Group#GROUP CarrierGroup. +-- @param #number Speed Speed in knots. +-- @param #boolean ToKIAS If true, adjust speed to altitude (KIAS). +-- @param #number Altitude Altitude the tanker orbits at in feet. +-- @param #number Delay (optional) Set the task after this many seconds. Defaults to one. +-- @param #number LastWaypoint (optional) Waypoint number of carrier group that when reached, ends the recovery tanker task. +-- @return #GROUP self +function GROUP:SetAsRecoveryTanker(CarrierGroup,Speed,ToKIAS,Altitude,Delay,LastWaypoint) + + local speed = ToKIAS == true and UTILS.KnotsToAltKIAS(Speed,Altitude) or Speed + speed = UTILS.KnotsToMps(speed) + + local alt = UTILS.FeetToMeters(Altitude) + local delay = Delay or 1 + + local task = self:TaskRecoveryTanker(CarrierGroup,speed,alt,LastWaypoint) + + self:SetTask(task,delay) + + local tankertask = self:EnRouteTaskTanker() + self:PushTask(tankertask,delay+2) + + return self +end --- **Wrapper** - UNIT is a wrapper class for the DCS Class Unit. -- -- === @@ -41243,10 +48989,11 @@ 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. --- This is done at the beginning of the mission (when the mission starts), and dynamically when new DCS Unit objects are spawned (using the @{SPAWN} class). +--- For each DCS Unit object alive within a running mission, a UNIT wrapper object (instance) will be created within the global _DATABASE object (an instance of @{Core.Database#DATABASE}). +-- This is done at the beginning of the mission (when the mission starts), and dynamically when new DCS Unit objects are spawned (using the @{Core.Spawn} class). -- -- The UNIT class **does not contain a :New()** method, rather it provides **:Find()** methods to retrieve the object reference -- using the DCS Unit or the DCS UnitName. @@ -41257,10 +49004,10 @@ end -- -- The UNIT class provides the following functions to retrieve quickly the relevant UNIT instance: -- --- * @{#UNIT.Find}(): Find a UNIT instance from the _DATABASE object using a DCS Unit object. --- * @{#UNIT.FindByName}(): Find a UNIT instance from the _DATABASE object using a DCS Unit name. +-- * @{#UNIT.Find}(): Find a UNIT instance from the global _DATABASE object (an instance of @{Core.Database#DATABASE}) using a DCS Unit object. +-- * @{#UNIT.FindByName}(): Find a UNIT instance from the global _DATABASE object (an instance of @{Core.Database#DATABASE}) using a DCS Unit name. -- --- IMPORTANT: ONE SHOULD NEVER SANATIZE these UNIT OBJECT REFERENCES! (make the UNIT object references nil). +-- IMPORTANT: ONE SHOULD NEVER SANITIZE these UNIT OBJECT REFERENCES! (make the UNIT object references nil). -- -- ## DCS UNIT APIs -- @@ -41305,10 +49052,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, } @@ -41321,7 +49069,7 @@ UNIT = { -- Registration. - + --- Create a new UNIT from DCSUnit. -- @param #UNIT self -- @param #string UnitName The name of the DCS unit. @@ -41329,11 +49077,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 ) @@ -41387,8 +49144,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. -- @@ -41417,7 +49194,7 @@ function UNIT:ReSpawnAt( Coordinate, Heading ) SpawnGroupTemplate.y = Coordinate.z self:F( #SpawnGroupTemplate.units ) - for UnitID, UnitData in pairs( SpawnGroup:GetUnits() ) do + for UnitID, UnitData in pairs( SpawnGroup:GetUnits() or {} ) do local GroupUnit = UnitData -- #UNIT self:F( GroupUnit:GetName() ) if GroupUnit:IsAlive() then @@ -41549,6 +49326,8 @@ function UNIT:IsPlayer() -- Get group. local group=self:GetGroup() + + if not group then return false end -- Units of template group. local units=group:GetTemplate().units @@ -41627,6 +49406,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. @@ -41661,7 +49451,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. @@ -41743,41 +49533,100 @@ 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 -- @return Wrapper.Group#GROUP The Group of the Unit or `nil` if the unit does not exist. function UNIT:GetGroup() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitGroup = GROUP:FindByName( DCSUnit:getGroup():getName() ) + self:F2( self.UnitName ) + local UnitGroup = GROUP:FindByName(self.GroupName) + if UnitGroup then return UnitGroup + else + local DCSUnit = self:GetDCSObject() + if DCSUnit then + local grp = DCSUnit:getGroup() + if grp then + local UnitGroup = GROUP:FindByName( grp:getName() ) + return UnitGroup + end + end end - return nil end - --- Need to add here functions to check if radar is on and which object etc. - --- Returns the prefix name of the DCS Unit. A prefix name is a part of the name before a '#'-sign. --- DCS Units spawned with the @{SPAWN} class contain a '#'-sign to indicate the end of the (base) DCS Unit name. +-- DCS Units spawned with the @{Core.Spawn#SPAWN} class contain a '#'-sign to indicate the end of the (base) DCS Unit name. -- The spawn sequence number and unit number are contained within the name after the '#' sign. -- @param #UNIT self -- @return #string The name of the DCS Unit. -- @return #nil The DCS Unit is not existing or alive. function UNIT:GetPrefix() - self:F2( self.UnitName ) + self:F2( self.UnitName ) local DCSUnit = self:GetDCSObject() - + if DCSUnit then - local UnitPrefix = string.match( self.UnitName, ".*#" ):sub( 1, -2 ) - self:T3( UnitPrefix ) - return UnitPrefix + local UnitPrefix = string.match( self.UnitName, ".*#" ):sub( 1, -2 ) + self:T3( UnitPrefix ) + return UnitPrefix end return nil @@ -41818,6 +49667,7 @@ end -- @return #number Number of rockets left. -- @return #number Number of bombs left. -- @return #number Number of missiles left. +-- @return #number Number of artillery shells left (with explosive mass, included in shells; shells can also be machine gun ammo) function UNIT:GetAmmunition() -- Init counter. @@ -41826,6 +49676,7 @@ function UNIT:GetAmmunition() local nrockets=0 local nmissiles=0 local nbombs=0 + local narti=0 local unit=self @@ -41845,8 +49696,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 @@ -41862,7 +49713,11 @@ function UNIT:GetAmmunition() -- Add up all shells. nshells=nshells+Nammo - + + if ammotable[w].desc.warhead and ammotable[w].desc.warhead.explosiveMass and ammotable[w].desc.warhead.explosiveMass > 0 then + narti=narti+Nammo + end + elseif Category==Weapon.Category.ROCKET then -- Add up all rockets. @@ -41874,8 +49729,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 @@ -41884,6 +49740,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 @@ -41894,11 +49754,9 @@ function UNIT:GetAmmunition() -- Total amount of ammunition. nammo=nshells+nrockets+nmissiles+nbombs - return nammo, nshells, nrockets, nbombs, nmissiles + return nammo, nshells, nrockets, nbombs, nmissiles, narti end - - --- Returns the unit sensors. -- @param #UNIT self -- @return DCS#Unit.Sensors Table of sensors. @@ -41947,7 +49805,9 @@ function UNIT:HasSEAD() local HasSEAD = false if UnitSEADAttributes["RADAR_BAND1_FOR_ARM"] and UnitSEADAttributes["RADAR_BAND1_FOR_ARM"] == true or - UnitSEADAttributes["RADAR_BAND2_FOR_ARM"] and UnitSEADAttributes["RADAR_BAND2_FOR_ARM"] == true then + UnitSEADAttributes["RADAR_BAND2_FOR_ARM"] and UnitSEADAttributes["RADAR_BAND2_FOR_ARM"] == true or + UnitSEADAttributes["Optical Tracker"] and UnitSEADAttributes["Optical Tracker"] == true + then HasSEAD = true end return HasSEAD @@ -42184,7 +50044,7 @@ function UNIT:GetThreatLevel() if Descriptor then local Attributes = Descriptor.attributes - + if self:IsGround() then local ThreatLevels = { @@ -42215,7 +50075,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] @@ -42335,24 +50195,24 @@ end -- @return true If the other DCS Unit is within the radius of the 2D point of the DCS Unit. -- @return #nil The DCS Unit is not existing or alive. function UNIT:OtherUnitInRadius( AwaitUnit, Radius ) - self:F2( { self.UnitName, AwaitUnit.UnitName, Radius } ) + self:F2( { self.UnitName, AwaitUnit.UnitName, Radius } ) local DCSUnit = self:GetDCSObject() if DCSUnit then - local UnitVec3 = self:GetVec3() - local AwaitUnitVec3 = AwaitUnit:GetVec3() - - if (((UnitVec3.x - AwaitUnitVec3.x)^2 + (UnitVec3.z - AwaitUnitVec3.z)^2)^0.5 <= Radius) then - self:T3( "true" ) - return true - else - self:T3( "false" ) - return false - end + local UnitVec3 = self:GetVec3() + local AwaitUnitVec3 = AwaitUnit:GetVec3() + + if (((UnitVec3.x - AwaitUnitVec3.x)^2 + (UnitVec3.z - AwaitUnitVec3.z)^2)^0.5 <= Radius) then + self:T3( "true" ) + return true + else + self:T3( "false" ) + return false + end end - return nil + return nil end @@ -42645,7 +50505,17 @@ function UNIT:EnableEmission(switch) return self end ---- **Wrapper** -- CLIENT wraps DCS Unit objects acting as a __Client__ or __Player__ within a mission. + +--- Get skill from Unit. +-- @param #UNIT self +-- @return #string Skill String of skill name. +function UNIT:GetSkill() + self:F2( self.UnitName ) + local name = self.UnitName + local skill = _DATABASE.Templates.Units[name].Template.skill or "Random" + return skill +end +--- **Wrapper** - CLIENT wraps DCS Unit objects acting as a __Client__ or __Player__ within a mission. -- -- === -- @@ -42660,6 +50530,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 @@ -42675,11 +50556,11 @@ end -- * Handles messages to players. -- * Manage the "state" of the DCS Unit. -- --- Clients are being used by the @{MISSION} class to follow players and register their successes. +-- Clients are being used by the @{Tasking.Mission#MISSION} class to follow players and register their successes. -- -- ## CLIENT reference methods -- --- For each DCS Unit having skill level Player or Client, a CLIENT wrapper object (instance) will be created within the _@{DATABASE} object. +-- For each DCS Unit having skill level Player or Client, a CLIENT wrapper object (instance) will be created within the global _DATABASE object (an instance of @{Core.Database#DATABASE}). -- This is done at the beginning of the mission (when the mission starts). -- -- The CLIENT class does not contain a :New() method, rather it provides :Find() methods to retrieve the object reference @@ -42691,10 +50572,10 @@ end -- -- The CLIENT class provides the following functions to retrieve quickly the relevant CLIENT instance: -- --- * @{#CLIENT.Find}(): Find a CLIENT instance from the _DATABASE object using a DCS Unit object. --- * @{#CLIENT.FindByName}(): Find a CLIENT instance from the _DATABASE object using a DCS Unit name. +-- * @{#CLIENT.Find}(): Find a CLIENT instance from the global _DATABASE object (an instance of @{Core.Database#DATABASE}) using a DCS Unit object. +-- * @{#CLIENT.FindByName}(): Find a CLIENT instance from the global _DATABASE object (an instance of @{Core.Database#DATABASE}) using a DCS Unit name. -- --- **IMPORTANT: ONE SHOULD NEVER SANATIZE these CLIENT OBJECT REFERENCES! (make the CLIENT object references nil).** +-- **IMPORTANT: ONE SHOULD NEVER SANITIZE these CLIENT OBJECT REFERENCES! (make the CLIENT object references nil).** -- -- @field #CLIENT CLIENT = { @@ -43198,7 +51079,7 @@ function CLIENT:Message( Message, MessageDuration, MessageCategory, MessageInter end end end ---- **Wrapper** -- STATIC wraps the DCS StaticObject class. +--- **Wrapper** - STATIC wraps the DCS StaticObject class. -- -- === -- @@ -43226,25 +51107,25 @@ end -- -- ## STATIC reference methods -- --- For each DCS Static will have a STATIC wrapper object (instance) within the _@{DATABASE} object. +-- For each DCS Static will have a STATIC wrapper object (instance) within the global _DATABASE object (an instance of @{Core.Database#DATABASE}). -- This is done at the beginning of the mission (when the mission starts). -- --- The STATIC class does not contain a :New() method, rather it provides :Find() methods to retrieve the object reference +-- The @{#STATIC} class does not contain a :New() method, rather it provides :Find() methods to retrieve the object reference -- using the Static Name. -- -- Another thing to know is that STATIC objects do not "contain" the DCS Static object. --- The STATIc methods will reference the DCS Static object by name when it is needed during API execution. +-- The @{#STATIC} methods will reference the DCS Static object by name when it is needed during API execution. -- If the DCS Static object does not exist or is nil, the STATIC methods will return nil and log an exception in the DCS.log file. -- --- The STATIc class provides the following functions to retrieve quickly the relevant STATIC instance: +-- The @{#STATIC} class provides the following functions to retrieve quickly the relevant STATIC instance: -- --- * @{#STATIC.FindByName}(): Find a STATIC instance from the _DATABASE object using a DCS Static name. +-- * @{#STATIC.FindByName}(): Find a STATIC instance from the global _DATABASE object (an instance of @{Core.Database#DATABASE}) using a DCS Static name. -- --- IMPORTANT: ONE SHOULD NEVER SANATIZE these STATIC OBJECT REFERENCES! (make the STATIC object references nil). +-- IMPORTANT: ONE SHOULD NEVER SANITIZE these STATIC OBJECT REFERENCES! (make the STATIC object references nil). -- -- @field #STATIC STATIC = { - ClassName = "STATIC", + ClassName = "STATIC", } @@ -43255,9 +51136,33 @@ STATIC = { function STATIC:Register( StaticName ) local self = BASE:Inherit( self, POSITIONABLE:New( StaticName ) ) self.StaticName = StaticName + + local DCSStatic = StaticObject.getByName( self.StaticName ) + if DCSStatic then + local Life0 = DCSStatic:getLife() or 1 + self.Life0 = Life0 + end + return self end +--- Get initial life points +-- @param #STATIC self +-- @return #number lifepoints +function STATIC:GetLife0() + return self.Life0 or 1 +end + +--- Get current life points +-- @param #STATIC self +-- @return #number lifepoints or nil +function STATIC:GetLife() + local DCSStatic = StaticObject.getByName( self.StaticName ) + if DCSStatic then + return DCSStatic:getLife() or 1 + end + return nil +end --- Finds a STATIC from the _DATABASE using a DCSStatic object. -- @param #STATIC self @@ -43285,7 +51190,7 @@ function STATIC:FindByName( StaticName, RaiseError ) self.StaticName = StaticName if StaticFound then - return StaticFound + return StaticFound end if RaiseError == nil or RaiseError == true then @@ -43362,9 +51267,9 @@ function STATIC:GetDCSObject() return nil end ---- Returns a list of one @{Static}. +--- Returns a list of one @{Wrapper.Static}. -- @param #STATIC self --- @return #list A list of one @{Static}. +-- @return #list A list of one @{Wrapper.Static}. function STATIC:GetUnits() self:F2( { self.StaticName } ) local DCSStatic = self:GetDCSObject() @@ -43456,8 +51361,7 @@ function STATIC:ReSpawnAt(Coordinate, Heading, Delay) return self end - ---- **Wrapper** -- AIRBASE is a wrapper class to handle the DCS Airbase objects. +--- **Wrapper** - AIRBASE is a wrapper class to handle the DCS Airbase objects. -- -- === -- @@ -43476,6 +51380,7 @@ end -- @field #table CategoryName Names of airbase categories. -- @field #string AirbaseName Name of the airbase. -- @field #number AirbaseID Airbase ID. +-- @field Core.Zone#ZONE AirbaseZone Circular zone around the airbase with a radius of 2500 meters. For ships this is a ZONE_UNIT object. -- @field #number category Airbase category. -- @field #table descriptors DCS descriptors. -- @field #boolean isAirdrome Airbase is an airdrome. @@ -43483,9 +51388,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: @@ -43495,7 +51402,7 @@ end -- -- ## AIRBASE reference methods -- --- For each DCS Airbase object alive within a running mission, a AIRBASE wrapper object (instance) will be created within the _@{DATABASE} object. +-- For each DCS Airbase object alive within a running mission, a AIRBASE wrapper object (instance) will be created within the global _DATABASE object (an instance of @{Core.Database#DATABASE}). -- This is done at the beginning of the mission (when the mission starts). -- -- The AIRBASE class **does not contain a :New()** method, rather it provides **:Find()** methods to retrieve the object reference @@ -43507,10 +51414,10 @@ end -- -- The AIRBASE class provides the following functions to retrieve quickly the relevant AIRBASE instance: -- --- * @{#AIRBASE.Find}(): Find a AIRBASE instance from the _DATABASE object using a DCS Airbase object. --- * @{#AIRBASE.FindByName}(): Find a AIRBASE instance from the _DATABASE object using a DCS Airbase name. +-- * @{#AIRBASE.Find}(): Find a AIRBASE instance from the global _DATABASE object (an instance of @{Core.Database#DATABASE}) using a DCS Airbase object. +-- * @{#AIRBASE.FindByName}(): Find a AIRBASE instance from the global _DATABASE object (an instance of @{Core.Database#DATABASE}) using a DCS Airbase name. -- --- IMPORTANT: ONE SHOULD NEVER SANATIZE these AIRBASE OBJECT REFERENCES! (make the AIRBASE object references nil). +-- IMPORTANT: ONE SHOULD NEVER SANITIZE these AIRBASE OBJECT REFERENCES! (make the AIRBASE object references nil). -- -- ## DCS Airbase APIs -- @@ -43521,14 +51428,14 @@ end -- -- @field #AIRBASE AIRBASE AIRBASE = { - ClassName="AIRBASE", + ClassName = "AIRBASE", CategoryName = { - [Airbase.Category.AIRDROME] = "Airdrome", - [Airbase.Category.HELIPAD] = "Helipad", - [Airbase.Category.SHIP] = "Ship", - }, - activerwyno=nil, - } + [Airbase.Category.AIRDROME] = "Airdrome", + [Airbase.Category.HELIPAD] = "Helipad", + [Airbase.Category.SHIP] = "Ship", + }, + activerwyno = nil, +} --- Enumeration to identify the airbases in the Caucasus region. -- @@ -43579,7 +51486,7 @@ AIRBASE.Caucasus = { ["Nalchik"] = "Nalchik", ["Mozdok"] = "Mozdok", ["Beslan"] = "Beslan", - } +} --- Airbases of the Nevada map: -- @@ -43595,32 +51502,32 @@ 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: -- @@ -43702,7 +51609,7 @@ AIRBASE.Normandy = { -- -- * AIRBASE.PersianGulf.Abu_Dhabi_International_Airport -- * AIRBASE.PersianGulf.Abu_Musa_Island_Airport --- * AIRBASE.PersianGulf.Al-Bateen_Airport +-- * AIRBASE.PersianGulf.Al_Bateen_Airport -- * AIRBASE.PersianGulf.Al_Ain_International_Airport -- * AIRBASE.PersianGulf.Al_Dhafra_AB -- * AIRBASE.PersianGulf.Al_Maktoum_Intl @@ -43721,7 +51628,7 @@ AIRBASE.Normandy = { -- * AIRBASE.PersianGulf.Lavan_Island_Airport -- * AIRBASE.PersianGulf.Liwa_Airbase -- * AIRBASE.PersianGulf.Qeshm_Island --- * AIRBASE.PersianGulf.Ras_Al_Khaimah_International_Airport +-- * AIRBASE.PersianGulf.Ras_Al_Khaimah -- * AIRBASE.PersianGulf.Sas_Al_Nakheel_Airport -- * AIRBASE.PersianGulf.Sharjah_Intl -- * AIRBASE.PersianGulf.Shiraz_International_Airport @@ -43774,7 +51681,10 @@ 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 = { ["Abbeville_Drucat"] = "Abbeville Drucat", @@ -43786,6 +51696,9 @@ AIRBASE.TheChannel = { ["Lympne"] = "Lympne", ["Detling"] = "Detling", ["High_Halden"] = "High Halden", + ["Biggin_Hill"] = "Biggin Hill", + ["Eastchurch"] = "Eastchurch", + ["Headcorn"] = "Headcorn", } --- Airbases of the Syria map: @@ -43804,7 +51717,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 @@ -43822,7 +51734,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 @@ -43843,6 +51754,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={ @@ -43860,7 +51782,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", @@ -43898,10 +51820,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 @@ -43911,16 +51842,70 @@ AIRBASE.Syria={ -- * AIRBASE.MarianaIslands.Tinian_Intl -- * AIRBASE.MarianaIslands.Olf_Orote -- ---@field MarianaIslands -AIRBASE.MarianaIslands={ - ["Rota_Intl"]="Rota Intl", - ["Andersen_AFB"]="Andersen AFB", - ["Antonio_B_Won_Pat_Intl"]="Antonio B. Won Pat Intl", - ["Saipan_Intl"]="Saipan Intl", - ["Tinian_Intl"]="Tinian Intl", - ["Olf_Orote"]="Olf Orote", +-- @field MarianaIslands +AIRBASE.MarianaIslands = { + ["Rota_Intl"] = "Rota Intl", + ["Andersen_AFB"] = "Andersen AFB", + ["Antonio_B_Won_Pat_Intl"] = "Antonio B. Won Pat Intl", + ["Saipan_Intl"] = "Saipan Intl", + ["Tinian_Intl"] = "Tinian Intl", + ["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 +-- * AIRBASE.SouthAtlantic.Puerto_Williams +-- * AIRBASE.SouthAtlantic.Puerto_Natales +-- * AIRBASE.SouthAtlantic.El_Calafate +-- * AIRBASE.SouthAtlantic.Puerto_Santa_Cruz +-- * AIRBASE.SouthAtlantic.Comandante_Luis_Piedrabuena +-- * AIRBASE.SouthAtlantic.Aerodromo_De_Tolhuin +-- * AIRBASE.SouthAtlantic.Porvenir_Airfield +-- * AIRBASE.SouthAtlantic.Almirante_Schroeders +-- * AIRBASE.SouthAtlantic.Rio_Turbio +-- * AIRBASE.SouthAtlantic.Rio_Chico +-- * AIRBASE.SouthAtlantic.Franco_Bianco +-- * AIRBASE.SouthAtlantic.Goose_Green +-- * AIRBASE.SouthAtlantic.Hipico +-- * AIRBASE.SouthAtlantic.CaletaTortel +-- +--@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", + ["Puerto_Williams"]="Puerto Williams", + ["Puerto_Natales"]="Puerto Natales", + ["El_Calafate"]="El Calafate", + ["Puerto_Santa_Cruz"]="Puerto Santa Cruz", + ["Comandante_Luis_Piedrabuena"]="Comandante Luis Piedrabuena", + ["Aerodromo_De_Tolhuin"]="Aerodromo De Tolhuin", + ["Porvenir_Airfield"]="Porvenir Airfield", + ["Almirante_Schroeders"]="Almirante Schroeders", + ["Rio_Turbio"]="Rio Turbio", + ["Rio_Chico"] = "Rio Chico", + ["Franco_Bianco"] = "Franco Bianco", + ["Goose_Green"] = "Goose Green", + ["Hipico"] = "Hipico", + ["CaletaTortel"] = "CaletaTortel", +} --- AIRBASE.ParkingSpot ".Coordinate, ".TerminalID", ".TerminalType", ".TOAC", ".Free", ".TerminalID0", ".DistToRwy". -- @type AIRBASE.ParkingSpot @@ -43931,6 +51916,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 -- @@ -43965,13 +51958,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 @@ -43985,19 +51995,22 @@ function AIRBASE:Register(AirbaseName) -- Inherit everything from positionable. local self=BASE:Inherit(self, POSITIONABLE:New(AirbaseName)) --#AIRBASE - + -- Set airbase name. self.AirbaseName=AirbaseName - + -- Set airbase ID. self.AirbaseID=self:GetID(true) - + -- 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 - + -- Set category. if self.category==Airbase.Category.AIRDROME then self.isAirdrome=true @@ -44005,23 +52018,49 @@ function AIRBASE:Register(AirbaseName) self.isHelipad=true elseif self.category==Airbase.Category.SHIP then self.isShip=true + -- DCS bug: Oil rigs and gas platforms have category=2 (ship). Also they cannot be retrieved by coalition.getStaticObjects() + if self.descriptors.typeName=="Oil rig" or self.descriptors.typeName=="Ga" then + self.isHelipad=true + self.isShip=false + self.category=Airbase.Category.HELIPAD + _DATABASE:AddStatic(AirbaseName) + end else self:E("ERROR: Unknown airbase category!") end - self:_InitParkingSpots() + -- 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. self:GetCoordinate() - + if vec2 then - -- TODO: For ships we need a moving zone. - self.AirbaseZone=ZONE_RADIUS:New( AirbaseName, vec2, 2500 ) + if self.isShip then + local unit=UNIT:FindByName(AirbaseName) + if unit then + self.AirbaseZone=ZONE_UNIT:New(AirbaseName, unit, 2500) + end + else + self.AirbaseZone=ZONE_RADIUS:New(AirbaseName, vec2, 2500) + end else self:E(string.format("ERROR: Cound not get position Vec2 of airbase %s", AirbaseName)) end + + -- Debug info. + self:T2(string.format("Registered airbase %s", tostring(self.AirbaseName))) return self end @@ -44152,7 +52191,7 @@ function AIRBASE:GetID(unique) local airbaseID=tonumber(DCSAirbase:getID()) local airbaseCategory=self:GetAirbaseCategory() - + if AirbaseName==self.AirbaseName then if airbaseCategory==Airbase.Category.SHIP or airbaseCategory==Airbase.Category.HELIPAD then -- Ships get a negative sign as their unit number might be the same as the ID of another airbase. @@ -44173,7 +52212,7 @@ end -- Black listed spots overrule white listed spots. -- **NOTE** that terminal IDs are not necessarily the same as those displayed in the mission editor! -- @param #AIRBASE self --- @param #table TerminalIdBlacklist Table of white listed terminal IDs. +-- @param #table TerminalIdWhitelist Table of white listed terminal IDs. -- @return #AIRBASE self -- @usage AIRBASE:FindByName("Batumi"):SetParkingSpotWhitelist({2, 3, 4}) --Only allow terminal IDs 2, 3, 4 function AIRBASE:SetParkingSpotWhitelist(TerminalIdWhitelist) @@ -44217,6 +52256,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 @@ -44391,16 +52466,31 @@ function AIRBASE:_InitParkingSpots() -- Init table. self.parking={} self.parkingByID={} - + self.NparkingTotal=0 self.NparkingTerminal={} for _,terminalType in pairs(AIRBASE.TerminalType) do self.NparkingTerminal[terminalType]=0 - end + 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 - + -- New parking spot. local park={} --#AIRBASE.ParkingSpot park.Vec3=spot.vTerminalPos @@ -44411,15 +52501,17 @@ 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 - + for _,terminalType in pairs(AIRBASE.TerminalType) do if self._CheckTerminalType(terminalType, park.TerminalType) then self.NparkingTerminal[terminalType]=self.NparkingTerminal[terminalType]+1 end - end - + end + self.parkingByID[park.TerminalID]=park table.insert(self.parking, park) end @@ -44443,7 +52535,7 @@ function AIRBASE:GetParkingSpotsTable(termtype) -- Get parking data of all spots (free or occupied) local parkingdata=self:GetParkingData(false) - + -- Get parking data of all free spots. local parkingfree=self:GetParkingData(true) @@ -44460,17 +52552,27 @@ function AIRBASE:GetParkingSpotsTable(termtype) -- Put coordinates of parking spots into table. local spots={} for _,_spot in pairs(parkingdata) do - + if AIRBASE._CheckTerminalType(_spot.Term_Type, termtype) then - + local spot=self:_GetParkingSpotByID(_spot.Term_Index) - - spot.Free=_isfree(_spot) -- updated - spot.TOAC=_spot.TO_AC -- updated - - table.insert(spots, spot) + + if spot then + + spot.Free=_isfree(_spot) -- updated + spot.TOAC=_spot.TO_AC -- updated + spot.AirbaseName=self.AirbaseName + + table.insert(spots, spot) + + else + + self:E(string.format("ERROR: Parking spot %s is nil!", tostring(_spot.Term_Index))) + + end + end - + end return spots @@ -44491,14 +52593,15 @@ function AIRBASE:GetFreeParkingSpotsTable(termtype, allowTOAC) for _,_spot in pairs(parkingfree) do if AIRBASE._CheckTerminalType(_spot.Term_Type, termtype) and _spot.Term_Index>0 then if (allowTOAC and allowTOAC==true) or _spot.TO_AC==false then - + local spot=self:_GetParkingSpotByID(_spot.Term_Index) spot.Free=true -- updated spot.TOAC=_spot.TO_AC -- updated - + spot.AirbaseName=self.AirbaseName + table.insert(freespots, spot) - + end end end @@ -44543,7 +52646,7 @@ function AIRBASE:MarkParkingSpots(termtype, mark) -- Get airbase name. local airbasename=self:GetName() - self:E(string.format("Parking spots at %s for termial type %s:", airbasename, tostring(termtype))) + self:E(string.format("Parking spots at %s for terminal type %s:", airbasename, tostring(termtype))) for _,_spot in pairs(parkingdata) do @@ -44620,14 +52723,25 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, parkingdata=parkingdata or self:GetParkingSpotsTable(terminaltype) -- Get the aircraft size, i.e. it's longest side of x,z. - local aircraft=group:GetUnit(1) - local _aircraftsize, ax,ay,az=aircraft:GetObjectSize() + local aircraft = nil -- fix local problem below + local _aircraftsize, ax,ay,az + if group and group.ClassName == "GROUP" then + aircraft=group:GetUnit(1) + _aircraftsize, ax,ay,az=aircraft:GetObjectSize() + else + -- SU27 dimensions + _aircraftsize = 23 + ax = 23 -- length + ay = 7 -- height + 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() -- Debug info. - self:E(string.format("%s: Looking for %d parking spot(s) for aircraft of size %.1f m (x=%.1f,y=%.1f,z=%.1f) at termial type %s.", airport, _nspots, _aircraftsize, ax, ay, az, tostring(terminaltype))) + self:T(string.format("%s: Looking for %d parking spot(s) for aircraft of size %.1f m (x=%.1f,y=%.1f,z=%.1f) at terminal type %s.", airport, _nspots, _aircraftsize, ax, ay, az, tostring(terminaltype))) -- Table of valid spots. local validspots={} @@ -44729,14 +52843,14 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, --_spot:MarkToAll(string.format("Parking spot %d free=%s", parkingspot.TerminalID, tostring(not occupied))) if occupied then - self:I(string.format("%s: Parking spot id %d occupied.", airport, _termid)) + self:T(string.format("%s: Parking spot id %d occupied.", airport, _termid)) else - self:I(string.format("%s: Parking spot id %d free.", airport, _termid)) + self:T(string.format("%s: Parking spot id %d free.", airport, _termid)) if nvalid<_nspots then table.insert(validspots, {Coordinate=_spot, TerminalID=_termid}) end nvalid=nvalid+1 - self:I(string.format("%s: Parking spot id %d free. Nfree=%d/%d.", airport, _termid, nvalid,_nspots)) + self:T(string.format("%s: Parking spot id %d free. Nfree=%d/%d.", airport, _termid, nvalid,_nspots)) end end -- loop over units @@ -44750,6 +52864,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. @@ -44768,7 +52883,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 @@ -44835,6 +52950,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. @@ -44851,7 +53191,7 @@ function AIRBASE:GetRunwayData(magvar, mark) -- Get spawn points on runway. These can be used to determine the runway heading. local runwaycoords=self:GetParkingSpotsCoordinates(AIRBASE.TerminalType.Runway) - + -- Debug: For finding the numbers of the spawn points belonging to each runway. if false then for i,_coord in pairs(runwaycoords) do @@ -44870,7 +53210,7 @@ function AIRBASE:GetRunwayData(magvar, mark) -- Airbase name. local name=self:GetName() - + -- Exceptions if name==AIRBASE.Nevada.Jean_Airport or @@ -44878,41 +53218,41 @@ 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 exception=1 - - elseif UTILS.GetDCSMap()==DCSMAP.Syria and N>=2 and + + elseif UTILS.GetDCSMap()==DCSMAP.Syria and N>=2 and name~=AIRBASE.Syria.Minakh and name~=AIRBASE.Syria.Damascus and name~=AIRBASE.Syria.Khalkhalah and name~=AIRBASE.Syria.Marj_Ruhayyil and name~=AIRBASE.Syria.Beirut_Rafic_Hariri then - + -- 1-->3, 2-->4, 3-->1, 4-->2 exception=2 - + end - + --- Function returning the index of the runway coordinate belonding to the given index i. local function f(i) local j - + if exception==1 then - + j=N-(i-1) -- 1-->4, 2-->3 - + elseif exception==2 then - + if i<=N2 then j=i+N2 -- 1-->3, 2-->4 else j=i-N2 -- 3-->1, 4-->3 end - + else if i%2==0 then @@ -44920,9 +53260,9 @@ function AIRBASE:GetRunwayData(magvar, mark) else j=i+1 -- odd 1-->2, 3-->4 end - + end - + -- Special case where there is no obvious order. if name==AIRBASE.Syria.Beirut_Rafic_Hariri then if i==1 then @@ -44955,7 +53295,7 @@ function AIRBASE:GetRunwayData(magvar, mark) j=2 end end - + return j end @@ -44964,7 +53304,7 @@ function AIRBASE:GetRunwayData(magvar, mark) -- Get the other spawn point coordinate. local j=f(i) - + -- Debug info. --env.info(string.format("Runway i=%s j=%s (N=%d #runwaycoord=%d)", tostring(i), tostring(j), N, #runwaycoords)) @@ -45002,26 +53342,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) + + local runway=self:GetRunwayByName(Name) + + if not runway then + runway=self:GetRunwayIntoWind(PreferLeft) + end + + if runway then + self:T(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 runways data (initialize if necessary). - local runways=self:GetRunwayData(magvar) +--- Get the active runway for takeoff. +-- @param #AIRBASE self +-- @return #AIRBASE.Runway The active runway for takeoff. +function AIRBASE:GetActiveRunwayTakeoff() + return self.runwayTakeoff +end - -- Return user forced active runway if it was set. - if self.activerwyno then - return runways[self.activerwyno] + +--- 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:T(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() @@ -45042,33 +53456,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= 1 and true or false +end + +--- Check if SCENERY Object is dead. +--@param #SCENERY self +--@return #number life +function SCENERY:IsDead() + return self:GetLife() < 1 and true or false +end + +--- Get the threat level of a SCENERY object. Always 0. --@param #SCENERY self --@return #number Threat level 0. --@return #string "Scenery". function SCENERY:GetThreatLevel() return 0, "Scenery" end ---- **Wrapper** - Markers On the F10 map. + +--- Find a SCENERY object from its name or id. Since SCENERY isn't registered in the Moose database (just too many objects per map), we need to do a scan first +-- to find the correct object. +--@param #SCENERY self +--@param #string Name The name/id of the scenery object as taken from the ME. Ex. '595785449' +--@param Core.Point#COORDINATE Coordinate Where to find the scenery object +--@param #number Radius (optional) Search radius around coordinate, defaults to 100 +--@return #SCENERY Scenery Object or `nil` if it cannot be found +function SCENERY:FindByName(Name, Coordinate, Radius) + + local radius = Radius or 100 + local name = Name or "unknown" + local scenery = nil + + --- + -- @param Core.Point#COORDINATE coordinate + -- @param #number radius + -- @param #string name + local function SceneryScan(coordinate, radius, name) + if coordinate ~= nil then + local scenerylist = coordinate:ScanScenery(radius) + local rscenery = nil + for _,_scenery in pairs(scenerylist) do + local scenery = _scenery -- Wrapper.Scenery#SCENERY + if tostring(scenery.SceneryName) == tostring(name) then + rscenery = scenery + break + end + end + return rscenery + end + return nil + end + + if Coordinate then + scenery = SceneryScan(Coordinate, radius, name) + end + + return scenery +end + +--- Find a SCENERY object from its name or id. Since SCENERY isn't registered in the Moose database (just too many objects per map), we need to do a scan first +-- to find the correct object. +--@param #SCENERY self +--@param #string Name The name or id of the scenery object as taken from the ME. Ex. '595785449' +--@param Core.Zone#ZONE Zone Where to find the scenery object. Can be handed as zone name. +--@param #number Radius (optional) Search radius around coordinate, defaults to 100 +--@return #SCENERY Scenery Object or `nil` if it cannot be found +function SCENERY:FindByNameInZone(Name, Zone, Radius) + local radius = Radius or 100 + local name = Name or "unknown" + if type(Zone) == "string" then + Zone = ZONE:FindByName(Zone) + end + local coordinate = Zone:GetCoordinate() + return self:FindByName(Name,coordinate,Radius) +end + +--- Find a SCENERY object from its zone name. Since SCENERY isn't registered in the Moose database (just too many objects per map), we need to do a scan first +-- to find the correct object. +--@param #SCENERY self +--@param #string ZoneName The name of the scenery zone as created with a right-click on the map in the mission editor and select "assigned to...". Can be handed over as ZONE object. +--@return #SCENERY First found Scenery Object or `nil` if it cannot be found +function SCENERY:FindByZoneName( ZoneName ) + local zone = ZoneName -- Core.Zone#ZONE + if type(ZoneName) == "string" then + zone = ZONE:FindByName(ZoneName) + end + local _id = zone:GetProperty('OBJECT ID') + if not _id then + -- this zone has no object ID + BASE:E("**** Zone without object ID: "..ZoneName.." | Type: "..tostring(zone.ClassName)) + if string.find(zone.ClassName,"POLYGON") then + zone:Scan({Object.Category.SCENERY}) + local scanned = zone:GetScannedScenery() + for _,_scenery in (scanned) do + local scenery = _scenery -- Wrapper.Scenery#SCENERY + if scenery:IsAlive() then + return scenery + end + end + return nil + else + local coordinate = zone:GetCoordinate() + local scanned = coordinate:ScanScenery() + for _,_scenery in (scanned) do + local scenery = _scenery -- Wrapper.Scenery#SCENERY + if scenery:IsAlive() then + return scenery + end + end + return nil + end + else + return self:FindByName(_id, zone:GetCoordinate()) + end +end + +--- Scan and find all SCENERY objects from a zone by zone-name. Since SCENERY isn't registered in the Moose database (just too many objects per map), we need to do a scan first +-- to find the correct object. +--@param #SCENERY self +--@param #string ZoneName The name of the zone, can be handed as ZONE_RADIUS or ZONE_POLYGON object +--@return #table of SCENERY Objects, or `nil` if nothing found +function SCENERY:FindAllByZoneName( ZoneName ) + local zone = ZoneName -- Core.Zone#ZONE_RADIUS + if type(ZoneName) == "string" then + zone = ZONE:FindByName(ZoneName) + end + local _id = zone:GetProperty('OBJECT ID') + if not _id then + -- this zone has no object ID + --BASE:E("**** Zone without object ID: "..ZoneName.." | Type: "..tostring(zone.ClassName)) + zone:Scan({Object.Category.SCENERY}) + local scanned = zone:GetScannedSceneryObjects() + if #scanned > 0 then + return scanned + else + return nil + end + else + local obj = self:FindByName(_id, zone:GetCoordinate()) + if obj then + return {obj} + else + return nil + end + end +end + +--- SCENERY objects cannot be destroyed via the API (at the punishment of game crash). +--@param #SCENERY self +--@return #SCENERY self +function SCENERY:Destroy() + return self +end--- **Wrapper** - Markers On the F10 map. -- -- **Main Features:** -- @@ -45212,8 +53831,7 @@ end -- -- ### Author: **funkyfranky** -- @module Wrapper.Marker --- @image Wrapper_Marker.png - +-- @image MOOSE_Core.JPG --- Marker class. -- @type MARKER @@ -45223,7 +53841,7 @@ end -- @field #number mid Marker ID. -- @field Core.Point#COORDINATE coordinate Coordinate of the mark. -- @field #string text Text displayed in the mark panel. --- @field #string message Message dispayed when the mark is added. +-- @field #string message Message displayed when the mark is added. -- @field #boolean readonly Marker is read-only. -- @field #number coalition Coalition to which the marker is displayed. -- @extends Core.Fsm#FSM @@ -45241,62 +53859,60 @@ end -- # Create a Marker -- -- -- Create a MARKER object at Batumi with a trivial text. --- local Coordinate=AIRBASE:FindByName("Batumi"):GetCoordinate() --- mymarker=MARKER:New(Coordinate, "I am Batumi Airfield") +-- local Coordinate = AIRBASE:FindByName( "Batumi" ):GetCoordinate() +-- mymarker = MARKER:New( Coordinate, "I am Batumi Airfield" ) -- --- Now this does **not** show the marker yet. We still need to specifiy to whom it is shown. There are several options, i.e. --- show the marker to everyone, to a speficic coaliton only, or only to a specific group. +-- Now this does **not** show the marker yet. We still need to specify to whom it is shown. There are several options, i.e. +-- show the marker to everyone, to a specific coalition only, or only to a specific group. -- -- ## For Everyone -- -- If the marker should be visible to everyone, you can use the :ToAll() function. -- --- mymarker=MARKER:New(Coordinate, "I am Batumi Airfield"):ToAll() +-- mymarker = MARKER:New( Coordinate, "I am Batumi Airfield" ):ToAll() -- --- ## For a Coaliton +-- ## For a Coalition -- -- If the maker should be visible to a specific coalition, you can use the :ToCoalition() function. -- --- mymarker=MARKER:New(Coordinate, "I am Batumi Airfield"):ToCoaliton(coaliton.side.BLUE) --- --- ### To Blue Coaliton --- --- ### To Red Coalition --- --- This would show the marker only to the Blue coaliton. +-- mymarker = MARKER:New( Coordinate , "I am Batumi Airfield" ):ToCoalition( coalition.side.BLUE ) +-- +-- This would show the marker only to the Blue coalition. -- -- ## For a Group -- +-- mymarker = MARKER:New( Coordinate , "Target Location" ):ToGroup( tankGroup ) -- -- # Removing a Marker --- +-- mymarker:Remove(60) +-- This removes the marker after 60 seconds -- -- # Updating a Marker -- -- The marker text and coordinate can be updated easily as shown below. -- --- However, note that **updateing involves to remove and recreate the marker if either text or its coordinate is changed**. +-- However, note that **updating involves to remove and recreate the marker if either text or its coordinate is changed**. -- *This is a DCS scripting engine limitation.* -- -- ## Update Text -- --- If you created a marker "mymarker" as shown above, you can update the dispayed test by +-- If you created a marker "mymarker" as shown above, you can update the displayed test by -- --- mymarker:UpdateText("I am the new text at Batumi") +-- mymarker:UpdateText( "I am the new text at Batumi" ) -- -- The update can also be delayed by, e.g. 90 seconds, using -- --- mymarker:UpdateText("I am the new text at Batumi", 90) +-- mymarker:UpdateText( "I am the new text at Batumi", 90 ) -- -- ## Update Coordinate -- -- If you created a marker "mymarker" as shown above, you can update its coordinate on the F10 map by -- --- mymarker:UpdateCoordinate(NewCoordinate) +-- mymarker:UpdateCoordinate( NewCoordinate ) -- -- The update can also be delayed by, e.g. 60 seconds, using -- --- mymarker:UpdateCoordinate(NewCoordinate, 60) +-- mymarker:UpdateCoordinate( NewCoordinate , 60 ) -- -- # Retrieve Data -- @@ -45304,18 +53920,18 @@ end -- -- ## Text -- --- local text=mymarker:GetText() --- env.info("Marker Text = " .. text) +-- local text =mymarker:GetText() +-- env.info( "Marker Text = " .. text ) -- -- ## Coordinate -- --- local Coordinate=mymarker:GetCoordinate() --- env.info("Marker Coordinate LL DSM = " .. Coordinate:ToStringLLDMS()) +-- local Coordinate = mymarker:GetCoordinate() +-- env.info( "Marker Coordinate LL DSM = " .. Coordinate:ToStringLLDMS() ) -- -- -- # FSM Events -- --- Moose creates addditonal events, so called FSM event, when markers are added, changed, removed, and text or the coordianteis updated. +-- Moose creates additional events, so called FSM event, when markers are added, changed, removed, and text or the coordinate is updated. -- -- These events can be captured and used for processing via OnAfter functions as shown below. -- @@ -45332,26 +53948,25 @@ end -- -- # Examples -- --- -- @field #MARKER MARKER = { - ClassName = "MARKER", - Debug = false, - lid = nil, - mid = nil, - coordinate = nil, - text = nil, - message = nil, - readonly = nil, - coalition = nil, + ClassName = "MARKER", + Debug = false, + lid = nil, + mid = nil, + coordinate = nil, + text = nil, + message = nil, + readonly = nil, + coalition = nil, } --- Marker ID. Running number. -_MARKERID=0 +_MARKERID = 0 --- Marker class version. -- @field #string version -MARKER.version="0.1.0" +MARKER.version="0.1.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -45371,38 +53986,38 @@ MARKER.version="0.1.0" -- @param Core.Point#COORDINATE Coordinate Coordinate where to place the marker. -- @param #string Text Text displayed on the mark panel. -- @return #MARKER self -function MARKER:New(Coordinate, Text) +function MARKER:New( Coordinate, Text ) -- Inherit everything from FSM class. - local self=BASE:Inherit(self, FSM:New()) -- #MARKER + local self = BASE:Inherit( self, FSM:New() ) -- #MARKER - self.coordinate=Coordinate + self.coordinate=UTILS.DeepCopy(Coordinate) - self.text=Text + self.text = Text -- Defaults - self.readonly=false - self.message="" + self.readonly = false + self.message = "" -- New marker ID. This is not the one of the actual marker. - _MARKERID=_MARKERID+1 + _MARKERID = _MARKERID + 1 - self.myid=_MARKERID + self.myid = _MARKERID -- Log ID. - self.lid=string.format("Marker #%d | ", self.myid) + self.lid = string.format( "Marker #%d | ", self.myid ) -- Start State. - self:SetStartState("Invisible") + self:SetStartState( "Invisible" ) -- Add FSM transitions. -- From State --> Event --> To State - self:AddTransition("Invisible", "Added", "Visible") -- Marker was added. - self:AddTransition("Visible", "Removed", "Invisible") -- Marker was removed. - self:AddTransition("*", "Changed", "*") -- Marker was changed. + self:AddTransition( "Invisible", "Added", "Visible" ) -- Marker was added. + self:AddTransition( "Visible", "Removed", "Invisible" ) -- Marker was removed. + self:AddTransition( "*", "Changed", "*" ) -- Marker was changed. - self:AddTransition("*", "TextUpdate", "*") -- Text updated. - self:AddTransition("*", "CoordUpdate", "*") -- Coordinates updated. + self:AddTransition( "*", "TextUpdate", "*" ) -- Text updated. + self:AddTransition( "*", "CoordUpdate", "*" ) -- Coordinates updated. --- Triggers the FSM event "Added". -- @function [parent=#MARKER] Added @@ -45422,7 +54037,6 @@ function MARKER:New(Coordinate, Text) -- @param #string To To state. -- @param Core.Event#EVENTDATA EventData Event data table. - --- Triggers the FSM event "Removed". -- @function [parent=#MARKER] Removed -- @param #MARKER self @@ -45441,7 +54055,6 @@ function MARKER:New(Coordinate, Text) -- @param #string To To state. -- @param Core.Event#EVENTDATA EventData Event data table. - --- Triggers the FSM event "Changed". -- @function [parent=#MARKER] Changed -- @param #MARKER self @@ -45460,7 +54073,6 @@ function MARKER:New(Coordinate, Text) -- @param #string To To state. -- @param Core.Event#EVENTDATA EventData Event data table. - --- Triggers the FSM event "TextUpdate". -- @function [parent=#MARKER] TextUpdate -- @param #MARKER self @@ -45479,7 +54091,6 @@ function MARKER:New(Coordinate, Text) -- @param #string To To state. -- @param #string Text The new text. - --- Triggers the FSM event "CoordUpdate". -- @function [parent=#MARKER] CoordUpdate -- @param #MARKER self @@ -45498,11 +54109,10 @@ function MARKER:New(Coordinate, Text) -- @param #string To To state. -- @param Core.Point#COORDINATE Coordinate The updated Coordinate. - -- Handle events. - self:HandleEvent(EVENTS.MarkAdded) - self:HandleEvent(EVENTS.MarkRemoved) - self:HandleEvent(EVENTS.MarkChange) + self:HandleEvent( EVENTS.MarkAdded ) + self:HandleEvent( EVENTS.MarkRemoved ) + self:HandleEvent( EVENTS.MarkChange ) return self end @@ -45511,12 +54121,22 @@ end -- User API Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Marker is readonly. Text cannot be changed and marker cannot be removed. +--- Marker is readonly. Text cannot be changed and marker cannot be removed. The will not update the marker in the game, Call MARKER:Refresh to update state. -- @param #MARKER self -- @return #MARKER self function MARKER:ReadOnly() - self.readonly=true + self.readonly = true + + return self +end + +--- Marker is read and write. Text cannot be changed and marker cannot be removed. The will not update the marker in the game, Call MARKER:Refresh to update state. +-- @param #MARKER self +-- @return #MARKER self +function MARKER:ReadWrite() + + self.readonly=false return self end @@ -45525,9 +54145,9 @@ end -- @param #MARKER self -- @param #string Text Message displayed when the marker is added. -- @return #MARKER self -function MARKER:Message(Text) +function MARKER:Message( Text ) - self.message=Text or "" + self.message = Text or "" return self end @@ -45536,28 +54156,28 @@ end -- @param #MARKER self -- @param #number Delay (Optional) Delay in seconds, before the marker is created. -- @return #MARKER self -function MARKER:ToAll(Delay) +function MARKER:ToAll( Delay ) - if Delay and Delay>0 then - self:ScheduleOnce(Delay, MARKER.ToAll, self) + if Delay and Delay > 0 then + self:ScheduleOnce( Delay, MARKER.ToAll, self ) else - self.toall=true - self.tocoaliton=nil - self.coalition=nil - self.togroup=nil - self.groupname=nil - self.groupid=nil + self.toall = true + self.tocoalition = nil + self.coalition = nil + self.togroup = nil + self.groupname = nil + self.groupid = nil -- First remove an existing mark. if self.shown then self:Remove() end - self.mid=UTILS.GetMarkID() + self.mid = UTILS.GetMarkID() -- Call DCS function. - trigger.action.markToAll(self.mid, self.text, self.coordinate:GetVec3(), self.readonly, self.message) + trigger.action.markToAll( self.mid, self.text, self.coordinate:GetVec3(), self.readonly, self.message ) end @@ -45566,32 +54186,32 @@ end --- Place marker visible for a specific coalition only. -- @param #MARKER self --- @param #number Coalition Coalition 1=Red, 2=Blue, 0=Neutral. See `coaliton.side.RED`. +-- @param #number Coalition Coalition 1=Red, 2=Blue, 0=Neutral. See `coalition.side.RED`. -- @param #number Delay (Optional) Delay in seconds, before the marker is created. -- @return #MARKER self -function MARKER:ToCoalition(Coalition, Delay) +function MARKER:ToCoalition( Coalition, Delay ) - if Delay and Delay>0 then - self:ScheduleOnce(Delay, MARKER.ToCoalition, self, Coalition) + if Delay and Delay > 0 then + self:ScheduleOnce( Delay, MARKER.ToCoalition, self, Coalition ) else - self.coalition=Coalition + self.coalition = Coalition - self.tocoaliton=true - self.toall=false - self.togroup=false - self.groupname=nil - self.groupid=nil + self.tocoalition = true + self.toall = false + self.togroup = false + self.groupname = nil + self.groupid = nil -- First remove an existing mark. if self.shown then self:Remove() end - self.mid=UTILS.GetMarkID() + self.mid = UTILS.GetMarkID() -- Call DCS function. - trigger.action.markToCoalition(self.mid, self.text, self.coordinate:GetVec3(), self.coalition, self.readonly, self.message) + trigger.action.markToCoalition( self.mid, self.text, self.coordinate:GetVec3(), self.coalition, self.readonly, self.message ) end @@ -45602,8 +54222,8 @@ end -- @param #MARKER self -- @param #number Delay (Optional) Delay in seconds, before the marker is created. -- @return #MARKER self -function MARKER:ToBlue(Delay) - self:ToCoalition(coalition.side.BLUE, Delay) +function MARKER:ToBlue( Delay ) + self:ToCoalition( coalition.side.BLUE, Delay ) return self end @@ -45611,8 +54231,8 @@ end -- @param #MARKER self -- @param #number Delay (Optional) Delay in seconds, before the marker is created. -- @return #MARKER self -function MARKER:ToRed(Delay) - self:ToCoalition(coalition.side.RED, Delay) +function MARKER:ToRed( Delay ) + self:ToCoalition( coalition.side.RED, Delay ) return self end @@ -45620,51 +54240,50 @@ end -- @param #MARKER self -- @param #number Delay (Optional) Delay in seconds, before the marker is created. -- @return #MARKER self -function MARKER:ToNeutral(Delay) - self:ToCoalition(coalition.side.NEUTRAL, Delay) +function MARKER:ToNeutral( Delay ) + self:ToCoalition( coalition.side.NEUTRAL, Delay ) return self end - --- Place marker visible for a specific group only. -- @param #MARKER self -- @param Wrapper.Group#GROUP Group The group to which the marker is displayed. -- @param #number Delay (Optional) Delay in seconds, before the marker is created. -- @return #MARKER self -function MARKER:ToGroup(Group, Delay) +function MARKER:ToGroup( Group, Delay ) - if Delay and Delay>0 then - self:ScheduleOnce(Delay, MARKER.ToGroup, self, Group) + if Delay and Delay > 0 then + self:ScheduleOnce( Delay, MARKER.ToGroup, self, Group ) else -- Check if group exists. - if Group and Group:IsAlive()~=nil then + if Group and Group:IsAlive() ~= nil then - self.groupid=Group:GetID() + self.groupid = Group:GetID() if self.groupid then - self.groupname=Group:GetName() + self.groupname = Group:GetName() - self.togroup=true - self.tocoaliton=nil - self.coalition=nil - self.toall=nil + self.togroup = true + self.tocoalition = nil + self.coalition = nil + self.toall = nil -- First remove an existing mark. if self.shown then self:Remove() end - self.mid=UTILS.GetMarkID() + self.mid = UTILS.GetMarkID() -- Call DCS function. - trigger.action.markToGroup(self.mid, self.text, self.coordinate:GetVec3(), self.groupid, self.readonly, self.message) + trigger.action.markToGroup( self.mid, self.text, self.coordinate:GetVec3(), self.groupid, self.readonly, self.message ) end else - --TODO: Warning! + -- TODO: Warning! end end @@ -45677,17 +54296,17 @@ end -- @param #string Text Updated text. -- @param #number Delay (Optional) Delay in seconds, before the marker is created. -- @return #MARKER self -function MARKER:UpdateText(Text, Delay) +function MARKER:UpdateText( Text, Delay ) - if Delay and Delay>0 then - self:ScheduleOnce(Delay, MARKER.UpdateText, self, Text) + if Delay and Delay > 0 then + self:ScheduleOnce( Delay, MARKER.UpdateText, self, Text ) else - self.text=tostring(Text) + self.text = tostring( Text ) self:Refresh() - self:TextUpdate(tostring(Text)) + self:TextUpdate( tostring( Text ) ) end @@ -45699,17 +54318,17 @@ end -- @param Core.Point#COORDINATE Coordinate The new coordinate. -- @param #number Delay (Optional) Delay in seconds, before the marker is created. -- @return #MARKER self -function MARKER:UpdateCoordinate(Coordinate, Delay) +function MARKER:UpdateCoordinate( Coordinate, Delay ) - if Delay and Delay>0 then - self:ScheduleOnce(Delay, MARKER.UpdateCoordinate, self, Coordinate) + if Delay and Delay > 0 then + self:ScheduleOnce( Delay, MARKER.UpdateCoordinate, self, Coordinate ) else - self.coordinate=Coordinate + self.coordinate = Coordinate self:Refresh() - self:CoordUpdate(Coordinate) + self:CoordUpdate( Coordinate ) end @@ -45720,28 +54339,28 @@ end -- @param #MARKER self -- @param #number Delay (Optional) Delay in seconds, before the marker is created. -- @return #MARKER self -function MARKER:Refresh(Delay) +function MARKER:Refresh( Delay ) - if Delay and Delay>0 then - self:ScheduleOnce(Delay, MARKER.Refresh, self) + if Delay and Delay > 0 then + self:ScheduleOnce( Delay, MARKER.Refresh, self ) else if self.toall then self:ToAll() - elseif self.tocoaliton then + elseif self.tocoalition then - self:ToCoalition(self.coalition) + self:ToCoalition( self.coalition ) elseif self.togroup then - local group=GROUP:FindByName(self.groupname) + local group = GROUP:FindByName( self.groupname ) - self:ToGroup(group) + self:ToGroup( group ) else - self:E(self.lid.."ERROR: unknown To in :Refresh()!") + self:E( self.lid .. "ERROR: unknown To in :Refresh()!" ) end end @@ -45753,16 +54372,16 @@ end -- @param #MARKER self -- @param #number Delay (Optional) Delay in seconds, before the marker is removed. -- @return #MARKER self -function MARKER:Remove(Delay) +function MARKER:Remove( Delay ) - if Delay and Delay>0 then - self:ScheduleOnce(Delay, MARKER.Remove, self) + if Delay and Delay > 0 then + self:ScheduleOnce( Delay, MARKER.Remove, self ) else if self.shown then -- Call DCS function. - trigger.action.removeMark(self.mid) + trigger.action.removeMark( self.mid ) end @@ -45787,26 +54406,25 @@ end --- Set text that is displayed in the marker panel. Note this does not show the marker. -- @param #MARKER self --- @param #string Text Marker text. Default is an empty sting "". +-- @param #string Text Marker text. Default is an empty string "". -- @return #MARKER self -function MARKER:SetText(Text) - self.text=Text and tostring(Text) or "" +function MARKER:SetText( Text ) + self.text = Text and tostring( Text ) or "" return self end - --- Check if marker is currently visible on the F10 map. -- @param #MARKER self -- @return #boolean True if the marker is currently visible. function MARKER:IsVisible() - return self:Is("Visible") + return self:Is( "Visible" ) end --- Check if marker is currently invisible on the F10 map. -- @param #MARKER self -- @return function MARKER:IsInvisible() - return self:Is("Invisible") + return self:Is( "Invisible" ) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -45816,19 +54434,19 @@ end --- Event function when a MARKER is added. -- @param #MARKER self -- @param Core.Event#EVENTDATA EventData -function MARKER:OnEventMarkAdded(EventData) +function MARKER:OnEventMarkAdded( EventData ) if EventData and EventData.MarkID then - local MarkID=EventData.MarkID + local MarkID = EventData.MarkID - self:T3(self.lid..string.format("Captured event MarkAdded for Mark ID=%s", tostring(MarkID))) + self:T3( self.lid .. string.format( "Captured event MarkAdded for Mark ID=%s", tostring( MarkID ) ) ) - if MarkID==self.mid then + if MarkID == self.mid then - self.shown=true + self.shown = true - self:Added(EventData) + self:Added( EventData ) end @@ -45839,19 +54457,21 @@ end --- Event function when a MARKER is removed. -- @param #MARKER self -- @param Core.Event#EVENTDATA EventData -function MARKER:OnEventMarkRemoved(EventData) +function MARKER:OnEventMarkRemoved( EventData ) if EventData and EventData.MarkID then + local MarkID = EventData.MarkID + local MarkID=EventData.MarkID - self:T3(self.lid..string.format("Captured event MarkAdded for Mark ID=%s", tostring(MarkID))) + self:T3(self.lid..string.format("Captured event MarkRemoved for Mark ID=%s", tostring(MarkID))) - if MarkID==self.mid then + if MarkID == self.mid then - self.shown=false + self.shown = false - self:Removed(EventData) + self:Removed( EventData ) end @@ -45862,26 +54482,32 @@ end --- Event function when a MARKER changed. -- @param #MARKER self -- @param Core.Event#EVENTDATA EventData -function MARKER:OnEventMarkChange(EventData) +function MARKER:OnEventMarkChange( EventData ) if EventData and EventData.MarkID then + local MarkID = EventData.MarkID + + self:T3( self.lid .. string.format( "Captured event MarkChange for Mark ID=%s", tostring( MarkID ) ) ) + + if MarkID == self.mid then + local MarkID=EventData.MarkID self:T3(self.lid..string.format("Captured event MarkChange for Mark ID=%s", tostring(MarkID))) if MarkID==self.mid then - self:Changed(EventData) + self.text=tostring(EventData.MarkText) - self:TextChanged(tostring(EventData.MarkText)) + self:Changed(EventData) end end end - +end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- FSM Event Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -45892,17 +54518,17 @@ end -- @param #string Event Event. -- @param #string To To state. -- @param Core.Event#EVENTDATA EventData Event data table. -function MARKER:onafterAdded(From, Event, To, EventData) +function MARKER:onafterAdded( From, Event, To, EventData ) -- Debug info. - local text=string.format("Captured event MarkAdded for myself:\n") - text=text..string.format("Marker ID = %s\n", tostring(EventData.MarkID)) - text=text..string.format("Coalition = %s\n", tostring(EventData.MarkCoalition)) - text=text..string.format("Group ID = %s\n", tostring(EventData.MarkGroupID)) - text=text..string.format("Initiator = %s\n", EventData.IniUnit and EventData.IniUnit:GetName() or "Nobody") - text=text..string.format("Coordinate = %s\n", EventData.MarkCoordinate and EventData.MarkCoordinate:ToStringLLDMS() or "Nowhere") - text=text..string.format("Text: \n%s", tostring(EventData.MarkText)) - self:T2(self.lid..text) + local text = string.format( "Captured event MarkAdded for myself:\n" ) + text = text .. string.format( "Marker ID = %s\n", tostring( EventData.MarkID ) ) + text = text .. string.format( "Coalition = %s\n", tostring( EventData.MarkCoalition ) ) + text = text .. string.format( "Group ID = %s\n", tostring( EventData.MarkGroupID ) ) + text = text .. string.format( "Initiator = %s\n", EventData.IniUnit and EventData.IniUnit:GetName() or "Nobody" ) + text = text .. string.format( "Coordinate = %s\n", EventData.MarkCoordinate and EventData.MarkCoordinate:ToStringLLDMS() or "Nowhere" ) + text = text .. string.format( "Text: \n%s", tostring( EventData.MarkText ) ) + self:T2( self.lid .. text ) end @@ -45912,17 +54538,17 @@ end -- @param #string Event Event. -- @param #string To To state. -- @param Core.Event#EVENTDATA EventData Event data table. -function MARKER:onafterRemoved(From, Event, To, EventData) +function MARKER:onafterRemoved( From, Event, To, EventData ) -- Debug info. - local text=string.format("Captured event MarkRemoved for myself:\n") - text=text..string.format("Marker ID = %s\n", tostring(EventData.MarkID)) - text=text..string.format("Coalition = %s\n", tostring(EventData.MarkCoalition)) - text=text..string.format("Group ID = %s\n", tostring(EventData.MarkGroupID)) - text=text..string.format("Initiator = %s\n", EventData.IniUnit and EventData.IniUnit:GetName() or "Nobody") - text=text..string.format("Coordinate = %s\n", EventData.MarkCoordinate and EventData.MarkCoordinate:ToStringLLDMS() or "Nowhere") - text=text..string.format("Text: \n%s", tostring(EventData.MarkText)) - self:T2(self.lid..text) + local text = string.format( "Captured event MarkRemoved for myself:\n" ) + text = text .. string.format( "Marker ID = %s\n", tostring( EventData.MarkID ) ) + text = text .. string.format( "Coalition = %s\n", tostring( EventData.MarkCoalition ) ) + text = text .. string.format( "Group ID = %s\n", tostring( EventData.MarkGroupID ) ) + text = text .. string.format( "Initiator = %s\n", EventData.IniUnit and EventData.IniUnit:GetName() or "Nobody" ) + text = text .. string.format( "Coordinate = %s\n", EventData.MarkCoordinate and EventData.MarkCoordinate:ToStringLLDMS() or "Nowhere" ) + text = text .. string.format( "Text: \n%s", tostring( EventData.MarkText ) ) + self:T2( self.lid .. text ) end @@ -45932,17 +54558,17 @@ end -- @param #string Event Event. -- @param #string To To state. -- @param Core.Event#EVENTDATA EventData Event data table. -function MARKER:onafterChanged(From, Event, To, EventData) +function MARKER:onafterChanged( From, Event, To, EventData ) -- Debug info. - local text=string.format("Captured event MarkChange for myself:\n") - text=text..string.format("Marker ID = %s\n", tostring(EventData.MarkID)) - text=text..string.format("Coalition = %s\n", tostring(EventData.MarkCoalition)) - text=text..string.format("Group ID = %s\n", tostring(EventData.MarkGroupID)) - text=text..string.format("Initiator = %s\n", EventData.IniUnit and EventData.IniUnit:GetName() or "Nobody") - text=text..string.format("Coordinate = %s\n", EventData.MarkCoordinate and EventData.MarkCoordinate:ToStringLLDMS() or "Nowhere") - text=text..string.format("Text: \n%s", tostring(EventData.MarkText)) - self:T2(self.lid..text) + local text = string.format( "Captured event MarkChange for myself:\n" ) + text = text .. string.format( "Marker ID = %s\n", tostring( EventData.MarkID ) ) + text = text .. string.format( "Coalition = %s\n", tostring( EventData.MarkCoalition ) ) + text = text .. string.format( "Group ID = %s\n", tostring( EventData.MarkGroupID ) ) + text = text .. string.format( "Initiator = %s\n", EventData.IniUnit and EventData.IniUnit:GetName() or "Nobody" ) + text = text .. string.format( "Coordinate = %s\n", EventData.MarkCoordinate and EventData.MarkCoordinate:ToStringLLDMS() or "Nowhere" ) + text = text .. string.format( "Text: \n%s", tostring( EventData.MarkText ) ) + self:T2( self.lid .. text ) end @@ -45952,9 +54578,9 @@ end -- @param #string Event Event. -- @param #string To To state. -- @param #string Text The updated text, displayed in the mark panel. -function MARKER:onafterTextUpdate(From, Event, To, Text) +function MARKER:onafterTextUpdate( From, Event, To, Text ) - self:T(self.lid..string.format("New Marker Text:\n%s", Text)) + self:T( self.lid .. string.format( "New Marker Text:\n%s", Text ) ) end @@ -45964,15 +54590,11 @@ end -- @param #string Event Event. -- @param #string To To state. -- @param Core.Point#COORDINATE Coordinate The updated coordinates. -function MARKER:onafterCoordUpdate(From, Event, To, Coordinate) +function MARKER:onafterCoordUpdate( From, Event, To, Coordinate ) - self:T(self.lid..string.format("New Marker Coordinate in LL DMS: %s", Coordinate:ToStringLLDMS())) + self:T( self.lid .. string.format( "New Marker Coordinate in LL DMS: %s", Coordinate:ToStringLLDMS() ) ) end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- **Cargo** - Management of CARGO logistics, that can be transported from and to transportation carriers. -- -- === @@ -46041,7 +54663,7 @@ end -- you can board the cargo into the carrier `CargoCarrier`. -- Simple, isn't it? Told you, and this is only the beginning. -- --- The boarding, unboarding, loading, unloading of cargo is however something that is not meant to be coded manualy by mission designers. +-- The boarding, unboarding, loading, unloading of cargo is however something that is not meant to be coded manually by mission designers. -- It would be too low-level and not end-user friendly to deal with cargo handling complexity. -- Things can become really complex if you want to make cargo being handled and behave in multiple scenarios. -- @@ -46052,8 +54674,8 @@ end -- -- ## 3.1) AI Cargo handlers. -- --- - @{AI.AI_Cargo_APC} will create for you the capatility to make an APC group handle cargo. --- - @{AI.AI_Cargo_Helicopter} will create for you the capatility to make a Helicopter group handle cargo. +-- - @{AI.AI_Cargo_APC} will create for you the capability to make an APC group handle cargo. +-- - @{AI.AI_Cargo_Helicopter} will create for you the capability to make a Helicopter group handle cargo. -- -- -- ## 3.2) AI Cargo transportation dispatchers. @@ -46061,7 +54683,7 @@ end -- There are also dispatchers that make AI work together to transport cargo automatically!!! -- -- - @{AI.AI_Cargo_Dispatcher_APC} derived classes will create for your dynamic cargo handlers controlled by AI ground vehicle groups (APCs) to transport cargo between sites. --- - @{AI.AI_Cargo_Dispatcher_Helicopters} derived classes will create for your dynamic cargo handlers controlled by AI helicpter groups to transport cargo between sites. +-- - @{AI.AI_Cargo_Dispatcher_Helicopters} derived classes will create for your dynamic cargo handlers controlled by AI helicopter groups to transport cargo between sites. -- -- ## 3.3) Cargo transportation tasking. -- @@ -46069,7 +54691,7 @@ end -- -- - @{Tasking.Task_CARGO} derived classes will create for you cargo transportation tasks, that allow human players to interact with MOOSE cargo objects to complete tasks. -- --- Please refer to the documentation reflected within these modules to understand the detailed capabilties. +-- Please refer to the documentation reflected within these modules to understand the detailed capabilities. -- -- # 4) Cargo SETs. -- @@ -46169,7 +54791,7 @@ end -- * is of type `Workmaterials` -- * will report when a carrier is within 500 meters -- * will board to carriers when the carrier is within 500 meters from the cargo object --- * will dissapear when the cargo is within 25 meters from the carrier during boarding +-- * will disappear when the cargo is within 25 meters from the carrier during boarding -- -- So the overall syntax of the #CARGO naming tag and arguments are: -- @@ -46181,27 +54803,29 @@ end -- * **NR=** Provide the maximum range in meters when the cargo units will be boarded within the carrier during boarding. -- Note that this option is optional, so can be omitted. The default value of the RR is 10 meters. -- --- ## 5.2) The \#CARGO tag to create CARGO_CRATE objects: +-- ## 5.2) The \#CARGO tag to create CARGO_CRATE or CARGO_SLINGLOAD objects: -- -- You can also use the \#CARGO tag on **static** objects, including **static cargo** objects of the mission editor. -- -- For example, the following #CARGO naming in the **static name** of the object, will create a CARGO_CRATE object when the mission starts. -- --- `Static #CARGO(T=Workmaterials,RR=500,NR=25)` +-- `Static #CARGO(T=Workmaterials,C=CRATE,RR=500,NR=25)` -- -- This will create a CARGO_CRATE object: -- -- * with the group name `Static #CARGO` -- * is of type `Workmaterials` +-- * is of category `CRATE` (as opposed to `SLING`) -- * will report when a carrier is within 500 meters -- * will board to carriers when the carrier is within 500 meters from the cargo object --- * will dissapear when the cargo is within 25 meters from the carrier during boarding +-- * will disappear when the cargo is within 25 meters from the carrier during boarding -- -- So the overall syntax of the #CARGO naming tag and arguments are: -- --- `StaticName #CARGO(T=CargoTypeName,RR=Range,NR=Range)` +-- `StaticName #CARGO(T=CargoTypeName,C=Category,RR=Range,NR=Range)` -- -- * **T=** Provide a text that contains the type name of the cargo object. This type name can be used to filter cargo within a SET_CARGO object. +-- * **C=** Provide either `CRATE` or `SLING` to have this static created as a CARGO_CRATE or CARGO_SLINGLOAD respectively. -- * **RR=** Provide the minimal range in meters when the report to the carrier, and board to the carrier. -- Note that this option is optional, so can be omitted. The default value of the RR is 250 meters. -- * **NR=** Provide the maximum range in meters when the cargo units will be boarded within the carrier during boarding. @@ -46350,7 +54974,7 @@ do -- CARGO -- @field #boolean Moveable This flag defines if the cargo is moveable. -- @field #boolean Representable This flag defines if the cargo can be represented by a DCS Unit. -- @field #boolean Containable This flag defines if the cargo can be contained within a DCS Unit. - + --- Defines the core functions that defines a cargo object within MOOSE. -- -- A cargo is a **logical object** defined that is available for transport, and has a life status within a simulation. @@ -46403,8 +55027,7 @@ do -- CARGO --- @type CARGO.CargoObjects -- @map < #string, Wrapper.Positionable#POSITIONABLE > The alive POSITIONABLE objects representing the the cargo. - - + --- CARGO Constructor. This class is an abstract class and should not be instantiated. -- @param #CARGO self -- @param #string Type @@ -46414,10 +55037,10 @@ do -- CARGO -- @param #number NearRadius (optional) -- @return #CARGO function CARGO:New( Type, Name, Weight, LoadRadius, NearRadius ) --R2.1 - + local self = BASE:Inherit( self, FSM:New() ) -- #CARGO self:F( { Type, Name, Weight, LoadRadius, NearRadius } ) - + self:SetStartState( "UnLoaded" ) self:AddTransition( { "UnLoaded", "Boarding" }, "Board", "Boarding" ) self:AddTransition( "Boarding" , "Boarding", "Boarding" ) @@ -46432,7 +55055,7 @@ do -- CARGO self:AddTransition( "*", "Destroyed", "Destroyed" ) self:AddTransition( "*", "Respawn", "UnLoaded" ) self:AddTransition( "*", "Reset", "UnLoaded" ) - + self.Type = Type self.Name = Name self.Weight = Weight or 0 @@ -46444,31 +55067,29 @@ do -- CARGO self.Containable = false self.CargoLimit = 0 - + self.LoadRadius = LoadRadius or 500 --self.NearRadius = NearRadius or 25 - + self:SetDeployed( false ) - + self.CargoScheduler = SCHEDULER:New() - + CARGOS[self.Name] = self - - + return self end - - + --- Find a CARGO in the _DATABASE. -- @param #CARGO self -- @param #string CargoName The Cargo Name. -- @return #CARGO self function CARGO:FindByName( CargoName ) - + local CargoFound = _DATABASE:FindCargo( CargoName ) return CargoFound end - + --- Get the x position of the cargo. -- @param #CARGO self -- @return #number @@ -46477,9 +55098,9 @@ do -- CARGO return self.CargoCarrier:GetCoordinate().x else return self.CargoObject:GetCoordinate().x - end + end end - + --- Get the y position of the cargo. -- @param #CARGO self -- @return #number @@ -46488,9 +55109,9 @@ do -- CARGO return self.CargoCarrier:GetCoordinate().z else return self.CargoObject:GetCoordinate().z - end + end end - + --- Get the heading of the cargo. -- @param #CARGO self -- @return #number @@ -46499,22 +55120,21 @@ do -- CARGO return self.CargoCarrier:GetHeading() else return self.CargoObject:GetHeading() - end + end end - - + --- Check if the cargo can be Slingloaded. -- @param #CARGO self function CARGO:CanSlingload() return false end - + --- Check if the cargo can be Boarded. -- @param #CARGO self function CARGO:CanBoard() return true end - + --- Check if the cargo can be Unboarded. -- @param #CARGO self function CARGO:CanUnboard() @@ -46526,14 +55146,13 @@ do -- CARGO function CARGO:CanLoad() return true end - + --- Check if the cargo can be Unloaded. -- @param #CARGO self function CARGO:CanUnload() return true end - --- Destroy the cargo. -- @param #CARGO self function CARGO:Destroy() @@ -46542,14 +55161,14 @@ do -- CARGO end self:Destroyed() end - + --- Get the name of the Cargo. -- @param #CARGO self -- @return #string The name of the Cargo. function CARGO:GetName() --R2.1 return self.Name end - + --- Get the current active object representing or being the Cargo. -- @param #CARGO self -- @return Wrapper.Positionable#POSITIONABLE The object representing or being the Cargo. @@ -46558,9 +55177,9 @@ do -- CARGO return self.CargoCarrier else return self.CargoObject - end + end end - + --- Get the object name of the Cargo. -- @param #CARGO self -- @return #string The object name of the Cargo. @@ -46569,9 +55188,9 @@ do -- CARGO return self.CargoCarrier:GetName() else return self.CargoObject:GetName() - end + end end - + --- Get the amount of Cargo. -- @param #CARGO self -- @return #number The amount of Cargo. @@ -46586,7 +55205,6 @@ do -- CARGO return self.Type end - --- Get the transportation method of the Cargo. -- @param #CARGO self -- @return #string The transportation method of the Cargo. @@ -46594,7 +55212,6 @@ do -- CARGO return self.TransportationMethod end - --- Get the coalition of the Cargo. -- @param #CARGO self -- @return Coalition @@ -46603,32 +55220,30 @@ do -- CARGO return self.CargoCarrier:GetCoalition() else return self.CargoObject:GetCoalition() - end + end end - --- Get the current coordinates of the Cargo. -- @param #CARGO self -- @return Core.Point#COORDINATE The coordinates of the Cargo. function CARGO:GetCoordinate() return self.CargoObject:GetCoordinate() end - + --- Check if cargo is destroyed. -- @param #CARGO self -- @return #boolean true if destroyed function CARGO:IsDestroyed() return self:Is( "Destroyed" ) end - - + --- Check if cargo is loaded. -- @param #CARGO self -- @return #boolean true if loaded function CARGO:IsLoaded() return self:Is( "Loaded" ) end - + --- Check if cargo is loaded. -- @param #CARGO self -- @param Wrapper.Unit#UNIT Carrier @@ -46636,14 +55251,14 @@ do -- CARGO function CARGO:IsLoadedInCarrier( Carrier ) return self.CargoCarrier and self.CargoCarrier:GetName() == Carrier:GetName() end - + --- Check if cargo is unloaded. -- @param #CARGO self -- @return #boolean true if unloaded function CARGO:IsUnLoaded() return self:Is( "UnLoaded" ) end - + --- Check if cargo is boarding. -- @param #CARGO self -- @return #boolean true if boarding @@ -46651,52 +55266,47 @@ do -- CARGO return self:Is( "Boarding" ) end - --- Check if cargo is unboarding. -- @param #CARGO self -- @return #boolean true if unboarding function CARGO:IsUnboarding() return self:Is( "UnBoarding" ) end - --- Check if cargo is alive. -- @param #CARGO self -- @return #boolean true if unloaded function CARGO:IsAlive() - + if self:IsLoaded() then return self.CargoCarrier:IsAlive() else return self.CargoObject:IsAlive() - end + end end - + --- Set the cargo as deployed. -- @param #CARGO self -- @param #boolean Deployed true if the cargo is to be deployed. false or nil otherwise. function CARGO:SetDeployed( Deployed ) self.Deployed = Deployed end - + --- Is the cargo deployed -- @param #CARGO self -- @return #boolean function CARGO:IsDeployed() return self.Deployed end - - - - + --- Template method to spawn a new representation of the CARGO in the simulator. -- @param #CARGO self -- @return #CARGO function CARGO:Spawn( PointVec2 ) self:F() - + end - + --- Signal a flare at the position of the CARGO. -- @param #CARGO self -- @param Utilities.Utils#FLARECOLOR FlareColor @@ -46705,31 +55315,31 @@ do -- CARGO trigger.action.signalFlare( self.CargoObject:GetVec3(), FlareColor , 0 ) end end - + --- Signal a white flare at the position of the CARGO. -- @param #CARGO self function CARGO:FlareWhite() self:Flare( trigger.flareColor.White ) end - + --- Signal a yellow flare at the position of the CARGO. -- @param #CARGO self function CARGO:FlareYellow() self:Flare( trigger.flareColor.Yellow ) end - + --- Signal a green flare at the position of the CARGO. -- @param #CARGO self function CARGO:FlareGreen() self:Flare( trigger.flareColor.Green ) end - + --- Signal a red flare at the position of the CARGO. -- @param #CARGO self function CARGO:FlareRed() self:Flare( trigger.flareColor.Red ) end - + --- Smoke the CARGO. -- @param #CARGO self -- @param Utilities.Utils#SMOKECOLOR SmokeColor The color of the smoke. @@ -46743,38 +55353,37 @@ do -- CARGO end end end - + --- Smoke the CARGO Green. -- @param #CARGO self function CARGO:SmokeGreen() self:Smoke( trigger.smokeColor.Green, Range ) end - + --- Smoke the CARGO Red. -- @param #CARGO self function CARGO:SmokeRed() self:Smoke( trigger.smokeColor.Red, Range ) end - + --- Smoke the CARGO White. -- @param #CARGO self function CARGO:SmokeWhite() self:Smoke( trigger.smokeColor.White, Range ) end - + --- Smoke the CARGO Orange. -- @param #CARGO self function CARGO:SmokeOrange() self:Smoke( trigger.smokeColor.Orange, Range ) end - + --- Smoke the CARGO Blue. -- @param #CARGO self function CARGO:SmokeBlue() self:Smoke( trigger.smokeColor.Blue, Range ) end - - + --- Set the Load radius, which is the radius till when the Cargo can be loaded. -- @param #CARGO self -- @param #number LoadRadius The radius till Cargo can be loaded. @@ -46782,23 +55391,21 @@ do -- CARGO function CARGO:SetLoadRadius( LoadRadius ) self.LoadRadius = LoadRadius or 150 end - + --- Get the Load radius, which is the radius till when the Cargo can be loaded. -- @param #CARGO self -- @return #number The radius till Cargo can be loaded. function CARGO:GetLoadRadius() return self.LoadRadius end - - - + --- Check if Cargo is in the LoadRadius for the Cargo to be Boarded or Loaded. -- @param #CARGO self -- @param Core.Point#COORDINATE Coordinate -- @return #boolean true if the CargoGroup is within the loading radius. function CARGO:IsInLoadRadius( Coordinate ) self:F( { Coordinate, LoadRadius = self.LoadRadius } ) - + local Distance = 0 if self:IsUnLoaded() then local CargoCoordinate = self.CargoObject:GetCoordinate() @@ -46808,18 +55415,17 @@ do -- CARGO return true end end - + return false end - --- Check if the Cargo can report itself to be Boarded or Loaded. -- @param #CARGO self -- @param Core.Point#COORDINATE Coordinate -- @return #boolean true if the Cargo can report itself. function CARGO:IsInReportRadius( Coordinate ) self:F( { Coordinate } ) - + local Distance = 0 if self:IsUnLoaded() then Distance = Coordinate:Get2DDistance( self.CargoObject:GetCoordinate() ) @@ -46828,7 +55434,7 @@ do -- CARGO return true end end - + return false end @@ -46840,7 +55446,7 @@ do -- CARGO -- @return #boolean function CARGO:IsNear( Coordinate, NearRadius ) --self:F( { PointVec2 = PointVec2, NearRadius = NearRadius } ) - + if self.CargoObject:IsAlive() then --local Distance = PointVec2:Get2DDistance( self.CargoObject:GetPointVec2() ) --self:F( { CargoObjectName = self.CargoObject:GetName() } ) @@ -46848,26 +55454,24 @@ do -- CARGO --self:F( { PointVec2 = PointVec2:GetVec2() } ) local Distance = Coordinate:Get2DDistance( self.CargoObject:GetCoordinate() ) --self:F( { Distance = Distance, NearRadius = NearRadius or "nil" } ) - + if Distance <= NearRadius then --self:F( { PointVec2 = PointVec2, NearRadius = NearRadius, IsNear = true } ) return true end end - + --self:F( { PointVec2 = PointVec2, NearRadius = NearRadius, IsNear = false } ) return false end - - - - --- Check if Cargo is the given @{Zone}. + + --- Check if Cargo is the given @{Core.Zone}. -- @param #CARGO self -- @param Core.Zone#ZONE_BASE Zone -- @return #boolean **true** if cargo is in the Zone, **false** if cargo is not in the Zone. function CARGO:IsInZone( Zone ) --self:F( { Zone } ) - + if self:IsLoaded() then return Zone:IsPointVec2InZone( self.CargoCarrier:GetPointVec2() ) else @@ -46877,34 +55481,33 @@ do -- CARGO else return false end - end - + end + return nil - + end - - + --- Get the current PointVec2 of the cargo. -- @param #CARGO self -- @return Core.Point#POINT_VEC2 function CARGO:GetPointVec2() return self.CargoObject:GetPointVec2() end - + --- Get the current Coordinate of the cargo. -- @param #CARGO self -- @return Core.Point#COORDINATE function CARGO:GetCoordinate() return self.CargoObject:GetCoordinate() end - + --- Get the weight of the cargo. -- @param #CARGO self -- @return #number Weight The weight in kg. function CARGO:GetWeight() return self.Weight end - + --- Set the weight of the cargo. -- @param #CARGO self -- @param #number Weight The weight in kg. @@ -46913,14 +55516,14 @@ do -- CARGO self.Weight = Weight return self end - + --- Get the volume of the cargo. -- @param #CARGO self -- @return #number Volume The volume in kg. function CARGO:GetVolume() return self.Volume end - + --- Set the volume of the cargo. -- @param #CARGO self -- @param #number Volume The volume in kg. @@ -46929,18 +55532,18 @@ do -- CARGO self.Volume = Volume return self end - + --- Send a CC message to a @{Wrapper.Group}. -- @param #CARGO self -- @param #string Message -- @param Wrapper.Group#GROUP CarrierGroup The Carrier Group. -- @param #string Name (optional) The name of the Group used as a prefix for the message to the Group. If not provided, there will be nothing shown. function CARGO:MessageToGroup( Message, CarrierGroup, Name ) - + MESSAGE:New( Message, 20, "Cargo " .. self:GetName() ):ToGroup( CarrierGroup ) - + end - + --- Report to a Carrier Group. -- @param #CARGO self -- @param #string Action The string describing the action for the cargo. @@ -46966,8 +55569,7 @@ do -- CARGO end end end - - + --- Report to a Carrier Group with a Flaring signal. -- @param #CARGO self -- @param Utils#UTILS.FlareColor FlareColor the color of the flare. @@ -46976,8 +55578,7 @@ do -- CARGO self.ReportFlareColor = FlareColor end - - + --- Report to a Carrier Group with a Smoking signal. -- @param #CARGO self -- @param Utils#UTILS.SmokeColor SmokeColor the color of the smoke. @@ -46986,8 +55587,7 @@ do -- CARGO self.ReportSmokeColor = SmokeColor end - - + --- Reset the reporting for a Carrier Group. -- @param #CARGO self -- @param #string Action The string describing the action for the cargo. @@ -46997,7 +55597,7 @@ do -- CARGO self.Reported[CarrierGroup][Action] = nil end - + --- Reset all the reporting for a Carrier Group. -- @param #CARGO self -- @param Wrapper.Group#GROUP CarrierGroup The Carrier Group to send the report to. @@ -47006,7 +55606,7 @@ do -- CARGO self.Reported[CarrierGroup] = nil end - + --- Respawn the cargo when destroyed -- @param #CARGO self -- @param #boolean RespawnDestroyed @@ -47019,11 +55619,8 @@ do -- CARGO else self.onenterDestroyed = nil end - - end - - + end end -- CARGO @@ -47048,7 +55645,7 @@ do -- CARGO_REPRESENTABLE -- @param #number NearRadius (optional) Radius in meters when the cargo is loaded into the carrier. -- @return #CARGO_REPRESENTABLE function CARGO_REPRESENTABLE:New( CargoObject, Type, Name, LoadRadius, NearRadius ) - + -- Inherit CARGO. local self = BASE:Inherit( self, CARGO:New( Type, Name, 0, LoadRadius, NearRadius ) ) -- #CARGO_REPRESENTABLE self:F( { Type, Name, LoadRadius, NearRadius } ) @@ -47056,10 +55653,10 @@ do -- CARGO_REPRESENTABLE -- Descriptors. local Desc=CargoObject:GetDesc() self:T({Desc=Desc}) - + -- Weight. local Weight = math.random( 80, 120 ) - + -- Adjust weight.. if Desc then if Desc.typeName == "2B11 mortar" then @@ -47070,8 +55667,8 @@ do -- CARGO_REPRESENTABLE end -- Set weight. - self:SetWeight( Weight ) - + self:SetWeight( Weight ) + return self end @@ -47079,14 +55676,14 @@ do -- CARGO_REPRESENTABLE -- @param #CARGO_REPRESENTABLE self -- @return #CARGO_REPRESENTABLE function CARGO_REPRESENTABLE:Destroy() - + -- Cargo objects are deleted from the _DATABASE and SET_CARGO objects. self:F( { CargoName = self:GetName() } ) --_EVENTDISPATCHER:CreateEventDeleteCargo( self ) - + return self end - + --- Route a cargo unit to a PointVec2. -- @param #CARGO_REPRESENTABLE self -- @param Core.Point#POINT_VEC2 ToPointVec2 @@ -47094,19 +55691,19 @@ do -- CARGO_REPRESENTABLE -- @return #CARGO_REPRESENTABLE function CARGO_REPRESENTABLE:RouteTo( ToPointVec2, Speed ) self:F2( ToPointVec2 ) - + local Points = {} - + local PointStartVec2 = self.CargoObject:GetPointVec2() - + Points[#Points+1] = PointStartVec2:WaypointGround( Speed ) Points[#Points+1] = ToPointVec2:WaypointGround( Speed ) - + local TaskRoute = self.CargoObject:TaskRoute( Points ) self.CargoObject:SetTask( TaskRoute, 2 ) - return self + return self end - + --- Send a message to a @{Wrapper.Group} through a communication channel near the cargo. -- @param #CARGO_REPRESENTABLE self -- @param #string Message @@ -47130,20 +55727,19 @@ do -- CARGO_REPRESENTABLE end end end - + end - end -- CARGO_REPRESENTABLE do -- CARGO_REPORTABLE - + --- @type CARGO_REPORTABLE -- @extends #CARGO CARGO_REPORTABLE = { ClassName = "CARGO_REPORTABLE" } - + --- CARGO_REPORTABLE Constructor. -- @param #CARGO_REPORTABLE self -- @param #string Type @@ -47155,31 +55751,23 @@ do -- CARGO_REPORTABLE function CARGO_REPORTABLE:New( Type, Name, Weight, LoadRadius, NearRadius ) local self = BASE:Inherit( self, CARGO:New( Type, Name, Weight, LoadRadius, NearRadius ) ) -- #CARGO_REPORTABLE self:F( { Type, Name, Weight, LoadRadius, NearRadius } ) - + return self end - + --- Send a CC message to a @{Wrapper.Group}. -- @param #CARGO_REPORTABLE self -- @param #string Message -- @param Wrapper.Group#GROUP TaskGroup -- @param #string Name (optional) The name of the Group used as a prefix for the message to the Group. If not provided, there will be nothing shown. function CARGO_REPORTABLE:MessageToGroup( Message, TaskGroup, Name ) - + MESSAGE:New( Message, 20, "Cargo " .. self:GetName() .. " reporting" ):ToGroup( TaskGroup ) - - end + end - end - - - - - - do -- CARGO_PACKAGE --- @type CARGO_PACKAGE @@ -47253,10 +55841,10 @@ function CARGO_PACKAGE:IsNear( CargoCarrier ) self:F() local CargoCarrierPoint = CargoCarrier:GetCoordinate() - + local Distance = CargoCarrierPoint:Get2DDistance( self.CargoCarrier:GetCoordinate() ) self:T( Distance ) - + if Distance <= self.NearRadius then return true else @@ -47307,7 +55895,7 @@ function CARGO_PACKAGE:onafterUnBoard( From, Event, To, CargoCarrier, Speed, UnL if not self.CargoInAir then self:_Next( self.FsmP.UnLoad, UnLoadDistance, Angle ) - + local Points = {} local StartPointVec2 = CargoCarrier:GetPointVec2() @@ -47362,7 +55950,7 @@ function CARGO_PACKAGE:onafterLoad( From, Event, To, CargoCarrier, Speed, LoadDi local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) local CargoDeployPointVec2 = StartPointVec2:Translate( LoadDistance, CargoDeployHeading ) - + local Points = {} Points[#Points+1] = StartPointVec2:WaypointGround( Speed ) Points[#Points+1] = CargoDeployPointVec2:WaypointGround( Speed ) @@ -47383,12 +55971,12 @@ end -- @param #number Angle function CARGO_PACKAGE:onafterUnLoad( From, Event, To, CargoCarrier, Speed, Distance, Angle ) self:F() - + local StartPointVec2 = self.CargoCarrier:GetPointVec2() local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) local CargoDeployPointVec2 = StartPointVec2:Translate( Distance, CargoDeployHeading ) - + self.CargoCarrier = CargoCarrier local Points = {} @@ -47400,9 +55988,8 @@ function CARGO_PACKAGE:onafterUnLoad( From, Event, To, CargoCarrier, Speed, Dist end - end ---- **Cargo** - Management of single cargo logistics, which are based on a @{Wrapper.Unit} object. +--- **Cargo** - Management of single cargo logistics, which are based on a UNIT object. -- -- === -- @@ -47792,7 +56379,7 @@ do -- CARGO_UNIT end end -- CARGO_UNIT ---- **Cargo** -- Management of single cargo crates, which are based on a @{Static} object. The cargo can only be slingloaded. +--- **Cargo** - Management of single cargo crates, which are based on a STATIC object. The cargo can only be slingloaded. -- -- === -- @@ -48062,7 +56649,7 @@ do -- CARGO_SLINGLOAD end end ---- **Cargo** -- Management of single cargo crates, which are based on a @{Static} object. +--- **Cargo** - Management of single cargo crates, which are based on a STATIC object. -- -- === -- @@ -48394,7 +56981,7 @@ do -- CARGO_CRATE end ---- **Cargo** - Management of grouped cargo logistics, which are based on a @{Wrapper.Group} object. +--- **Cargo** - Management of grouped cargo logistics, which are based on a GROUP object. -- -- === -- @@ -48443,7 +57030,7 @@ do -- CARGO_GROUP --- CARGO_GROUP constructor. -- This make a new CARGO_GROUP from a @{Wrapper.Group} object. - -- It will "ungroup" the group object within the sim, and will create a @{Set} of individual Unit objects. + -- It will "ungroup" the group object within the sim, and will create a @{Core.Set} of individual Unit objects. -- @param #CARGO_GROUP self -- @param Wrapper.Group#GROUP CargoGroup Group to be transported as cargo. -- @param #string Type Cargo type, e.g. "Infantry". This is the type used in SET_CARGO:New():FilterTypes("Infantry") to define the valid cargo groups of the set. @@ -49123,7 +57710,7 @@ do -- CARGO_GROUP end end - --- Check if the first element of the CargoGroup is the given @{Zone}. + --- Check if the first element of the CargoGroup is the given @{Core.Zone}. -- @param #CARGO_GROUP self -- @param Core.Zone#ZONE_BASE Zone -- @return #boolean **true** if the first element of the CargoGroup is in the Zone @@ -49162,12 +57749,12 @@ do -- CARGO_GROUP end -- CARGO_GROUP ---- **Functional** - Administer the scoring of player achievements, and create a CSV file logging the scoring events for use at team or squadron websites. --- +--- **Functional** - Administer the scoring of player achievements, file and log the scoring events for use at websites. +-- -- === --- +-- -- ## Features: --- +-- -- * Set the scoring scales based on threat level. -- * Positive scores and negative scores. -- * A contribution model to score achievements. @@ -49176,171 +57763,169 @@ end -- CARGO_GROUP -- * Score the hits and destroys of units. -- * Score the hits and destroys of statics. -- * Score the hits and destroys of scenery. --- * Log scores into a CSV file. +-- * (optional) Log scores into a CSV file. -- * Connect to a remote server using JSON and IP. --- +-- -- === --- +-- -- ## Missions: --- +-- -- [SCO - Scoring](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/SCO%20-%20Scoring) --- +-- -- === --- --- Administers the scoring of player achievements, +-- +-- Administers the scoring of player achievements, -- and creates a CSV file logging the scoring events and results for use at team or squadron websites. --- --- SCORING automatically calculates the threat level of the objects hit and destroyed by players, +-- +-- SCORING automatically calculates the threat level of the objects hit and destroyed by players, -- which can be @{Wrapper.Unit}, @{Static) and @{Scenery} objects. --- --- Positive score points are granted when enemy or neutral targets are destroyed. --- Negative score points or penalties are given when a friendly target is hit or destroyed. +-- +-- Positive score points are granted when enemy or neutral targets are destroyed. +-- Negative score points or penalties are given when a friendly target is hit or destroyed. -- This brings a lot of dynamism in the scoring, where players need to take care to inflict damage on the right target. -- By default, penalties weight heavier in the scoring, to ensure that players don't commit fratricide. -- The total score of the player is calculated by **adding the scores minus the penalties**. --- +-- -- ![Banner Image](..\Presentations\SCORING\Dia4.JPG) --- +-- -- The score value is calculated based on the **threat level of the player** and the **threat level of the target**. --- A calculated score takes the threat level of the target divided by a balanced threat level of the player unit. --- As such, if the threat level of the target is high, and the player threat level is low, a higher score will be given than +-- A calculated score takes the threat level of the target divided by a balanced threat level of the player unit. +-- As such, if the threat level of the target is high, and the player threat level is low, a higher score will be given than -- if the threat level of the player would be high too. --- +-- -- ![Banner Image](..\Presentations\SCORING\Dia5.JPG) --- +-- -- When multiple players hit the same target, and finally succeed in destroying the target, then each player who contributed to the target -- destruction, will receive a score. This is important for targets that require significant damage before it can be destroyed, like -- ships or heavy planes. --- +-- -- ![Banner Image](..\Presentations\SCORING\Dia13.JPG) --- +-- -- Optionally, the score values can be **scaled** by a **scale**. Specific scales can be set for positive cores or negative penalties. -- The default range of the scores granted is a value between 0 and 10. The default range of penalties given is a value between 0 and 30. --- +-- -- ![Banner Image](..\Presentations\SCORING\Dia7.JPG) --- +-- -- **Additional scores** can be granted to **specific objects**, when the player(s) destroy these objects. --- +-- -- ![Banner Image](..\Presentations\SCORING\Dia9.JPG) --- --- Various @{Zone}s can be defined for which scores are also granted when objects in that @{Zone} are destroyed. +-- +-- Various @{Core.Zone}s can be defined for which scores are also granted when objects in that @{Core.Zone} are destroyed. -- This is **specifically useful** to designate **scenery targets on the map** that will generate points when destroyed. --- --- With a small change in MissionScripting.lua, the scoring results can also be logged in a **CSV file**. +-- +-- With a small change in MissionScripting.lua, the scoring results can also be logged in a **CSV file**. -- These CSV files can be used to: --- +-- -- * Upload scoring to a database or a BI tool to publish the scoring results to the player community. -- * Upload scoring in an (online) Excel like tool, using pivot tables and pivot charts to show mission results. --- * Share scoring amoung players after the mission to discuss mission results. --- +-- * Share scoring among players after the mission to discuss mission results. +-- -- Scores can be **reported**. **Menu options** are automatically added to **each player group** when a player joins a client slot or a CA unit. --- Use the radio menu F10 to consult the scores while running the mission. +-- Use the radio menu F10 to consult the scores while running the mission. -- Scores can be reported for your user, or an overall score can be reported of all players currently active in the mission. --- +-- -- === --- +-- -- ### Authors: **FlightControl** --- +-- -- ### Contributions: --- +-- -- * **Wingthor (TAW)**: Testing & Advice. -- * **Dutch-Baron (TAW)**: Testing & Advice. -- * **[Whisper](http://forums.eagle.ru/member.php?u=3829): Testing and Advice. --- --- === --- +-- +-- === +-- -- @module Functional.Scoring -- @image Scoring.JPG - --- @type SCORING -- @field Players A collection of the current players that have joined the game. -- @extends Core.Base#BASE --- SCORING class --- +-- -- # Constructor: --- +-- -- local Scoring = SCORING:New( "Scoring File" ) --- --- +-- -- # Set the destroy score or penalty scale: --- +-- -- Score scales can be set for scores granted when enemies or friendlies are destroyed. --- Use the method @{#SCORING.SetScaleDestroyScore}() to set the scale of enemy destroys (positive destroys). +-- Use the method @{#SCORING.SetScaleDestroyScore}() to set the scale of enemy destroys (positive destroys). -- Use the method @{#SCORING.SetScaleDestroyPenalty}() to set the scale of friendly destroys (negative destroys). --- +-- -- local Scoring = SCORING:New( "Scoring File" ) -- Scoring:SetScaleDestroyScore( 10 ) -- Scoring:SetScaleDestroyPenalty( 40 ) --- +-- -- The above sets the scale for valid scores to 10. So scores will be given in a scale from 0 to 10. -- The penalties will be given in a scale from 0 to 40. --- +-- -- # Define special targets that will give extra scores: --- +-- -- Special targets can be set that will give extra scores to the players when these are destroyed. --- Use the methods @{#SCORING.AddUnitScore}() and @{#SCORING.RemoveUnitScore}() to specify a special additional score for a specific @{Wrapper.Unit}s. --- Use the methods @{#SCORING.AddStaticScore}() and @{#SCORING.RemoveStaticScore}() to specify a special additional score for a specific @{Static}s. --- Use the method @{#SCORING.SetGroupGroup}() to specify a special additional score for a specific @{Wrapper.Group}s. --- +-- Use the methods @{#SCORING.AddUnitScore}() and @{#SCORING.RemoveUnitScore}() to specify a special additional score for a specific @{Wrapper.Unit}s. +-- Use the methods @{#SCORING.AddStaticScore}() and @{#SCORING.RemoveStaticScore}() to specify a special additional score for a specific @{Wrapper.Static}s. +-- Use the method @{#SCORING.SetGroupGroup}() to specify a special additional score for a specific @{Wrapper.Group}s. +-- -- local Scoring = SCORING:New( "Scoring File" ) -- Scoring:AddUnitScore( UNIT:FindByName( "Unit #001" ), 200 ) -- Scoring:AddStaticScore( STATIC:FindByName( "Static #1" ), 100 ) --- +-- -- The above grants an additional score of 200 points for Unit #001 and an additional 100 points of Static #1 if these are destroyed. -- Note that later in the mission, one can remove these scores set, for example, when the a goal achievement time limit is over. -- For example, this can be done as follows: --- +-- -- Scoring:RemoveUnitScore( UNIT:FindByName( "Unit #001" ) ) --- +-- -- # Define destruction zones that will give extra scores: --- +-- -- Define zones of destruction. Any object destroyed within the zone of the given category will give extra points. --- Use the method @{#SCORING.AddZoneScore}() to add a @{Zone} for additional scoring. --- Use the method @{#SCORING.RemoveZoneScore}() to remove a @{Zone} for additional scoring. --- There are interesting variations that can be achieved with this functionality. For example, if the @{Zone} is a @{Core.Zone#ZONE_UNIT}, --- then the zone is a moving zone, and anything destroyed within that @{Zone} will generate points. --- The other implementation could be to designate a scenery target (a building) in the mission editor surrounded by a @{Zone}, +-- Use the method @{#SCORING.AddZoneScore}() to add a @{Core.Zone} for additional scoring. +-- Use the method @{#SCORING.RemoveZoneScore}() to remove a @{Core.Zone} for additional scoring. +-- There are interesting variations that can be achieved with this functionality. For example, if the @{Core.Zone} is a @{Core.Zone#ZONE_UNIT}, +-- then the zone is a moving zone, and anything destroyed within that @{Core.Zone} will generate points. +-- The other implementation could be to designate a scenery target (a building) in the mission editor surrounded by a @{Core.Zone}, -- just large enough around that building. --- +-- -- # Add extra Goal scores upon an event or a condition: --- +-- -- A mission has goals and achievements. The scoring system provides an API to set additional scores when a goal or achievement event happens. -- Use the method @{#SCORING.AddGoalScore}() to add a score for a Player at any time in your mission. --- +-- -- # (Decommissioned) Configure fratricide level. --- --- **This functionality is decomissioned until the DCS bug concerning Unit:destroy() not being functional in multi player for player units has been fixed by ED**. --- +-- +-- **This functionality is decommissioned until the DCS bug concerning Unit:destroy() not being functional in multi player for player units has been fixed by ED**. +-- -- When a player commits too much damage to friendlies, his penalty score will reach a certain level. --- Use the method @{#SCORING.SetFratricide}() to define the level when a player gets kicked. --- By default, the fratricide level is the default penalty mutiplier * 2 for the penalty score. --- +-- Use the method @{#SCORING.SetFratricide}() to define the level when a player gets kicked. +-- By default, the fratricide level is the default penalty multiplier * 2 for the penalty score. +-- -- # Penalty score when a player changes the coalition. --- +-- -- When a player changes the coalition, he can receive a penalty score. -- Use the method @{#SCORING.SetCoalitionChangePenalty}() to define the penalty when a player changes coalition. --- By default, the penalty for changing coalition is the default penalty scale. --- +-- By default, the penalty for changing coalition is the default penalty scale. +-- -- # Define output CSV files. --- +-- -- The CSV file is given the name of the string given in the @{#SCORING.New}{} constructor, followed by the .csv extension. -- The file is incrementally saved in the **\\Saved Games\\DCS\\Logs** folder, and has a time stamp indicating each mission run. -- See the following example: --- +-- -- local ScoringFirstMission = SCORING:New( "FirstMission" ) -- local ScoringSecondMission = SCORING:New( "SecondMission" ) --- +-- -- The above documents that 2 Scoring objects are created, ScoringFirstMission and ScoringSecondMission. --- --- ### **IMPORTANT!!!* +-- +-- ### **IMPORTANT!!!* -- In order to allow DCS world to write CSV files, you need to adapt a configuration file in your DCS world installation **on the server**. -- For this, browse to the **missionscripting.lua** file in your DCS world installation folder. -- For me, this installation folder is in _D:\\Program Files\\Eagle Dynamics\\DCS World\Scripts_. --- +-- -- Edit a few code lines in the MissionScripting.lua file. Comment out the lines **os**, **io** and **lfs**: --- +-- -- do -- --sanitizeModule('os') -- --sanitizeModule('io') @@ -49348,90 +57933,88 @@ end -- CARGO_GROUP -- require = nil -- loadlib = nil -- end --- --- When these lines are not sanitized, functions become available to check the time, and to write files to your system at the above specified location. +-- +-- When these lines are not sanitized, functions become available to check the time, and to write files to your system at the above specified location. -- Note that the MissionScripting.lua file provides a warning. So please beware of this warning as outlined by Eagle Dynamics! --- +-- -- --Sanitize Mission Scripting environment --- --This makes unavailable some unsecure functions. --- --Mission downloaded from server to client may contain potentialy harmful lua code that may use these functions. --- --You can remove the code below and make availble these functions at your own risk. --- +-- --This makes unavailable some unsecure functions. +-- --Mission downloaded from server to client may contain potentially harmful lua code that may use these functions. +-- --You can remove the code below and make available these functions at your own risk. +-- -- The MOOSE designer cannot take any responsibility of any damage inflicted as a result of the de-sanitization. -- That being said, I hope that the SCORING class provides you with a great add-on to score your squad mates achievements. --- +-- -- # Configure messages. --- +-- -- When players hit or destroy targets, messages are sent. -- Various methods exist to configure: --- +-- -- * Which messages are sent upon the event. -- * Which audience receives the message. --- +-- -- ## Configure the messages sent upon the event. --- +-- -- Use the following methods to configure when to send messages. By default, all messages are sent. --- +-- -- * @{#SCORING.SetMessagesHit}(): Configure to send messages after a target has been hit. -- * @{#SCORING.SetMessagesDestroy}(): Configure to send messages after a target has been destroyed. -- * @{#SCORING.SetMessagesAddon}(): Configure to send messages for additional score, after a target has been destroyed. -- * @{#SCORING.SetMessagesZone}(): Configure to send messages for additional score, after a target has been destroyed within a given zone. --- +-- -- ## Configure the audience of the messages. --- +-- -- Use the following methods to configure the audience of the messages. By default, the messages are sent to all players in the mission. --- +-- -- * @{#SCORING.SetMessagesToAll}(): Configure to send messages to all players. -- * @{#SCORING.SetMessagesToCoalition}(): Configure to send messages to only those players within the same coalition as the player. -- -- === --- +-- -- @field #SCORING SCORING = { ClassName = "SCORING", ClassID = 0, Players = {}, + AutoSave = true, } -local _SCORINGCoalition = - { - [1] = "Red", - [2] = "Blue", - } +local _SCORINGCoalition = { + [1] = "Red", + [2] = "Blue", +} -local _SCORINGCategory = - { - [Unit.Category.AIRPLANE] = "Plane", - [Unit.Category.HELICOPTER] = "Helicopter", - [Unit.Category.GROUND_UNIT] = "Vehicle", - [Unit.Category.SHIP] = "Ship", - [Unit.Category.STRUCTURE] = "Structure", - } +local _SCORINGCategory = { + [Unit.Category.AIRPLANE] = "Plane", + [Unit.Category.HELICOPTER] = "Helicopter", + [Unit.Category.GROUND_UNIT] = "Vehicle", + [Unit.Category.SHIP] = "Ship", + [Unit.Category.STRUCTURE] = "Structure", +} --- Creates a new SCORING object to administer the scoring achieved by players. -- @param #SCORING self -- @param #string GameName The name of the game. This name is also logged in the CSV score file. -- @return #SCORING self -- @usage --- --- -- Define a new scoring object for the mission Gori Valley. --- ScoringObject = SCORING:New( "Gori Valley" ) --- +-- +-- -- Define a new scoring object for the mission Gori Valley. +-- ScoringObject = SCORING:New( "Gori Valley" ) +-- function SCORING:New( GameName ) -- Inherits from BASE local self = BASE:Inherit( self, BASE:New() ) -- #SCORING - - if GameName then + + if GameName then self.GameName = GameName else error( "A game name must be given to register the scoring results" ) end - - + -- Additional Object scores self.ScoringObjects = {} - + -- Additional Zone scores. self.ScoringZones = {} @@ -49441,45 +58024,45 @@ function SCORING:New( GameName ) self:SetMessagesDestroy( true ) self:SetMessagesScore( true ) self:SetMessagesZone( true ) - + -- Scales self:SetScaleDestroyScore( 10 ) self:SetScaleDestroyPenalty( 30 ) -- Default fratricide penalty level (maximum penalty that can be assigned to a player before he gets kicked). self:SetFratricide( self.ScaleDestroyPenalty * 3 ) + self.penaltyonfratricide = true -- Default penalty when a player changes coalition. self:SetCoalitionChangePenalty( self.ScaleDestroyPenalty ) + self.penaltyoncoalitionchange = true self:SetDisplayMessagePrefix() - + -- Event handlers self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) self:HandleEvent( EVENTS.Hit, self._EventOnHit ) self:HandleEvent( EVENTS.Birth ) - --self:HandleEvent( EVENTS.PlayerEnterUnit ) + -- self:HandleEvent( EVENTS.PlayerEnterUnit ) self:HandleEvent( EVENTS.PlayerLeaveUnit ) - - -- During mission startup, especially for single player, + + -- During mission startup, especially for single player, -- iterate the database for the player that has joined, and add him to the scoring, and set the menu. -- But this can only be started one second after the mission has started, so i need to schedule this ... - self.ScoringPlayerScan = BASE:ScheduleOnce( 1, - function() - for PlayerName, PlayerUnit in pairs( _DATABASE:GetPlayerUnits() ) do - self:_AddPlayerFromUnit( PlayerUnit ) - self:SetScoringMenu( PlayerUnit:GetGroup() ) - end + self.ScoringPlayerScan = BASE:ScheduleOnce( 1, function() + for PlayerName, PlayerUnit in pairs( _DATABASE:GetPlayerUnits() ) do + self:_AddPlayerFromUnit( PlayerUnit ) + self:SetScoringMenu( PlayerUnit:GetGroup() ) end - ) - + end ) -- Create the CSV file. + self.AutoSave = true self:OpenCSV( GameName ) return self - + end --- Set a prefix string that will be displayed at each scoring message sent. @@ -49491,7 +58074,6 @@ function SCORING:SetDisplayMessagePrefix( DisplayMessagePrefix ) return self end - --- Set the scale for scoring valid destroys (enemy destroys). -- A default calculated score is a value between 1 and 10. -- The scale magnifies the scores given to the players. @@ -49511,12 +58093,12 @@ end function SCORING:SetScaleDestroyPenalty( Scale ) self.ScaleDestroyPenalty = Scale - + return self end --- Add a @{Wrapper.Unit} for additional scoring when the @{Wrapper.Unit} is destroyed. --- Note that if there was already a @{Wrapper.Unit} declared within the scoring with the same name, +-- Note that if there was already a @{Wrapper.Unit} declared within the scoring with the same name, -- then the old @{Wrapper.Unit} will be replaced with the new @{Wrapper.Unit}. -- @param #SCORING self -- @param Wrapper.Unit#UNIT ScoreUnit The @{Wrapper.Unit} for which the Score needs to be given. @@ -49527,7 +58109,7 @@ function SCORING:AddUnitScore( ScoreUnit, Score ) local UnitName = ScoreUnit:GetName() self.ScoringObjects[UnitName] = Score - + return self end @@ -49540,15 +58122,15 @@ function SCORING:RemoveUnitScore( ScoreUnit ) local UnitName = ScoreUnit:GetName() self.ScoringObjects[UnitName] = nil - + return self end ---- Add a @{Static} for additional scoring when the @{Static} is destroyed. --- Note that if there was already a @{Static} declared within the scoring with the same name, --- then the old @{Static} will be replaced with the new @{Static}. +--- Add a @{Wrapper.Static} for additional scoring when the @{Wrapper.Static} is destroyed. +-- Note that if there was already a @{Wrapper.Static} declared within the scoring with the same name, +-- then the old @{Wrapper.Static} will be replaced with the new @{Wrapper.Static}. -- @param #SCORING self --- @param Wrapper.Static#UNIT ScoreStatic The @{Static} for which the Score needs to be given. +-- @param Wrapper.Static#UNIT ScoreStatic The @{Wrapper.Static} for which the Score needs to be given. -- @param #number Score The Score value. -- @return #SCORING function SCORING:AddStaticScore( ScoreStatic, Score ) @@ -49556,24 +58138,23 @@ function SCORING:AddStaticScore( ScoreStatic, Score ) local StaticName = ScoreStatic:GetName() self.ScoringObjects[StaticName] = Score - + return self end ---- Removes a @{Static} for additional scoring when the @{Static} is destroyed. +--- Removes a @{Wrapper.Static} for additional scoring when the @{Wrapper.Static} is destroyed. -- @param #SCORING self --- @param Wrapper.Static#UNIT ScoreStatic The @{Static} for which the Score needs to be given. +-- @param Wrapper.Static#UNIT ScoreStatic The @{Wrapper.Static} for which the Score needs to be given. -- @return #SCORING function SCORING:RemoveStaticScore( ScoreStatic ) local StaticName = ScoreStatic:GetName() self.ScoringObjects[StaticName] = nil - + return self end - --- Specify a special additional score for a @{Wrapper.Group}. -- @param #SCORING self -- @param Wrapper.Group#GROUP ScoreGroup The @{Wrapper.Group} for which each @{Wrapper.Unit} a Score is given. @@ -49587,15 +58168,15 @@ function SCORING:AddScoreGroup( ScoreGroup, Score ) local UnitName = ScoreUnit:GetName() self.ScoringObjects[UnitName] = Score end - + return self end ---- Add a @{Zone} to define additional scoring when any object is destroyed in that zone. --- Note that if a @{Zone} with the same name is already within the scoring added, the @{Zone} (type) and Score will be replaced! +--- Add a @{Core.Zone} to define additional scoring when any object is destroyed in that zone. +-- Note that if a @{Core.Zone} with the same name is already within the scoring added, the @{Core.Zone} (type) and Score will be replaced! -- This allows for a dynamic destruction zone evolution within your mission. -- @param #SCORING self --- @param Core.Zone#ZONE_BASE ScoreZone The @{Zone} which defines the destruction score perimeters. +-- @param Core.Zone#ZONE_BASE ScoreZone The @{Core.Zone} which defines the destruction score perimeters. -- Note that a zone can be a polygon or a moving zone. -- @param #number Score The Score value. -- @return #SCORING @@ -49606,15 +58187,15 @@ function SCORING:AddZoneScore( ScoreZone, Score ) self.ScoringZones[ZoneName] = {} self.ScoringZones[ZoneName].ScoreZone = ScoreZone self.ScoringZones[ZoneName].Score = Score - + return self end ---- Remove a @{Zone} for additional scoring. --- The scoring will search if any @{Zone} is added with the given name, and will remove that zone from the scoring. +--- Remove a @{Core.Zone} for additional scoring. +-- The scoring will search if any @{Core.Zone} is added with the given name, and will remove that zone from the scoring. -- This allows for a dynamic destruction zone evolution within your mission. -- @param #SCORING self --- @param Core.Zone#ZONE_BASE ScoreZone The @{Zone} which defines the destruction score perimeters. +-- @param Core.Zone#ZONE_BASE ScoreZone The @{Core.Zone} which defines the destruction score perimeters. -- Note that a zone can be a polygon or a moving zone. -- @return #SCORING function SCORING:RemoveZoneScore( ScoreZone ) @@ -49622,14 +58203,13 @@ function SCORING:RemoveZoneScore( ScoreZone ) local ZoneName = ScoreZone:GetName() self.ScoringZones[ZoneName] = nil - + return self end - --- Configure to send messages after a target has been hit. -- @param #SCORING self --- @param #boolean OnOff If true is given, the messages are sent. +-- @param #boolean OnOff If true is given, the messages are sent. -- @return #SCORING function SCORING:SetMessagesHit( OnOff ) @@ -49647,7 +58227,7 @@ end --- Configure to send messages after a target has been destroyed. -- @param #SCORING self --- @param #boolean OnOff If true is given, the messages are sent. +-- @param #boolean OnOff If true is given, the messages are sent. -- @return #SCORING function SCORING:SetMessagesDestroy( OnOff ) @@ -49665,7 +58245,7 @@ end --- Configure to send messages after a target has been destroyed and receives additional scores. -- @param #SCORING self --- @param #boolean OnOff If true is given, the messages are sent. +-- @param #boolean OnOff If true is given, the messages are sent. -- @return #SCORING function SCORING:SetMessagesScore( OnOff ) @@ -49683,7 +58263,7 @@ end --- Configure to send messages after a target has been hit in a zone, and additional score is received. -- @param #SCORING self --- @param #boolean OnOff If true is given, the messages are sent. +-- @param #boolean OnOff If true is given, the messages are sent. -- @return #SCORING function SCORING:SetMessagesZone( OnOff ) @@ -49733,10 +58313,9 @@ function SCORING:IfMessagesToCoalition() return self.MessagesAudience == 2 end - --- When a player commits too much damage to friendlies, his penalty score will reach a certain level. --- Use this method to define the level when a player gets kicked. --- By default, the fratricide level is the default penalty mutiplier * 2 for the penalty score. +-- Use this method to define the level when a player gets kicked. +-- By default, the fratricide level is the default penalty multiplier * 2 for the penalty score. -- @param #SCORING self -- @param #number Fratricide The amount of maximum penalty that may be inflicted by a friendly player before he gets kicked. -- @return #SCORING @@ -49746,12 +58325,29 @@ function SCORING:SetFratricide( Fratricide ) return self end +--- Decide if fratricide is leading to penalties (true) or not (false) +-- @param #SCORING self +-- @param #boolean OnOff Switch for Fratricide +-- @return #SCORING +function SCORING:SwitchFratricide( OnOff ) + self.penaltyonfratricide = OnOff + return self +end + +--- Decide if a change of coalition is leading to penalties (true) or not (false) +-- @param #SCORING self +-- @param #boolean OnOff Switch for Coalition Changes. +-- @return #SCORING +function SCORING:SwitchTreason( OnOff ) + self.penaltyoncoalitionchange = OnOff + return self +end --- When a player changes the coalition, he can receive a penalty score. -- Use the method @{#SCORING.SetCoalitionChangePenalty}() to define the penalty when a player changes coalition. --- By default, the penalty for changing coalition is the default penalty scale. +-- By default, the penalty for changing coalition is the default penalty scale. -- @param #SCORING self --- @param #number CoalitionChangePenalty The amount of penalty that is given. +-- @param #number CoalitionChangePenalty The amount of penalty that is given. -- @return #SCORING function SCORING:SetCoalitionChangePenalty( CoalitionChangePenalty ) @@ -49759,20 +58355,18 @@ function SCORING:SetCoalitionChangePenalty( CoalitionChangePenalty ) return self end - --- Sets the scoring menu. -- @param #SCORING self -- @return #SCORING function SCORING:SetScoringMenu( ScoringGroup ) - local Menu = MENU_GROUP:New( ScoringGroup, 'Scoring and Statistics' ) - local ReportGroupSummary = MENU_GROUP_COMMAND:New( ScoringGroup, 'Summary report players in group', Menu, SCORING.ReportScoreGroupSummary, self, ScoringGroup ) - local ReportGroupDetailed = MENU_GROUP_COMMAND:New( ScoringGroup, 'Detailed report players in group', Menu, SCORING.ReportScoreGroupDetailed, self, ScoringGroup ) - local ReportToAllSummary = MENU_GROUP_COMMAND:New( ScoringGroup, 'Summary report all players', Menu, SCORING.ReportScoreAllSummary, self, ScoringGroup ) - self:SetState( ScoringGroup, "ScoringMenu", Menu ) + local Menu = MENU_GROUP:New( ScoringGroup, 'Scoring and Statistics' ) + local ReportGroupSummary = MENU_GROUP_COMMAND:New( ScoringGroup, 'Summary report players in group', Menu, SCORING.ReportScoreGroupSummary, self, ScoringGroup ) + local ReportGroupDetailed = MENU_GROUP_COMMAND:New( ScoringGroup, 'Detailed report players in group', Menu, SCORING.ReportScoreGroupDetailed, self, ScoringGroup ) + local ReportToAllSummary = MENU_GROUP_COMMAND:New( ScoringGroup, 'Summary report all players', Menu, SCORING.ReportScoreAllSummary, self, ScoringGroup ) + self:SetState( ScoringGroup, "ScoringMenu", Menu ) return self end - --- Add a new player entering a Unit. -- @param #SCORING self -- @param Wrapper.Unit#UNIT UnitData @@ -49811,49 +58405,46 @@ function SCORING:_AddPlayerFromUnit( UnitData ) if not self.Players[PlayerName].UnitCoalition then self.Players[PlayerName].UnitCoalition = UnitCoalition else - if self.Players[PlayerName].UnitCoalition ~= UnitCoalition then - self.Players[PlayerName].Penalty = self.Players[PlayerName].Penalty + 50 + -- TODO: switch for coalition changes, make penalty alterable + if self.Players[PlayerName].UnitCoalition ~= UnitCoalition and self.penaltyoncoalitionchange then + self.Players[PlayerName].Penalty = self.Players[PlayerName].Penalty + self.CoalitionChangePenalty or 50 self.Players[PlayerName].PenaltyCoalition = self.Players[PlayerName].PenaltyCoalition + 1 MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' changed coalition from " .. _SCORINGCoalition[self.Players[PlayerName].UnitCoalition] .. " to " .. _SCORINGCoalition[UnitCoalition] .. - "(changed " .. self.Players[PlayerName].PenaltyCoalition .. " times the coalition). 50 Penalty points added.", + "(changed " .. self.Players[PlayerName].PenaltyCoalition .. " times the coalition). ".. self.CoalitionChangePenalty .."Penalty points added.", MESSAGE.Type.Information ):ToAll() - self:ScoreCSV( PlayerName, "", "COALITION_PENALTY", 1, -50, self.Players[PlayerName].UnitName, _SCORINGCoalition[self.Players[PlayerName].UnitCoalition], _SCORINGCategory[self.Players[PlayerName].UnitCategory], self.Players[PlayerName].UnitType, + self:ScoreCSV( PlayerName, "", "COALITION_PENALTY", 1, -1*self.CoalitionChangePenalty, self.Players[PlayerName].UnitName, _SCORINGCoalition[self.Players[PlayerName].UnitCoalition], _SCORINGCategory[self.Players[PlayerName].UnitCategory], self.Players[PlayerName].UnitType, UnitName, _SCORINGCoalition[UnitCoalition], _SCORINGCategory[UnitCategory], UnitData:GetTypeName() ) end end - + self.Players[PlayerName].UnitName = UnitName self.Players[PlayerName].UnitCoalition = UnitCoalition self.Players[PlayerName].UnitCategory = UnitCategory self.Players[PlayerName].UnitType = UnitTypeName - self.Players[PlayerName].UNIT = UnitData + self.Players[PlayerName].UNIT = UnitData self.Players[PlayerName].ThreatLevel = UnitThreatLevel self.Players[PlayerName].ThreatType = UnitThreatType - - -- TODO: DCS bug concerning Units with skill level client don't get destroyed in multi player. This logic is deactivated until this bug gets fixed. - --[[ - if self.Players[PlayerName].Penalty > 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 - ):ToAll() + MESSAGE.Type.Information ) + :ToAll() self.Players[PlayerName].PenaltyWarning = self.Players[PlayerName].PenaltyWarning + 1 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() + MESSAGE.Type.Information ) + :ToAll() UnitData:GetGroup():Destroy() end - --]] - end end - --- Add a goal score for a player. -- The method takes the Player name for which the Goal score needs to be set. -- The GoalTag is a string or identifier that is taken into the CSV file scoring log to identify the goal. @@ -49869,28 +58460,28 @@ function SCORING:AddGoalScorePlayer( PlayerName, GoalTag, Text, Score ) self:F( { PlayerName, PlayerName, GoalTag, Text, Score } ) -- PlayerName can be nil, if the Unit with the player crashed or due to another reason. - if PlayerName then + if PlayerName then local PlayerData = self.Players[PlayerName] PlayerData.Goals[GoalTag] = PlayerData.Goals[GoalTag] or { Score = 0 } - PlayerData.Goals[GoalTag].Score = PlayerData.Goals[GoalTag].Score + Score + PlayerData.Goals[GoalTag].Score = PlayerData.Goals[GoalTag].Score + Score PlayerData.Score = PlayerData.Score + Score - - MESSAGE:NewType( self.DisplayMessagePrefix .. Text, MESSAGE.Type.Information ):ToAll() - + + MESSAGE:NewType( self.DisplayMessagePrefix .. Text, + MESSAGE.Type.Information ) + :ToAll() + self:ScoreCSV( PlayerName, "", "GOAL_" .. string.upper( GoalTag ), 1, Score, nil ) end end - - --- Add a goal score for a player. -- The method takes the PlayerUnit for which the Goal score needs to be set. -- The GoalTag is a string or identifier that is taken into the CSV file scoring log to identify the goal. -- A free text can be given that is shown to the players. -- The Score can be both positive and negative. -- @param #SCORING self --- @param Wrapper.Unit#UNIT PlayerUnit The @{Wrapper.Unit} of the Player. Other Properties for the scoring are taken from this PlayerUnit, like coalition, type etc. +-- @param Wrapper.Unit#UNIT PlayerUnit The @{Wrapper.Unit} of the Player. Other Properties for the scoring are taken from this PlayerUnit, like coalition, type etc. -- @param #string GoalTag The string or identifier that is used in the CSV file to identify the goal (sort or group later in Excel). -- @param #string Text A free text that is shown to the players. -- @param #number Score The score can be both positive or negative ( Penalty ). @@ -49901,20 +58492,21 @@ function SCORING:AddGoalScore( PlayerUnit, GoalTag, Text, Score ) self:F( { PlayerUnit.UnitName, PlayerName, GoalTag, Text, Score } ) -- PlayerName can be nil, if the Unit with the player crashed or due to another reason. - if PlayerName then + if PlayerName then local PlayerData = self.Players[PlayerName] PlayerData.Goals[GoalTag] = PlayerData.Goals[GoalTag] or { Score = 0 } - PlayerData.Goals[GoalTag].Score = PlayerData.Goals[GoalTag].Score + Score + PlayerData.Goals[GoalTag].Score = PlayerData.Goals[GoalTag].Score + Score PlayerData.Score = PlayerData.Score + Score - - MESSAGE:NewType( self.DisplayMessagePrefix .. Text, MESSAGE.Type.Information ):ToAll() - + + MESSAGE:NewType( self.DisplayMessagePrefix .. Text, + MESSAGE.Type.Information ) + :ToAll() + self:ScoreCSV( PlayerName, "", "GOAL_" .. string.upper( GoalTag ), 1, Score, PlayerUnit:GetName() ) end end - --- Registers Scores the players completing a Mission Task. -- @param #SCORING self -- @param Tasking.Mission#MISSION Mission @@ -49929,23 +58521,25 @@ function SCORING:_AddMissionTaskScore( Mission, PlayerUnit, Text, Score ) self:F( { Mission:GetName(), PlayerUnit.UnitName, PlayerName, Text, Score } ) -- PlayerName can be nil, if the Unit with the player crashed or due to another reason. - if PlayerName then + if PlayerName then local PlayerData = self.Players[PlayerName] - + if not PlayerData.Mission[MissionName] then PlayerData.Mission[MissionName] = {} PlayerData.Mission[MissionName].ScoreTask = 0 PlayerData.Mission[MissionName].ScoreMission = 0 end - + self:T( PlayerName ) self:T( PlayerData.Mission[MissionName] ) - + PlayerData.Score = self.Players[PlayerName].Score + Score PlayerData.Mission[MissionName].ScoreTask = self.Players[PlayerName].Mission[MissionName].ScoreTask + Score - - MESSAGE:NewType( self.DisplayMessagePrefix .. Mission:GetText() .. " : " .. Text .. " Score: " .. Score, MESSAGE.Type.Information ):ToAll() - + + MESSAGE:NewType( self.DisplayMessagePrefix .. Mission:GetText() .. " : " .. Text .. " Score: " .. Score, + MESSAGE.Type.Information ) + :ToAll() + self:ScoreCSV( PlayerName, "", "TASK_" .. MissionName:gsub( ' ', '_' ), 1, Score, PlayerUnit:GetName() ) end end @@ -49963,21 +58557,21 @@ function SCORING:_AddMissionGoalScore( Mission, PlayerName, Text, Score ) self:F( { Mission:GetName(), PlayerName, Text, Score } ) -- PlayerName can be nil, if the Unit with the player crashed or due to another reason. - if PlayerName then + if PlayerName then local PlayerData = self.Players[PlayerName] - + if not PlayerData.Mission[MissionName] then PlayerData.Mission[MissionName] = {} PlayerData.Mission[MissionName].ScoreTask = 0 PlayerData.Mission[MissionName].ScoreMission = 0 end - + self:T( PlayerName ) self:T( PlayerData.Mission[MissionName] ) - + PlayerData.Score = self.Players[PlayerName].Score + Score PlayerData.Mission[MissionName].ScoreTask = self.Players[PlayerName].Mission[MissionName].ScoreTask + Score - + MESSAGE:NewType( string.format( "%s%s: %s! Player %s receives %d score!", self.DisplayMessagePrefix, Mission:GetText(), Text, PlayerName, Score ), MESSAGE.Type.Information ):ToAll() self:ScoreCSV( PlayerName, "", "TASK_" .. MissionName:gsub( ' ', '_' ), 1, Score ) @@ -49991,7 +58585,7 @@ end -- @param #string Text -- @param #number Score function SCORING:_AddMissionScore( Mission, Text, Score ) - + local MissionName = Mission:GetName() self:F( { Mission, Text, Score } ) @@ -50005,32 +58599,30 @@ function SCORING:_AddMissionScore( Mission, Text, Score ) PlayerData.Score = PlayerData.Score + Score PlayerData.Mission[MissionName].ScoreMission = PlayerData.Mission[MissionName].ScoreMission + Score - MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' has " .. Text .. " in " .. Mission:GetText() .. ". " .. - Score .. " mission score!", - MESSAGE.Type.Information ):ToAll() + MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' has " .. Text .. " in " .. Mission:GetText() .. ". " .. Score .. " mission score!", + MESSAGE.Type.Information ) + :ToAll() self:ScoreCSV( PlayerName, "", "MISSION_" .. MissionName:gsub( ' ', '_' ), 1, Score ) end end end - - --- Handles the OnPlayerEnterUnit event for the scoring. -- @param #SCORING self -- @param Core.Event#EVENTDATA Event ---function SCORING:OnEventPlayerEnterUnit( Event ) +-- function SCORING:OnEventPlayerEnterUnit( Event ) -- if Event.IniUnit then -- self:_AddPlayerFromUnit( Event.IniUnit ) -- self:SetScoringMenu( Event.IniGroup ) -- end ---end +-- end --- Handles the OnBirth event for the scoring. -- @param #SCORING self -- @param Core.Event#EVENTDATA Event function SCORING:OnEventBirth( Event ) - + if Event.IniUnit then if Event.IniObjectCategory == 1 then local PlayerName = Event.IniUnit:GetPlayerName() @@ -50050,12 +58642,11 @@ function SCORING:OnEventPlayerLeaveUnit( Event ) local Menu = self:GetState( Event.IniUnit:GetGroup(), "ScoringMenu" ) -- Core.Menu#MENU_GROUP if Menu then -- TODO: Check if this fixes #281. - --Menu:Remove() + -- Menu:Remove() end end end - --- Handles the OnHit event for the scoring. -- @param #SCORING self -- @param Core.Event#EVENTDATA Event @@ -50100,9 +58691,9 @@ function SCORING:_EventOnHit( Event ) InitPlayerName = Event.IniPlayerName InitCoalition = Event.IniCoalition - --TODO: Workaround Client DCS Bug - --InitCategory = InitUnit:getCategory() - --InitCategory = InitUnit:getDesc().category + -- TODO: Workaround Client DCS Bug + -- InitCategory = InitUnit:getCategory() + -- InitCategory = InitUnit:getDesc().category InitCategory = Event.IniCategory InitType = Event.IniTypeName @@ -50110,10 +58701,9 @@ function SCORING:_EventOnHit( Event ) InitUnitCategory = _SCORINGCategory[InitCategory] InitUnitType = InitType - self:T( { InitUnitName, InitGroupName, InitPlayerName, InitCoalition, InitCategory, InitType , InitUnitCoalition, InitUnitCategory, InitUnitType } ) + self:T( { InitUnitName, InitGroupName, InitPlayerName, InitCoalition, InitCategory, InitType, InitUnitCoalition, InitUnitCategory, InitUnitType } ) end - if Event.TgtDCSUnit then TargetUnit = Event.TgtDCSUnit @@ -50124,9 +58714,9 @@ function SCORING:_EventOnHit( Event ) TargetPlayerName = Event.TgtPlayerName TargetCoalition = Event.TgtCoalition - --TODO: Workaround Client DCS Bug - --TargetCategory = TargetUnit:getCategory() - --TargetCategory = TargetUnit:getDesc().category + -- TODO: Workaround Client DCS Bug + -- TargetCategory = TargetUnit:getCategory() + -- TargetCategory = TargetUnit:getDesc().category TargetCategory = Event.TgtCategory TargetType = Event.TgtTypeName @@ -50145,21 +58735,21 @@ function SCORING:_EventOnHit( Event ) end self:T( "Hitting Something" ) - + -- What is he hitting? if TargetCategory then - + -- A target got hit, score it. -- Player contains the score data from self.Players[InitPlayerName] local Player = self.Players[InitPlayerName] - + -- Ensure there is a hit table per TargetCategory and TargetUnitName. Player.Hit[TargetCategory] = Player.Hit[TargetCategory] or {} Player.Hit[TargetCategory][TargetUnitName] = Player.Hit[TargetCategory][TargetUnitName] or {} - + -- PlayerHit contains the score counters and data per unit that was hit. local PlayerHit = Player.Hit[TargetCategory][TargetUnitName] - + PlayerHit.Score = PlayerHit.Score or 0 PlayerHit.Penalty = PlayerHit.Penalty or 0 PlayerHit.ScoreHit = PlayerHit.ScoreHit or 0 @@ -50171,39 +58761,33 @@ function SCORING:_EventOnHit( Event ) -- Only grant hit scores if there was more than one second between the last hit. if timer.getTime() - PlayerHit.TimeStamp > 1 then PlayerHit.TimeStamp = timer.getTime() - + if TargetPlayerName ~= nil then -- It is a player hitting another player ... - + -- Ensure there is a Player to Player hit reference table. Player.HitPlayers[TargetPlayerName] = true end - + local Score = 0 - + if InitCoalition then -- A coalition object was hit. if InitCoalition == TargetCoalition then Player.Penalty = Player.Penalty + 10 PlayerHit.Penalty = PlayerHit.Penalty + 10 PlayerHit.PenaltyHit = PlayerHit.PenaltyHit + 1 - + if TargetPlayerName ~= nil then -- It is a player hitting another player ... - MESSAGE - :NewType( self.DisplayMessagePrefix .. "Player '" .. InitPlayerName .. "' hit friendly player '" .. TargetPlayerName .. "' " .. - TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.PenaltyHit .. " times. " .. - "Penalty: -" .. PlayerHit.Penalty .. ". Score Total:" .. Player.Score - Player.Penalty, - MESSAGE.Type.Update - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) + MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. InitPlayerName .. "' hit friendly player '" .. TargetPlayerName .. "' " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.PenaltyHit .. " times. " .. + "Penalty: -" .. PlayerHit.Penalty .. ". Score Total:" .. Player.Score - Player.Penalty, + MESSAGE.Type.Update ) + :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) else - MESSAGE - :NewType( self.DisplayMessagePrefix .. "Player '" .. InitPlayerName .. "' hit friendly target " .. - TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.PenaltyHit .. " times. " .. - "Penalty: -" .. PlayerHit.Penalty .. ". Score Total:" .. Player.Score - Player.Penalty, - MESSAGE.Type.Update - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) + MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. InitPlayerName .. "' hit friendly target " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.PenaltyHit .. " times. " .. + "Penalty: -" .. PlayerHit.Penalty .. ". Score Total:" .. Player.Score - Player.Penalty, + MESSAGE.Type.Update ) + :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) end self:ScoreCSV( InitPlayerName, TargetPlayerName, "HIT_PENALTY", 1, -10, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) else @@ -50211,33 +58795,26 @@ function SCORING:_EventOnHit( Event ) PlayerHit.Score = PlayerHit.Score + 1 PlayerHit.ScoreHit = PlayerHit.ScoreHit + 1 if TargetPlayerName ~= nil then -- It is a player hitting another player ... - MESSAGE - :NewType( self.DisplayMessagePrefix .. "Player '" .. InitPlayerName .. "' hit enemy player '" .. TargetPlayerName .. "' " .. - TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.ScoreHit .. " times. " .. - "Score: " .. PlayerHit.Score .. ". Score Total:" .. Player.Score - Player.Penalty, - MESSAGE.Type.Update - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) + MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. InitPlayerName .. "' hit enemy player '" .. TargetPlayerName .. "' " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.ScoreHit .. " times. " .. + "Score: " .. PlayerHit.Score .. ". Score Total:" .. Player.Score - Player.Penalty, + MESSAGE.Type.Update ) + :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) else - MESSAGE - :NewType( self.DisplayMessagePrefix .. "Player '" .. InitPlayerName .. "' hit enemy target " .. - TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.ScoreHit .. " times. " .. - "Score: " .. PlayerHit.Score .. ". Score Total:" .. Player.Score - Player.Penalty, - MESSAGE.Type.Update - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) + MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. InitPlayerName .. "' hit enemy target " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.ScoreHit .. " times. " .. + "Score: " .. PlayerHit.Score .. ". Score Total:" .. Player.Score - Player.Penalty, + MESSAGE.Type.Update ) + :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) end self:ScoreCSV( InitPlayerName, TargetPlayerName, "HIT_SCORE", 1, 1, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) end else -- A scenery object was hit. - MESSAGE - :NewType( self.DisplayMessagePrefix .. "Player '" .. InitPlayerName .. "' hit scenery object.", - MESSAGE.Type.Update - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) + MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. InitPlayerName .. "' hit scenery object.", + MESSAGE.Type.Update ) + :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) + self:ScoreCSV( InitPlayerName, "", "HIT_SCORE", 1, 0, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, "", "Scenery", TargetUnitType ) end end @@ -50246,10 +58823,10 @@ function SCORING:_EventOnHit( Event ) elseif InitPlayerName == nil then -- It is an AI hitting a player??? end - + -- It is a weapon initiated by a player, that is hitting something -- This seems to occur only with scenery and static objects. - if Event.WeaponPlayerName ~= nil then + if Event.WeaponPlayerName ~= nil then self:_AddPlayerFromUnit( Event.WeaponUNIT ) if self.Players[Event.WeaponPlayerName] then -- This should normally not happen, but i'll test it anyway. if TargetPlayerName ~= nil then -- It is a player hitting another player ... @@ -50257,21 +58834,21 @@ function SCORING:_EventOnHit( Event ) end self:T( "Hitting Scenery" ) - + -- What is he hitting? if TargetCategory then - + -- A scenery or static got hit, score it. -- Player contains the score data from self.Players[WeaponPlayerName] local Player = self.Players[Event.WeaponPlayerName] - + -- Ensure there is a hit table per TargetCategory and TargetUnitName. Player.Hit[TargetCategory] = Player.Hit[TargetCategory] or {} Player.Hit[TargetCategory][TargetUnitName] = Player.Hit[TargetCategory][TargetUnitName] or {} - + -- PlayerHit contains the score counters and data per unit that was hit. local PlayerHit = Player.Hit[TargetCategory][TargetUnitName] - + PlayerHit.Score = PlayerHit.Score or 0 PlayerHit.Penalty = PlayerHit.Penalty or 0 PlayerHit.ScoreHit = PlayerHit.ScoreHit or 0 @@ -50283,15 +58860,15 @@ function SCORING:_EventOnHit( Event ) -- Only grant hit scores if there was more than one second between the last hit. if timer.getTime() - PlayerHit.TimeStamp > 1 then PlayerHit.TimeStamp = timer.getTime() - + local Score = 0 - + 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 " .. @@ -50306,23 +58883,20 @@ function SCORING:_EventOnHit( Event ) Player.Score = Player.Score + 1 PlayerHit.Score = PlayerHit.Score + 1 PlayerHit.ScoreHit = PlayerHit.ScoreHit + 1 - MESSAGE - :NewType( self.DisplayMessagePrefix .. "Player '" .. Event.WeaponPlayerName .. "' hit enemy target " .. - TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - "Score: +" .. PlayerHit.Score .. " = " .. Player.Score - Player.Penalty, - MESSAGE.Type.Update - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( Event.WeaponCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) + MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. Event.WeaponPlayerName .. "' hit enemy target " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. + "Score: +" .. PlayerHit.Score .. " = " .. Player.Score - Player.Penalty, + MESSAGE.Type.Update ) + :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) + :ToCoalitionIf( Event.WeaponCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) + self:ScoreCSV( Event.WeaponPlayerName, TargetPlayerName, "HIT_SCORE", 1, 1, Event.WeaponName, Event.WeaponCoalition, Event.WeaponCategory, Event.WeaponTypeName, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) end else -- A scenery object was hit. - MESSAGE - :NewType( self.DisplayMessagePrefix .. "Player '" .. Event.WeaponPlayerName .. "' hit scenery object.", - MESSAGE.Type.Update - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) + MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. Event.WeaponPlayerName .. "' hit scenery object.", + MESSAGE.Type.Update ) + :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) + self:ScoreCSV( Event.WeaponPlayerName, "", "HIT_SCORE", 1, 0, Event.WeaponName, Event.WeaponCoalition, Event.WeaponCategory, Event.WeaponTypeName, TargetUnitName, "", "Scenery", TargetUnitType ) end end @@ -50358,8 +58932,8 @@ function SCORING:_EventOnDeadOrCrash( Event ) TargetPlayerName = Event.IniPlayerName TargetCoalition = Event.IniCoalition - --TargetCategory = TargetUnit:getCategory() - --TargetCategory = TargetUnit:getDesc().category -- Workaround + -- TargetCategory = TargetUnit:getCategory() + -- TargetCategory = TargetUnit:getDesc().category -- Workaround TargetCategory = Event.IniCategory TargetType = Event.IniTypeName @@ -50389,10 +58963,10 @@ function SCORING:_EventOnDeadOrCrash( Event ) -- What is the player destroying? if Player and Player.Hit and Player.Hit[TargetCategory] and Player.Hit[TargetCategory][TargetUnitName] and Player.Hit[TargetCategory][TargetUnitName].TimeStamp ~= 0 then -- Was there a hit for this unit for this player before registered??? - + local TargetThreatLevel = Player.Hit[TargetCategory][TargetUnitName].ThreatLevel local TargetThreatType = Player.Hit[TargetCategory][TargetUnitName].ThreatType - + Player.Destroy[TargetCategory] = Player.Destroy[TargetCategory] or {} Player.Destroy[TargetCategory][TargetType] = Player.Destroy[TargetCategory][TargetType] or {} @@ -50400,7 +58974,7 @@ function SCORING:_EventOnDeadOrCrash( Event ) local TargetDestroy = Player.Destroy[TargetCategory][TargetType] TargetDestroy.Score = TargetDestroy.Score or 0 TargetDestroy.ScoreDestroy = TargetDestroy.ScoreDestroy or 0 - TargetDestroy.Penalty = TargetDestroy.Penalty or 0 + TargetDestroy.Penalty = TargetDestroy.Penalty or 0 TargetDestroy.PenaltyDestroy = TargetDestroy.PenaltyDestroy or 0 if TargetCoalition then @@ -50408,85 +58982,73 @@ function SCORING:_EventOnDeadOrCrash( Event ) local ThreatLevelTarget = TargetThreatLevel local ThreatTypeTarget = TargetThreatType local ThreatLevelPlayer = Player.ThreatLevel / 10 + 1 - local ThreatPenalty = math.ceil( ( ThreatLevelTarget / ThreatLevelPlayer ) * self.ScaleDestroyPenalty / 10 ) - self:F( { ThreatLevel = ThreatPenalty, ThreatLevelTarget = ThreatLevelTarget, ThreatTypeTarget = ThreatTypeTarget, ThreatLevelPlayer = ThreatLevelPlayer } ) - + local ThreatPenalty = math.ceil( (ThreatLevelTarget / ThreatLevelPlayer) * self.ScaleDestroyPenalty / 10 ) + self:F( { ThreatLevel = ThreatPenalty, ThreatLevelTarget = ThreatLevelTarget, ThreatTypeTarget = ThreatTypeTarget, ThreatLevelPlayer = ThreatLevelPlayer } ) + Player.Penalty = Player.Penalty + ThreatPenalty TargetDestroy.Penalty = TargetDestroy.Penalty + ThreatPenalty TargetDestroy.PenaltyDestroy = TargetDestroy.PenaltyDestroy + 1 - + if Player.HitPlayers[TargetPlayerName] then -- A player destroyed another player - MESSAGE - :NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' destroyed friendly player '" .. TargetPlayerName .. "' " .. - TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. - "Penalty: -" .. TargetDestroy.Penalty .. " = " .. Player.Score - Player.Penalty, - MESSAGE.Type.Information - ) - :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) + MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' destroyed friendly player '" .. TargetPlayerName .. "' " .. TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. + "Penalty: -" .. TargetDestroy.Penalty .. " = " .. Player.Score - Player.Penalty, + MESSAGE.Type.Information ) + :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) else - MESSAGE - :NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' destroyed friendly target " .. - TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. - "Penalty: -" .. TargetDestroy.Penalty .. " = " .. Player.Score - Player.Penalty, - MESSAGE.Type.Information - ) - :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) + MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' destroyed friendly target " .. TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. + "Penalty: -" .. TargetDestroy.Penalty .. " = " .. Player.Score - Player.Penalty, + MESSAGE.Type.Information ) + :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) end - Destroyed = true self:ScoreCSV( PlayerName, TargetPlayerName, "DESTROY_PENALTY", 1, ThreatPenalty, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + Destroyed = true else local ThreatLevelTarget = TargetThreatLevel local ThreatTypeTarget = TargetThreatType local ThreatLevelPlayer = Player.ThreatLevel / 10 + 1 - local ThreatScore = math.ceil( ( ThreatLevelTarget / ThreatLevelPlayer ) * self.ScaleDestroyScore / 10 ) - - self:F( { ThreatLevel = ThreatScore, ThreatLevelTarget = ThreatLevelTarget, ThreatTypeTarget = ThreatTypeTarget, ThreatLevelPlayer = ThreatLevelPlayer } ) - + local ThreatScore = math.ceil( (ThreatLevelTarget / ThreatLevelPlayer) * self.ScaleDestroyScore / 10 ) + + self:F( { ThreatLevel = ThreatScore, ThreatLevelTarget = ThreatLevelTarget, ThreatTypeTarget = ThreatTypeTarget, ThreatLevelPlayer = ThreatLevelPlayer } ) + Player.Score = Player.Score + ThreatScore TargetDestroy.Score = TargetDestroy.Score + ThreatScore TargetDestroy.ScoreDestroy = TargetDestroy.ScoreDestroy + 1 if Player.HitPlayers[TargetPlayerName] then -- A player destroyed another player - MESSAGE - :NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' destroyed enemy player '" .. TargetPlayerName .. "' " .. - TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. - "Score: +" .. TargetDestroy.Score .. " = " .. Player.Score - Player.Penalty, - MESSAGE.Type.Information - ) - :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) + MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' destroyed enemy player '" .. TargetPlayerName .. "' " .. TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. + "Score: +" .. TargetDestroy.Score .. " = " .. Player.Score - Player.Penalty, + MESSAGE.Type.Information ) + :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) else - MESSAGE - :NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' destroyed enemy " .. - TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. - "Score: +" .. TargetDestroy.Score .. " = " .. Player.Score - Player.Penalty, - MESSAGE.Type.Information - ) - :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) + MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' destroyed enemy " .. TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. + "Score: +" .. TargetDestroy.Score .. " = " .. Player.Score - Player.Penalty, + MESSAGE.Type.Information ) + :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) end - Destroyed = true + self:ScoreCSV( PlayerName, TargetPlayerName, "DESTROY_SCORE", 1, ThreatScore, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - + Destroyed = true + local UnitName = TargetUnit:GetName() local Score = self.ScoringObjects[UnitName] if Score then Player.Score = Player.Score + Score TargetDestroy.Score = TargetDestroy.Score + Score - MESSAGE - :NewType( self.DisplayMessagePrefix .. "Special target '" .. TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. " destroyed! " .. - "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! Total: " .. Player.Score - Player.Penalty, - MESSAGE.Type.Information - ) - :ToAllIf( self:IfMessagesScore() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesScore() and self:IfMessagesToCoalition() ) + MESSAGE:NewType( self.DisplayMessagePrefix .. "Special target '" .. TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. " destroyed! " .. + "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! Total: " .. Player.Score - Player.Penalty, + MESSAGE.Type.Information ) + :ToAllIf( self:IfMessagesScore() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesScore() and self:IfMessagesToCoalition() ) + self:ScoreCSV( PlayerName, TargetPlayerName, "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) Destroyed = true end - + -- Check if there are Zones where the destruction happened. for ZoneName, ScoreZoneData in pairs( self.ScoringZones ) do self:F( { ScoringZone = ScoreZoneData } ) @@ -50495,42 +59057,39 @@ function SCORING:_EventOnDeadOrCrash( Event ) if ScoreZone:IsVec2InZone( TargetUnit:GetVec2() ) then Player.Score = Player.Score + Score TargetDestroy.Score = TargetDestroy.Score + Score - MESSAGE - :NewType( self.DisplayMessagePrefix .. "Target destroyed in zone '" .. ScoreZone:GetName() .. "'." .. - "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! " .. - "Total: " .. Player.Score - Player.Penalty, - MESSAGE.Type.Information ) - :ToAllIf( self:IfMessagesZone() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesZone() and self:IfMessagesToCoalition() ) + MESSAGE:NewType( self.DisplayMessagePrefix .. "Target destroyed in zone '" .. ScoreZone:GetName() .. "'." .. + "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! " .. "Total: " .. Player.Score - Player.Penalty, + MESSAGE.Type.Information ) + :ToAllIf( self:IfMessagesZone() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesZone() and self:IfMessagesToCoalition() ) + self:ScoreCSV( PlayerName, TargetPlayerName, "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) Destroyed = true end end - + end else -- Check if there are Zones where the destruction happened. for ZoneName, ScoreZoneData in pairs( self.ScoringZones ) do - self:F( { ScoringZone = ScoreZoneData } ) + self:F( { ScoringZone = ScoreZoneData } ) local ScoreZone = ScoreZoneData.ScoreZone -- Core.Zone#ZONE_BASE local Score = ScoreZoneData.Score if ScoreZone:IsVec2InZone( TargetUnit:GetVec2() ) then Player.Score = Player.Score + Score TargetDestroy.Score = TargetDestroy.Score + Score - MESSAGE - :NewType( self.DisplayMessagePrefix .. "Scenery destroyed in zone '" .. ScoreZone:GetName() .. "'." .. - "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! " .. - "Total: " .. Player.Score - Player.Penalty, - MESSAGE.Type.Information - ) - :ToAllIf( self:IfMessagesZone() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesZone() and self:IfMessagesToCoalition() ) - Destroyed = true + MESSAGE:NewType( self.DisplayMessagePrefix .. "Scenery destroyed in zone '" .. ScoreZone:GetName() .. "'." .. + "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! " .. "Total: " .. Player.Score - Player.Penalty, + MESSAGE.Type.Information ) + :ToAllIf( self:IfMessagesZone() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesZone() and self:IfMessagesToCoalition() ) + self:ScoreCSV( PlayerName, "", "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, "", "Scenery", TargetUnitType ) + Destroyed = true end end end - + -- Delete now the hit cache if the target was destroyed. -- Otherwise points will be granted every time a target gets killed by the players that hit that target. -- This is only relevant for player to player destroys. @@ -50542,7 +59101,6 @@ function SCORING:_EventOnDeadOrCrash( Event ) end end - --- Produce detailed report of player hit scores. -- @param #SCORING self -- @param #string PlayerName The name of the player. @@ -50584,18 +59142,17 @@ function SCORING:ReportDetailedPlayerHits( PlayerName ) PlayerScore = PlayerScore + Score PlayerPenalty = PlayerPenalty + Penalty else - --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) + -- ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) end end if ScoreMessageHits ~= "" then ScoreMessage = "Hits: " .. ScoreMessageHits end end - + return ScoreMessage, PlayerScore, PlayerPenalty end - --- Produce detailed report of player destroy scores. -- @param #SCORING self -- @param #string PlayerName The name of the player. @@ -50642,7 +59199,7 @@ function SCORING:ReportDetailedPlayerDestroys( PlayerName ) PlayerScore = PlayerScore + Score PlayerPenalty = PlayerPenalty + Penalty else - --ScoreMessageDestroys = ScoreMessageDestroys .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) + -- ScoreMessageDestroys = ScoreMessageDestroys .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) end end if ScoreMessageDestroys ~= "" then @@ -50682,7 +59239,7 @@ function SCORING:ReportDetailedPlayerCoalitionChanges( PlayerName ) ScoreMessage = ScoreMessage .. "Coalition Penalties: " .. ScoreMessageCoalitionChangePenalties end end - + return ScoreMessage, PlayerScore, PlayerPenalty end @@ -50719,7 +59276,7 @@ function SCORING:ReportDetailedPlayerGoals( PlayerName ) ScoreMessage = "Goals: " .. ScoreMessageGoal end end - + return ScoreMessage, PlayerScore, PlayerPenalty end @@ -50757,11 +59314,10 @@ function SCORING:ReportDetailedPlayerMissions( PlayerName ) ScoreMessage = "Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ")" end end - + return ScoreMessage, PlayerScore, PlayerPenalty end - --- Report Group Score Summary -- @param #SCORING self -- @param Wrapper.Group#GROUP PlayerGroup The player group. @@ -50775,11 +59331,11 @@ function SCORING:ReportScoreGroupSummary( PlayerGroup ) for UnitID, PlayerUnit in pairs( PlayerUnits ) do local PlayerUnit = PlayerUnit -- Wrapper.Unit#UNIT local PlayerName = PlayerUnit:GetPlayerName() - + if PlayerName then - + local ReportHits, ScoreHits, PenaltyHits = self:ReportDetailedPlayerHits( PlayerName ) - ReportHits = ReportHits ~= "" and "\n- " .. ReportHits or ReportHits + ReportHits = ReportHits ~= "" and "\n- " .. ReportHits or ReportHits self:F( { ReportHits, ScoreHits, PenaltyHits } ) local ReportDestroys, ScoreDestroys, PenaltyDestroys = self:ReportDetailedPlayerDestroys( PlayerName ) @@ -50797,17 +59353,16 @@ function SCORING:ReportScoreGroupSummary( PlayerGroup ) local ReportMissions, ScoreMissions, PenaltyMissions = self:ReportDetailedPlayerMissions( PlayerName ) ReportMissions = ReportMissions ~= "" and "\n- " .. ReportMissions or ReportMissions self:F( { ReportMissions, ScoreMissions, PenaltyMissions } ) - + local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + PenaltyGoals + PenaltyMissions - - PlayerMessage = - string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )", - PlayerName, - PlayerScore - PlayerPenalty, - PlayerScore, - PlayerPenalty - ) + + PlayerMessage = string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )", + PlayerName, + PlayerScore - PlayerPenalty, + PlayerScore, + PlayerPenalty + ) MESSAGE:NewType( PlayerMessage, MESSAGE.Type.Detailed ):ToGroup( PlayerGroup ) end end @@ -50827,11 +59382,11 @@ function SCORING:ReportScoreGroupDetailed( PlayerGroup ) for UnitID, PlayerUnit in pairs( PlayerUnits ) do local PlayerUnit = PlayerUnit -- Wrapper.Unit#UNIT local PlayerName = PlayerUnit:GetPlayerName() - + if PlayerName then - + local ReportHits, ScoreHits, PenaltyHits = self:ReportDetailedPlayerHits( PlayerName ) - ReportHits = ReportHits ~= "" and "\n- " .. ReportHits or ReportHits + ReportHits = ReportHits ~= "" and "\n- " .. ReportHits or ReportHits self:F( { ReportHits, ScoreHits, PenaltyHits } ) local ReportDestroys, ScoreDestroys, PenaltyDestroys = self:ReportDetailedPlayerDestroys( PlayerName ) @@ -50841,7 +59396,7 @@ function SCORING:ReportScoreGroupDetailed( PlayerGroup ) local ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges = self:ReportDetailedPlayerCoalitionChanges( PlayerName ) ReportCoalitionChanges = ReportCoalitionChanges ~= "" and "\n- " .. ReportCoalitionChanges or ReportCoalitionChanges self:F( { ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges } ) - + local ReportGoals, ScoreGoals, PenaltyGoals = self:ReportDetailedPlayerGoals( PlayerName ) ReportGoals = ReportGoals ~= "" and "\n- " .. ReportGoals or ReportGoals self:F( { ReportGoals, ScoreGoals, PenaltyGoals } ) @@ -50849,9 +59404,9 @@ function SCORING:ReportScoreGroupDetailed( PlayerGroup ) local ReportMissions, ScoreMissions, PenaltyMissions = self:ReportDetailedPlayerMissions( PlayerName ) ReportMissions = ReportMissions ~= "" and "\n- " .. ReportMissions or ReportMissions 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", @@ -50881,13 +59436,13 @@ function SCORING:ReportScoreAllSummary( PlayerGroup ) self:T( { "Summary Score Report of All Players", Players = self.Players } ) for PlayerName, PlayerData in pairs( self.Players ) do - + self:T( { PlayerName = PlayerName, PlayerGroup = PlayerGroup } ) - + if PlayerName then - + local ReportHits, ScoreHits, PenaltyHits = self:ReportDetailedPlayerHits( PlayerName ) - ReportHits = ReportHits ~= "" and "\n- " .. ReportHits or ReportHits + ReportHits = ReportHits ~= "" and "\n- " .. ReportHits or ReportHits self:F( { ReportHits, ScoreHits, PenaltyHits } ) local ReportDestroys, ScoreDestroys, PenaltyDestroys = self:ReportDetailedPlayerDestroys( PlayerName ) @@ -50905,9 +59460,9 @@ function SCORING:ReportScoreAllSummary( PlayerGroup ) local ReportMissions, ScoreMissions, PenaltyMissions = self:ReportDetailedPlayerMissions( PlayerName ) ReportMissions = ReportMissions ~= "" and "\n- " .. ReportMissions or ReportMissions 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 )", @@ -50922,17 +59477,16 @@ function SCORING:ReportScoreAllSummary( PlayerGroup ) end - -function SCORING:SecondsToClock(sSeconds) +function SCORING:SecondsToClock( sSeconds ) local nSeconds = sSeconds if nSeconds == 0 then - --return nil; + -- return nil; return "00:00:00"; else - nHours = string.format("%02.f", math.floor(nSeconds/3600)); - nMins = string.format("%02.f", math.floor(nSeconds/60 - (nHours*60))); - nSecs = string.format("%02.f", math.floor(nSeconds - nHours*3600 - nMins *60)); - return nHours..":"..nMins..":"..nSecs + nHours = string.format( "%02.f", math.floor( nSeconds / 3600 ) ); + nMins = string.format( "%02.f", math.floor( nSeconds / 60 - (nHours * 60) ) ); + nSecs = string.format( "%02.f", math.floor( nSeconds - nHours * 3600 - nMins * 60 ) ); + return nHours .. ":" .. nMins .. ":" .. nSecs end end @@ -50946,8 +59500,8 @@ end -- ScoringObject:OpenCSV( "Player Scores" ) function SCORING:OpenCSV( ScoringCSV ) self:F( ScoringCSV ) - - if lfs and io and os then + + if lfs and io and os and self.AutoSave then if ScoringCSV then self.ScoringCSV = ScoringCSV local fdir = lfs.writedir() .. [[Logs\]] .. self.ScoringCSV .. " " .. os.date( "%Y-%m-%d %H-%M-%S" ) .. ".csv" @@ -50957,9 +59511,9 @@ function SCORING:OpenCSV( ScoringCSV ) error( "Error: Cannot open CSV file in " .. lfs.writedir() ) end - self.CSVFile:write( '"GameName","RunTime","Time","PlayerName","TargetPlayerName","ScoreType","PlayerUnitCoaltion","PlayerUnitCategory","PlayerUnitType","PlayerUnitName","TargetUnitCoalition","TargetUnitCategory","TargetUnitType","TargetUnitName","Times","Score"\n' ) - - self.RunTime = os.date("%y-%m-%d_%H-%M-%S") + self.CSVFile:write( '"GameName","RunTime","Time","PlayerName","TargetPlayerName","ScoreType","PlayerUnitCoalition","PlayerUnitCategory","PlayerUnitType","PlayerUnitName","TargetUnitCoalition","TargetUnitCategory","TargetUnitType","TargetUnitName","Times","Score"\n' ) + + self.RunTime = os.date( "%y-%m-%d_%H-%M-%S" ) else error( "A string containing the CSV file name must be given." ) end @@ -50969,7 +59523,6 @@ function SCORING:OpenCSV( ScoringCSV ) return self end - --- Registers a score for a player. -- @param #SCORING self -- @param #string PlayerName The name of the player. @@ -50987,10 +59540,10 @@ end -- @param #string TargetUnitType The type of the target unit. -- @return #SCORING self function SCORING:ScoreCSV( PlayerName, TargetPlayerName, ScoreType, ScoreTimes, ScoreAmount, PlayerUnitName, PlayerUnitCoalition, PlayerUnitCategory, PlayerUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - --write statistic information to file + -- write statistic information to file local ScoreTime = self:SecondsToClock( timer.getTime() ) PlayerName = PlayerName:gsub( '"', '_' ) - + TargetPlayerName = TargetPlayerName or "" TargetPlayerName = TargetPlayerName:gsub( '"', '_' ) @@ -50999,7 +59552,7 @@ function SCORING:ScoreCSV( PlayerName, TargetPlayerName, ScoreType, ScoreTimes, if PlayerUnit then if not PlayerUnitCategory then - --PlayerUnitCategory = SCORINGCategory[PlayerUnit:getCategory()] + -- PlayerUnitCategory = SCORINGCategory[PlayerUnit:getCategory()] PlayerUnitCategory = _SCORINGCategory[PlayerUnit:getDesc().category] end @@ -51028,7 +59581,7 @@ function SCORING:ScoreCSV( PlayerName, TargetPlayerName, ScoreType, ScoreTimes, TargetUnitType = TargetUnitType or "" TargetUnitName = TargetUnitName or "" - if lfs and io and os then + if lfs and io and os and self.AutoSave then self.CSVFile:write( '"' .. self.GameName .. '"' .. ',' .. '"' .. self.RunTime .. '"' .. ',' .. @@ -51052,14 +59605,24 @@ function SCORING:ScoreCSV( PlayerName, TargetPlayerName, ScoreType, ScoreTimes, end end - +--- Close CSV file +-- @param #SCORING self +-- @return #SCORING self function SCORING:CloseCSV() - if lfs and io and os then + if lfs and io and os and self.AutoSave then self.CSVFile:close() end end ---- **Functional** -- Keep airbases clean of crashing or colliding airplanes, and kill missiles when being fired at airbases. +--- Registers a score for a player. +-- @param #SCORING self +-- @param #boolean OnOff Switch saving to CSV on = true or off = false +-- @return #SCORING self +function SCORING:SwitchAutoSave(OnOff) + self.AutoSave = OnOff + return self +end +--- **Functional** - Keep airbases clean of crashing or colliding airplanes, and kill missiles when being fired at airbases. -- -- === -- @@ -51503,7 +60066,7 @@ function CLEANUP_AIRBASE.__:CleanUpSchedule() return true end ---- **Functional** -- Limit the movement of simulaneous moving ground vehicles. +--- **Functional** - Limit the movement of simulaneous moving ground vehicles. -- -- === -- @@ -51535,23 +60098,23 @@ MOVEMENT = { function MOVEMENT:New( MovePrefixes, MoveMaximum ) local self = BASE:Inherit( self, BASE:New() ) -- #MOVEMENT self:F( { MovePrefixes, MoveMaximum } ) - + if type( MovePrefixes ) == 'table' then self.MovePrefixes = MovePrefixes else self.MovePrefixes = { MovePrefixes } end - self.MoveCount = 0 -- The internal counter of the amount of Moveing the has happened since MoveStart. - self.MoveMaximum = MoveMaximum -- Contains the Maximum amount of units that are allowed to move... - self.AliveUnits = 0 -- Contains the counter how many units are currently alive - self.MoveUnits = {} -- Reflects if the Moving for this MovePrefixes is going to be scheduled or not. - + self.MoveCount = 0 -- The internal counter of the amount of Moving the has happened since MoveStart. + self.MoveMaximum = MoveMaximum -- Contains the Maximum amount of units that are allowed to move. + self.AliveUnits = 0 -- Contains the counter how many units are currently alive. + self.MoveUnits = {} -- Reflects if the Moving for this MovePrefixes is going to be scheduled or not. + self:HandleEvent( EVENTS.Birth ) - + -- self:AddEvent( world.event.S_EVENT_BIRTH, self.OnBirth ) -- -- self:EnableEvents() - + self:ScheduleStart() return self @@ -51572,7 +60135,7 @@ function MOVEMENT:ScheduleStop() end --- Captures the birth events when new Units were spawned. --- @todo This method should become obsolete. The new @{DATABASE} class will handle the collection administration. +-- @todo This method should become obsolete. The global _DATABASE object (an instance of @{Core.Database#DATABASE}) will handle the collection administration. -- @param #MOVEMENT self -- @param Core.Event#EVENTDATA self function MOVEMENT:OnEventBirth( EventData ) @@ -51591,14 +60154,14 @@ function MOVEMENT:OnEventBirth( EventData ) end end end - + EventData.IniUnit:HandleEvent( EVENTS.DEAD, self.OnDeadOrCrash ) end end --- Captures the Dead or Crash events when Units crash or are destroyed. --- @todo This method should become obsolete. The new @{DATABASE} class will handle the collection administration. +-- @todo This method should become obsolete. The global _DATABASE object (an instance of @{Core.Database#DATABASE}) will handle the collection administration. function MOVEMENT:OnDeadOrCrash( Event ) self:F( { Event } ) @@ -51641,57 +60204,68 @@ function MOVEMENT:_Scheduler() end return true end ---- **Functional** -- Make SAM sites execute evasive and defensive behaviour when being fired upon. --- +--- **Functional** - Make SAM sites execute evasive and defensive behaviour when being fired upon. +-- -- === --- +-- -- ## Features: --- +-- -- * 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. +-- -- === --- +-- -- ## Missions: --- +-- -- [SEV - SEAD Evasion](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/SEV%20-%20SEAD%20Evasion) --- +-- -- === --- +-- -- ### Authors: **FlightControl**, **applevangelist** --- --- Last Update: July 2021 --- +-- +-- Last Update: Feb 2022 +-- -- === --- +-- -- @module Functional.Sead -- @image SEAD.JPG ---- @type SEAD +--- +-- @type SEAD -- @extends Core.Base#BASE --- 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. --- +-- -- SEAD_RU_SAM_Defenses = SEAD:New( { 'RU SA-6 Kub', 'RU SA-6 Defenses', 'RU MI-26 Troops', 'RU Attack Gori' } ) --- +-- -- @field #SEAD SEAD = { - ClassName = "SEAD", + ClassName = "SEAD", TargetSkill = { Average = { Evade = 30, DelayOn = { 40, 60 } } , Good = { Evade = 20, DelayOn = { 30, 50 } } , High = { Evade = 15, DelayOn = { 20, 40 } } , - Excellent = { Evade = 10, DelayOn = { 10, 30 } } - }, + Excellent = { Evade = 10, DelayOn = { 10, 30 } } + }, SEADGroupPrefixes = {}, SuppressedGroups = {}, - EngagementRange = 75 -- default 75% engagement range Feature Request #1355 + EngagementRange = 75, -- default 75% engagement range Feature Request #1355 + Padding = 10, + CallBack = nil, + UseCallBack = false, + debug = false, } --- Missile enumerators @@ -51702,30 +60276,54 @@ SEAD = { ["AGM_122"] = "AGM_122", ["AGM_84"] = "AGM_84", ["AGM_45"] = "AGM_45", - ["ALARN"] = "ALARM", + ["ALARM"] = "ALARM", ["LD-10"] = "LD-10", ["X_58"] = "X_58", ["X_28"] = "X_28", ["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 + -- @field HarmData + SEAD.HarmData = { + -- km and mach + ["AGM_88"] = { 150, 3}, + ["AGM_45"] = { 12, 2}, + ["AGM_122"] = { 16.5, 2.3}, + ["AGM_84"] = { 280, 0.8}, + ["ALARM"] = { 45, 2}, + ["LD-10"] = { 60, 4}, + ["X_58"] = { 70, 4}, + ["X_28"] = { 80, 2.5}, + ["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. -- When an anti radiation missile is fired (KH-58, KH-31P, KH-31A, KH-25MPU, HARM missiles), the SA will shut down their radars and will take evasive actions... -- Chances are big that the missile will miss. -- @param #SEAD self --- @param table{string,...}|string SEADGroupPrefixes which is a table of Prefixes of the SA Groups in the DCS mission editor on which evasive actions need to be taken. --- @return SEAD +-- @param #table SEADGroupPrefixes Table of #string entries or single #string, which is a table of Prefixes of the SA Groups in the DCS mission editor on which evasive actions need to be taken. +-- @param #number Padding (Optional) Extra number of seconds to add to radar switch-back-on time +-- @return #SEAD self -- @usage -- -- CCCP SEAD Defenses -- -- Defends the Russian SA installations from SEAD attacks. -- 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 ) +function SEAD:New( SEADGroupPrefixes, Padding ) + + local self = BASE:Inherit( self, FSM:New() ) + self:T( SEADGroupPrefixes ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( SEADGroupPrefixes ) - if type( SEADGroupPrefixes ) == 'table' then for SEADGroupPrefixID, SEADGroupPrefix in pairs( SEADGroupPrefixes ) do self.SEADGroupPrefixes[SEADGroupPrefix] = SEADGroupPrefix @@ -51733,20 +60331,36 @@ function SEAD:New( SEADGroupPrefixes ) else self.SEADGroupPrefixes[SEADGroupPrefixes] = SEADGroupPrefixes end + + 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.2.9") + + -- 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 function SEAD:UpdateSet( SEADGroupPrefixes ) self:T( SEADGroupPrefixes ) - + if type( SEADGroupPrefixes ) == 'table' then for SEADGroupPrefixID, SEADGroupPrefix in pairs( SEADGroupPrefixes ) do self.SEADGroupPrefixes[SEADGroupPrefix] = SEADGroupPrefix @@ -51760,8 +60374,8 @@ 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 --- @return self +-- @param #number range Set the engagement range in percent, e.g. 55 (default 75) +-- @return #SEAD self function SEAD:SetEngagementRange(range) self:T( { range } ) range = range or 75 @@ -51773,105 +60387,344 @@ function SEAD:SetEngagementRange(range) return self end - --- Check if a known HARM was fired - -- @param #SEAD self - -- @param #string WeaponName - -- @return #boolean Returns true for a match - function SEAD:_CheckHarms(WeaponName) - self:T( { WeaponName } ) - local hit = false - for _,_name in pairs (SEAD.Harms) do - if string.find(WeaponName,_name,1) then hit = true 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 (default 10 seconds) +-- @return #SEAD self +function SEAD:SetPadding(Padding) + self:T( { Padding } ) + local padding = Padding or 10 + if padding < 10 then padding = 10 end + self.Padding = padding + return self +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 + +--- 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 - return hit + 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({_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 } ) - + 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 local SEADWeapon = EventData.Weapon -- Identify the weapon fired local SEADWeaponName = EventData.WeaponName -- return weapon type self:T( "*** SEAD - Missile Launched = " .. SEADWeaponName) - self:T({ SEADWeapon }) - + --self:T({ SEADWeapon }) + if self:_CheckHarms(SEADWeaponName) then + self:T( '*** SEAD - Weapon Match' ) local _targetskill = "Random" - local _targetMimgroupName = "none" - local _evade = math.random (1,100) -- random number for chance of evading action - local _targetMim = EventData.Weapon:getTarget() -- Identify target - local _targetUnit = UNIT:Find(_targetMim) -- Unit name by DCS Object - if _targetUnit and _targetUnit:IsAlive() then - local _targetMimgroup = _targetUnit:GetGroup() - _targetMimgroupName = _targetMimgroup:GetName() -- group name - --local _targetskill = _DATABASE.Templates.Units[_targetUnit].Template.skill - self:T( self.SEADGroupPrefixes ) - self:T( _targetMimgroupName ) + local _targetgroupname = "none" + local _target = EventData.Weapon:getTarget() -- Identify target + 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 + 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( SEADGroupPrefix ) - if string.find( _targetMimgroupName, SEADGroupPrefix, 1, true ) then + self:T("Target = ".. _targetgroupname .. " | Prefix = " .. SEADGroupPrefix ) + if string.find( _targetgroupname, SEADGroupPrefix,1,true ) then SEADGroupFound = true - self:T( '*** SEAD - Group Found' ) + self:T( '*** SEAD - Group Match Found' ) break end - 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 - if (_evade > self.TargetSkill[_targetskill].Evade) then - - self:T( string.format("*** SEAD - Evading, target skill " ..string.format(_targetskill)) ) - - local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon)) - local _targetMimcont= _targetMimgroup:getController() - - routines.groupRandomDistSelf(_targetMimgroup,300,'Diamond',250,20) -- move randomly - - --tracker ID table to switch groups off and on again - local id = { - groupName = _targetMimgroup, - ctrl = _targetMimcont - } - - local function SuppressionEnd(id) --switch group back on - local range = self.EngagementRange -- Feature Request #1355 - self:T(string.format("*** SEAD - Engagement Range is %d", range)) - id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.RED) - --id.groupName:enableEmission(true) - id.ctrl:setOption(AI.Option.Ground.id.AC_ENGAGEMENT_RANGE_RESTRICTION,range) --Feature Request #1355 - self.SuppressedGroups[id.groupName] = nil --delete group id from table when done - end - -- randomize switch-on time - local delay = math.random(self.TargetSkill[_targetskill].DelayOn[1], self.TargetSkill[_targetskill].DelayOn[2]) - local SuppressionEndTime = timer.getTime() + delay - --create entry - if self.SuppressedGroups[id.groupName] == nil then --no timer entry for this group yet - self.SuppressedGroups[id.groupName] = { - SuppressionEndTime = delay - } - Controller.setOption(_targetMimcont, AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) - --_targetMimgroup:enableEmission(false) - timer.scheduleFunction(SuppressionEnd, id, SuppressionEndTime) --Schedule the SuppressionEnd() function - end - end - end + self:ManageEvasion(_targetskill,_targetgroup,SEADPlanePos,SEADWeaponName,SEADGroup) end end + return self end ---- **Functional** -- Taking the lead of AI escorting your flight. +--- **Functional** - Taking the lead of AI escorting your flight. -- -- === -- @@ -53272,44 +62125,44 @@ function ESCORT:_ReportTargetsScheduler() return false end ---- **Functional** -- Train missile defence and deflection. --- +--- **Functional** - Train missile defence and deflection. +-- -- === -- -- ## Features: --- +-- -- * Track the missiles fired at you and other players, providing bearing and range information of the missiles towards the airplanes. --- * Provide alerts of missile launches, including detailed information of the units launching, including bearing, range ° +-- * Provide alerts of missile launches, including detailed information of the units launching, including bearing, range -- * Provide alerts when a missile would have killed your aircraft. -- * Provide alerts when the missile self destructs. -- * Enable / Disable and Configure the Missile Trainer using the various menu options. --- +-- -- === --- +-- -- ## Missions: --- +-- -- [MIT - Missile Trainer](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/MIT%20-%20Missile%20Trainer) --- +-- -- === --- +-- -- Uses the MOOSE messaging system to be alerted of any missiles fired, and when a missile would hit your aircraft, -- the class will destroy the missile within a certain range, to avoid damage to your aircraft. --- +-- -- When running a mission where the missile trainer is used, the following radio menu structure ( 'Radio Menu' -> 'Other (F10)' -> 'MissileTrainer' ) options are available for the players: --- +-- -- * **Messages**: Menu to configure all messages. -- * **Messages On**: Show all messages. -- * **Messages Off**: Disable all messages. -- * **Tracking**: Menu to configure missile tracking messages. -- * **To All**: Shows missile tracking messages to all players. --- * **To Target**: Shows missile tracking messages only to the player where the missile is targetted at. +-- * **To Target**: Shows missile tracking messages only to the player where the missile is targeted at. -- * **Tracking On**: Show missile tracking messages. -- * **Tracking Off**: Disable missile tracking messages. -- * **Frequency Increase**: Increases the missile tracking message frequency with one second. -- * **Frequency Decrease**: Decreases the missile tracking message frequency with one second. -- * **Alerts**: Menu to configure alert messages. -- * **To All**: Shows alert messages to all players. --- * **To Target**: Shows alert messages only to the player where the missile is (was) targetted at. +-- * **To Target**: Shows alert messages only to the player where the missile is (was) targeted at. -- * **Hits On**: Show missile hit alert messages. -- * **Hits Off**: Disable missile hit alert messages. -- * **Launches On**: Show missile launch messages. @@ -53319,23 +62172,23 @@ end -- * **Range Off**: Disable range information when a missile is fired to a target. -- * **Bearing On**: Shows bearing information when a missile is fired to a target. -- * **Bearing Off**: Disable bearing information when a missile is fired to a target. --- * **Distance**: Menu to configure the distance when a missile needs to be destroyed when near to a player, during tracking. This will improve/influence hit calculation accuracy, but has the risk of damaging the aircraft when the missile reaches the aircraft before the distance is measured. +-- * **Distance**: Menu to configure the distance when a missile needs to be destroyed when near to a player, during tracking. This will improve/influence hit calculation accuracy, but has the risk of damaging the aircraft when the missile reaches the aircraft before the distance is measured. -- * **50 meter**: Destroys the missile when the distance to the aircraft is below or equal to 50 meter. -- * **100 meter**: Destroys the missile when the distance to the aircraft is below or equal to 100 meter. -- * **150 meter**: Destroys the missile when the distance to the aircraft is below or equal to 150 meter. -- * **200 meter**: Destroys the missile when the distance to the aircraft is below or equal to 200 meter. --- +-- -- === --- +-- -- ### Authors: **FlightControl** --- +-- -- ### Contributions: --- --- * **Stuka (Danny)**: Who you can search on the Eagle Dynamics Forums. Working together with Danny has resulted in the MISSILETRAINER class. --- Danny has shared his ideas and together we made a design. +-- +-- * **Stuka (Danny)**: Who you can search on the Eagle Dynamics Forums. Working together with Danny has resulted in the MISSILETRAINER class. +-- Danny has shared his ideas and together we made a design. -- Together with the **476 virtual team**, we tested the MISSILETRAINER class, and got much positive feedback! -- * **132nd Squadron**: Testing and optimizing the logic. --- +-- -- === -- -- @module Functional.MissileTrainer @@ -53350,7 +62203,7 @@ end --- -- -- # Constructor: --- +-- -- Create a new MISSILETRAINER object with the @{#MISSILETRAINER.New} method: -- -- * @{#MISSILETRAINER.New}: Creates a new MISSILETRAINER object taking the maximum distance to your aircraft to evaluate when a missile needs to be destroyed. @@ -53358,11 +62211,11 @@ end -- MISSILETRAINER will collect each unit declared in the mission with a skill level "Client" and "Player", and will monitor the missiles shot at those. -- -- # Initialization: --- +-- -- A MISSILETRAINER object will behave differently based on the usage of initialization methods: -- -- * @{#MISSILETRAINER.InitMessagesOnOff}: Sets by default the display of any message to be ON or OFF. --- * @{#MISSILETRAINER.InitTrackingToAll}: Sets by default the missile tracking report for all players or only for those missiles targetted to you. +-- * @{#MISSILETRAINER.InitTrackingToAll}: Sets by default the missile tracking report for all players or only for those missiles targeted to you. -- * @{#MISSILETRAINER.InitTrackingOnOff}: Sets by default the display of missile tracking report to be ON or OFF. -- * @{#MISSILETRAINER.InitTrackingFrequency}: Increases, decreases the missile tracking message display frequency with the provided time interval in seconds. -- * @{#MISSILETRAINER.InitAlertsToAll}: Sets by default the display of alerts to be shown to all players or only to you. @@ -53371,8 +62224,8 @@ end -- * @{#MISSILETRAINER.InitRangeOnOff}: Sets by default the display of range information of missiles ON of OFF. -- * @{#MISSILETRAINER.InitBearingOnOff}: Sets by default the display of bearing information of missiles ON of OFF. -- * @{#MISSILETRAINER.InitMenusOnOff}: Allows to configure the options through the radio menu. --- --- @field #MISSILETRAINER +-- +-- @field #MISSILETRAINER MISSILETRAINER = { ClassName = "MISSILETRAINER", TrackingMissiles = {}, @@ -53441,7 +62294,7 @@ end -- When a missile is fired a SCHEDULER is set off that follows the missile. When near a certain a client player, the missile will be destroyed. -- @param #MISSILETRAINER self -- @param #number Distance The distance in meters when a tracked missile needs to be destroyed when close to a player. --- @param #string Briefing (Optional) Will show a text to the players when starting their mission. Can be used for briefing purposes. +-- @param #string Briefing (Optional) Will show a text to the players when starting their mission. Can be used for briefing purposes. -- @return #MISSILETRAINER function MISSILETRAINER:New( Distance, Briefing ) local self = BASE:Inherit( self, BASE:New() ) @@ -53468,8 +62321,8 @@ function MISSILETRAINER:New( Distance, Briefing ) -- self:F( "ForEach:" .. Client.UnitName ) -- Client:Alive( self._Alive, self ) -- end --- - self.DBClients:ForEachClient( +-- + self.DBClients:ForEachClient( function( Client ) self:F( "ForEach:" .. Client.UnitName ) Client:Alive( self._Alive, self ) @@ -53481,9 +62334,9 @@ function MISSILETRAINER:New( Distance, Briefing ) -- self.DB:ForEachClient( -- --- @param Wrapper.Client#CLIENT Client -- function( Client ) --- +-- -- ... actions ... --- +-- -- end -- ) @@ -53499,7 +62352,7 @@ function MISSILETRAINER:New( Distance, Briefing ) self.DetailsRangeOnOff = true self.DetailsBearingOnOff = true - + self.MenusOnOff = true self.TrackingMissiles = {} @@ -53530,7 +62383,7 @@ function MISSILETRAINER:InitMessagesOnOff( MessagesOnOff ) return self end ---- Sets by default the missile tracking report for all players or only for those missiles targetted to you. +--- Sets by default the missile tracking report for all players or only for those missiles targeted to you. -- @param #MISSILETRAINER self -- @param #boolean TrackingToAll true or false -- @return #MISSILETRAINER self @@ -53567,7 +62420,7 @@ end --- Increases, decreases the missile tracking message display frequency with the provided time interval in seconds. -- The default frequency is a 3 second interval, so the Tracking Frequency parameter specifies the increase or decrease from the default 3 seconds or the last frequency update. -- @param #MISSILETRAINER self --- @param #number TrackingFrequency Provide a negative or positive value in seconds to incraese or decrease the display frequency. +-- @param #number TrackingFrequency Provide a negative or positive value in seconds to incraese or decrease the display frequency. -- @return #MISSILETRAINER self function MISSILETRAINER:InitTrackingFrequency( TrackingFrequency ) self:F( TrackingFrequency ) @@ -53752,30 +62605,30 @@ function MISSILETRAINER:OnEventShot( EVentData ) if TrainerTargetDCSUnit then local TrainerTargetDCSUnitName = Unit.getName( TrainerTargetDCSUnit ) local TrainerTargetSkill = _DATABASE.Templates.Units[TrainerTargetDCSUnitName].Template.skill - + self:T(TrainerTargetDCSUnitName ) - + local Client = self.DBClients:FindClient( TrainerTargetDCSUnitName ) if Client then - + local TrainerSourceUnit = UNIT:Find( TrainerSourceDCSUnit ) local TrainerTargetUnit = UNIT:Find( TrainerTargetDCSUnit ) - + if self.MessagesOnOff == true and self.AlertsLaunchesOnOff == true then - + local Message = MESSAGE:New( string.format( "%s launched a %s", TrainerSourceUnit:GetTypeName(), TrainerWeaponName ) .. self:_AddRange( Client, TrainerWeapon ) .. self:_AddBearing( Client, TrainerWeapon ), 5, "Launch Alert" ) - + if self.AlertsToAll then Message:ToAll() else Message:ToClient( Client ) end end - + local ClientID = Client:GetID() self:T( ClientID ) local MissileData = {} @@ -53853,52 +62706,52 @@ function MISSILETRAINER:_TrackMissiles() end -- ALERTS PART - + -- Loop for all Player Clients to check the alerts and deletion of missiles. for ClientDataID, ClientData in pairs( self.TrackingMissiles ) do local Client = ClientData.Client - + if Client and Client:IsAlive() then for MissileDataID, MissileData in pairs( ClientData.MissileData ) do self:T3( MissileDataID ) - + local TrainerSourceUnit = MissileData.TrainerSourceUnit local TrainerWeapon = MissileData.TrainerWeapon local TrainerTargetUnit = MissileData.TrainerTargetUnit local TrainerWeaponTypeName = MissileData.TrainerWeaponTypeName local TrainerWeaponLaunched = MissileData.TrainerWeaponLaunched - + if Client and Client:IsAlive() and TrainerSourceUnit and TrainerSourceUnit:IsAlive() and TrainerWeapon and TrainerWeapon:isExist() and TrainerTargetUnit and TrainerTargetUnit:IsAlive() then local PositionMissile = TrainerWeapon:getPosition().p local TargetVec3 = Client:GetVec3() - + local Distance = ( ( PositionMissile.x - TargetVec3.x )^2 + ( PositionMissile.y - TargetVec3.y )^2 + ( PositionMissile.z - TargetVec3.z )^2 ) ^ 0.5 / 1000 - + if Distance <= self.Distance then -- Hit alert TrainerWeapon:destroy() if self.MessagesOnOff == true and self.AlertsHitsOnOff == true then - + self:T( "killed" ) - + local Message = MESSAGE:New( string.format( "%s launched by %s killed %s", TrainerWeapon:getTypeName(), TrainerSourceUnit:GetTypeName(), TrainerTargetUnit:GetPlayerName() ), 15, "Hit Alert" ) - + if self.AlertsToAll == true then Message:ToAll() else Message:ToClient( Client ) end - + MissileData = nil table.remove( ClientData.MissileData, MissileDataID ) self:T(ClientData.MissileData) @@ -53913,7 +62766,7 @@ function MISSILETRAINER:_TrackMissiles() TrainerWeaponTypeName, TrainerSourceUnit:GetTypeName() ), 5, "Tracking" ) - + if self.AlertsToAll == true then Message:ToAll() else @@ -53934,41 +62787,41 @@ function MISSILETRAINER:_TrackMissiles() if ShowMessages == true and self.MessagesOnOff == true and self.TrackingOnOff == true then -- Only do this when tracking information needs to be displayed. -- TRACKING PART - + -- For the current client, the missile range and bearing details are displayed To the Player Client. -- For the other clients, the missile range and bearing details are displayed To the other Player Clients. - -- To achieve this, a cross loop is done for each Player Client <-> Other Player Client missile information. - + -- To achieve this, a cross loop is done for each Player Client <-> Other Player Client missile information. + -- Main Player Client loop for ClientDataID, ClientData in pairs( self.TrackingMissiles ) do - + local Client = ClientData.Client --self:T2( { Client:GetName() } ) - - + + ClientData.MessageToClient = "" ClientData.MessageToAll = "" - + -- Other Players Client loop for TrackingDataID, TrackingData in pairs( self.TrackingMissiles ) do - + for MissileDataID, MissileData in pairs( TrackingData.MissileData ) do --self:T3( MissileDataID ) - + local TrainerSourceUnit = MissileData.TrainerSourceUnit local TrainerWeapon = MissileData.TrainerWeapon local TrainerTargetUnit = MissileData.TrainerTargetUnit local TrainerWeaponTypeName = MissileData.TrainerWeaponTypeName local TrainerWeaponLaunched = MissileData.TrainerWeaponLaunched - + if Client and Client:IsAlive() and TrainerSourceUnit and TrainerSourceUnit:IsAlive() and TrainerWeapon and TrainerWeapon:isExist() and TrainerTargetUnit and TrainerTargetUnit:IsAlive() then - + if ShowMessages == true then local TrackingTo TrackingTo = string.format( " -> %s", TrainerWeaponTypeName ) - + if ClientDataID == TrackingDataID then if ClientData.MessageToClient == "" then ClientData.MessageToClient = "Missiles to You:\n" @@ -53986,7 +62839,7 @@ function MISSILETRAINER:_TrackMissiles() end end end - + -- Once the Player Client and the Other Player Client tracking messages are prepared, show them. if ClientData.MessageToClient ~= "" or ClientData.MessageToAll ~= "" then local Message = MESSAGE:New( ClientData.MessageToClient .. ClientData.MessageToAll, 1, "Tracking" ):ToClient( Client ) @@ -53996,7 +62849,7 @@ function MISSILETRAINER:_TrackMissiles() return true end ---- **Functional** -- Monitor airbase traffic and regulate speed while taxiing. +--- **Functional** - Monitor airbase traffic and regulate speed while taxiing. -- -- === -- @@ -54016,6 +62869,8 @@ end -- -- ### Contributions: Dutch Baron - Concept & Testing -- ### Author: FlightControl - Framework Design & Programming +-- ### Refactoring to use the Runway auto-detection: Applevangelist +-- @date August 2022 -- -- === -- @@ -54026,21 +62881,20 @@ end -- @field Core.Set#SET_CLIENT SetClient -- @extends Core.Base#BASE ---- Base class for ATC\_GROUND implementations. +--- [DEPRECATED, use ATC_GROUND_UNIVERSAL] Base class for ATC\_GROUND implementations. -- @field #ATC_GROUND ATC_GROUND = { ClassName = "ATC_GROUND", SetClient = nil, Airbases = nil, AirbaseNames = nil, - --KickSpeed = nil, -- The maximum speed in meters per second for all airbases until a player gets kicked. This is overridden at each derived class. } --- @type ATC_GROUND.AirbaseNames -- @list <#string> ---- Creates a new ATC\_GROUND object. +--- [DEPRECATED, use ATC_GROUND_UNIVERSAL] Creates a new ATC\_GROUND object. -- @param #ATC_GROUND self -- @param Airbases A table of Airbase Names. -- @return #ATC_GROUND self @@ -54057,10 +62911,18 @@ function ATC_GROUND:New( Airbases, AirbaseList ) for AirbaseID, Airbase in pairs( self.Airbases ) do - Airbase.ZoneBoundary = _DATABASE:FindAirbase( AirbaseID ):GetZone() + -- Specified ZoneBoundary is used if set or Airbase radius by default + if Airbase.ZoneBoundary then + Airbase.ZoneBoundary = ZONE_POLYGON_BASE:New( "Boundary " .. AirbaseID, Airbase.ZoneBoundary ) + else + Airbase.ZoneBoundary = _DATABASE:FindAirbase( AirbaseID ):GetZone() + end + Airbase.ZoneRunways = {} - for PointsRunwayID, PointsRunway in pairs( Airbase.PointsRunways ) do - Airbase.ZoneRunways[PointsRunwayID] = ZONE_POLYGON_BASE:New( "Runway " .. PointsRunwayID, PointsRunway ) + if Airbase.PointsRunways then + for PointsRunwayID, PointsRunway in pairs( Airbase.PointsRunways ) do + Airbase.ZoneRunways[PointsRunwayID] = ZONE_POLYGON_BASE:New( "Runway " .. PointsRunwayID, PointsRunway ) + end end Airbase.Monitor = self.AirbaseList and false or true -- When AirbaseList is not given, monitor every Airbase, otherwise don't monitor any (yet). end @@ -54070,13 +62932,6 @@ function ATC_GROUND:New( Airbases, AirbaseList ) self.Airbases[AirbaseName].Monitor = true end --- -- Template --- local TemplateBoundary = GROUP:FindByName( "Template Boundary" ) --- self.Airbases.Template.ZoneBoundary = ZONE_POLYGON:New( "Template Boundary", TemplateBoundary ):SmokeZone(SMOKECOLOR.White):Flush() --- --- local TemplateRunway1 = GROUP:FindByName( "Template Runway 1" ) --- self.Airbases.Template.ZoneRunways[1] = ZONE_POLYGON:New( "Template Runway 1", TemplateRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - self.SetClient:ForEachClient( --- @param Wrapper.Client#CLIENT Client function( Client ) @@ -54095,7 +62950,6 @@ function ATC_GROUND:New( Airbases, AirbaseList ) return self end - --- Smoke the airbases runways. -- @param #ATC_GROUND self -- @param Utilities.Utils#SMOKECOLOR SmokeColor The color of the smoke around the runways. @@ -54109,7 +62963,6 @@ function ATC_GROUND:SmokeRunways( SmokeColor ) end end - --- Set the maximum speed in meters per second (Mps) until the player gets kicked. -- An airbase can be specified to set the kick speed for. -- @param #ATC_GROUND self @@ -54244,7 +63097,6 @@ function ATC_GROUND:SetMaximumKickSpeedMiph( MaximumKickSpeedMiph, Airbase ) return self end - --- @param #ATC_GROUND self function ATC_GROUND:_AirbaseMonitor() @@ -54400,11 +63252,455 @@ function ATC_GROUND:_AirbaseMonitor() return true end +--- +-- @type ATC_GROUND_UNIVERSAL +-- @field Core.Set#SET_CLIENT SetClient +-- @field #string Version +-- @field #string ClassName +-- @field #table Airbases +-- @field #table AirbaseList +-- @field #number KickSpeed +-- @extends Core.Base#BASE + +--- Base class for ATC\_GROUND\_UNIVERSAL implementations. +-- @field #ATC_GROUND_UNIVERSAL +ATC_GROUND_UNIVERSAL = { + ClassName = "ATC_GROUND_UNIVERSAL", + Version = "0.0.1", + SetClient = nil, + Airbases = nil, + AirbaseList = nil, + KickSpeed = nil, -- The maximum speed in meters per second for all airbases until a player gets kicked. This is overridden at each derived class. +} + +--- Creates a new ATC\_GROUND\_UNIVERSAL object. This works on any map. +-- @param #ATC_GROUND_UNIVERSAL self +-- @param AirbaseList (Optional) A table of Airbase Names. +-- @return #ATC_GROUND_UNIVERSAL self +function ATC_GROUND_UNIVERSAL:New(AirbaseList) + + -- Inherits from BASE + local self = BASE:Inherit( self, BASE:New() ) -- #ATC_GROUND + self:E( { self.ClassName } ) + + self.Airbases = {} + + for _name,_ in pairs(_DATABASE.AIRBASES) do + self.Airbases[_name]={} + end + + self.AirbaseList = AirbaseList + + self.SetClient = SET_CLIENT:New():FilterCategories( "plane" ):FilterStart() + + + for AirbaseID, Airbase in pairs( self.Airbases ) do + -- Specified ZoneBoundary is used if set or Airbase radius by default + if Airbase.ZoneBoundary then + Airbase.ZoneBoundary = ZONE_POLYGON_BASE:New( "Boundary " .. AirbaseID, Airbase.ZoneBoundary ) + else + Airbase.ZoneBoundary = _DATABASE:FindAirbase( AirbaseID ):GetZone() + end + + Airbase.ZoneRunways = AIRBASE:FindByName(AirbaseID):GetRunways() + Airbase.Monitor = self.AirbaseList and false or true -- When AirbaseList is not given, monitor every Airbase, otherwise don't monitor any (yet). + end + + -- Now activate the monitoring for the airbases that need to be monitored. + for AirbaseID, AirbaseName in pairs( self.AirbaseList or {} ) do + self.Airbases[AirbaseName].Monitor = true + end + + self.SetClient:ForEachClient( + --- @param Wrapper.Client#CLIENT Client + function( Client ) + Client:SetState( self, "Speeding", false ) + Client:SetState( self, "Warnings", 0) + Client:SetState( self, "IsOffRunway", false ) + Client:SetState( self, "OffRunwayWarnings", 0 ) + Client:SetState( self, "Taxi", false ) + end + ) + + -- This is simple slot blocker is used on the server. + SSB = USERFLAG:New( "SSB" ) + SSB:Set( 100 ) + + -- Kickspeed + self.KickSpeed = UTILS.KnotsToMps(10) + self:SetMaximumKickSpeedMiph(30) + + return self +end + + +--- Add a specific Airbase Boundary if you don't want to use the round zone that is auto-created. +-- @param #ATC_GROUND_UNIVERSAL self +-- @param #string Airbase The name of the Airbase +-- @param Core.Zone#ZONE Zone The ZONE object to be used, e.g. a ZONE_POLYGON +-- @return #ATC_GROUND_UNIVERSAL self +function ATC_GROUND_UNIVERSAL:SetAirbaseBoundaries(Airbase, Zone) + self.Airbases[Airbase].ZoneBoundary = Zone + return self +end + +--- Smoke the airbases runways. +-- @param #ATC_GROUND_UNIVERSAL self +-- @param Utilities.Utils#SMOKECOLOR SmokeColor The color of the smoke around the runways. +-- @return #ATC_GROUND_UNIVERSAL self +function ATC_GROUND_UNIVERSAL:SmokeRunways( SmokeColor ) + + local SmokeColor = SmokeColor or SMOKECOLOR.Red + for AirbaseID, Airbase in pairs( self.Airbases ) do + if Airbase.ZoneRunways then + for _,_runwaydata in pairs (Airbase.ZoneRunways) do + local runwaydata = _runwaydata -- Wrapper.Airbase#AIRBASE.Runway + runwaydata.zone:SmokeZone(SmokeColor) + end + end + end + + return self +end + +--- Draw the airbases runways. +-- @param #ATC_GROUND_UNIVERSAL self +-- @param #table Color The color of the line around the runways, in RGB, e.g `{1,0,0}` for red. +-- @return #ATC_GROUND_UNIVERSAL self +function ATC_GROUND_UNIVERSAL:DrawRunways( Color ) + + local Color = Color or {1,0,0} + for AirbaseID, Airbase in pairs( self.Airbases ) do + if Airbase.ZoneRunways then + for _,_runwaydata in pairs (Airbase.ZoneRunways) do + local runwaydata = _runwaydata -- Wrapper.Airbase#AIRBASE.Runway + runwaydata.zone:DrawZone(-1,Color) + end + end + end + + return self +end + +--- Draw the airbases boundaries. +-- @param #ATC_GROUND_UNIVERSAL self +-- @param #table Color The color of the line around the runways, in RGB, e.g `{1,0,0}` for red. +-- @return #ATC_GROUND_UNIVERSAL self +function ATC_GROUND_UNIVERSAL:DrawBoundaries( Color ) + + local Color = Color or {1,0,0} + for AirbaseID, Airbase in pairs( self.Airbases ) do + if Airbase.ZoneBoundary then + Airbase.ZoneBoundary:DrawZone(-1, Color) + end + end + + return self +end + +--- Set the maximum speed in meters per second (Mps) until the player gets kicked. +-- An airbase can be specified to set the kick speed for. +-- @param #ATC_GROUND_UNIVERSAL self +-- @param #number KickSpeed The speed in Mps. +-- @param #string Airbase (optional) The airbase name to set the kick speed for. +-- @return #ATC_GROUND_UNIVERSAL self +-- @usage +-- +-- -- Declare Atc_Ground +-- +-- Atc_Ground = ATC_GROUND_UNIVERSAL:New() +-- +-- -- Then use one of these methods... +-- +-- Atc_Ground:SetKickSpeed( UTILS.KmphToMps( 80 ) ) -- Kick the players at 80 kilometers per hour +-- +-- Atc_Ground:SetKickSpeed( UTILS.MiphToMps( 100 ) ) -- Kick the players at 100 miles per hour +-- +-- Atc_Ground:SetKickSpeed( 24 ) -- Kick the players at 24 meters per second ( 24 * 3.6 = 86.4 kilometers per hour ) +-- +function ATC_GROUND_UNIVERSAL:SetKickSpeed( KickSpeed, Airbase ) + + if not Airbase then + self.KickSpeed = KickSpeed + else + self.Airbases[Airbase].KickSpeed = KickSpeed + end + + return self +end + +--- Set the maximum speed in Kmph until the player gets kicked. +-- @param #ATC_GROUND_UNIVERSAL self +-- @param #number KickSpeed Set the speed in Kmph. +-- @param #string Airbase (optional) The airbase name to set the kick speed for. +-- @return #ATC_GROUND_UNIVERSAL self +-- +-- Atc_Ground:SetKickSpeedKmph( 80 ) -- Kick the players at 80 kilometers per hour +-- +function ATC_GROUND_UNIVERSAL:SetKickSpeedKmph( KickSpeed, Airbase ) + + self:SetKickSpeed( UTILS.KmphToMps( KickSpeed ), Airbase ) + + return self +end + +--- Set the maximum speed in Miph until the player gets kicked. +-- @param #ATC_GROUND_UNIVERSAL self +-- @param #number KickSpeedMiph Set the speed in Mph. +-- @param #string Airbase (optional) The airbase name to set the kick speed for. +-- @return #ATC_GROUND_UNIVERSAL self +-- +-- Atc_Ground:SetKickSpeedMiph( 100 ) -- Kick the players at 100 miles per hour +-- +function ATC_GROUND_UNIVERSAL:SetKickSpeedMiph( KickSpeedMiph, Airbase ) + + self:SetKickSpeed( UTILS.MiphToMps( KickSpeedMiph ), Airbase ) + + return self +end + + +--- Set the maximum kick speed in meters per second (Mps) until the player gets kicked. +-- There are no warnings given if this speed is reached, and is to prevent players to take off from the airbase! +-- An airbase can be specified to set the maximum kick speed for. +-- @param #ATC_GROUND_UNIVERSAL self +-- @param #number MaximumKickSpeed The speed in Mps. +-- @param #string Airbase (optional) The airbase name to set the kick speed for. +-- @return #ATC_GROUND_UNIVERSAL self +-- @usage +-- +-- -- Declare Atc_Ground +-- +-- Atc_Ground = ATC_GROUND_UNIVERSAL:New() +-- +-- -- Then use one of these methods... +-- +-- Atc_Ground:SetMaximumKickSpeed( UTILS.KmphToMps( 80 ) ) -- Kick the players at 80 kilometers per hour +-- +-- Atc_Ground:SetMaximumKickSpeed( UTILS.MiphToMps( 100 ) ) -- Kick the players at 100 miles per hour +-- +-- Atc_Ground:SetMaximumKickSpeed( 24 ) -- Kick the players at 24 meters per second ( 24 * 3.6 = 86.4 kilometers per hour ) +-- +function ATC_GROUND_UNIVERSAL:SetMaximumKickSpeed( MaximumKickSpeed, Airbase ) + + if not Airbase then + self.MaximumKickSpeed = MaximumKickSpeed + else + self.Airbases[Airbase].MaximumKickSpeed = MaximumKickSpeed + end + + return self +end + +--- Set the maximum kick speed in kilometers per hour (Kmph) until the player gets kicked. +-- There are no warnings given if this speed is reached, and is to prevent players to take off from the airbase! +-- An airbase can be specified to set the maximum kick speed for. +-- @param #ATC_GROUND_UNIVERSAL self +-- @param #number MaximumKickSpeed Set the speed in Kmph. +-- @param #string Airbase (optional) The airbase name to set the kick speed for. +-- @return #ATC_GROUND_UNIVERSAL self +-- +-- Atc_Ground:SetMaximumKickSpeedKmph( 150 ) -- Kick the players at 150 kilometers per hour +-- +function ATC_GROUND_UNIVERSAL:SetMaximumKickSpeedKmph( MaximumKickSpeed, Airbase ) + + self:SetMaximumKickSpeed( UTILS.KmphToMps( MaximumKickSpeed ), Airbase ) + + return self +end + +--- Set the maximum kick speed in miles per hour (Miph) until the player gets kicked. +-- There are no warnings given if this speed is reached, and is to prevent players to take off from the airbase! +-- An airbase can be specified to set the maximum kick speed for. +-- @param #ATC_GROUND_UNIVERSAL self +-- @param #number MaximumKickSpeedMiph Set the speed in Mph. +-- @param #string Airbase (optional) The airbase name to set the kick speed for. +-- @return #ATC_GROUND_UNIVERSAL self +-- +-- Atc_Ground:SetMaximumKickSpeedMiph( 100 ) -- Kick the players at 100 miles per hour +-- +function ATC_GROUND_UNIVERSAL:SetMaximumKickSpeedMiph( MaximumKickSpeedMiph, Airbase ) + + self:SetMaximumKickSpeed( UTILS.MiphToMps( MaximumKickSpeedMiph ), Airbase ) + + return self +end + +--- [Internal] Monitoring function +-- @param #ATC_GROUND_UNIVERSAL self +-- @return #ATC_GROUND_UNIVERSAL self +function ATC_GROUND_UNIVERSAL:_AirbaseMonitor() + + self.SetClient:ForEachClient( + --- @param Wrapper.Client#CLIENT Client + function( Client ) + + if Client:IsAlive() then + + local IsOnGround = Client:InAir() == false + + for AirbaseID, AirbaseMeta in pairs( self.Airbases ) do + self:E( AirbaseID, AirbaseMeta.KickSpeed ) + + if AirbaseMeta.Monitor == true and Client:IsInZone( AirbaseMeta.ZoneBoundary ) then + + local NotInRunwayZone = true + + if AirbaseMeta.ZoneRunways then + for _,_runwaydata in pairs (AirbaseMeta.ZoneRunways) do + local runwaydata = _runwaydata -- Wrapper.Airbase#AIRBASE.Runway + NotInRunwayZone = ( Client:IsNotInZone( _runwaydata.zone ) == true ) and NotInRunwayZone or false + end + end + + if NotInRunwayZone then + + if IsOnGround then + local Taxi = Client:GetState( self, "Taxi" ) + self:E( Taxi ) + if Taxi == false then + local Velocity = VELOCITY:New( AirbaseMeta.KickSpeed or self.KickSpeed ) + Client:Message( "Welcome to " .. AirbaseID .. ". The maximum taxiing speed is " .. + Velocity:ToString() , 20, "ATC" ) + Client:SetState( self, "Taxi", true ) + end + + -- TODO: GetVelocityKMH function usage + local Velocity = VELOCITY_POSITIONABLE:New( Client ) + --MESSAGE:New( "Velocity = " .. Velocity:ToString(), 1 ):ToAll() + local IsAboveRunway = Client:IsAboveRunway() + self:T( {IsAboveRunway, IsOnGround, Velocity:Get() }) + + if IsOnGround then + local Speeding = false + if AirbaseMeta.MaximumKickSpeed then + if Velocity:Get() > AirbaseMeta.MaximumKickSpeed then + Speeding = true + end + else + if Velocity:Get() > self.MaximumKickSpeed then + Speeding = true + end + end + if Speeding == true then + MESSAGE:New( "Penalty! Player " .. Client:GetPlayerName() .. + " has been kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll() + Client:Destroy() + Client:SetState( self, "Speeding", false ) + Client:SetState( self, "Warnings", 0 ) + end + end + + + if IsOnGround then + + local Speeding = false + if AirbaseMeta.KickSpeed then -- If there is a speed defined for the airbase, use that only. + if Velocity:Get() > AirbaseMeta.KickSpeed then + Speeding = true + end + else + if Velocity:Get() > self.KickSpeed then + Speeding = true + end + end + if Speeding == true then + local IsSpeeding = Client:GetState( self, "Speeding" ) + + if IsSpeeding == true then + local SpeedingWarnings = Client:GetState( self, "Warnings" ) + self:T( SpeedingWarnings ) + + if SpeedingWarnings <= 3 then + Client:Message( "Warning " .. SpeedingWarnings .. "/3! Airbase traffic rule violation! Slow down now! Your speed is " .. + Velocity:ToString(), 5, "ATC" ) + Client:SetState( self, "Warnings", SpeedingWarnings + 1 ) + else + MESSAGE:New( "Penalty! Player " .. Client:GetPlayerName() .. " has been kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll() + --- @param Wrapper.Client#CLIENT Client + Client:Destroy() + Client:SetState( self, "Speeding", false ) + Client:SetState( self, "Warnings", 0 ) + end + + else + Client:Message( "Attention! You are speeding on the taxiway, slow down! Your speed is " .. + Velocity:ToString(), 5, "ATC" ) + Client:SetState( self, "Speeding", true ) + Client:SetState( self, "Warnings", 1 ) + end + + else + Client:SetState( self, "Speeding", false ) + Client:SetState( self, "Warnings", 0 ) + end + end + + if IsOnGround and not IsAboveRunway then + + local IsOffRunway = Client:GetState( self, "IsOffRunway" ) + + if IsOffRunway == true then + local OffRunwayWarnings = Client:GetState( self, "OffRunwayWarnings" ) + self:T( OffRunwayWarnings ) + + if OffRunwayWarnings <= 3 then + Client:Message( "Warning " .. OffRunwayWarnings .. "/3! Airbase traffic rule violation! Get back on the taxi immediately!", 5, "ATC" ) + Client:SetState( self, "OffRunwayWarnings", OffRunwayWarnings + 1 ) + else + MESSAGE:New( "Penalty! Player " .. Client:GetPlayerName() .. " has been kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll() + --- @param Wrapper.Client#CLIENT Client + Client:Destroy() + Client:SetState( self, "IsOffRunway", false ) + Client:SetState( self, "OffRunwayWarnings", 0 ) + end + else + Client:Message( "Attention! You are off the taxiway. Get back on the taxiway immediately!", 5, "ATC" ) + Client:SetState( self, "IsOffRunway", true ) + Client:SetState( self, "OffRunwayWarnings", 1 ) + end + + else + Client:SetState( self, "IsOffRunway", false ) + Client:SetState( self, "OffRunwayWarnings", 0 ) + end + end + else + Client:SetState( self, "Speeding", false ) + Client:SetState( self, "Warnings", 0 ) + Client:SetState( self, "IsOffRunway", false ) + Client:SetState( self, "OffRunwayWarnings", 0 ) + local Taxi = Client:GetState( self, "Taxi" ) + if Taxi == true then + Client:Message( "You have progressed to the runway ... Await take-off clearance ...", 20, "ATC" ) + Client:SetState( self, "Taxi", false ) + end + end + end + end + else + Client:SetState( self, "Taxi", false ) + end + end + ) + + return true +end + +--- Start SCHEDULER for ATC_GROUND_UNIVERSAL object. +-- @param #ATC_GROUND_UNIVERSAL self +-- @param RepeatScanSeconds Time in second for defining occurency of alerts. +-- @return #ATC_GROUND_UNIVERSAL self +function ATC_GROUND_UNIVERSAL:Start( RepeatScanSeconds ) + RepeatScanSeconds = RepeatScanSeconds or 0.05 + self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, RepeatScanSeconds ) + return self +end --- @type ATC_GROUND_CAUCASUS -- @extends #ATC_GROUND ---- # ATC\_GROUND\_CAUCASUS, extends @{#ATC_GROUND} +--- # ATC\_GROUND\_CAUCASUS, extends @{#ATC_GROUND_UNIVERSAL} -- -- The ATC\_GROUND\_CAUCASUS class monitors the speed of the airplanes at the airbase during taxi. -- The pilots may not drive faster than the maximum speed for the airbase, or they will be despawned. @@ -54512,269 +63808,6 @@ end -- @field #ATC_GROUND_CAUCASUS ATC_GROUND_CAUCASUS = { ClassName = "ATC_GROUND_CAUCASUS", - Airbases = { - [AIRBASE.Caucasus.Anapa_Vityazevo] = { - PointsRunways = { - [1] = { - [1]={["y"]=242140.57142858,["x"]=-6478.8571428583,}, - [2]={["y"]=242188.57142858,["x"]=-6522.0000000011,}, - [3]={["y"]=244124.2857143,["x"]=-4344.0000000011,}, - [4]={["y"]=244068.2857143,["x"]=-4296.5714285726,}, - [5]={["y"]=242140.57142858,["x"]=-6480.0000000011,} - }, - }, - }, - [AIRBASE.Caucasus.Batumi] = { - PointsRunways = { - [1] = { - [1]={["y"]=616442.28571429,["x"]=-355090.28571429,}, - [2]={["y"]=618450.57142857,["x"]=-356522,}, - [3]={["y"]=618407.71428571,["x"]=-356584.85714286,}, - [4]={["y"]=618361.99999999,["x"]=-356554.85714286,}, - [5]={["y"]=618324.85714285,["x"]=-356599.14285715,}, - [6]={["y"]=618250.57142856,["x"]=-356543.42857143,}, - [7]={["y"]=618257.7142857,["x"]=-356496.28571429,}, - [8]={["y"]=618237.7142857,["x"]=-356459.14285715,}, - [9]={["y"]=616555.71428571,["x"]=-355258.85714286,}, - [10]={["y"]=616486.28571428,["x"]=-355280.57142858,}, - [11]={["y"]=616410.57142856,["x"]=-355227.71428572,}, - [12]={["y"]=616441.99999999,["x"]=-355179.14285715,}, - [13]={["y"]=616401.99999999,["x"]=-355147.71428572,}, - [14]={["y"]=616441.42857142,["x"]=-355092.57142858,}, - }, - }, - }, - [AIRBASE.Caucasus.Beslan] = { - PointsRunways = { - [1] = { - [1]={["y"]=842104.57142857,["x"]=-148460.57142857,}, - [2]={["y"]=845225.71428572,["x"]=-148656,}, - [3]={["y"]=845220.57142858,["x"]=-148750,}, - [4]={["y"]=842098.85714286,["x"]=-148556.28571429,}, - [5]={["y"]=842104,["x"]=-148460.28571429,}, - }, - }, - }, - [AIRBASE.Caucasus.Gelendzhik] = { - PointsRunways = { - [1] = { - [1]={["y"]=297834.00000001,["x"]=-51107.428571429,}, - [2]={["y"]=297786.57142858,["x"]=-51068.857142858,}, - [3]={["y"]=298946.57142858,["x"]=-49686.000000001,}, - [4]={["y"]=298993.14285715,["x"]=-49725.714285715,}, - [5]={["y"]=297835.14285715,["x"]=-51107.714285715,}, - }, - }, - }, - [AIRBASE.Caucasus.Gudauta] = { - PointsRunways = { - [1] = { - [1]={["y"]=517096.57142857,["x"]=-197804.57142857,}, - [2]={["y"]=515880.85714285,["x"]=-195590.28571429,}, - [3]={["y"]=515812.28571428,["x"]=-195628.85714286,}, - [4]={["y"]=517036.57142857,["x"]=-197834.57142857,}, - [5]={["y"]=517097.99999999,["x"]=-197807.42857143,}, - }, - }, - }, - [AIRBASE.Caucasus.Kobuleti] = { - PointsRunways = { - [1] = { - [1]={["y"]=634509.71428571,["x"]=-318339.42857144,}, - [2]={["y"]=636767.42857143,["x"]=-317516.57142858,}, - [3]={["y"]=636790,["x"]=-317575.71428572,}, - [4]={["y"]=634531.42857143,["x"]=-318398.00000001,}, - [5]={["y"]=634510.28571429,["x"]=-318339.71428572,}, - }, - }, - }, - [AIRBASE.Caucasus.Krasnodar_Center] = { - PointsRunways = { - [1] = { - [1]={["y"]=369205.42857144,["x"]=11789.142857142,}, - [2]={["y"]=369209.71428572,["x"]=11714.857142856,}, - [3]={["y"]=366699.71428572,["x"]=11581.714285713,}, - [4]={["y"]=366698.28571429,["x"]=11659.142857142,}, - [5]={["y"]=369208.85714286,["x"]=11788.57142857,}, - }, - }, - }, - [AIRBASE.Caucasus.Krasnodar_Pashkovsky] = { - PointsRunways = { - [1] = { - [1]={["y"]=385891.14285715,["x"]=8416.5714285703,}, - [2]={["y"]=385842.28571429,["x"]=8467.9999999989,}, - [3]={["y"]=384180.85714286,["x"]=6917.1428571417,}, - [4]={["y"]=384228.57142858,["x"]=6867.7142857132,}, - [5]={["y"]=385891.14285715,["x"]=8416.5714285703,}, - }, - [2] = { - [1]={["y"]=386714.85714286,["x"]=6674.857142856,}, - [2]={["y"]=386757.71428572,["x"]=6627.7142857132,}, - [3]={["y"]=389028.57142858,["x"]=8741.4285714275,}, - [4]={["y"]=388981.71428572,["x"]=8790.5714285703,}, - [5]={["y"]=386714.57142858,["x"]=6674.5714285703,}, - }, - }, - }, - [AIRBASE.Caucasus.Krymsk] = { - PointsRunways = { - [1] = { - [1]={["y"]=293522.00000001,["x"]=-7567.4285714297,}, - [2]={["y"]=293578.57142858,["x"]=-7616.0000000011,}, - [3]={["y"]=295246.00000001,["x"]=-5591.142857144,}, - [4]={["y"]=295187.71428573,["x"]=-5546.0000000011,}, - [5]={["y"]=293523.14285715,["x"]=-7568.2857142868,}, - }, - }, - }, - [AIRBASE.Caucasus.Kutaisi] = { - PointsRunways = { - [1] = { - [1]={["y"]=682638,["x"]=-285202.28571429,}, - [2]={["y"]=685050.28571429,["x"]=-284507.42857144,}, - [3]={["y"]=685068.85714286,["x"]=-284578.85714286,}, - [4]={["y"]=682657.42857143,["x"]=-285264.28571429,}, - [5]={["y"]=682638.28571429,["x"]=-285202.85714286,}, - }, - }, - }, - [AIRBASE.Caucasus.Maykop_Khanskaya] = { - PointsRunways = { - [1] = { - [1]={["y"]=457005.42857143,["x"]=-27668.000000001,}, - [2]={["y"]=459028.85714286,["x"]=-25168.857142858,}, - [3]={["y"]=459082.57142857,["x"]=-25216.857142858,}, - [4]={["y"]=457060,["x"]=-27714.285714287,}, - [5]={["y"]=457004.57142857,["x"]=-27669.714285715,}, - }, - }, - }, - [AIRBASE.Caucasus.Mineralnye_Vody] = { - PointsRunways = { - [1] = { - [1]={["y"]=703904,["x"]=-50352.571428573,}, - [2]={["y"]=707596.28571429,["x"]=-52094.571428573,}, - [3]={["y"]=707560.57142858,["x"]=-52161.714285716,}, - [4]={["y"]=703871.71428572,["x"]=-50420.571428573,}, - [5]={["y"]=703902,["x"]=-50352.000000002,}, - }, - }, - }, - [AIRBASE.Caucasus.Mozdok] = { - PointsRunways = { - [1] = { - [1]={["y"]=832201.14285715,["x"]=-83699.428571431,}, - [2]={["y"]=832212.57142857,["x"]=-83780.571428574,}, - [3]={["y"]=835730.28571429,["x"]=-83335.714285717,}, - [4]={["y"]=835718.85714286,["x"]=-83246.571428574,}, - [5]={["y"]=832200.57142857,["x"]=-83700.000000002,}, - }, - }, - }, - [AIRBASE.Caucasus.Nalchik] = { - PointsRunways = { - [1] = { - [1]={["y"]=759454.28571429,["x"]=-125551.42857143,}, - [2]={["y"]=759492.85714286,["x"]=-125610.85714286,}, - [3]={["y"]=761406.28571429,["x"]=-124304.28571429,}, - [4]={["y"]=761361.14285714,["x"]=-124239.71428572,}, - [5]={["y"]=759456,["x"]=-125552.57142857,}, - }, - }, - }, - [AIRBASE.Caucasus.Novorossiysk] = { - PointsRunways = { - [1] = { - [1]={["y"]=278673.14285716,["x"]=-41615.142857144,}, - [2]={["y"]=278625.42857144,["x"]=-41570.571428572,}, - [3]={["y"]=279835.42857144,["x"]=-40226.000000001,}, - [4]={["y"]=279882.2857143,["x"]=-40270.000000001,}, - [5]={["y"]=278672.00000001,["x"]=-41614.857142858,}, - }, - }, - }, - [AIRBASE.Caucasus.Senaki_Kolkhi] = { - PointsRunways = { - [1] = { - [1]={["y"]=646060.85714285,["x"]=-281736,}, - [2]={["y"]=646056.57142857,["x"]=-281631.71428571,}, - [3]={["y"]=648442.28571428,["x"]=-281840.28571428,}, - [4]={["y"]=648432.28571428,["x"]=-281918.85714286,}, - [5]={["y"]=646063.71428571,["x"]=-281738.85714286,}, - }, - }, - }, - [AIRBASE.Caucasus.Sochi_Adler] = { - PointsRunways = { - [1] = { - [1]={["y"]=460831.42857143,["x"]=-165180,}, - [2]={["y"]=460878.57142857,["x"]=-165257.14285714,}, - [3]={["y"]=463663.71428571,["x"]=-163793.14285714,}, - [4]={["y"]=463612.28571428,["x"]=-163697.42857143,}, - [5]={["y"]=460831.42857143,["x"]=-165177.14285714,}, - }, - [2] = { - [1]={["y"]=460831.42857143,["x"]=-165180,}, - [2]={["y"]=460878.57142857,["x"]=-165257.14285714,}, - [3]={["y"]=463663.71428571,["x"]=-163793.14285714,}, - [4]={["y"]=463612.28571428,["x"]=-163697.42857143,}, - [5]={["y"]=460831.42857143,["x"]=-165177.14285714,}, - }, - }, - }, - [AIRBASE.Caucasus.Soganlug] = { - PointsRunways = { - [1] = { - [1]={["y"]=894525.71428571,["x"]=-316964,}, - [2]={["y"]=896363.14285714,["x"]=-318634.28571428,}, - [3]={["y"]=896299.14285714,["x"]=-318702.85714286,}, - [4]={["y"]=894464,["x"]=-317031.71428571,}, - [5]={["y"]=894524.57142857,["x"]=-316963.71428571,}, - }, - }, - }, - [AIRBASE.Caucasus.Sukhumi_Babushara] = { - PointsRunways = { - [1] = { - [1]={["y"]=562684,["x"]=-219779.71428571,}, - [2]={["y"]=562717.71428571,["x"]=-219718,}, - [3]={["y"]=566046.85714286,["x"]=-221376.57142857,}, - [4]={["y"]=566012.28571428,["x"]=-221446.57142857,}, - [5]={["y"]=562684.57142857,["x"]=-219782.57142857,}, - }, - }, - }, - [AIRBASE.Caucasus.Tbilisi_Lochini] = { - PointsRunways = { - [1] = { - [1]={["y"]=895261.14285715,["x"]=-314652.28571428,}, - [2]={["y"]=897654.57142857,["x"]=-316523.14285714,}, - [3]={["y"]=897711.71428571,["x"]=-316450.28571429,}, - [4]={["y"]=895327.42857143,["x"]=-314568.85714286,}, - [5]={["y"]=895261.71428572,["x"]=-314656,}, - }, - [2] = { - [1]={["y"]=895605.71428572,["x"]=-314724.57142857,}, - [2]={["y"]=897639.71428572,["x"]=-316148,}, - [3]={["y"]=897683.42857143,["x"]=-316087.14285714,}, - [4]={["y"]=895650,["x"]=-314660,}, - [5]={["y"]=895606,["x"]=-314724.85714286,} - }, - }, - }, - [AIRBASE.Caucasus.Vaziani] = { - PointsRunways = { - [1] = { - [1]={["y"]=902239.14285714,["x"]=-318190.85714286,}, - [2]={["y"]=904014.28571428,["x"]=-319994.57142857,}, - [3]={["y"]=904064.85714285,["x"]=-319945.14285715,}, - [4]={["y"]=902294.57142857,["x"]=-318146,}, - [5]={["y"]=902247.71428571,["x"]=-318190.85714286,}, - }, - }, - }, - }, } --- Creates a new ATC_GROUND_CAUCASUS object. @@ -54784,216 +63817,11 @@ ATC_GROUND_CAUCASUS = { function ATC_GROUND_CAUCASUS:New( AirbaseNames ) -- Inherits from BASE - local self = BASE:Inherit( self, ATC_GROUND:New( self.Airbases, AirbaseNames ) ) + local self = BASE:Inherit( self, ATC_GROUND_UNIVERSAL:New(AirbaseNames) ) self:SetKickSpeedKmph( 50 ) self:SetMaximumKickSpeedKmph( 150 ) - -- -- AnapaVityazevo - -- local AnapaVityazevoBoundary = GROUP:FindByName( "AnapaVityazevo Boundary" ) - -- self.Airbases.AnapaVityazevo.ZoneBoundary = ZONE_POLYGON:New( "AnapaVityazevo Boundary", AnapaVityazevoBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local AnapaVityazevoRunway1 = GROUP:FindByName( "AnapaVityazevo Runway 1" ) - -- self.Airbases.AnapaVityazevo.ZoneRunways[1] = ZONE_POLYGON:New( "AnapaVityazevo Runway 1", AnapaVityazevoRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Batumi - -- local BatumiBoundary = GROUP:FindByName( "Batumi Boundary" ) - -- self.Airbases.Batumi.ZoneBoundary = ZONE_POLYGON:New( "Batumi Boundary", BatumiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local BatumiRunway1 = GROUP:FindByName( "Batumi Runway 1" ) - -- self.Airbases.Batumi.ZoneRunways[1] = ZONE_POLYGON:New( "Batumi Runway 1", BatumiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Beslan - -- local BeslanBoundary = GROUP:FindByName( "Beslan Boundary" ) - -- self.Airbases.Beslan.ZoneBoundary = ZONE_POLYGON:New( "Beslan Boundary", BeslanBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local BeslanRunway1 = GROUP:FindByName( "Beslan Runway 1" ) - -- self.Airbases.Beslan.ZoneRunways[1] = ZONE_POLYGON:New( "Beslan Runway 1", BeslanRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Gelendzhik - -- local GelendzhikBoundary = GROUP:FindByName( "Gelendzhik Boundary" ) - -- self.Airbases.Gelendzhik.ZoneBoundary = ZONE_POLYGON:New( "Gelendzhik Boundary", GelendzhikBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local GelendzhikRunway1 = GROUP:FindByName( "Gelendzhik Runway 1" ) - -- self.Airbases.Gelendzhik.ZoneRunways[1] = ZONE_POLYGON:New( "Gelendzhik Runway 1", GelendzhikRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Gudauta - -- local GudautaBoundary = GROUP:FindByName( "Gudauta Boundary" ) - -- self.Airbases.Gudauta.ZoneBoundary = ZONE_POLYGON:New( "Gudauta Boundary", GudautaBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local GudautaRunway1 = GROUP:FindByName( "Gudauta Runway 1" ) - -- self.Airbases.Gudauta.ZoneRunways[1] = ZONE_POLYGON:New( "Gudauta Runway 1", GudautaRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Kobuleti - -- local KobuletiBoundary = GROUP:FindByName( "Kobuleti Boundary" ) - -- self.Airbases.Kobuleti.ZoneBoundary = ZONE_POLYGON:New( "Kobuleti Boundary", KobuletiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local KobuletiRunway1 = GROUP:FindByName( "Kobuleti Runway 1" ) - -- self.Airbases.Kobuleti.ZoneRunways[1] = ZONE_POLYGON:New( "Kobuleti Runway 1", KobuletiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- KrasnodarCenter - -- local KrasnodarCenterBoundary = GROUP:FindByName( "KrasnodarCenter Boundary" ) - -- self.Airbases.KrasnodarCenter.ZoneBoundary = ZONE_POLYGON:New( "KrasnodarCenter Boundary", KrasnodarCenterBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local KrasnodarCenterRunway1 = GROUP:FindByName( "KrasnodarCenter Runway 1" ) - -- self.Airbases.KrasnodarCenter.ZoneRunways[1] = ZONE_POLYGON:New( "KrasnodarCenter Runway 1", KrasnodarCenterRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- KrasnodarPashkovsky - -- local KrasnodarPashkovskyBoundary = GROUP:FindByName( "KrasnodarPashkovsky Boundary" ) - -- self.Airbases.KrasnodarPashkovsky.ZoneBoundary = ZONE_POLYGON:New( "KrasnodarPashkovsky Boundary", KrasnodarPashkovskyBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local KrasnodarPashkovskyRunway1 = GROUP:FindByName( "KrasnodarPashkovsky Runway 1" ) - -- self.Airbases.KrasnodarPashkovsky.ZoneRunways[1] = ZONE_POLYGON:New( "KrasnodarPashkovsky Runway 1", KrasnodarPashkovskyRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- local KrasnodarPashkovskyRunway2 = GROUP:FindByName( "KrasnodarPashkovsky Runway 2" ) - -- self.Airbases.KrasnodarPashkovsky.ZoneRunways[2] = ZONE_POLYGON:New( "KrasnodarPashkovsky Runway 2", KrasnodarPashkovskyRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Krymsk - -- local KrymskBoundary = GROUP:FindByName( "Krymsk Boundary" ) - -- self.Airbases.Krymsk.ZoneBoundary = ZONE_POLYGON:New( "Krymsk Boundary", KrymskBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local KrymskRunway1 = GROUP:FindByName( "Krymsk Runway 1" ) - -- self.Airbases.Krymsk.ZoneRunways[1] = ZONE_POLYGON:New( "Krymsk Runway 1", KrymskRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Kutaisi - -- local KutaisiBoundary = GROUP:FindByName( "Kutaisi Boundary" ) - -- self.Airbases.Kutaisi.ZoneBoundary = ZONE_POLYGON:New( "Kutaisi Boundary", KutaisiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local KutaisiRunway1 = GROUP:FindByName( "Kutaisi Runway 1" ) - -- self.Airbases.Kutaisi.ZoneRunways[1] = ZONE_POLYGON:New( "Kutaisi Runway 1", KutaisiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- MaykopKhanskaya - -- local MaykopKhanskayaBoundary = GROUP:FindByName( "MaykopKhanskaya Boundary" ) - -- self.Airbases.MaykopKhanskaya.ZoneBoundary = ZONE_POLYGON:New( "MaykopKhanskaya Boundary", MaykopKhanskayaBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local MaykopKhanskayaRunway1 = GROUP:FindByName( "MaykopKhanskaya Runway 1" ) - -- self.Airbases.MaykopKhanskaya.ZoneRunways[1] = ZONE_POLYGON:New( "MaykopKhanskaya Runway 1", MaykopKhanskayaRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- MineralnyeVody - -- local MineralnyeVodyBoundary = GROUP:FindByName( "MineralnyeVody Boundary" ) - -- self.Airbases.MineralnyeVody.ZoneBoundary = ZONE_POLYGON:New( "MineralnyeVody Boundary", MineralnyeVodyBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local MineralnyeVodyRunway1 = GROUP:FindByName( "MineralnyeVody Runway 1" ) - -- self.Airbases.MineralnyeVody.ZoneRunways[1] = ZONE_POLYGON:New( "MineralnyeVody Runway 1", MineralnyeVodyRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Mozdok - -- local MozdokBoundary = GROUP:FindByName( "Mozdok Boundary" ) - -- self.Airbases.Mozdok.ZoneBoundary = ZONE_POLYGON:New( "Mozdok Boundary", MozdokBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local MozdokRunway1 = GROUP:FindByName( "Mozdok Runway 1" ) - -- self.Airbases.Mozdok.ZoneRunways[1] = ZONE_POLYGON:New( "Mozdok Runway 1", MozdokRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Nalchik - -- local NalchikBoundary = GROUP:FindByName( "Nalchik Boundary" ) - -- self.Airbases.Nalchik.ZoneBoundary = ZONE_POLYGON:New( "Nalchik Boundary", NalchikBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local NalchikRunway1 = GROUP:FindByName( "Nalchik Runway 1" ) - -- self.Airbases.Nalchik.ZoneRunways[1] = ZONE_POLYGON:New( "Nalchik Runway 1", NalchikRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Novorossiysk - -- local NovorossiyskBoundary = GROUP:FindByName( "Novorossiysk Boundary" ) - -- self.Airbases.Novorossiysk.ZoneBoundary = ZONE_POLYGON:New( "Novorossiysk Boundary", NovorossiyskBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local NovorossiyskRunway1 = GROUP:FindByName( "Novorossiysk Runway 1" ) - -- self.Airbases.Novorossiysk.ZoneRunways[1] = ZONE_POLYGON:New( "Novorossiysk Runway 1", NovorossiyskRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- SenakiKolkhi - -- local SenakiKolkhiBoundary = GROUP:FindByName( "SenakiKolkhi Boundary" ) - -- self.Airbases.SenakiKolkhi.ZoneBoundary = ZONE_POLYGON:New( "SenakiKolkhi Boundary", SenakiKolkhiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local SenakiKolkhiRunway1 = GROUP:FindByName( "SenakiKolkhi Runway 1" ) - -- self.Airbases.SenakiKolkhi.ZoneRunways[1] = ZONE_POLYGON:New( "SenakiKolkhi Runway 1", SenakiKolkhiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- SochiAdler - -- local SochiAdlerBoundary = GROUP:FindByName( "SochiAdler Boundary" ) - -- self.Airbases.SochiAdler.ZoneBoundary = ZONE_POLYGON:New( "SochiAdler Boundary", SochiAdlerBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local SochiAdlerRunway1 = GROUP:FindByName( "SochiAdler Runway 1" ) - -- self.Airbases.SochiAdler.ZoneRunways[1] = ZONE_POLYGON:New( "SochiAdler Runway 1", SochiAdlerRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- local SochiAdlerRunway2 = GROUP:FindByName( "SochiAdler Runway 2" ) - -- self.Airbases.SochiAdler.ZoneRunways[2] = ZONE_POLYGON:New( "SochiAdler Runway 2", SochiAdlerRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Soganlug - -- local SoganlugBoundary = GROUP:FindByName( "Soganlug Boundary" ) - -- self.Airbases.Soganlug.ZoneBoundary = ZONE_POLYGON:New( "Soganlug Boundary", SoganlugBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local SoganlugRunway1 = GROUP:FindByName( "Soganlug Runway 1" ) - -- self.Airbases.Soganlug.ZoneRunways[1] = ZONE_POLYGON:New( "Soganlug Runway 1", SoganlugRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- SukhumiBabushara - -- local SukhumiBabusharaBoundary = GROUP:FindByName( "SukhumiBabushara Boundary" ) - -- self.Airbases.SukhumiBabushara.ZoneBoundary = ZONE_POLYGON:New( "SukhumiBabushara Boundary", SukhumiBabusharaBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local SukhumiBabusharaRunway1 = GROUP:FindByName( "SukhumiBabushara Runway 1" ) - -- self.Airbases.SukhumiBabushara.ZoneRunways[1] = ZONE_POLYGON:New( "SukhumiBabushara Runway 1", SukhumiBabusharaRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- TbilisiLochini - -- local TbilisiLochiniBoundary = GROUP:FindByName( "TbilisiLochini Boundary" ) - -- self.Airbases.TbilisiLochini.ZoneBoundary = ZONE_POLYGON:New( "TbilisiLochini Boundary", TbilisiLochiniBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local TbilisiLochiniRunway1 = GROUP:FindByName( "TbilisiLochini Runway 1" ) - -- self.Airbases.TbilisiLochini.ZoneRunways[1] = ZONE_POLYGON:New( "TbilisiLochini Runway 1", TbilisiLochiniRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- local TbilisiLochiniRunway2 = GROUP:FindByName( "TbilisiLochini Runway 2" ) - -- self.Airbases.TbilisiLochini.ZoneRunways[2] = ZONE_POLYGON:New( "TbilisiLochini Runway 2", TbilisiLochiniRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Vaziani - -- local VazianiBoundary = GROUP:FindByName( "Vaziani Boundary" ) - -- self.Airbases.Vaziani.ZoneBoundary = ZONE_POLYGON:New( "Vaziani Boundary", VazianiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local VazianiRunway1 = GROUP:FindByName( "Vaziani Runway 1" ) - -- self.Airbases.Vaziani.ZoneRunways[1] = ZONE_POLYGON:New( "Vaziani Runway 1", VazianiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - - - -- Template - -- local TemplateBoundary = GROUP:FindByName( "Template Boundary" ) - -- self.Airbases.Template.ZoneBoundary = ZONE_POLYGON:New( "Template Boundary", TemplateBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local TemplateRunway1 = GROUP:FindByName( "Template Runway 1" ) - -- self.Airbases.Template.ZoneRunways[1] = ZONE_POLYGON:New( "Template Runway 1", TemplateRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - return self end @@ -55051,7 +63879,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` @@ -55103,445 +63931,37 @@ end -- ## 2. Set various options -- -- There are various methods that you can use to tweak the behaviour of the ATC\_GROUND classes. --- --- ### 2.1 Speed limit at an airbase. --- --- * @{#ATC_GROUND.SetKickSpeed}(): Set the speed limit allowed at an airbase in meters per second. --- * @{#ATC_GROUND.SetKickSpeedKmph}(): Set the speed limit allowed at an airbase in kilometers per hour. --- * @{#ATC_GROUND.SetKickSpeedMiph}(): Set the speed limit allowed at an airbase in miles per hour. --- --- ### 2.2 Prevent Takeoff at an airbase. Players will be kicked immediately. --- --- * @{#ATC_GROUND.SetMaximumKickSpeed}(): Set the maximum speed allowed at an airbase in meters per second. --- * @{#ATC_GROUND.SetMaximumKickSpeedKmph}(): Set the maximum speed allowed at an airbase in kilometers per hour. --- * @{#ATC_GROUND.SetMaximumKickSpeedMiph}(): Set the maximum speed allowed at an airbase in miles per hour. --- --- --- @field #ATC_GROUND_NEVADA -ATC_GROUND_NEVADA = { - ClassName = "ATC_GROUND_NEVADA", - Airbases = { - - [AIRBASE.Nevada.Beatty_Airport] = { - PointsRunways = { - [1] = { - [1]={["y"]=-174950.05857143,["x"]=-329679.65,}, - [2]={["y"]=-174946.53828571,["x"]=-331394.03885715,}, - [3]={["y"]=-174967.10971429,["x"]=-331394.32457143,}, - [4]={["y"]=-174971.01828571,["x"]=-329682.59171429,}, - }, - }, - }, - [AIRBASE.Nevada.Boulder_City_Airport] = { - PointsRunways = { - [1] = { - [1] = {["y"]=-1317.841714286,["x"]=-429014.92857142,}, - [2] = {["y"]=-951.26228571458,["x"]=-430310.21142856,}, - [3] = {["y"]=-978.11942857172,["x"]=-430317.06857142,}, - [4] = {["y"]=-1347.5088571432,["x"]=-429023.98485713,}, - }, - [2] = { - [1] = {["y"]=-1879.955714286,["x"]=-429783.83742856,}, - [2] = {["y"]=-256.25257142886,["x"]=-430023.63542856,}, - [3] = {["y"]=-260.25257142886,["x"]=-430048.77828571,}, - [4] = {["y"]=-1883.955714286,["x"]=-429807.83742856,}, - }, - }, - }, - [AIRBASE.Nevada.Creech_AFB] = { - PointsRunways = { - [1] = { - [1] = {["y"]=-74234.729142857,["x"]=-360501.80857143,}, - [2] = {["y"]=-77606.122285714,["x"]=-360417.86542857,}, - [3] = {["y"]=-77608.578,["x"]=-360486.13428571,}, - [4] = {["y"]=-74237.930571428,["x"]=-360586.25628571,}, - }, - [2] = { - [1] = {["y"]=-75807.571428572,["x"]=-359073.42857142,}, - [2] = {["y"]=-74770.142857144,["x"]=-360581.71428571,}, - [3] = {["y"]=-74641.285714287,["x"]=-360585.42857142,}, - [4] = {["y"]=-75734.142857144,["x"]=-359023.14285714,}, - }, - }, - }, - [AIRBASE.Nevada.Echo_Bay] = { - PointsRunways = { - [1] = { - [1] = {["y"]=33182.919428572,["x"]=-388698.21657142,}, - [2] = {["y"]=34202.543142857,["x"]=-388469.55485714,}, - [3] = {["y"]=34207.686,["x"]=-388488.69771428,}, - [4] = {["y"]=33185.422285715,["x"]=-388717.82228571,}, - }, - }, - }, - [AIRBASE.Nevada.Groom_Lake_AFB] = { - PointsRunways = { - [1] = { - [1] = {["y"]=-85971.465428571,["x"]=-290567.77,}, - [2] = {["y"]=-87691.155428571,["x"]=-286637.75428571,}, - [3] = {["y"]=-87756.714285715,["x"]=-286663.99999999,}, - [4] = {["y"]=-86035.940285714,["x"]=-290598.81314286,}, - }, - [2] = { - [1] = {["y"]=-86741.547142857,["x"]=-290353.31971428,}, - [2] = {["y"]=-89672.714285714,["x"]=-283546.57142855,}, - [3] = {["y"]=-89772.142857143,["x"]=-283587.71428569,}, - [4] = {["y"]=-86799.623714285,["x"]=-290374.16771428,}, - }, - }, - }, - [AIRBASE.Nevada.Henderson_Executive_Airport] = { - PointsRunways = { - [1] = { - [1] = {["y"]=-25837.500571429,["x"]=-426404.25257142,}, - [2] = {["y"]=-25843.509428571,["x"]=-428752.67942856,}, - [3] = {["y"]=-25902.343714286,["x"]=-428749.96399999,}, - [4] = {["y"]=-25934.667142857,["x"]=-426411.45657142,}, - }, - [2] = { - [1] = {["y"]=-25650.296285714,["x"]=-426510.17971428,}, - [2] = {["y"]=-25632.443428571,["x"]=-428297.11428571,}, - [3] = {["y"]=-25686.690285714,["x"]=-428299.37457142,}, - [4] = {["y"]=-25708.296285714,["x"]=-426515.15114285,}, - }, - }, - }, - [AIRBASE.Nevada.Jean_Airport] = { - PointsRunways = { - [1] = { - [1] = {["y"]=-42549.187142857,["x"]=-449663.23257143,}, - [2] = {["y"]=-43367.466285714,["x"]=-451044.77657143,}, - [3] = {["y"]=-43395.180571429,["x"]=-451028.20514286,}, - [4] = {["y"]=-42579.893142857,["x"]=-449648.18371428,}, - }, - [2] = { - [1] = {["y"]=-42588.359428572,["x"]=-449900.14342857,}, - [2] = {["y"]=-43349.698285714,["x"]=-451185.46857143,}, - [3] = {["y"]=-43369.624571429,["x"]=-451173.49342857,}, - [4] = {["y"]=-42609.216571429,["x"]=-449891.28628571,}, - }, - }, - }, - [AIRBASE.Nevada.Laughlin_Airport] = { - PointsRunways = { - [1] = { - [1] = {["y"]=28231.600857143,["x"]=-515555.94114286,}, - [2] = {["y"]=28453.728285714,["x"]=-518170.78885714,}, - [3] = {["y"]=28370.788285714,["x"]=-518176.25742857,}, - [4] = {["y"]=28138.022857143,["x"]=-515573.07514286,}, - }, - [2] = { - [1] = {["y"]=28231.600857143,["x"]=-515555.94114286,}, - [2] = {["y"]=28453.728285714,["x"]=-518170.78885714,}, - [3] = {["y"]=28370.788285714,["x"]=-518176.25742857,}, - [4] = {["y"]=28138.022857143,["x"]=-515573.07514286,}, - }, - }, - }, - [AIRBASE.Nevada.Lincoln_County] = { - PointsRunways = { - [1] = { - [1]={["y"]=33222.34171429,["x"]=-223959.40171429,}, - [2]={["y"]=33200.040000004,["x"]=-225369.36828572,}, - [3]={["y"]=33177.634571428,["x"]=-225369.21485715,}, - [4]={["y"]=33201.198857147,["x"]=-223960.54457143,}, - }, - }, - }, - [AIRBASE.Nevada.McCarran_International_Airport] = { - PointsRunways = { - [1] = { - [1] = {["y"]=-29406.035714286,["x"]=-416102.48199999,}, - [2] = {["y"]=-24680.714285715,["x"]=-416003.14285713,}, - [3] = {["y"]=-24681.857142858,["x"]=-415926.57142856,}, - [4] = {["y"]=-29408.42857143,["x"]=-416016.57142856,}, - }, - [2] = { - [1] = {["y"]=-28567.221714286,["x"]=-416378.61799999,}, - [2] = {["y"]=-25109.912285714,["x"]=-416309.92914285,}, - [3] = {["y"]=-25112.508,["x"]=-416240.78714285,}, - [4] = {["y"]=-28576.247428571,["x"]=-416308.49514285,}, - }, - [3] = { - [1] = {["y"]=-29255.953142857,["x"]=-416307.10657142,}, - [2] = {["y"]=-28005.571428572,["x"]=-413449.7142857,}, - [3] = {["y"]=-28068.714285715,["x"]=-413422.85714284,}, - [4] = {["y"]=-29331.000000001,["x"]=-416275.7142857,}, - }, - [4] = { - [1] = {["y"]=-28994.901714286,["x"]=-416423.0522857,}, - [2] = {["y"]=-27697.571428572,["x"]=-413464.57142856,}, - [3] = {["y"]=-27767.857142858,["x"]=-413434.28571427,}, - [4] = {["y"]=-29073.000000001,["x"]=-416386.85714284,}, - }, - }, - }, - [AIRBASE.Nevada.Mesquite] = { - PointsRunways = { - [1] = { - [1] = {["y"]=68188.340285714,["x"]=-330302.54742857,}, - [2] = {["y"]=68911.303428571,["x"]=-328920.76571429,}, - [3] = {["y"]=68936.927142857,["x"]=-328933.888,}, - [4] = {["y"]=68212.460285714,["x"]=-330317.19171429,}, - }, - }, - }, - [AIRBASE.Nevada.Mina_Airport_3Q0] = { - PointsRunways = { - [1] = { - [1] = {["y"]=-290054.57371429,["x"]=-160930.02228572,}, - [2] = {["y"]=-289469.77457143,["x"]=-162048.73571429,}, - [3] = {["y"]=-289520.06028572,["x"]=-162074.73571429,}, - [4] = {["y"]=-290104.69085714,["x"]=-160956.19457143,}, - }, - }, - }, - [AIRBASE.Nevada.Nellis_AFB] = { - PointsRunways = { - [1] = { - [1] = {["y"]=-18614.218571428,["x"]=-399437.91085714,}, - [2] = {["y"]=-16217.857142857,["x"]=-396596.85714286,}, - [3] = {["y"]=-16300.142857143,["x"]=-396530,}, - [4] = {["y"]=-18692.543428571,["x"]=-399381.31114286,}, - }, - [2] = { - [1] = {["y"]=-18388.948857143,["x"]=-399630.51828571,}, - [2] = {["y"]=-16011,["x"]=-396806.85714286,}, - [3] = {["y"]=-16074.714285714,["x"]=-396751.71428572,}, - [4] = {["y"]=-18451.571428572,["x"]=-399580.85714285,}, - }, - }, - }, - [AIRBASE.Nevada.Pahute_Mesa_Airstrip] = { - PointsRunways = { - [1] = { - [1] = {["y"]=-132690.40942857,["x"]=-302733.53085714,}, - [2] = {["y"]=-133112.43228571,["x"]=-304499.70742857,}, - [3] = {["y"]=-133179.91685714,["x"]=-304485.544,}, - [4] = {["y"]=-132759.988,["x"]=-302723.326,}, - }, - }, - }, - [AIRBASE.Nevada.Tonopah_Test_Range_Airfield] = { - PointsRunways = { - [1] = { - [1] = {["y"]=-175389.162,["x"]=-224778.07685715,}, - [2] = {["y"]=-173942.15485714,["x"]=-228210.27571429,}, - [3] = {["y"]=-174001.77085714,["x"]=-228233.60371429,}, - [4] = {["y"]=-175452.38685714,["x"]=-224806.84200001,}, - }, - }, - }, - [AIRBASE.Nevada.Tonopah_Airport] = { - PointsRunways = { - [1] = { - [1] = {["y"]=-202128.25228571,["x"]=-196701.34314286,}, - [2] = {["y"]=-201562.40828571,["x"]=-198814.99714286,}, - [3] = {["y"]=-201591.44828571,["x"]=-198820.93714286,}, - [4] = {["y"]=-202156.06828571,["x"]=-196707.68714286,}, - }, - [2] = { - [1] = {["y"]=-202084.57171428,["x"]=-196722.02228572,}, - [2] = {["y"]=-200592.75485714,["x"]=-197768.05571429,}, - [3] = {["y"]=-200605.37285714,["x"]=-197783.49228572,}, - [4] = {["y"]=-202097.14314285,["x"]=-196739.16514286,}, - }, - }, - }, - [AIRBASE.Nevada.North_Las_Vegas] = { - PointsRunways = { - [1] = { - [1] = {["y"]=-32599.017714286,["x"]=-400913.26485714,}, - [2] = {["y"]=-30881.068857143,["x"]=-400837.94628571,}, - [3] = {["y"]=-30879.354571428,["x"]=-400873.08914285,}, - [4] = {["y"]=-32595.966285714,["x"]=-400947.13571428,}, - }, - [2] = { - [1] = {["y"]=-32499.448571428,["x"]=-400690.99514285,}, - [2] = {["y"]=-31247.514857143,["x"]=-401868.95571428,}, - [3] = {["y"]=-31271.802857143,["x"]=-401894.97857142,}, - [4] = {["y"]=-32520.02,["x"]=-400716.99514285,}, - }, - [3] = { - [1] = {["y"]=-31865.254857143,["x"]=-400999.74057143,}, - [2] = {["y"]=-30893.604,["x"]=-401908.85742857,}, - [3] = {["y"]=-30915.578857143,["x"]=-401936.03685714,}, - [4] = {["y"]=-31884.969142858,["x"]=-401020.59771429,}, - }, - }, - }, - }, -} - ---- Creates a new ATC_GROUND_NEVADA object. --- @param #ATC_GROUND_NEVADA self --- @param AirbaseNames A list {} of airbase names (Use AIRBASE.Nevada enumerator). --- @return #ATC_GROUND_NEVADA self -function ATC_GROUND_NEVADA:New( AirbaseNames ) - - -- Inherits from BASE - local self = BASE:Inherit( self, ATC_GROUND:New( self.Airbases, AirbaseNames ) ) - - self:SetKickSpeedKmph( 50 ) - self:SetMaximumKickSpeedKmph( 150 ) - - -- These lines here are for the demonstration mission. - -- They create in the dcs.log the coordinates of the runway polygons, that are then - -- taken by the moose designer from the dcs.log and reworked to define the - -- Airbases structure, which is part of the class. - -- When new airbases are added or airbases are changed on the map, - -- the MOOSE designer willde-comment this section and apply the changes in the demo - -- mission, and do a re-run to create a new dcs.log, and then add the changed coordinates - -- in the Airbases structure. - -- So, this needs to stay commented normally once a map has been finished. - - --[[ - - -- Beatty - do - local VillagePrefix = "Beatty" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - -- Boulder - do - local VillagePrefix = "Boulder" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) - local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - -- Creech - do - local VillagePrefix = "Creech" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) - local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - -- Echo - do - local VillagePrefix = "Echo" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - -- Groom Lake - do - local VillagePrefix = "GroomLake" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) - local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - -- Henderson - do - local VillagePrefix = "Henderson" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) - local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - -- Jean - do - local VillagePrefix = "Jean" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) - local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - -- Laughlin - do - local VillagePrefix = "Laughlin" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - -- Lincoln - do - local VillagePrefix = "Lincoln" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - -- McCarran - do - local VillagePrefix = "McCarran" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) - local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - local Runway3 = GROUP:FindByName( VillagePrefix .. " 3" ) - local Zone3 = ZONE_POLYGON:New( VillagePrefix .. " 3", Runway3 ):SmokeZone(SMOKECOLOR.Red):Flush() - local Runway4 = GROUP:FindByName( VillagePrefix .. " 4" ) - local Zone4 = ZONE_POLYGON:New( VillagePrefix .. " 4", Runway4 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - -- Mesquite - do - local VillagePrefix = "Mesquite" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - -- Mina - do - local VillagePrefix = "Mina" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end +-- +-- ### 2.1 Speed limit at an airbase. +-- +-- * @{#ATC_GROUND.SetKickSpeed}(): Set the speed limit allowed at an airbase in meters per second. +-- * @{#ATC_GROUND.SetKickSpeedKmph}(): Set the speed limit allowed at an airbase in kilometers per hour. +-- * @{#ATC_GROUND.SetKickSpeedMiph}(): Set the speed limit allowed at an airbase in miles per hour. +-- +-- ### 2.2 Prevent Takeoff at an airbase. Players will be kicked immediately. +-- +-- * @{#ATC_GROUND.SetMaximumKickSpeed}(): Set the maximum speed allowed at an airbase in meters per second. +-- * @{#ATC_GROUND.SetMaximumKickSpeedKmph}(): Set the maximum speed allowed at an airbase in kilometers per hour. +-- * @{#ATC_GROUND.SetMaximumKickSpeedMiph}(): Set the maximum speed allowed at an airbase in miles per hour. +-- +-- +-- @field #ATC_GROUND_NEVADA +ATC_GROUND_NEVADA = { + ClassName = "ATC_GROUND_NEVADA", +} - -- Nellis - do - local VillagePrefix = "Nellis" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) - local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - -- Pahute - do - local VillagePrefix = "Pahute" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end +--- Creates a new ATC_GROUND_NEVADA object. +-- @param #ATC_GROUND_NEVADA self +-- @param AirbaseNames A list {} of airbase names (Use AIRBASE.Nevada enumerator). +-- @return #ATC_GROUND_NEVADA self +function ATC_GROUND_NEVADA:New( AirbaseNames ) - -- TonopahTR - do - local VillagePrefix = "TonopahTR" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end + -- Inherits from BASE + local self = BASE:Inherit( self, ATC_GROUND_UNIVERSAL:New( AirbaseNames ) ) - -- Tonopah - do - local VillagePrefix = "Tonopah" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) - local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - end + self:SetKickSpeedKmph( 50 ) + self:SetMaximumKickSpeedKmph( 150 ) - -- Vegas - do - local VillagePrefix = "Vegas" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) - local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - local Runway3 = GROUP:FindByName( VillagePrefix .. " 3" ) - local Zone3 = ZONE_POLYGON:New( VillagePrefix .. " 3", Runway3 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - --]] - return self end @@ -55682,440 +64102,7 @@ end -- -- @field #ATC_GROUND_NORMANDY ATC_GROUND_NORMANDY = { - ClassName = "ATC_GROUND_NORMANDY", - Airbases = { - [AIRBASE.Normandy.Azeville] = { - PointsRunways = { - [1] = { - [1]={["y"]=-74194.387714285,["x"]=-2691.1399999998,}, - [2]={["y"]=-73160.282571428,["x"]=-2310.0274285712,}, - [3]={["y"]=-73141.711142857,["x"]=-2357.7417142855,}, - [4]={["y"]=-74176.959142857,["x"]=-2741.997142857,}, - }, - }, - }, - [AIRBASE.Normandy.Bazenville] = { - PointsRunways = { - [1] = { - [1]={["y"]=-19246.209999999,["x"]=-21246.748,}, - [2]={["y"]=-17883.70142857,["x"]=-20219.009714285,}, - [3]={["y"]=-17855.415714285,["x"]=-20256.438285714,}, - [4]={["y"]=-19217.791999999,["x"]=-21283.597714285,}, - }, - }, - }, - [AIRBASE.Normandy.Beny_sur_Mer] = { - PointsRunways = { - [1] = { - [1]={["y"]=-8592.7442857133,["x"]=-20386.15542857,}, - [2]={["y"]=-8404.4931428561,["x"]=-21744.113142856,}, - [3]={["y"]=-8267.9917142847,["x"]=-21724.97742857,}, - [4]={["y"]=-8451.0482857133,["x"]=-20368.87542857,}, - }, - }, - }, - [AIRBASE.Normandy.Beuzeville] = { - PointsRunways = { - [1] = { - [1]={["y"]=-71552.573428571,["x"]=-8744.3688571427,}, - [2]={["y"]=-72577.765714285,["x"]=-9638.5682857141,}, - [3]={["y"]=-72609.304285714,["x"]=-9601.2954285712,}, - [4]={["y"]=-71585.849428571,["x"]=-8709.9648571426,}, - }, - }, - }, - [AIRBASE.Normandy.Biniville] = { - PointsRunways = { - [1] = { - [1]={["y"]=-84757.320285714,["x"]=-7377.1354285713,}, - [2]={["y"]=-84271.482,["x"]=-7956.4859999999,}, - [3]={["y"]=-84299.482,["x"]=-7981.6288571427,}, - [4]={["y"]=-84784.969714286,["x"]=-7402.0588571427,}, - }, - }, - }, - [AIRBASE.Normandy.Brucheville] = { - PointsRunways = { - [1] = { - [1]={["y"]=-65546.792857142,["x"]=-14615.640857143,}, - [2]={["y"]=-66914.692,["x"]=-15232.713714285,}, - [3]={["y"]=-66896.527714285,["x"]=-15271.948571428,}, - [4]={["y"]=-65528.393714285,["x"]=-14657.995714286,}, - }, - }, - }, - [AIRBASE.Normandy.Cardonville] = { - PointsRunways = { - [1] = { - [1]={["y"]=-54280.445428571,["x"]=-15843.749142857,}, - [2]={["y"]=-53646.998571428,["x"]=-17143.012285714,}, - [3]={["y"]=-53683.93,["x"]=-17161.317428571,}, - [4]={["y"]=-54323.354571428,["x"]=-15855.004,}, - }, - }, - }, - [AIRBASE.Normandy.Carpiquet] = { - PointsRunways = { - [1] = { - [1]={["y"]=-10751.325714285,["x"]=-34229.494,}, - [2]={["y"]=-9283.5279999993,["x"]=-35192.352857142,}, - [3]={["y"]=-9325.2005714274,["x"]=-35260.967714285,}, - [4]={["y"]=-10794.90942857,["x"]=-34287.041428571,}, - }, - }, - }, - [AIRBASE.Normandy.Chailey] = { - PointsRunways = { - [1] = { - [1]={["y"]=12895.585714292,["x"]=164683.05657144,}, - [2]={["y"]=11410.727142863,["x"]=163606.54485715,}, - [3]={["y"]=11363.012857149,["x"]=163671.97342858,}, - [4]={["y"]=12797.537142863,["x"]=164711.01857144,}, - [5]={["y"]=12862.902857149,["x"]=164726.99685715,}, - }, - [2] = { - [1]={["y"]=11805.316000006,["x"]=164502.90971429,}, - [2]={["y"]=11997.280857149,["x"]=163032.65542858,}, - [3]={["y"]=11918.640857149,["x"]=163023.04657144,}, - [4]={["y"]=11726.973428578,["x"]=164489.94257143,}, - }, - }, - }, - [AIRBASE.Normandy.Chippelle] = { - PointsRunways = { - [1] = { - [1]={["y"]=-48540.313999999,["x"]=-28884.795999999,}, - [2]={["y"]=-47251.820285713,["x"]=-28140.128571427,}, - [3]={["y"]=-47274.551714285,["x"]=-28103.758285713,}, - [4]={["y"]=-48555.657714285,["x"]=-28839.90142857,}, - }, - }, - }, - [AIRBASE.Normandy.Cretteville] = { - PointsRunways = { - [1] = { - [1]={["y"]=-78351.723142857,["x"]=-18177.725428571,}, - [2]={["y"]=-77220.322285714,["x"]=-19125.687714286,}, - [3]={["y"]=-77247.899428571,["x"]=-19158.49,}, - [4]={["y"]=-78380.008857143,["x"]=-18208.011142857,}, - }, - }, - }, - [AIRBASE.Normandy.Cricqueville_en_Bessin] = { - PointsRunways = { - [1] = { - [1]={["y"]=-50875.034571428,["x"]=-14322.404571428,}, - [2]={["y"]=-50681.148571428,["x"]=-15825.258,}, - [3]={["y"]=-50717.434285713,["x"]=-15829.829428571,}, - [4]={["y"]=-50910.569428571,["x"]=-14327.562857142,}, - }, - }, - }, - [AIRBASE.Normandy.Deux_Jumeaux] = { - PointsRunways = { - [1] = { - [1]={["y"]=-49575.410857142,["x"]=-16575.161142857,}, - [2]={["y"]=-48149.077999999,["x"]=-16952.193428571,}, - [3]={["y"]=-48159.935142856,["x"]=-16996.764857142,}, - [4]={["y"]=-49584.839428571,["x"]=-16617.732571428,}, - }, - }, - }, - [AIRBASE.Normandy.Evreux] = { - PointsRunways = { - [1] = { - [1]={["y"]=112906.84828572,["x"]=-45585.824857142,}, - [2]={["y"]=112050.38228572,["x"]=-46811.871999999,}, - [3]={["y"]=111980.05371429,["x"]=-46762.173142856,}, - [4]={["y"]=112833.54542857,["x"]=-45540.010571428,}, - }, - [2] = { - [1]={["y"]=112046.02085714,["x"]=-45091.056571428,}, - [2]={["y"]=112488.668,["x"]=-46623.617999999,}, - [3]={["y"]=112405.66914286,["x"]=-46647.419142856,}, - [4]={["y"]=111966.03657143,["x"]=-45112.604285713,}, - }, - }, - }, - [AIRBASE.Normandy.Ford_AF] = { - PointsRunways = { - [1] = { - [1]={["y"]=-26506.13971428,["x"]=147514.39971429,}, - [2]={["y"]=-25012.977428565,["x"]=147566.14485715,}, - [3]={["y"]=-25009.851428565,["x"]=147482.63600001,}, - [4]={["y"]=-26503.693999994,["x"]=147427.33228572,}, - }, - [2] = { - [1]={["y"]=-25169.701999994,["x"]=148421.09257143,}, - [2]={["y"]=-26092.421999994,["x"]=147190.89628572,}, - [3]={["y"]=-26158.136285708,["x"]=147240.89628572,}, - [4]={["y"]=-25252.357999994,["x"]=148448.64457143,}, - }, - }, - }, - [AIRBASE.Normandy.Funtington] = { - PointsRunways = { - [1] = { - [1]={["y"]=-44698.388571423,["x"]=152952.17257143,}, - [2]={["y"]=-46452.993142851,["x"]=152388.77885714,}, - [3]={["y"]=-46476.361142851,["x"]=152470.05885714,}, - [4]={["y"]=-44787.256571423,["x"]=153009.52,}, - [5]={["y"]=-44715.581428566,["x"]=153002.08714286,}, - }, - [2] = { - [1]={["y"]=-45792.665999994,["x"]=153123.894,}, - [2]={["y"]=-46068.084857137,["x"]=151665.98342857,}, - [3]={["y"]=-46148.632285708,["x"]=151681.58685714,}, - [4]={["y"]=-45871.25971428,["x"]=153136.82714286,}, - }, - }, - }, - [AIRBASE.Normandy.Lantheuil] = { - PointsRunways = { - [1] = { - [1]={["y"]=-17158.84542857,["x"]=-24602.999428571,}, - [2]={["y"]=-15978.59342857,["x"]=-23922.978571428,}, - [3]={["y"]=-15932.021999999,["x"]=-24004.121428571,}, - [4]={["y"]=-17090.734857142,["x"]=-24673.248,}, - }, - }, - }, - [AIRBASE.Normandy.Lessay] = { - PointsRunways = { - [1] = { - [1]={["y"]=-87667.304571429,["x"]=-33220.165714286,}, - [2]={["y"]=-86146.607714286,["x"]=-34248.483142857,}, - [3]={["y"]=-86191.538285714,["x"]=-34316.991142857,}, - [4]={["y"]=-87712.212,["x"]=-33291.774857143,}, - }, - [2] = { - [1]={["y"]=-87125.123142857,["x"]=-34183.682571429,}, - [2]={["y"]=-85803.278285715,["x"]=-33498.428857143,}, - [3]={["y"]=-85768.408285715,["x"]=-33570.13,}, - [4]={["y"]=-87087.688571429,["x"]=-34258.272285715,}, - }, - }, - }, - [AIRBASE.Normandy.Lignerolles] = { - PointsRunways = { - [1] = { - [1]={["y"]=-35279.611714285,["x"]=-35232.026857142,}, - [2]={["y"]=-33804.948857142,["x"]=-35770.713999999,}, - [3]={["y"]=-33789.876285713,["x"]=-35726.655714284,}, - [4]={["y"]=-35263.548285713,["x"]=-35192.75542857,}, - }, - }, - }, - [AIRBASE.Normandy.Longues_sur_Mer] = { - PointsRunways = { - [1] = { - [1]={["y"]=-29444.070285713,["x"]=-16334.105428571,}, - [2]={["y"]=-28265.52942857,["x"]=-17011.557999999,}, - [3]={["y"]=-28344.74742857,["x"]=-17143.587999999,}, - [4]={["y"]=-29529.616285713,["x"]=-16477.766571428,}, - }, - }, - }, - [AIRBASE.Normandy.Maupertus] = { - PointsRunways = { - [1] = { - [1]={["y"]=-85605.340857143,["x"]=16175.267714286,}, - [2]={["y"]=-84132.567142857,["x"]=15895.905714286,}, - [3]={["y"]=-84139.995142857,["x"]=15847.623714286,}, - [4]={["y"]=-85613.626571429,["x"]=16132.410571429,}, - }, - }, - }, - [AIRBASE.Normandy.Meautis] = { - PointsRunways = { - [1] = { - [1]={["y"]=-72642.527714286,["x"]=-24593.622285714,}, - [2]={["y"]=-71298.672571429,["x"]=-24352.651142857,}, - [3]={["y"]=-71290.101142857,["x"]=-24398.365428571,}, - [4]={["y"]=-72631.715714286,["x"]=-24639.966857143,}, - }, - }, - }, - [AIRBASE.Normandy.Le_Molay] = { - PointsRunways = { - [1] = { - [1]={["y"]=-41876.526857142,["x"]=-26701.052285713,}, - [2]={["y"]=-40979.545714285,["x"]=-25675.045999999,}, - [3]={["y"]=-41017.687428571,["x"]=-25644.272571427,}, - [4]={["y"]=-41913.638285713,["x"]=-26665.137999999,}, - }, - }, - }, - [AIRBASE.Normandy.Needs_Oar_Point] = { - PointsRunways = { - [1] = { - [1]={["y"]=-83882.441142851,["x"]=141429.83314286,}, - [2]={["y"]=-85138.159428566,["x"]=140187.52828572,}, - [3]={["y"]=-85208.323428566,["x"]=140161.04371429,}, - [4]={["y"]=-85245.751999994,["x"]=140201.61514286,}, - [5]={["y"]=-83939.966571423,["x"]=141485.22085714,}, - }, - [2] = { - [1]={["y"]=-84528.76571428,["x"]=141988.01428572,}, - [2]={["y"]=-84116.98971428,["x"]=140565.78685714,}, - [3]={["y"]=-84199.35771428,["x"]=140541.14685714,}, - [4]={["y"]=-84605.051428566,["x"]=141966.01428572,}, - }, - }, - }, - [AIRBASE.Normandy.Picauville] = { - PointsRunways = { - [1] = { - [1]={["y"]=-80808.838571429,["x"]=-11834.554571428,}, - [2]={["y"]=-79531.574285714,["x"]=-12311.274,}, - [3]={["y"]=-79549.355428571,["x"]=-12356.928285714,}, - [4]={["y"]=-80827.815142857,["x"]=-11901.835142857,}, - }, - }, - }, - [AIRBASE.Normandy.Rucqueville] = { - PointsRunways = { - [1] = { - [1]={["y"]=-20023.988857141,["x"]=-26569.565428571,}, - [2]={["y"]=-18688.92542857,["x"]=-26571.086571428,}, - [3]={["y"]=-18688.012571427,["x"]=-26611.252285713,}, - [4]={["y"]=-20022.218857141,["x"]=-26608.505428571,}, - }, - }, - }, - [AIRBASE.Normandy.Saint_Pierre_du_Mont] = { - PointsRunways = { - [1] = { - [1]={["y"]=-48015.384571428,["x"]=-11886.631714285,}, - [2]={["y"]=-46540.412285713,["x"]=-11945.226571428,}, - [3]={["y"]=-46541.349999999,["x"]=-11991.174571428,}, - [4]={["y"]=-48016.837142856,["x"]=-11929.371142857,}, - }, - }, - }, - [AIRBASE.Normandy.Sainte_Croix_sur_Mer] = { - PointsRunways = { - [1] = { - [1]={["y"]=-15877.817999999,["x"]=-18812.579999999,}, - [2]={["y"]=-14464.377142856,["x"]=-18807.46,}, - [3]={["y"]=-14463.879714285,["x"]=-18759.706857142,}, - [4]={["y"]=-15878.229142856,["x"]=-18764.071428571,}, - }, - }, - }, - [AIRBASE.Normandy.Sainte_Laurent_sur_Mer] = { - PointsRunways = { - [1] = { - [1]={["y"]=-41676.834857142,["x"]=-14475.109428571,}, - [2]={["y"]=-40566.11142857,["x"]=-14817.319999999,}, - [3]={["y"]=-40579.543999999,["x"]=-14860.059999999,}, - [4]={["y"]=-41687.120571427,["x"]=-14509.680857142,}, - }, - }, - }, - [AIRBASE.Normandy.Sommervieu] = { - PointsRunways = { - [1] = { - [1]={["y"]=-26821.913714284,["x"]=-21390.466571427,}, - [2]={["y"]=-25465.308857142,["x"]=-21296.859999999,}, - [3]={["y"]=-25462.451714284,["x"]=-21343.717142856,}, - [4]={["y"]=-26818.002285713,["x"]=-21440.532857142,}, - }, - }, - }, - [AIRBASE.Normandy.Tangmere] = { - PointsRunways = { - [1] = { - [1]={["y"]=-34684.581142851,["x"]=150459.61657143,}, - [2]={["y"]=-33250.625428566,["x"]=149954.17,}, - [3]={["y"]=-33275.724285708,["x"]=149874.69028572,}, - [4]={["y"]=-34709.020571423,["x"]=150377.93742857,}, - }, - [2] = { - [1]={["y"]=-33103.438857137,["x"]=150812.72542857,}, - [2]={["y"]=-34410.246285708,["x"]=150009.73142857,}, - [3]={["y"]=-34453.535142851,["x"]=150082.02685714,}, - [4]={["y"]=-33176.545999994,["x"]=150870.22542857,}, - }, - }, - }, - [AIRBASE.Normandy.Argentan] = { - PointsRunways = { - [1] = { - [1]={["y"]=22322.280338032,["x"]=-78607.309765269,}, - [2]={["y"]=23032.778713963,["x"]=-78967.17709893,}, - [3]={["y"]=23015.27074041,["x"]=-79008.02903722,}, - [4]={["y"]=22299.944963827,["x"]=-78650.366148928,}, - }, - }, - }, - [AIRBASE.Normandy.Goulet] = { - PointsRunways = { - [1] = { - [1]={["y"]=24901.788373185,["x"]=-89139.367511763,}, - [2]={["y"]=25459.965967043,["x"]=-89709.67940114,}, - [3]={["y"]=25422.459962713,["x"]=-89741.669816598,}, - [4]={["y"]=24857.663662208,["x"]=-89173.56416277,}, - }, - }, - }, - [AIRBASE.Normandy.Essay] = { - PointsRunways = { - [1] = { - [1]={["y"]=44610.072022849,["x"]=-105469.21149064,}, - [2]={["y"]=45417.939023956,["x"]=-105536.08535277,}, - [3]={["y"]=45412.558368383,["x"]=-105585.27991801,}, - [4]={["y"]=44602.38537203,["x"]=-105516.10006064,}, - }, - }, - }, - [AIRBASE.Normandy.Hauterive] = { - PointsRunways = { - [1] = { - [1]={["y"]=40617.185360953,["x"]=-107657.10147517,}, - [2]={["y"]=41114.628372034,["x"]=-108298.77015609,}, - [3]={["y"]=41080.006684855,["x"]=-108319.06562788,}, - [4]={["y"]=40584.558402807,["x"]=-107692.29370481,}, - }, - }, - }, - [AIRBASE.Normandy.Vrigny] = { - PointsRunways = { - [1] = { - [1]={["y"]=24892.131051827,["x"]=-89131.628297486,}, - [2]={["y"]=25469.738000575,["x"]=-89709.235246234,}, - [3]={["y"]=25418.869206793,["x"]=-89738.771965204,}, - [4]={["y"]=24859.312475193,["x"]=-89171.010589446,}, - }, - }, - }, - [AIRBASE.Normandy.Barville] = { - PointsRunways = { - [1] = { - [1]={["y"]=49027.850333166,["x"]=-109217.05049066,}, - [2]={["y"]=49755.022185805,["x"]=-110346.63783457,}, - [3]={["y"]=49682.657996586,["x"]=-110401.35222154,}, - [4]={["y"]=48921.951519675,["x"]=-109285.88471943,}, - }, - [2] = { - [1]={["y"]=48429.522036941,["x"]=-109818.90874734,}, - [2]={["y"]=49746.197284681,["x"]=-109954.81222465,}, - [3]={["y"]=49735.607403332,["x"]=-110032.47135455,}, - [4]={["y"]=48420.697135816,["x"]=-109900.09783768,}, - }, - }, - }, - [AIRBASE.Normandy.Conches] = { - PointsRunways = { - [1] = { - [1]={["y"]=95099.187473266,["x"]=-56389.619005858,}, - [2]={["y"]=95181.545025963,["x"]=-56465.440244849,}, - [3]={["y"]=94071.678958666,["x"]=-57627.596821795,}, - [4]={["y"]=94005.008558864,["x"]=-57558.31189651,}, - }, - }, - }, - }, + ClassName = "ATC_GROUND_NORMANDY", } @@ -56126,285 +64113,10 @@ ATC_GROUND_NORMANDY = { function ATC_GROUND_NORMANDY:New( AirbaseNames ) -- Inherits from BASE - local self = BASE:Inherit( self, ATC_GROUND:New( self.Airbases, AirbaseNames ) ) -- #ATC_GROUND_NORMANDY + local self = BASE:Inherit( self, ATC_GROUND_UNIVERSAL:New( AirbaseNames ) ) -- #ATC_GROUND_NORMANDY self:SetKickSpeedKmph( 40 ) self:SetMaximumKickSpeedKmph( 100 ) - - -- These lines here are for the demonstration mission. - -- They create in the dcs.log the coordinates of the runway polygons, that are then - -- taken by the moose designer from the dcs.log and reworked to define the - -- Airbases structure, which is part of the class. - -- When new airbases are added or airbases are changed on the map, - -- the MOOSE designer willde-comment this section and apply the changes in the demo - -- mission, and do a re-run to create a new dcs.log, and then add the changed coordinates - -- in the Airbases structure. - -- So, this needs to stay commented normally once a map has been finished. - - --[[ - - -- Azeville - do - local VillagePrefix = "Azeville" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Bazenville - do - local VillagePrefix = "Bazenville" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Beny - do - local VillagePrefix = "Beny" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Beuzeville - do - local VillagePrefix = "Beuzeville" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Biniville - do - local VillagePrefix = "Biniville" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Brucheville - do - local VillagePrefix = "Brucheville" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Cardonville - do - local VillagePrefix = "Cardonville" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Carpiquet - do - local VillagePrefix = "Carpiquet" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Chailey - do - local VillagePrefix = "Chailey" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) - local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Chippelle - do - local VillagePrefix = "Chippelle" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Cretteville - do - local VillagePrefix = "Cretteville" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Cricqueville - do - local VillagePrefix = "Cricqueville" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Deux - do - local VillagePrefix = "Deux" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Evreux - do - local VillagePrefix = "Evreux" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) - local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Ford - do - local VillagePrefix = "Ford" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) - local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Funtington - do - local VillagePrefix = "Funtington" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) - local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Lantheuil - do - local VillagePrefix = "Lantheuil" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Lessay - do - local VillagePrefix = "Lessay" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) - local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Lignerolles - do - local VillagePrefix = "Lignerolles" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Longues - do - local VillagePrefix = "Longues" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Maupertus - do - local VillagePrefix = "Maupertus" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Meautis - do - local VillagePrefix = "Meautis" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Molay - do - local VillagePrefix = "Molay" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Oar - do - local VillagePrefix = "Oar" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) - local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Picauville - do - local VillagePrefix = "Picauville" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Rucqueville - do - local VillagePrefix = "Rucqueville" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- SaintPierre - do - local VillagePrefix = "SaintPierre" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- SainteCroix - do - local VillagePrefix = "SainteCroix" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - --SainteLaurent - do - local VillagePrefix = "SainteLaurent" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Sommervieu - do - local VillagePrefix = "Sommervieu" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Tangmere - do - local VillagePrefix = "Tangmere" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) - local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - --]] return self end @@ -56536,452 +64248,8 @@ end -- @field #ATC_GROUND_PERSIANGULF ATC_GROUND_PERSIANGULF = { ClassName = "ATC_GROUND_PERSIANGULF", - Airbases = { - [AIRBASE.PersianGulf.Abu_Musa_Island_Airport] = { - PointsRunways = { - [1] = { - [1]={["y"]=-122813.71002344,["x"]=-31689.936027827,}, - [2]={["y"]=-122827.82488722,["x"]=-31590.105445836,}, - [3]={["y"]=-122769.5689949,["x"]=-31583.176330891,}, - [4]={["y"]=-122726.96776968,["x"]=-31614.998932862,}, - [5]={["y"]=-121293.92414543,["x"]=-31467.947715689,}, - [6]={["y"]=-121296.4904843,["x"]=-31432.018971528,}, - [7]={["y"]=-121236.18152088,["x"]=-31424.576588809,}, - [8]={["y"]=-121190.50068902,["x"]=-31458.452261875,}, - [9]={["y"]=-119839.83654246,["x"]=-31319.356695194,}, - [10]={["y"]=-119824.69514313,["x"]=-31423.293419374,}, - [11]={["y"]=-119886.80054375,["x"]=-31430.22253432,}, - [12]={["y"]=-119932.22474173,["x"]=-31395.320325706,}, - [13]={["y"]=-122813.9472789,["x"]=-31689.81193251,}, - }, - }, - }, - [AIRBASE.PersianGulf.Al_Dhafra_AB] = { - PointsRunways = { - [1] = { - [1]={["y"]=-174672.06004916,["x"]=-209880.97145616,}, - [2]={["y"]=-174705.15693282,["x"]=-209923.15131918,}, - [3]={["y"]=-171819.05380065,["x"]=-212172.84298281,}, - [4]={["y"]=-171785.09826475,["x"]=-212129.87417284,}, - [5]={["y"]=-174671.96413454,["x"]=-209880.52453983,}, - }, - [2] = { - [1]={["y"]=-174351.95872272,["x"]=-211813.88516693,}, - [2]={["y"]=-174381.29169939,["x"]=-211851.81242636,}, - [3]={["y"]=-171493.65648904,["x"]=-214102.92235002,}, - [4]={["y"]=-171464.99693831,["x"]=-214062.78788361,}, - [5]={["y"]=-174351.8628081,["x"]=-211813.4382506,}, - }, - }, - }, - [AIRBASE.PersianGulf.Al_Maktoum_Intl] = { - PointsRunways = { - [1] = { - [1]={["y"]=-111879.49046471,["x"]=-138953.80105841,}, - [2]={["y"]=-111917.23447224,["x"]=-139018.2804046,}, - [3]={["y"]=-108092.98121312,["x"]=-141406.67838426,}, - [4]={["y"]=-108052.34416748,["x"]=-141341.82058294,}, - [5]={["y"]=-111879.5412879,["x"]=-138952.87693763,}, - }, - }, - }, - [AIRBASE.PersianGulf.Al_Minhad_AB] = { - PointsRunways = { - [1] = { - [1]={["y"]=-91070.628933035,["x"]=-125989.64095162,}, - [2]={["y"]=-91072.346560159,["x"]=-126040.59722299,}, - [3]={["y"]=-87098.282779771,["x"]=-126039.41747017,}, - [4]={["y"]=-87099.632735396,["x"]=-125991.26905291,}, - [5]={["y"]=-91071.031270042,["x"]=-125987.44617225,}, - }, - }, - }, - [AIRBASE.PersianGulf.Bandar_Abbas_Intl] = { - PointsRunways = { - [1] = { - [1]={["y"]=12988.484058788,["x"]=113979.99250505,}, - [2]={["y"]=13037.8836239,["x"]=113952.60241152,}, - [3]={["y"]=14877.313199902,["x"]=117414.37833333,}, - [4]={["y"]=14828.777486364,["x"]=117439.06043783,}, - [5]={["y"]=12988.939584604,["x"]=113979.52494386,}, - }, - [2] = { - [1]={["y"]=13203.406014284,["x"]=113848.44907555,}, - [2]={["y"]=13258.268500181,["x"]=113818.47303925,}, - [3]={["y"]=15315.015323566,["x"]=117694.27156647,}, - [4]={["y"]=15264.815746383,["x"]=117725.22168173,}, - [5]={["y"]=13203.861540099,["x"]=113847.98151436,}, - }, - }, - }, - [AIRBASE.PersianGulf.Bandar_Lengeh] = { - PointsRunways = { - [1] = { - [1]={["y"]=-142373.15541415,["x"]=41364.94047809,}, - [2]={["y"]=-142363.30071107,["x"]=41298.112282592,}, - [3]={["y"]=-142217.57151662,["x"]=41320.35666061,}, - [4]={["y"]=-142213.00856728,["x"]=41291.838227254,}, - [5]={["y"]=-142131.44584788,["x"]=41301.534494595,}, - [6]={["y"]=-142132.58658522,["x"]=41323.778872613,}, - [7]={["y"]=-142123.17550221,["x"]=41336.041798956,}, - [8]={["y"]=-139580.45381288,["x"]=41711.022304533,}, - [9]={["y"]=-139590.04241918,["x"]=41778.350996659,}, - [10]={["y"]=-139732.41237808,["x"]=41757.089304408,}, - [11]={["y"]=-139736.7897853,["x"]=41785.646675372,}, - [12]={["y"]=-139816.41690726,["x"]=41775.641173137,}, - [13]={["y"]=-139816.00001133,["x"]=41754.58792885,}, - [14]={["y"]=-139824.1294819,["x"]=41743.748634761,}, - [15]={["y"]=-142373.20183966,["x"]=41365.161507021,}, - }, - }, - }, - [AIRBASE.PersianGulf.Dubai_Intl] = { - PointsRunways = { - [1] = { - [1]={["y"]=-89693.511670714,["x"]=-100490.47082052,}, - [2]={["y"]=-89731.488328846,["x"]=-100555.50584758,}, - [3]={["y"]=-85706.437275049,["x"]=-103076.68123933,}, - [4]={["y"]=-85669.519216262,["x"]=-103010.44994755,}, - [5]={["y"]=-89693.036962487,["x"]=-100489.9961123,}, - }, - [2] = { - [1]={["y"]=-90797.505501889,["x"]=-99344.082465487,}, - [2]={["y"]=-90835.482160021,["x"]=-99409.11749254,}, - [3]={["y"]=-87210.216900398,["x"]=-101681.72494832,}, - [4]={["y"]=-87171.474397253,["x"]=-101619.20256393,}, - [5]={["y"]=-90797.030793662,["x"]=-99343.607757261,}, - }, - }, - }, - [AIRBASE.PersianGulf.Fujairah_Intl] = { - PointsRunways = { - [1] = { - [1]={["y"]=5808.8716147284,["x"]=-116602.15633995,}, - [2]={["y"]=5781.9885293892,["x"]=-116666.67574476,}, - [3]={["y"]=9435.1910907931,["x"]=-118192.91910235,}, - [4]={["y"]=9459.878635843,["x"]=-118134.40047704,}, - [5]={["y"]=5808.4078522575,["x"]=-116603.31550719,}, - }, - }, - }, - [AIRBASE.PersianGulf.Havadarya] = { - PointsRunways = { - [1] = { - [1]={["y"]=-7565.4887830428,["x"]=109074.13162774,}, - [2]={["y"]=-7557.8281079193,["x"]=109030.65729641,}, - [3]={["y"]=-4987.3556518085,["x"]=109524.49147773,}, - [4]={["y"]=-4996.215358578,["x"]=109566.57508489,}, - [5]={["y"]=-7565.4936338604,["x"]=109074.32262205,}, - }, - }, - }, - [AIRBASE.PersianGulf.Kerman_Airport] = { - PointsRunways = { - [1] = { - [1]={["y"]=70375.468628778,["x"]=456046.12685302,}, - [2]={["y"]=70297.050081575,["x"]=456015.1578105,}, - [3]={["y"]=71814.291673715,["x"]=452165.51037702,}, - [4]={["y"]=71902.918622452,["x"]=452188.46411914,}, - [5]={["y"]=70860.465673482,["x"]=454829.89695989,}, - [6]={["y"]=70862.525255971,["x"]=454892.77675983,}, - [7]={["y"]=70816.157465062,["x"]=454922.77944807,}, - [8]={["y"]=70462.749176371,["x"]=455833.38051827,}, - [9]={["y"]=70483.400377364,["x"]=455901.17880077,}, - [10]={["y"]=70453.787334431,["x"]=455974.8217628,}, - [11]={["y"]=70405.860962315,["x"]=455961.57382254,}, - [12]={["y"]=70374.689338175,["x"]=456046.51649833,}, - }, - }, - }, - [AIRBASE.PersianGulf.Khasab] = { - PointsRunways = { - [1] = { - [1]={["y"]=-534.81827307392,["x"]=-1495.070060483,}, - [2]={["y"]=-434.82912685139,["x"]=-1519.8421462589,}, - [3]={["y"]=-405.55302547993,["x"]=-1413.0969766429,}, - [4]={["y"]=-424.92029254105,["x"]=-1352.0675653224,}, - [5]={["y"]=216.05735069389,["x"]=1206.9187095195,}, - [6]={["y"]=116.42961315781,["x"]=1229.9576238247,}, - [7]={["y"]=88.253643635887,["x"]=1123.7918160128,}, - [8]={["y"]=101.1741158476,["x"]=1042.6886109249,}, - [9]={["y"]=-535.31436058928,["x"]=-1494.8762081291,}, - }, - }, - }, - [AIRBASE.PersianGulf.Lar_Airbase] = { - PointsRunways = { - [1] = { - [1]={["y"]=-183987.5454359,["x"]=169021.72039309,}, - [2]={["y"]=-183988.41292374,["x"]=168955.27082471,}, - [3]={["y"]=-180847.92031188,["x"]=168930.46175795,}, - [4]={["y"]=-180806.58653731,["x"]=168888.39641215,}, - [5]={["y"]=-180740.37934087,["x"]=168886.56748407,}, - [6]={["y"]=-180735.62412787,["x"]=168932.65647164,}, - [7]={["y"]=-180685.14571291,["x"]=168934.11961411,}, - [8]={["y"]=-180682.5852136,["x"]=169001.78995301,}, - [9]={["y"]=-183987.48111493,["x"]=169021.35002828,}, - }, - }, - }, - [AIRBASE.PersianGulf.Qeshm_Island] = { - PointsRunways = { - [1] = { - [1]={["y"]=-35140.372717152,["x"]=63373.658918509,}, - [2]={["y"]=-35098.556715749,["x"]=63320.377239302,}, - [3]={["y"]=-34991.318905699,["x"]=63408.730403557,}, - [4]={["y"]=-34984.574389344,["x"]=63401.311435566,}, - [5]={["y"]=-34991.993357335,["x"]=63313.632722947,}, - [6]={["y"]=-34956.921872287,["x"]=63265.746656824,}, - [7]={["y"]=-34917.129225791,["x"]=63261.699947011,}, - [8]={["y"]=-34832.822771349,["x"]=63337.23853019,}, - [9]={["y"]=-34915.105870884,["x"]=63436.382920614,}, - [10]={["y"]=-34906.337999622,["x"]=63478.198922017,}, - [11]={["y"]=-32728.533668488,["x"]=65307.986209216,}, - [12]={["y"]=-32676.600892552,["x"]=65299.218337954,}, - [13]={["y"]=-32623.99366498,["x"]=65334.964274638,}, - [14]={["y"]=-32626.691471522,["x"]=65388.92040548,}, - [15]={["y"]=-31822.745121968,["x"]=66067.418750826,}, - [16]={["y"]=-31777.556862387,["x"]=66068.767654097,}, - [17]={["y"]=-31691.227053039,["x"]=65974.344425122,}, - [18]={["y"]=-31606.246146962,["x"]=66042.464040311,}, - [19]={["y"]=-31602.199437148,["x"]=66084.280041714,}, - [20]={["y"]=-31632.549760747,["x"]=66124.747139846,}, - [21]={["y"]=-31727.647441358,["x"]=66134.189462744,}, - [22]={["y"]=-31734.391957713,["x"]=66141.608430735,}, - [23]={["y"]=-31632.549760747,["x"]=66225.914885176,}, - [24]={["y"]=-31673.691310515,["x"]=66277.173209477,}, - [25]={["y"]=-35140.880825624,["x"]=63373.905965825,}, - }, - }, - }, - [AIRBASE.PersianGulf.Sharjah_Intl] = { - PointsRunways = { - [1] = { - [1]={["y"]=-71668.808658476,["x"]=-93980.156242153,}, - [2]={["y"]=-75307.847363315,["x"]=-91617.097584505,}, - [3]={["y"]=-75280.458023829,["x"]=-91574.709321014,}, - [4]={["y"]=-72249.697184234,["x"]=-93529.134331507,}, - [5]={["y"]=-72179.919581256,["x"]=-93526.199759419,}, - [6]={["y"]=-72138.183444896,["x"]=-93597.933743788,}, - [7]={["y"]=-71638.654062835,["x"]=-93927.584008321,}, - [8]={["y"]=-71668.325847279,["x"]=-93979.428115206,}, - }, - [2] = { - [1]={["y"]=-71553.225408723,["x"]=-93775.312323319,}, - [2]={["y"]=-75168.13829548,["x"]=-91426.51571111,}, - [3]={["y"]=-75125.388157445,["x"]=-91363.754870166,}, - [4]={["y"]=-71510.511081666,["x"]=-93703.252275385,}, - [5]={["y"]=-71552.247218027,["x"]=-93775.638386885,}, - }, - }, - }, - [AIRBASE.PersianGulf.Shiraz_International_Airport] = { - PointsRunways = { - [1] = { - [1]={["y"]=-353995.75579778,["x"]=382327.42294273,}, - [2]={["y"]=-354029.77009807,["x"]=382265.46199492,}, - [3]={["y"]=-349407.98049238,["x"]=379941.14030526,}, - [4]={["y"]=-349376.87025024,["x"]=380004.69408564,}, - [5]={["y"]=-353995.71101815,["x"]=382327.59771695,}, - }, - [2] = { - [1]={["y"]=-354056.29510012,["x"]=381845.97598829,}, - [2]={["y"]=-354091.48797289,["x"]=381783.6025623,}, - [3]={["y"]=-349650.64038107,["x"]=379550.92898242,}, - [4]={["y"]=-349624.41889127,["x"]=379614.92719482,}, - [5]={["y"]=-354056.25032049,["x"]=381846.15076251,}, - }, - }, - }, - [AIRBASE.PersianGulf.Sir_Abu_Nuayr] = { - PointsRunways = { - [1] = { - [1]={["y"]=-203367.3128691,["x"]=-103017.22553918,}, - [2]={["y"]=-203373.59664477,["x"]=-103054.92819323,}, - [3]={["y"]=-202578.27577922,["x"]=-103188.26018333,}, - [4]={["y"]=-202571.37254488,["x"]=-103151.01482599,}, - [5]={["y"]=-203367.65259839,["x"]=-103016.48202662,}, - [6]={["y"]=-203291.39594004,["x"]=-102985.49774228,}, - }, - }, - }, - [AIRBASE.PersianGulf.Sirri_Island] = { - PointsRunways = { - [1] = { - [1]={["y"]=-169713.12842428,["x"]=-27766.658020853,}, - [2]={["y"]=-169682.02009414,["x"]=-27726.583172021,}, - [3]={["y"]=-169727.21866794,["x"]=-27691.632048154,}, - [4]={["y"]=-169694.28043602,["x"]=-27650.276268081,}, - [5]={["y"]=-169763.08474269,["x"]=-27598.490047901,}, - [6]={["y"]=-169825.30140298,["x"]=-27607.090586235,}, - [7]={["y"]=-171614.98889813,["x"]=-26246.247907014,}, - [8]={["y"]=-171620.85326172,["x"]=-26187.105176343,}, - [9]={["y"]=-171686.10990337,["x"]=-26138.56820961,}, - [10]={["y"]=-171716.55468456,["x"]=-26178.745338885,}, - [11]={["y"]=-171764.9668776,["x"]=-26142.810515186,}, - [12]={["y"]=-171796.29599657,["x"]=-26183.416460911,}, - [13]={["y"]=-169713.5628285,["x"]=-27766.883787223,}, - }, - }, - }, - [AIRBASE.PersianGulf.Tunb_Island_AFB] = { - PointsRunways = { - [1] = { - [1]={["y"]=-92923.634698863,["x"]=9547.6862547173,}, - [2]={["y"]=-92963.030803298,["x"]=9565.7274614215,}, - [3]={["y"]=-92934.128053782,["x"]=9619.2987996964,}, - [4]={["y"]=-92970.946842975,["x"]=9640.1014155901,}, - [5]={["y"]=-92949.591945243,["x"]=9682.8112110532,}, - [6]={["y"]=-92899.518391942,["x"]=9699.7478540817,}, - [7]={["y"]=-91969.13471408,["x"]=11464.627292768,}, - [8]={["y"]=-91983.666755417,["x"]=11515.293058512,}, - [9]={["y"]=-91960.101282978,["x"]=11557.710908902,}, - [10]={["y"]=-91921.021874517,["x"]=11539.251288825,}, - [11]={["y"]=-91893.725202275,["x"]=11589.720675632,}, - [12]={["y"]=-91859.751646175,["x"]=11571.850192366,}, - [13]={["y"]=-92922.149728329,["x"]=9547.2937058617,}, - }, - }, - }, - [AIRBASE.PersianGulf.Tunb_Kochak] = { - PointsRunways = { - [1] = { - [1]={["y"]=-109925.50271188,["x"]=8974.5666013181,}, - [2]={["y"]=-109905.7382908,["x"]=8937.53274444,}, - [3]={["y"]=-109009.93726324,["x"]=9072.2234968343,}, - [4]={["y"]=-109040.82867587,["x"]=9104.9871291834,}, - [5]={["y"]=-109925.26515172,["x"]=8974.091480998,}, - }, - }, - }, - [AIRBASE.PersianGulf.Sas_Al_Nakheel_Airport] = { - PointsRunways = { - [1] = { - [1]={["y"]=-176230.75865538,["x"]=-188732.01369812,}, - [2]={["y"]=-176274.78045186,["x"]=-188744.8049371,}, - [3]={["y"]=-175692.03171595,["x"]=-190564.17145168,}, - [4]={["y"]=-175649.7486572,["x"]=-190550.58435053,}, - [5]={["y"]=-176230.66274076,["x"]=-188731.5667818,}, - }, - }, - }, - [AIRBASE.PersianGulf.Bandar_e_Jask_airfield] = { - PointsRunways = { - [1] = { - [1]={["y"]=155156.73167657,["x"]=-57837.031277333,}, - [2]={["y"]=155130.38996239,["x"]=-57790.475605714,}, - [3]={["y"]=157137.17872571,["x"]=-56710.411783359,}, - [4]={["y"]=157148.46631801,["x"]=-56688.071756941,}, - [5]={["y"]=157220.07198163,["x"]=-56649.035500253,}, - [6]={["y"]=157227.83220133,["x"]=-56662.204357931,}, - [7]={["y"]=157359.6383572,["x"]=-56590.481115222,}, - [8]={["y"]=157383.03659539,["x"]=-56633.044744502,}, - [9]={["y"]=155156.7940421,["x"]=-57837.149989814,}, - }, - }, - }, - [AIRBASE.PersianGulf.Abu_Dhabi_International_Airport] = { - PointsRunways = { - [1] = { - [1]={["y"]=-163964.56943899,["x"]=-189427.63621921,}, - [2]={["y"]=-164005.96838287,["x"]=-189478.90226888,}, - [3]={["y"]=-160798.22080495,["x"]=-192054.59531727,}, - [4]={["y"]=-160755.05282258,["x"]=-192002.58569997,}, - [5]={["y"]=-163964.47352437,["x"]=-189427.18930288,}, - }, - [2] = { - [1]={["y"]=-163615.44952024,["x"]=-187144.00786922,}, - [2]={["y"]=-163656.84846411,["x"]=-187195.27391888,}, - [3]={["y"]=-160452.71811093,["x"]=-189764.86593382,}, - [4]={["y"]=-160411.94568221,["x"]=-189715.47961171,}, - [5]={["y"]=-163615.35360562,["x"]=-187143.56095289,}, - }, - }, - }, - [AIRBASE.PersianGulf.Al_Bateen_Airport] = { - PointsRunways = { - [1] = { - [1]={["y"]=-183207.51774197,["x"]=-189871.8319832,}, - [2]={["y"]=-183240.61462564,["x"]=-189914.01184622,}, - [3]={["y"]=-180748.88998479,["x"]=-191943.30402837,}, - [4]={["y"]=-180711.83076051,["x"]=-191896.52435182,}, - [5]={["y"]=-183207.42182735,["x"]=-189871.38506688,}, - }, - }, - }, - [AIRBASE.PersianGulf.Kish_International_Airport] = { - PointsRunways = { - [1] = { - [1]={["y"]=-227330.79164594,["x"]=42691.91536494,}, - [2]={["y"]=-227321.58531968,["x"]=42758.113234714,}, - [3]={["y"]=-223235.73004619,["x"]=42313.579195302,}, - [4]={["y"]=-223240.99080406,["x"]=42247.819722016,}, - [5]={["y"]=-227330.67774245,["x"]=42691.785682556,}, - }, - [2] = { - [1]={["y"]=-227283.77911886,["x"]=42987.748941936,}, - [2]={["y"]=-227274.5727926,["x"]=43053.946811711,}, - [3]={["y"]=-222907.94761294,["x"]=42580.826755904,}, - [4]={["y"]=-222915.76510871,["x"]=42514.58376547,}, - [5]={["y"]=-227283.66521537,["x"]=42987.619259553,}, - }, - }, - }, - [AIRBASE.PersianGulf.Al_Ain_International_Airport] = { - PointsRunways = { - [1] = { - [1]={["y"]=-65165.315648901,["x"]=-209042.45716363,}, - [2]={["y"]=-65112.933878375,["x"]=-209048.84518442,}, - [3]={["y"]=-65672.013626755,["x"]=-213019.66479976,}, - [4]={["y"]=-65722.555424932,["x"]=-213013.91596964,}, - [5]={["y"]=-65165.400582791,["x"]=-209042.15059908,}, - }, - }, - }, - [AIRBASE.PersianGulf.Lavan_Island_Airport] = { - PointsRunways = { - [1] = { - [1]={["y"]=-288099.83301495,["x"]=76353.443273049,}, - [2]={["y"]=-288119.51457685,["x"]=76302.756224611,}, - [3]={["y"]=-288070.96603401,["x"]=76283.898526152,}, - [4]={["y"]=-288085.61084238,["x"]=76247.386812114,}, - [5]={["y"]=-288032.04695421,["x"]=76224.316223573,}, - [6]={["y"]=-287991.12173627,["x"]=76245.38067398,}, - [7]={["y"]=-287489.96435675,["x"]=76037.610404141,}, - [8]={["y"]=-287497.65444594,["x"]=76017.686082159,}, - [9]={["y"]=-287453.61120787,["x"]=75998.111309685,}, - [10]={["y"]=-287419.70490555,["x"]=76007.199596905,}, - [11]={["y"]=-285642.24565503,["x"]=75279.787069797,}, - [12]={["y"]=-285625.46727862,["x"]=75239.239326815,}, - [13]={["y"]=-285570.23845628,["x"]=75217.217707782,}, - [14]={["y"]=-285555.20782742,["x"]=75252.172658628,}, - [15]={["y"]=-285505.92134673,["x"]=75231.199688121,}, - [16]={["y"]=-285484.28380792,["x"]=75284.258832895,}, - [17]={["y"]=-288099.97979219,["x"]=76354.32393647,}, - }, - }, - }, - [AIRBASE.PersianGulf.Jiroft_Airport] = { - PointsRunways = { - [1] = { - [1]={["y"]=140376.87310595,["x"]=283748.07558774,}, - [2]={["y"]=140299.43760975,["x"]=283655.81201779,}, - [3]={["y"]=143008.43807723,["x"]=281517.41347718,}, - [4]={["y"]=143052.6952428,["x"]=281573.25195709,}, - [5]={["y"]=142946.60213095,["x"]=281656.5960586,}, - [6]={["y"]=142975.14179847,["x"]=281687.20381796,}, - [7]={["y"]=142932.12548801,["x"]=281724.01585287,}, - [8]={["y"]=142870.49635092,["x"]=281719.05243244,}, - [9]={["y"]=140437.35783025,["x"]=283640.84253664,}, - [10]={["y"]=140433.27045062,["x"]=283705.80267729,}, - [11]={["y"]=140376.77702493,["x"]=283747.8442964,}, - }, - }, - }, - }, } - --- Creates a new ATC_GROUND_PERSIANGULF object. -- @param #ATC_GROUND_PERSIANGULF self -- @param AirbaseNames A list {} of airbase names (Use AIRBASE.PersianGulf enumerator). @@ -56989,266 +64257,11 @@ ATC_GROUND_PERSIANGULF = { function ATC_GROUND_PERSIANGULF:New( AirbaseNames ) -- Inherits from BASE - local self = BASE:Inherit( self, ATC_GROUND:New( self.Airbases, AirbaseNames ) ) -- #ATC_GROUND_PERSIANGULF + local self = BASE:Inherit( self, ATC_GROUND_UNIVERSAL:New( AirbaseNames ) ) -- #ATC_GROUND_PERSIANGULF self:SetKickSpeedKmph( 50 ) self:SetMaximumKickSpeedKmph( 150 ) - - -- These lines here are for the demonstration mission. - -- They create in the dcs.log the coordinates of the runway polygons, that are then - -- taken by the moose designer from the dcs.log and reworked to define the - -- Airbases structure, which is part of the class. - -- When new airbases are added or airbases are changed on the map, - -- the MOOSE designer willde-comment this section and apply the changes in the demo - -- mission, and do a re-run to create a new dcs.log, and then add the changed coordinates - -- in the Airbases structure. - -- So, this needs to stay commented normally once a map has been finished. - - - --[[ - - -- Abu_Musa_Island_Airport - do - local VillagePrefix = "Abu_Musa_Island_Airport" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Al_Dhafra_AB - do - local VillagePrefix = "Al_Dhafra_AB" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) - local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Al_Maktoum_Intl - do - local VillagePrefix = "Al_Maktoum_Intl" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Al_Minhad_AB - do - local VillagePrefix = "Al_Minhad_AB" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Bandar_Abbas_Intl - do - local VillagePrefix = "Bandar_Abbas_Intl" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) - local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Bandar_Lengeh - do - local VillagePrefix = "Bandar_Lengeh" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Dubai_Intl - do - local VillagePrefix = "Dubai_Intl" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) - local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Fujairah_Intl - do - local VillagePrefix = "Fujairah_Intl" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Havadarya - do - local VillagePrefix = "Havadarya" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Kerman_Airport - do - local VillagePrefix = "Kerman_Airport" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Khasab - do - local VillagePrefix = "Khasab" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Lar_Airbase - do - local VillagePrefix = "Lar_Airbase" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Qeshm_Island - do - local VillagePrefix = "Qeshm_Island" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Sharjah_Intl - do - local VillagePrefix = "Sharjah_Intl" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) - local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Shiraz_International_Airport - do - local VillagePrefix = "Shiraz_International_Airport" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) - local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Sir_Abu_Nuayr - do - local VillagePrefix = "Sir_Abu_Nuayr" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Sirri_Island - do - local VillagePrefix = "Sirri_Island" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Tunb_Island_AFB - do - local VillagePrefix = "Tunb_Island_AFB" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Tunb_Kochak - do - local VillagePrefix = "Tunb_Kochak" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Sas_Al_Nakheel_Airport - do - local VillagePrefix = "Sas_Al_Nakheel_Airport" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Bandar_e_Jask_airfield - do - local VillagePrefix = "Bandar_e_Jask_airfield" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Abu_Dhabi_International_Airport - do - local VillagePrefix = "Abu_Dhabi_International_Airport" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) - local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Al_Bateen_Airport - do - local VillagePrefix = "Al_Bateen_Airport" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Kish_International_Airport - do - local VillagePrefix = "Kish_International_Airport" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) - local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Al_Ain_International_Airport - do - local VillagePrefix = "Al_Ain_International_Airport" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Lavan_Island_Airport - do - local VillagePrefix = "Lavan_Island_Airport" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Jiroft_Airport - do - local VillagePrefix = "Jiroft_Airport" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - - -- Bandar_Abbas_Intl - do - local VillagePrefix = "Bandar_Abbas_Intl" - local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) - local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) - local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - end - - --]] - return self end --- Start SCHEDULER for ATC_GROUND_PERSIANGULF object. @@ -57261,271 +64274,388 @@ function ATC_GROUND_PERSIANGULF:Start( RepeatScanSeconds ) end + --- @type ATC_GROUND_MARIANAISLANDS +-- @extends #ATC_GROUND ---- **Functional** -- Models the detection of enemy units by FACs or RECCEs and group them according various methods. + +--- # ATC\_GROUND\_MARIANA, extends @{#ATC_GROUND} -- --- === +-- The ATC\_GROUND\_MARIANA class monitors the speed of the airplanes at the airbase during taxi. +-- The pilots may not drive faster than the maximum speed for the airbase, or they will be despawned. -- --- ## Features: +-- --- +-- +-- ![Banner Image](..\Presentations\ATC_GROUND\Dia1.JPG) +-- +-- --- +-- +-- The default maximum speed for the airbases at Persian Gulf is **50 km/h**. Warnings are given if this speed limit is trespassed. +-- Players will be immediately kicked when driving faster than **150 km/h** on the taxi way. +-- +-- The ATC\_GROUND\_MARIANA class monitors the speed of the airplanes at the airbase during taxi. +-- The pilots may not drive faster than the maximum speed for the airbase, or they will be despawned. +-- +-- The pilot will receive 3 times a warning during speeding. After the 3rd warning, if the pilot is still driving +-- faster than the maximum allowed speed, the pilot will be kicked. +-- +-- Different airbases have different maximum speeds, according safety regulations. +-- +-- # Airbases monitored +-- +-- The following airbases are monitored at the Mariana Island region. +-- Use the @{Wrapper.Airbase#AIRBASE.MarianaIslands} enumeration to select the airbases to be monitored. -- +-- * AIRBASE.MarianaIslands.Rota_Intl +-- * AIRBASE.MarianaIslands.Andersen_AFB +-- * AIRBASE.MarianaIslands.Antonio_B_Won_Pat_Intl +-- * AIRBASE.MarianaIslands.Saipan_Intl +-- * AIRBASE.MarianaIslands.Tinian_Intl +-- * AIRBASE.MarianaIslands.Olf_Orote +-- +-- # Installation +-- +-- ## In Single Player Missions +-- +-- ATC\_GROUND is fully functional in single player. +-- +-- ## In Multi Player Missions +-- +-- ATC\_GROUND is functional in multi player, however ... +-- +-- Due to a bug in DCS since release 1.5, the despawning of clients are not anymore working in multi player. +-- To **work around this problem**, a much better solution has been made, using the **slot blocker** script designed +-- by Ciribob. +-- +-- With the help of __Ciribob__, this script has been extended to also kick client players while in flight. +-- ATC\_GROUND is communicating with this modified script to kick players! +-- +-- Install the file **SimpleSlotBlockGameGUI.lua** on the server, following the installation instructions described by Ciribob. +-- +-- [Simple Slot Blocker from Ciribob & FlightControl](https://github.com/ciribob/DCS-SimpleSlotBlock) +-- +-- # Script it! +-- +-- ## 1. ATC_GROUND_MARIANAISLANDS Constructor +-- +-- Creates a new ATC_GROUND_MARIANAISLANDS object that will monitor pilots taxiing behaviour. +-- +-- -- This creates a new ATC_GROUND_MARIANAISLANDS object. +-- +-- -- Monitor for these clients the airbases. +-- AirbasePoliceCaucasus = ATC_GROUND_MARIANAISLANDS:New() +-- +-- ATC_Ground = ATC_GROUND_MARIANAISLANDS:New( +-- { AIRBASE.MarianaIslands.Andersen_AFB, +-- AIRBASE.MarianaIslands.Saipan_Intl +-- } +-- ) +-- +-- +-- ## 2. Set various options +-- +-- There are various methods that you can use to tweak the behaviour of the ATC\_GROUND classes. +-- +-- ### 2.1 Speed limit at an airbase. +-- +-- * @{#ATC_GROUND.SetKickSpeed}(): Set the speed limit allowed at an airbase in meters per second. +-- * @{#ATC_GROUND.SetKickSpeedKmph}(): Set the speed limit allowed at an airbase in kilometers per hour. +-- * @{#ATC_GROUND.SetKickSpeedMiph}(): Set the speed limit allowed at an airbase in miles per hour. +-- +-- ### 2.2 Prevent Takeoff at an airbase. Players will be kicked immediately. +-- +-- * @{#ATC_GROUND.SetMaximumKickSpeed}(): Set the maximum speed allowed at an airbase in meters per second. +-- * @{#ATC_GROUND.SetMaximumKickSpeedKmph}(): Set the maximum speed allowed at an airbase in kilometers per hour. +-- * @{#ATC_GROUND.SetMaximumKickSpeedMiph}(): Set the maximum speed allowed at an airbase in miles per hour. +-- +---- @field #ATC_GROUND_MARIANAISLANDS +ATC_GROUND_MARIANAISLANDS = { + ClassName = "ATC_GROUND_MARIANAISLANDS", +} + +--- Creates a new ATC_GROUND_MARIANAISLANDS object. +-- @param #ATC_GROUND_MARIANAISLANDS self +-- @param AirbaseNames A list {} of airbase names (Use AIRBASE.MarianaIslands enumerator). +-- @return #ATC_GROUND_MARIANAISLANDS self +function ATC_GROUND_MARIANAISLANDS:New( AirbaseNames ) + + -- Inherits from BASE + local self = BASE:Inherit( self, ATC_GROUND_UNIVERSAL:New( self.Airbases, AirbaseNames ) ) + + self:SetKickSpeedKmph( 50 ) + self:SetMaximumKickSpeedKmph( 150 ) + + return self +end + +--- Start SCHEDULER for ATC_GROUND_MARIANAISLANDS object. +-- @param #ATC_GROUND_MARIANAISLANDS self +-- @param RepeatScanSeconds Time in second for defining occurency of alerts. +-- @return nothing +function ATC_GROUND_MARIANAISLANDS:Start( RepeatScanSeconds ) + RepeatScanSeconds = RepeatScanSeconds or 0.05 + self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, RepeatScanSeconds ) +end +--- **Functional** - Models the detection of enemy units by FACs or RECCEs and group them according various methods. +-- +-- === +-- +-- ## Features: +-- -- * Detection of targets by recce units. -- * Group detected targets per unit, type or area (zone). -- * Keep persistency of detected targets, if when detection is lost. -- * Provide an indication of detected targets. -- * Report detected targets. -- * Refresh detection upon specified time intervals. --- +-- -- === --- +-- -- ## Missions: --- +-- -- [DET - Detection](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/DET%20-%20Detection) --- +-- -- === --- --- Facilitate the detection of enemy units within the battle zone executed by FACs (Forward Air Controllers) or RECCEs (Reconnassance Units). +-- +-- Facilitate the detection of enemy units within the battle zone executed by FACs (Forward Air Controllers) or RECCEs (Reconnaissance Units). -- It uses the in-built detection capabilities of DCS World, but adds new functionalities. --- +-- -- === --- --- ### Contributions: --- +-- +-- ### Contributions: +-- -- * Mechanist : Early concept of DETECTION_AREAS. --- --- ### Authors: --- +-- +-- ### Authors: +-- -- * FlightControl : Analysis, Design, Programming, Testing --- +-- -- === --- +-- -- @module Functional.Detection -- @image Detection.JPG - do -- DETECTION_BASE --- @type DETECTION_BASE - -- @field Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role. + -- @field Core.Set#SET_GROUP DetectionSetGroup The @{Core.Set} of GROUPs in the Forward Air Controller role. -- @field DCS#Distance DetectionRange The range till which targets are accepted to be detected. -- @field #DETECTION_BASE.DetectedObjects DetectedObjects The list of detected objects. -- @field #table DetectedObjectsIdentified Map of the DetectedObjects identified. -- @field #number DetectionRun -- @extends Core.Fsm#FSM - + --- Defines the core functions to administer detected objects. -- The DETECTION_BASE class will detect objects within the battle zone for a list of @{Wrapper.Group}s detecting targets following (a) detection method(s). - -- + -- -- ## DETECTION_BASE constructor - -- + -- -- Construct a new DETECTION_BASE instance using the @{#DETECTION_BASE.New}() method. - -- + -- -- ## Initialization - -- + -- -- By default, detection will return detected objects with all the detection sensors available. - -- However, you can ask how the objects were found with specific detection methods. + -- However, you can ask how the objects were found with specific detection methods. -- If you use one of the below methods, the detection will work with the detection method specified. -- You can specify to apply multiple detection methods. - -- + -- -- Use the following functions to report the objects it detected using the methods Visual, Optical, Radar, IRST, RWR, DLINK: - -- + -- -- * @{#DETECTION_BASE.InitDetectVisual}(): Detected using Visual. -- * @{#DETECTION_BASE.InitDetectOptical}(): Detected using Optical. -- * @{#DETECTION_BASE.InitDetectRadar}(): Detected using Radar. -- * @{#DETECTION_BASE.InitDetectIRST}(): Detected using IRST. -- * @{#DETECTION_BASE.InitDetectRWR}(): Detected using RWR. -- * @{#DETECTION_BASE.InitDetectDLINK}(): Detected using DLINK. - -- + -- -- ## **Filter** detected units based on **category of the unit** - -- - -- Filter the detected units based on Unit.Category using the method @{#DETECTION_BASE.FilterCategories}(). + -- + -- Filter the detected units based on Unit.Category using the method @{#DETECTION_BASE.FilterCategories}(). -- The different values of Unit.Category can be: - -- + -- -- * Unit.Category.AIRPLANE -- * Unit.Category.GROUND_UNIT -- * Unit.Category.HELICOPTER -- * Unit.Category.SHIP -- * Unit.Category.STRUCTURE - -- + -- -- Multiple Unit.Category entries can be given as a table and then these will be evaluated as an OR expression. - -- + -- -- Example to filter a single category (Unit.Category.AIRPLANE). - -- - -- DetectionObject:FilterCategories( Unit.Category.AIRPLANE ) - -- + -- + -- DetectionObject:FilterCategories( Unit.Category.AIRPLANE ) + -- -- Example to filter multiple categories (Unit.Category.AIRPLANE, Unit.Category.HELICOPTER). Note the {}. - -- + -- -- DetectionObject:FilterCategories( { Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } ) - -- - -- + -- -- ## **DETECTION_ derived classes** group the detected units into a **DetectedItems[]** list - -- - -- DETECTION_BASE derived classes build a list called DetectedItems[], which is essentially a first later + -- + -- DETECTION_BASE derived classes build a list called DetectedItems[], which is essentially a first later -- of grouping of detected units. Each DetectedItem within the DetectedItems[] list contains -- a SET_UNIT object that contains the detected units that belong to that group. - -- - -- Derived classes will apply different methods to group the detected units. + -- + -- Derived classes will apply different methods to group the detected units. -- Examples are per area, per quadrant, per distance, per type. - -- See further the derived DETECTION classes on which grouping methods are currently supported. - -- + -- See further the derived DETECTION classes on which grouping methods are currently supported. + -- -- Various methods exist how to retrieve the grouped items from a DETECTION_BASE derived class: - -- + -- -- * The method @{Functional.Detection#DETECTION_BASE.GetDetectedItems}() retrieves the DetectedItems[] list. -- * A DetectedItem from the DetectedItems[] list can be retrieved using the method @{Functional.Detection#DETECTION_BASE.GetDetectedItem}( DetectedItemIndex ). -- Note that this method returns a DetectedItem element from the list, that contains a Set variable and further information -- about the DetectedItem that is set by the DETECTION_BASE derived classes, used to group the DetectedItem. -- * A DetectedSet from the DetectedItems[] list can be retrieved using the method @{Functional.Detection#DETECTION_BASE.GetDetectedSet}( DetectedItemIndex ). -- This method retrieves the Set from a DetectedItem element from the DetectedItem list (DetectedItems[ DetectedItemIndex ].Set ). - -- + -- -- ## **Visual filters** to fine-tune the probability of the detected objects - -- + -- -- By default, DCS World will return any object that is in LOS and within "visual reach", or detectable through one of the electronic detection means. -- That being said, the DCS World detection algorithm can sometimes be unrealistic. -- Especially for a visual detection, DCS World is able to report within 1 second a detailed detection of a group of 20 units (including types of the units) that are 10 kilometers away, using only visual capabilities. -- Additionally, trees and other obstacles are not accounted during the DCS World detection. - -- + -- -- Therefore, an additional (optional) filtering has been built into the DETECTION_BASE class, that can be set for visual detected units. -- For electronic detection, this filtering is not applied, only for visually detected targets. - -- + -- -- The following additional filtering can be applied for visual filtering: - -- + -- -- * A probability factor per kilometer distance. -- * A probability factor based on the alpha angle between the detected object and the unit detecting. -- A detection from a higher altitude allows for better detection than when on the ground. -- * Define a probability factor for "cloudy zones", which are zones where forests or villages are located. In these zones, detection will be much more difficult. - -- The mission designer needs to define these cloudy zones within the mission, and needs to register these zones in the DETECTION_ objects additing a probability factor per zone. - -- + -- The mission designer needs to define these cloudy zones within the mission, and needs to register these zones in the DETECTION_ objects adding a probability factor per zone. + -- -- I advise however, that, when you first use the DETECTION derived classes, that you don't use these filters. - -- Only when you experience unrealistic behaviour in your missions, these filters could be applied. - -- - -- + -- Only when you experience unrealistic behavior in your missions, these filters could be applied. + -- -- ### Distance visual detection probability - -- + -- -- Upon a **visual** detection, the further away a detected object is, the less likely it is to be detected properly. -- Also, the speed of accurate detection plays a role. - -- + -- -- A distance probability factor between 0 and 1 can be given, that will model a linear extrapolated probability over 10 km distance. - -- + -- -- For example, if a probability factor of 0.6 (60%) is given, the extrapolated probabilities over 15 kilometers would like like: -- 1 km: 96%, 2 km: 92%, 3 km: 88%, 4 km: 84%, 5 km: 80%, 6 km: 76%, 7 km: 72%, 8 km: 68%, 9 km: 64%, 10 km: 60%, 11 km: 56%, 12 km: 52%, 13 km: 48%, 14 km: 44%, 15 km: 40%. - -- + -- -- Note that based on this probability factor, not only the detection but also the **type** of the unit will be applied! - -- + -- -- Use the method @{Functional.Detection#DETECTION_BASE.SetDistanceProbability}() to set the probability factor upon a 10 km distance. - -- + -- -- ### Alpha Angle visual detection probability - -- + -- -- Upon a **visual** detection, the higher the unit is during the detecting process, the more likely the detected unit is to be detected properly. -- A detection at a 90% alpha angle is the most optimal, a detection at 10% is less and a detection at 0% is less likely to be correct. - -- + -- -- A probability factor between 0 and 1 can be given, that will model a progressive extrapolated probability if the target would be detected at a 0° angle. - -- + -- -- For example, if a alpha angle probability factor of 0.7 is given, the extrapolated probabilities of the different angles would look like: -- 0°: 70%, 10°: 75,21%, 20°: 80,26%, 30°: 85%, 40°: 89,28%, 50°: 92,98%, 60°: 95,98%, 70°: 98,19%, 80°: 99,54%, 90°: 100% - -- + -- -- Use the method @{Functional.Detection#DETECTION_BASE.SetAlphaAngleProbability}() to set the probability factor if 0°. - -- + -- -- ### Cloudy Zones detection probability - -- + -- -- Upon a **visual** detection, the more a detected unit is within a cloudy zone, the less likely the detected unit is to be detected successfully. -- The Cloudy Zones work with the ZONE_BASE derived classes. The mission designer can define within the mission -- zones that reflect cloudy areas where detected units may not be so easily visually detected. - -- + -- -- Use the method @{Functional.Detection#DETECTION_BASE.SetZoneProbability}() to set for a defined number of zones, the probability factors. - -- + -- -- Note however, that the more zones are defined to be "cloudy" within a detection, the more performance it will take -- from the DETECTION_BASE to calculate the presence of the detected unit within each zone. - -- Expecially for ZONE_POLYGON, try to limit the amount of nodes of the polygon! - -- - -- Typically, this kind of filter would be applied for very specific areas were a detection needs to be very realisting for + -- Especially for ZONE_POLYGON, try to limit the amount of nodes of the polygon! + -- + -- Typically, this kind of filter would be applied for very specific areas where a detection needs to be very realistic for -- AI not to detect so easily targets within a forrest or village rich area. - -- + -- -- ## Accept / Reject detected units - -- - -- DETECTION_BASE can accept or reject successful detections based on the location of the detected object, + -- + -- DETECTION_BASE can accept or reject successful detections based on the location of the detected object, -- if it is located in range or located inside or outside of specific zones. - -- + -- -- ### Detection acceptance of within range limit - -- + -- -- A range can be set that will limit a successful detection for a unit. -- Use the method @{Functional.Detection#DETECTION_BASE.SetAcceptRange}() to apply a range in meters till where detected units will be accepted. - -- + -- -- local SetGroup = SET_GROUP:New():FilterPrefixes( "FAC" ):FilterStart() -- Build a SetGroup of Forward Air Controllers. - -- + -- -- -- Build a detect object. -- local Detection = DETECTION_UNITS:New( SetGroup ) - -- + -- -- -- This will accept detected units if the range is below 5000 meters. - -- Detection:SetAcceptRange( 5000 ) - -- + -- Detection:SetAcceptRange( 5000 ) + -- -- -- Start the Detection. -- Detection:Start() - -- - -- + -- + -- -- ### Detection acceptance if within zone(s). - -- + -- -- Specific ZONE_BASE object(s) can be given as a parameter, which will only accept a detection if the unit is within the specified ZONE_BASE object(s). -- Use the method @{Functional.Detection#DETECTION_BASE.SetAcceptZones}() will accept detected units if they are within the specified zones. - -- + -- -- local SetGroup = SET_GROUP:New():FilterPrefixes( "FAC" ):FilterStart() -- Build a SetGroup of Forward Air Controllers. - -- + -- -- -- Search fo the zones where units are to be accepted. -- local ZoneAccept1 = ZONE:New( "AcceptZone1" ) -- local ZoneAccept2 = ZONE:New( "AcceptZone2" ) - -- + -- -- -- Build a detect object. -- local Detection = DETECTION_UNITS:New( SetGroup ) - -- + -- -- -- This will accept detected units by Detection when the unit is within ZoneAccept1 OR ZoneAccept2. -- Detection:SetAcceptZones( { ZoneAccept1, ZoneAccept2 } ) - -- + -- -- -- Start the Detection. -- Detection:Start() - -- - -- ### Detection rejectance if within zone(s). - -- + -- + -- ### Detection rejection if within zone(s). + -- -- Specific ZONE_BASE object(s) can be given as a parameter, which will reject detection if the unit is within the specified ZONE_BASE object(s). -- Use the method @{Functional.Detection#DETECTION_BASE.SetRejectZones}() will reject detected units if they are within the specified zones. -- An example of how to use the method is shown below. - -- + -- -- local SetGroup = SET_GROUP:New():FilterPrefixes( "FAC" ):FilterStart() -- Build a SetGroup of Forward Air Controllers. - -- + -- -- -- Search fo the zones where units are to be rejected. -- local ZoneReject1 = ZONE:New( "RejectZone1" ) -- local ZoneReject2 = ZONE:New( "RejectZone2" ) - -- + -- -- -- Build a detect object. -- local Detection = DETECTION_UNITS:New( SetGroup ) - -- + -- -- -- This will reject detected units by Detection when the unit is within ZoneReject1 OR ZoneReject2. - -- Detection:SetRejectZones( { ZoneReject1, ZoneReject2 } ) - -- + -- Detection:SetRejectZones( { ZoneReject1, ZoneReject2 } ) + -- -- -- Start the Detection. -- Detection:Start() - -- + -- -- ## Detection of Friendlies Nearby - -- + -- -- Use the method @{Functional.Detection#DETECTION_BASE.SetFriendliesRange}() to set the range what will indicate when friendlies are nearby -- a DetectedItem. The default range is 6000 meters. For air detections, it is advisory to use about 30.000 meters. - -- + -- -- ## DETECTION_BASE is a Finite State Machine -- -- Various Events and State Transitions can be tailored using DETECTION_BASE. - -- + -- -- ### DETECTION_BASE States - -- + -- -- * **Detecting**: The detection is running. -- * **Stopped**: The detection is stopped. - -- + -- -- ### DETECTION_BASE Events - -- + -- -- * **Start**: Start the detection process. -- * **Detect**: Detect new units. -- * **Detected**: New units have been detected. -- * **Stop**: Stop the detection process. - -- + -- -- @field #DETECTION_BASE DETECTION_BASE - -- + -- DETECTION_BASE = { ClassName = "DETECTION_BASE", DetectionSetGroup = nil, @@ -57534,12 +64664,12 @@ do -- DETECTION_BASE DetectionRun = 0, DetectedObjectsIdentified = {}, DetectedItems = {}, - DetectedItemsByIndex = {}, + DetectedItemsByIndex = {}, } - + --- @type DETECTION_BASE.DetectedObjects -- @list <#DETECTION_BASE.DetectedObject> - + --- @type DETECTION_BASE.DetectedObject -- @field #string Name -- @field #boolean IsVisible @@ -57552,11 +64682,10 @@ do -- DETECTION_BASE -- @field #boolean LastPos -- @field #number LastVelocity - --- @type DETECTION_BASE.DetectedItems -- @list <#DETECTION_BASE.DetectedItem> - - --- Detected item data structrue. + + --- Detected item data structure. -- @type DETECTION_BASE.DetectedItem -- @field #boolean IsDetected Indicates if the DetectedItem has been detected or not. -- @field Core.Set#SET_UNIT Set The Set of Units in the detected area. @@ -57567,7 +64696,7 @@ do -- DETECTION_BASE -- @field #boolean FriendliesNearBy Indicates if there are friendlies within the detected area. -- @field Wrapper.Unit#UNIT NearestFAC The nearest FAC near the Area. -- @field Core.Point#COORDINATE Coordinate The last known coordinate of the DetectedItem. - -- @field Core.Point#COORDINATE InterceptCoord Intercept coordiante. + -- @field Core.Point#COORDINATE InterceptCoord Intercept coordinate. -- @field #number DistanceRecce Distance in meters of the Recce. -- @field #number Index Detected item key. Could also be a string. -- @field #string ItemID ItemPrefix .. "." .. self.DetectedItemMax. @@ -57575,7 +64704,7 @@ do -- DETECTION_BASE -- @field #table PlayersNearBy Table of nearby players. -- @field #table FriendliesDistance Table of distances to friendly units. -- @field #string TypeName Type name of the detected unit. - -- @field #string CategoryName Catetory name of the detected unit. + -- @field #string CategoryName Category name of the detected unit. -- @field #string Name Name of the detected object. -- @field #boolean IsVisible If true, detected object is visible. -- @field #number LastTime Last time the detected item was seen. @@ -57584,31 +64713,31 @@ do -- DETECTION_BASE -- @field #boolean KnowType Type of detected item is known. -- @field #boolean KnowDistance Distance to the detected item is known. -- @field #number Distance Distance to the detected item. - + --- DETECTION constructor. -- @param #DETECTION_BASE self - -- @param Core.Set#SET_GROUP DetectionSet The @{Set} of @{Group}s that is used to detect the units. + -- @param Core.Set#SET_GROUP DetectionSet The @{Core.Set} of @{Wrapper.Group}s that is used to detect the units. -- @return #DETECTION_BASE self function DETECTION_BASE:New( DetectionSet ) - + -- Inherits from BASE local self = BASE:Inherit( self, FSM:New() ) -- #DETECTION_BASE - + self.DetectedItemCount = 0 self.DetectedItemMax = 0 self.DetectedItems = {} - + self.DetectionSet = DetectionSet - + self.RefreshTimeInterval = 30 - + self:InitDetectVisual( nil ) self:InitDetectOptical( nil ) self:InitDetectRadar( nil ) self:InitDetectRWR( nil ) self:InitDetectIRST( nil ) self:InitDetectDLINK( nil ) - + self:FilterCategories( { Unit.Category.AIRPLANE, Unit.Category.GROUND_UNIT, @@ -57616,15 +64745,15 @@ do -- DETECTION_BASE Unit.Category.SHIP, Unit.Category.STRUCTURE } ) - + self:SetFriendliesRange( 6000 ) - + -- Create FSM transitions. - + self:SetStartState( "Stopped" ) - - self:AddTransition( "Stopped", "Start", "Detecting") - + + self:AddTransition( "Stopped", "Start", "Detecting" ) + --- OnLeave Transition Handler for State Stopped. -- @function [parent=#DETECTION_BASE] OnLeaveStopped -- @param #DETECTION_BASE self @@ -57632,14 +64761,14 @@ do -- DETECTION_BASE -- @param #string Event The Event string. -- @param #string To The To State string. -- @return #boolean Return false to cancel Transition. - + --- OnEnter Transition Handler for State Stopped. -- @function [parent=#DETECTION_BASE] OnEnterStopped -- @param #DETECTION_BASE self -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. - + --- OnBefore Transition Handler for Event Start. -- @function [parent=#DETECTION_BASE] OnBeforeStart -- @param #DETECTION_BASE self @@ -57647,23 +64776,23 @@ do -- DETECTION_BASE -- @param #string Event The Event string. -- @param #string To The To State string. -- @return #boolean Return false to cancel Transition. - + --- OnAfter Transition Handler for Event Start. -- @function [parent=#DETECTION_BASE] OnAfterStart -- @param #DETECTION_BASE self -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. - + --- Synchronous Event Trigger for Event Start. -- @function [parent=#DETECTION_BASE] Start -- @param #DETECTION_BASE self - + --- Asynchronous Event Trigger for Event Start. -- @function [parent=#DETECTION_BASE] __Start -- @param #DETECTION_BASE self -- @param #number Delay The delay in seconds. - + --- OnLeave Transition Handler for State Detecting. -- @function [parent=#DETECTION_BASE] OnLeaveDetecting -- @param #DETECTION_BASE self @@ -57671,17 +64800,17 @@ do -- DETECTION_BASE -- @param #string Event The Event string. -- @param #string To The To State string. -- @return #boolean Return false to cancel Transition. - + --- OnEnter Transition Handler for State Detecting. -- @function [parent=#DETECTION_BASE] OnEnterDetecting -- @param #DETECTION_BASE self -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. - + self:AddTransition( "Detecting", "Detect", "Detecting" ) self:AddTransition( "Detecting", "Detection", "Detecting" ) - + --- OnBefore Transition Handler for Event Detect. -- @function [parent=#DETECTION_BASE] OnBeforeDetect -- @param #DETECTION_BASE self @@ -57689,26 +64818,25 @@ do -- DETECTION_BASE -- @param #string Event The Event string. -- @param #string To The To State string. -- @return #boolean Return false to cancel Transition. - + --- OnAfter Transition Handler for Event Detect. -- @function [parent=#DETECTION_BASE] OnAfterDetect -- @param #DETECTION_BASE self -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. - + --- Synchronous Event Trigger for Event Detect. -- @function [parent=#DETECTION_BASE] Detect -- @param #DETECTION_BASE self - + --- Asynchronous Event Trigger for Event Detect. -- @function [parent=#DETECTION_BASE] __Detect -- @param #DETECTION_BASE self -- @param #number Delay The delay in seconds. - - + self:AddTransition( "Detecting", "Detected", "Detecting" ) - + --- OnBefore Transition Handler for Event Detected. -- @function [parent=#DETECTION_BASE] OnBeforeDetected -- @param #DETECTION_BASE self @@ -57716,7 +64844,7 @@ do -- DETECTION_BASE -- @param #string Event The Event string. -- @param #string To The To State string. -- @return #boolean Return false to cancel Transition. - + --- OnAfter Transition Handler for Event Detected. -- @function [parent=#DETECTION_BASE] OnAfterDetected -- @param #DETECTION_BASE self @@ -57724,20 +64852,20 @@ do -- DETECTION_BASE -- @param #string Event The Event string. -- @param #string To The To State string. -- @param #table Units Table of detected units. - + --- Synchronous Event Trigger for Event Detected. -- @function [parent=#DETECTION_BASE] Detected -- @param #DETECTION_BASE self -- @param #table Units Table of detected units. - + --- Asynchronous Event Trigger for Event Detected. -- @function [parent=#DETECTION_BASE] __Detected -- @param #DETECTION_BASE self -- @param #number Delay The delay in seconds. -- @param #table Units Table of detected units. - + self:AddTransition( "Detecting", "DetectedItem", "Detecting" ) - + --- OnAfter Transition Handler for Event DetectedItem. -- @function [parent=#DETECTION_BASE] OnAfterDetectedItem -- @param #DETECTION_BASE self @@ -57745,9 +64873,9 @@ do -- DETECTION_BASE -- @param #string Event The Event string. -- @param #string To The To State string. -- @param #table DetectedItem The DetectedItem. - + self:AddTransition( "*", "Stop", "Stopped" ) - + --- OnBefore Transition Handler for Event Stop. -- @function [parent=#DETECTION_BASE] OnBeforeStop -- @param #DETECTION_BASE self @@ -57755,23 +64883,23 @@ do -- DETECTION_BASE -- @param #string Event The Event string. -- @param #string To The To State string. -- @return #boolean Return false to cancel Transition. - + --- OnAfter Transition Handler for Event Stop. -- @function [parent=#DETECTION_BASE] OnAfterStop -- @param #DETECTION_BASE self -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. - + --- Synchronous Event Trigger for Event Stop. -- @function [parent=#DETECTION_BASE] Stop -- @param #DETECTION_BASE self - + --- Asynchronous Event Trigger for Event Stop. -- @function [parent=#DETECTION_BASE] __Stop -- @param #DETECTION_BASE self -- @param #number Delay The delay in seconds. - + --- OnLeave Transition Handler for State Stopped. -- @function [parent=#DETECTION_BASE] OnLeaveStopped -- @param #DETECTION_BASE self @@ -57779,24 +64907,24 @@ do -- DETECTION_BASE -- @param #string Event The Event string. -- @param #string To The To State string. -- @return #boolean Return false to cancel Transition. - + --- OnEnter Transition Handler for State Stopped. -- @function [parent=#DETECTION_BASE] OnEnterStopped -- @param #DETECTION_BASE self -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. - + return self end - + do -- State Transition Handling - + --- @param #DETECTION_BASE self -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. - function DETECTION_BASE:onafterStart(From,Event,To) + function DETECTION_BASE:onafterStart( From, Event, To ) self:__Detect( 1 ) end @@ -57804,18 +64932,18 @@ do -- DETECTION_BASE -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. - function DETECTION_BASE:onafterDetect(From,Event,To) + function DETECTION_BASE:onafterDetect( From, Event, To ) local DetectDelay = 0.1 self.DetectionCount = 0 self.DetectionRun = 0 self:UnIdentifyAllDetectedObjects() -- Resets the DetectedObjectsIdentified table - + local DetectionTimeStamp = timer.getTime() - + -- Reset detection cache for the next detection run. for DetectionObjectName, DetectedObjectData in pairs( self.DetectedObjects ) do - + self.DetectedObjects[DetectionObjectName].IsDetected = false self.DetectedObjects[DetectionObjectName].IsVisible = false self.DetectedObjects[DetectionObjectName].KnowDistance = nil @@ -57823,23 +64951,21 @@ do -- DETECTION_BASE self.DetectedObjects[DetectionObjectName].LastPos = nil self.DetectedObjects[DetectionObjectName].LastVelocity = nil self.DetectedObjects[DetectionObjectName].Distance = 10000000 - + end - + -- Count alive(!) groups only. Solves issue #1173 https://github.com/FlightControl-Master/MOOSE/issues/1173 self.DetectionCount = self:CountAliveRecce() - - local DetectionInterval = self.DetectionCount / ( self.RefreshTimeInterval - 1 ) - - self:ForEachAliveRecce( - function( DetectionGroup ) - self:__Detection( DetectDelay, DetectionGroup, DetectionTimeStamp ) -- Process each detection asynchronously. - DetectDelay = DetectDelay + DetectionInterval - end - ) - + + local DetectionInterval = self.DetectionCount / (self.RefreshTimeInterval - 1) + + self:ForEachAliveRecce( function( DetectionGroup ) + self:__Detection( DetectDelay, DetectionGroup, DetectionTimeStamp ) -- Process each detection asynchronously. + DetectDelay = DetectDelay + DetectionInterval + end ) + self:__Detect( -self.RefreshTimeInterval ) - + end --- @param #DETECTION_BASE self @@ -57848,41 +64974,40 @@ do -- DETECTION_BASE return self.DetectionSet:CountAlive() - end - + end + --- @param #DETECTION_BASE self function DETECTION_BASE:ForEachAliveRecce( IteratorFunction, ... ) self:F2( arg ) - + self.DetectionSet:ForEachGroupAlive( IteratorFunction, arg ) - + return self end - - + --- @param #DETECTION_BASE self -- @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 ) - - --self:F( { DetectedObjects = self.DetectedObjects } ) - + function DETECTION_BASE:onafterDetection( From, Event, To, Detection, DetectionTimeStamp ) + + -- self:F( { DetectedObjects = self.DetectedObjects } ) + self.DetectionRun = self.DetectionRun + 1 - + local HasDetectedObjects = false - + if Detection and Detection:IsAlive() then - - --self:T( { "DetectionGroup is Alive", DetectionGroup:GetName() } ) - + + -- self:T( { "DetectionGroup is Alive", DetectionGroup:GetName() } ) + local DetectionGroupName = Detection:GetName() - local DetectionUnit = Detection:GetUnit(1) - + local DetectionUnit = Detection:GetUnit( 1 ) + local DetectedUnits = {} - + local DetectedTargets = Detection:GetDetectedTargets( self.DetectVisual, self.DetectOptical, @@ -57891,29 +65016,29 @@ do -- DETECTION_BASE self.DetectRWR, self.DetectDLINK ) - + self:F( { DetectedTargets = DetectedTargets } ) - + for DetectionObjectID, Detection in pairs( DetectedTargets ) do local DetectedObject = Detection.object -- DCS#Object - + if DetectedObject and DetectedObject:isExist() and DetectedObject.id_ < 50000000 then -- and ( DetectedObject:getCategory() == Object.Category.UNIT or DetectedObject:getCategory() == Object.Category.STATIC ) then local DetectedObjectName = DetectedObject:getName() if not self.DetectedObjects[DetectedObjectName] then - self.DetectedObjects[DetectedObjectName] = self.DetectedObjects[DetectedObjectName] or {} + self.DetectedObjects[DetectedObjectName] = self.DetectedObjects[DetectedObjectName] or {} self.DetectedObjects[DetectedObjectName].Name = DetectedObjectName self.DetectedObjects[DetectedObjectName].Object = DetectedObject end end end - + for DetectionObjectName, DetectedObjectData in pairs( self.DetectedObjects ) do - + local DetectedObject = DetectedObjectData.Object - + if DetectedObject:isExist() then - - local TargetIsDetected, TargetIsVisible, TargetLastTime, TargetKnowType, TargetKnowDistance, TargetLastPos, TargetLastVelocity = DetectionUnit:IsTargetDetected( + + local TargetIsDetected, TargetIsVisible, TargetLastTime, TargetKnowType, TargetKnowDistance, TargetLastPos, TargetLastVelocity = DetectionUnit:IsTargetDetected( DetectedObject, self.DetectVisual, self.DetectOptical, @@ -57922,9 +65047,9 @@ do -- DETECTION_BASE self.DetectRWR, self.DetectDLINK ) - - --self:T2( { TargetIsDetected = TargetIsDetected, TargetIsVisible = TargetIsVisible, TargetLastTime = TargetLastTime, TargetKnowType = TargetKnowType, TargetKnowDistance = TargetKnowDistance, TargetLastPos = TargetLastPos, TargetLastVelocity = TargetLastVelocity } ) - + + -- self:T2( { TargetIsDetected = TargetIsDetected, TargetIsVisible = TargetIsVisible, TargetLastTime = TargetLastTime, TargetKnowType = TargetKnowType, TargetKnowDistance = TargetKnowDistance, TargetLastPos = TargetLastPos, TargetLastVelocity = TargetLastVelocity } ) + -- Only process if the target is visible. Detection also returns invisible units. --if Detection.visible == true then @@ -57935,7 +65060,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 + @@ -57979,7 +65104,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 @@ -58024,7 +65149,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 @@ -58052,47 +65177,47 @@ do -- DETECTION_BASE if TargetIsDetected and not self.DetectedObjects[DetectedObjectName].KnowType then self.DetectedObjects[DetectedObjectName].KnowType = TargetIsDetected and TargetKnowType - end - self.DetectedObjects[DetectedObjectName].KnowDistance = TargetKnowDistance -- Detection.distance -- TargetKnowDistance - self.DetectedObjects[DetectedObjectName].LastTime = ( TargetIsDetected and TargetIsVisible == false ) and TargetLastTime - self.DetectedObjects[DetectedObjectName].LastPos = ( TargetIsDetected and TargetIsVisible == false ) and TargetLastPos - self.DetectedObjects[DetectedObjectName].LastVelocity = ( TargetIsDetected and TargetIsVisible == false ) and TargetLastVelocity - - if not self.DetectedObjects[DetectedObjectName].Distance or ( Distance and self.DetectedObjects[DetectedObjectName].Distance > Distance ) then - self.DetectedObjects[DetectedObjectName].Distance = Distance - end - - self.DetectedObjects[DetectedObjectName].DetectionTimeStamp = DetectionTimeStamp - - self:F( { DetectedObject = self.DetectedObjects[DetectedObjectName] } ) - - local DetectedUnit = UNIT:FindByName( DetectedObjectName ) - - DetectedUnits[DetectedObjectName] = DetectedUnit - else - -- if beyond the DetectionRange then nullify... - self:F( { DetectedObject = "No more detection for " .. DetectedObjectName } ) - if self.DetectedObjects[DetectedObjectName] then - self.DetectedObjects[DetectedObjectName] = nil - end end - - --self:T2( self.DetectedObjects ) + self.DetectedObjects[DetectedObjectName].KnowDistance = TargetKnowDistance -- Detection.distance -- TargetKnowDistance + self.DetectedObjects[DetectedObjectName].LastTime = (TargetIsDetected and TargetIsVisible == false) and TargetLastTime + self.DetectedObjects[DetectedObjectName].LastPos = (TargetIsDetected and TargetIsVisible == false) and TargetLastPos + self.DetectedObjects[DetectedObjectName].LastVelocity = (TargetIsDetected and TargetIsVisible == false) and TargetLastVelocity + + if not self.DetectedObjects[DetectedObjectName].Distance or (Distance and self.DetectedObjects[DetectedObjectName].Distance > Distance) then + self.DetectedObjects[DetectedObjectName].Distance = Distance + end + + self.DetectedObjects[DetectedObjectName].DetectionTimeStamp = DetectionTimeStamp + + self:F( { DetectedObject = self.DetectedObjects[DetectedObjectName] } ) + + local DetectedUnit = UNIT:FindByName( DetectedObjectName ) + + DetectedUnits[DetectedObjectName] = DetectedUnit + else + -- if beyond the DetectionRange then nullify... + self:F( { DetectedObject = "No more detection for " .. DetectedObjectName } ) + if self.DetectedObjects[DetectedObjectName] then + self.DetectedObjects[DetectedObjectName] = nil + end + end + + -- self:T2( self.DetectedObjects ) else -- The previously detected object does not exist anymore, delete from the cache. self:F( "Removing from DetectedObjects: " .. DetectionObjectName ) self.DetectedObjects[DetectionObjectName] = nil end end - + if HasDetectedObjects then self:__Detected( 0.1, DetectedUnits ) end - + end - + if self.DetectionCount > 0 and self.DetectionRun == self.DetectionCount then - + -- First check if all DetectedObjects were detected. -- This is important. When there are DetectedObjects in the list, but were not detected, -- And these remain undetected for more than 60 seconds, then these DetectedObjects will be flagged as not Detected. @@ -58105,45 +65230,43 @@ do -- DETECTION_BASE end self:CreateDetectionItems() -- Polymorphic call to Create/Update the DetectionItems list for the DETECTION_ class grouping method. - + for DetectedItemID, DetectedItem in pairs( self.DetectedItems ) do - + self:UpdateDetectedItemDetection( DetectedItem ) - + self:CleanDetectionItem( DetectedItem, DetectedItemID ) -- Any DetectionItem that has a Set with zero elements in it, must be removed from the DetectionItems list. - + if DetectedItem then self:__DetectedItem( 0.1, DetectedItem ) end - + end end - end - - + end - + do -- DetectionItems Creation - + --- Clean the DetectedItem table. -- @param #DETECTION_BASE self -- @return #DETECTION_BASE function DETECTION_BASE:CleanDetectionItem( DetectedItem, DetectedItemID ) - + -- We clean all DetectedItems. -- if there are any remaining DetectedItems with no Set Objects then the Item in the DetectedItems must be deleted. - + local DetectedSet = DetectedItem.Set - + if DetectedSet:Count() == 0 then self:RemoveDetectedItem( DetectedItemID ) end return self end - + --- Forget a Unit from a DetectionItem -- @param #DETECTION_BASE self -- @param #string UnitName The UnitName that needs to be forgotten from the DetectionItem Sets. @@ -58151,7 +65274,7 @@ do -- DETECTION_BASE function DETECTION_BASE:ForgetDetectedUnit( UnitName ) local DetectedItems = self:GetDetectedItems() - + for DetectedItemIndex, DetectedItem in pairs( DetectedItems ) do local DetectedSet = self:GetDetectedItemSet( DetectedItem ) if DetectedSet then @@ -58161,96 +65284,95 @@ do -- DETECTION_BASE return self end - + --- Make a DetectionSet table. This function will be overridden in the derived clsses. -- @param #DETECTION_BASE self -- @return #DETECTION_BASE function DETECTION_BASE:CreateDetectionItems() - + self:F( "Error, in DETECTION_BASE class..." ) return self end - end - + do -- Initialization methods - + --- Detect Visual. -- @param #DETECTION_BASE self -- @param #boolean DetectVisual -- @return #DETECTION_BASE self function DETECTION_BASE:InitDetectVisual( DetectVisual ) - + self.DetectVisual = DetectVisual - + return self end - + --- Detect Optical. -- @param #DETECTION_BASE self -- @param #boolean DetectOptical -- @return #DETECTION_BASE self function DETECTION_BASE:InitDetectOptical( DetectOptical ) - self:F2() - + self:F2() + self.DetectOptical = DetectOptical - + return self end - + --- Detect Radar. -- @param #DETECTION_BASE self -- @param #boolean DetectRadar -- @return #DETECTION_BASE self function DETECTION_BASE:InitDetectRadar( DetectRadar ) self:F2() - + self.DetectRadar = DetectRadar - + return self end - + --- Detect IRST. -- @param #DETECTION_BASE self -- @param #boolean DetectIRST -- @return #DETECTION_BASE self function DETECTION_BASE:InitDetectIRST( DetectIRST ) self:F2() - + self.DetectIRST = DetectIRST - + return self end - + --- Detect RWR. -- @param #DETECTION_BASE self -- @param #boolean DetectRWR -- @return #DETECTION_BASE self function DETECTION_BASE:InitDetectRWR( DetectRWR ) self:F2() - + self.DetectRWR = DetectRWR - + return self end - + --- Detect DLINK. -- @param #DETECTION_BASE self -- @param #boolean DetectDLINK -- @return #DETECTION_BASE self function DETECTION_BASE:InitDetectDLINK( DetectDLINK ) self:F2() - + self.DetectDLINK = DetectDLINK - + return self end - + end - + do -- Filter methods - + --- Filter the detected units based on Unit.Category -- The different values of Unit.Category can be: -- @@ -58275,35 +65397,35 @@ do -- DETECTION_BASE -- @return #DETECTION_BASE self function DETECTION_BASE:FilterCategories( FilterCategories ) self:F2() - + self._.FilterCategories = {} if type( FilterCategories ) == "table" then for CategoryID, Category in pairs( FilterCategories ) do self._.FilterCategories[Category] = Category - end + end else self._.FilterCategories[FilterCategories] = FilterCategories end return self - + end end do - + --- Set the detection interval time in seconds. -- @param #DETECTION_BASE self -- @param #number RefreshTimeInterval Interval in seconds. -- @return #DETECTION_BASE self function DETECTION_BASE:SetRefreshTimeInterval( RefreshTimeInterval ) self:F2() - + self.RefreshTimeInterval = RefreshTimeInterval - + return self end - + end do -- Friendlies Radius @@ -58312,18 +65434,18 @@ do -- DETECTION_BASE -- @param #DETECTION_BASE self -- @param #number FriendliesRange Radius to use when checking if Friendlies are nearby. -- @return #DETECTION_BASE self - function DETECTION_BASE:SetFriendliesRange( FriendliesRange ) --R2.2 Friendlies range + function DETECTION_BASE:SetFriendliesRange( FriendliesRange ) -- R2.2 Friendlies range self:F2() - + self.FriendliesRange = FriendliesRange - + return self end - + end - + do -- Intercept Point - + --- Set the parameters to calculate to optimal intercept point. -- @param #DETECTION_BASE self -- @param #boolean Intercept Intercept is true if an intercept point is calculated. Intercept is false if it is disabled. The default Intercept is false. @@ -58331,36 +65453,36 @@ do -- DETECTION_BASE -- @return #DETECTION_BASE self function DETECTION_BASE:SetIntercept( Intercept, InterceptDelay ) self:F2() - + self.Intercept = Intercept self.InterceptDelay = InterceptDelay - + return self end end - + do -- Accept / Reject detected units - + --- Accept detections if within a range in meters. -- @param #DETECTION_BASE self -- @param #number AcceptRange Accept a detection if the unit is within the AcceptRange in meters. -- @return #DETECTION_BASE self function DETECTION_BASE:SetAcceptRange( AcceptRange ) self:F2() - + self.AcceptRange = AcceptRange - + return self end - + --- Accept detections if within the specified zone(s). -- @param #DETECTION_BASE self -- @param Core.Zone#ZONE_BASE AcceptZones Can be a list or ZONE_BASE objects, or a single ZONE_BASE object. -- @return #DETECTION_BASE self function DETECTION_BASE:SetAcceptZones( AcceptZones ) self:F2() - + if type( AcceptZones ) == "table" then if AcceptZones.ClassName and AcceptZones:IsInstanceOf( ZONE_BASE ) then self.AcceptZones = { AcceptZones } @@ -58371,17 +65493,17 @@ do -- DETECTION_BASE self:F( { "AcceptZones must be a list of ZONE_BASE derived objects or one ZONE_BASE derived object", AcceptZones } ) error() end - + return self end - + --- Reject detections if within the specified zone(s). -- @param #DETECTION_BASE self -- @param Core.Zone#ZONE_BASE RejectZones Can be a list or ZONE_BASE objects, or a single ZONE_BASE object. -- @return #DETECTION_BASE self function DETECTION_BASE:SetRejectZones( RejectZones ) self:F2() - + if type( RejectZones ) == "table" then if RejectZones.ClassName and RejectZones:IsInstanceOf( ZONE_BASE ) then self.RejectZones = { RejectZones } @@ -58392,14 +65514,14 @@ do -- DETECTION_BASE self:F( { "RejectZones must be a list of ZONE_BASE derived objects or one ZONE_BASE derived object", RejectZones } ) error() end - + return self end - + end - + do -- Probability methods - + --- Upon a **visual** detection, the further away a detected object is, the less likely it is to be detected properly. -- Also, the speed of accurate detection plays a role. -- A distance probability factor between 0 and 1 can be given, that will model a linear extrapolated probability over 10 km distance. @@ -58410,13 +65532,12 @@ do -- DETECTION_BASE -- @return #DETECTION_BASE self function DETECTION_BASE:SetDistanceProbability( DistanceProbability ) self:F2() - + self.DistanceProbability = DistanceProbability - + return self end - - + --- Upon a **visual** detection, the higher the unit is during the detecting process, the more likely the detected unit is to be detected properly. -- A detection at a 90% alpha angle is the most optimal, a detection at 10% is less and a detection at 0% is less likely to be correct. -- @@ -58429,12 +65550,12 @@ do -- DETECTION_BASE -- @return #DETECTION_BASE self function DETECTION_BASE:SetAlphaAngleProbability( AlphaAngleProbability ) self:F2() - + self.AlphaAngleProbability = AlphaAngleProbability - + return self end - + --- Upon a **visual** detection, the more a detected unit is within a cloudy zone, the less likely the detected unit is to be detected successfully. -- The Cloudy Zones work with the ZONE_BASE derived classes. The mission designer can define within the mission -- zones that reflect cloudy areas where detected units may not be so easily visually detected. @@ -58443,26 +65564,25 @@ do -- DETECTION_BASE -- @return #DETECTION_BASE self function DETECTION_BASE:SetZoneProbability( ZoneArray ) self:F2() - - self.ZoneProbability = ZoneArray - + + self.ZoneProbability = ZoneArray + return self end - - + end - + do -- Change processing - + --- Accepts changes from the detected item. -- @param #DETECTION_BASE self -- @param #DETECTION_BASE.DetectedItem DetectedItem -- @return #DETECTION_BASE self function DETECTION_BASE:AcceptChanges( DetectedItem ) - + DetectedItem.Changed = false DetectedItem.Changes = {} - + return self end @@ -58472,21 +65592,20 @@ do -- DETECTION_BASE -- @param #string ChangeCode -- @return #DETECTION_BASE self function DETECTION_BASE:AddChangeItem( DetectedItem, ChangeCode, ItemUnitType ) - + DetectedItem.Changed = true local ID = DetectedItem.ID - + DetectedItem.Changes = DetectedItem.Changes or {} DetectedItem.Changes[ChangeCode] = DetectedItem.Changes[ChangeCode] or {} DetectedItem.Changes[ChangeCode].ID = ID DetectedItem.Changes[ChangeCode].ItemUnitType = ItemUnitType - + self:F( { "Change on Detected Item:", DetectedItemID = DetectedItem.ID, ChangeCode = ChangeCode, ItemUnitType = ItemUnitType } ) - + return self end - - + --- Add a change to the detected zone. -- @param #DETECTION_BASE self -- @param #DETECTION_BASE.DetectedItem DetectedItem @@ -58494,25 +65613,25 @@ do -- DETECTION_BASE -- @param #string ChangeUnitType -- @return #DETECTION_BASE self function DETECTION_BASE:AddChangeUnit( DetectedItem, ChangeCode, ChangeUnitType ) - + DetectedItem.Changed = true local ID = DetectedItem.ID - + DetectedItem.Changes = DetectedItem.Changes or {} DetectedItem.Changes[ChangeCode] = DetectedItem.Changes[ChangeCode] or {} DetectedItem.Changes[ChangeCode][ChangeUnitType] = DetectedItem.Changes[ChangeCode][ChangeUnitType] or 0 DetectedItem.Changes[ChangeCode][ChangeUnitType] = DetectedItem.Changes[ChangeCode][ChangeUnitType] + 1 DetectedItem.Changes[ChangeCode].ID = ID - + self:F( { "Change on Detected Unit:", DetectedItemID = DetectedItem.ID, ChangeCode = ChangeCode, ChangeUnitType = ChangeUnitType } ) - + return self end - + end - + do -- Friendly calculations - + --- This will allow during friendly search any recce or detection unit to be also considered as a friendly. -- By default, recce aren't considered friendly, because that would mean that a recce would be also an attacking friendly, -- and this is wrong. @@ -58531,9 +65650,9 @@ do -- DETECTION_BASE self:F( { FriendlyPrefix = Prefix } ) self.FriendlyPrefixes[Prefix] = Prefix end - return self + return self end - + --- This will allow during friendly search only units of the specified list of categories. -- @param #DETECTION_BASE self -- @param #string FriendlyCategories A list of unit categories. @@ -58541,113 +65660,112 @@ do -- DETECTION_BASE -- @usage -- -- Only allow Ships and Vehicles to be part of the friendly team. -- Detection:SetFriendlyCategories( { Unit.Category.SHIP, Unit.Category.GROUND_UNIT } ) - + --- Returns if there are friendlies nearby the FAC units ... -- @param #DETECTION_BASE self -- @param #DETECTION_BASE.DetectedItem DetectedItem -- @param DCS#Unit.Category Category The category of the unit. -- @return #boolean true if there are friendlies nearby function DETECTION_BASE:IsFriendliesNearBy( DetectedItem, Category ) --- self:F( { "FriendliesNearBy Test", DetectedItem.FriendliesNearBy } ) - return ( DetectedItem.FriendliesNearBy and DetectedItem.FriendliesNearBy[Category] ~= nil ) or false + -- self:F( { "FriendliesNearBy Test", DetectedItem.FriendliesNearBy } ) + return (DetectedItem.FriendliesNearBy and DetectedItem.FriendliesNearBy[Category] ~= nil) or false end - + --- Returns friendly units nearby the FAC units ... -- @param #DETECTION_BASE self -- @param #DETECTION_BASE.DetectedItem DetectedItem -- @param DCS#Unit.Category Category The category of the unit. - -- @return #map<#string,Wrapper.Unit#UNIT> The map of Friendly UNITs. + -- @return #map<#string,Wrapper.Unit#UNIT> The map of Friendly UNITs. function DETECTION_BASE:GetFriendliesNearBy( DetectedItem, Category ) - + return DetectedItem.FriendliesNearBy and DetectedItem.FriendliesNearBy[Category] end - + --- Returns if there are friendlies nearby the intercept ... -- @param #DETECTION_BASE self -- @param #DETECTION_BASE.DetectedItem DetectedItem -- @return #boolean trhe if there are friendlies near the intercept. function DETECTION_BASE:IsFriendliesNearIntercept( DetectedItem ) - + return DetectedItem.FriendliesNearIntercept ~= nil or false end - + --- Returns friendly units nearby the intercept point ... -- @param #DETECTION_BASE self -- @param #DETECTION_BASE.DetectedItem DetectedItem The detected item. -- @return #map<#string,Wrapper.Unit#UNIT> The map of Friendly UNITs. function DETECTION_BASE:GetFriendliesNearIntercept( DetectedItem ) - + return DetectedItem.FriendliesNearIntercept end - - --- Returns the distance used to identify friendlies near the deteted item ... + + --- Returns the distance used to identify friendlies near the detected item ... -- @param #DETECTION_BASE self -- @param #DETECTION_BASE.DetectedItem DetectedItem The detected item. -- @return #table A table of distances to friendlies. function DETECTION_BASE:GetFriendliesDistance( DetectedItem ) - + return DetectedItem.FriendliesDistance end - + --- Returns if there are friendlies nearby the FAC units ... -- @param #DETECTION_BASE self -- @param #DETECTION_BASE.DetectedItem DetectedItem - -- @return #boolean trhe if there are friendlies nearby + -- @return #boolean true if there are friendlies nearby function DETECTION_BASE:IsPlayersNearBy( DetectedItem ) - + return DetectedItem.PlayersNearBy ~= nil end - + --- Returns friendly units nearby the FAC units ... -- @param #DETECTION_BASE self -- @param #DETECTION_BASE.DetectedItem DetectedItem The detected item. - -- @return #map<#string,Wrapper.Unit#UNIT> The map of Friendly UNITs. + -- @return #map<#string,Wrapper.Unit#UNIT> The map of Friendly UNITs. function DETECTION_BASE:GetPlayersNearBy( DetectedItem ) - + return DetectedItem.PlayersNearBy end - + --- Background worker function to determine if there are friendlies nearby ... -- @param #DETECTION_BASE self -- @param #table TargetData function DETECTION_BASE:ReportFriendliesNearBy( TargetData ) - --self:F( { "Search Friendlies", DetectedItem = TargetData.DetectedItem } ) - - local DetectedItem = TargetData.DetectedItem --#DETECTION_BASE.DetectedItem + -- self:F( { "Search Friendlies", DetectedItem = TargetData.DetectedItem } ) + + local DetectedItem = TargetData.DetectedItem -- #DETECTION_BASE.DetectedItem local DetectedSet = TargetData.DetectedItem.Set local DetectedUnit = DetectedSet:GetFirst() -- Wrapper.Unit#UNIT - + DetectedItem.FriendliesNearBy = nil -- We need to ensure that the DetectedUnit is alive! if DetectedUnit and DetectedUnit:IsAlive() then - + local DetectedUnitCoord = DetectedUnit:GetCoordinate() local InterceptCoord = TargetData.InterceptCoord or DetectedUnitCoord - + local SphereSearch = { - id = world.VolumeType.SPHERE, + id = world.VolumeType.SPHERE, params = { - point = InterceptCoord:GetVec3(), - radius = self.FriendliesRange, + point = InterceptCoord:GetVec3(), + radius = self.FriendliesRange, } - - } - + } + --- @param DCS#Unit FoundDCSUnit -- @param Wrapper.Group#GROUP ReportGroup -- @param Core.Set#SET_GROUP ReportSetGroup local FindNearByFriendlies = function( FoundDCSUnit, ReportGroupData ) - - local DetectedItem = ReportGroupData.DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem + + local DetectedItem = ReportGroupData.DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem local DetectedSet = ReportGroupData.DetectedItem.Set local DetectedUnit = DetectedSet:GetFirst() -- Wrapper.Unit#UNIT local DetectedUnitCoord = DetectedUnit:GetCoordinate() local InterceptCoord = ReportGroupData.InterceptCoord or DetectedUnitCoord local ReportSetGroup = ReportGroupData.ReportSetGroup - + local EnemyCoalition = DetectedUnit:GetCoalition() - + local FoundUnitCoalition = FoundDCSUnit:getCoalition() local FoundUnitCategory = FoundDCSUnit:getDesc().category local FoundUnitName = FoundDCSUnit:getName() @@ -58655,25 +65773,25 @@ do -- DETECTION_BASE local EnemyUnitName = DetectedUnit:GetName() local FoundUnitInReportSetGroup = ReportSetGroup:FindGroup( FoundUnitGroupName ) ~= nil - --self:T( { "Friendlies search:", FoundUnitName, FoundUnitCoalition, EnemyUnitName, EnemyCoalition, FoundUnitInReportSetGroup } ) - + -- self:T( { "Friendlies search:", FoundUnitName, FoundUnitCoalition, EnemyUnitName, EnemyCoalition, FoundUnitInReportSetGroup } ) + if FoundUnitInReportSetGroup == true then -- If the recce was part of the friendlies found, then check if the recce is part of the allowed friendly unit prefixes. for PrefixID, Prefix in pairs( self.FriendlyPrefixes or {} ) do - --self:F( { "Friendly Prefix:", Prefix = Prefix } ) + -- self:F( { "Friendly Prefix:", Prefix = Prefix } ) -- In case a match is found (so a recce unit name is part of the friendly prefixes), then report that recce to be part of the friendlies. -- This is important if CAP planes (so planes using their own radar) to be scanning for targets as part of the EWR network. -- But CAP planes are also attackers, so they need to be considered friendlies too! -- I chose to use prefixes because it is the fastest way to check. - if string.find( FoundUnitName, Prefix:gsub ("-", "%%-"), 1 ) then + if string.find( FoundUnitName, Prefix:gsub( "-", "%%-" ), 1 ) then FoundUnitInReportSetGroup = false break end end end - - --self:F( { "Friendlies near Target:", FoundUnitName, FoundUnitCoalition, EnemyUnitName, EnemyCoalition, FoundUnitInReportSetGroup } ) - + + -- self:F( { "Friendlies near Target:", FoundUnitName, FoundUnitCoalition, EnemyUnitName, EnemyCoalition, FoundUnitInReportSetGroup } ) + if FoundUnitCoalition ~= EnemyCoalition and FoundUnitInReportSetGroup == false then local FriendlyUnit = UNIT:Find( FoundDCSUnit ) local FriendlyUnitName = FriendlyUnit:GetName() @@ -58687,65 +65805,64 @@ do -- DETECTION_BASE local Distance = DetectedUnitCoord:Get2DDistance( FriendlyUnit:GetCoordinate() ) DetectedItem.FriendliesDistance = DetectedItem.FriendliesDistance or {} DetectedItem.FriendliesDistance[Distance] = FriendlyUnit - --self:F( { "Friendlies Found:", FriendlyUnitName = FriendlyUnitName, Distance = Distance, FriendlyUnitCategory = FriendlyUnitCategory, FriendliesCategory = self.FriendliesCategory } ) + -- self:F( { "Friendlies Found:", FriendlyUnitName = FriendlyUnitName, Distance = Distance, FriendlyUnitCategory = FriendlyUnitCategory, FriendliesCategory = self.FriendliesCategory } ) return true end - + return true end - + world.searchObjects( Object.Category.UNIT, SphereSearch, FindNearByFriendlies, TargetData ) DetectedItem.PlayersNearBy = nil - + _DATABASE:ForEachPlayer( - --- @param Wrapper.Unit#UNIT PlayerUnit - function( PlayerUnitName ) - local PlayerUnit = UNIT:FindByName( PlayerUnitName ) + --- @param Wrapper.Unit#UNIT PlayerUnit + function( PlayerUnitName ) + local PlayerUnit = UNIT:FindByName( PlayerUnitName ) - -- Fix for issue https://github.com/FlightControl-Master/MOOSE/issues/1225 - if PlayerUnit and PlayerUnit:IsAlive() then - local coord=PlayerUnit:GetCoordinate() - - if coord and coord:IsInRadius( DetectedUnitCoord, self.FriendliesRange ) then + -- Fix for issue https://github.com/FlightControl-Master/MOOSE/issues/1225 + if PlayerUnit and PlayerUnit:IsAlive() then + local coord = PlayerUnit:GetCoordinate() + + if coord and coord:IsInRadius( DetectedUnitCoord, self.FriendliesRange ) then + + local PlayerUnitCategory = PlayerUnit:GetDesc().category + + if (not self.FriendliesCategory) or (self.FriendliesCategory and (self.FriendliesCategory == PlayerUnitCategory)) then + + local PlayerUnitName = PlayerUnit:GetName() + + DetectedItem.PlayersNearBy = DetectedItem.PlayersNearBy or {} + DetectedItem.PlayersNearBy[PlayerUnitName] = PlayerUnit + + -- Friendlies are sorted per unit category. + DetectedItem.FriendliesNearBy = DetectedItem.FriendliesNearBy or {} + DetectedItem.FriendliesNearBy[PlayerUnitCategory] = DetectedItem.FriendliesNearBy[PlayerUnitCategory] or {} + DetectedItem.FriendliesNearBy[PlayerUnitCategory][PlayerUnitName] = PlayerUnit + + local Distance = DetectedUnitCoord:Get2DDistance( PlayerUnit:GetCoordinate() ) + DetectedItem.FriendliesDistance = DetectedItem.FriendliesDistance or {} + DetectedItem.FriendliesDistance[Distance] = PlayerUnit - local PlayerUnitCategory = PlayerUnit:GetDesc().category - - if ( not self.FriendliesCategory ) or ( self.FriendliesCategory and ( self.FriendliesCategory == PlayerUnitCategory ) ) then - - local PlayerUnitName = PlayerUnit:GetName() - - DetectedItem.PlayersNearBy = DetectedItem.PlayersNearBy or {} - DetectedItem.PlayersNearBy[PlayerUnitName] = PlayerUnit - - -- Friendlies are sorted per unit category. - DetectedItem.FriendliesNearBy = DetectedItem.FriendliesNearBy or {} - DetectedItem.FriendliesNearBy[PlayerUnitCategory] = DetectedItem.FriendliesNearBy[PlayerUnitCategory] or {} - DetectedItem.FriendliesNearBy[PlayerUnitCategory][PlayerUnitName] = PlayerUnit - - local Distance = DetectedUnitCoord:Get2DDistance( PlayerUnit:GetCoordinate() ) - DetectedItem.FriendliesDistance = DetectedItem.FriendliesDistance or {} - DetectedItem.FriendliesDistance[Distance] = PlayerUnit - - end end end end - ) - end + end ) + end self:F( { Friendlies = DetectedItem.FriendliesNearBy, Players = DetectedItem.PlayersNearBy } ) - + end - + end - + --- Determines if a detected object has already been identified during detection processing. -- @param #DETECTION_BASE self -- @param #DETECTION_BASE.DetectedObject DetectedObject -- @return #boolean true if already identified. function DETECTION_BASE:IsDetectedObjectIdentified( DetectedObject ) - + local DetectedObjectName = DetectedObject.Name if DetectedObjectName then local DetectedObjectIdentified = self.DetectedObjectsIdentified[DetectedObjectName] == true @@ -58754,71 +65871,70 @@ do -- DETECTION_BASE return nil end end - + --- Identifies a detected object during detection processing. -- @param #DETECTION_BASE self -- @param #DETECTION_BASE.DetectedObject DetectedObject function DETECTION_BASE:IdentifyDetectedObject( DetectedObject ) - --self:F( { "Identified:", DetectedObject.Name } ) - + -- self:F( { "Identified:", DetectedObject.Name } ) + local DetectedObjectName = DetectedObject.Name self.DetectedObjectsIdentified[DetectedObjectName] = true end - + --- UnIdentify a detected object during detection processing. -- @param #DETECTION_BASE self -- @param #DETECTION_BASE.DetectedObject DetectedObject function DETECTION_BASE:UnIdentifyDetectedObject( DetectedObject ) - + local DetectedObjectName = DetectedObject.Name self.DetectedObjectsIdentified[DetectedObjectName] = false end - + --- UnIdentify all detected objects during detection processing. -- @param #DETECTION_BASE self function DETECTION_BASE:UnIdentifyAllDetectedObjects() - + self.DetectedObjectsIdentified = {} -- Table will be garbage collected. end - + --- Gets a detected object with a given name. -- @param #DETECTION_BASE self -- @param #string ObjectName -- @return #DETECTION_BASE.DetectedObject function DETECTION_BASE:GetDetectedObject( ObjectName ) - self:F2( { ObjectName = ObjectName } ) - + self:F2( { ObjectName = ObjectName } ) + if ObjectName then local DetectedObject = self.DetectedObjects[ObjectName] - + if DetectedObject then - --self:F( { DetectedObjects = self.DetectedObjects } ) + -- self:F( { DetectedObjects = self.DetectedObjects } ) -- Only return detected objects that are alive! local DetectedUnit = UNIT:FindByName( ObjectName ) if DetectedUnit and DetectedUnit:IsAlive() then if self:IsDetectedObjectIdentified( DetectedObject ) == false then - --self:F( { DetectedObject = DetectedObject } ) + -- self:F( { DetectedObject = DetectedObject } ) return DetectedObject end end end end - + return nil end - --- Gets a detected unit type name, taking into account the detection results. -- @param #DETECTION_BASE self -- @param Wrapper.Unit#UNIT DetectedUnit -- @return #string The type name function DETECTION_BASE:GetDetectedUnitTypeName( DetectedUnit ) - --self:F2( ObjectName ) - + -- self:F2( ObjectName ) + if DetectedUnit and DetectedUnit:IsAlive() then local DetectedUnitName = DetectedUnit:GetName() local DetectedObject = self.DetectedObjects[DetectedUnitName] - + if DetectedObject then if DetectedObject.KnowType then return DetectedUnit:GetTypeName() @@ -58831,11 +65947,10 @@ do -- DETECTION_BASE else return "Dead:" .. DetectedUnit:GetName() end - + return "Undetected:" .. DetectedUnit:GetName() end - --- Adds a new DetectedItem to the DetectedItems list. -- The DetectedItem is a table and contains a SET_UNIT in the field Set. -- @param #DETECTION_BASE self @@ -58844,29 +65959,28 @@ do -- DETECTION_BASE -- @param Core.Set#SET_UNIT Set (optional) The Set of Units to be added. -- @return #DETECTION_BASE.DetectedItem function DETECTION_BASE:AddDetectedItem( ItemPrefix, DetectedItemKey, Set ) - - local DetectedItem = {} --#DETECTION_BASE.DetectedItem + + local DetectedItem = {} -- #DETECTION_BASE.DetectedItem self.DetectedItemCount = self.DetectedItemCount + 1 self.DetectedItemMax = self.DetectedItemMax + 1 - - + DetectedItemKey = DetectedItemKey or self.DetectedItemMax self.DetectedItems[DetectedItemKey] = DetectedItem self.DetectedItemsByIndex[DetectedItemKey] = DetectedItem DetectedItem.Index = DetectedItemKey - + DetectedItem.Set = Set or SET_UNIT:New():FilterDeads():FilterCrashes() DetectedItem.ItemID = ItemPrefix .. "." .. self.DetectedItemMax DetectedItem.ID = self.DetectedItemMax DetectedItem.Removed = false - + if self.Locking then self:LockDetectedItem( DetectedItem ) end - + return DetectedItem end - + --- Adds a new DetectedItem to the DetectedItems list. -- The DetectedItem is a table and contains a SET_UNIT in the field Set. -- @param #DETECTION_BASE self @@ -58875,22 +65989,22 @@ do -- DETECTION_BASE -- @param Core.Zone#ZONE_UNIT Zone (optional) The Zone to be added where the Units are located. -- @return #DETECTION_BASE.DetectedItem function DETECTION_BASE:AddDetectedItemZone( ItemPrefix, DetectedItemKey, Set, Zone ) - + self:F( { ItemPrefix, DetectedItemKey, Set, Zone } ) - + local DetectedItem = self:AddDetectedItem( ItemPrefix, DetectedItemKey, Set ) DetectedItem.Zone = Zone - + return DetectedItem end - + --- Removes an existing DetectedItem from the DetectedItems list. -- The DetectedItem is a table and contains a SET_UNIT in the field Set. -- @param #DETECTION_BASE self -- @param DetectedItemKey The key in the DetectedItems list where the item needs to be removed. function DETECTION_BASE:RemoveDetectedItem( DetectedItemKey ) - + local DetectedItem = self.DetectedItems[DetectedItemKey] if DetectedItem then @@ -58900,125 +66014,124 @@ do -- DETECTION_BASE self.DetectedItems[DetectedItemKey] = nil end end - - + --- Get the DetectedItems by Key. -- This will return the DetectedItems collection, indexed by the Key, which can be any object that acts as the key of the detection. -- @param #DETECTION_BASE self -- @return #DETECTION_BASE.DetectedItems function DETECTION_BASE:GetDetectedItems() - + return self.DetectedItems end - + --- Get the DetectedItems by Index. -- This will return the DetectedItems collection, indexed by an internal numerical Index. -- @param #DETECTION_BASE self -- @return #DETECTION_BASE.DetectedItems function DETECTION_BASE:GetDetectedItemsByIndex() - + return self.DetectedItemsByIndex end - + --- Get the amount of SETs with detected objects. -- @param #DETECTION_BASE self - -- @return #number The amount of detected items. Note that the amount of detected items can differ with the reality, because detections are not real-time but doen in intervals! + -- @return #number The amount of detected items. Note that the amount of detected items can differ with the reality, because detections are not real-time but done in intervals! function DETECTION_BASE:GetDetectedItemsCount() - + local DetectedCount = self.DetectedItemCount return DetectedCount end - + --- Get a detected item using a given Key. -- @param #DETECTION_BASE self -- @param Key -- @return #DETECTION_BASE.DetectedItem function DETECTION_BASE:GetDetectedItemByKey( Key ) - + self:F( { DetectedItems = self.DetectedItems } ) - + local DetectedItem = self.DetectedItems[Key] if DetectedItem then return DetectedItem end - + return nil end - + --- Get a detected item using a given numeric index. -- @param #DETECTION_BASE self -- @param #number Index -- @return #DETECTION_BASE.DetectedItem function DETECTION_BASE:GetDetectedItemByIndex( Index ) - + self:F( { self.DetectedItemsByIndex } ) - + local DetectedItem = self.DetectedItemsByIndex[Index] if DetectedItem then return DetectedItem end - + return nil end - + --- Get a detected ItemID using a given numeric index. -- @param #DETECTION_BASE self -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem. -- @return #string DetectedItemID - function DETECTION_BASE:GetDetectedItemID( DetectedItem ) --R2.1 - + function DETECTION_BASE:GetDetectedItemID( DetectedItem ) -- R2.1 + return DetectedItem and DetectedItem.ItemID or "" end - + --- Get a detected ID using a given numeric index. -- @param #DETECTION_BASE self -- @param #number Index -- @return #string DetectedItemID - function DETECTION_BASE:GetDetectedID( Index ) --R2.1 - + function DETECTION_BASE:GetDetectedID( Index ) -- R2.1 + local DetectedItem = self.DetectedItemsByIndex[Index] if DetectedItem then return DetectedItem.ID end - + return "" end - - --- Get the @{Core.Set#SET_UNIT} of a detecttion area using a given numeric index. + + --- Get the @{Core.Set#SET_UNIT} of a detection area using a given numeric index. -- @param #DETECTION_BASE self -- @param #DETECTION_BASE.DetectedItem DetectedItem -- @return Core.Set#SET_UNIT DetectedSet function DETECTION_BASE:GetDetectedItemSet( DetectedItem ) - + local DetectedSetUnit = DetectedItem and DetectedItem.Set if DetectedSetUnit then return DetectedSetUnit end - + return nil end - + --- Set IsDetected flag for the DetectedItem, which can have more units. -- @param #DETECTION_BASE self -- @return #DETECTION_BASE.DetectedItem DetectedItem -- @return #boolean true if at least one UNIT is detected from the DetectedSet, false if no UNIT was detected from the DetectedSet. function DETECTION_BASE:UpdateDetectedItemDetection( DetectedItem ) - + local IsDetected = false - + for UnitName, UnitData in pairs( DetectedItem.Set:GetSet() ) do local DetectedObject = self.DetectedObjects[UnitName] - self:F({UnitName = UnitName, IsDetected = DetectedObject.IsDetected}) + self:F( { UnitName = UnitName, IsDetected = DetectedObject.IsDetected } ) if DetectedObject.IsDetected then IsDetected = true break end end - + self:F( { IsDetected = DetectedItem.IsDetected } ) - + DetectedItem.IsDetected = IsDetected - + return IsDetected end @@ -59026,31 +66139,30 @@ do -- DETECTION_BASE -- @param #DETECTION_BASE self -- @param #DETECTION_BASE.DetectedItem DetectedItem -- @return #boolean true if at least one UNIT is detected from the DetectedSet, false if no UNIT was detected from the DetectedSet. - function DETECTION_BASE:IsDetectedItemDetected( DetectedItem ) - + function DETECTION_BASE:IsDetectedItemDetected( DetectedItem ) + return DetectedItem.IsDetected end - do -- Zones - + --- Get the @{Core.Zone#ZONE_UNIT} of a detection area using a given numeric index. -- @param #DETECTION_BASE self -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem. -- @return Core.Zone#ZONE_UNIT DetectedZone function DETECTION_BASE:GetDetectedItemZone( DetectedItem ) - + local DetectedZone = DetectedItem and DetectedItem.Zone if DetectedZone then return DetectedZone end - + local Detected - + return nil end - end + end --- Lock the detected items when created and lock all existing detected items. -- @param #DETECTION_BASE self @@ -59065,7 +66177,6 @@ do -- DETECTION_BASE return self end - --- Unlock the detected items when created and unlock all existing detected items. -- @param #DETECTION_BASE self -- @return #DETECTION_BASE @@ -59078,7 +66189,7 @@ do -- DETECTION_BASE return self end - + --- Validate if the detected item is locked. -- @param #DETECTION_BASE self -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem. @@ -59088,7 +66199,6 @@ do -- DETECTION_BASE return self.Locking and DetectedItem.Locked == true end - --- Lock a detected item. -- @param #DETECTION_BASE self @@ -59112,9 +66222,6 @@ do -- DETECTION_BASE return self end - - - --- Set the detected item coordinate. -- @param #DETECTION_BASE self -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem to set the coordinate at. @@ -59123,7 +66230,7 @@ do -- DETECTION_BASE -- @return #DETECTION_BASE function DETECTION_BASE:SetDetectedItemCoordinate( DetectedItem, Coordinate, DetectedItemUnit ) self:F( { Coordinate = Coordinate } ) - + if DetectedItem then if DetectedItemUnit then DetectedItem.Coordinate = Coordinate @@ -59134,18 +66241,17 @@ do -- DETECTION_BASE end end - --- Get the detected item coordinate. -- @param #DETECTION_BASE self -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem to set the coordinate at. -- @return Core.Point#COORDINATE function DETECTION_BASE:GetDetectedItemCoordinate( DetectedItem ) self:F( { DetectedItem = DetectedItem } ) - + if DetectedItem then return DetectedItem.Coordinate end - + return nil end @@ -59153,30 +66259,28 @@ do -- DETECTION_BASE -- @param #DETECTION_BASE self -- @return #table A table of Core.Point#COORDINATE function DETECTION_BASE:GetDetectedItemCoordinates() - + local Coordinates = {} - + for DetectedItemID, DetectedItem in pairs( self:GetDetectedItems() ) do Coordinates[DetectedItem] = self:GetDetectedItemCoordinate( DetectedItem ) end - + return Coordinates end - --- Set the detected item threatlevel. + --- Set the detected item threat level. -- @param #DETECTION_BASE self - -- @param #DETECTION_BASE.DetectedItem The DetectedItem to calculate the threatlevel for. + -- @param #DETECTION_BASE.DetectedItem The DetectedItem to calculate the threat level for. -- @return #DETECTION_BASE function DETECTION_BASE:SetDetectedItemThreatLevel( DetectedItem ) - + local DetectedSet = DetectedItem.Set - + if DetectedItem then DetectedItem.ThreatLevel, DetectedItem.ThreatText = DetectedSet:CalculateThreatLevelA2G() end end - - --- Get the detected item coordinate. -- @param #DETECTION_BASE self @@ -59184,15 +66288,14 @@ do -- DETECTION_BASE -- @return #number ThreatLevel function DETECTION_BASE:GetDetectedItemThreatLevel( DetectedItem ) self:F( { DetectedItem = DetectedItem } ) - + if DetectedItem then self:F( { ThreatLevel = DetectedItem.ThreatLevel, ThreatText = DetectedItem.ThreatText } ) return DetectedItem.ThreatLevel or 0, DetectedItem.ThreatText or "" end - + return nil, "" end - --- Report summary of a detected item using a given numeric index. -- @param #DETECTION_BASE self @@ -59204,8 +66307,8 @@ do -- DETECTION_BASE self:F() return nil end - - --- Report detailed of a detectedion result. + + --- Report detailed of a detection result. -- @param #DETECTION_BASE self -- @param Wrapper.Group#GROUP AttackGroup The group to generate the report for. -- @return #string @@ -59213,25 +66316,25 @@ do -- DETECTION_BASE self:F() return nil end - + --- Get the Detection Set. -- @param #DETECTION_BASE self -- @return #DETECTION_BASE self function DETECTION_BASE:GetDetectionSet() - + local DetectionSet = self.DetectionSet return DetectionSet end - - --- Find the nearest Recce of the DetectedItem. + + --- Find the nearest Recce of the DetectedItem. -- @param #DETECTION_BASE self -- @param #DETECTION_BASE.DetectedItem DetectedItem -- @return Wrapper.Unit#UNIT The nearest FAC unit function DETECTION_BASE:NearestRecce( DetectedItem ) - + local NearestRecce = nil local DistanceRecce = 1000000000 -- Units are not further than 1000000 km away from an area :-) - + for RecceGroupName, RecceGroup in pairs( self.DetectionSet:GetSet() ) do if RecceGroup and RecceGroup:IsAlive() then for RecceUnit, RecceUnit in pairs( RecceGroup:GetUnits() ) do @@ -59246,14 +66349,12 @@ do -- DETECTION_BASE end end end - + DetectedItem.NearestFAC = NearestRecce DetectedItem.DistanceRecce = DistanceRecce - + end - - - + --- Schedule the DETECTION construction. -- @param #DETECTION_BASE self -- @param #number DelayTime The delay in seconds to wait the reporting. @@ -59261,10 +66362,10 @@ do -- DETECTION_BASE -- @return #DETECTION_BASE self function DETECTION_BASE:Schedule( DelayTime, RepeatInterval ) self:F2() - + self.ScheduleDelayTime = DelayTime self.ScheduleRepeatInterval = RepeatInterval - + self.DetectionScheduler = SCHEDULER:New( self, self._DetectionScheduler, { self, "Detection" }, DelayTime, RepeatInterval ) return self end @@ -59278,31 +66379,31 @@ do -- DETECTION_UNITS -- @extends Functional.Detection#DETECTION_BASE --- Will detect units within the battle zone. - -- - -- It will build a DetectedItems list filled with DetectedItems. Each DetectedItem will contain a field Set, which contains a @{Core.Set#SET_UNIT} containing ONE @{UNIT} object reference. - -- Beware that when the amount of units detected is large, the DetectedItems list will be large also. - -- + -- + -- It will build a DetectedItems list filled with DetectedItems. Each DetectedItem will contain a field Set, which contains a @{Core.Set#SET_UNIT} containing ONE @{Wrapper.Unit#UNIT} object reference. + -- Beware that when the amount of units detected is large, the DetectedItems list will be large also. + -- -- @field #DETECTION_UNITS DETECTION_UNITS = { ClassName = "DETECTION_UNITS", DetectionRange = nil, } - + --- DETECTION_UNITS constructor. -- @param Functional.Detection#DETECTION_UNITS self - -- @param Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role. + -- @param Core.Set#SET_GROUP DetectionSetGroup The @{Core.Set} of GROUPs in the Forward Air Controller role. -- @return Functional.Detection#DETECTION_UNITS self function DETECTION_UNITS:New( DetectionSetGroup ) - + -- Inherits from DETECTION_BASE local self = BASE:Inherit( self, DETECTION_BASE:New( DetectionSetGroup ) ) -- #DETECTION_UNITS - + self._SmokeDetectedUnits = false self._FlareDetectedUnits = false self._SmokeDetectedZones = false self._FlareDetectedZones = false self._BoundDetectedZones = false - + return self end @@ -59312,70 +66413,69 @@ do -- DETECTION_UNITS -- @return #string The Changes text function DETECTION_UNITS:GetChangeText( DetectedItem ) self:F( DetectedItem ) - + local MT = {} - + for ChangeCode, ChangeData in pairs( DetectedItem.Changes ) do - + if ChangeCode == "AU" then local MTUT = {} for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do - if ChangeUnitType ~= "ID" then - MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType + if ChangeUnitType ~= "ID" then + MTUT[#MTUT + 1] = ChangeUnitCount .. " of " .. ChangeUnitType end end - MT[#MT+1] = " New target(s) detected: " .. table.concat( MTUT, ", " ) .. "." + MT[#MT + 1] = " New target(s) detected: " .. table.concat( MTUT, ", " ) .. "." end - + if ChangeCode == "RU" then local MTUT = {} for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do - if ChangeUnitType ~= "ID" then - MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType + if ChangeUnitType ~= "ID" then + MTUT[#MTUT + 1] = ChangeUnitCount .. " of " .. ChangeUnitType end end - MT[#MT+1] = " Invisible or destroyed target(s): " .. table.concat( MTUT, ", " ) .. "." + MT[#MT + 1] = " Invisible or destroyed target(s): " .. table.concat( MTUT, ", " ) .. "." end - + end - + return table.concat( MT, "\n" ) - + end - - - --- Create the DetectedItems list from the DetectedObjects table. + + --- Create the DetectedItems list from the DetectedObjects table. -- For each DetectedItem, a one field array is created containing the Unit detected. -- @param #DETECTION_UNITS self -- @return #DETECTION_UNITS self function DETECTION_UNITS:CreateDetectionItems() -- Loop the current detected items, and check if each object still exists and is detected. - + for DetectedItemKey, _DetectedItem in pairs( self.DetectedItems ) do - local DetectedItem=_DetectedItem --#DETECTION_BASE.DetectedItem - + local DetectedItem = _DetectedItem -- #DETECTION_BASE.DetectedItem + local DetectedItemSet = DetectedItem.Set -- Core.Set#SET_UNIT - + for DetectedUnitName, DetectedUnitData in pairs( DetectedItemSet:GetSet() ) do local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT local DetectedObject = nil - --self:F( DetectedUnit ) + -- self:F( DetectedUnit ) if DetectedUnit:IsAlive() then - --self:F(DetectedUnit:GetName()) + -- self:F(DetectedUnit:GetName()) DetectedObject = self:GetDetectedObject( DetectedUnit:GetName() ) end if DetectedObject then - + -- Yes, the DetectedUnit is still detected or exists. Flag as identified. self:IdentifyDetectedObject( DetectedObject ) - + self:F( { "**DETECTED**", IsVisible = DetectedObject.IsVisible } ) -- Update the detection with the new data provided. - DetectedItem.TypeName = DetectedUnit:GetTypeName() - DetectedItem.CategoryName = DetectedUnit:GetCategoryName() + DetectedItem.TypeName = DetectedUnit:GetTypeName() + DetectedItem.CategoryName = DetectedUnit:GetCategoryName() DetectedItem.Name = DetectedObject.Name - DetectedItem.IsVisible = DetectedObject.IsVisible + DetectedItem.IsVisible = DetectedObject.IsVisible DetectedItem.LastTime = DetectedObject.LastTime DetectedItem.LastPos = DetectedObject.LastPos DetectedItem.LastVelocity = DetectedObject.LastVelocity @@ -59395,25 +66495,24 @@ do -- DETECTION_UNITS end end - -- Now we need to loop through the unidentified detected units and add these... These are all new items. for DetectedUnitName, DetectedObjectData in pairs( self.DetectedObjects ) do - + local DetectedObject = self:GetDetectedObject( DetectedUnitName ) if DetectedObject then self:T( { "Detected Unit #", DetectedUnitName } ) - + local DetectedUnit = UNIT:FindByName( DetectedUnitName ) -- Wrapper.Unit#UNIT - + if DetectedUnit then local DetectedTypeName = DetectedUnit:GetTypeName() local DetectedItem = self:GetDetectedItemByKey( DetectedUnitName ) if not DetectedItem then self:T( "Added new DetectedItem" ) DetectedItem = self:AddDetectedItem( "UNIT", DetectedUnitName ) - DetectedItem.TypeName = DetectedUnit:GetTypeName() + DetectedItem.TypeName = DetectedUnit:GetTypeName() DetectedItem.Name = DetectedObject.Name - DetectedItem.IsVisible = DetectedObject.IsVisible + DetectedItem.IsVisible = DetectedObject.IsVisible DetectedItem.LastTime = DetectedObject.LastTime DetectedItem.LastPos = DetectedObject.LastPos DetectedItem.LastVelocity = DetectedObject.LastVelocity @@ -59421,18 +66520,18 @@ do -- DETECTION_UNITS DetectedItem.KnowDistance = DetectedObject.KnowDistance DetectedItem.Distance = DetectedObject.Distance end - + DetectedItem.Set:AddUnit( DetectedUnit ) self:AddChangeUnit( DetectedItem, "AU", DetectedTypeName ) end - end + end end - + for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do - + local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem local DetectedSet = DetectedItem.Set - + -- Set the last known coordinate. local DetectedFirstUnit = DetectedSet:GetFirst() local DetectedFirstUnitCoord = DetectedFirstUnit:GetCoordinate() @@ -59441,28 +66540,28 @@ do -- DETECTION_UNITS self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSet } ) -- Fill the Friendlies table self:SetDetectedItemThreatLevel( DetectedItem ) self:NearestRecce( DetectedItem ) - + end - + end - --- Report summary of a DetectedItem using a given numeric index. -- @param #DETECTION_UNITS self -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem. -- @param Wrapper.Group#GROUP AttackGroup The group to generate the report for. -- @param Core.Settings#SETTINGS Settings Message formatting settings to use. + -- @param #boolean ForceA2GCoordinate Set creation of A2G coordinate -- @return Core.Report#REPORT The report of the detection items. - function DETECTION_UNITS:DetectedItemReportSummary( DetectedItem, AttackGroup, Settings ) + function DETECTION_UNITS:DetectedItemReportSummary( DetectedItem, AttackGroup, Settings, ForceA2GCoordinate ) self:F( { DetectedItem = DetectedItem } ) - + local DetectedItemID = self:GetDetectedItemID( DetectedItem ) - + if DetectedItem then local ReportSummary = "" local UnitDistanceText = "" local UnitCategoryText = "" - + if DetectedItem.KnowType then local UnitCategoryName = DetectedItem.CategoryName if UnitCategoryName then @@ -59474,7 +66573,7 @@ do -- DETECTION_UNITS else UnitCategoryText = "Unknown" end - + if DetectedItem.KnowDistance then if DetectedItem.IsVisible then UnitDistanceText = " at " .. string.format( "%.2f", DetectedItem.Distance ) .. " km" @@ -59484,33 +66583,36 @@ do -- DETECTION_UNITS UnitDistanceText = " at +/- " .. string.format( "%.0f", DetectedItem.Distance ) .. " km" end end - - --TODO: solve Index reference + + -- TODO: solve Index reference local DetectedItemCoordinate = self:GetDetectedItemCoordinate( DetectedItem ) local DetectedItemCoordText = DetectedItemCoordinate:ToString( AttackGroup, Settings ) - + + if ForceA2GCoordinate then + DetectedItemCoordText = DetectedItemCoordinate:ToStringA2G(AttackGroup,Settings) + end + local ThreatLevelA2G = self:GetDetectedItemThreatLevel( DetectedItem ) - + local Report = REPORT:New() - Report:Add(DetectedItemID .. ", " .. DetectedItemCoordText) - Report:Add( string.format( "Threat: [%s]", string.rep( "■", ThreatLevelA2G ), string.rep( "□", 10-ThreatLevelA2G ) ) ) - Report:Add( string.format("Type: %s%s", UnitCategoryText, UnitDistanceText ) ) - Report:Add( string.format("Visible: %s", DetectedItem.IsVisible and "yes" or "no" ) ) - Report:Add( string.format("Detected: %s", DetectedItem.IsDetected and "yes" or "no" ) ) - Report:Add( string.format("Distance: %s", DetectedItem.KnowDistance and "yes" or "no" ) ) + Report:Add( DetectedItemID .. ", " .. DetectedItemCoordText ) + Report:Add( string.format( "Threat: [%s]", string.rep( "■", ThreatLevelA2G ), string.rep( "□", 10 - ThreatLevelA2G ) ) ) + Report:Add( string.format( "Type: %s%s", UnitCategoryText, UnitDistanceText ) ) + Report:Add( string.format( "Visible: %s", DetectedItem.IsVisible and "yes" or "no" ) ) + Report:Add( string.format( "Detected: %s", DetectedItem.IsDetected and "yes" or "no" ) ) + Report:Add( string.format( "Distance: %s", DetectedItem.KnowDistance and "yes" or "no" ) ) return Report end return nil end - --- Report detailed of a detection result. -- @param #DETECTION_UNITS self -- @param Wrapper.Group#GROUP AttackGroup The group to generate the report for. -- @return #string function DETECTION_UNITS:DetectedReportDetailed( AttackGroup ) self:F() - + local Report = REPORT:New() for DetectedItemIndex, DetectedItem in pairs( self.DetectedItems ) do local DetectedItem = DetectedItem -- #DETECTION_BASE.DetectedItem @@ -59518,9 +66620,9 @@ do -- DETECTION_UNITS Report:SetTitle( "Detected units:" ) Report:Add( ReportSummary:Text() ) end - + local ReportText = Report:Text() - + return ReportText end @@ -59532,31 +66634,31 @@ do -- DETECTION_TYPES -- @extends Functional.Detection#DETECTION_BASE --- Will detect units within the battle zone. - -- It will build a DetectedItems[] list filled with DetectedItems, grouped by the type of units detected. - -- Each DetectedItem will contain a field Set, which contains a @{Core.Set#SET_UNIT} containing ONE @{UNIT} object reference. - -- Beware that when the amount of different types detected is large, the DetectedItems[] list will be large also. - -- + -- It will build a DetectedItems[] list filled with DetectedItems, grouped by the type of units detected. + -- Each DetectedItem will contain a field Set, which contains a @{Core.Set#SET_UNIT} containing ONE @{Wrapper.Unit#UNIT} object reference. + -- Beware that when the amount of different types detected is large, the DetectedItems[] list will be large also. + -- -- @field #DETECTION_TYPES DETECTION_TYPES = { ClassName = "DETECTION_TYPES", DetectionRange = nil, } - + --- DETECTION_TYPES constructor. -- @param Functional.Detection#DETECTION_TYPES self - -- @param Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Recce role. + -- @param Core.Set#SET_GROUP DetectionSetGroup The @{Core.Set} of GROUPs in the Recce role. -- @return Functional.Detection#DETECTION_TYPES self function DETECTION_TYPES:New( DetectionSetGroup ) - + -- Inherits from DETECTION_BASE local self = BASE:Inherit( self, DETECTION_BASE:New( DetectionSetGroup ) ) -- #DETECTION_TYPES - + self._SmokeDetectedUnits = false self._FlareDetectedUnits = false self._SmokeDetectedZones = false self._FlareDetectedZones = false self._BoundDetectedZones = false - + return self end @@ -59566,61 +66668,60 @@ do -- DETECTION_TYPES -- @return #string The Changes text function DETECTION_TYPES:GetChangeText( DetectedItem ) self:F( DetectedItem ) - + local MT = {} - + for ChangeCode, ChangeData in pairs( DetectedItem.Changes ) do - + if ChangeCode == "AU" then local MTUT = {} for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do - if ChangeUnitType ~= "ID" then - MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType + if ChangeUnitType ~= "ID" then + MTUT[#MTUT + 1] = ChangeUnitCount .. " of " .. ChangeUnitType end end - MT[#MT+1] = " New target(s) detected: " .. table.concat( MTUT, ", " ) .. "." + MT[#MT + 1] = " New target(s) detected: " .. table.concat( MTUT, ", " ) .. "." end - + if ChangeCode == "RU" then local MTUT = {} for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do - if ChangeUnitType ~= "ID" then - MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType + if ChangeUnitType ~= "ID" then + MTUT[#MTUT + 1] = ChangeUnitCount .. " of " .. ChangeUnitType end end - MT[#MT+1] = " Invisible or destroyed target(s): " .. table.concat( MTUT, ", " ) .. "." + MT[#MT + 1] = " Invisible or destroyed target(s): " .. table.concat( MTUT, ", " ) .. "." end - + end - + return table.concat( MT, "\n" ) - + end - - - --- Create the DetectedItems list from the DetectedObjects table. + + --- Create the DetectedItems list from the DetectedObjects table. -- For each DetectedItem, a one field array is created containing the Unit detected. -- @param #DETECTION_TYPES self -- @return #DETECTION_TYPES self function DETECTION_TYPES:CreateDetectionItems() - + -- Loop the current detected items, and check if each object still exists and is detected. - + for DetectedItemKey, DetectedItem in pairs( self.DetectedItems ) do - + local DetectedItemSet = DetectedItem.Set -- Core.Set#SET_UNIT local DetectedTypeName = DetectedItem.TypeName - + for DetectedUnitName, DetectedUnitData in pairs( DetectedItemSet:GetSet() ) do local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT local DetectedObject = nil if DetectedUnit:IsAlive() then - --self:F(DetectedUnit:GetName()) + -- self:F(DetectedUnit:GetName()) DetectedObject = self:GetDetectedObject( DetectedUnit:GetName() ) end if DetectedObject then - + -- Yes, the DetectedUnit is still detected or exists. Flag as identified. self:IdentifyDetectedObject( DetectedObject ) else @@ -59636,16 +66737,15 @@ do -- DETECTION_TYPES end end - -- Now we need to loop through the unidentified detected units and add these... These are all new items. for DetectedUnitName, DetectedObjectData in pairs( self.DetectedObjects ) do - + local DetectedObject = self:GetDetectedObject( DetectedUnitName ) if DetectedObject then self:T( { "Detected Unit #", DetectedUnitName } ) - + local DetectedUnit = UNIT:FindByName( DetectedUnitName ) -- Wrapper.Unit#UNIT - + if DetectedUnit then local DetectedTypeName = DetectedUnit:GetTypeName() local DetectedItem = self:GetDetectedItemByKey( DetectedTypeName ) @@ -59653,21 +66753,19 @@ do -- DETECTION_TYPES DetectedItem = self:AddDetectedItem( "TYPE", DetectedTypeName ) DetectedItem.TypeName = DetectedTypeName end - + DetectedItem.Set:AddUnit( DetectedUnit ) self:AddChangeUnit( DetectedItem, "AU", DetectedTypeName ) end - end + end end - - -- Check if there are any friendlies nearby. for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do - + local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem local DetectedSet = DetectedItem.Set - + -- Set the last known coordinate. local DetectedFirstUnit = DetectedSet:GetFirst() local DetectedUnitCoord = DetectedFirstUnit:GetCoordinate() @@ -59677,8 +66775,6 @@ do -- DETECTION_TYPES self:SetDetectedItemThreatLevel( DetectedItem ) self:NearestRecce( DetectedItem ) end - - end @@ -59690,35 +66786,35 @@ do -- DETECTION_TYPES -- @return Core.Report#REPORT The report of the detection items. function DETECTION_TYPES:DetectedItemReportSummary( DetectedItem, AttackGroup, Settings ) self:F( { DetectedItem = DetectedItem } ) - + local DetectedSet = self:GetDetectedItemSet( DetectedItem ) local DetectedItemID = self:GetDetectedItemID( DetectedItem ) - + self:T( DetectedItem ) if DetectedItem then local ThreatLevelA2G = self:GetDetectedItemThreatLevel( DetectedItem ) local DetectedItemsCount = DetectedSet:Count() local DetectedItemType = DetectedItem.TypeName - + local DetectedItemCoordinate = self:GetDetectedItemCoordinate( DetectedItem ) local DetectedItemCoordText = DetectedItemCoordinate:ToString( AttackGroup, Settings ) local Report = REPORT:New() - Report:Add(DetectedItemID .. ", " .. DetectedItemCoordText) - Report:Add( string.format( "Threat: [%s%s]", string.rep( "■", ThreatLevelA2G ), string.rep( "□", 10-ThreatLevelA2G ) ) ) - Report:Add( string.format("Type: %2d of %s", DetectedItemsCount, DetectedItemType ) ) + Report:Add( DetectedItemID .. ", " .. DetectedItemCoordText ) + Report:Add( string.format( "Threat: [%s%s]", string.rep( "■", ThreatLevelA2G ), string.rep( "□", 10 - ThreatLevelA2G ) ) ) + Report:Add( string.format( "Type: %2d of %s", DetectedItemsCount, DetectedItemType ) ) return Report end end - + --- Report detailed of a detection result. -- @param #DETECTION_TYPES self -- @param Wrapper.Group#GROUP AttackGroup The group to generate the report for. -- @return #string function DETECTION_TYPES:DetectedReportDetailed( AttackGroup ) self:F() - + local Report = REPORT:New() for DetectedItemIndex, DetectedItem in pairs( self.DetectedItems ) do local DetectedItem = DetectedItem -- #DETECTION_BASE.DetectedItem @@ -59726,81 +66822,121 @@ do -- DETECTION_TYPES Report:SetTitle( "Detected types:" ) Report:Add( ReportSummary:Text() ) end - + local ReportText = Report:Text() - + return ReportText end end - do -- DETECTION_AREAS --- @type DETECTION_AREAS -- @field DCS#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target. - -- @field #DETECTION_BASE.DetectedItems DetectedItems A list of areas containing the set of @{Wrapper.Unit}s, @{Zone}s, the center @{Wrapper.Unit} within the zone, and ID of each area that was detected within a DetectionZoneRange. + -- @field #DETECTION_BASE.DetectedItems DetectedItems A list of areas containing the set of @{Wrapper.Unit}s, @{Core.Zone}s, the center @{Wrapper.Unit} within the zone, and ID of each area that was detected within a DetectionZoneRange. -- @extends Functional.Detection#DETECTION_BASE - --- Detect units within the battle zone for a list of @{Wrapper.Group}s detecting targets following (a) detection method(s), + --- Detect units within the battle zone for a list of @{Wrapper.Group}s detecting targets following (a) detection method(s), -- and will build a list (table) of @{Core.Set#SET_UNIT}s containing the @{Wrapper.Unit#UNIT}s detected. -- The class is group the detected units within zones given a DetectedZoneRange parameter. -- A set with multiple detected zones will be created as there are groups of units detected. - -- + -- -- ## 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 - -- + -- -- Use the methods @{Functional.Detection#DETECTION_AREAS.FlareDetectedUnits}() or @{Functional.Detection#DETECTION_AREAS.SmokeDetectedUnits}() to flare or smoke the detected units when a new detection has taken place. - -- + -- -- ## 4.5) Flare or Smoke or Bound detected zones - -- + -- -- Use the methods: - -- - -- * @{Functional.Detection#DETECTION_AREAS.FlareDetectedZones}() to flare in a color + -- + -- * @{Functional.Detection#DETECTION_AREAS.FlareDetectedZones}() to flare in a color -- * @{Functional.Detection#DETECTION_AREAS.SmokeDetectedZones}() to smoke in a color -- * @{Functional.Detection#DETECTION_AREAS.SmokeDetectedZones}() to bound with a tire with a white flag - -- + -- -- the detected zones when a new detection has taken place. - -- + -- -- @field #DETECTION_AREAS DETECTION_AREAS = { ClassName = "DETECTION_AREAS", DetectionZoneRange = nil, } - - + --- DETECTION_AREAS constructor. -- @param #DETECTION_AREAS self - -- @param Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role. + -- @param Core.Set#SET_GROUP DetectionSetGroup The @{Core.Set} of GROUPs in the Forward Air Controller role. -- @param DCS#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target. -- @return #DETECTION_AREAS function DETECTION_AREAS:New( DetectionSetGroup, DetectionZoneRange ) - + -- Inherits from DETECTION_BASE local self = BASE:Inherit( self, DETECTION_BASE:New( DetectionSetGroup ) ) - + self.DetectionZoneRange = DetectionZoneRange - + self._SmokeDetectedUnits = false self._FlareDetectedUnits = false self._SmokeDetectedZones = false self._FlareDetectedZones = false self._BoundDetectedZones = false - + return self end - + --- Retrieve set of detected zones. + -- @param #DETECTION_AREAS self + -- @return Core.Set#SET_ZONE The @{Core.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. @@ -59809,26 +66945,26 @@ do -- DETECTION_AREAS -- @return Core.Report#REPORT The report of the detection items. function DETECTION_AREAS:DetectedItemReportMenu( DetectedItem, AttackGroup, Settings ) self:F( { DetectedItem = DetectedItem } ) - + local DetectedItemID = self:GetDetectedItemID( DetectedItem ) - + if DetectedItem then local DetectedSet = self:GetDetectedItemSet( DetectedItem ) local ReportSummaryItem - + local DetectedZone = self:GetDetectedItemZone( DetectedItem ) local DetectedItemCoordinate = DetectedZone:GetCoordinate() local DetectedItemCoordText = DetectedItemCoordinate:ToString( AttackGroup, Settings ) local ThreatLevelA2G = self:GetDetectedItemThreatLevel( DetectedItem ) - + local Report = REPORT:New() Report:Add( DetectedItemID ) - Report:Add( string.format( "Threat: [%s%s]", string.rep( "■", ThreatLevelA2G ), string.rep( "□", 10-ThreatLevelA2G ) ) ) - + Report:Add( string.format( "Threat: [%s%s]", string.rep( "■", ThreatLevelA2G ), string.rep( "□", 10 - ThreatLevelA2G ) ) ) + return Report end - + return nil end @@ -59840,14 +66976,14 @@ do -- DETECTION_AREAS -- @return Core.Report#REPORT The report of the detection items. function DETECTION_AREAS:DetectedItemReportSummary( DetectedItem, AttackGroup, Settings ) self:F( { DetectedItem = DetectedItem } ) - + local DetectedItemID = self:GetDetectedItemID( DetectedItem ) - + if DetectedItem then local DetectedSet = self:GetDetectedItemSet( DetectedItem ) local ReportSummaryItem - - --local DetectedZone = self:GetDetectedItemZone( DetectedItem ) + + -- local DetectedZone = self:GetDetectedItemZone( DetectedItem ) local DetectedItemCoordinate = self:GetDetectedItemCoordinate( DetectedItem ) local DetectedAir = DetectedSet:HasAirUnits() local DetectedAltitude = self:GetDetectedItemCoordinate( DetectedItem ) @@ -59857,21 +66993,20 @@ do -- DETECTION_AREAS else DetectedItemCoordText = DetectedItemCoordinate:ToStringA2G( AttackGroup, Settings ) end - local ThreatLevelA2G = self:GetDetectedItemThreatLevel( DetectedItem ) local DetectedItemsCount = DetectedSet:Count() local DetectedItemsTypes = DetectedSet:GetTypeNames() - + local Report = REPORT:New() - Report:Add(DetectedItemID .. ", " .. DetectedItemCoordText) - Report:Add( string.format( "Threat: [%s%s]", string.rep( "■", ThreatLevelA2G ), string.rep( "□", 10-ThreatLevelA2G ) ) ) - Report:Add( string.format("Type: %2d of %s", DetectedItemsCount, DetectedItemsTypes ) ) - --Report:Add( string.format("Detected: %s", DetectedItem.IsDetected and "yes" or "no" ) ) - + Report:Add( DetectedItemID .. ", " .. DetectedItemCoordText ) + Report:Add( string.format( "Threat: [%s%s]", string.rep( "■", ThreatLevelA2G ), string.rep( "□", 10 - ThreatLevelA2G ) ) ) + Report:Add( string.format( "Type: %2d of %s", DetectedItemsCount, DetectedItemsTypes ) ) + -- Report:Add( string.format("Detected: %s", DetectedItem.IsDetected and "yes" or "no" ) ) + return Report end - + return nil end @@ -59879,9 +67014,9 @@ do -- DETECTION_AREAS -- @param #DETECTION_AREAS self -- @param Wrapper.Group#GROUP AttackGroup The group to generate the report for. -- @return #string - function DETECTION_AREAS:DetectedReportDetailed( AttackGroup ) --R2.1 Fixed missing report + function DETECTION_AREAS:DetectedReportDetailed( AttackGroup ) -- R2.1 Fixed missing report self:F() - + local Report = REPORT:New() for DetectedItemIndex, DetectedItem in pairs( self.DetectedItems ) do local DetectedItem = DetectedItem -- #DETECTION_BASE.DetectedItem @@ -59889,13 +67024,12 @@ do -- DETECTION_AREAS Report:SetTitle( "Detected areas:" ) Report:Add( ReportSummary:Text() ) end - + local ReportText = Report:Text() - + return ReportText end - --- Calculate the optimal intercept point of the DetectedItem. -- @param #DETECTION_AREAS self -- @param #DETECTION_BASE.DetectedItem DetectedItem @@ -59908,56 +67042,54 @@ do -- DETECTION_AREAS if self.Intercept then local DetectedSet = DetectedItem.Set -- todo: speed - + local TranslateDistance = DetectedSpeed * self.InterceptDelay - + local InterceptCoord = DetectedCoord:Translate( TranslateDistance, DetectedHeading ) - + DetectedItem.InterceptCoord = InterceptCoord else DetectedItem.InterceptCoord = DetectedCoord end - + end - - --- Smoke the detected units -- @param #DETECTION_AREAS self -- @return #DETECTION_AREAS self function DETECTION_AREAS:SmokeDetectedUnits() self:F2() - + self._SmokeDetectedUnits = true return self end - + --- Flare the detected units -- @param #DETECTION_AREAS self -- @return #DETECTION_AREAS self function DETECTION_AREAS:FlareDetectedUnits() self:F2() - + self._FlareDetectedUnits = true return self end - + --- Smoke the detected zones -- @param #DETECTION_AREAS self -- @return #DETECTION_AREAS self function DETECTION_AREAS:SmokeDetectedZones() self:F2() - + self._SmokeDetectedZones = true return self end - + --- Flare the detected zones -- @param #DETECTION_AREAS self -- @return #DETECTION_AREAS self function DETECTION_AREAS:FlareDetectedZones() self:F2() - + self._FlareDetectedZones = true return self end @@ -59967,127 +67099,123 @@ do -- DETECTION_AREAS -- @return #DETECTION_AREAS self function DETECTION_AREAS:BoundDetectedZones() self:F2() - + self._BoundDetectedZones = true return self end - + --- Make text documenting the changes of the detected zone. -- @param #DETECTION_AREAS self -- @param #DETECTION_BASE.DetectedItem DetectedItem -- @return #string The Changes text function DETECTION_AREAS:GetChangeText( DetectedItem ) self:F( DetectedItem ) - + local MT = {} - + for ChangeCode, ChangeData in pairs( DetectedItem.Changes ) do - + if ChangeCode == "AA" then - MT[#MT+1] = "Detected new area " .. ChangeData.ID .. ". The center target is a " .. ChangeData.ItemUnitType .. "." + MT[#MT + 1] = "Detected new area " .. ChangeData.ID .. ". The center target is a " .. ChangeData.ItemUnitType .. "." end - + if ChangeCode == "RAU" then - MT[#MT+1] = "Changed area " .. ChangeData.ID .. ". Removed the center target." + MT[#MT + 1] = "Changed area " .. ChangeData.ID .. ". Removed the center target." end - + if ChangeCode == "AAU" then - MT[#MT+1] = "Changed area " .. ChangeData.ID .. ". The new center target is a " .. ChangeData.ItemUnitType .. "." + MT[#MT + 1] = "Changed area " .. ChangeData.ID .. ". The new center target is a " .. ChangeData.ItemUnitType .. "." end - + if ChangeCode == "RA" then - MT[#MT+1] = "Removed old area " .. ChangeData.ID .. ". No more targets in this area." + MT[#MT + 1] = "Removed old area " .. ChangeData.ID .. ". No more targets in this area." end - + if ChangeCode == "AU" then local MTUT = {} for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do - if ChangeUnitType ~= "ID" then - MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType + if ChangeUnitType ~= "ID" then + MTUT[#MTUT + 1] = ChangeUnitCount .. " of " .. ChangeUnitType end end - MT[#MT+1] = "Detected for area " .. ChangeData.ID .. " new target(s) " .. table.concat( MTUT, ", " ) .. "." + MT[#MT + 1] = "Detected for area " .. ChangeData.ID .. " new target(s) " .. table.concat( MTUT, ", " ) .. "." end - + if ChangeCode == "RU" then local MTUT = {} for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do - if ChangeUnitType ~= "ID" then - MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType + if ChangeUnitType ~= "ID" then + MTUT[#MTUT + 1] = ChangeUnitCount .. " of " .. ChangeUnitType end end - MT[#MT+1] = "Removed for area " .. ChangeData.ID .. " invisible or destroyed target(s) " .. table.concat( MTUT, ", " ) .. "." + MT[#MT + 1] = "Removed for area " .. ChangeData.ID .. " invisible or destroyed target(s) " .. table.concat( MTUT, ", " ) .. "." end - + end - + return table.concat( MT, "\n" ) - + end - - - --- Make a DetectionSet table. This function will be overridden in the derived clsses. + + --- Make a DetectionSet table. This function will be overridden in the derived classes. -- @param #DETECTION_AREAS self -- @return #DETECTION_AREAS self function DETECTION_AREAS:CreateDetectionItems() - - + self:F( "Checking Detected Items for new Detected Units ..." ) - --self:F( { DetectedObjects = self.DetectedObjects } ) - + -- self:F( { DetectedObjects = self.DetectedObjects } ) + -- First go through all detected sets, and check if there are new detected units, match all existing detected units and identify undetected units. -- Regroup when needed, split groups when needed. for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do - + local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem - + if DetectedItem then - + self:T2( { "Detected Item ID: ", DetectedItemID } ) - + local DetectedSet = DetectedItem.Set - + local AreaExists = false -- This flag will determine of the detected area is still existing. - + -- First test if the center unit is detected in the detection area. self:T3( { "Zone Center Unit:", DetectedItem.Zone.ZoneUNIT.UnitName } ) local DetectedZoneObject = self:GetDetectedObject( DetectedItem.Zone.ZoneUNIT.UnitName ) self:T3( { "Detected Zone Object:", DetectedItem.Zone:GetName(), DetectedZoneObject } ) - + if DetectedZoneObject then - - --self:IdentifyDetectedObject( DetectedZoneObject ) + + -- self:IdentifyDetectedObject( DetectedZoneObject ) AreaExists = true - - - + else -- The center object of the detected area has not been detected. Find an other unit of the set to become the center of the area. -- First remove the center unit from the set. DetectedSet:RemoveUnitsByName( DetectedItem.Zone.ZoneUNIT.UnitName ) - + self:AddChangeItem( DetectedItem, 'RAU', self:GetDetectedUnitTypeName( DetectedItem.Zone.ZoneUNIT ) ) - + -- Then search for a new center area unit within the set. Note that the new area unit candidate must be within the area range. for DetectedUnitName, DetectedUnitData in pairs( DetectedSet:GetSet() ) do - + local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT - local DetectedObject = self:GetDetectedObject( DetectedUnit.UnitName ) + local DetectedObject = self:GetDetectedObject( DetectedUnit.UnitName ) local DetectedUnitTypeName = self:GetDetectedUnitTypeName( DetectedUnit ) - + -- The DetectedObject can be nil when the DetectedUnit is not alive anymore or it is not in the DetectedObjects map. -- If the DetectedUnit was already identified, DetectedObject will be nil. if DetectedObject then self:IdentifyDetectedObject( DetectedObject ) AreaExists = true - - --DetectedItem.Zone:BoundZone( 12, self.CountryID, true) - + + -- DetectedItem.Zone:BoundZone( 12, self.CountryID, true) + -- Assign the Unit as the new center unit of the detected area. DetectedItem.Zone = ZONE_UNIT:New( DetectedUnit:GetName(), DetectedUnit, self.DetectionZoneRange ) - + self:AddChangeItem( DetectedItem, "AAU", DetectedUnitTypeName ) - + -- We don't need to add the DetectedObject to the area set, because it is already there ... break else @@ -60096,30 +67224,30 @@ do -- DETECTION_AREAS end end end - + -- Now we've determined the center unit of the area, now we can iterate the units in the detected area. -- Note that the position of the area may have moved due to the center unit repositioning. -- If no center unit was identified, then the detected area does not exist anymore and should be deleted, as there are no valid units that can be the center unit. if AreaExists then - + -- ok, we found the center unit of the area, now iterate through the detected area set and see which units are still within the center unit zone ... -- Those units within the zone are flagged as Identified. -- If a unit was not found in the set, remove it from the set. This may be added later to other existing or new sets. for DetectedUnitName, DetectedUnitData in pairs( DetectedSet:GetSet() ) do - + local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT local DetectedUnitTypeName = self:GetDetectedUnitTypeName( DetectedUnit ) local DetectedObject = nil if DetectedUnit:IsAlive() then - --self:F(DetectedUnit:GetName()) + -- self:F(DetectedUnit:GetName()) DetectedObject = self:GetDetectedObject( DetectedUnit:GetName() ) end if DetectedObject then -- Check if the DetectedUnit is within the DetectedItem.Zone if DetectedUnit:IsInZone( DetectedItem.Zone ) then - + -- Yes, the DetectedUnit is within the DetectedItem.Zone, no changes, DetectedUnit can be kept within the Set. self:IdentifyDetectedObject( DetectedObject ) DetectedSet:AddUnit( DetectedUnit ) @@ -60129,47 +67257,45 @@ do -- DETECTION_AREAS DetectedSet:Remove( DetectedUnitName ) self:AddChangeUnit( DetectedItem, "RU", DetectedUnitTypeName ) end - + else -- There was no DetectedObject, remove DetectedUnit from the Set. self:AddChangeUnit( DetectedItem, "RU", "destroyed target" ) DetectedSet:Remove( DetectedUnitName ) - + -- The DetectedObject has been identified, because it does not exist ... -- self:IdentifyDetectedObject( DetectedObject ) end end else - --DetectedItem.Zone:BoundZone( 12, self.CountryID, true) + -- DetectedItem.Zone:BoundZone( 12, self.CountryID, true) self:RemoveDetectedItem( DetectedItemID ) self:AddChangeItem( DetectedItem, "RA" ) end end end - - - + -- We iterated through the existing detection areas and: -- - We checked which units are still detected in each detection area. Those units were flagged as Identified. - -- - We recentered the detection area to new center units where it was needed. + -- - We re-centered the detection area to new center units where it was needed. -- -- Now we need to loop through the unidentified detected units and see where they belong: -- - They can be added to a new detection area and become the new center unit. -- - They can be added to a new detection area. for DetectedUnitName, DetectedObjectData in pairs( self.DetectedObjects ) do - + local DetectedObject = self:GetDetectedObject( DetectedUnitName ) - + if DetectedObject then - + -- We found an unidentified unit outside of any existing detection area. local DetectedUnit = UNIT:FindByName( DetectedUnitName ) -- Wrapper.Unit#UNIT local DetectedUnitTypeName = self:GetDetectedUnitTypeName( DetectedUnit ) - + local AddedToDetectionArea = false - + for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do - + local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem if DetectedItem then local DetectedSet = DetectedItem.Set @@ -60181,74 +67307,71 @@ do -- DETECTION_AREAS end end end - + if AddedToDetectionArea == false then - + -- New detection area - local DetectedItem = self:AddDetectedItemZone( "AREA", nil, + local DetectedItem = self:AddDetectedItemZone( "AREA", nil, SET_UNIT:New():FilterDeads():FilterCrashes(), ZONE_UNIT:New( DetectedUnitName, DetectedUnit, self.DetectionZoneRange ) ) - --self:F( DetectedItem.Zone.ZoneUNIT.UnitName ) + -- self:F( DetectedItem.Zone.ZoneUNIT.UnitName ) DetectedItem.Set:AddUnit( DetectedUnit ) self:AddChangeItem( DetectedItem, "AA", DetectedUnitTypeName ) - end + end end end - + -- Now all the tests should have been build, now make some smoke and flares... -- We also report here the friendlies within the detected areas. - + for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do - + local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem local DetectedSet = DetectedItem.Set local DetectedFirstUnit = DetectedSet:GetFirst() local DetectedZone = DetectedItem.Zone - + -- Set the last known coordinate to the detection item. local DetectedZoneCoord = DetectedZone:GetCoordinate() self:SetDetectedItemCoordinate( DetectedItem, DetectedZoneCoord, DetectedFirstUnit ) - + self:CalculateIntercept( DetectedItem ) - + -- We search for friendlies nearby. -- If there weren't any friendlies nearby, and now there are friendlies nearby, we flag the area as "changed". -- If there were friendlies nearby, and now there aren't any friendlies nearby, we flag the area as "changed". -- This is for the A2G dispatcher to detect if there is a change in the tactical situation. local OldFriendliesNearbyGround = self:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSet } ) -- Fill the Friendlies table - local NewFriendliesNearbyGround = self:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) + local NewFriendliesNearbyGround = self:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) if OldFriendliesNearbyGround ~= NewFriendliesNearbyGround then DetectedItem.Changed = true end - self:SetDetectedItemThreatLevel( DetectedItem ) -- Calculate A2G threat level + self:SetDetectedItemThreatLevel( DetectedItem ) -- Calculate A2G threat level self:NearestRecce( DetectedItem ) - if DETECTION_AREAS._SmokeDetectedUnits or self._SmokeDetectedUnits then DetectedZone.ZoneUNIT:SmokeRed() end - - --DetectedSet:Flush( self ) - - DetectedSet:ForEachUnit( - --- @param Wrapper.Unit#UNIT DetectedUnit - function( DetectedUnit ) - if DetectedUnit:IsAlive() then - --self:T( "Detected Set #" .. DetectedItem.ID .. ":" .. DetectedUnit:GetName() ) - if DETECTION_AREAS._FlareDetectedUnits or self._FlareDetectedUnits then - DetectedUnit:FlareGreen() - end - if DETECTION_AREAS._SmokeDetectedUnits or self._SmokeDetectedUnits then - DetectedUnit:SmokeGreen() - end + + -- DetectedSet:Flush( self ) + + DetectedSet:ForEachUnit( --- @param Wrapper.Unit#UNIT DetectedUnit + function( DetectedUnit ) + if DetectedUnit:IsAlive() then + -- self:T( "Detected Set #" .. DetectedItem.ID .. ":" .. DetectedUnit:GetName() ) + if DETECTION_AREAS._FlareDetectedUnits or self._FlareDetectedUnits then + DetectedUnit:FlareGreen() + end + if DETECTION_AREAS._SmokeDetectedUnits or self._SmokeDetectedUnits then + DetectedUnit:SmokeGreen() end end - ) + end ) if DETECTION_AREAS._FlareDetectedZones or self._FlareDetectedZones then - DetectedZone:FlareZone( SMOKECOLOR.White, 30, math.random( 0,90 ) ) + DetectedZone:FlareZone( SMOKECOLOR.White, 30, math.random( 0, 90 ) ) end if DETECTION_AREAS._SmokeDetectedZones or self._SmokeDetectedZones then DetectedZone:SmokeZone( SMOKECOLOR.White, 30 ) @@ -60259,17 +67382,20 @@ do -- DETECTION_AREAS DetectedZone:BoundZone( 12, self.CountryID ) end end - + end - -end - +end + +--- **Functional** - Captures the class DETECTION_ZONES. +-- @module Functional.DetectionZones +-- @image MOOSE.JPG + do -- DETECTION_ZONES --- @type DETECTION_ZONES -- @field DCS#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target. - -- @field #DETECTION_BASE.DetectedItems DetectedItems A list of areas containing the set of @{Wrapper.Unit}s, @{Zone}s, the center @{Wrapper.Unit} within the zone, and ID of each area that was detected within a DetectionZoneRange. + -- @field #DETECTION_BASE.DetectedItems DetectedItems A list of areas containing the set of @{Wrapper.Unit}s, @{Core.Zone}s, the center @{Wrapper.Unit} within the zone, and ID of each area that was detected within a DetectionZoneRange. -- @extends Functional.Detection#DETECTION_BASE --- (old, to be revised ) Detect units within the battle zone for a list of @{Core.Zone}s detecting targets following (a) detection method(s), @@ -60307,27 +67433,27 @@ do -- DETECTION_ZONES ClassName = "DETECTION_ZONES", DetectionZoneRange = nil, } - - + + --- DETECTION_ZONES constructor. -- @param #DETECTION_ZONES self - -- @param Core.Set#SET_ZONE DetectionSetZone The @{Set} of ZONE_RADIUS. + -- @param Core.Set#SET_ZONE DetectionSetZone The @{Core.Set} of ZONE_RADIUS. -- @param DCS#Coalition.side DetectionCoalition The coalition of the detection. -- @return #DETECTION_ZONES function DETECTION_ZONES:New( DetectionSetZone, DetectionCoalition ) - + -- Inherits from DETECTION_BASE local self = BASE:Inherit( self, DETECTION_BASE:New( DetectionSetZone ) ) -- #DETECTION_ZONES - + self.DetectionSetZone = DetectionSetZone -- Core.Set#SET_ZONE self.DetectionCoalition = DetectionCoalition - + self._SmokeDetectedUnits = false self._FlareDetectedUnits = false self._SmokeDetectedZones = false self._FlareDetectedZones = false self._BoundDetectedZones = false - + return self end @@ -60680,7 +67806,7 @@ do -- DETECTION_ZONES return IsDetected end -end --- **Functional** -- Management of target **Designation**. Lase, smoke and illuminate targets. +end --- **Functional** - Management of target **Designation**. Lase, smoke and illuminate targets. -- -- === -- @@ -60730,7 +67856,7 @@ end --- **Functional** -- Management of target **Designation**. Lase, smoke and -- -- ![Banner Image](..\Presentations\DESIGNATE\Dia3.JPG) -- --- A typical mission setup would require Recce (a @{Set} of Recce) to be detecting potential targets. +-- A typical mission setup would require Recce (a @{Core.Set} of Recce) to be detecting potential targets. -- The DetectionObject will group the detected targets based on the detection method being used. -- Possible detection methods could be by Area, by Type or by Unit. -- Each grouping will result in a **TargetGroup**, for terminology and clarity we will use this term throughout the document. @@ -60895,7 +68021,7 @@ do -- DESIGNATE -- In order to prevent an overflow of designations due to many Detected Targets, there is a -- Maximum Designations scope that is set in the DesignationObject. -- - -- The method @{#DESIGNATE.SetMaximumDesignations}() will put a limit on the amount of designations put in scope of the DesignationObject. + -- The method @{#DESIGNATE.SetMaximumDesignations}() will put a limit on the amount of designations (target groups) put in scope of the DesignationObject. -- Using the menu system, the player can "forget" a designation, so that gradually a new designation can be put in scope when detected. -- -- # 4. Laser codes @@ -60958,7 +68084,7 @@ do -- DESIGNATE -- # 7. Designate Menu Location for a Mission -- -- You can make DESIGNATE work for a @{Tasking.Mission#MISSION} object. In this way, the designate menu will not appear in the root of the radio menu, but in the menu of the Mission. - -- Use the method @{#DESIGNATE.SetMission}() to set the @{Mission} object for the designate function. + -- Use the method @{#DESIGNATE.SetMission}() to set the @{Tasking.Mission} object for the designate function. -- -- # 8. Status Report -- @@ -61244,7 +68370,8 @@ do -- DESIGNATE end - --- Set the maximum amount of designations. + --- Set the maximum amount of designations (target groups). This will put a limit on the amount of designations in scope. + -- Using the menu system, the player can "forget" a designation, so that gradually a new designation can be put in scope when detected. -- @param #DESIGNATE self -- @param #number MaximumDesignations -- @return #DESIGNATE @@ -61284,7 +68411,7 @@ do -- DESIGNATE end - --- Set the maximum amount of markings FACs will do, per designated target group. + --- Set the maximum amount of markings FACs will do, per designated target group. This will limit the number of parallelly marked units of a target group. -- @param #DESIGNATE self -- @param #number MaximumMarkings Maximum markings FACs will do, per designated target group. -- @return #DESIGNATE @@ -61593,8 +68720,8 @@ do -- DESIGNATE for DesignateIndex, Designating in pairs( self.Designating ) do local DetectedItem = DetectedItems[DesignateIndex] if DetectedItem then - local Report = self.Detection:DetectedItemReportSummary( DetectedItem, AttackGroup ):Text( ", " ) - DetectedReport:Add( string.rep( "-", 140 ) ) + local Report = self.Detection:DetectedItemReportSummary( DetectedItem, AttackGroup, nil, true ):Text( ", " ) + DetectedReport:Add( string.rep( "-", 40 ) ) DetectedReport:Add( " - " .. Report ) if string.find( Designating, "L" ) then DetectedReport:Add( " - " .. "Lasing Targets" ) @@ -61874,8 +69001,8 @@ do -- DESIGNATE local MarkingCount = 0 local MarkedTypes = {} - local ReportTypes = REPORT:New() - local ReportLaserCodes = REPORT:New() + --local ReportTypes = REPORT:New() + --local ReportLaserCodes = REPORT:New() TargetSetUnit:Flush( self ) @@ -61925,8 +69052,8 @@ do -- DESIGNATE if not Recce then self:F( "Lasing..." ) - self.RecceSet:Flush( self) - + --self.RecceSet:Flush( self) + for RecceGroupID, RecceGroup in pairs( self.RecceSet:GetSet() ) do for UnitID, UnitData in pairs( RecceGroup:GetUnits() or {} ) do @@ -61964,13 +69091,13 @@ do -- DESIGNATE -- OK. We have assigned for the Recce a TargetUnit. We can exit the function. MarkingCount = MarkingCount + 1 local TargetUnitType = TargetUnit:GetTypeName() - --RecceUnit:MessageToSetGroup( "Marking " .. TargetUnit:GetTypeName() .. " with laser " .. RecceUnit:GetSpot().LaserCode .. " for " .. Duration .. "s.", - -- 5, self.AttackSet, DesignateName ) + RecceUnit:MessageToSetGroup( "Marking " .. TargetUnit:GetTypeName() .. " with laser " .. RecceUnit:GetSpot().LaserCode .. " for " .. Duration .. "s.", + 10, self.AttackSet, DesignateName ) if not MarkedTypes[TargetUnitType] then MarkedTypes[TargetUnitType] = true - ReportTypes:Add(TargetUnitType) + --ReportTypes:Add(TargetUnitType) end - ReportLaserCodes:Add(RecceUnit.LaserCode) + --ReportLaserCodes:Add(RecceUnit.LaserCode) return end else @@ -61985,16 +69112,16 @@ do -- DESIGNATE if Recce then Recce:LaseOff() - Recce:MessageToSetGroup( "Target " .. TargetUnit:GetTypeName() "out of LOS. Cancelling lase!", 5, self.AttackSet, self.DesignateName ) + Recce:MessageToSetGroup( "Target " .. TargetUnit:GetTypeName() "out of LOS. Cancelling lase!", 10, self.AttackSet, self.DesignateName ) end else --MarkingCount = MarkingCount + 1 local TargetUnitType = TargetUnit:GetTypeName() if not MarkedTypes[TargetUnitType] then MarkedTypes[TargetUnitType] = true - ReportTypes:Add(TargetUnitType) + --ReportTypes:Add(TargetUnitType) end - ReportLaserCodes:Add(RecceUnit.LaserCode) + --ReportLaserCodes:Add(RecceUnit.LaserCode) end end end @@ -62004,19 +69131,19 @@ do -- DESIGNATE local TargetUnitType = TargetUnit:GetTypeName() if not MarkedTypes[TargetUnitType] then MarkedTypes[TargetUnitType] = true - ReportTypes:Add(TargetUnitType) + --ReportTypes:Add(TargetUnitType) end - ReportLaserCodes:Add(Recce.LaserCode) - --Recce:MessageToSetGroup( self.DesignateName .. ": Marking " .. TargetUnit:GetTypeName() .. " with laser " .. Recce.LaserCode .. ".", 5, self.AttackSet ) + --ReportLaserCodes:Add(Recce.LaserCode) + Recce:MessageToSetGroup( self.DesignateName .. ": Marking " .. TargetUnit:GetTypeName() .. " with laser " .. Recce.LaserCode .. ".", 10, self.AttackSet ) end end end end ) - local MarkedTypesText = ReportTypes:Text(', ') - local MarkedLaserCodesText = ReportLaserCodes:Text(', ') - self.CC:GetPositionable():MessageToSetGroup( "Marking " .. MarkingCount .. " x " .. MarkedTypesText .. ", code " .. MarkedLaserCodesText .. ".", 5, self.AttackSet, self.DesignateName ) + --local MarkedTypesText = ReportTypes:Text(', ') + --local MarkedLaserCodesText = ReportLaserCodes:Text(', ') + --self.CC:GetPositionable():MessageToSetGroup( "Marking " .. MarkingCount .. " x " .. MarkedTypesText .. ", code " .. MarkedLaserCodesText .. ".", 5, self.AttackSet, self.DesignateName ) self:__Lasing( -self.LaseDuration, Index, Duration, LaserCodeRequested ) @@ -62154,15 +69281,15 @@ end --- **Functional** - Create random airtraffic in your missions. --- +-- -- === --- --- The aim of the RAT class is to fill the empty DCS world with randomized air traffic and bring more life to your airports. --- In particular, it is designed to spawn AI air units at random airports. These units will be assigned a random flight path to another random airport on the map. +-- +-- The aim of the RAT class is to fill the empty DCS world with randomized air traffic and bring more life to your airports. +-- In particular, it is designed to spawn AI air units at random airports. These units will be assigned a random flight path to another random airport on the map. -- Even the mission designer will not know where aircraft will be spawned and which route they follow. --- +-- -- ## Features: --- +-- -- * Very simple interface. Just one unit and two lines of Lua code needed to fill your map. -- * High degree of randomization. Aircraft will spawn at random airports, have random routes and random destinations. -- * Specific departure and/or destination airports can be chosen. @@ -62174,39 +69301,39 @@ end -- * Aircraft can report their status during the route. -- * All of the above can be customized by the user if necessary. -- * All current (Caucasus, Nevada, Normandy, Persian Gulf) and future maps are supported. --- +-- -- The RAT class creates an entry in the F10 radio menu which allows to: --- +-- -- * Create new groups on-the-fly, i.e. at run time within the mission, -- * Destroy specific groups (e.g. if they get stuck or damaged and block a runway), -- * Request the status of all RAT aircraft or individual groups, -- * Place markers at waypoints on the F10 map for each group. --- +-- -- Note that by its very nature, this class is suited best for civil or transport aircraft. However, it also works perfectly fine for military aircraft of any kind. --- +-- -- More of the documentation include some simple examples can be found further down this page. --- +-- -- === --- +-- -- ## Missions: -- -- ### [RAT - Random Air Traffic](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/RAT%20-%20Random%20Air%20Traffic) --- +-- -- === --- +-- -- # YouTube Channel --- --- ### [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) +-- +-- ### [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) -- ### [MOOSE - RAT - Random Air Traffic](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0u4Zxywtg-mx_ov4vi68CO) --- +-- -- === --- +-- -- ### Author: **[funkyfranky](https://forums.eagle.ru/member.php?u=115026)** --- +-- -- ### Contributions: [FlightControl](https://forums.eagle.ru/member.php?u=89536) --- +-- -- === --- @module Functional.Rat +-- @module Functional.RAT -- @image RAT.JPG ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -62224,7 +69351,7 @@ end -- @field #string category Category of aircarft: "plane" or "heli". -- @field #number groupsize Number of aircraft in group. -- @field #string friendly Possible departure/destination airport: all=blue+red+neutral, same=spawn+neutral, spawnonly=spawn, blue=blue+neutral, blueonly=blue, red=red+neutral, redonly=red. --- @field #table ctable Table with the valid coalitons from choice self.friendly. +-- @field #table ctable Table with the valid coalitions from choice self.friendly. -- @field #table aircraft Table which holds the basic aircraft properties (speed, range, ...). -- @field #number Vcruisemax Max cruise speed in m/s (250 m/s = 900 km/h = 486 kt) set by user. -- @field #number Vclimb Default climb rate in ft/min. @@ -62263,7 +69390,7 @@ end -- @field #number FLminuser Minimum flight level set by user. -- @field #number FLmaxuser Maximum flight level set by user. -- @field #boolean commute Aircraft commute between departure and destination, i.e. when respawned the departure airport becomes the new destiation. --- @field #boolean starshape If true, aircraft travel A-->B-->A-->C-->A-->D... for commute. +-- @field #boolean starshape If true, aircraft travel A-->B-->A-->C-->A-->D... for commute. -- @field #string homebase Home base for commute and return zone. Aircraft will always return to this base but otherwise travel in a star shaped way. -- @field #boolean continuejourney Aircraft will continue their journey, i.e. get respawned at their destination with a new random destination. -- @field #number ngroups Number of groups to be spawned in total. @@ -62298,7 +69425,7 @@ end -- @field #number onboardnum0 (Optional) Starting value of the automatically appended numbering of aircraft within a flight. Default is 1. -- @field #boolean checkonrunway Aircraft are checked if they were accidentally spawned on the runway. Default is true. -- @field #number onrunwayradius Distance (in meters) from a runway spawn point until a unit is considered to have accidentally been spawned on a runway. Default is 75 m. --- @field #number onrunwaymaxretry Number of respawn retries (on ground) at other airports if a group gets accidentally spawned on the runway. Default is 3. +-- @field #number onrunwaymaxretry Number of respawn retries (on ground) at other airports if a group gets accidentally spawned on the runway. Default is 3. -- @field #boolean checkontop Aircraft are checked if they were accidentally spawned on top of another unit. Default is true. -- @field #number ontopradius Radius in meters until which a unit is considered to be on top of another. Default is 2 m. -- @field Wrapper.Airbase#AIRBASE.TerminalType termtype Type of terminal to be used when spawning at an airbase. @@ -62312,25 +69439,25 @@ end --- Implements an easy to use way to randomly fill your map with AI aircraft. -- -- ## Airport Selection --- +-- -- ![Process](..\Presentations\RAT\RAT_Airport_Selection.png) --- +-- -- ### Default settings: --- +-- -- * By default, aircraft are spawned at airports of their own coalition (blue or red) or neutral airports. -- * Destination airports are by default also of neutral or of the same coalition as the template group of the spawned aircraft. -- * Possible destinations are restricted by their distance to the departure airport. The maximal distance depends on the max range of spawned aircraft type and its initial fuel amount. --- +-- -- ### The default behavior can be changed: --- +-- -- * A specific departure and/or destination airport can be chosen. -- * Valid coalitions can be set, e.g. only red, blue or neutral, all three "colours". -- * It is possible to start in air within a zone defined in the mission editor or within a zone above an airport of the map. --- +-- -- ## Flight Plan --- +-- -- ![Process](..\Presentations\RAT\RAT_Flight_Plan.png) --- +-- -- * A general flight plan has five main airborne segments: Climb, cruise, descent, holding and final approach. -- * Events monitored during the flight are: birth, engine-start, take-off, landing and engine-shutdown. -- * The default flight level (FL) is set to ~FL200, i.e. 20000 feet ASL but randomized for each aircraft. @@ -62342,56 +69469,56 @@ end -- * The altitude of theholding point is ~1200 m AGL. Holding patterns might or might not happen with variable duration. -- * If an aircraft is spawned in air, the procedure omitts taxi and take-off and starts with the climb/cruising part. -- * All values are randomized for each spawned aircraft. --- +-- -- ## Mission Editor Setup --- +-- -- ![Process](..\Presentations\RAT\RAT_Mission_Setup.png) --- +-- -- Basic mission setup is very simple and essentially a three step process: --- +-- -- * Place your aircraft **anywhere** on the map. It really does not matter where you put it. -- * Give the group a good name. In the example above the group is named "RAT_YAK". --- * Activate the "LATE ACTIVATION" tick box. Note that this aircraft will not be spawned itself but serves a template for each RAT aircraft spawned when the mission starts. --- +-- * Activate the "LATE ACTIVATION" tick box. Note that this aircraft will not be spawned itself but serves a template for each RAT aircraft spawned when the mission starts. +-- -- Voilà, your already done! --- +-- -- Optionally, you can set a specific livery for the aircraft or give it some weapons. -- However, the aircraft will by default not engage any enemies. Think of them as beeing on a peaceful or ferry mission. --- +-- -- ## Basic Lua Script --- +-- -- ![Process](..\Presentations\RAT\RAT_Basic_Lua_Script.png) --- +-- -- The basic Lua script for one template group consits of two simple lines as shown in the picture above. --- +-- -- * **Line 2** creates a new RAT object "yak". The only required parameter for the constructor @{#RAT.New}() is the name of the group as defined in the mission editor. In this example it is "RAT_YAK". -- * **Line 5** trigger the command to spawn the aircraft. The (optional) parameter for the @{#RAT.Spawn}() function is the number of aircraft to be spawned of this object. -- By default each of these aircraft gets a random departure airport anywhere on the map and a random destination airport, which lies within range of the of the selected aircraft type. --- +-- -- In this simple example aircraft are respawned with a completely new flightplan when they have reached their destination airport. -- The "old" aircraft is despawned (destroyed) after it has shut-down its engines and a new aircraft of the same type is spawned at a random departure airport anywhere on the map. -- Hence, the default flight plan for a RAT aircraft will be: Fly from airport A to B, get respawned at C and fly to D, get respawned at E and fly to F, ... -- This ensures that you always have a constant number of AI aircraft on your map. --- +-- -- ## Parking Problems --- --- One big issue in DCS is that not all aircraft can be spawned on every airport or airbase. In particular, bigger aircraft might not have a valid parking spot at smaller airports and +-- +-- One big issue in DCS is that not all aircraft can be spawned on every airport or airbase. In particular, bigger aircraft might not have a valid parking spot at smaller airports and -- airstripes. This can lead to multiple problems in DCS. --- +-- -- * Landing: When an aircraft tries to land at an airport where it does not have a valid parking spot, it is immidiately despawned the moment its wheels touch the runway, i.e. -- when a landing event is triggered. This leads to the loss of the RAT aircraft. On possible way to circumvent the this problem is to let another RAT aircraft spawn at landing -- and not when it shuts down its engines. See the @{RAT.RespawnAfterLanding}() function. -- * Spawning: When a big aircraft is dynamically spawned on a small airbase a few things can go wrong. For example, it could be spawned at a parking spot with a shelter. -- Or it could be damaged by a scenery object when it is taxiing out to the runway, or it could overlap with other aircraft on parking spots near by. --- +-- -- You can check yourself if an aircraft has a valid parking spot at an airbase by dragging its group on the airport in the mission editor and set it to start from ramp. -- If it stays at the airport, it has a valid parking spot, if it jumps to another airport, it does not have a valid parking spot on that airbase. --- +-- -- ### Setting the Terminal Type -- Each parking spot has a specific type depending on its size or if a helicopter spot or a shelter etc. The classification is not perfect but it is the best we have. -- If you encounter problems described above, you can request a specific terminal type for the RAT aircraft. This can be done by the @{#RAT.SetTerminalType}(*terminaltype*) -- function. The parameter *terminaltype* can be set as follows --- +-- -- * AIRBASE.TerminalType.HelicopterOnly: Special spots for Helicopers. -- * AIRBASE.TerminalType.Shelter: Hardened Air Shelter. Currently only on Caucaus map. -- * AIRBASE.TerminalType.OpenMed: Open/Shelter air airplane only. @@ -62399,96 +69526,96 @@ end -- * AIRBASE.TerminalType.OpenMedOrBig: Combines OpenMed and OpenBig spots. -- * AIRBASE.TerminalType.HelicopterUsable: Combines HelicopterOnly, OpenMed and OpenBig. -- * AIRBASE.TerminalType.FighterAircraft: Combines Shelter, OpenMed and OpenBig spots. So effectively all spots usable by fixed wing aircraft. --- +-- -- So for example -- c17=RAT:New("C-17") -- c17:SetTerminalType(AIRBASE.TerminalType.OpenBig) -- c17:Spawn(5) --- +-- -- This would randomly spawn five C-17s but only on airports which have big open air parking spots. Note that also only destination airports are allowed -- which do have this type of parking spot. This should ensure that the aircraft is able to land at the destination without beeing despawned immidiately. --- +-- -- Also, the aircraft are spawned only on the requested parking spot types and not on any other type. If no parking spot of this type is availabe at the -- moment of spawning, the group is automatically spawned in air above the selected airport. --- +-- -- ## Examples --- +-- -- Here are a few examples, how you can modify the default settings of RAT class objects. --- +-- -- ### Specify Departure and Destinations --- +-- -- ![Process](..\Presentations\RAT\RAT_Examples_Specify_Departure_and_Destination.png) --- +-- -- In the picture above you find a few possibilities how to modify the default behaviour to spawn at random airports and fly to random destinations. --- +-- -- In particular, you can specify fixed departure and/or destination airports. This is done via the @{#RAT.SetDeparture}() or @{#RAT.SetDestination}() functions, respectively. --- +-- -- * If you only fix a specific departure airport via @{#RAT.SetDeparture}() all aircraft will be spawned at that airport and get random destination airports. -- * If you only fix the destination airport via @{#RAT.SetDestination}(), aircraft a spawned at random departure airports but will all fly to the destination airport. -- * If you fix departure and destination airports, aircraft will only travel from between those airports. --- When the aircraft reaches its destination, it will be respawned at its departure and fly again to its destination. --- +-- When the aircraft reaches its destination, it will be respawned at its departure and fly again to its destination. +-- -- There is also an option that allows aircraft to "continue their journey" from their destination. This is achieved by the @{#RAT.ContinueJourney}() function. -- In that case, when the aircraft arrives at its first destination it will be respawned at that very airport and get a new random destination. -- So the flight plan in this case would be: Fly from airport A to B, then from B to C, then from C to D, ... --- +-- -- It is also possible to make aircraft "commute" between two airports, i.e. flying from airport A to B and then back from B to A, etc. -- This can be done by the @{#RAT.Commute}() function. Note that if no departure or destination airports are specified, the first departure and destination are chosen randomly. -- Then the aircraft will fly back and forth between those two airports indefinetly. --- --- +-- +-- -- ### Spawn in Air --- +-- -- ![Process](..\Presentations\RAT\RAT_Examples_Spawn_in_Air.png) --- +-- -- Aircraft can also be spawned in air rather than at airports on the ground. This is done by setting @{#RAT.SetTakeoff}() to "air". --- +-- -- By default, aircraft are spawned randomly above airports of the map. --- +-- -- The @{#RAT.SetDeparture}() option can be used to specify zones, which have been defined in the mission editor as departure zones. -- Aircraft will then be spawned at a random point within the zone or zones. --- +-- -- Note that @{#RAT.SetDeparture}() also accepts airport names. For an air takeoff these are treated like zones with a radius of XX kilometers. -- Again, aircraft are spawned at random points within these zones around the airport. --- +-- -- ### Misc Options --- +-- -- ![Process](..\Presentations\RAT\RAT_Examples_Misc.png) --- +-- -- The default "takeoff" type of RAT aircraft is that they are spawned with hot or cold engines. -- The choice is random, so 50% of aircraft will be spawned with hot engines while the other 50% will be spawned with cold engines. -- This setting can be changed using the @{#RAT.SetTakeoff}() function. The possible parameters for starting on ground are: --- +-- -- * @{#RAT.SetTakeoff}("cold"), which means that all aircraft are spawned with their engines off, -- * @{#RAT.SetTakeoff}("hot"), which means that all aircraft are spawned with their engines on, -- * @{#RAT.SetTakeoff}("runway"), which means that all aircraft are spawned already at the runway ready to takeoff. -- Note that in this case the default spawn intervall is set to 180 seconds in order to avoid aircraft jamms on the runway. Generally, this takeoff at runways should be used with care and problems are to be expected. --- --- +-- +-- -- The options @{#RAT.SetMinDistance}() and @{#RAT.SetMaxDistance}() can be used to restrict the range from departure to destination. For example --- +-- -- * @{#RAT.SetMinDistance}(100) will cause only random destination airports to be selected which are **at least** 100 km away from the departure airport. -- * @{#RAT.SetMaxDistance}(150) will allow only destination airports which are **less than** 150 km away from the departure airport. --- +-- -- ![Process](..\Presentations\RAT\RAT_Gaussian.png) --- +-- -- By default planes get a cruise altitude of ~20,000 ft ASL. The actual altitude is sampled from a Gaussian distribution. The picture shows this distribution -- if one would spawn 1000 planes. As can be seen most planes get a cruising alt of around FL200. Other values are possible but less likely the further away -- one gets from the expectation value. --- +-- -- The expectation value, i.e. the altitude most aircraft get, can be set with the function @{#RAT.SetFLcruise}(). -- It is possible to restrict the minimum cruise altitude by @{#RAT.SetFLmin}() and the maximum cruise altitude by @{#RAT.SetFLmax}() --- +-- -- The cruise altitude can also be given in meters ASL by the functions @{#RAT.SetCruiseAltitude}(), @{#RAT.SetMinCruiseAltitude}() and @{#RAT.SetMaxCruiseAltitude}(). --- +-- -- For example: --- +-- -- * @{#RAT.SetFLcruise}(300) will cause most planes fly around FL300. -- * @{#RAT.SetFLmin}(100) restricts the cruising alt such that no plane will fly below FL100. Note that this automatically changes the minimum distance from departure to destination. --- That means that only destinations are possible for which the aircraft has had enought time to reach that flight level and descent again. +-- That means that only destinations are possible for which the aircraft has had enought time to reach that flight level and descent again. -- * @{#RAT.SetFLmax}(200) will restrict the cruise alt to maximum FL200, i.e. no aircraft will travel above this height. --- --- +-- +-- -- @field #RAT RAT={ ClassName = "RAT", -- Name of class: RAT = Random Air Traffic. @@ -62503,7 +69630,7 @@ RAT={ category = nil, -- Category of aircarft: "plane" or "heli". groupsize=nil, -- Number of aircraft in the group. friendly = "same", -- Possible departure/destination airport: same=spawn+neutral, spawnonly=spawn, blue=blue+neutral, blueonly=blue, red=red+neutral, redonly=red, neutral. - ctable = {}, -- Table with the valid coalitons from choice self.friendly. + ctable = {}, -- Table with the valid coalitions from choice self.friendly. aircraft = {}, -- Table which holds the basic aircraft properties (speed, range, ...). Vcruisemax=nil, -- Max cruise speed in set by user. Vclimb=1500, -- Default climb rate in ft/min. @@ -62546,7 +69673,7 @@ RAT={ homebase=nil, -- Home base for commute. continuejourney=false, -- Aircraft will continue their journey, i.e. get respawned at their destination with a new random destination. alive=0, -- Number of groups which are alive. - ngroups=nil, -- Number of groups to be spawned in total. + ngroups=nil, -- Number of groups to be spawned in total. f10menu=false, -- Add an F10 menu for RAT. Menu={}, -- F10 menu items for this RAT object. SubMenuName=nil, -- Submenu name for RAT object. @@ -62569,11 +69696,11 @@ RAT={ uncontrolled=false, -- Spawn uncontrolled aircraft. invisible=false, -- Spawn aircraft as invisible. immortal=false, -- Spawn aircraft as indestructible. - activate_uncontrolled=false, -- Activate uncontrolled aircraft (randomly). + activate_uncontrolled=false, -- Activate uncontrolled aircraft (randomly). activate_delay=5, -- Delay in seconds before first uncontrolled group is activated. activate_delta=5, -- Time interval in seconds between activation of uncontrolled groups. activate_frand=0, -- Randomization factor of time interval (activate_delta) between activating uncontrolled groups. - activate_max=1, -- Max number of uncontrolle aircraft, which will be activated at a time. + activate_max=1, -- Max number of uncontrolle aircraft, which will be activated at a time. onboardnum=nil, -- Tail number. onboardnum0=1, -- (Optional) Starting value of the automatically appended numbering of aircraft within a flight. Default is one. checkonrunway=true, -- Check whether aircraft have been spawned on the runway. @@ -62591,7 +69718,7 @@ RAT={ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Categories of the RAT class. +--- Categories of the RAT class. -- @list cat -- @field #string plane Plane. -- @field #string heli Heli. @@ -62607,7 +69734,7 @@ RAT.wp={ air=1, runway=2, hot=3, - cold=4, + cold=4, climb=5, cruise=6, descent=7, @@ -62752,7 +69879,7 @@ RAT.version={ --- Create a new RAT object. -- @param #RAT self -- @param #string groupname Name of the group as defined in the mission editor. This group is serving as a template for all spawned units. --- @param #string alias (Optional) Alias of the group. This is and optional parameter but must(!) be used if the same template group is used for more than one RAT object. +-- @param #string alias (Optional) Alias of the group. This is and optional parameter but must(!) be used if the same template group is used for more than one RAT object. -- @return #RAT Object of RAT class or nil if the group does not exist in the mission editor. -- @usage yak1:RAT("RAT_YAK") will create a RAT object called "yak1". The template group in the mission editor must have the name "RAT_YAK". -- @usage yak2:RAT("RAT_YAK", "Yak2") will create a RAT object "yak2". The template group in the mission editor must have the name "RAT_YAK" but the group will be called "Yak2" in e.g. the F10 menu. @@ -62770,22 +69897,22 @@ function RAT:New(groupname, alias) -- Welcome message. self:F(RAT.id..string.format("Creating new RAT object from template: %s.", groupname)) - + -- Set alias. alias=alias or groupname - + -- Alias of groupname. self.alias=alias - + -- Get template group defined in the mission editor. local DCSgroup=Group.getByName(groupname) - + -- Check the group actually exists. if DCSgroup==nil then self:E(RAT.id..string.format("ERROR: Group with name %s does not exist in the mission editor!", groupname)) return nil end - + -- Store template group. self.templategroup=GROUP:FindByName(groupname) @@ -62794,13 +69921,13 @@ function RAT:New(groupname, alias) -- Set own coalition. self.coalition=DCSgroup:getCoalition() - + -- Initialize aircraft parameters based on ME group template. self:_InitAircraft(DCSgroup) - + -- Get all airports of current map (Caucasus, NTTR, Normandy, ...). self:_GetAirportsOfMap() - + return self end @@ -62812,7 +69939,7 @@ end -- @param #RAT self -- @param #number naircraft (Optional) Number of aircraft to spawn. Default is one aircraft. -- @return #boolean True if spawning was successful or nil if nothing was spawned. --- @usage yak:Spawn(5) will spawn five aircraft. By default aircraft will spawn at neutral and red airports if the template group is part of the red coaliton. +-- @usage yak:Spawn(5) will spawn five aircraft. By default aircraft will spawn at neutral and red airports if the template group is part of the red coalition. function RAT:Spawn(naircraft) -- Make sure that this function is only been called once per RAT object. @@ -62825,46 +69952,46 @@ function RAT:Spawn(naircraft) -- Number of aircraft to spawn. Default is one. self.ngroups=naircraft or 1 - + -- Init RAT ATC if not already done. if self.ATCswitch and not RAT.ATC.init then self:_ATCInit(self.airports_map) end - + -- Create F10 main menu if it does not exists yet. if self.f10menu and not RAT.MenuF10 then RAT.MenuF10 = MENU_MISSION:New("RAT") end - + -- Set the coalition table based on choice of self.coalition and self.friendly. self:_SetCoalitionTable() - + -- Get all airports of this map beloning to friendly coalition(s). self:_GetAirportsOfCoalition() - + -- Set submenuname if it has not been set by user. if not self.SubMenuName then self.SubMenuName=self.alias end - -- Get all departure airports inside a Moose zone. + -- Get all departure airports inside a Moose zone. if self.departure_Azone~=nil then self.departure_ports=self:_GetAirportsInZone(self.departure_Azone) end - - -- Get all destination airports inside a Moose zone. + + -- Get all destination airports inside a Moose zone. if self.destination_Azone~=nil then self.destination_ports=self:_GetAirportsInZone(self.destination_Azone) end - + -- Add all friendly airports to possible departures/destinations if self.addfriendlydepartures then self:_AddFriendlyAirports(self.departure_ports) end if self.addfriendlydestinations then self:_AddFriendlyAirports(self.destination_ports) - end - + end + -- Setting and possibly correction min/max/cruise flight levels. if self.FLcruise==nil then -- Default flight level (ASL). @@ -62877,11 +70004,11 @@ function RAT:Spawn(naircraft) end end - -- Enable helos to go to destinations 100 meters away. + -- Enable helos to go to destinations 100 meters away. if self.category==RAT.cat.heli then self.mindist=50 - end - + end + -- Run consistency checks. self:_CheckConsistency() @@ -62914,7 +70041,7 @@ function RAT:Spawn(naircraft) text=text..string.format("Return Zone: %s\n", tostring(self.returnzone)) text=text..string.format("Spawn delay: %4.1f\n", self.spawndelay) text=text..string.format("Spawn interval: %4.1f\n", self.spawninterval) - text=text..string.format("Respawn delay: %s\n", tostring(self.respawn_delay)) + text=text..string.format("Respawn delay: %s\n", tostring(self.respawn_delay)) text=text..string.format("Respawn off: %s\n", tostring(self.norespawn)) text=text..string.format("Respawn after landing: %s\n", tostring(self.respawn_at_landing)) text=text..string.format("Respawn after take-off: %s\n", tostring(self.respawn_after_takeoff)) @@ -62960,7 +70087,7 @@ function RAT:Spawn(naircraft) end text=text..string.format("******************************************************\n") self:T(RAT.id..text) - + -- Create submenus. if self.f10menu then self.Menu[self.SubMenuName]=MENU_MISSION:New(self.SubMenuName, RAT.MenuF10) @@ -62969,7 +70096,7 @@ function RAT:Spawn(naircraft) MENU_MISSION_COMMAND:New("Delete markers", self.Menu[self.SubMenuName], self._DeleteMarkers, self) MENU_MISSION_COMMAND:New("Status report", self.Menu[self.SubMenuName], self.Status, self, true) end - + -- Schedule spawning of aircraft. local Tstart=self.spawndelay local dt=self.spawninterval @@ -62978,10 +70105,10 @@ function RAT:Spawn(naircraft) dt=math.max(dt, 180) end local Tstop=Tstart+dt*(self.ngroups-1) - + -- Status check and report scheduler. SCHEDULER:New(nil, self.Status, {self}, Tstart+1, self.statusinterval) - + -- Handle events. self:HandleEvent(EVENTS.Birth, self._OnBirth) self:HandleEvent(EVENTS.EngineStartup, self._OnEngineStartup) @@ -62996,15 +70123,15 @@ function RAT:Spawn(naircraft) if self.ngroups==0 then return nil end - + -- Start scheduled spawning. SCHEDULER:New(nil, self._SpawnWithRoute, {self}, Tstart, dt, 0.0, Tstop) - + -- Start scheduled activation of uncontrolled groups. if self.uncontrolled and self.activate_uncontrolled then SCHEDULER:New(nil, self._ActivateUncontrolled, {self}, self.activate_delay, self.activate_delta, self.activate_frand) end - + return true end @@ -63019,7 +70146,7 @@ function RAT:_CheckConsistency() -- User has used SetDeparture() if not self.random_departure then - + -- Count departure airports and zones. for _,name in pairs(self.departure_ports) do if self:_AirportExists(name) then @@ -63028,7 +70155,7 @@ function RAT:_CheckConsistency() self.Ndeparture_Zones=self.Ndeparture_Zones+1 end end - + -- What can go wrong? -- Only zones but not takeoff air == > Enable takeoff air. if self.Ndeparture_Zones>0 and self.takeoff~=RAT.wp.air then @@ -63046,7 +70173,7 @@ function RAT:_CheckConsistency() -- User has used SetDestination() if not self.random_destination then - + -- Count destination airports and zones. for _,name in pairs(self.destination_ports) do if self:_AirportExists(name) then @@ -63054,10 +70181,10 @@ function RAT:_CheckConsistency() elseif self:_ZoneExists(name) then self.Ndestination_Zones=self.Ndestination_Zones+1 end - end - + end + -- One zone specified as destination ==> Enable destination zone. - -- This does not apply to return zone because the destination is the zone and not the final destination which can be an airport. + -- This does not apply to return zone because the destination is the zone and not the final destination which can be an airport. if self.Ndestination_Zones>0 and self.landing~=RAT.wp.air and not self.returnzone then self.landing=RAT.wp.air self.destinationzone=true @@ -63070,19 +70197,19 @@ function RAT:_CheckConsistency() self:E(RAT.id.."ERROR: "..text) MESSAGE:New(text, 30):ToAll() end - end - + end + -- Destination zone and return zone should not be used together. if self.destinationzone and self.returnzone then self:E(RAT.id.."ERROR: Destination zone _and_ return to zone not possible! Disabling return to zone.") self.returnzone=false end - -- If returning to a zone, we set the landing type to "air" if takeoff is in air. + -- If returning to a zone, we set the landing type to "air" if takeoff is in air. -- Because if we start in air we want to end in air. But default landing is ground. if self.returnzone and self.takeoff==RAT.wp.air then self.landing=RAT.wp.air end - + -- Ensure that neither FLmin nor FLmax are above the aircrafts service ceiling. if self.FLminuser then self.FLminuser=math.min(self.FLminuser, self.aircraft.ceiling) @@ -63093,17 +70220,17 @@ function RAT:_CheckConsistency() if self.FLcruise then self.FLcruise=math.min(self.FLcruise, self.aircraft.ceiling) end - + -- FL min > FL max case ==> spaw values if self.FLminuser and self.FLmaxuser then if self.FLminuser > self.FLmaxuser then local min=self.FLminuser local max=self.FLmaxuser self.FLminuser=max - self.FLmaxuser=min + self.FLmaxuser=min end end - + -- Cruise alt < FL min if self.FLminuser and self.FLcruise FL max if self.FLmaxuser and self.FLcruise>self.FLmaxuser then self.FLcruise=self.FLmaxuser end - + -- Uncontrolled aircraft must start with engines off. if self.uncontrolled then -- SOLVED: Strangly, it does not work with RAT.wp.cold only with RAT.wp.hot! @@ -63150,11 +70277,11 @@ end --- Set coalition of RAT group. You can make red templates blue and vice versa. -- Note that a country is also set automatically if it has not done before via RAT:SetCountry. --- +-- -- * For blue, the country is set to USA. -- * For red, the country is set to RUSSIA. -- * For neutral, the country is set to SWITZERLAND. --- +-- -- This is important, since it is ultimately the COUNTRY that determines the coalition of the aircraft. -- You can set the country explicitly via the RAT:SetCountry() function if necessary. -- @param #RAT self @@ -63176,14 +70303,14 @@ function RAT:SetCoalitionAircraft(color) self.coalition=coalition.side.NEUTRAL if not self.country then self.country=country.id.SWITZERLAND - end + end end return self end --- Set country of RAT group. -- See [DCS_enum_country](https://wiki.hoggitworld.com/view/DCS_enum_country). --- +-- -- This overrules the coalition settings. So if you want your group to be of a specific coalition, you have to set a country that is part of that coalition. -- @param #RAT self -- @param DCS#country.id id DCS country enumerator ID. For example country.id.USA or country.id.RUSSIA. @@ -63200,7 +70327,7 @@ end -- @param #RAT self -- @param Wrapper.Airbase#AIRBASE.TerminalType termtype Type of terminal. Use enumerator AIRBASE.TerminalType.XXX. -- @return #RAT RAT self object. --- +-- -- @usage -- c17=RAT:New("C-17 BIG Plane") -- c17:SetTerminalType(AIRBASE.TerminalType.OpenBig) -- Only very big parking spots are used. @@ -63266,35 +70393,35 @@ function RAT:SetDespawnAirOFF() end --- Set takeoff type. Starting cold at airport, starting hot at airport, starting at runway, starting in the air. --- Default is "takeoff-coldorhot". So there is a 50% chance that the aircraft starts with cold engines and 50% that it starts with hot engines. +-- Default is "takeoff-coldorhot". So there is a 50% chance that the aircraft starts with cold engines and 50% that it starts with hot engines. -- @param #RAT self -- @param #string type Type can be "takeoff-cold" or "cold", "takeoff-hot" or "hot", "takeoff-runway" or "runway", "air". -- @return #RAT RAT self object. -- @usage RAT:Takeoff("hot") will spawn RAT objects at airports with engines started. -- @usage RAT:Takeoff("cold") will spawn RAT objects at airports with engines off. --- @usage RAT:Takeoff("air") will spawn RAT objects in air over random airports or within pre-defined zones. +-- @usage RAT:Takeoff("air") will spawn RAT objects in air over random airports or within pre-defined zones. function RAT:SetTakeoff(type) self:F2(type) - + local _Type if type:lower()=="takeoff-cold" or type:lower()=="cold" then _Type=RAT.wp.cold elseif type:lower()=="takeoff-hot" or type:lower()=="hot" then _Type=RAT.wp.hot - elseif type:lower()=="takeoff-runway" or type:lower()=="runway" then + elseif type:lower()=="takeoff-runway" or type:lower()=="runway" then _Type=RAT.wp.runway elseif type:lower()=="air" then _Type=RAT.wp.air else _Type=RAT.wp.coldorhot end - + self.takeoff=_Type - + return self end ---- Set takeoff type cold. Aircraft will spawn at a parking spot with engines off. +--- Set takeoff type cold. Aircraft will spawn at a parking spot with engines off. -- @param #RAT self -- @return #RAT RAT self object. function RAT:SetTakeoffCold() @@ -63302,7 +70429,7 @@ function RAT:SetTakeoffCold() return self end ---- Set takeoff type to hot. Aircraft will spawn at a parking spot with engines on. +--- Set takeoff type to hot. Aircraft will spawn at a parking spot with engines on. -- @param #RAT self -- @return #RAT RAT self object. function RAT:SetTakeoffHot() @@ -63310,7 +70437,7 @@ function RAT:SetTakeoffHot() return self end ---- Set takeoff type to runway. Aircraft will spawn directly on the runway. +--- Set takeoff type to runway. Aircraft will spawn directly on the runway. -- @param #RAT self -- @return #RAT RAT self object. function RAT:SetTakeoffRunway() @@ -63318,7 +70445,7 @@ function RAT:SetTakeoffRunway() return self end ---- Set takeoff type to cold or hot. Aircraft will spawn at a parking spot with 50:50 change of engines on or off. +--- Set takeoff type to cold or hot. Aircraft will spawn at a parking spot with 50:50 change of engines on or off. -- @param #RAT self -- @return #RAT RAT self object. function RAT:SetTakeoffColdOrHot() @@ -63326,7 +70453,7 @@ function RAT:SetTakeoffColdOrHot() return self end ---- Set takeoff type to air. Aircraft will spawn in the air. +--- Set takeoff type to air. Aircraft will spawn in the air. -- @param #RAT self -- @return #RAT RAT self object. function RAT:SetTakeoffAir() @@ -63346,21 +70473,21 @@ function RAT:SetDeparture(departurenames) -- Random departure is deactivated now that user specified departure ports. self.random_departure=false - + -- Convert input to table. local names if type(departurenames)=="table" then - names=departurenames + names=departurenames elseif type(departurenames)=="string" then names={departurenames} else -- error message self:E(RAT.id.."ERROR: Input parameter must be a string or a table in SetDeparture()!") end - + -- Put names into arrays. for _,name in pairs(names) do - + if self:_AirportExists(name) then -- If an airport with this name exists, we put it in the ports array. table.insert(self.departure_ports, name) @@ -63370,9 +70497,9 @@ function RAT:SetDeparture(departurenames) else self:E(RAT.id.."ERROR: No departure airport or zone found with name "..name) end - + end - + return self end @@ -63386,7 +70513,7 @@ function RAT:SetDestination(destinationnames) -- Random departure is deactivated now that user specified departure ports. self.random_destination=false - + -- Convert input to table local names if type(destinationnames)=="table" then @@ -63397,10 +70524,10 @@ function RAT:SetDestination(destinationnames) -- Error message. self:E(RAT.id.."ERROR: Input parameter must be a string or a table in SetDestination()!") end - + -- Put names into arrays. for _,name in pairs(names) do - + if self:_AirportExists(name) then -- If an airport with this name exists, we put it in the ports array. table.insert(self.destination_ports, name) @@ -63410,7 +70537,7 @@ function RAT:SetDestination(destinationnames) else self:E(RAT.id.."ERROR: No destination airport or zone found with name "..name) end - + end return self @@ -63421,13 +70548,13 @@ end -- @return #RAT RAT self object. function RAT:DestinationZone() self:F2() - + -- Destination is a zone. Needs special care. self.destinationzone=true - + -- Landing type is "air" because we don't actually land at the airport. self.landing=RAT.wp.air - + return self end @@ -63444,33 +70571,33 @@ end --- Include all airports which lie in a zone as possible destinations. -- @param #RAT self --- @param Core.Zone#ZONE zone Zone in which the departure airports lie. Has to be a MOOSE zone. +-- @param Core.Zone#ZONE zone Zone in which the destination airports lie. Has to be a MOOSE zone. -- @return #RAT RAT self object. function RAT:SetDestinationsFromZone(zone) self:F2(zone) -- Random departure is deactivated now that user specified departure ports. self.random_destination=false - + -- Set zone. self.destination_Azone=zone - + return self end --- Include all airports which lie in a zone as possible destinations. -- @param #RAT self --- @param Core.Zone#ZONE zone Zone in which the destination airports lie. Has to be a MOOSE zone. +-- @param Core.Zone#ZONE zone Zone in which the departure airports lie. Has to be a MOOSE zone. -- @return #RAT RAT self object. function RAT:SetDeparturesFromZone(zone) self:F2(zone) - + -- Random departure is deactivated now that user specified departure ports. self.random_departure=false -- Set zone. self.departure_Azone=zone - + return self end @@ -63506,7 +70633,7 @@ function RAT:ExcludedAirports(ports) return self end ---- Set skill of AI aircraft. Default is "High". +--- Set skill of AI aircraft. Default is "High". -- @param #RAT self -- @param #string skill Skill, options are "Average", "Good", "High", "Excellent" and "Random". Parameter is case insensitive. -- @return #RAT RAT self object. @@ -63577,7 +70704,7 @@ function RAT:Commute(starshape) return self end ---- Set the delay before first group is spawned. +--- Set the delay before first group is spawned. -- @param #RAT self -- @param #number delay Delay in seconds. Default is 5 seconds. Minimum delay is 0.5 seconds. -- @return #RAT RAT self object. @@ -63692,7 +70819,7 @@ end --- Check if aircraft have accidentally been spawned on the runway. If so they will be removed immediatly. -- @param #RAT self -- @param #boolean switch If true, check is performed. If false, this check is omitted. --- @param #number radius Distance in meters until a unit is considered to have spawned accidentally on the runway. Default is 75 m. +-- @param #number radius Distance in meters until a unit is considered to have spawned accidentally on the runway. Default is 75 m. -- @return #RAT RAT self object. function RAT:CheckOnRunway(switch, distance) self:F2(switch) @@ -63772,7 +70899,7 @@ function RAT:RadioModulation(modulation) return self end ---- Radio menu On. Default is off. +--- Radio menu On. Default is off. -- @param #RAT self -- @return #RAT RAT self object. function RAT:RadioMenuON() @@ -63781,7 +70908,7 @@ function RAT:RadioMenuON() return self end ---- Radio menu Off. This is the default setting. +--- Radio menu Off. This is the default setting. -- @param #RAT self -- @return #RAT RAT self object. function RAT:RadioMenuOFF() @@ -63790,7 +70917,7 @@ function RAT:RadioMenuOFF() return self end ---- Aircraft are invisible. +--- Aircraft are invisible. -- @param #RAT self -- @return #RAT RAT self object. function RAT:Invisible() @@ -63799,7 +70926,7 @@ function RAT:Invisible() return self end ---- Turn EPLRS datalink on/off. +--- Turn EPLRS datalink on/off. -- @param #RAT self -- @param #boolean switch If true (or nil), turn EPLRS on. -- @return #RAT RAT self object. @@ -63812,7 +70939,7 @@ function RAT:SetEPLRS(switch) return self end ---- Aircraft are immortal. +--- Aircraft are immortal. -- @param #RAT self -- @return #RAT RAT self object. function RAT:Immortal() @@ -63830,7 +70957,7 @@ function RAT:Uncontrolled() return self end ---- Activate uncontrolled aircraft. +--- Activate uncontrolled aircraft. -- @param #RAT self -- @param #number maxactivated Maximal numnber of activated aircraft. Absolute maximum will be the number of spawned groups. Default is 1. -- @param #number delay Time delay in seconds before (first) aircraft is activated. Default is 1 second. @@ -63845,21 +70972,21 @@ function RAT:ActivateUncontrolled(maxactivated, delay, delta, frand) self.activate_delay=delay or 1 self.activate_delta=delta or 1 self.activate_frand=frand or 0 - + -- Ensure min delay is one second. self.activate_delay=math.max(self.activate_delay,1) - + -- Ensure min delta is one second. self.activate_delta=math.max(self.activate_delta,0) - + -- Ensure frand is in [0,...,1] self.activate_frand=math.max(self.activate_frand,0) self.activate_frand=math.min(self.activate_frand,1) - + return self end ---- Set the time after which inactive groups will be destroyed. +--- Set the time after which inactive groups will be destroyed. -- @param #RAT self -- @param #number time Time in seconds. Default is 600 seconds = 10 minutes. Minimum is 60 seconds. -- @return #RAT RAT self object. @@ -63914,7 +71041,7 @@ end -- @return #RAT RAT self object. function RAT:SetROE(roe) self:F2(roe) - if roe=="return" then + if roe=="return" then self.roe=RAT.ROE.returnfire elseif roe=="free" then self.roe=RAT.ROE.weaponfree @@ -63966,7 +71093,7 @@ end --- Turn messages from ATC on or off. Default is on. This setting effects all RAT objects and groups! -- @param #RAT self -- @param #boolean switch Enable (true) or disable (false) messages from ATC. --- @return #RAT RAT self object. +-- @return #RAT RAT self object. function RAT:ATC_Messages(switch) self:F2(switch) if switch==nil then @@ -63976,7 +71103,7 @@ function RAT:ATC_Messages(switch) return self end ---- Max number of planes that get landing clearance of the RAT ATC. This setting effects all RAT objects and groups! +--- Max number of planes that get landing clearance of the RAT ATC. This setting effects all RAT objects and groups! -- @param #RAT self -- @param #number n Number of aircraft that are allowed to land simultaniously. Default is 2. -- @return #RAT RAT self object. @@ -63986,7 +71113,7 @@ function RAT:ATC_Clearance(n) return self end ---- Delay between granting landing clearance for simultanious landings. This setting effects all RAT objects and groups! +--- Delay between granting landing clearance for simultanious landings. This setting effects all RAT objects and groups! -- @param #RAT self -- @param #number time Delay time when the next aircraft will get landing clearance event if the previous one did not land yet. Default is 240 sec. -- @return #RAT RAT self object. @@ -64142,7 +71269,7 @@ end --- Set onboard number prefix. Same as setting "TAIL #" in the mission editor. Note that if you dont use this function, the values defined in the template group of the ME are taken. -- @param #RAT self --- @param #string tailnumprefix String of the tail number prefix. If flight consists of more than one aircraft, two digits are appended automatically, i.e. 001, 002, ... +-- @param #string tailnumprefix String of the tail number prefix. If flight consists of more than one aircraft, two digits are appended automatically, i.e. 001, 002, ... -- @param #number zero (Optional) Starting value of the automatically appended numbering of aircraft within a flight. Default is 0. -- @return #RAT RAT self object. function RAT:SetOnboardNum(tailnumprefix, zero) @@ -64168,7 +71295,7 @@ function RAT:_InitAircraft(DCSgroup) local DCSdesc=DCSunit:getDesc() local DCScategory=DCSgroup:getCategory() local DCStype=DCSunit:getTypeName() - + -- set category if DCScategory==Group.Category.AIRPLANE then self.category=RAT.cat.plane @@ -64178,37 +71305,43 @@ function RAT:_InitAircraft(DCSgroup) self.category="other" self:E(RAT.id.."ERROR: Group of RAT is neither airplane nor helicopter!") end - + -- Get type of aircraft. self.aircraft.type=DCStype - + -- inital fuel in % self.aircraft.fuel=DCSunit:getFuel() -- operational range in NM converted to m self.aircraft.Rmax = DCSdesc.range*RAT.unit.nm2m - + -- effective range taking fuel into accound and a 5% reserve self.aircraft.Reff = self.aircraft.Rmax*self.aircraft.fuel*0.95 - + -- max airspeed from group self.aircraft.Vmax = DCSdesc.speedMax - + -- max climb speed in m/s self.aircraft.Vymax=DCSdesc.VyMax - + -- service ceiling in meters self.aircraft.ceiling=DCSdesc.Hmax - + -- Store all descriptors. --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 local text=string.format("\n******************************************************\n") text=text..string.format("Aircraft parameters:\n") @@ -64254,7 +71387,7 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live -- Set takeoff type. local takeoff=self.takeoff local landing=self.landing - + -- Overrule takeoff/landing by what comes in. if _takeoff then takeoff=_takeoff @@ -64262,27 +71395,27 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live if _landing then landing=_landing end - + -- Random choice between cold and hot. if takeoff==RAT.wp.coldorhot then local temp={RAT.wp.cold, RAT.wp.hot} takeoff=temp[math.random(2)] end - + -- Number of respawn attempts after spawning on runway. local nrespawn=0 if _nrespawn then nrespawn=_nrespawn end - + -- Set flight plan. local departure, destination, waypoints, WPholding, WPfinal = self:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) - + -- Return nil if we could not find a departure destination or waypoints if not (departure and destination and waypoints) then return nil end - + -- Set (another) livery. local livery if _livery then @@ -64296,20 +71429,20 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live else livery=nil end - + -- Modify the spawn template to follow the flight plan. local successful=self:_ModifySpawnTemplate(waypoints, livery, _lastpos, departure, takeoff, parkingdata) if not successful then return nil end - + -- Actually spawn the group. local group=self:SpawnWithIndex(self.SpawnIndex) -- Wrapper.Group#GROUP - + -- Increase counter of alive groups (also uncontrolled ones). self.alive=self.alive+1 self:T(RAT.id..string.format("Alive groups counter now = %d.",self.alive)) - + -- ATC is monitoring this flight (if it is supposed to land). if self.ATCswitch and landing==RAT.wp.landing then if self.returnzone then @@ -64318,34 +71451,34 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live self:_ATCAddFlight(group:GetName(), destination:GetName()) end end - + -- Place markers of waypoints on F10 map. if self.placemarkers then self:_PlaceMarkers(waypoints, self.SpawnIndex) end - + -- Set group to be invisible. if self.invisible then self:_CommandInvisible(group, true) end - + -- Set group to be immortal. if self.immortal then self:_CommandImmortal(group, true) end - + -- Set group to be immortal. if self.eplrs then group:CommandEPLRS(true, 1) - end - + end + -- Set ROE, default is "weapon hold". self:_SetROE(group, self.roe) - + -- Set ROT, default is "no reaction". self:_SetROT(group, self.rot) - -- Init ratcraft array. + -- Init ratcraft array. self.ratcraft[self.SpawnIndex]={} self.ratcraft[self.SpawnIndex]["group"]=group self.ratcraft[self.SpawnIndex]["destination"]=destination @@ -64373,28 +71506,28 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live self.ratcraft[self.SpawnIndex]["P0"]=group:GetCoordinate() self.ratcraft[self.SpawnIndex]["Pnow"]=group:GetCoordinate() self.ratcraft[self.SpawnIndex]["Distance"]=0 - + -- Each aircraft gets its own takeoff type. self.ratcraft[self.SpawnIndex].takeoff=takeoff self.ratcraft[self.SpawnIndex].landing=landing self.ratcraft[self.SpawnIndex].wpholding=WPholding self.ratcraft[self.SpawnIndex].wpfinal=WPfinal - + -- Aircraft is active or spawned in uncontrolled state. self.ratcraft[self.SpawnIndex].active=not self.uncontrolled - + -- Set status to spawned. This will be overwritten in birth event. self.ratcraft[self.SpawnIndex]["status"]=RAT.status.Spawned - + -- Livery self.ratcraft[self.SpawnIndex].livery=livery - + -- If this switch is set to true, the aircraft will be despawned the next time the status function is called. self.ratcraft[self.SpawnIndex].despawnme=false - + -- Number of preformed spawn attempts for this group. self.ratcraft[self.SpawnIndex].nrespawn=nrespawn - + -- Create submenu for this group. if self.f10menu then local name=self.aircraft.type.." ID "..tostring(self.SpawnIndex) @@ -64409,13 +71542,13 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"]=MENU_MISSION:New("Set ROT", self.Menu[self.SubMenuName].groups[self.SpawnIndex]) MENU_MISSION_COMMAND:New("No reaction", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"], self._SetROT, self, group, RAT.ROT.noreaction) MENU_MISSION_COMMAND:New("Passive defense", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"], self._SetROT, self, group, RAT.ROT.passive) - MENU_MISSION_COMMAND:New("Evade on fire", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"], self._SetROT, self, group, RAT.ROT.evade) + MENU_MISSION_COMMAND:New("Evade on fire", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"], self._SetROT, self, group, RAT.ROT.evade) -- F10/RAT//Group X/ MENU_MISSION_COMMAND:New("Despawn group", self.Menu[self.SubMenuName].groups[self.SpawnIndex], self._Despawn, self, group) MENU_MISSION_COMMAND:New("Place markers", self.Menu[self.SubMenuName].groups[self.SpawnIndex], self._PlaceMarkers, self, waypoints, self.SpawnIndex) MENU_MISSION_COMMAND:New("Status report", self.Menu[self.SubMenuName].groups[self.SpawnIndex], self.Status, self, true, self.SpawnIndex) end - + return self.SpawnIndex end @@ -64435,10 +71568,10 @@ end -- @param Core.Point#COORDINATE lastpos Last known position of the group. -- @param #number delay Delay before respawn function RAT:_Respawn(index, lastpos, delay) - + -- Get the spawn index from group --local index=self:GetSpawnIndexFromGroup(group) - + -- Get departure and destination from previous journey. local departure=self.ratcraft[index].departure local destination=self.ratcraft[index].destination @@ -64447,7 +71580,7 @@ function RAT:_Respawn(index, lastpos, delay) local livery=self.ratcraft[index].livery local lastwp=self.ratcraft[index].waypoints[#self.ratcraft[index].waypoints] --local lastpos=group:GetCoordinate() - + local _departure=nil local _destination=nil local _takeoff=nil @@ -64455,15 +71588,15 @@ function RAT:_Respawn(index, lastpos, delay) local _livery=nil local _lastwp=nil local _lastpos=nil - + if self.continuejourney then - + -- We continue our journey from the old departure airport. _departure=destination:GetName() - + -- Use the same livery for next aircraft. _livery=livery - + -- Last known position of the aircraft, which should be the sparking spot location. -- Note: we have to check that it was supposed to land and not respawned directly after landing or after takeoff. -- TODO: Need to think if continuejourney with respawn_after_takeoff actually makes sense. @@ -64473,15 +71606,15 @@ function RAT:_Respawn(index, lastpos, delay) _lastpos=lastpos end end - + if self.destinationzone then - + -- Case: X --> Zone --> Zone --> Zone _takeoff=RAT.wp.air _landing=RAT.wp.air - + elseif self.returnzone then - + -- Case: X --> Zone --> X, X --> Zone --> X -- We flew to a zone and back. Takeoff type does not change. _takeoff=self.takeoff @@ -64492,22 +71625,22 @@ function RAT:_Respawn(index, lastpos, delay) else _landing=RAT.wp.landing end - + -- Departure stays the same. (The destination is the zone here.) _departure=departure:GetName() - + else - + -- Default case. Takeoff and landing type does not change. _takeoff=self.takeoff _landing=self.landing - + end - + elseif self.commute then - + -- We commute between departure and destination. - + if self.starshape==true then if destination:GetName()==self.homebase then -- We are at our home base ==> destination is again randomly selected. @@ -64523,10 +71656,10 @@ function RAT:_Respawn(index, lastpos, delay) _departure=destination:GetName() _destination=departure:GetName() end - + -- Use the same livery for next aircraft. _livery=livery - + -- Last known position of the aircraft, which should be the sparking spot location. -- Note: we have to check that it was supposed to land and not respawned directly after landing or after takeoff. -- TODO: Need to think if commute with respawn_after_takeoff actually makes sense. @@ -64534,22 +71667,22 @@ function RAT:_Respawn(index, lastpos, delay) -- Check that we have landed on an airport or FARP but not a ship (which would be categroy 1). if destination:GetCategory()==4 then _lastpos=lastpos - end + end end - + -- Handle takeoff type. if self.destinationzone then -- self.takeoff is either RAT.wp.air or RAT.wp.cold -- self.landing is RAT.wp.Air - + if self.takeoff==RAT.wp.air then - + -- Case: Zone <--> Zone (both have takeoff air) _takeoff=RAT.wp.air -- = self.takeoff (because we just checked) _landing=RAT.wp.air -- = self.landing (because destinationzone) - + else - + -- Case: Airport <--> Zone if takeoff==RAT.wp.air then -- Last takeoff was air so we are at the airport now, takeoff is from ground. @@ -64560,31 +71693,31 @@ function RAT:_Respawn(index, lastpos, delay) _takeoff=RAT.wp.air _landing=RAT.wp.landing end - + end - + elseif self.returnzone then - + -- We flew to a zone and back. No need to swap departure and destination. _departure=departure:GetName() _destination=destination:GetName() - + -- Takeoff and landing should also not change. _takeoff=self.takeoff _landing=self.landing - - end - + + end + end - + -- Take the last waypoint as initial waypoint for next plane. if _takeoff==RAT.wp.air and (self.continuejourney or self.commute) then _lastwp=lastwp end - + -- Debug self:T2({departure=_departure, destination=_destination, takeoff=_takeoff, landing=_landing, livery=_livery, lastwp=_lastwp}) - + -- We should give it at least 3 sec since this seems to be the time until free parking spots after despawn are available again (Sirri Island test). local respawndelay if delay then @@ -64594,7 +71727,7 @@ function RAT:_Respawn(index, lastpos, delay) else respawndelay=3 end - + -- Spawn new group. local arg={} arg.self=self @@ -64607,7 +71740,7 @@ function RAT:_Respawn(index, lastpos, delay) arg.lastpos=_lastpos self:T(RAT.id..string.format("%s delayed respawn in %.1f seconds.", self.alias, respawndelay)) SCHEDULER:New(nil, self._SpawnWithRouteTimer, {arg}, respawndelay) - + end --- Delayed spawn function called by scheduler. @@ -64631,7 +71764,7 @@ end -- @return #table Table of flight plan waypoints. -- @return #nil If no valid departure or destination airport could be found. function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) - + -- Max cruise speed. local VxCruiseMax if self.Vcruisemax then @@ -64641,39 +71774,39 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) -- Max cruise speed 90% of Vmax or 900 km/h whichever is lower. VxCruiseMax = math.min(self.aircraft.Vmax*0.90, 250) end - + -- Min cruise speed 70% of max cruise or 600 km/h whichever is lower. local VxCruiseMin = math.min(VxCruiseMax*0.70, 166) - + -- Cruise speed (randomized). Expectation value at midpoint between min and max. local VxCruise = UTILS.RandomGaussian((VxCruiseMax-VxCruiseMin)/2+VxCruiseMin, (VxCruiseMax-VxCruiseMax)/4, VxCruiseMin, VxCruiseMax) - + -- Climb speed 90% ov Vmax but max 720 km/h. local VxClimb = math.min(self.aircraft.Vmax*0.90, 200) - + -- Descent speed 60% of Vmax but max 500 km/h. local VxDescent = math.min(self.aircraft.Vmax*0.60, 140) - + -- Holding speed is 90% of descent speed. local VxHolding = VxDescent*0.9 - + -- Final leg is 90% of holding speed. local VxFinal = VxHolding*0.9 - + -- Reasonably civil climb speed Vy=1500 ft/min = 7.6 m/s but max aircraft specific climb rate. local VyClimb=math.min(self.Vclimb*RAT.unit.ft2meter/60, self.aircraft.Vymax) - + -- Climb angle in rad. local AlphaClimb=math.asin(VyClimb/VxClimb) - + -- Descent angle in rad. local AlphaDescent=math.rad(self.AlphaDescent) - + -- Expected cruise level (peak of Gaussian distribution) local FLcruise_expect=self.FLcruise - - - -- DEPARTURE AIRPORT + + + -- DEPARTURE AIRPORT -- Departure airport or zone. local departure=nil if _departure then @@ -64690,15 +71823,15 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) else local text=string.format("ERROR! Specified departure airport %s does not exist for %s.", _departure, self.alias) self:E(RAT.id..text) - end - + end + else departure=self:_PickDeparture(takeoff) if self.commute and self.starshape==true and self.homebase==nil then self.homebase=departure:GetName() end end - + -- Return nil if no departure could be found. if not departure then local text=string.format("ERROR! No valid departure airport could be found for %s.", self.alias) @@ -64716,11 +71849,11 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) -- For an air start, we take a random point within the spawn zone. local vec2=departure:GetRandomVec2() Pdeparture=COORDINATE:NewFromVec2(vec2) - end + end else Pdeparture=departure:GetCoordinate() end - + -- Height ASL of departure point. local H_departure if takeoff==RAT.wp.air then @@ -64731,7 +71864,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) else Hmin=50 end - -- Departure altitude is 70% of default cruise with 30% variation and limited to 1000 m AGL (50 m for helos). + -- Departure altitude is 70% of default cruise with 30% variation and limited to 1000 m AGL (50 m for helos). H_departure=self:_Randomize(FLcruise_expect*0.7, 0.3, Pdeparture.y+Hmin, FLcruise_expect) if self.FLminuser then H_departure=math.max(H_departure,self.FLminuser) @@ -64743,18 +71876,18 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) else H_departure=Pdeparture.y end - + -- Adjust min distance between departure and destination for user set min flight level. local mindist=self.mindist if self.FLminuser then - + -- We can conly consider the symmetric case, because no destination selected yet. local hclimb=self.FLminuser-H_departure local hdescent=self.FLminuser-H_departure - + -- Minimum distance for l local Dclimb, Ddescent, Dtot=self:_MinDistance(AlphaClimb, AlphaDescent, hclimb, hdescent) - + if takeoff==RAT.wp.air and landing==RAT.wpair then mindist=0 -- Takeoff and landing are in air. No mindist required. elseif takeoff==RAT.wp.air then @@ -64764,32 +71897,32 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) else mindist=Dtot -- Takeoff and landing on ground. Need both space to climb and descent. end - + -- Mindist is at least self.mindist. mindist=math.max(self.mindist, mindist) - + local text=string.format("Adjusting min distance to %d km (for given min FL%03d)", mindist/1000, self.FLminuser/RAT.unit.FL2m) self:T(RAT.id..text) end - + -- DESTINATION AIRPORT local destination=nil if _destination then - + if self:_AirportExists(_destination) then - + destination=AIRBASE:FindByName(_destination) if landing==RAT.wp.air or self.returnzone then destination=destination:GetZone() end - + elseif self:_ZoneExists(_destination) then destination=ZONE:New(_destination) else local text=string.format("ERROR: Specified destination airport/zone %s does not exist for %s!", _destination, self.alias) self:E(RAT.id.."ERROR: "..text) end - + else -- This handles the case where we have a journey and the first flight is done, i.e. _departure is set. @@ -64799,7 +71932,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) if self.continuejourney and _departure and #self.destination_ports<3 then random=true end - + -- In case of a returnzone the destination (i.e. return point) is always a zone. local mylanding=landing local acrange=self.aircraft.Reff @@ -64807,11 +71940,11 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) mylanding=RAT.wp.air acrange=self.aircraft.Reff/2 -- Aircraft needs to go to zone and back home. end - + -- Pick a destination airport. destination=self:_PickDestination(departure, Pdeparture, mindist, math.min(acrange, self.maxdist), random, mylanding) end - + -- Return nil if no departure could be found. if not destination then local text=string.format("No valid destination airport could be found for %s!", self.alias) @@ -64819,14 +71952,14 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) self:E(RAT.id.."ERROR: "..text) return nil end - + -- Check that departure and destination are not the same. Should not happen due to mindist. if destination:GetName()==departure:GetName() then local text=string.format("%s: Destination and departure are identical. Airport/zone %s.", self.alias, destination:GetName()) MESSAGE:New(text, 30):ToAll() self:E(RAT.id.."ERROR: "..text) end - + -- Get a random point inside zone return zone. local Preturn local destination_returnzone @@ -64839,7 +71972,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) -- Set departure to destination. destination=departure end - + -- Get destination coordinate. Either in a zone or exactly at the airport. local Pdestination if landing==RAT.wp.air then @@ -64848,10 +71981,10 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) else Pdestination=destination:GetCoordinate() end - + -- Height ASL of destination airport/zone. local H_destination=Pdestination.y - + -- DESCENT/HOLDING POINT -- Get a random point between 5 and 10 km away from the destination. local Rhmin=8000 @@ -64861,14 +71994,14 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) Rhmin=500 Rhmax=1000 end - + -- Coordinates of the holding point. y is the land height at that point. local Vholding=Pdestination:GetRandomVec2InRadius(Rhmax, Rhmin) local Pholding=COORDINATE:NewFromVec2(Vholding) - + -- AGL height of holding point. local H_holding=Pholding.y - + -- Holding point altitude. For planes between 1600 and 2400 m AGL. For helos 160 to 240 m AGL. local h_holding if self.category==RAT.cat.plane then @@ -64877,38 +72010,38 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) h_holding=150 end h_holding=self:_Randomize(h_holding, 0.2) - + -- This is the actual height ASL of the holding point we want to fly to local Hh_holding=H_holding+h_holding - + -- When we dont land, we set the holding altitude to the departure or cruise alt. -- This is used in the calculations. if landing==RAT.wp.air then Hh_holding=H_departure end - + -- Distance from holding point to final destination. local d_holding=Pholding:Get2DDistance(Pdestination) - + -- GENERAL local heading local d_total if self.returnzone then - + -- Heading from departure to destination in return zone. heading=self:_Course(Pdeparture, Preturn) - + -- Total distance to return zone and back. d_total=Pdeparture:Get2DDistance(Preturn) + Preturn:Get2DDistance(Pholding) - + else -- Heading from departure to holding point of destination. heading=self:_Course(Pdeparture, Pholding) - + -- Total distance between departure and holding point near destination. d_total=Pdeparture:Get2DDistance(Pholding) end - + -- Max height in case of air start, i.e. if we only would descent to holding point for the given distance. if takeoff==RAT.wp.air then local H_departure_max @@ -64919,15 +72052,15 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) end H_departure=math.min(H_departure, H_departure_max) end - + -------------------------------------------- - + -- Height difference between departure and destination. local deltaH=math.abs(H_departure-Hh_holding) - + -- Slope between departure and destination. local phi = math.atan(deltaH/d_total) - + -- Adjusted climb/descent angles. local phi_climb local phi_descent @@ -64946,18 +72079,18 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) else D_total = math.sqrt(deltaH*deltaH+d_total*d_total) end - + -- SSA triangle for sloped case. local gamma=math.rad(180)-phi_climb-phi_descent local a = D_total*math.sin(phi_climb)/math.sin(gamma) local b = D_total*math.sin(phi_descent)/math.sin(gamma) local hphi_max = b*math.sin(phi_climb) local hphi_max2 = a*math.sin(phi_descent) - + -- Height of triangle. local h_max1 = b*math.sin(AlphaClimb) local h_max2 = a*math.sin(AlphaDescent) - + -- Max height relative to departure or destination. local h_max if (H_departure > Hh_holding) then @@ -64965,23 +72098,23 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) else h_max=math.max(h_max1, h_max2) end - + -- Max flight level aircraft can reach for given angles and distance. local FLmax = h_max+H_departure - - --CRUISE + + --CRUISE -- Min cruise alt is just above holding point at destination or departure height, whatever is larger. local FLmin=math.max(H_departure, Hh_holding) - + -- For helicopters we take cruise alt between 50 to 1000 meters above ground. Default cruise alt is ~150 m. - if self.category==RAT.cat.heli then + if self.category==RAT.cat.heli then FLmin=math.max(H_departure, H_destination)+50 FLmax=math.max(H_departure, H_destination)+1000 end - + -- Ensure that FLmax not above its service ceiling. FLmax=math.min(FLmax, self.aircraft.ceiling) - + -- Overrule setting if user specified min/max flight level explicitly. if self.FLminuser then FLmin=math.max(self.FLminuser, FLmin) -- Still take care that we dont fly too high. @@ -64989,12 +72122,12 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) if self.FLmaxuser then FLmax=math.min(self.FLmaxuser, FLmax) -- Still take care that we dont fly too low. end - + -- If the route is very short we set FLmin a bit lower than FLmax. if FLmin>FLmax then FLmin=FLmax end - + -- Expected cruise altitude - peak of gaussian distribution. if FLcruise_expectFLmax then FLcruise_expect=FLmax end - + -- Set cruise altitude. Selected from Gaussian distribution but limited to FLmin and FLmax. local FLcruise=UTILS.RandomGaussian(FLcruise_expect, math.abs(FLmax-FLmin)/4, FLmin, FLmax) - + -- Overrule setting if user specified a flight level explicitly. if self.FLuser then FLcruise=self.FLuser @@ -65017,12 +72150,12 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) -- Climb and descent heights. local h_climb = FLcruise - H_departure local h_descent = FLcruise - Hh_holding - + -- Distances. local d_climb = h_climb/math.tan(AlphaClimb) local d_descent = h_descent/math.tan(AlphaDescent) - local d_cruise = d_total-d_climb-d_descent - + local d_cruise = d_total-d_climb-d_descent + -- debug message local text=string.format("\n******************************************************\n") text=text..string.format("Template = %s\n", self.SpawnTemplatePrefix) @@ -65054,7 +72187,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) text=text..string.format("FLmin = %6.1f m ASL = FL%03d\n", FLmin, FLmin/RAT.unit.FL2m) text=text..string.format("FLcruise = %6.1f m ASL = FL%03d\n", FLcruise, FLcruise/RAT.unit.FL2m) text=text..string.format("FLmax = %6.1f m ASL = FL%03d\n", FLmax, FLmax/RAT.unit.FL2m) - text=text..string.format("\nAngles:\n") + text=text..string.format("\nAngles:\n") text=text..string.format("Alpha climb = %6.2f Deg\n", math.deg(AlphaClimb)) text=text..string.format("Alpha descent = %6.2f Deg\n", math.deg(AlphaDescent)) text=text..string.format("Phi (slope) = %6.2f Deg\n", math.deg(phi)) @@ -65064,7 +72197,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) -- Max heights and distances if we would travel at FLmax. local h_climb_max = FLmax - H_departure local h_descent_max = FLmax - Hh_holding - local d_climb_max = h_climb_max/math.tan(AlphaClimb) + local d_climb_max = h_climb_max/math.tan(AlphaClimb) local d_descent_max = h_descent_max/math.tan(AlphaDescent) local d_cruise_max = d_total-d_climb_max-d_descent_max text=text..string.format("Heading = %6.1f Deg\n", heading) @@ -65087,7 +72220,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) end text=text..string.format("******************************************************\n") self:T2(RAT.id..text) - + -- Ensure that cruise distance is positve. Can be slightly negative in special cases. And we don't want to turn back. if d_cruise<0 then d_cruise=100 @@ -65098,80 +72231,80 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) local c={} local wpholding=nil local wpfinal=nil - + -- Departure/Take-off c[#c+1]=Pdeparture wp[#wp+1]=self:_Waypoint(#wp+1, "Departure", takeoff, c[#wp+1], VxClimb, H_departure, departure) self.waypointdescriptions[#wp]="Departure" self.waypointstatus[#wp]=RAT.status.Departure - + -- Climb if takeoff==RAT.wp.air then - + -- Air start. if d_climb < 5000 or d_cruise < 5000 then -- We omit the climb phase completely and add it to the cruise part. d_cruise=d_cruise+d_climb - else + else -- Only one waypoint at the end of climb = begin of cruise. c[#c+1]=c[#c]:Translate(d_climb, heading) - + wp[#wp+1]=self:_Waypoint(#wp+1, "Begin of Cruise", RAT.wp.cruise, c[#wp+1], VxCruise, FLcruise) self.waypointdescriptions[#wp]="Begin of Cruise" self.waypointstatus[#wp]=RAT.status.Cruise end - + else - + -- Ground start. c[#c+1]=c[#c]:Translate(d_climb/2, heading) c[#c+1]=c[#c]:Translate(d_climb/2, heading) - + wp[#wp+1]=self:_Waypoint(#wp+1, "Climb", RAT.wp.climb, c[#wp+1], VxClimb, H_departure+(FLcruise-H_departure)/2) self.waypointdescriptions[#wp]="Climb" self.waypointstatus[#wp]=RAT.status.Climb - + wp[#wp+1]=self:_Waypoint(#wp+1, "Begin of Cruise", RAT.wp.cruise, c[#wp+1], VxCruise, FLcruise) self.waypointdescriptions[#wp]="Begin of Cruise" self.waypointstatus[#wp]=RAT.status.Cruise - + end - + -- Cruise - + -- First add the little bit from begin of cruise to the return point. - if self.returnzone then - c[#c+1]=Preturn + if self.returnzone then + c[#c+1]=Preturn wp[#wp+1]=self:_Waypoint(#wp+1, "Return Zone", RAT.wp.cruise, c[#wp+1], VxCruise, FLcruise) self.waypointdescriptions[#wp]="Return Zone" self.waypointstatus[#wp]=RAT.status.Uturn end - + if landing==RAT.wp.air then - + -- Next waypoint is already the final destination. c[#c+1]=Pdestination wp[#wp+1]=self:_Waypoint(#wp+1, "Final Destination", RAT.wp.finalwp, c[#wp+1], VxCruise, FLcruise) self.waypointdescriptions[#wp]="Final Destination" self.waypointstatus[#wp]=RAT.status.Destination - + elseif self.returnzone then - - -- The little bit back to end of cruise. - c[#c+1]=c[#c]:Translate(d_cruise/2, heading-180) + + -- The little bit back to end of cruise. + c[#c+1]=c[#c]:Translate(d_cruise/2, heading-180) wp[#wp+1]=self:_Waypoint(#wp+1, "End of Cruise", RAT.wp.cruise, c[#wp+1], VxCruise, FLcruise) self.waypointdescriptions[#wp]="End of Cruise" self.waypointstatus[#wp]=RAT.status.Descent - + else - + c[#c+1]=c[#c]:Translate(d_cruise, heading) wp[#wp+1]=self:_Waypoint(#wp+1, "End of Cruise", RAT.wp.cruise, c[#wp+1], VxCruise, FLcruise) self.waypointdescriptions[#wp]="End of Cruise" self.waypointstatus[#wp]=RAT.status.Descent - + end - + -- Descent (only if we acually want to land) if landing==RAT.wp.landing then if self.returnzone then @@ -65186,45 +72319,45 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) self.waypointstatus[#wp]=RAT.status.DescentHolding end end - + -- Holding and final destination. if landing==RAT.wp.landing then -- Holding point - c[#c+1]=Pholding + c[#c+1]=Pholding wp[#wp+1]=self:_Waypoint(#wp+1, "Holding Point", RAT.wp.holding, c[#wp+1], VxHolding, H_holding+h_holding) self.waypointdescriptions[#wp]="Holding Point" self.waypointstatus[#wp]=RAT.status.Holding wpholding=#wp -- Final destination. - c[#c+1]=Pdestination + c[#c+1]=Pdestination wp[#wp+1]=self:_Waypoint(#wp+1, "Final Destination", landing, c[#wp+1], VxFinal, H_destination, destination) self.waypointdescriptions[#wp]="Final Destination" self.waypointstatus[#wp]=RAT.status.Destination - + end - + -- Final Waypoint wpfinal=#wp - + -- Fill table with waypoints. local waypoints={} for _,p in ipairs(wp) do table.insert(waypoints, p) end - + -- Some info on the route. self:_Routeinfo(waypoints, "Waypoint info in set_route:") - + -- Return departure, destination and waypoints. if self.returnzone then -- We return the actual zone here because returning the departure leads to problems with commute. - return departure, destination_returnzone, waypoints, wpholding, wpfinal + return departure, destination_returnzone, waypoints, wpholding, wpfinal else return departure, destination, waypoints, wpholding, wpfinal end - + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -65239,41 +72372,41 @@ function RAT:_PickDeparture(takeoff) -- Array of possible departure airports or zones. local departures={} - + if self.random_departure then - + -- Airports of friendly coalitions. for _,_airport in pairs(self.airports) do - + local airport=_airport --Wrapper.Airbase#AIRBASE - + local name=airport:GetName() if not self:_Excluded(name) then if takeoff==RAT.wp.air then - + table.insert(departures, airport:GetZone()) -- insert zone object. - + else - + -- Check if airbase has the right terminals. local nspots=1 if self.termtype~=nil then nspots=airport:GetParkingSpotsNumber(self.termtype) end - + if nspots>0 then table.insert(departures, airport) -- insert airport object. end end end - + end - + else - + -- Destination airports or zones specified by user. for _,name in pairs(self.departure_ports) do - + local dep=nil if self:_AirportExists(name) then if takeoff==RAT.wp.air then @@ -65298,22 +72431,22 @@ function RAT:_PickDeparture(takeoff) else self:E(RAT.id..string.format("ERROR: No airport or zone found with name %s.", name)) end - + -- Add to departures table. if dep then table.insert(departures, dep) end - - end - + + end + end - + -- Info message. self:T(RAT.id..string.format("Number of possible departures for %s= %d", self.alias, #departures)) - + -- Select departure airport or zone. local departure=departures[math.random(#departures)] - + local text if departure and departure:GetName() then if takeoff==RAT.wp.air then @@ -65327,7 +72460,7 @@ function RAT:_PickDeparture(takeoff) self:E(RAT.id..string.format("ERROR! No departure airport or zone found for %s.", self.alias)) departure=nil end - + return departure end @@ -65348,18 +72481,18 @@ function RAT:_PickDestination(departure, q, minrange, maxrange, random, landing) -- All possible destinations. local destinations={} - + if random then - + -- Airports of friendly coalitions. for _,_airport in pairs(self.airports) do local airport=_airport --Wrapper.Airbase#AIRBASE local name=airport:GetName() if self:_IsFriendly(name) and not self:_Excluded(name) and name~=departure:GetName() then - + -- Distance from departure to possible destination local distance=q:Get2DDistance(airport:GetCoordinate()) - + -- Check if distance form departure to destination is within min/max range. if distance>=minrange and distance<=maxrange then if landing==RAT.wp.air then @@ -65377,12 +72510,12 @@ function RAT:_PickDestination(departure, q, minrange, maxrange, random, landing) end end end - + else - + -- Destination airports or zones specified by user. for _,name in pairs(self.destination_ports) do - + -- Make sure departure and destination are not identical. if name ~= departure:GetName() then @@ -65410,11 +72543,11 @@ function RAT:_PickDestination(departure, q, minrange, maxrange, random, landing) else self:E(RAT.id..string.format("ERROR! No airport or zone found with name %s", name)) end - + if dest then -- Distance from departure to possible destination local distance=q:Get2DDistance(dest:GetCoordinate()) - + -- Add as possible destination if zone is within range. if distance>=minrange and distance<=maxrange then table.insert(destinations, dest) @@ -65423,14 +72556,14 @@ function RAT:_PickDestination(departure, q, minrange, maxrange, random, landing) self:T(RAT.id..text) end end - + end - end + end end - + -- Info message. self:T(RAT.id..string.format("Number of possible destinations = %s.", #destinations)) - + if #destinations > 0 then --- Compare distance of destination airports. -- @param Core.Point#COORDINATE a Coordinate of point a. @@ -65445,15 +72578,15 @@ function RAT:_PickDestination(departure, q, minrange, maxrange, random, landing) else destinations=nil end - - + + -- Randomly select one possible destination. local destination if destinations and #destinations>0 then - + -- Random selection. destination=destinations[math.random(#destinations)] -- Wrapper.Airbase#AIRBASE - + -- Debug message. local text if landing==RAT.wp.air then @@ -65463,15 +72596,15 @@ function RAT:_PickDestination(departure, q, minrange, maxrange, random, landing) end self:T(RAT.id..text) --MESSAGE:New(text, 30):ToAllIf(self.Debug) - + else self:E(RAT.id.."ERROR! No destination airport or zone found.") destination=nil end - + -- Return the chosen destination. - return destination - + return destination + end --- Find airports within a zone. @@ -65483,7 +72616,7 @@ function RAT:_GetAirportsInZone(zone) for _,airport in pairs(self.airports) do local name=airport:GetName() local coord=airport:GetCoordinate() - + if zone:IsPointVec3InZone(coord) then table.insert(airports, name) end @@ -65524,9 +72657,9 @@ end -- @param #RAT self function RAT:_GetAirportsOfMap() local _coalition - + for i=0,2 do -- cycle coalition.side 0=NEUTRAL, 1=RED, 2=BLUE - + -- set coalition if i==0 then _coalition=coalition.side.NEUTRAL @@ -65535,33 +72668,33 @@ function RAT:_GetAirportsOfMap() elseif i==2 then _coalition=coalition.side.BLUE end - + -- get airbases of coalition local ab=coalition.getAirbases(i) - + -- loop over airbases and put them in a table for _,airbase in pairs(ab) do - + local _id=airbase:getID() local _p=airbase:getPosition().p local _name=airbase:getName() local _myab=AIRBASE:FindByName(_name) - + if _myab then - + -- Add airport to table. table.insert(self.airports_map, _myab) - + local text="MOOSE: Airport ID = ".._myab:GetID().." and Name = ".._myab:GetName()..", Category = ".._myab:GetCategory()..", TypeName = ".._myab:GetTypeName() self:T(RAT.id..text) - + else - + self:E(RAT.id..string.format("WARNING: Airbase %s does not exsist as MOOSE object!", tostring(_name))) - + end end - + end end @@ -65579,7 +72712,7 @@ function RAT:_GetAirportsOfCoalition() -- Planes cannot land on ships. --local condition2=self.category==RAT.cat.plane and airport:GetCategory()==1 local condition2=self.category==RAT.cat.plane and category==Airbase.Category.SHIP - + -- Check that airport has the requested terminal types. -- NOT good here because we would also not allow any airport zones! --[[ @@ -65589,14 +72722,14 @@ function RAT:_GetAirportsOfCoalition() end local condition3 = nspots==0 ]] - + if not (condition1 or condition2) then table.insert(self.airports, airport) end end end end - + if #self.airports==0 then local text=string.format("No possible departure/destination airports found for RAT %s.", tostring(self.alias)) MESSAGE:New(text, 10):ToAll() @@ -65615,33 +72748,33 @@ function RAT:Status(message, forID) -- Optional arguments. if message==nil then message=false - end + end if forID==nil then forID=false end - + -- Current time. local Tnow=timer.getTime() - + -- Alive counter. local nalive=0 - + -- Loop over all ratcraft. for spawnindex,ratcraft in ipairs(self.ratcraft) do - + -- Get group. local group=ratcraft.group --Wrapper.Group#GROUP - + if group and group:IsAlive() then nalive=nalive+1 - + -- Gather some information. local prefix=self:_GetPrefixFromGroup(group) local life=self:_GetLife(group) local fuel=group:GetFuel()*100.0 local airborne=group:InAir() local coords=group:GetCoordinate() - local alt=coords.y + local alt=coords.y or 1000 --local vel=group:GetVelocityKMH() local departure=ratcraft.departure:GetName() local destination=ratcraft.destination:GetName() @@ -65650,7 +72783,7 @@ function RAT:Status(message, forID) local active=ratcraft.active local Nunits=ratcraft.nunits -- group:GetSize() local N0units=group:GetInitialSize() - + -- Monitor time and distance on ground. local Tg=0 local Dg=0 @@ -65667,51 +72800,51 @@ function RAT:Status(message, forID) if ratcraft["Tground"] then -- Aircraft was already on ground. Calculate total time on ground. Tg=Tnow-ratcraft["Tground"] - + -- Distance on ground since last check. Dg=coords:Get2DDistance(ratcraft["Pground"]) - + -- Time interval since last check. dTlast=Tnow-ratcraft["Tlastcheck"] - + -- If more than Tinactive seconds passed since last check ==> check how much we moved meanwhile. if dTlast > self.Tinactive then - + --[[ if Dg<50 and active and status~=RAT.status.EventBirth then stationary=true end ]] - + -- Loop over all units. for _,_unit in pairs(group:GetUnits()) do - + if _unit and _unit:IsAlive() then - + -- Unit name, coord and distance since last check. local unitname=_unit:GetName() local unitcoord=_unit:GetCoordinate() local Ug=unitcoord:Get2DDistance(ratcraft.Uground[unitname]) - + -- Debug info self:T2(RAT.id..string.format("Unit %s travelled distance on ground %.1f m since %d seconds.", unitname, Ug, dTlast)) - + -- If aircraft did not move more than 50 m since last check, we call it stationary and despawn it. - -- Aircraft which are spawned uncontrolled or starting their engines are not counted. + -- Aircraft which are spawned uncontrolled or starting their engines are not counted. if Ug<50 and active and status~=RAT.status.EventBirth then stationary=true end - + -- Update coords. ratcraft["Uground"][unitname]=unitcoord end end - + -- Set the current time to know when the next check is necessary. ratcraft["Tlastcheck"]=Tnow - ratcraft["Pground"]=coords + ratcraft["Pground"]=coords end - + else -- First time we see that the aircraft is on ground. Initialize the times and position. ratcraft["Tground"]=Tnow @@ -65724,18 +72857,18 @@ function RAT:Status(message, forID) end end end - + -- Monitor travelled distance since last check. local Pn=coords local Dtravel=Pn:Get2DDistance(ratcraft["Pnow"]) ratcraft["Pnow"]=Pn - + -- Add up the travelled distance. ratcraft["Distance"]=ratcraft["Distance"]+Dtravel - + -- Distance remaining to destination. local Ddestination=Pn:Get2DDistance(ratcraft.destination:GetCoordinate()) - + -- Status report. if (forID and spawnindex==forID) or (not forID) then local text=string.format("ID %i of flight %s", spawnindex, prefix) @@ -65772,44 +72905,44 @@ function RAT:Status(message, forID) MESSAGE:New(text, 20):ToAll() end end - + -- Despawn groups if they are on ground and don't move or are damaged. if not airborne then - + -- Despawn unit if it did not move more then 50 m in the last 180 seconds. if stationary then local text=string.format("Group %s is despawned after being %d seconds inaktive on ground.", self.alias, dTlast) self:T(RAT.id..text) self:_Despawn(group) end - + -- Despawn group if life is < 10% and distance travelled < 100 m. if life<10 and Dtravel<100 then local text=string.format("Damaged group %s is despawned. Life = %3.0f", self.alias, life) self:T(RAT.id..text) self:_Despawn(group) end - + end - + -- Despawn groups after they have reached their destination zones. if ratcraft.despawnme then - + local text=string.format("Flight %s will be despawned NOW!", self.alias) self:T(RAT.id..text) - + -- Respawn group if (not self.norespawn) and (not self.respawn_after_takeoff) then local idx=self:GetSpawnIndexFromGroup(group) - local coord=group:GetCoordinate() + local coord=group:GetCoordinate() self:_Respawn(idx, coord, 0) end - + -- Despawn old group. if self.despawnair then self:_Despawn(group, 0) end - + end else @@ -65817,14 +72950,14 @@ function RAT:Status(message, forID) local text=string.format("Group does not exist in loop ratcraft status.") self:T2(RAT.id..text) end - + end - + -- Alive groups. local text=string.format("Alive groups of %s: %d, nalive=%d/%d", self.alias, self.alive, nalive, self.ngroups) self:T(RAT.id..text) MESSAGE:New(text, 20):ToAllIf(message and not forID) - + end --- Get (relative) life of first unit of a group. @@ -65856,26 +72989,26 @@ function RAT:_SetStatus(group, status) -- Get index from groupname. local index=self:GetSpawnIndexFromGroup(group) - + if self.ratcraft[index] then - + -- Set new status. self.ratcraft[index].status=status - + -- No status update message for "first waypoint", "holding" local no1 = status==RAT.status.Departure local no2 = status==RAT.status.EventBirthAir local no3 = status==RAT.status.Holding - + local text=string.format("Flight %s: %s.", group:GetName(), status) self:T(RAT.id..text) - + if not (no1 or no2 or no3) then MESSAGE:New(text, 10):ToAllIf(self.reportstatus) end - + end - + end end @@ -65889,16 +73022,16 @@ function RAT:GetStatus(group) -- Get index from groupname. local index=self:GetSpawnIndexFromGroup(group) - + if self.ratcraft[index] then - + -- Set new status. return self.ratcraft[index].status - + end - + end - + return "nonexistant" end @@ -65913,20 +73046,20 @@ function RAT:_OnBirth(EventData) self:T3(RAT.id.."Captured event birth!") local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP - + if SpawnGroup then - + -- Get the template name of the group. This can be nil if this was not a spawned group. local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) - + if EventPrefix then - + -- Check that the template name actually belongs to this object. if EventPrefix == self.alias then - + local text="Event: Group "..SpawnGroup:GetName().." was born." self:T(RAT.id..text) - + -- Set status. local status="unknown in birth" if SpawnGroup:InAir() then @@ -65937,7 +73070,7 @@ function RAT:_OnBirth(EventData) status=RAT.status.EventBirth end self:_SetStatus(SpawnGroup, status) - + -- Get some info ablout this flight. local i=self:GetSpawnIndexFromGroup(SpawnGroup) local _departure=self.ratcraft[i].departure:GetName() @@ -65946,10 +73079,10 @@ function RAT:_OnBirth(EventData) local _takeoff=self.ratcraft[i].takeoff local _landing=self.ratcraft[i].landing local _livery=self.ratcraft[i].livery - + -- Some is only useful for an actual airbase (not a zone). local _airbase=AIRBASE:FindByName(_departure) - + -- Check if aircraft group was accidentally spawned on the runway. -- This can happen due to no parking slots available and other DCS bugs. local onrunway=false @@ -65957,12 +73090,12 @@ function RAT:_OnBirth(EventData) -- Check that we did not want to spawn at a runway or in air. if self.checkonrunway and _takeoff ~= RAT.wp.runway and _takeoff ~= RAT.wp.air then onrunway=_airbase:CheckOnRunWay(SpawnGroup, self.onrunwayradius, false) - end + end end - + -- Workaround if group was spawned on runway. if onrunway then - + -- Error message. local text=string.format("ERROR: RAT group of %s was spawned on runway. Group #%d will be despawned immediately!", self.alias, i) MESSAGE:New(text,30):ToAllIf(self.Debug) @@ -65970,42 +73103,42 @@ function RAT:_OnBirth(EventData) if self.Debug then SpawnGroup:FlareRed() end - + -- Despawn the group. self:_Despawn(SpawnGroup) - + -- Try to respawn the group if there is at least another airport or random airport selection is used. if (self.Ndeparture_Airports>=2 or self.random_departure) and _nrespawn new state %s.", SpawnGroup:GetName(), currentstate, status) self:T(RAT.id..text) - + -- Respawn group. local idx=self:GetSpawnIndexFromGroup(SpawnGroup) local coord=SpawnGroup:GetCoordinate() @@ -66196,13 +73329,13 @@ function RAT:_OnEngineShutdown(EventData) -- Despawn group. text="Event: Group "..SpawnGroup:GetName().." will be destroyed now." self:T(RAT.id..text) - self:_Despawn(SpawnGroup) + self:_Despawn(SpawnGroup) end end end - + else self:T2(RAT.id.."ERROR: Group does not exist in RAT:_OnEngineShutdown().") end @@ -66214,19 +73347,19 @@ end function RAT:_OnHit(EventData) self:F3(EventData) self:T(RAT.id..string.format("Captured event Hit by %s! Initiator %s. Target %s", self.alias, tostring(EventData.IniUnitName), tostring(EventData.TgtUnitName))) - + local SpawnGroup = EventData.TgtGroup --Wrapper.Group#GROUP - + if SpawnGroup then - + -- Get the template name of the group. This can be nil if this was not a spawned group. local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) - + -- Check that the template name actually belongs to this object. if EventPrefix and EventPrefix == self.alias then -- Debug info. self:T(RAT.id..string.format("Event: Group %s was hit. Unit %s.", SpawnGroup:GetName(), tostring(EventData.TgtUnitName))) - + local text=string.format("%s, unit %s was hit!", self.alias, EventData.TgtUnitName) MESSAGE:New(text, 10):ToAllIf(self.reportstatus or self.Debug) end @@ -66239,37 +73372,37 @@ end function RAT:_OnDeadOrCrash(EventData) self:F3(EventData) self:T3(RAT.id.."Captured event DeadOrCrash!") - + local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP - + if SpawnGroup then - + -- Get the template name of the group. This can be nil if this was not a spawned group. local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) - + if EventPrefix then - + -- Check that the template name actually belongs to this object. if EventPrefix == self.alias then - + -- Decrease group alive counter. self.alive=self.alive-1 - + -- Debug info. - local text=string.format("Event: Group %s crashed or died. Alive counter = %d.", SpawnGroup:GetName(), self.alive) + local text=string.format("Event: Group %s crashed or died. Alive counter = %d.", SpawnGroup:GetName(), self.alive) self:T(RAT.id..text) - + -- Split crash and dead events. if EventData.id == world.event.S_EVENT_CRASH then - - -- Call crash event. This handles when a group crashed or + + -- Call crash event. This handles when a group crashed or self:_OnCrash(EventData) - + elseif EventData.id == world.event.S_EVENT_DEAD then - + -- Call dead event. self:_OnDead(EventData) - + end end end @@ -66282,26 +73415,26 @@ end function RAT:_OnDead(EventData) self:F3(EventData) self:T3(RAT.id.."Captured event Dead!") - + local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP - + if SpawnGroup then - + -- Get the template name of the group. This can be nil if this was not a spawned group. local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) - + if EventPrefix then - + -- Check that the template name actually belongs to this object. if EventPrefix == self.alias then - - local text=string.format("Event: Group %s died. Unit %s.", SpawnGroup:GetName(), EventData.IniUnitName) + + local text=string.format("Event: Group %s died. Unit %s.", SpawnGroup:GetName(), EventData.IniUnitName) self:T(RAT.id..text) - + -- Set status. local status=RAT.status.EventDead self:_SetStatus(SpawnGroup, status) - + end end @@ -66318,29 +73451,29 @@ function RAT:_OnCrash(EventData) self:T3(RAT.id.."Captured event Crash!") local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP - + if SpawnGroup then -- Get the template name of the group. This can be nil if this was not a spawned group. local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) - + -- Check that the template name actually belongs to this object. if EventPrefix and EventPrefix == self.alias then - + -- Update number of alive units in the group. local _i=self:GetSpawnIndexFromGroup(SpawnGroup) self.ratcraft[_i].nunits=self.ratcraft[_i].nunits-1 local _n=self.ratcraft[_i].nunits local _n0=SpawnGroup:GetInitialSize() - - -- Debug info. + + -- Debug info. local text=string.format("Event: Group %s crashed. Unit %s. Units still alive %d of %d.", SpawnGroup:GetName(), EventData.IniUnitName, _n, _n0) self:T(RAT.id..text) - + -- Set status. local status=RAT.status.EventCrash self:_SetStatus(SpawnGroup, status) - + -- Respawn group if all units are dead. if _n==0 and self.respawn_after_crash and not self.norespawn then local text=string.format("No units left of group %s. Group will be respawned now.", SpawnGroup:GetName()) @@ -66352,7 +73485,7 @@ function RAT:_OnCrash(EventData) end end - + else if self.Debug then self:E(RAT.id.."ERROR: Group does not exist in RAT:_OnCrash().") @@ -66368,16 +73501,16 @@ end function RAT:_Despawn(group, delay) if group ~= nil then - + -- Get spawnindex of group. local index=self:GetSpawnIndexFromGroup(group) - + if index ~= nil then - + self.ratcraft[index].group=nil self.ratcraft[index]["status"]="Dead" - - --TODO: Maybe here could be some more arrays deleted? + + --TODO: Maybe here could be some more arrays deleted? --TODO: Somehow this causes issues. --[[ --self.ratcraft[index]["group"]=group @@ -66403,8 +73536,8 @@ function RAT:_Despawn(group, delay) ]] -- Remove ratcraft table entry. --table.remove(self.ratcraft, index) - - + + -- We should give it at least 3 sec since this seems to be the time until free parking spots after despawn are available again (Sirri Island test). local despawndelay=0 if delay then @@ -66414,20 +73547,20 @@ function RAT:_Despawn(group, delay) -- Despawn afer respawn_delay. Actual respawn happens in +3 seconds to allow for free parking. despawndelay=self.respawn_delay end - - -- This will destroy the DCS group and create a single DEAD event. + + -- This will destroy the DCS group and create a single DEAD event. --if despawndelay>0.5 then self:T(RAT.id..string.format("%s delayed despawn in %.1f seconds.", self.alias, despawndelay)) SCHEDULER:New(nil, self._Destroy, {self, group}, despawndelay) --else --self:_Destroy(group) - --end + --end -- Remove submenu for this group. if self.f10menu and self.SubMenuName ~= nil then self.Menu[self.SubMenuName]["groups"][index]:Remove() end - + end end end @@ -66443,23 +73576,23 @@ function RAT:_Destroy(group) local DCSGroup = group:GetDCSObject() -- DCS#Group if DCSGroup and DCSGroup:isExist() then - + -- Cread one single Dead event and delete units from database. local triggerdead=true for _,DCSUnit in pairs(DCSGroup:getUnits()) do - + -- Dead event. if DCSUnit then if triggerdead then self:_CreateEventDead(timer.getTime(), DCSUnit) triggerdead=false end - + -- Delete from data base. _DATABASE:DeleteUnit(DCSUnit:getName()) end end - + -- Destroy DCS group. DCSGroup:destroy() DCSGroup = nil @@ -66500,10 +73633,10 @@ function RAT:_Waypoint(index, description, Type, Coord, Speed, Altitude, Airport -- Altitude of input parameter or y-component of 3D-coordinate. local _Altitude=Altitude or Coord.y - + -- Land height at given coordinate. local Hland=Coord:GetLandHeight() - + -- convert type and action in DCS format local _Type=nil local _Action=nil @@ -66516,7 +73649,7 @@ function RAT:_Waypoint(index, description, Type, Coord, Speed, Altitude, Airport _Altitude = 10 _alttype="RADIO" elseif Type==RAT.wp.hot then - -- take-off with engine on + -- take-off with engine on _Type="TakeOffParkingHot" _Action="From Parking Area Hot" _Altitude = 10 @@ -66589,7 +73722,7 @@ function RAT:_Waypoint(index, description, Type, Coord, Speed, Altitude, Airport end text=text.."******************************************************\n" self:T2(RAT.id..text) - + -- define waypoint local RoutePoint = {} -- coordinates and altitude @@ -66598,7 +73731,7 @@ function RAT:_Waypoint(index, description, Type, Coord, Speed, Altitude, Airport RoutePoint.alt = _Altitude -- altitude type: BARO=ASL or RADIO=AGL RoutePoint.alt_type = _alttype - -- type + -- type RoutePoint.type = _Type RoutePoint.action = _Action -- speed in m/s @@ -66609,7 +73742,7 @@ function RAT:_Waypoint(index, description, Type, Coord, Speed, Altitude, Airport RoutePoint.ETA_locked = false -- waypoint description RoutePoint.name=description - + if (Airport~=nil) and (Type~=RAT.wp.air) then local AirbaseID = Airport:GetID() local AirbaseCategory = Airport:GetAirbaseCategory() @@ -66623,7 +73756,7 @@ function RAT:_Waypoint(index, description, Type, Coord, Speed, Altitude, Airport RoutePoint.airdromeId = AirbaseID else self:T(RAT.id.."Unknown Airport category in _Waypoint()!") - end + end end -- properties RoutePoint.properties = { @@ -66637,16 +73770,16 @@ function RAT:_Waypoint(index, description, Type, Coord, Speed, Altitude, Airport local TaskCombo = {} local TaskHolding = self:_TaskHolding({x=Coord.x, y=Coord.z}, Altitude, Speed, self:_Randomize(90,0.9)) local TaskWaypoint = self:_TaskFunction("RAT._WaypointFunction", self, index) - + RoutePoint.task = {} RoutePoint.task.id = "ComboTask" RoutePoint.task.params = {} - + TaskCombo[#TaskCombo+1]=TaskWaypoint if Type==RAT.wp.holding then TaskCombo[#TaskCombo+1]=TaskHolding end - + RoutePoint.task.params.tasks = TaskCombo -- Return waypoint. @@ -66688,10 +73821,10 @@ function RAT:_Routeinfo(waypoints, comment) end text=text..string.format("Total distance = %6.1f km\n", total/1000) text=text..string.format("******************************************************\n") - + -- Debug info. self:T2(RAT.id..text) - + -- return total route length in meters return total end @@ -66717,7 +73850,7 @@ function RAT:_TaskHolding(P1, Altitude, Speed, Duration) dx=200 dy=0 end - + local P2={} P2.x=P1.x+dx P2.y=P1.y+dy @@ -66732,12 +73865,12 @@ function RAT:_TaskHolding(P1, Altitude, Speed, Duration) altitude = Altitude } } - + local DCSTask={} DCSTask.id="ControlledTask" DCSTask.params={} DCSTask.params.task=Task - + if self.ATCswitch then -- Set stop condition for holding. Either flag=1 or after max. X min holding. local userflagname=string.format("%s#%03d", self.alias, self.SpawnIndex+1) @@ -66746,7 +73879,7 @@ function RAT:_TaskHolding(P1, Altitude, Speed, Duration) else DCSTask.params.stopCondition={duration=Duration} end - + return DCSTask end @@ -66759,32 +73892,32 @@ function RAT._WaypointFunction(group, rat, wp) -- Current time and Spawnindex. local Tnow=timer.getTime() local sdx=rat:GetSpawnIndexFromGroup(group) - + -- Departure and destination names. local departure=rat.ratcraft[sdx].departure:GetName() local destination=rat.ratcraft[sdx].destination:GetName() local landing=rat.ratcraft[sdx].landing local WPholding=rat.ratcraft[sdx].wpholding local WPfinal=rat.ratcraft[sdx].wpfinal - - + + -- For messages local text - + -- Info on passing waypoint. text=string.format("Flight %s passing waypoint #%d %s.", group:GetName(), wp, rat.waypointdescriptions[wp]) BASE.T(rat, RAT.id..text) - + -- New status. local status=rat.waypointstatus[wp] rat:_SetStatus(group, status) - + if wp==WPholding then - + -- Aircraft arrived at holding point text=string.format("Flight %s to %s ATC: Holding and awaiting landing clearance.", group:GetName(), destination) MESSAGE:New(text, 10):ToAllIf(rat.reportstatus) - + -- Register aircraft at ATC. if rat.ATCswitch then if rat.f10menu then @@ -66793,12 +73926,12 @@ function RAT._WaypointFunction(group, rat, wp) rat._ATCRegisterFlight(rat, group:GetName(), Tnow) end end - + if wp==WPfinal then text=string.format("Flight %s arrived at final destination %s.", group:GetName(), destination) MESSAGE:New(text, 10):ToAllIf(rat.reportstatus) BASE.T(rat, RAT.id..text) - + if landing==RAT.wp.air then text=string.format("Activating despawn switch for flight %s! Group will be detroyed soon.", group:GetName()) MESSAGE:New(text, 10):ToAllIf(rat.Debug) @@ -66814,14 +73947,14 @@ end -- @param #string FunctionString Name of the function to be called. function RAT:_TaskFunction(FunctionString, ... ) self:F2({FunctionString, arg}) - + local DCSTask local ArgumentKey - + -- Templatename and anticipated name the group will get local templatename=self.templategroup:GetName() local groupname=self:_AnticipatedGroupName() - + local DCSScript = {} DCSScript[#DCSScript+1] = "local MissionControllable = GROUP:FindByName(\""..groupname.."\") " DCSScript[#DCSScript+1] = "local RATtemplateControllable = GROUP:FindByName(\""..templatename.."\") " @@ -66834,7 +73967,7 @@ function RAT:_TaskFunction(FunctionString, ... ) else DCSScript[#DCSScript+1] = FunctionString .. "( MissionControllable )" end - + DCSTask = self.templategroup:TaskWrappedAction(self.templategroup:CommandDoScript(table.concat(DCSScript))) return DCSTask @@ -66842,7 +73975,7 @@ end --- Anticipated group name from alias and spawn index. -- @param #RAT self --- @param #number index Spawnindex of group if given or self.SpawnIndex+1 by default. +-- @param #number index Spawnindex of group if given or self.SpawnIndex+1 by default. -- @return #string Name the group will get after it is spawned. function RAT:_AnticipatedGroupName(index) local index=index or self.SpawnIndex+1 @@ -66855,21 +73988,21 @@ end -- @param #RAT self function RAT:_ActivateUncontrolled() self:F() - - -- Spawn indices of uncontrolled inactive aircraft. + + -- Spawn indices of uncontrolled inactive aircraft. local idx={} local rat={} - + -- Number of active aircraft. local nactive=0 - + -- Loop over RAT groups and count the active ones. for spawnindex,ratcraft in pairs(self.ratcraft) do - + local group=ratcraft.group --Wrapper.Group#GROUP - + if group and group:IsAlive() then - + local text=string.format("Uncontrolled: Group = %s (spawnindex = %d), active = %s.", ratcraft.group:GetName(), spawnindex, tostring(ratcraft.active)) self:T2(RAT.id..text) @@ -66878,22 +74011,22 @@ function RAT:_ActivateUncontrolled() else table.insert(idx, spawnindex) end - + end end - + -- Debug message. local text=string.format("Uncontrolled: Ninactive = %d, Nactive = %d (of max %d).", #idx, nactive, self.activate_max) self:T(RAT.id..text) - + if #idx>0 and nactive Less effort. @@ -67386,7 +74519,7 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace, departure, take -- Terminal type specified explicitly. self:T(RAT.id..string.format("Helo group %s is at %s using terminal type %d.", self.alias, departure:GetName(), termtype)) spots=departure:FindFreeParkingSpotForAircraft(TemplateGroup, termtype, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits) - nfree=#spots + nfree=#spots end else -- Fixed wing aircraft is spawned. @@ -67403,15 +74536,15 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace, departure, take nfree=#spots if nfree=1 then - + -- All units get the same spot. DCS takes care of the rest. for i=1,nunits do table.insert(parkingspots, spots[1].Coordinate) @@ -67447,46 +74580,46 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace, departure, take end -- This is actually used... PointVec3=spots[1].Coordinate - + else -- If there is absolutely not spot ==> air start! _notenough=true end - + elseif spawnonairport then - + if nfree>=nunits then - + for i=1,nunits do table.insert(parkingspots, spots[i].Coordinate) table.insert(parkingindex, spots[i].TerminalID) end - + else -- Not enough spots for the whole group ==> air start! - _notenough=true - end + _notenough=true + end end - + -- Not enough spots ==> Prepare airstart. if _notenough then - - if self.respawn_inair and not self.SpawnUnControlled then + + if self.respawn_inair and not self.SpawnUnControlled then self:E(RAT.id..string.format("WARNING: Group %s has no parking spots at %s ==> air start!", self.SpawnTemplatePrefix, departure:GetName())) - + -- Not enough parking spots at the airport ==> Spawn in air. spawnonground=false spawnonship=false spawnonfarp=false spawnonrunway=false - + -- Set waypoint type/action to turning point. waypoints[1].type = GROUPTEMPLATE.Takeoff[GROUP.Takeoff.Air][1] -- type = Turning Point waypoints[1].action = GROUPTEMPLATE.Takeoff[GROUP.Takeoff.Air][2] -- action = Turning Point - + -- Adjust altitude to be 500-1000 m above the airbase. PointVec3.x=PointVec3.x+math.random(-1500,1500) - PointVec3.z=PointVec3.z+math.random(-1500,1500) + PointVec3.z=PointVec3.z+math.random(-1500,1500) if self.category==RAT.cat.heli then PointVec3.y=PointVec3:GetLandHeight()+math.random(100,1000) else @@ -67498,122 +74631,122 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace, departure, take return nil end end - + else - + -- Air start requested initially! - + --PointVec3.y is already set from first waypoint here! - + end --- new - + -- Translate the position of the Group Template to the Vec3. for UnitID = 1, nunits do - + -- Template of the current unit. local UnitTemplate = SpawnTemplate.units[UnitID] - - -- Tranlate position and preserve the relative position/formation of all aircraft. + + -- Tranlate position and preserve the relative position/formation of all aircraft. local SX = UnitTemplate.x - local SY = UnitTemplate.y + local SY = UnitTemplate.y local BX = SpawnTemplate.route.points[1].x local BY = SpawnTemplate.route.points[1].y local TX = PointVec3.x + (SX-BX) local TY = PointVec3.z + (SY-BY) - + if spawnonground then - - -- Sh°ps and FARPS seem to have a build in queue. + + -- Ships and FARPS seem to have a build in queue. if spawnonship or spawnonfarp or spawnonrunway or automatic then self:T(RAT.id..string.format("RAT group %s spawning at farp, ship or runway %s.", self.alias, departure:GetName())) -- Spawn on ship. We take only the position of the ship. SpawnTemplate.units[UnitID].x = PointVec3.x --TX SpawnTemplate.units[UnitID].y = PointVec3.z --TY - SpawnTemplate.units[UnitID].alt = PointVec3.y + SpawnTemplate.units[UnitID].alt = PointVec3.y else self:T(RAT.id..string.format("RAT group %s spawning at airbase %s on parking spot id %d", self.alias, departure:GetName(), parkingindex[UnitID])) - + -- Get coordinates of parking spot. SpawnTemplate.units[UnitID].x = parkingspots[UnitID].x SpawnTemplate.units[UnitID].y = parkingspots[UnitID].z - SpawnTemplate.units[UnitID].alt = parkingspots[UnitID].y + SpawnTemplate.units[UnitID].alt = parkingspots[UnitID].y end - - else + + else self:T(RAT.id..string.format("RAT group %s spawning in air at %s.", self.alias, departure:GetName())) - + -- Spawn in air as requested initially. Original template orientation is perserved, altitude is already correctly set. SpawnTemplate.units[UnitID].x = TX SpawnTemplate.units[UnitID].y = TY - SpawnTemplate.units[UnitID].alt = PointVec3.y + SpawnTemplate.units[UnitID].alt = PointVec3.y end - - -- Place marker at spawn position. + + -- Place marker at spawn position. if self.Debug then local unitspawn=COORDINATE:New(SpawnTemplate.units[UnitID].x, SpawnTemplate.units[UnitID].alt, SpawnTemplate.units[UnitID].y) unitspawn:MarkToAll(string.format("RAT %s Spawnplace unit #%d", self.alias, UnitID)) end - + -- Parking spot id. UnitTemplate.parking = nil UnitTemplate.parking_id = nil if parkingindex[UnitID] and not automatic then UnitTemplate.parking = parkingindex[UnitID] end - + -- Debug info. self:T2(RAT.id..string.format("RAT group %s unit number %d: Parking = %s",self.alias, UnitID, tostring(UnitTemplate.parking))) self:T2(RAT.id..string.format("RAT group %s unit number %d: Parking ID = %s",self.alias, UnitID, tostring(UnitTemplate.parking_id))) - + -- Set initial heading. SpawnTemplate.units[UnitID].heading = heading SpawnTemplate.units[UnitID].psi = -heading - + -- Set livery (will be the same for all units of the group). if livery then SpawnTemplate.units[UnitID].livery_id = livery end - + -- Set type of aircraft. if self.actype then SpawnTemplate.units[UnitID]["type"] = self.actype end - + -- Set AI skill. SpawnTemplate.units[UnitID]["skill"] = self.skill - + -- Onboard number. if self.onboardnum then SpawnTemplate.units[UnitID]["onboard_num"] = string.format("%s%d%02d", self.onboardnum, (self.SpawnIndex-1)%10, (self.onboardnum0-1)+UnitID) end - - -- Modify coaltion and country of template. + + -- Modify coalition and country of template. SpawnTemplate.CoalitionID=self.coalition if self.country then SpawnTemplate.CountryID=self.country end - + end - + -- Copy waypoints into spawntemplate. By this we avoid the nasty DCS "landing bug" :) for i,wp in ipairs(waypoints) do SpawnTemplate.route.points[i]=wp end - + -- Also modify x,y of the template. Not sure why. SpawnTemplate.x = PointVec3.x SpawnTemplate.y = PointVec3.z - + -- Enable/disable radio. Same as checking the COMM box in the ME if self.radio then SpawnTemplate.communication=self.radio end - + -- Set radio frequency and modulation. if self.frequency then SpawnTemplate.frequency=self.frequency @@ -67621,12 +74754,12 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace, departure, take if self.modulation then SpawnTemplate.modulation=self.modulation end - + -- Debug output. self:T(SpawnTemplate) end end - + return true end @@ -67699,15 +74832,15 @@ function RAT:_ATCStatus() -- Current time. local Tnow=timer.getTime() - + for name,_ in pairs(RAT.ATC.flight) do -- Holding time at destination. local hold=RAT.ATC.flight[name].holding local dest=RAT.ATC.flight[name].destination - + if hold >= 0 then - + -- Some string whether the runway is busy or not. local busy="Runway state is unknown" if RAT.ATC.airport[dest].Nonfinal>0 then @@ -67715,29 +74848,29 @@ function RAT:_ATCStatus() else busy="Runway is currently clear" end - + -- Aircraft is holding. local text=string.format("ATC %s: Flight %s is holding for %i:%02d. %s.", dest, name, hold/60, hold%60, busy) BASE:T(RAT.id..text) - + elseif hold==RAT.ATC.onfinal then - + -- Aircarft is on final approach for landing. local Tfinal=Tnow-RAT.ATC.flight[name].Tonfinal - + local text=string.format("ATC %s: Flight %s is on final. Waiting %i:%02d for landing event.", dest, name, Tfinal/60, Tfinal%60) BASE:T(RAT.id..text) - + elseif hold==RAT.ATC.unregistered then - + -- Aircraft has not arrived at holding point. --self:T(string.format("ATC %s: Flight %s is not registered yet (hold %d).", dest, name, hold)) - + else BASE:E(RAT.id.."ERROR: Unknown holding time in RAT:_ATCStatus().") end end - + end --- Main ATC function. Updates the landing queue of all airports and inceases holding time for all flights. @@ -67746,17 +74879,17 @@ function RAT:_ATCCheck() -- Init queue of flights at all airports. RAT:_ATCQueue() - + -- Current time. local Tnow=timer.getTime() - + for name,_ in pairs(RAT.ATC.airport) do - + for qID,flight in ipairs(RAT.ATC.airport[name].queue) do - + -- Number of aircraft in queue. local nqueue=#RAT.ATC.airport[name].queue - + -- Conditions to clear an aircraft for landing local landing1 if RAT.ATC.airport[name].Tlastclearance then @@ -67770,31 +74903,31 @@ function RAT:_ATCCheck() if not landing1 and not landing2 then - + -- Update holding time. RAT.ATC.flight[flight].holding=Tnow-RAT.ATC.flight[flight].Tarrive - + -- Debug message. local text=string.format("ATC %s: Flight %s runway is busy. You are #%d of %d in landing queue. Your holding time is %i:%02d.", name, flight,qID, nqueue, RAT.ATC.flight[flight].holding/60, RAT.ATC.flight[flight].holding%60) BASE:T(RAT.id..text) - + else - + local text=string.format("ATC %s: Flight %s was cleared for landing. Your holding time was %i:%02d.", name, flight, RAT.ATC.flight[flight].holding/60, RAT.ATC.flight[flight].holding%60) BASE:T(RAT.id..text) - + -- Clear flight for landing. RAT:_ATCClearForLanding(name, flight) - + end - + end - + end - + -- Update queue of flights at all airports. RAT:_ATCQueue() - + end --- Giving landing clearance for aircraft by setting user flag. @@ -67811,15 +74944,18 @@ function RAT:_ATCClearForLanding(airport, flight) -- Number of planes on final approach. RAT.ATC.airport[airport].Nonfinal=RAT.ATC.airport[airport].Nonfinal+1 -- Last time an aircraft got landing clearance. - RAT.ATC.airport[airport].Tlastclearance=timer.getTime() + RAT.ATC.airport[airport].Tlastclearance=timer.getTime() -- Current time. RAT.ATC.flight[flight].Tonfinal=timer.getTime() -- Set user flag to 1 ==> stop condition for holding. trigger.action.setUserFlag(flight, 1) local flagvalue=trigger.misc.getUserFlag(flight) - + -- Debug message. local text1=string.format("ATC %s: Flight %s cleared for landing (flag=%d).", airport, flight, flagvalue) + if string.find(flight,"#") then + flight = string.match(flight,"^(.+)#") + end local text2=string.format("ATC %s: Flight %s you are cleared for landing.", airport, flight) BASE:T( RAT.id..text1) MESSAGE:New(text2, 10):ToAllIf(RAT.ATC.messages) @@ -67831,44 +74967,47 @@ end function RAT:_ATCFlightLanded(name) if RAT.ATC.flight[name] then - + -- Destination airport. local dest=RAT.ATC.flight[name].destination - + -- Times for holding and final approach. local Tnow=timer.getTime() local Tfinal=Tnow-RAT.ATC.flight[name].Tonfinal local Thold=RAT.ATC.flight[name].Tonfinal-RAT.ATC.flight[name].Tarrive - + -- Airport is not busy any more. RAT.ATC.airport[dest].busy=false - + -- No aircraft on final any more. RAT.ATC.airport[dest].onfinal[name]=nil - + -- Decrease number of aircraft on final. RAT.ATC.airport[dest].Nonfinal=RAT.ATC.airport[dest].Nonfinal-1 - + -- Remove this flight from list of flights. RAT:_ATCDelFlight(RAT.ATC.flight, name) - + -- Increase landing counter to monitor traffic. RAT.ATC.airport[dest].traffic=RAT.ATC.airport[dest].traffic+1 - + -- Number of planes landing per hour. local TrafficPerHour=RAT.ATC.airport[dest].traffic/(timer.getTime()-RAT.ATC.T0)*3600 - + -- Debug info local text1=string.format("ATC %s: Flight %s landed. Tholding = %i:%02d, Tfinal = %i:%02d.", dest, name, Thold/60, Thold%60, Tfinal/60, Tfinal%60) local text2=string.format("ATC %s: Number of flights still on final %d.", dest, RAT.ATC.airport[dest].Nonfinal) local text3=string.format("ATC %s: Traffic report: Number of planes landed in total %d. Flights/hour = %3.2f.", dest, RAT.ATC.airport[dest].traffic, TrafficPerHour) + if string.find(name,"#") then + name = string.match(name,"^(.+)#") + end local text4=string.format("ATC %s: Flight %s landed. Welcome to %s.", dest, name, dest) BASE:T(RAT.id..text1) BASE:T(RAT.id..text2) BASE:T(RAT.id..text3) MESSAGE:New(text4, 10):ToAllIf(RAT.ATC.messages) end - + end --- Creates a landing queue for all flights holding at airports. Aircraft with longest holding time gets first permission to land. @@ -67876,7 +75015,7 @@ end function RAT:_ATCQueue() for airport,_ in pairs(RAT.ATC.airport) do - + -- Local airport queue. local _queue={} @@ -67884,26 +75023,26 @@ function RAT:_ATCQueue() for name,_ in pairs(RAT.ATC.flight) do --fvh local Tnow=timer.getTime() - + -- Update holding time (unless holing is set to onfinal=-100) if RAT.ATC.flight[name].holding>=0 then RAT.ATC.flight[name].holding=Tnow-RAT.ATC.flight[name].Tarrive end local hold=RAT.ATC.flight[name].holding local dest=RAT.ATC.flight[name].destination - + -- Flight is holding at this airport. if hold>=0 and airport==dest then _queue[#_queue+1]={name,hold} end end - + -- Sort queue w.r.t holding time in ascending order. local function compare(a,b) return a[2] > b[2] end table.sort(_queue, compare) - + -- Transfer queue to airport queue. RAT.ATC.airport[airport].queue={} for k,v in ipairs(_queue) do @@ -67931,36 +75070,36 @@ end -- @extends Core.Base#BASE ---# RATMANAGER class, extends @{Core.Base#BASE} --- The RATMANAGER class manages spawning of multiple RAT objects in a very simple way. It is created by the @{#RATMANAGER.New}() contructor. +-- The RATMANAGER class manages spawning of multiple RAT objects in a very simple way. It is created by the @{#RATMANAGER.New}() contructor. -- RAT objects with different "tasks" can be defined as usual. However, they **must not** be spawned via the @{#RAT.Spawn}() function. --- +-- -- Instead, these objects can be added to the manager via the @{#RATMANAGER.Add}(ratobject, min) function, where the first parameter "ratobject" is the @{#RAT} object, while the second parameter "min" defines the -- minimum number of RAT aircraft of that object, which are alive at all time. --- +-- -- The @{#RATMANAGER} must be started by the @{#RATMANAGER.Start}(startime) function, where the optional argument "startime" specifies the delay time in seconds after which the manager is started and the spawning beginns. -- If desired, the @{#RATMANAGER} can be stopped by the @{#RATMANAGER.Stop}(stoptime) function. The parameter "stoptime" specifies the time delay in seconds after which the manager stops. -- When this happens, no new aircraft will be spawned and the population will eventually decrease to zero. --- +-- -- When you are using a time intervall like @{#RATMANAGER.dTspawn}(delay), @{#RATMANAGER} will ignore the amount set with @{#RATMANAGER.New}(). @{#RATMANAGER.dTspawn}(delay) will spawn infinite groups. --- +-- -- ## Example -- In this example, three different @{#RAT} objects are created (but not spawned manually). The @{#RATMANAGER} takes care that at least five aircraft of each type are alive and that the total number of aircraft -- spawned is 25. The @{#RATMANAGER} is started after 30 seconds and stopped after two hours. --- +-- -- local a10c=RAT:New("RAT_A10C", "A-10C managed") -- a10c:SetDeparture({"Batumi"}) --- +-- -- local f15c=RAT:New("RAT_F15C", "F15C managed") -- f15c:SetDeparture({"Sochi-Adler"}) -- f15c:DestinationZone() -- f15c:SetDestination({"Zone C"}) --- +-- -- local av8b=RAT:New("RAT_AV8B", "AV8B managed") -- av8b:SetDeparture({"Zone C"}) -- av8b:SetTakeoff("air") -- av8b:DestinationZone() -- av8b:SetDestination({"Zone A"}) --- +-- -- local manager=RATMANAGER:New(25) -- manager:Add(a10c, 5) -- manager:Add(f15c, 5) @@ -67996,13 +75135,13 @@ function RATMANAGER:New(ntot) -- Inherit BASE. local self=BASE:Inherit(self, BASE:New()) -- #RATMANAGER - + -- Total number of RAT groups. self.ntot=ntot or 1 - + -- Debug info self:E(RATMANAGER.id..string.format("Creating manager for %d groups.", ntot)) - + return self end @@ -68017,21 +75156,21 @@ function RATMANAGER:Add(ratobject,min) --Automatic respawning is disabled. ratobject.norespawn=true ratobject.f10menu=false - + -- Increase RAT object counter. self.nrat=self.nrat+1 - + self.rat[self.nrat]=ratobject self.alive[self.nrat]=0 self.name[self.nrat]=ratobject.alias self.min[self.nrat]=min or 1 - + -- Debug info. self:T(RATMANAGER.id..string.format("Adding ratobject %s with min flights = %d", self.name[self.nrat],self.min[self.nrat])) - + -- Call spawn to initialize RAT parameters. ratobject:Spawn(0) - + return self end @@ -68055,7 +75194,7 @@ function RATMANAGER:Start(delay) -- Start scheduler. SCHEDULER:New(nil, self._Start, {self}, delay) - + return self end @@ -68073,7 +75212,7 @@ function RATMANAGER:_Start() -- Get randum number of new RAT groups. local N=self:_RollDice(self.nrat, self.ntot, self.min, self.alive) - + -- Loop over all RAT objects and spawn groups. local time=0.0 for i=1,self.nrat do @@ -68082,26 +75221,26 @@ function RATMANAGER:_Start() SCHEDULER:New(nil, RAT._SpawnWithRoute, {self.rat[i]}, time) end end - + -- Start activation scheduler for uncontrolled aircraft. - for i=1,self.nrat do + for i=1,self.nrat do if self.rat[i].uncontrolled and self.rat[i].activate_uncontrolled then -- Start activating stuff but not before the latest spawn has happend. - local Tactivate=math.max(time+1, self.rat[i].activate_delay) + local Tactivate=math.max(time+1, self.rat[i].activate_delay) SCHEDULER:New(self.rat[i], self.rat[i]._ActivateUncontrolled, {self.rat[i]}, Tactivate, self.rat[i].activate_delta, self.rat[i].activate_frand) end end - + -- Start the manager. But not earlier than the latest spawn has happened! local TstartManager=math.max(time+1, self.Tcheck) - + -- Start manager scheduler. self.manager, self.managerid = SCHEDULER:New(self, self._Manage, {self}, TstartManager, self.Tcheck) --Core.Scheduler#SCHEDULER - + -- Info local text=string.format(RATMANAGER.id.."Starting RAT manager with scheduler ID %s in %d seconds. Repeat interval %d seconds.", self.managerid, TstartManager, self.Tcheck) self:E(text) - + return self end @@ -68150,14 +75289,14 @@ function RATMANAGER:_Manage() -- Count total number of groups. local ntot=self:_Count() - + -- Debug info. local text=string.format("Number of alive groups %d. New groups to be spawned %d.", ntot, self.ntot-ntot) self:T(RATMANAGER.id..text) - + -- Get number of necessary spawns. local N=self:_RollDice(self.nrat, self.ntot, self.min, self.alive) - + -- Loop over all RAT objects and spawn new groups if necessary. local time=0.0 for i=1,self.nrat do @@ -68174,13 +75313,13 @@ function RATMANAGER:_Count() -- Init total counter. local ntotal=0 - + -- Loop over all RAT objects. for i=1,self.nrat do local n=0 - + local ratobject=self.rat[i] --#RAT - + -- Loop over the RAT groups of this object. for spawnindex,ratcraft in pairs(ratobject.ratcraft) do local group=ratcraft.group --Wrapper.Group#GROUP @@ -68188,18 +75327,18 @@ function RATMANAGER:_Count() n=n+1 end end - + -- Alive groups of this RAT object. self.alive[i]=n - + -- Grand total. ntotal=ntotal+n - + -- Debug output. local text=string.format("Number of alive groups of %s = %d", self.name[i], n) self:T(RATMANAGER.id..text) end - + -- Return grand total. return ntotal end @@ -68211,7 +75350,7 @@ end -- @param #table min Minimum number of groups for each RAT object. -- @param #table alive Number of alive groups of each RAT object. function RATMANAGER:_RollDice(nrat,ntot,min,alive) - + -- Calculate sum. local function sum(A,index) local summe=0 @@ -68219,8 +75358,8 @@ function RATMANAGER:_RollDice(nrat,ntot,min,alive) summe=summe+A[i] end return summe - end - + end + -- Table of number of groups. local N={} local M={} @@ -68230,57 +75369,57 @@ function RATMANAGER:_RollDice(nrat,ntot,min,alive) M[#M+1]=math.max(alive[i], min[i]) P[#P+1]=math.max(min[i]-alive[i],0) end - + -- Min/max group arrays. local mini={} local maxi={} - + -- Arrays. local rattab={} for i=1,nrat do table.insert(rattab,i) end local done={} - + -- Number of new groups to be added. local nnew=ntot for i=1,nrat do nnew=nnew-alive[i] end - + for i=1,nrat-1 do - + -- Random entry from . local r=math.random(#rattab) -- Get value local j=rattab[r] - + table.remove(rattab, r) table.insert(done,j) - + -- Sum up the number of already distributed groups. local sN=sum(N, done) - -- Sum up the minimum number of yet to be distributed groups. + -- Sum up the minimum number of yet to be distributed groups. local sP=sum(P, rattab) - + -- Max number that can be distributed for this object. maxi[j]=nnew-sN-sP - + -- Min number that should be distributed for this object mini[j]=P[j] - + -- Random number of new groups for this RAT object. if maxi[j] >= mini[j] then N[j]=math.random(mini[j], maxi[j]) else N[j]=0 end - + -- Debug info self:T3(string.format("RATMANAGER: i=%d, alive=%d, min=%d, mini=%d, maxi=%d, add=%d, sumN=%d, sumP=%d", j, alive[j], min[j], mini[j], maxi[j], N[j],sN, sP)) - + end - + -- Last RAT object, number of groups is determined from number of already distributed groups and nnew. local j=rattab[1] N[j]=nnew-sum(N, done) @@ -68288,7 +75427,7 @@ function RATMANAGER:_RollDice(nrat,ntot,min,alive) maxi[j]=nnew-sum(N, done) table.remove(rattab, 1) table.insert(done,j) - + -- Debug info local text=RATMANAGER.id.."\n" for i=1,nrat do @@ -68296,11 +75435,10 @@ function RATMANAGER:_RollDice(nrat,ntot,min,alive) end text=text..string.format("Total # of groups to add = %d", sum(N, done)) self:T(text) - + -- Return number of groups to be spawned. return N end - --- **Functional** - Range Practice. -- -- === @@ -68322,23 +75460,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) @@ -68348,16 +75486,16 @@ end -- ### Author: **[funkyfranky](https://forums.eagle.ru/member.php?u=115026)** -- -- ### Contributions: [FlightControl](https://forums.eagle.ru/member.php?u=89536), [Ciribob](https://forums.eagle.ru/member.php?u=112175) --- +-- ### SRS Additions: Applevangelist +-- -- === -- @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. @@ -68380,13 +75518,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. @@ -68402,10 +75540,17 @@ 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. +-- @field Sound.SRS#MSRS controlmsrs +-- @field Sound.SRS#MSRSQUEUE controlsrsQ +-- @field Sound.SRS#MSRS instructmsrs +-- @field Sound.SRS#MSRSQUEUE instructsrsQ -- @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) @@ -68429,17 +75574,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 @{Wrapper.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! -- @@ -68450,27 +75595,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 @{Wrapper.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. @@ -68486,60 +75630,75 @@ 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. +-- +-- ## Voice output via SRS -- +-- Alternatively, the voice output can be fully done via SRS, **no sound file additions needed**. Set up SRS with @{#RANGE.SetSRS}(). Range control and instructor frequencies and voices can then be +-- set via @{#RANGE.SetSRSRangeControl}() and @{#RANGE.SetSRSRangeInstructor}() +-- -- # 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. -- @@ -68558,9 +75717,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) @@ -68570,6 +75729,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 @@ -68582,78 +75742,78 @@ end -- BASE:TraceLevel(1) -- BASE:TraceClass("RANGE") -- --- To get even more output you can increase the trace level to 2 or even 3, c.f. @{BASE} for more details. +-- To get even more output you can increase the trace level to 2 or even 3, c.f. @{Core.Base#BASE} for more details. -- -- The function @{#RANGE.DebugON}() can be used to send messages on screen. It also smokes all defined strafe and bombing targets, the strafe pit approach boxes and the range zone. -- -- 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. @@ -68661,10 +75821,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. @@ -68701,6 +75861,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. @@ -68712,6 +75880,32 @@ RANGE.TargetType={ -- @field #string airframe Aircraft type of player. -- @field #number time Time via timer.getAbsTime() in seconds of impact. -- @field #string date OS date. +-- @field #number attackHdg Attack heading in degrees. +-- @field #number attackVel Attack velocity in knots. +-- @field #number attackAlt Attack altitude in feet. +-- @field #string clock Time of the run. +-- @field #string rangename Name of the range. + +--- 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. +-- @field #string name Name of the target pit. +-- @field #number roundsFired Number of rounds fired. +-- @field #number roundsHit Number of rounds that hit the target. +-- @field #number strafeAccuracy Accuracy of the run in percent. +-- @field #string clock Time of the run. +-- @field #string rangename Name of the range. +-- @field #boolean invalid Invalid pass. + +--- 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 @@ -68765,110 +75959,109 @@ 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.5.1" ---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. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- 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 + 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. @@ -68876,6 +76069,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. @@ -68933,6 +76128,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 @@ -68983,136 +76209,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 - + if self.rangecontrolfreq and not self.useSRS 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 - + if self.instructorfreq and not self.useSRS 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() @@ -69124,10 +76350,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 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -69136,10 +76362,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 @@ -69147,8 +76373,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 @@ -69156,8 +76382,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 @@ -69165,7 +76391,7 @@ end -- @param #RANGE self -- @return #RANGE self function RANGE:SetAutosaveOn() - self.autosave=true + self.autosave = true return self end @@ -69173,7 +76399,36 @@ 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 + +--- Set FunkMan socket. Bombing and strafing results will be send to your Discord bot. +-- **Requires running FunkMan program**. +-- @param #RANGE self +-- @param #number Port Port. Default `10042`. +-- @param #string Host Host. Default "127.0.0.1". +-- @return #RANGE self +function RANGE:SetFunkManOn(Port, Host) + + self.funkmanSocket=SOCKET:New(Port, Host) + return self end @@ -69182,9 +76437,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 @@ -69192,8 +76447,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 @@ -69201,8 +76456,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 @@ -69210,11 +76465,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 @@ -69223,8 +76478,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 @@ -69233,8 +76488,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 @@ -69243,8 +76498,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 @@ -69252,8 +76507,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 @@ -69261,8 +76516,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 @@ -69270,8 +76525,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 @@ -69279,8 +76534,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 @@ -69288,8 +76543,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 @@ -69297,7 +76552,7 @@ end -- @param #RANGE self -- @return #RANGE self function RANGE:DebugON() - self.Debug=true + self.Debug = true return self end @@ -69305,7 +76560,7 @@ end -- @param #RANGE self -- @return #RANGE self function RANGE:DebugOFF() - self.Debug=false + self.Debug = false return self end @@ -69313,7 +76568,7 @@ end -- @param #RANGE self -- @return #RANGE self function RANGE:SetMessagesOFF() - self.messages=false + self.messages = false return self end @@ -69321,16 +76576,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 @@ -69338,7 +76592,7 @@ end -- @param #RANGE self -- @return #RANGE self function RANGE:TrackBombsOFF() - self.trackbombs=false + self.trackbombs = false return self end @@ -69346,7 +76600,7 @@ end -- @param #RANGE self -- @return #RANGE self function RANGE:TrackRocketsON() - self.trackrockets=true + self.trackrockets = true return self end @@ -69354,7 +76608,7 @@ end -- @param #RANGE self -- @return #RANGE self function RANGE:TrackRocketsOFF() - self.trackrockets=false + self.trackrockets = false return self end @@ -69362,7 +76616,7 @@ end -- @param #RANGE self -- @return #RANGE self function RANGE:TrackMissilesON() - self.trackmissiles=true + self.trackmissiles = true return self end @@ -69370,184 +76624,271 @@ end -- @param #RANGE self -- @return #RANGE self function RANGE:TrackMissilesOFF() - self.trackmissiles=false + self.trackmissiles = false + return self +end + +--- Use SRS Simple-Text-To-Speech for transmissions. No sound files necessary. +-- @param #RANGE self +-- @param #string PathToSRS Path to SRS directory. +-- @param #number Port SRS port. Default 5002. +-- @param #number Coalition Coalition side, e.g. coalition.side.BLUE or coalition.side.RED +-- @param #number Frequency Frequency to use, defaults to 256 (same as rangecontrol) +-- @param #number Modulation Modulation to use, defaults to radio.modulation.AM +-- @param #number Volume Volume, between 0.0 and 1.0. Defaults to 1.0 +-- @param #string PathToGoogleKey Path to Google TTS credentials. +-- @return #RANGE self +function RANGE:SetSRS(PathToSRS, Port, Coalition, Frequency, Modulation, Volume, PathToGoogleKey) + if PathToSRS then + + self.useSRS=true + + self.controlmsrs=MSRS:New(PathToSRS, Frequency or 256, Modulation or radio.modulation.AM, Volume or 1.0) + self.controlmsrs:SetPort(Port) + self.controlmsrs:SetCoalition(Coalition or coalition.side.BLUE) + self.controlmsrs:SetLabel("RANGEC") + self.controlsrsQ = MSRSQUEUE:New("CONTROL") + + self.instructmsrs=MSRS:New(PathToSRS, Frequency or 305, Modulation or radio.modulation.AM, Volume or 1.0) + self.instructmsrs:SetPort(Port) + self.instructmsrs:SetCoalition(Coalition or coalition.side.BLUE) + self.instructmsrs:SetLabel("RANGEI") + self.instructsrsQ = MSRSQUEUE:New("INSTRUCT") + + if PathToGoogleKey then + self.instructmsrs:SetGoogle(PathToGoogleKey) + self.instructmsrs:SetGoogle(PathToGoogleKey) + end + + else + self:E(self.lid..string.format("ERROR: No SRS path specified!")) + end return self end +--- (SRS) Set range control frequency and voice. +-- @param #RANGE self +-- @param #number frequency Frequency in MHz. Default 256 MHz. +-- @param #number modulation Modulation, defaults to radio.modulation.AM. +-- @param #string voice Voice. +-- @param #string culture Culture, defaults to "en-US". +-- @param #string gender Gender, defaults to "female". +-- @param #string relayunitname Name of the unit used for transmission location. +-- @return #RANGE self +function RANGE:SetSRSRangeControl( frequency, modulation, voice, culture, gender, relayunitname ) + self.rangecontrolfreq = frequency or 256 + self.controlmsrs:SetFrequencies(self.rangecontrolfreq) + self.controlmsrs:SetModulations(modulation or radio.modulation.AM) + self.controlmsrs:SetVoice(voice) + self.controlmsrs:SetCulture(culture or "en-US") + self.controlmsrs:SetGender(gender or "female") + self.rangecontrol = true + if relayunitname then + local unit = UNIT:FindByName(relayunitname) + local Coordinate = unit:GetCoordinate() + self.rangecontrolrelayname = relayunitname + end + return self +end ---- Enable range control and set frequency. +--- (SRS) Set range instructor frequency and voice. +-- @param #RANGE self +-- @param #number frequency Frequency in MHz. Default 305 MHz. +-- @param #number modulation Modulation, defaults to radio.modulation.AM. +-- @param #string voice Voice. +-- @param #string culture Culture, defaults to "en-US". +-- @param #string gender Gender, defaults to "male". +-- @param #string relayunitname Name of the unit used for transmission location. +-- @return #RANGE self +function RANGE:SetSRSRangeInstructor( frequency, modulation, voice, culture, gender, relayunitname ) + self.instructorfreq = frequency or 305 + self.instructmsrs:SetFrequencies(self.instructorfreq) + self.instructmsrs:SetModulations(modulation or radio.modulation.AM) + self.instructmsrs:SetVoice(voice) + self.instructmsrs:SetCulture(culture or "en-US") + self.instructmsrs:SetGender(gender or "male") + self.instructor = true + if relayunitname then + local unit = UNIT:FindByName(relayunitname) + local Coordinate = unit:GetCoordinate() + self.instructmsrs:SetCoordinate(Coordinate) + self.instructorrelayname = relayunitname + end + return self +end + +--- Enable range control and set frequency (non-SRS). -- @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 ---- Enable instructor radio and set frequency. +--- Enable instructor radio and set frequency (non-SRS). -- @param #RANGE self -- @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() - - 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 + -- _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 -- 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. @@ -69561,29 +76902,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 @@ -69591,36 +76932,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) - - 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) + 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, randommove ) 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 @@ -69634,77 +76975,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) - 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 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() 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 @@ -69715,16 +77055,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 @@ -69737,42 +77077,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 @@ -69783,120 +77123,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 @@ -69906,18 +77243,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 @@ -69925,11 +77262,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 @@ -69938,30 +77275,35 @@ 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 + -- DONE - SRS output + local text = string.format( "%s, Invalid hit!\nYou already passed foul line distance of %d m for target %s.", self:_myname( _unitName ), _d, targetname ) + if self.useSRS then + local ttstext = string.format( "%s, Invalid hit! You already passed foul line distance of %d meters for target %s.", self:_myname( _unitName ), _d, targetname ) + self.controlsrsQ:NewTransmission(ttstext,nil,self.controlmsrs,nil,2) + end + self:_DisplayMessageToGroup( _unit, text ) + self:T2( self.id .. text ) + _currentTarget.pastfoulline = true end end @@ -69970,9 +77312,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 @@ -69981,11 +77323,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 @@ -69993,43 +77335,54 @@ 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 -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 - + + if EventData.IniPlayerName == 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) @@ -70038,39 +77391,43 @@ 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 ) + + -- 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 + 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 ---------------------------- @@ -70078,11 +77435,10 @@ 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 - else ----------------------------- @@ -70090,55 +77446,58 @@ 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 --Core.Point#COORDINATE + 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 + local bombtarget=_bombtarget --#RANGE.BombTarget -- 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 + _closetTarget = bombtarget + _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 + elseif _distance <= bombtarget.goodhitrange then _hitquality = "GOOD" - elseif _distance <= 2*_bombtarget.goodhitrange then + elseif _distance <= 2 * bombtarget.goodhitrange then _hitquality = "INEFFECTIVE" else _hitquality = "POOR" @@ -70152,44 +77511,65 @@ 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 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 _results = self.bombPlayerResults[_playername] + + local result = {} -- #RANGE.BombResult + result.command=SOCKET.DataType.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.clock = UTILS.SecondsToClock(result.time, true) + result.midate = UTILS.GetDCSMissionDate() + result.theatre = env.mission.theatre + 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) + 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) - + -- DONE SRS message + local _message = string.format( "%s, weapon impacted too far from nearest range target (>%.1f km). No score!", _callsign, self.scorebombdistance / 1000 ) + if self.useSRS then + local ttstext = string.format( "%s, weapon impacted too far from nearest range target, mor than %.1f kilometer. No score!", _callsign, self.scorebombdistance / 1000 ) + self.controlsrsQ:NewTransmission(ttstext,nil,self.controlmsrs,nil,2) + end + 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) + -- weapon impacted too far from the nearest target! No Score! + if self.useSRS then + self.controlsrsQ:NewTransmission(_message,nil,self.controlmsrs,nil,1) + else + self.rangecontrol:NewTransmission( RANGE.Sound.RCWeaponImpactedTooFar.filename, RANGE.Sound.RCWeaponImpactedTooFar.duration, self.soundpath, nil, nil, _message, self.subduration ) + end 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 @@ -70197,10 +77577,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 @@ -70213,47 +77593,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. @@ -70262,23 +77641,31 @@ 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), ".") - - -- 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]) + if self.useSRS then + local text = string.format("You entered the bombing range. For hit assessment, contact the range controller at %.3f MHz", self.rangecontrolfreq) + local ttstext = string.format("You entered the bombing range. For hit assessment, contact the range controller at %.3f mega hertz.", self.rangecontrolfreq) + local group = player.client:GetGroup() + self.instructsrsQ:NewTransmission(ttstext,nil,self.instructmsrs,nil,1,{group},text,10) + else + -- Range control radio frequency split. + local RF = UTILS.Split( string.format( "%.3f", self.rangecontrolfreq ), "." ) + + -- Radio message that player entered the range + -- You entered the bombing range. For hit assessment, contact the range controller at xy MHz + 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 ) end - self.instructor:NewTransmission(RANGE.Sound.IRMegaHertz.filename, RANGE.Sound.IRMegaHertz.duration, self.soundpath) end - + end --- Function called after player leaves the range zone. @@ -70287,15 +77674,21 @@ 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) + -- You left the bombing range zone. Have a nice day! + if self.useSRS then + local text = "You left the bombing range zone. Have a nice day!" + local group = player.client:GetGroup() + self.instructsrsQ:NewTransmission(text,nil,self.instructmsrs,nil,1,{group},text,10) + else + self.instructor:NewTransmission( RANGE.Sound.IRExitRange.filename, RANGE.Sound.IRExitRange.duration, self.soundpath ) + end end end - --- Function called after bomb impact on range. -- @param #RANGE self -- @param #string From From state. @@ -70303,53 +77696,82 @@ 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 + 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 (%d m)", player.playername, result.radial, UTILS.MetersToFeet( result.distance ), 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.."." + 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) + + if self.useSRS then + local group = player.client:GetGroup() + self.controlsrsQ:NewTransmission(text,nil,self.controlmsrs,nil,1,{group},text,10) + else + 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 + end -- Unit. - local unit=UNIT:FindByName(player.unitname) - - -- Send message. - self:_DisplayMessageToGroup(unit, text, nil, true) - self:T(self.id..text) + if player.unitname and not self.useSRS 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 self:Save() end + + -- Send result to FunkMan, which creates fancy MatLab figures and sends them to Discord via a bot. + if self.funkmanSocket then + self.funkmanSocket:SendTable(result) + end + +end + +--- Function called after strafing run. +-- @param #RANGE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #RANGE.PlayerData player Player data table. +-- @param #RANGE.StrafeResult result Result of run. +function RANGE:onafterStrafeResult( From, Event, To, player, result) + + if self.funkmanSocket then + self.funkmanSocket:SendTable(result) + end end @@ -70358,11 +77780,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 @@ -70372,50 +77794,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, true) + 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. @@ -70423,11 +77845,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 @@ -70437,128 +77859,199 @@ 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 ) + _args.coord:Smoke(_args.color) + --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, _multiplayer = 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.roundsHit > b.roundsHit + 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.roundsHit, result.name, result.roundsQuality) end -- 10 runs @@ -70567,26 +78060,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, _multiplayer ) 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, _multiplayer = self:_GetPlayerUnitAndName( _unitName ) -- Check if we have a unit which is a player. if _unit and _playername then @@ -70595,241 +78088,269 @@ 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 - if _best == nil or _result.hits > _best.hits then + for _, _result in pairs( _results ) do + if _best == nil or _result.roundsHit > _best.roundsHit then _best = _result end end -- 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.roundsHit, _best.name, _best.roundsQuality ) + table.insert( _playerResults, { msg = text, hits = _best.roundsHit } ) 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, _multiplayer ) 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, _multiplayer = 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, _multiplayer ) 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, _multiplayer = 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, _multiplayer ) 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, _multiplayer = self:_GetPlayerUnitAndName( _unitname ) -- Check if we have a player. if unit and playername then - + self:I(playername) -- 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() ) + alive = relay:IsAlive() and "ok" or "N/A" + 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() ) + alive = relay:IsAlive() and "ok" or "N/A" + 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, _multiplayer ) -- Debug output. - self:T2(self.id..text) + self:T2( self.id .. text ) end end end @@ -70837,150 +78358,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, _multiplayer = 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, _multiplayer ) 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, _multiplayer = 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, _multiplayer ) 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, _multiplayer = 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, _multiplayer ) -- 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 @@ -70992,23 +78511,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 @@ -71017,9 +78536,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 @@ -71031,64 +78550,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 @@ -71096,68 +78618,120 @@ 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) + if self.useSRS then + local group = _unit:GetGroup() + local text = "You left the strafing zone too quickly! No score!" + --self.controlsrsQ:NewTransmission(text,nil,self.controlmsrs,nil,1,{group},text,10) + self.controlsrsQ:NewTransmission(text,nil,self.controlmsrs,nil,1) + else + -- You left the strafing zone too quickly! No score! + self.rangecontrol:NewTransmission( RANGE.Sound.RCLeftStrafePitTooQuickly.filename, RANGE.Sound.RCLeftStrafePitTooQuickly.duration, self.soundpath ) + end 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 - - -- Judge this pass. Text is displayed on summary. - if _result.hits >= _result.zone.goodPass*2 then - _result.text = "EXCELLENT PASS" - _sound=RANGE.Sound.RCExcellentPass - elseif _result.hits >= _result.zone.goodPass then - _result.text = "GOOD PASS" - _sound=RANGE.Sound.RCGoodPass - elseif _result.hits >= _result.zone.goodPass/2 then - _result.text = "INEFFECTIVE PASS" - _sound=RANGE.Sound.RCIneffectivePass - else - _result.text = "POOR PASS" - _sound=RANGE.Sound.RCPoorPass - end - + local _result = self.strafeStatus[_unitID] --#RANGE.StrafeStatus + + local _sound = nil -- #RANGE.Soundfile + -- 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 + 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 ) + local ttstext = 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 ) + ttstext = ttstext .. string.format( ". Total rounds fired %d. Accuracy %.1f percent.", shots, accur ) end - _text=_text..string.format("\n%s", _result.text) + _text = _text .. string.format( "\n%s", resulttext ) + ttstext = ttstext .. string.format( " %s", resulttext ) -- Send message. - self:_DisplayMessageToGroup(_unit, _text) + self:_DisplayMessageToGroup( _unit, _text ) + + -- Strafe result. + local result = {} -- #RANGE.StrafeResult + result.command=SOCKET.DataType.STRAFERESULT + result.player=_playername + result.name=_result.zone.name or "unknown" + result.time = timer.getAbsTime() + result.clock = UTILS.SecondsToClock(result.time) + result.midate = UTILS.GetDCSMissionDate() + result.theatre = env.mission.theatre + 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 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) + if self.rangecontrol then + if self.useSRS then + self.controlsrsQ:NewTransmission(ttstext,nil,self.controlmsrs,nil,1) + else + 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 ) + end + self.rangecontrol:NewTransmission( _sound.filename, _sound.duration, self.soundpath, nil, 0.5 ) end - self.rangecontrol:NewTransmission(_sound.filename, _sound.duration, self.soundpath, nil, 0.5) end -- Set strafe status to nil. @@ -71165,7 +78739,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 @@ -71174,32 +78748,40 @@ 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 + if self.useSRS then + self.controlsrsQ:NewTransmission(_msg,nil,self.controlmsrs,nil,1) + else + self.rangecontrol:NewTransmission( RANGE.Sound.RCRollingInOnStrafeTarget.filename, RANGE.Sound.RCRollingInOnStrafeTarget.duration, self.soundpath ) + end + 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 @@ -71219,18 +78801,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 @@ -71240,7 +78822,7 @@ function RANGE:_AddF10Commands(_unitName) self.MenuAddedTo[_gid] = true -- Range root menu path. - local _rangePath=nil + local _rangePath = nil if RANGE.MenuF10Root then @@ -71248,7 +78830,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 @@ -71258,61 +78841,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 or "N/A") 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 or "N/A") end end @@ -71325,80 +78910,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 @@ -71410,46 +78994,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 @@ -71457,67 +79041,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 @@ -71528,19 +79112,20 @@ 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}) +-- @param #boolean _togroup If true, display the message to the group in any case +function RANGE:_DisplayMessageToGroup( _unit, _text, _time, _clear, display, _togroup ) + 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 @@ -71548,22 +79133,28 @@ 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() + local _grp = _unit:GetGroup() -- 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 + if _togroup and _grp then + local m = MESSAGE:New(_text,_time,nil,_clear):ToGroup(_grp) + else + local m = MESSAGE:New(_text,_time,nil,_clear):ToUnit(_unit) + end 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 ) if _examinerid then - trigger.action.outTextForGroup(_examinerid, _text, _time, _clear) + local m = MESSAGE:New(_text,_time,nil,_clear):ToGroup(_examinerid) end end end @@ -71573,20 +79164,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, _multiplayer = 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.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) + self:_DisplayMessageToGroup( unit, text, 5, false, true ) end end @@ -71594,20 +79185,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, _multiplayer = 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.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) + self:_DisplayMessageToGroup( unit, text, 5, false, true ) end end @@ -71615,19 +79206,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, _multiplayer = 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, _multiplayer = 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 @@ -71635,20 +79269,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, _multiplayer = 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 @@ -71656,21 +79290,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 @@ -71678,17 +79312,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 @@ -71696,21 +79330,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 @@ -71719,14 +79353,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 @@ -71735,14 +79369,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 @@ -71751,22 +79385,22 @@ end -- @param #RANGE self -- @param Utilities.Utils#SMOKECOLOR color Color Id. -- @return #string Color text. -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" +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" else - txt=string.format("unknown color (%s)", tostring(color)) + txt = string.format( "unknown color (%s)", tostring( color ) ) end return txt @@ -71776,20 +79410,20 @@ end -- @param #RANGE self -- @param Utilities.Utils#FLARECOLOR color Color Id. -- @return #string Color text. -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" +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" else - txt=string.format("unknown color (%s)", tostring(color)) + txt = string.format( "unknown color (%s)", tostring( color ) ) end return txt @@ -71799,33 +79433,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. @@ -71836,17 +79470,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 @@ -71857,23 +79491,30 @@ end -- @param #string _unitName Name of the player unit. -- @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) +-- @return #boolean If true, group has > 1 player in it +function RANGE:_GetPlayerUnitAndName( _unitName ) + self:F2( _unitName ) if _unitName ~= nil then - + + local multiplayer = false + -- 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 + self:F2(playername) + local grp = unit:GetGroup() + if grp and grp:CountAliveUnits() > 1 then + multiplayer = true + end + return unit, playername, multiplayer end end @@ -71881,25 +79522,27 @@ function RANGE:_GetPlayerUnitAndName(_unitName) end -- Return nil if we could not find a player. - return nil,nil + return nil, 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) - - 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) +function RANGE:_myname( unitname ) + self:F2( unitname ) + local pname = "Ghost 1 1" + local unit = UNIT:FindByName( unitname ) + if unit and unit:IsAlive() then + local grp = unit:GetGroup() + if grp and grp:IsAlive() then + pname = grp:GetCustomCallSign(true,true) + end + end + return pname end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- **Functional (WIP)** -- Base class that models processes to achieve goals involving a Zone. +--- **Functional** - Base class that models processes to achieve goals involving a Zone. -- -- === -- @@ -71909,7 +79552,7 @@ end -- === -- -- ### Author: **FlightControl** --- ### Contributions: **funkyfranky** +-- ### Contributions: **funkyfranky**, **Applevangelist** -- -- === -- @@ -71927,7 +79570,6 @@ do -- Zone -- @field #boolean SmokeZone If true, smoke zone. -- @extends Core.Zone#ZONE_RADIUS - --- Models processes that have a Goal with a defined achievement involving a Zone. -- Derived classes implement the ways how the achievements can be realized. -- @@ -71954,21 +79596,26 @@ do -- Zone SmokeColor = nil, SmokeZone = nil, } - + --- ZONE_GOAL Constructor. -- @param #ZONE_GOAL self - -- @param Core.Zone#ZONE_RADIUS Zone A @{Zone} object with the goal to be achieved. + -- @param Core.Zone#ZONE_RADIUS Zone A @{Core.Zone} object with the goal to be achieved. Alternatively, can be handed as the name of late activated group describing a ZONE_POLYGON with its waypoints. -- @return #ZONE_GOAL function ZONE_GOAL:New( Zone ) - - local self = BASE:Inherit( self, ZONE_RADIUS:New( Zone:GetName(), Zone:GetVec2(), Zone:GetRadius() ) ) -- #ZONE_GOAL - self:F( { Zone = Zone } ) + BASE:I({Zone=Zone}) + local self = BASE:Inherit( self, BASE:New()) + if type(Zone) == "string" then + self = BASE:Inherit( self, ZONE_POLYGON:NewFromGroupName(Zone) ) + else + self = BASE:Inherit( self, ZONE_RADIUS:New( Zone:GetName(), Zone:GetVec2(), Zone:GetRadius() ) ) -- #ZONE_GOAL + self:F( { Zone = Zone } ) + end -- Goal object. self.Goal = GOAL:New() self.SmokeTime = nil - + -- Set smoke ON. self:SetSmokeZone(true) @@ -71982,7 +79629,7 @@ do -- Zone -- @function [parent=#ZONE_GOAL] __DestroyedUnit -- @param #ZONE_GOAL self -- @param #number delay Delay in seconds. - + --- DestroyedUnit Handler OnAfter for ZONE_GOAL -- @function [parent=#ZONE_GOAL] OnAfterDestroyedUnit -- @param #ZONE_GOAL self @@ -71994,15 +79641,15 @@ do -- Zone return self end - + --- Get the Zone. -- @param #ZONE_GOAL self -- @return #ZONE_GOAL function ZONE_GOAL:GetZone() return self end - - + + --- Get the name of the Zone. -- @param #ZONE_GOAL self -- @return #string @@ -72010,7 +79657,6 @@ do -- Zone return self:GetName() end - --- Activate smoking of zone with the color or the current owner. -- @param #ZONE_GOAL self -- @param #boolean switch If *true* or *nil* activate smoke. If *false* or *nil*, no smoke. @@ -72032,11 +79678,10 @@ do -- Zone -- @param DCS#SMOKECOLOR.Color SmokeColor function ZONE_GOAL:Smoke( SmokeColor ) self:F( { SmokeColor = SmokeColor} ) - + self.SmokeColor = SmokeColor end - - + --- Flare the zone boundary. -- @param #ZONE_GOAL self -- @param DCS#SMOKECOLOR.Color FlareColor @@ -72044,7 +79689,6 @@ do -- Zone self:FlareZone( FlareColor, 30) end - --- When started, check the Smoke and the Zone status. -- @param #ZONE_GOAL self function ZONE_GOAL:onafterGuard() @@ -72056,17 +79700,16 @@ do -- Zone end end - --- Check status Smoke. -- @param #ZONE_GOAL self function ZONE_GOAL:StatusSmoke() self:F({self.SmokeTime, self.SmokeColor}) - + if self.SmokeZone then - + -- Current time. local CurrentTime = timer.getTime() - + -- Restart smoke every 5 min. if self.SmokeTime == nil or self.SmokeTime + 300 <= CurrentTime then if self.SmokeColor then @@ -72074,11 +79717,10 @@ do -- Zone self.SmokeTime = CurrentTime end end - + end - - end + end --- @param #ZONE_GOAL self -- @param Core.Event#EVENTDATA EventData Event data table. @@ -72086,54 +79728,53 @@ do -- Zone self:F( { "EventDead", EventData } ) self:F( { EventData.IniUnit } ) - + if EventData.IniDCSUnit then local Vec3 = EventData.IniDCSUnit:getPosition().p self:F( { Vec3 = Vec3 } ) - + if Vec3 and self:IsVec3InZone(Vec3) then - + local PlayerHits = _DATABASE.HITS[EventData.IniUnitName] - + if PlayerHits then - + for PlayerName, PlayerHit in pairs( PlayerHits.Players or {} ) do self.Goal:AddPlayerContribution( PlayerName ) self:DestroyedUnit( EventData.IniUnitName, PlayerName ) end - + end - + end end - + end - - + --- Activate the event UnitDestroyed to be fired when a unit is destroyed in the zone. -- @param #ZONE_GOAL self function ZONE_GOAL:MonitorDestroyedUnits() self:HandleEvent( EVENTS.Dead, self.__Destroyed ) self:HandleEvent( EVENTS.Crash, self.__Destroyed ) - + end - + end ---- **Functional (WIP)** -- Base class that models processes to achieve goals involving a Zone for a Coalition. +--- **Functional** - Base class that models processes to achieve goals involving a Zone for a Coalition. -- -- === --- --- ZONE_GOAL_COALITION models processes that have a Goal with a defined achievement involving a Zone for a Coalition. +-- +-- ZONE_GOAL_COALITION models processes that have a Goal with a defined achievement involving a Zone for a Coalition. -- Derived classes implement the ways how the achievements can be realized. --- +-- -- === --- +-- -- ### Author: **FlightControl** --- +-- -- === --- +-- -- @module Functional.ZoneGoalCoalition -- @image MOOSE.JPG @@ -72147,68 +79788,66 @@ do -- ZoneGoal -- @field #table ObjectCategories Table of object categories that are able to hold a zone. Default is UNITS and STATICS. -- @extends Functional.ZoneGoal#ZONE_GOAL - - --- ZONE_GOAL_COALITION models processes that have a Goal with a defined achievement involving a Zone for a Coalition. + --- ZONE_GOAL_COALITION models processes that have a Goal with a defined achievement involving a Zone for a Coalition. -- Derived classes implement the ways how the achievements can be realized. - -- + -- -- ## 1. ZONE_GOAL_COALITION constructor - -- + -- -- * @{#ZONE_GOAL_COALITION.New}(): Creates a new ZONE_GOAL_COALITION object. - -- + -- -- ## 2. ZONE_GOAL_COALITION is a finite state machine (FSM). - -- + -- -- ### 2.1 ZONE_GOAL_COALITION States - -- + -- -- ### 2.2 ZONE_GOAL_COALITION Events - -- + -- -- ### 2.3 ZONE_GOAL_COALITION State Machine - -- + -- -- @field #ZONE_GOAL_COALITION ZONE_GOAL_COALITION = { - ClassName = "ZONE_GOAL_COALITION", - Coalition = nil, - PreviousCoaliton = nil, - UnitCategories = nil, + ClassName = "ZONE_GOAL_COALITION", + Coalition = nil, + PreviousCoalition = nil, + UnitCategories = nil, ObjectCategories = nil, } - + --- @field #table ZONE_GOAL_COALITION.States ZONE_GOAL_COALITION.States = {} - + --- ZONE_GOAL_COALITION Constructor. -- @param #ZONE_GOAL_COALITION self - -- @param Core.Zone#ZONE Zone A @{Zone} object with the goal to be achieved. + -- @param Core.Zone#ZONE Zone A @{Core.Zone} object with the goal to be achieved. -- @param DCSCoalition.DCSCoalition#coalition Coalition The initial coalition owning the zone. Default coalition.side.NEUTRAL. -- @param #table UnitCategories Table of unit categories. See [DCS Class Unit](https://wiki.hoggitworld.com/view/DCS_Class_Unit). Default {Unit.Category.GROUND_UNIT}. -- @return #ZONE_GOAL_COALITION function ZONE_GOAL_COALITION:New( Zone, Coalition, UnitCategories ) - + if not Zone then - BASE:E("ERROR: No Zone specified in ZONE_GOAL_COALITON!") + BASE:E( "ERROR: No Zone specified in ZONE_GOAL_COALITION!" ) return nil end - + -- Inherit ZONE_GOAL. local self = BASE:Inherit( self, ZONE_GOAL:New( Zone ) ) -- #ZONE_GOAL_COALITION - self:F( { Zone = Zone, Coalition = Coalition } ) + self:F( { Zone = Zone, Coalition = Coalition } ) -- Set initial owner. - self:SetCoalition( Coalition or coalition.side.NEUTRAL) - + self:SetCoalition( Coalition or coalition.side.NEUTRAL ) + -- Set default unit and object categories for the zone scan. - self:SetUnitCategories(UnitCategories) + self:SetUnitCategories( UnitCategories ) self:SetObjectCategories() - + return self end - --- Set the owning coalition of the zone. -- @param #ZONE_GOAL_COALITION self -- @param DCSCoalition.DCSCoalition#coalition Coalition The coalition ID, e.g. *coalition.side.RED*. -- @return #ZONE_GOAL_COALITION function ZONE_GOAL_COALITION:SetCoalition( Coalition ) - self.PreviousCoalition=self.Coalition or Coalition + self.PreviousCoalition = self.Coalition or Coalition self.Coalition = Coalition return self end @@ -72218,31 +79857,31 @@ do -- ZoneGoal -- @param #table UnitCategories Table of unit categories. See [DCS Class Unit](https://wiki.hoggitworld.com/view/DCS_Class_Unit). Default {Unit.Category.GROUND_UNIT}. -- @return #ZONE_GOAL_COALITION function ZONE_GOAL_COALITION:SetUnitCategories( UnitCategories ) - - if UnitCategories and type(UnitCategories)~="table" then - UnitCategories={UnitCategories} + + if UnitCategories and type( UnitCategories ) ~= "table" then + UnitCategories = { UnitCategories } end - - self.UnitCategories=UnitCategories or {Unit.Category.GROUND_UNIT} - + + self.UnitCategories = UnitCategories or { Unit.Category.GROUND_UNIT } + return self end - + --- Set the owning coalition of the zone. -- @param #ZONE_GOAL_COALITION self -- @param #table ObjectCategories Table of unit categories. See [DCS Class Object](https://wiki.hoggitworld.com/view/DCS_Class_Object). Default {Object.Category.UNIT, Object.Category.STATIC}, i.e. all UNITS and STATICS. -- @return #ZONE_GOAL_COALITION function ZONE_GOAL_COALITION:SetObjectCategories( ObjectCategories ) - - if ObjectCategories and type(ObjectCategories)~="table" then - ObjectCategories={ObjectCategories} + + if ObjectCategories and type( ObjectCategories ) ~= "table" then + ObjectCategories = { ObjectCategories } end - - self.ObjectCategories=ObjectCategories or {Object.Category.UNIT, Object.Category.STATIC} - + + self.ObjectCategories = ObjectCategories or { Object.Category.UNIT, Object.Category.STATIC } + return self - end - + end + --- Get the owning coalition of the zone. -- @param #ZONE_GOAL_COALITION self -- @return DCSCoalition.DCSCoalition#coalition Coalition. @@ -72250,43 +79889,41 @@ do -- ZoneGoal return self.Coalition end - --- Get the previous coaliton, i.e. the one owning the zone before the current one. + --- Get the previous coalition, i.e. the one owning the zone before the current one. -- @param #ZONE_GOAL_COALITION self -- @return DCSCoalition.DCSCoalition#coalition Coalition. function ZONE_GOAL_COALITION:GetPreviousCoalition() return self.PreviousCoalition end - --- Get the owning coalition name of the zone. -- @param #ZONE_GOAL_COALITION self -- @return #string Coalition name. function ZONE_GOAL_COALITION:GetCoalitionName() - return UTILS.GetCoalitionName(self.Coalition) + return UTILS.GetCoalitionName( self.Coalition ) end - --- Check status Coalition ownership. -- @param #ZONE_GOAL_COALITION self -- @return #ZONE_GOAL_COALITION function ZONE_GOAL_COALITION:StatusZone() - + -- Get current state. local State = self:GetState() - + -- Debug text. - local text=string.format("Zone state=%s, Owner=%s, Scanning...", State, self:GetCoalitionName()) - self:F(text) - + local text = string.format( "Zone state=%s, Owner=%s, Scanning...", State, self:GetCoalitionName() ) + self:F( text ) + -- Scan zone. self:Scan( self.ObjectCategories, self.UnitCategories ) - + return self end - + end ---- **Functional** -- Models the process to zone guarding and capturing. +--- **Functional** - Models the process to zone guarding and capturing. -- -- === -- @@ -72356,9 +79993,9 @@ do -- ZONE_CAPTURE_COALITION -- -- In order to use ZONE_CAPTURE_COALITION, you need to: -- - -- * Create a @{Zone} object from one of the ZONE_ classes. - -- Note that ZONE_POLYGON_ classes are not yet functional. - -- The only functional ZONE_ classses are those derived from a ZONE_RADIUS. + -- * Create a @{Core.Zone} object from one of the ZONE_ classes. + -- The functional ZONE_ classses are those derived from a ZONE_RADIUS. + -- In order to use a ZONE_POLYGON, hand over the **GROUP name** of a late activated group forming a polygon with it's waypoints. -- * Set the state of the zone. Most of the time, Guarded would be the initial state. -- * Start the zone capturing **monitoring process**. -- This will check the presence of friendly and/or enemy units within the zone and will transition the state of the zone when the tactical situation changed. @@ -72651,7 +80288,7 @@ do -- ZONE_CAPTURE_COALITION --- ZONE_CAPTURE_COALITION Constructor. -- @param #ZONE_CAPTURE_COALITION self - -- @param Core.Zone#ZONE Zone A @{Zone} object with the goal to be achieved. + -- @param Core.Zone#ZONE Zone A @{Core.Zone} object with the goal to be achieved. Alternatively, can be handed as the name of late activated group describing a @{ZONE_POLYGON} with its waypoints. -- @param DCSCoalition.DCSCoalition#coalition Coalition The initial coalition owning the zone. -- @param #table UnitCategories Table of unit categories. See [DCS Class Unit](https://wiki.hoggitworld.com/view/DCS_Class_Unit). Default {Unit.Category.GROUND_UNIT}. -- @param #table ObjectCategories Table of unit categories. See [DCS Class Object](https://wiki.hoggitworld.com/view/DCS_Class_Object). Default {Object.Category.UNIT, Object.Category.STATIC}, i.e. all UNITS and STATICS. @@ -73003,20 +80640,21 @@ do -- ZONE_CAPTURE_COALITION local UnitHit = EventData.TgtUnit + if UnitHit.ClassName ~= "SCENERY" then -- 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.Coalition then - - -- Update last hit time. - self.HitTimeLast=timer.getTime() - - -- Only trigger attacked event if not already in state "Attacked". - if self:GetState()~="Attacked" then - self:F2("Hit ==> Attack") - self:Attack() - end - + if UnitHit and UnitHit:IsInZone(self) and UnitHit:GetCoalition()==self.Coalition then + + -- Update last hit time. + self.HitTimeLast=timer.getTime() + + -- Only trigger attacked event if not already in state "Attacked". + if self:GetState()~="Attacked" then + self:F2("Hit ==> Attack") + self:Attack() + end + + end end - end end @@ -73091,7 +80729,7 @@ do -- ZONE_CAPTURE_COALITION return IsEmpty end - --- Check if zone is "Guarded", i.e. only one (the defending) coaliton is present inside the zone. + --- Check if zone is "Guarded", i.e. only one (the defending) coalition is present inside the zone. -- @param #ZONE_CAPTURE_COALITION self -- @return #boolean self:IsAllInZoneOfCoalition( self.Coalition ) function ZONE_CAPTURE_COALITION:IsGuarded() @@ -73113,7 +80751,7 @@ do -- ZONE_CAPTURE_COALITION return IsCaptured end - --- Check if zone is "Attacked", i.e. another coaliton entered the zone. + --- Check if zone is "Attacked", i.e. another coalition entered the zone. -- @param #ZONE_CAPTURE_COALITION self -- @return #boolean self:IsSomeInZoneOfCoalition( self.Coalition ) function ZONE_CAPTURE_COALITION:IsAttacked() @@ -73178,30 +80816,31 @@ do -- ZONE_CAPTURE_COALITION end -- Status text. - local text=string.format("CAPTURE ZONE %s: Owner=%s (Previous=%s): #blue=%d, #red=%d, Status %s", self:GetZoneName(), self:GetCoalitionName(), UTILS.GetCoalitionName(self:GetPreviousCoalition()), nBlue, nRed, State) - local NewState = self:GetState() - if NewState~=State then - text=text..string.format(" --> %s", NewState) + if false then + local text=string.format("CAPTURE ZONE %s: Owner=%s (Previous=%s): #blue=%d, #red=%d, Status %s", self:GetZoneName(), self:GetCoalitionName(), UTILS.GetCoalitionName(self:GetPreviousCoalition()), nBlue, nRed, State) + local NewState = self:GetState() + if NewState~=State then + text=text..string.format(" --> %s", NewState) + end + self:I(text) end - self:I(text) - + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Misc Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - --- Update Mark on F10 map. -- @param #ZONE_CAPTURE_COALITION self function ZONE_CAPTURE_COALITION:Mark() - + if self.MarkOn then - + local Coord = self:GetCoordinate() local ZoneName = self:GetZoneName() local State = self:GetState() - + -- Remove marks. if self.MarkRed then Coord:RemoveMark(self.MarkRed) @@ -73209,21 +80848,21 @@ do -- ZONE_CAPTURE_COALITION if self.MarkBlue then Coord:RemoveMark(self.MarkBlue) end - - -- Create new marks for each coaliton. + + -- Create new marks for each coalition. if self.Coalition == coalition.side.BLUE then - self.MarkBlue = Coord:MarkToCoalitionBlue( "Coalition: Blue\nGuard Zone: " .. ZoneName .. "\nStatus: " .. State ) + self.MarkBlue = Coord:MarkToCoalitionBlue( "Coalition: Blue\nGuard Zone: " .. ZoneName .. "\nStatus: " .. State ) self.MarkRed = Coord:MarkToCoalitionRed( "Coalition: Blue\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State ) elseif self.Coalition == coalition.side.RED then - self.MarkRed = Coord:MarkToCoalitionRed( "Coalition: Red\nGuard Zone: " .. ZoneName .. "\nStatus: " .. State ) + self.MarkRed = Coord:MarkToCoalitionRed( "Coalition: Red\nGuard Zone: " .. ZoneName .. "\nStatus: " .. State ) self.MarkBlue = Coord:MarkToCoalitionBlue( "Coalition: Red\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State ) else - self.MarkRed = Coord:MarkToCoalitionRed( "Coalition: Neutral\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State ) + self.MarkRed = Coord:MarkToCoalitionRed( "Coalition: Neutral\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State ) self.MarkBlue = Coord:MarkToCoalitionBlue( "Coalition: Neutral\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State ) end - + end - + end end @@ -73259,7 +80898,7 @@ end -- ### Contributions: [FlightControl](https://forums.eagle.ru/member.php?u=89536) -- -- ==== --- @module Functional.Arty +-- @module Functional.Artillery -- @image Artillery.JPG ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -73302,7 +80941,7 @@ end -- @field Core.Point#COORDINATE RearmingPlaceCoord Coordinates of the rearming place. If the place is more than 100 m away from the ARTY group, the group will go there. -- @field #boolean RearmingArtyOnRoad If true, ARTY group will move to rearming place using mainly roads. Default false. -- @field Core.Point#COORDINATE InitialCoord Initial coordinates of the ARTY group. --- @field #boolean report Arty group sends messages about their current state or target to its coaliton. +-- @field #boolean report Arty group sends messages about their current state or target to its coalition. -- @field #table ammoshells Table holding names of the shell types which are included when counting the ammo. Default is {"weapons.shells"} which include most shells. -- @field #table ammorockets Table holding names of the rocket types which are included when counting the ammo. Default is {"weapons.nurs"} which includes most unguided rockets. -- @field #table ammomissiles Table holding names of the missile types which are included when counting the ammo. Default is {"weapons.missiles"} which includes some guided missiles. @@ -73337,7 +80976,7 @@ end --- Enables mission designers easily to assign targets for artillery units. Since the implementation is based on a Finite State Model (FSM), the mission designer can -- interact with the process at certain events or states. -- --- A new ARTY object can be created with the @{#ARTY.New}(*group*) contructor. +-- A new ARTY object can be created with the @{#ARTY.New}(*group*) constructor. -- The parameter *group* has to be a MOOSE Group object and defines ARTY group. -- -- The ARTY FSM process can be started by the @{#ARTY.Start}() command. @@ -73375,7 +81014,7 @@ end -- When a new target is assigned via the @{#ARTY.AssignTargetCoord}() function (see below), the **NewTarget** event is triggered. -- -- ## Assigning Targets --- Assigning targets is a central point of the ARTY class. Multiple targets can be assigned simultanioulsly and are put into a queue. +-- Assigning targets is a central point of the ARTY class. Multiple targets can be assigned simultaneously and are put into a queue. -- Of course, targets can be added at any time during the mission. For example, once they are detected by a reconnaissance unit. -- -- In order to add a target, the function @{#ARTY.AssignTargetCoord}(*coord*, *prio*, *radius*, *nshells*, *maxengage*, *time*, *weapontype*, *name*) has to be used. @@ -73390,7 +81029,7 @@ end -- * *maxengage*: Number of times a target is engaged. -- * *time*: Time of day the engagement is schedule in the format "hh:mm:ss" for hh=hours, mm=minutes, ss=seconds. -- For example "10:15:35". In the case the attack will be executed at a quarter past ten in the morning at the day the mission started. --- If the engagement should start on the following day the format can be specified as "10:15:35+1", where the +1 denots the following day. +-- If the engagement should start on the following day the format can be specified as "10:15:35+1", where the +1 denotes the following day. -- This is useful for longer running missions or if the mission starts at 23:00 hours and the attack should be scheduled at 01:00 hours on the following day. -- Of course, later days are also possible by appending "+2", "+3", etc. -- **Note** that the time has to be given as a string. So the enclosing quotation marks "" are important. @@ -73408,7 +81047,7 @@ end -- Let's first consider the case that none of the targets is scheduled to be executed at a certain time (*time*=nil). -- The ARTY group will first engage the target with higher priority (*prio*=10). After the engagement is finished, the target with lower priority is attacked. -- This is because the target with lower prio has been attacked one time less. After the attack on the lower priority task is finished and both targets --- have been engaged equally often, the target with the higher priority is engaged again. This coninues until a target has engaged three times. +-- have been engaged equally often, the target with the higher priority is engaged again. This continues until a target has engaged three times. -- Once the maximum number of engagements is reached, the target is deleted from the queue. -- -- In other words, the queue is first sorted with respect to the number of engagements and targets with the same number of engagements are sorted with @@ -73419,7 +81058,7 @@ end -- As mentioned above, targets can be engaged at a specific time of the day via the *time* parameter. -- -- If the *time* parameter is specified for a target, the first engagement of that target will happen at that time of the day and not before. --- This also applies when multiple engagements are requested via the *maxengage* parameter. The first attack will not happen before the specifed time. +-- This also applies when multiple engagements are requested via the *maxengage* parameter. The first attack will not happen before the specified time. -- When that timed attack is finished, the *time* parameter is deleted and the remaining engagements are carried out in the same manner as for untimed targets (described above). -- -- Of course, it can happen that a scheduled task should be executed at a time, when another target is already under attack. @@ -73430,7 +81069,7 @@ end -- -- ## Determining the Amount of Ammo -- --- In order to determin when a unit is out of ammo and possible initiate the rearming process it is necessary to know which types of weapons have to be counted. +-- In order to determine when a unit is out of ammo and possible initiate the rearming process it is necessary to know which types of weapons have to be counted. -- For most artillery unit types, this is simple because they only have one type of weapon and hence ammunition. -- -- However, there are more complex scenarios. For example, naval units carry a big arsenal of different ammunition types ranging from various cannon shell types @@ -73446,7 +81085,7 @@ end -- **Note** that the default parameters "weapons.shells", "weapons.nurs", "weapons.missiles" **should in priciple** capture all the corresponding ammo types. -- However, the logic searches for the string "weapon.missies" in the ammo type. Especially for missiles, this string is often not contained in the ammo type descriptor. -- --- One way to determin which types of ammo the unit carries, one can use the debug mode of the arty class via @{#ARTY.SetDebugON}(). +-- One way to determine which types of ammo the unit carries, one can use the debug mode of the arty class via @{#ARTY.SetDebugON}(). -- In debug mode, the all ammo types of the group are printed to the monitor as message and can be found in the DCS.log file. -- -- ## Employing Selected Weapons @@ -73503,7 +81142,7 @@ end -- -- ## Simulated Weapons -- --- In addtion to the standard weapons a group has available some special weapon types that are not possible to use in the native DCS environment are simulated. +-- In addition to the standard weapons a group has available some special weapon types that are not possible to use in the native DCS environment are simulated. -- -- ### Tactical Nukes -- @@ -73512,9 +81151,9 @@ end -- -- By default, they group does not have any nukes available. To give the group the ability the function @{#ARTY.SetTacNukeShells}(*n*) can be used. -- This supplies the group with *n* nuclear shells, where *n* is restricted to the number of conventional shells the group can carry. --- Note that the group must always have convenctional shells left in order to fire a nuclear shell. +-- Note that the group must always have conventional shells left in order to fire a nuclear shell. -- --- The default explostion strength is 0.075 kilo tons TNT. The can be changed with the @{#ARTY.SetTacNukeWarhead}(*strength*), where *strength* is given in kilo tons TNT. +-- The default explosion strength is 0.075 kilo tons TNT. The can be changed with the @{#ARTY.SetTacNukeWarhead}(*strength*), where *strength* is given in kilo tons TNT. -- -- ### Illumination Shells -- @@ -73530,12 +81169,12 @@ end -- -- ### Smoke Shells -- --- In a similar way to illumination shells, ARTY groups can also employ smoke shells. The numer of smoke shells the group has available is set by the function +-- In a similar way to illumination shells, ARTY groups can also employ smoke shells. The number of smoke shells the group has available is set by the function -- @{#ARTY.SetSmokeShells}(*n*, *color*), where *n* is the number of shells and *color* defines the smoke color. Default is SMOKECOLOR.Red. -- -- The weapon type to be used in the @{#ARTY.AssignTargetCoord}() function is *ARTY.WeaponType.SmokeShells*. -- --- The explosive shell the group fired is destroyed shortly before its impact on the ground and smoke of the speficied color is triggered at that position. +-- The explosive shell the group fired is destroyed shortly before its impact on the ground and smoke of the specified color is triggered at that position. -- -- -- ## Assignments via Markers on F10 Map @@ -73549,15 +81188,15 @@ end -- ### Target Assignments -- A new target can be assigned by writing **arty engage** in the marker text. -- This is followed by a **comma separated list** of (optional) keywords and parameters. --- First, it is important to address the ARTY group or groups that should engage. This can be done in numrous ways. The keywords are *battery*, *alias*, *cluster*. +-- First, it is important to address the ARTY group or groups that should engage. This can be done in numerous ways. The keywords are *battery*, *alias*, *cluster*. -- It is also possible to address all ARTY groups by the keyword *everyone* or *allbatteries*. These two can be used synonymously. -- **Note that**, if no battery is assigned nothing will happen. -- -- * *everyone* or *allbatteries* The target is assigned to all batteries. -- * *battery* Name of the ARTY group that the target is assigned to. Note that **the name is case sensitive** and has to be given in quotation marks. Default is all ARTY groups of the right coalition. -- * *alias* Alias of the ARTY group that the target is assigned to. The alias is **case sensitive** and needs to be in quotation marks. --- * *cluster* The cluster of ARTY groups that is addessed. Clusters can be defined by the function @{#ARTY.AddToCluster}(*clusters*). Names are **case sensitive** and need to be in quotation marks. --- * *key* A number to authorize the target assignment. Only specifing the correct number will trigger an engagement. +-- * *cluster* The cluster of ARTY groups that is addressed. Clusters can be defined by the function @{#ARTY.AddToCluster}(*clusters*). Names are **case sensitive** and need to be in quotation marks. +-- * *key* A number to authorize the target assignment. Only specifying the correct number will trigger an engagement. -- * *time* Time for which which the engagement is schedules, e.g. 08:42. Default is as soon as possible. -- * *prio* Priority of the engagement as number between 1 (high prio) and 100 (low prio). Default is 50, i.e. medium priority. -- * *shots* Number of shots (shells, rockets or missiles) fired at each engagement. Default is 5. @@ -73582,8 +81221,8 @@ end -- arty engage, battery "Paladin Alpha", weapon nukes, shots 1, time 20:15 -- arty engage, battery "Horwitzer 1", lldms 41:51:00N 41:47:58E -- --- Note that the keywords and parameters are *case insensitve*. Only exception are the battery, alias and cluster names. --- These must be exactly the same as the names of the goups defined in the mission editor or the aliases and cluster names defined in the script. +-- Note that the keywords and parameters are *case insensitive*. Only exception are the battery, alias and cluster names. +-- These must be exactly the same as the names of the groups defined in the mission editor or the aliases and cluster names defined in the script. -- -- ### Relocation Assignments -- @@ -73592,11 +81231,11 @@ end -- * *time* Time for which which the relocation/move is schedules, e.g. 08:42. Default is as soon as possible. -- * *speed* The speed in km/h the group will drive at. Default is 70% of its max possible speed. -- * *on road* Group will use mainly roads. Default is off, i.e. it will go in a straight line from its current position to the assigned coordinate. --- * *canceltarget* Group will cancel all running firing engagements and immidiately start to move. Default is that group will wait until is current assignment is over. +-- * *canceltarget* Group will cancel all running firing engagements and immediately start to move. Default is that group will wait until is current assignment is over. -- * *battery* Name of the ARTY group that the relocation is assigned to. -- * *alias* Alias of the ARTY group that the target is assigned to. The alias is **case sensitive** and needs to be in quotation marks. --- * *cluster* The cluster of ARTY groups that is addessed. Clusters can be defined by the function @{#ARTY.AddToCluster}(*clusters*). Names are **case sensitive** and need to be in quotation marks. --- * *key* A number to authorize the target assignment. Only specifing the correct number will trigger an engagement. +-- * *cluster* The cluster of ARTY groups that is addressed. Clusters can be defined by the function @{#ARTY.AddToCluster}(*clusters*). Names are **case sensitive** and need to be in quotation marks. +-- * *key* A number to authorize the target assignment. Only specifying the correct number will trigger an engagement. -- * *lldms* Specify the coordinates in Lat/Long degrees, minutes and seconds format. The actual location of the marker is unimportant. The group will move to the coordinates given in the lldms keyword. -- Format is DD:MM:SS[N,S] DD:MM:SS[W,E]. See example below. -- * *readonly* Marker cannot be deleted by users any more. Hence, assignment cannot be cancelled by removing the marker. @@ -73639,12 +81278,12 @@ end -- -- A few options can be set by marks. The corresponding keyword is **arty set**. This can be used to define the rearming place and group for a battery. -- --- To set the reamring place of a group at the marker position type +-- To set the rearming place of a group at the marker position type -- arty set, battery "Paladin Alpha", rearming place -- -- Setting the rearming group is independent of the position of the mark. Just create one anywhere on the map and type -- arty set, battery "Mortar Bravo", rearming group "Ammo Truck M818" --- Note that the name of the rearming group has to be given in quotation marks and spellt exactly as the group name defined in the mission editor. +-- Note that the name of the rearming group has to be given in quotation marks and spelt exactly as the group name defined in the mission editor. -- -- ## Transporting -- @@ -76651,7 +84290,7 @@ function ARTY:onafterMove(Controllable, From, Event, To, move) -- Set current move. self.currentMove=move - -- Route group to coodinate. + -- Route group to coordinate. self:_Move(self.Controllable, move.coord, move.speed, move.onroad) end @@ -78651,6 +86290,7 @@ end -- @field #boolean eventmoose If true, events are handled by MOOSE. If false, events are handled directly by DCS eventhandler. Default true. -- @field Core.Zone#ZONE BattleZone -- @field #boolean AutoEngage +-- @field #table waypoints Waypoints of the group as defined in the ME. -- @extends Core.Fsm#FSM_CONTROLLABLE -- @@ -78831,6 +86471,7 @@ SUPPRESSION={ DefaultAlarmState = "Auto", DefaultROE = "Weapon Free", eventmoose = true, + waypoints = {}, } --- Enumerator of possible rules of engagement. @@ -78861,7 +86502,7 @@ SUPPRESSION.MenuF10=nil --- PSEUDOATC version. -- @field #number version -SUPPRESSION.version="0.9.3" +SUPPRESSION.version="0.9.4" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -78875,7 +86516,7 @@ SUPPRESSION.version="0.9.3" --- Creates a new AI_suppression object. -- @param #SUPPRESSION self -- @param Wrapper.Group#GROUP group The GROUP object for which suppression should be applied. --- @return #SUPPRESSION SUPPRESSION object or *nil* if group does not exist or is not a ground group. +-- @return #SUPPRESSION self function SUPPRESSION:New(group) -- Inherits from FSM_CONTROLLABLE @@ -78886,7 +86527,7 @@ function SUPPRESSION:New(group) self.lid=string.format("SUPPRESSION %s | ", tostring(group:GetName())) self:T(self.lid..string.format("SUPPRESSION version %s. Activating suppressive fire for group %s", SUPPRESSION.version, group:GetName())) else - self:E(self.lid.."SUPPRESSION | Requested group does not exist! (Has to be a MOOSE group.)") + self:E("SUPPRESSION | Requested group does not exist! (Has to be a MOOSE group)") return nil end @@ -79752,6 +87393,16 @@ function SUPPRESSION:onafterFightBack(Controllable, From, Event, To) -- Set ROE and alarm state back to default. self:_SetROE() self:_SetAlarmState() + + local group=Controllable --Wrapper.Group#GROUP + + local Waypoints = group:GetTemplateRoutePoints() + +-- env.info("FF waypoints",showMessageBox) +-- self:I(Waypoints) + + group:Route(Waypoints, 5) + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -79817,7 +87468,7 @@ function SUPPRESSION:onafterFallBack(Controllable, From, Event, To, AttackUnit) self:_SetROE(SUPPRESSION.ROE.Hold) -- Set alarm state to GREEN and let the unit run away. - self:_SetAlarmState(SUPPRESSION.AlarmState.Green) + self:_SetAlarmState(SUPPRESSION.AlarmState.Auto) -- Make the group run away. self:_Run(Coord, self.Speed, self.Formation, self.FallbackWait) @@ -80103,7 +87754,7 @@ end -- @param #SUPPRESSION self -- @param Core.Event#EVENTDATA EventData function SUPPRESSION:_OnEventHit(EventData) - self:F(EventData) + self:F3(EventData) local GroupNameSelf=self.Controllable:GetName() local GroupNameTgt=EventData.TgtGroupName @@ -80242,15 +87893,15 @@ end function SUPPRESSION:_Run(fin, speed, formation, wait) speed=speed or 20 - formation=formation or "Off road" + formation=formation or ENUMS.Formation.Vehicle.OffRoad wait=wait or 30 - local group=self.Controllable -- Wrapper.Controllable#CONTROLLABLE + local group=self.Controllable -- Wrapper.Group#GROUP if group and group:IsAlive() then -- Clear all tasks. - group:ClearTasks() + --group:ClearTasks() -- Current coordinates of group. local ini=group:GetCoordinate() @@ -80260,57 +87911,18 @@ function SUPPRESSION:_Run(fin, speed, formation, wait) -- Heading from ini to fin. local heading=self:_Heading(ini, fin) - - -- Number of waypoints. - local nx - if dist <= 50 then - nx=2 - elseif dist <= 100 then - nx=3 - elseif dist <= 500 then - nx=4 - else - nx=5 - end - - -- Number of intermediate waypoints. - local dx=dist/(nx-1) - + -- Waypoint and task arrays. local wp={} local tasks={} -- First waypoint is the current position of the group. wp[1]=ini:WaypointGround(speed, formation) - tasks[1]=group:TaskFunction("SUPPRESSION._Passing_Waypoint", self, 1, false) if self.Debug then local MarkerID=ini:MarkToAll(string.format("Waypoing %d of group %s (initial)", #wp, self.Controllable:GetName())) end - self:T2(self.lid..string.format("Number of waypoints %d", nx)) - for i=1,nx-2 do - - local x=dx*i - local coord=ini:Translate(x, heading) - - wp[#wp+1]=coord:WaypointGround(speed, formation) - tasks[#tasks+1]=group:TaskFunction("SUPPRESSION._Passing_Waypoint", self, #wp, false) - - self:T2(self.lid..string.format("%d x = %4.1f", i, x)) - if self.Debug then - local MarkerID=coord:MarkToAll(string.format("Waypoing %d of group %s", #wp, self.Controllable:GetName())) - end - - end - self:T2(self.lid..string.format("Total distance: %4.1f", dist)) - - -- Final waypoint. - wp[#wp+1]=fin:WaypointGround(speed, formation) - if self.Debug then - local MarkerID=fin:MarkToAll(string.format("Waypoing %d of group %s (final)", #wp, self.Controllable:GetName())) - end - -- Task to hold. local ConditionWait=group:TaskCondition(nil, nil, nil, nil, wait, nil) local TaskHold = group:TaskHold() @@ -80319,25 +87931,15 @@ function SUPPRESSION:_Run(fin, speed, formation, wait) local TaskComboFin = {} TaskComboFin[#TaskComboFin+1] = group:TaskFunction("SUPPRESSION._Passing_Waypoint", self, #wp, true) TaskComboFin[#TaskComboFin+1] = group:TaskControlled(TaskHold, ConditionWait) - - -- Add final task. - tasks[#tasks+1]=group:TaskCombo(TaskComboFin) - - -- Original waypoints of the group. - local Waypoints = group:GetTemplateRoutePoints() - - -- New points are added to the default route. - for i,p in ipairs(wp) do - table.insert(Waypoints, i, wp[i]) - end - -- Set task for all waypoints. - for i,wp in ipairs(Waypoints) do - group:SetTaskWaypoint(Waypoints[i], tasks[i]) - end + -- Final waypoint. + wp[#wp+1]=fin:WaypointGround(speed, formation, TaskComboFin) + if self.Debug then + local MarkerID=fin:MarkToAll(string.format("Waypoing %d of group %s (final)", #wp, self.Controllable:GetName())) + end -- Submit task and route group along waypoints. - group:Route(Waypoints) + group:Route(wp) else self:E(self.lid..string.format("ERROR: Group is not alive!")) @@ -80356,7 +87958,7 @@ function SUPPRESSION._Passing_Waypoint(group, Fsm, i, final) local text=string.format("Group %s passing waypoint %d (final=%s)", group:GetName(), i, tostring(final)) MESSAGE:New(text,10):ToAllIf(Fsm.Debug) if Fsm.Debug then - env.info(self.lid..text) + env.info(Fsm.lid..text) end if final then @@ -80457,7 +88059,7 @@ function SUPPRESSION:_GetLife() local groupstrength=#units/self.IniGroupStrength*100 - self.T2(self.lid..string.format("Group %s _GetLife nunits = %d", self.Controllable:GetName(), #units)) + self:T2(self.lid..string.format("Group %s _GetLife nunits = %d", self.Controllable:GetName(), #units)) for _,unit in pairs(units) do @@ -80618,7 +88220,7 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- **Functional** - Rudimentary ATC. +--- **Functional** - Basic ATC. -- -- ![Banner Image](..\Presentations\PSEUDOATC\PSEUDOATC_Main.jpg) -- @@ -80665,6 +88267,7 @@ end -- @field #number talt Interval in seconds between reporting altitude until touchdown. Default 3 sec. -- @field #boolean chatty Display some messages on events like take-off and touchdown. -- @field #boolean eventsmoose If true, events are handled by MOOSE. If false, events are handled directly by DCS eventhandler. +-- @field #boolean reportplayername If true, use playername not callsign on callouts -- @extends Core.Base#BASE --- Adds some rudimentary ATC functionality via the radio menu. @@ -80708,6 +88311,7 @@ PSEUDOATC={ talt=3, chatty=true, eventsmoose=true, + reportplayername = false, } ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -80718,7 +88322,7 @@ PSEUDOATC.id="PseudoATC | " --- PSEUDOATC version. -- @field #number version -PSEUDOATC.version="0.9.2" +PSEUDOATC.version="0.9.5" ----------------------------------------------------------------------------------------------------------------------------------------- @@ -80803,6 +88407,13 @@ function PSEUDOATC:SetMessageDuration(duration) self.mdur=duration or 30 end +--- Use player name, not call sign, in callouts +-- @param #PSEUDOATC self +function PSEUDOATC:SetReportPlayername() + self.reportplayername = true + return self +end + --- Set time interval after which the F10 radio menu is refreshed. -- @param #PSEUDOATC self -- @param #number interval Interval in seconds. Default is every 120 sec. @@ -81061,14 +88672,18 @@ function PSEUDOATC:PlayerLanded(unit, place) local group=unit:GetGroup() local GID=group:GetID() local UID=unit:GetDCSObject():getID() - local PlayerName=self.group[GID].player[UID].playername - local UnitName=self.group[GID].player[UID].unitname - local GroupName=self.group[GID].player[UID].groupname - - -- Debug message. - local text=string.format("Player %s in unit %s of group %s (id=%d) landed at %s.", PlayerName, UnitName, GroupName, GID, place) - self:T(PSEUDOATC.id..text) - MESSAGE:New(text, 30):ToAllIf(self.Debug) + --local PlayerName=self.group[GID].player[UID].playername + --local UnitName=self.group[GID].player[UID].unitname + --local GroupName=self.group[GID].player[UID].groupname + local PlayerName = unit:GetPlayerName() or "Ghost" + local UnitName = unit:GetName() or "Ghostplane" + local GroupName = group:GetName() or "Ghostgroup" + if self.Debug then + -- Debug message. + local text=string.format("Player %s in unit %s of group %s landed at %s.", PlayerName, UnitName, GroupName, place) + self:T(PSEUDOATC.id..text) + MESSAGE:New(text, 30):ToAllIf(self.Debug) + end -- Stop altitude reporting timer if its activated. self:AltitudeTimerStop(GID,UID) @@ -81090,21 +88705,28 @@ function PSEUDOATC:PlayerTakeOff(unit, place) -- Gather some information. local group=unit:GetGroup() - local GID=group:GetID() - local UID=unit:GetDCSObject():getID() - local PlayerName=self.group[GID].player[UID].playername - local CallSign=self.group[GID].player[UID].callsign - local UnitName=self.group[GID].player[UID].unitname - local GroupName=self.group[GID].player[UID].groupname - - -- Debug message. - local text=string.format("Player %s in unit %s of group %s (id=%d) took off at %s.", PlayerName, UnitName, GroupName, GID, place) - self:T(PSEUDOATC.id..text) - MESSAGE:New(text, 30):ToAllIf(self.Debug) - + --local GID=group:GetID() + --local UID=unit:GetDCSObject():getID() + --local PlayerName=self.group[GID].player[UID].playername + --local CallSign=self.group[GID].player[UID].callsign + --local UnitName=self.group[GID].player[UID].unitname + --local GroupName=self.group[GID].player[UID].groupname + local PlayerName = unit:GetPlayerName() or "Ghost" + local UnitName = unit:GetName() or "Ghostplane" + local GroupName = group:GetName() or "Ghostgroup" + local CallSign = unit:GetCallsign() or "Ghost11" + if self.Debug then + -- Debug message. + local text=string.format("Player %s in unit %s of group %s took off at %s.", PlayerName, UnitName, GroupName, place) + self:T(PSEUDOATC.id..text) + MESSAGE:New(text, 30):ToAllIf(self.Debug) + end -- Bye-Bye message. if place and self.chatty then local text=string.format("%s, %s, you are airborne. Have a safe trip!", place, CallSign) + if self.reportplayername then + text=string.format("%s, %s, you are airborne. Have a safe trip!", place, PlayerName) + end MESSAGE:New(text, self.mdur):ToGroup(group) end @@ -81121,7 +88743,7 @@ function PSEUDOATC:PlayerLeft(unit) local GID=group:GetID() local UID=unit:GetDCSObject():getID() - if self.group[GID].player[UID] then + if self.group[GID] and self.group[GID].player and self.group[GID].player[UID] then local PlayerName=self.group[GID].player[UID].playername local CallSign=self.group[GID].player[UID].callsign local UnitName=self.group[GID].player[UID].unitname @@ -81307,7 +88929,9 @@ function PSEUDOATC:MenuWaypoints(GID, UID) -- Position of Waypoint local pos=COORDINATE:New(wp.x, wp.alt, wp.y) local name=string.format("Waypoint %d", i-1) - + if wp.name and wp.name ~= "" then + name = string.format("Waypoint %s",wp.name) + end -- "F10/PseudoATC/Waypoints/Waypoint X" local submenu=missionCommands.addSubMenuForGroup(GID, name, self.group[GID].player[UID].menu_waypoints) @@ -81362,7 +88986,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 @@ -81464,7 +89088,8 @@ function PSEUDOATC:ReportHeight(GID, UID, dt, _clear) local position=unit:GetCoordinate() local height=get_AGL(position) local callsign=unit:GetCallsign() - + local PlayerName=self.group[GID].player[UID].playername + -- Settings. local settings=_DATABASE:GetPlayerSettings(self.group[GID].player[UID].playername) or _SETTINGS --Core.Settings#SETTINGS @@ -81476,7 +89101,9 @@ function PSEUDOATC:ReportHeight(GID, UID, dt, _clear) -- Message text. local _text=string.format("%s, your altitude is %s AGL.", callsign, Hs) - + if self.reportplayername then + _text=string.format("%s, your altitude is %s AGL.", PlayerName, Hs) + end -- Append flight level. if _clear==false then _text=_text..string.format(" FL%03d.", position.y/30.48) @@ -81569,11 +89196,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 @@ -81661,9 +89291,6 @@ function PSEUDOATC:_myname(unitname) return string.format("%s (%s)", csign, pname) end - - - --- **Functional** - Simulation of logistic operations. -- -- === @@ -81712,17 +89339,22 @@ end -- @type WAREHOUSE -- @field #string ClassName Name of the class. -- @field #boolean Debug If true, send debug messages to all. +-- @field #number verbosity Verbosity level. -- @field #string wid Identifier of the warehouse printed before other output to DCS.log file. -- @field #boolean Report If true, send status messages to coalition. -- @field Wrapper.Static#STATIC warehouse The phyical warehouse structure. -- @field #string alias Alias of the warehouse. Name its called when sending messages. --- @field Core.Zone#ZONE zone Zone around the warehouse. If this zone is captured, the warehouse and all its assets goes to the capturing coaliton. +-- @field Core.Zone#ZONE zone Zone around the warehouse. If this zone is captured, the warehouse and all its assets goes to the capturing coalition. -- @field Wrapper.Airbase#AIRBASE airbase Airbase the warehouse belongs to. -- @field #string airbasename Name of the airbase associated to the warehouse. -- @field Core.Point#COORDINATE road Closest point to warehouse on road. -- @field Core.Point#COORDINATE rail Closest point to warehouse on rail. -- @field Core.Zone#ZONE spawnzone Zone in which assets are spawned. -- @field #number uid Unique ID of the warehouse. +-- @field #boolean markerOn If true, markers are displayed on the F10 map. +-- @field Wrapper.Marker#MARKER markerWarehouse Marker warehouse. +-- @field Wrapper.Marker#MARKER markerRoad Road connection. +-- @field Wrapper.Marker#MARKER markerRail Rail road connection. -- @field #number markerid ID of the warehouse marker at the airbase. -- @field #number dTstatus Time interval in seconds of updating the warehouse status and processing new events. Default 30 seconds. -- @field #number queueid Unit id of each request in the queue. Essentially a running number starting at one and incremented when a new request is added. @@ -81741,11 +89373,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 #boolean markerOn If true, markers are displayed on the F10 map. +-- @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! @@ -81955,13 +89590,13 @@ end -- -- Assets of the warehouse can be requested by other MOOSE warehouses. A request will first be scrutinized to check if can be fulfilled at all. If the request is valid, it is -- put into the warehouse queue and processed as soon as possible. --- +-- -- Requested assets spawn in various "Rule of Engagement Rules" (ROE) and Alerts modes. If your assets will cross into dangerous areas, be sure to change these states. You can do this in @{#WAREHOUSE:OnAfterAssetSpawned}(*From, *Event, *To, *group, *asset, *request)) function. -- -- Initial Spawn states is as follows: -- GROUND: ROE, "Return Fire" Alarm, "Green" --- AIR: ROE, "Return Fire" Reaction to Threat, "Passive Defense" --- NAVAL ROE, "Return Fire" Alarm,"N/A" +-- AIR: ROE, "Return Fire" Reaction to Threat, "Passive Defense" +-- NAVAL ROE, "Return Fire" Alarm,"N/A" -- -- A request can be added by the @{#WAREHOUSE.AddRequest}(*warehouse*, *AssetDescriptor*, *AssetDescriptorValue*, *nAsset*, *TransportType*, *nTransport*, *Prio*, *Assignment*) function. -- The parameters are @@ -82007,6 +89642,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. @@ -82430,7 +90066,7 @@ end -- warehouseBatumi:Load("D:\\My Warehouse Data\\") -- warehouseBatumi:Start() -- --- This sequence loads all assets from file. If a warehouse was captured in the last mission, it also respawns the static warehouse structure with the right coaliton. +-- This sequence loads all assets from file. If a warehouse was captured in the last mission, it also respawns the static warehouse structure with the right coalition. -- However, it due to DCS limitations it is not possible to set the airbase coalition. This has to be done manually in the mission editor. Or alternatively, one could -- spawn some ground units via a self request and let them capture the airbase. -- @@ -82931,7 +90567,7 @@ end -- -- ## Example 13: Battlefield Air Interdiction -- --- This example show how to couple the WAREHOUSE class with the @{AI.AI_Bai} class. +-- This example show how to couple the WAREHOUSE class with the @{AI.AI_BAI} class. -- Four enemy targets have been located at the famous Kobuleti X. All three available Viggen 2-ship flights are assigned to kill at least one of the BMPs to complete their mission. -- -- -- Start Warehouse at Kobuleti. @@ -83220,6 +90856,7 @@ end WAREHOUSE = { ClassName = "WAREHOUSE", Debug = false, + verbosity = 0, lid = nil, Report = true, warehouse = nil, @@ -83231,7 +90868,6 @@ WAREHOUSE = { rail = nil, spawnzone = nil, uid = nil, - markerid = nil, dTstatus = 30, queueid = 0, stock = {}, @@ -83250,7 +90886,8 @@ WAREHOUSE = { autosavepath = nil, autosavefile = nil, saveparking = false, - isunit = false, + isUnit = false, + isShip = false, lowfuelthresh = 0.15, respawnafterdestroyed=false, respawndelay = nil, @@ -83259,6 +90896,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. @@ -83267,7 +90906,7 @@ WAREHOUSE = { -- @field #number range Range of the unit in meters. -- @field #number speedmax Maximum speed in km/h the group can do. -- @field #number size Maximum size in length and with of the asset in meters. --- @field #number weight The weight of the whole asset group in kilo gramms. +-- @field #number weight The weight of the whole asset group in kilograms. -- @field DCS#Object.Desc DCSdesc All DCS descriptors. -- @field #WAREHOUSE.Attribute attribute Generalized attribute of the group. -- @field #table cargobay Array of cargo bays of all units in an asset group. @@ -83280,8 +90919,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 @@ -83304,6 +90952,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 @@ -83349,6 +90998,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. @@ -83375,6 +91025,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", @@ -83477,7 +91128,7 @@ WAREHOUSE.version="1.0.2" -- DONE: Add shipping lanes between warehouses. -- DONE: Handle cases with immobile units <== should be handled by dispatcher classes. -- DONE: Handle cases for aircraft carriers and other ships. Place warehouse on carrier possible? On others probably not - exclude them? --- DONE: Add general message function for sending to coaliton or debug. +-- DONE: Add general message function for sending to coalition or debug. -- DONE: Fine tune event handlers. -- DONE: Improve generalized attributes. -- DONE: If warehouse is destroyed, all asssets are gone. @@ -83501,41 +91152,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 - --env.info(string.format("No warehouse unit with name %s found trying static.", tostring(warehousename))) 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 @@ -83545,10 +91205,10 @@ function WAREHOUSE:New(warehouse, alias) -- Set unique ID for this warehouse. self.uid=_WAREHOUSEDB.WarehouseID - + -- Coalition of the warehouse. self.coalition=self.warehouse:GetCoalition() - + -- Country of the warehouse. self.countryid=self.warehouse:GetCountry() @@ -83559,11 +91219,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.allowSpawnOnClientSpots=false -- Add warehouse to database. _WAREHOUSEDB.Warehouses[self.uid]=self @@ -83579,26 +91248,26 @@ function WAREHOUSE:New(warehouse, alias) -- From State --> Event --> To State self:AddTransition("NotReadyYet", "Load", "Loaded") -- Load the warehouse state from scatch. self:AddTransition("Stopped", "Load", "Loaded") -- Load the warehouse state stopped state. - + self:AddTransition("NotReadyYet", "Start", "Running") -- Start the warehouse from scratch. self:AddTransition("Loaded", "Start", "Running") -- Start the warehouse when loaded from disk. - + self:AddTransition("*", "Status", "*") -- Status update. - + self:AddTransition("*", "AddAsset", "*") -- Add asset to warehouse stock. self:AddTransition("*", "NewAsset", "*") -- New asset was added to warehouse stock. - + self:AddTransition("*", "AddRequest", "*") -- New request from other warehouse. self:AddTransition("Running", "Request", "*") -- Process a request. Only in running mode. self:AddTransition("Running", "RequestSpawned", "*") -- Assets of request were spawned. self:AddTransition("Attacked", "Request", "*") -- Process a request. Only in running mode. - + self:AddTransition("*", "Unloaded", "*") -- Cargo has been unloaded from the carrier (unused ==> unnecessary?). self:AddTransition("*", "AssetSpawned", "*") -- Asset has been spawned into the world. self:AddTransition("*", "AssetLowFuel", "*") -- Asset is low on fuel. - + self:AddTransition("*", "Arrived", "*") -- Cargo or transport group has arrived. - + self:AddTransition("*", "Delivered", "*") -- All cargo groups of a request have been delivered to the requesting warehouse. self:AddTransition("Running", "SelfRequest", "*") -- Request to warehouse itself. Requested assets are only spawned but not delivered anywhere. self:AddTransition("Attacked", "SelfRequest", "*") -- Request to warehouse itself. Also possible when warehouse is under attack! @@ -83614,6 +91283,8 @@ function WAREHOUSE:New(warehouse, alias) self:AddTransition("Attacked", "Captured", "Running") -- Warehouse was captured by another coalition. It must have been attacked first. self:AddTransition("*", "AirbaseCaptured", "*") -- Airbase was captured by other coalition. self:AddTransition("*", "AirbaseRecaptured", "*") -- Airbase was re-captured from other coalition. + self:AddTransition("*", "RunwayDestroyed", "*") -- Runway of the airbase was destroyed. + self:AddTransition("*", "RunwayRepaired", "*") -- Runway of the airbase was repaired. self:AddTransition("*", "AssetDead", "*") -- An asset group died. self:AddTransition("*", "Destroyed", "Destroyed") -- Warehouse was destroyed. All assets in stock are gone and warehouse is stopped. self:AddTransition("Destroyed", "Respawn", "Running") -- Respawn warehouse after it was destroyed. @@ -84207,6 +91878,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%). @@ -84225,6 +91904,15 @@ function WAREHOUSE:SetStatusUpdate(timeinterval) return self end +--- Set verbosity level. +-- @param #WAREHOUSE self +-- @param #number VerbosityLevel Level of output (higher=more). Default 0. +-- @return #WAREHOUSE self +function WAREHOUSE:SetVerbosityLevel(VerbosityLevel) + self.verbosity=VerbosityLevel or 0 + return self +end + --- Set a zone where the (ground) assets of the warehouse are spawned once requested. -- @param #WAREHOUSE self -- @param Core.Zone#ZONE zone The spawn zone. @@ -84236,6 +91924,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 @@ -84246,6 +91940,13 @@ function WAREHOUSE:SetWarehouseZone(zone) return self end +--- Get the warehouse zone. +-- @param #WAREHOUSE self +-- @return Core.Zone#ZONE The warehouse zone. +function WAREHOUSE:GetWarehouseZone() + return self.zone +end + --- Set auto defence on. When the warehouse is under attack, all ground assets are spawned automatically and will defend the warehouse zone. -- @param #WAREHOUSE self -- @return #WAREHOUSE self @@ -84277,9 +91978,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 @@ -84294,6 +91994,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 @@ -84730,6 +92449,23 @@ function WAREHOUSE:GetCoordinate() return self.warehouse:GetCoordinate() end +--- Get 3D vector of warehouse static. +-- @param #WAREHOUSE self +-- @return DCS#Vec3 The 3D vector of the warehouse. +function 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() + local vec2=self.warehouse:GetVec2() + return vec2 +end + + --- Get coalition side of warehouse static. -- @param #WAREHOUSE self -- @return #number Coalition side, i.e. number of @{DCS#coalition.side}. @@ -84795,18 +92531,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. @@ -84821,7 +92545,7 @@ end -- @param MinAssets (Optional) Minimum number of assets the warehouse should have. Default 0. -- @param #string Descriptor (Optional) Descriptor describing the selected assets which should be in stock. See @{#WAREHOUSE.Descriptor} for possible values. -- @param DescriptorValue (Optional) Descriptor value selecting the type of assets which should be in stock. --- @param DCS#Coalition.side Coalition (Optional) Coalition side of the warehouse. Default is the same coaliton as the present warehouse. Set to false for any coalition. +-- @param DCS#Coalition.side Coalition (Optional) Coalition side of the warehouse. Default is the same coalition as the present warehouse. Set to false for any coalition. -- @param Core.Point#COORDINATE RefCoordinate (Optional) Coordinate to which the closest warehouse is searched. Default is the warehouse calling this function. -- @return #WAREHOUSE The the nearest warehouse object. Or nil if no warehouse is found. -- @return #number The distance to the nearest warehouse in meters. Or nil if no warehouse is found. @@ -84920,6 +92644,65 @@ function WAREHOUSE:FindAssetInDB(group) return nil end +--- Check if runway is operational. +-- @param #WAREHOUSE self +-- @return #boolean If `true`, runway is operational. +function WAREHOUSE:IsRunwayOperational() + if self.airbase then + if self.runwaydestroyed then + return false + else + return true + end + end + return nil +end + +--- Set the time until the runway(s) of an airdrome are repaired after it has been destroyed. +-- Note that this is the time, the DCS engine uses not something we can control on a user level or we could get via scripting. +-- You need to input the value. On the DCS forum it was stated that this is currently one hour. Hence this is the default value. +-- @param #WAREHOUSE self +-- @param #number RepairTime Time in seconds until the runway is repaired. Default 3600 sec (one hour). +-- @return #WAREHOUSE self +function WAREHOUSE:SetRunwayRepairtime(RepairTime) + self.runwayrepairtime=RepairTime or 3600 + return self +end + +--- Check if runway is operational. +-- @param #WAREHOUSE self +-- @return #number Time in seconds until the runway is repaired. Will return 0 if runway is repaired. +function WAREHOUSE:GetRunwayRepairtime() + if self.runwaydestroyed then + local Tnow=timer.getAbsTime() + local Tsince=Tnow-self.runwaydestroyed + local Trepair=math.max(self.runwayrepairtime-Tsince, 0) + return Trepair + end + return 0 +end + +--- Check if 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 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -84933,7 +92716,7 @@ function WAREHOUSE:onafterStart(From, Event, To) -- Short info. local text=string.format("Starting warehouse %s alias %s:\n",self.warehouse:GetName(), self.alias) - text=text..string.format("Coaliton = %s\n", self:GetCoalitionName()) + text=text..string.format("Coalition = %s\n", self:GetCoalitionName()) text=text..string.format("Country = %s\n", self:GetCountryName()) text=text..string.format("Airbase = %s (category=%d)\n", self:GetAirbaseName(), self:GetAirbaseCategory()) env.info(text) @@ -84941,16 +92724,6 @@ function WAREHOUSE:onafterStart(From, Event, To) -- Save self in static object. Easier to retrieve later. self.warehouse:SetState(self.warehouse, "WAREHOUSE", self) - -- THIS! caused aircraft to be spawned and started but they would never begin their route! - -- VERY strange. Need to test more. - --[[ - -- Debug mark warehouse & spawn zone. - self.zone:BoundZone(30, self.country) - self.spawnzone:BoundZone(30, self.country) - ]] - - --self.spawnzone:GetCoordinate():MarkToCoalition(string.format("Warehouse %s spawn zone", self.alias), self:GetCoalition()) - -- Get the closest point on road wrt spawnzone of ground assets. local _road=self.spawnzone:GetCoordinate():GetClosestPointToRoad() if _road and self.road==nil then @@ -85060,7 +92833,7 @@ function WAREHOUSE:onafterStop(From, Event, To) self:_UpdateWarehouseMarkText() -- Clear all pending schedules. - --self.CallScheduler:Clear() + self.CallScheduler:Clear() end --- On after "Pause" event. Pauses the warehouse, i.e. no requests are processed. However, new requests and new assets can be added in this state. @@ -85090,13 +92863,17 @@ end -- @param #string To To state. function WAREHOUSE:onafterStatus(From, Event, To) - local FSMstate=self:GetState() + -- General info. + if self.verbosity>=1 then - local coalition=self:GetCoalitionName() - local country=self:GetCountryName() - - -- Info. - self:I(self.lid..string.format("State=%s %s [%s]: Assets=%d, Requests: waiting=%d, pending=%d", FSMstate, country, coalition, #self.stock, #self.queue, #self.pending)) + local FSMstate=self:GetState() + + local coalition=self:GetCoalitionName() + local country=self:GetCountryName() + + -- Info. + self:I(self.lid..string.format("State=%s %s [%s]: Assets=%d, Requests: waiting=%d, pending=%d", FSMstate, country, coalition, #self.stock, #self.queue, #self.pending)) + end -- Check if any pending jobs are done and can be deleted from the queue. self:_JobDone() @@ -85106,6 +92883,14 @@ function WAREHOUSE:onafterStatus(From, Event, To) -- Check if warehouse is being attacked or has even been captured. self:_CheckConquered() + + if self:IsRunwayOperational()==false then + local Trepair=self:GetRunwayRepairtime() + self:I(self.lid..string.format("Runway destroyed! Will be repaired in %d sec", Trepair)) + if Trepair==0 then + self:RunwayRepaired() + end + end -- Check if requests are valid and remove invalid one. self:_CheckRequestConsistancy(self.queue) @@ -85128,7 +92913,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() @@ -85153,170 +92938,174 @@ function WAREHOUSE:_JobDone() -- Loop over all pending requests of this warehouse. for _,request in pairs(self.pending) do local request=request --#WAREHOUSE.Pendingitem + + if request.born then - -- Count number of cargo groups. - local ncargo=0 - if request.cargogroupset then - ncargo=request.cargogroupset:Count() - end - - -- Count number of transport groups (if any). - local ntransport=0 - if request.transportgroupset then - ntransport=request.transportgroupset:Count() - end - - local ncargotot=request.nasset - local ncargodelivered=request.ndelivered - - -- Dead cargo: Ndead=Ntot-Ndeliverd-Nalive, - local ncargodead=ncargotot-ncargodelivered-ncargo - - - local ntransporttot=request.ntransport - local ntransporthome=request.ntransporthome - - -- Dead transport: Ndead=Ntot-Nhome-Nalive. - local ntransportdead=ntransporttot-ntransporthome-ntransport - - local text=string.format("Request id=%d: Cargo: Ntot=%d, Nalive=%d, Ndelivered=%d, Ndead=%d | Transport: Ntot=%d, Nalive=%d, Nhome=%d, Ndead=%d", - request.uid, ncargotot, ncargo, ncargodelivered, ncargodead, ntransporttot, ntransport, ntransporthome, ntransportdead) - self:T(self.lid..text) - - - -- Handle different cases depending on what asset are still around. - if ncargo==0 then - --------------------- - -- Cargo delivered -- - --------------------- - - -- Trigger delivered event. - if not self.delivered[request.uid] then - self:Delivered(request) + -- Count number of cargo groups. + local ncargo=0 + if request.cargogroupset then + ncargo=request.cargogroupset:Count() end - - -- Check if transports are back home? - if ntransport==0 then - --------------- - -- Job done! -- - --------------- - - -- Info on job. - local text=string.format("Warehouse %s: Job on request id=%d for warehouse %s done!\n", self.alias, request.uid, request.warehouse.alias) - text=text..string.format("- %d of %d assets delivered. Casualties %d.", ncargodelivered, ncargotot, ncargodead) - if request.ntransport>0 then - text=text..string.format("\n- %d of %d transports returned home. Casualties %d.", ntransporthome, ntransporttot, ntransportdead) + + -- Count number of transport groups (if any). + local ntransport=0 + if request.transportgroupset then + ntransport=request.transportgroupset:Count() + end + + local ncargotot=request.nasset + local ncargodelivered=request.ndelivered + + -- Dead cargo: Ndead=Ntot-Ndeliverd-Nalive, + local ncargodead=ncargotot-ncargodelivered-ncargo + + + local ntransporttot=request.ntransport + local ntransporthome=request.ntransporthome + + -- Dead transport: Ndead=Ntot-Nhome-Nalive. + local ntransportdead=ntransporttot-ntransporthome-ntransport + + local text=string.format("Request id=%d: Cargo: Ntot=%d, Nalive=%d, Ndelivered=%d, Ndead=%d | Transport: Ntot=%d, Nalive=%d, Nhome=%d, Ndead=%d", + request.uid, ncargotot, ncargo, ncargodelivered, ncargodead, ntransporttot, ntransport, ntransporthome, ntransportdead) + self:T(self.lid..text) + + + -- Handle different cases depending on what asset are still around. + if ncargo==0 then + --------------------- + -- Cargo delivered -- + --------------------- + + -- Trigger delivered event. + if not self.delivered[request.uid] then + self:Delivered(request) end - self:_InfoMessage(text, 20) - - -- Mark request for deletion. - table.insert(done, request) - - else - ----------------------------------- - -- No cargo but still transports -- - ----------------------------------- - - -- This is difficult! How do I know if transports were unused? They could also be just on their way back home. - -- ==> Need to do a lot of checks. - - -- All transports are dead but there is still cargo left ==> Put cargo back into stock. - for _,_group in pairs(request.transportgroupset:GetSetObjects()) do - local group=_group --Wrapper.Group#GROUP - - -- Check if group is alive. - if group and group:IsAlive() then - - -- Check if group is in the spawn zone? - local category=group:GetCategory() - - -- Get current speed. - local speed=group:GetVelocityKMH() - local notmoving=speed<1 - - -- Closest airbase. - local airbase=group:GetCoordinate():GetClosestAirbase():GetName() - local athomebase=self.airbase and self.airbase:GetName()==airbase - - -- On ground - local onground=not group:InAir() - - -- In spawn zone. - local inspawnzone=group:IsPartlyOrCompletelyInZone(self.spawnzone) - - -- Check conditions for being back home. - local ishome=false - if category==Group.Category.GROUND or category==Group.Category.HELICOPTER then - -- Units go back to the spawn zone, helicopters land and they should not move any more. - ishome=inspawnzone and onground and notmoving - elseif category==Group.Category.AIRPLANE then - -- Planes need to be on ground at their home airbase and should not move any more. - ishome=athomebase and onground and notmoving + + -- Check if transports are back home? + if ntransport==0 then + --------------- + -- Job done! -- + --------------- + + -- Info on job. + if self.verbosity>=1 then + local text=string.format("Warehouse %s: Job on request id=%d for warehouse %s done!\n", self.alias, request.uid, request.warehouse.alias) + text=text..string.format("- %d of %d assets delivered. Casualties %d.", ncargodelivered, ncargotot, ncargodead) + if request.ntransport>0 then + text=text..string.format("\n- %d of %d transports returned home. Casualties %d.", ntransporthome, ntransporttot, ntransportdead) end - - -- Debug text. - local text=string.format("Group %s: speed=%d km/h, onground=%s , airbase=%s, spawnzone=%s ==> ishome=%s", group:GetName(), speed, tostring(onground), airbase, tostring(inspawnzone), tostring(ishome)) - self:I(self.lid..text) - - if ishome then - - -- Info message. - local text=string.format("Warehouse %s: Transport group arrived back home and no cargo left for request id=%d.\nSending transport group %s back to stock.", self.alias, request.uid, group:GetName()) - self:_InfoMessage(text) - - -- Debug smoke. - if self.Debug then - group:SmokeRed() + self:_InfoMessage(text, 20) + end + + -- Mark request for deletion. + table.insert(done, request) + + else + ----------------------------------- + -- No cargo but still transports -- + ----------------------------------- + + -- This is difficult! How do I know if transports were unused? They could also be just on their way back home. + -- ==> Need to do a lot of checks. + + -- All transports are dead but there is still cargo left ==> Put cargo back into stock. + for _,_group in pairs(request.transportgroupset:GetSetObjects()) do + local group=_group --Wrapper.Group#GROUP + + -- Check if group is alive. + if group and group:IsAlive() then + + -- Check if group is in the spawn zone? + local category=group:GetCategory() + + -- Get current speed. + local speed=group:GetVelocityKMH() + local notmoving=speed<1 + + -- Closest airbase. + local airbase=group:GetCoordinate():GetClosestAirbase():GetName() + local athomebase=self.airbase and self.airbase:GetName()==airbase + + -- On ground + local onground=not group:InAir() + + -- In spawn zone. + local inspawnzone=group:IsPartlyOrCompletelyInZone(self.spawnzone) + + -- Check conditions for being back home. + local ishome=false + if category==Group.Category.GROUND or category==Group.Category.HELICOPTER then + -- Units go back to the spawn zone, helicopters land and they should not move any more. + ishome=inspawnzone and onground and notmoving + elseif category==Group.Category.AIRPLANE then + -- Planes need to be on ground at their home airbase and should not move any more. + ishome=athomebase and onground and notmoving + end + + -- Debug text. + local text=string.format("Group %s: speed=%d km/h, onground=%s , airbase=%s, spawnzone=%s ==> ishome=%s", group:GetName(), speed, tostring(onground), airbase, tostring(inspawnzone), tostring(ishome)) + self:T(self.lid..text) + + if ishome then + + -- Info message. + local text=string.format("Warehouse %s: Transport group arrived back home and no cargo left for request id=%d.\nSending transport group %s back to stock.", self.alias, request.uid, group:GetName()) + self:T(self.lid..text) + + -- Debug smoke. + if self.Debug then + group:SmokeRed() + end + + -- Group arrived. + self:Arrived(group) end - - -- Group arrived. - self:Arrived(group) end end + end - - end - - else - - if ntransport==0 and request.ntransport>0 then - ----------------------------------- - -- Still cargo but no transports -- - ----------------------------------- - - local ncargoalive=0 - - -- All transports are dead but there is still cargo left ==> Put cargo back into stock. - for _,_group in pairs(request.cargogroupset:GetSetObjects()) do - --local group=group --Wrapper.Group#GROUP - - -- These groups have been respawned as cargo, i.e. their name changed! - local groupname=_group:GetName() - local group=GROUP:FindByName(groupname.."#CARGO") - - -- Check if group is alive. - if group and group:IsAlive() then - - -- Check if group is in spawn zone? - if group:IsPartlyOrCompletelyInZone(self.spawnzone) then - -- Debug smoke. - if self.Debug then - group:SmokeBlue() + + else + + if ntransport==0 and request.ntransport>0 then + ----------------------------------- + -- Still cargo but no transports -- + ----------------------------------- + + local ncargoalive=0 + + -- All transports are dead but there is still cargo left ==> Put cargo back into stock. + for _,_group in pairs(request.cargogroupset:GetSetObjects()) do + --local group=group --Wrapper.Group#GROUP + + -- These groups have been respawned as cargo, i.e. their name changed! + local groupname=_group:GetName() + local group=GROUP:FindByName(groupname.."#CARGO") + + -- Check if group is alive. + if group and group:IsAlive() then + + -- Check if group is in spawn zone? + if group:IsPartlyOrCompletelyInZone(self.spawnzone) then + -- Debug smoke. + if self.Debug then + group:SmokeBlue() + end + -- Add asset group back to stock. + self:AddAsset(group) + ncargoalive=ncargoalive+1 end - -- Add asset group back to stock. - self:AddAsset(group) - ncargoalive=ncargoalive+1 end + end - + + -- Info message. + self:_InfoMessage(string.format("Warehouse %s: All transports of request id=%s dead! Putting remaining %s cargo assets back into warehouse!", self.alias, request.uid, ncargoalive)) end - - -- Info message. - self:_InfoMessage(string.format("Warehouse %s: All transports of request id=%s dead! Putting remaining %s cargo assets back into warehouse!", self.alias, request.uid, ncargoalive)) + end - - end - + end -- born check end -- loop over requests -- Remove pending requests if done. @@ -85440,7 +93229,7 @@ function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribu local wid,aid,rid=self:_GetIDsFromGroup(group) if wid and aid and rid then - + --------------------------- -- This is a KNOWN asset -- --------------------------- @@ -85456,7 +93245,7 @@ function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribu -- Increase number of cargo delivered and transports home. local istransport=warehouse:_GroupIsTransport(group,request) - + if istransport==true then request.ntransporthome=request.ntransporthome+1 request.transportgroupset:Remove(group:GetName(), true) @@ -85466,7 +93255,7 @@ function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribu request.ndelivered=request.ndelivered+1 local namewo=self:_GetNameWithOut(group) request.cargogroupset:Remove(namewo, true) - local ncargo=request.cargogroupset:Count() + local ncargo=request.cargogroupset:Count() self:T2(warehouse.lid..string.format("Cargo %s: %d of %s delivered. CargoSet=%d", namewo, request.ndelivered, tostring(request.nasset), ncargo)) else self:E(warehouse.lid..string.format("WARNING: Group %s is neither cargo nor transport! Need to investigate...", group:GetName())) @@ -85496,10 +93285,10 @@ function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribu asset.livery=liveries end end - + -- Set skill. asset.skill=skill or asset.skill - + -- Asset now belongs to this warehouse. Set warehouse ID. asset.wid=self.uid @@ -85508,8 +93297,15 @@ 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 + + -- Destroy group if it is alive. + if group:IsAlive()==true then + asset.damage=asset.life0-group:GetLife() + end -- Add asset to stock. table.insert(self.stock, asset) @@ -85521,7 +93317,7 @@ function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribu end else - + ------------------------- -- This is a NEW asset -- ------------------------- @@ -85553,8 +93349,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. - 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 @@ -85610,6 +93419,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() @@ -85618,8 +93428,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 @@ -85668,6 +93479,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 @@ -85680,6 +93492,10 @@ 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) if i==1 then @@ -85716,7 +93532,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}) @@ -85802,7 +93618,7 @@ function WAREHOUSE:onbeforeAddRequest(From, Event, To, warehouse, AssetDescripto self:_ErrorMessage("ERROR: Invalid request. Asset assignment type must be passed as a string!", 5) okay=false end - + elseif AssetDescriptor==WAREHOUSE.Descriptor.ASSETLIST then if type(AssetDescriptorValue)~="table" then @@ -85887,9 +93703,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 @@ -85929,7 +93752,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 @@ -85951,10 +93774,12 @@ end function WAREHOUSE:onafterRequest(From, Event, To, Request) -- Info message. - local text=string.format("Warehouse %s: Processing request id=%d from warehouse %s.\n", self.alias, Request.uid, Request.warehouse.alias) - text=text..string.format("Requested %s assets of %s=%s.\n", tostring(Request.nasset), Request.assetdesc, Request.assetdesc==WAREHOUSE.Descriptor.ASSETLIST and "Asset list" or Request.assetdescval) - text=text..string.format("Transports %s of type %s.", tostring(Request.ntransport), tostring(Request.transporttype)) - self:_InfoMessage(text, 5) + if self.verbosity>=1 then + local text=string.format("Warehouse %s: Processing request id=%d from warehouse %s.\n", self.alias, Request.uid, Request.warehouse.alias) + text=text..string.format("Requested %s assets of %s=%s.\n", tostring(Request.nasset), Request.assetdesc, Request.assetdesc==WAREHOUSE.Descriptor.ASSETLIST and "Asset list" or Request.assetdescval) + text=text..string.format("Transports %s of type %s.", tostring(Request.ntransport), tostring(Request.transporttype)) + self:_InfoMessage(text, 5) + end ------------------------------------------------------------------------------------------------------------------------------------ -- Cargo assets. @@ -86004,7 +93829,7 @@ function WAREHOUSE:onafterRequest(From, Event, To, Request) _assetitem.arrived=false local spawngroup=nil --Wrapper.Group#GROUP - + -- Add asset by id to all assets table. Request.assets[_assetitem.uid]=_assetitem @@ -86044,6 +93869,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. @@ -86458,8 +94288,15 @@ end 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 @@ -86467,8 +94304,9 @@ 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 --- On after "Arrived" event. Triggered when a group has arrived at its destination warehouse. @@ -86512,23 +94350,9 @@ function WAREHOUSE:onafterArrived(From, Event, To, group) if group:IsGround() and group:GetSpeedMax()>1 then group:RouteGroundTo(warehouse:GetCoordinate(), group:GetSpeedMax()*0.3, "Off Road") end - - -- NOTE: This is done in the AddAsset() function. Dont know, why we do it also here. - --[[ - if istransport==true then - request.ntransporthome=request.ntransporthome+1 - request.transportgroupset:Remove(group:GetName(), true) - self:T2(warehouse.lid..string.format("Transport %d of %s returned home.", request.ntransporthome, tostring(request.ntransport))) - elseif istransport==false then - request.ndelivered=request.ndelivered+1 - request.cargogroupset:Remove(self:_GetNameWithOut(group), true) - self:T2(warehouse.lid..string.format("Cargo %d of %s delivered.", request.ndelivered, tostring(request.nasset))) - else - self:E(warehouse.lid..string.format("ERROR: Group %s is neither cargo nor transport!", group:GetName())) - end - ]] - + -- Move asset from pending queue into new warehouse. + self:T(self.lid.."Asset arrived at warehouse adding in 60 sec") warehouse:__AddAsset(60, group) end @@ -86543,8 +94367,10 @@ end function WAREHOUSE:onafterDelivered(From, Event, To, request) -- Debug info - local text=string.format("Warehouse %s: All assets delivered to warehouse %s!", self.alias, request.warehouse.alias) - self:_InfoMessage(text, 5) + if self.verbosity>=1 then + local text=string.format("Warehouse %s: All assets delivered to warehouse %s!", self.alias, request.warehouse.alias) + self:_InfoMessage(text, 5) + end -- Make some noise :) if self.Debug then @@ -86734,15 +94560,15 @@ function WAREHOUSE:onafterChangeCountry(From, Event, To, Country) -- Delete all waiting requests because they are not valid any more. self.queue=nil self.queue={} - + if self.airbasename then - + -- Get airbase of this warehouse. local airbase=AIRBASE:FindByName(self.airbasename) - + -- Get coalition of the airbase. - local airbaseCoalition=airbase:GetCoalition() - + local airbaseCoalition=airbase:GetCoalition() + if CoalitionNew==airbaseCoalition then -- Airbase already owned by the coalition that captured the warehouse. Airbase can be used by this warehouse. self.airbase=airbase @@ -86750,7 +94576,7 @@ function WAREHOUSE:onafterChangeCountry(From, Event, To, Country) -- Airbase is owned by other coalition. So this warehouse does not have an airbase until it is captured. self.airbase=nil end - + end -- Debug smoke. @@ -86790,7 +94616,7 @@ function WAREHOUSE:onafterCaptured(From, Event, To, Coalition, Country) -- Message. local text=string.format("Warehouse %s: We were captured by enemy coalition (side=%d)!", self.alias, Coalition) self:_InfoMessage(text) - + end @@ -86845,24 +94671,37 @@ function WAREHOUSE:onafterAirbaseRecaptured(From, Event, To, Coalition) end ---- On before "AssetSpawned" event. Checks whether the asset was already set to "spawned" for groups with multiple units. +--- On after "RunwayDestroyed" event. -- @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 +function WAREHOUSE:onafterRunwayDestroyed(From, Event, To) + + -- Message. + local text=string.format("Warehouse %s: Runway %s destroyed!", self.alias, self.airbasename) + self:_InfoMessage(text) + + self.runwaydestroyed=timer.getAbsTime() + end +--- On after "RunwayRepaired" event. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function WAREHOUSE:onafterRunwayRepaired(From, Event, To) + + -- Message. + local text=string.format("Warehouse %s: Runway %s repaired!", self.alias, self.airbasename) + self:_InfoMessage(text) + + self.runwaydestroyed=nil + +end + + --- On after "AssetSpawned" event triggered when an asset group is spawned into the cruel world. -- @param #WAREHOUSE self -- @param #string From From state. @@ -86873,10 +94712,28 @@ end -- @param #WAREHOUSE.Pendingitem request The request of the dead asset. function WAREHOUSE:onafterAssetSpawned(From, Event, To, group, asset, request) local text=string.format("Asset %s from request id=%d was spawned!", asset.spawngroupname, request.uid) - self:I(self.lid..text) + self:T(self.lid..text) -- 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 @@ -86884,22 +94741,23 @@ function WAREHOUSE:onafterAssetSpawned(From, Event, To, group, asset, request) local assetitem=_asset --#WAREHOUSE.Assetitem -- Debug info. - self:T2(self.lid..string.format("Asset %s spawned %s as %s", assetitem.templatename, tostring(assetitem.spawned), tostring(assetitem.spawngroupname))) - + self:T(self.lid..string.format("Asset %s spawned %s as %s", assetitem.templatename, tostring(assetitem.spawned), tostring(assetitem.spawngroupname))) + if assetitem.spawned then n=n+1 else - self:E(self.lid.."FF What?! This should not happen!") + -- Now this can happend if multiple groups need to be spawned in one request. + --self:I(self.lid.."FF What?! This should not happen!") end end -- Trigger event. if n==request.nasset+request.ntransport then - self:T3(self.lid..string.format("All assets %d (ncargo=%d + ntransport=%d) of request rid=%d spawned. Calling RequestSpawned", n, request.nasset, request.ntransport, request.uid)) + self:T(self.lid..string.format("All assets %d (ncargo=%d + ntransport=%d) of request rid=%d spawned. Calling RequestSpawned", n, request.nasset, request.ntransport, request.uid)) self:RequestSpawned(request, request.cargogroupset, request.transportgroupset) else - self:T3(self.lid..string.format("Not all assets %d (ncargo=%d + ntransport=%d) of request rid=%d spawned YET", n, request.nasset, request.ntransport, request.uid)) + self:T(self.lid..string.format("Not all assets %d (ncargo=%d + ntransport=%d) of request rid=%d spawned YET", n, request.nasset, request.ntransport, request.uid)) end end @@ -86912,9 +94770,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 @@ -86944,11 +94858,11 @@ function WAREHOUSE:onafterDestroyed(From, Event, To) for k,_ in pairs(self.queue) do self.queue[k]=nil end - + for k,_ in pairs(self.stock) do --self.stock[k]=nil end - + for k=#self.stock,1,-1 do --local asset=self.stock[k] --#WAREHOUSE.Assetitem --self:AssetDead(asset, nil) @@ -87196,6 +95110,7 @@ function WAREHOUSE:_SpawnAssetRequest(Request) -- Now we try to find all parking spots for all cargo groups in advance. Due to the for loop, the parking spots do not get updated while spawning. local Parking={} if Request.cargocategory==Group.Category.AIRPLANE or Request.cargocategory==Group.Category.HELICOPTER then + --TODO: Check for airstart. Should be a request property. Parking=self:_FindParkingForAssets(self.airbase, cargoassets) or {} end @@ -87211,30 +95126,30 @@ function WAREHOUSE:_SpawnAssetRequest(Request) -- Set asset status to not spawned until we capture its birth event. asset.spawned=false asset.iscargo=true - + -- Set request ID. asset.rid=Request.uid -- Spawn group name. local _alias=asset.spawngroupname - + --Request add asset by id. - Request.assets[asset.uid]=asset + Request.assets[asset.uid]=asset -- Spawn an asset group. local _group=nil --Wrapper.Group#GROUP 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 @@ -87244,7 +95159,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!") @@ -87252,11 +95167,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 @@ -87269,9 +95189,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 @@ -87311,9 +95231,12 @@ function WAREHOUSE:_SpawnAssetGroundNaval(alias, asset, request, spawnzone, aiof end if asset.skill then unit.skill= asset.skill - end + end end + + -- Late activation. + template.lateActivation=lateactivated template.route.points[1].x = coord.x template.route.points[1].y = coord.z @@ -87325,14 +95248,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 @@ -87346,21 +95261,49 @@ 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 hotstart Spawn aircraft with engines already on. Default is a cold start with engines 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:_SpawnAssetAircraft(alias, asset, request, parking, uncontrolled, hotstart) +function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrolled, lateactivated) if asset and asset.category==Group.Category.AIRPLANE or asset.category==Group.Category.HELICOPTER then -- Prepare the spawn template. local template=self:_SpawnAssetPrepareTemplate(asset, alias) + -- Cold start (default). + local _type=COORDINATE.WaypointType.TakeOffParking + local _action=COORDINATE.WaypointAction.FromParkingArea + + -- Hot start. + if asset.takeoffType and asset.takeoffType==COORDINATE.WaypointType.TakeOffParkingHot then + _type=COORDINATE.WaypointType.TakeOffParkingHot + _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. if request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then -- Get flight path if the group goes to another warehouse by itself. if request.toself then - local wp=self.airbase:GetCoordinate():WaypointAir("RADIO", COORDINATE.WaypointType.TakeOffParking, COORDINATE.WaypointAction.FromParkingArea, 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) @@ -87368,18 +95311,8 @@ function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrol else - -- Cold start (default). - local _type=COORDINATE.WaypointType.TakeOffParking - local _action=COORDINATE.WaypointAction.FromParkingArea - - -- Hot start. - if hotstart then - _type=COORDINATE.WaypointType.TakeOffParkingHot - _action=COORDINATE.WaypointAction.FromParkingAreaHot - end - -- First route point is the warehouse airbase. - template.route.points[1]=self.airbase:GetCoordinate():WaypointAir("BARO",_type,_action, 0, true, self.airbase, nil, "Spawnpoint") + template.route.points[1]=self.airbase:GetCoordinate():WaypointAir("BARO", _type, _action, 0, true, self.airbase, nil, "Spawnpoint") end @@ -87394,7 +95327,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 @@ -87416,17 +95349,30 @@ 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)) + local text=string.format("Spawnplace unit %s terminal %d.", unit.name, terminal) + coord:MarkToAll(text) + env.info(text) end unit.x=coord.x @@ -87447,15 +95393,15 @@ function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrol end if asset.payload then - unit.payload=asset.payload.pylons + unit.payload=asset.payload.pylons end - + if asset.modex then unit.onboard_num=asset.modex[i] end if asset.callsign then unit.callsign=asset.callsign[i] - end + end end @@ -87508,12 +95454,12 @@ function WAREHOUSE:_SpawnAssetPrepareTemplate(asset, alias) -- No late activation. template.lateActivation=false - + if asset.missionTask then - self:I(self.lid..string.format("Setting mission task to %s", tostring(asset.missionTask))) + self:T(self.lid..string.format("Setting mission task to %s", tostring(asset.missionTask))) template.task=asset.missionTask end - + -- No predefined task. --template.taskSelected=false @@ -87598,18 +95544,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) @@ -87687,9 +95625,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()))) @@ -87792,7 +95733,7 @@ end --- Get a warehouse request from its unique id. -- @param #WAREHOUSE self -- @param #number id Request ID. --- @return #WAREHOUSE.Pendingitem The warehouse requested - either queued or pending. +-- @return #WAREHOUSE.Pendingitem The warehouse requested - either queued or pending. -- @return #boolean If *true*, request is queued, if *false*, request is pending, if *nil*, request could not be found. function WAREHOUSE:GetRequestByID(id) @@ -87830,40 +95771,21 @@ function WAREHOUSE:_OnEventBirth(EventData) local wid,aid,rid=self:_GetIDsFromGroup(group) if wid==self.uid then - + -- Get asset and request from id. local asset=self:GetAssetByID(aid) local request=self:GetRequestByID(rid) - - -- Debug message. - self:T(self.lid..string.format("Warehouse %s captured event birth of its asset unit %s. spawned=%s", self.alias, EventData.IniUnitName, tostring(asset.spawned))) - - -- 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) - self:AssetSpawned(group, asset, request) + + 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 + + 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))) end else @@ -87983,7 +95905,6 @@ function WAREHOUSE:_OnEventArrived(EventData) local istransport=self:_GroupIsTransport(group, request) -- Get closest airbase. - -- Note, this crashed at somepoint when the Tarawa was in the mission. Don't know why. Deleting the Tarawa and adding it again solved the problem. local closest=group:GetCoordinate():GetClosestAirbase() -- Check if engine shutdown happend at right airbase because the event is also triggered in other situations. @@ -87992,15 +95913,19 @@ function WAREHOUSE:_OnEventArrived(EventData) -- Check that group is cargo and not transport. if istransport==false and rightairbase then - -- Debug info. - local text=string.format("Air asset group %s from warehouse %s arrived at its destination.", group:GetName(), self.alias) - self:_InfoMessage(text) - -- Trigger arrived event for this group. Note that each unit of a group will trigger this event. So the onafterArrived function needs to take care of that. -- Actually, we only take the first unit of the group that arrives. If it does, we assume the whole group arrived, which might not be the case, since -- some units might still be taxiing or whatever. Therefore, we add 10 seconds for each additional unit of the group until the first arrived event is triggered. local nunits=#group:GetUnits() local dt=10*(nunits-1)+1 -- one unit = 1 sec, two units = 11 sec, three units = 21 sec before we call the group arrived. + + -- Debug info. + if self.verbosity>=1 then + local text=string.format("Air asset group %s from warehouse %s arrived at its destination. Trigger Arrived event in %d sec", group:GetName(), self.alias, dt) + self:_InfoMessage(text) + end + + -- Arrived event. self:__Arrived(dt, group) end @@ -88034,9 +95959,13 @@ function WAREHOUSE:_OnEventCrashOrDead(EventData) -- Trigger Destroyed event. self:Destroyed() end + if self.airbase and self.airbasename and self.airbasename==EventData.IniUnitName then + self:RunwayDestroyed() + 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 @@ -88051,7 +95980,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 @@ -88061,7 +95990,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 @@ -88074,38 +96003,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 @@ -88113,19 +96050,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: @@ -88133,10 +96058,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] @@ -88151,25 +96073,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 @@ -88212,7 +96117,7 @@ function WAREHOUSE:_OnEventBaseCaptured(EventData) self:AirbaseRecaptured(NewCoalitionAirbase) end else - -- Captured airbase belongs to this warehouse but was captured by other coaltion. + -- Captured airbase belongs to this warehouse but was captured by other coalition. if NewCoalitionAirbase ~= self:GetCoalition() then self:AirbaseCaptured(NewCoalitionAirbase) end @@ -88405,7 +96310,7 @@ function WAREHOUSE:_CheckRequestConsistancy(queue) -- Request from enemy coalition? if self:GetCoalition()~=request.warehouse:GetCoalition() then - self:E(self.lid..string.format("ERROR: INVALID request. Requesting warehouse is of wrong coaltion! Own coalition %s != %s of requesting warehouse.", self:GetCoalitionName(), request.warehouse:GetCoalitionName())) + self:E(self.lid..string.format("ERROR: INVALID request. Requesting warehouse is of wrong coalition! Own coalition %s != %s of requesting warehouse.", self:GetCoalitionName(), request.warehouse:GetCoalitionName())) valid=false end @@ -88555,10 +96460,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. @@ -88773,6 +96677,7 @@ function WAREHOUSE:_CheckRequestNow(request) local _transports local _assetattribute local _assetcategory + local _assetairstart=false -- Check if at least one (cargo) asset is available. if _nassets>0 then @@ -88780,18 +96685,44 @@ function WAREHOUSE:_CheckRequestNow(request) -- Get the attibute of the requested asset. _assetattribute=_assets[1].attribute _assetcategory=_assets[1].category + _assetairstart=_assets[1].takeoffType and _assets[1].takeoffType==COORDINATE.WaypointType.TurningPoint or false -- Check available parking for air asset units. - if self.airbase and (_assetcategory==Group.Category.AIRPLANE or _assetcategory==Group.Category.HELICOPTER) then - - local Parking=self:_FindParkingForAssets(self.airbase,_assets) - - --if Parking==nil and not (self.category==Airbase.Category.HELIPAD) then - if Parking==nil then - local text=string.format("Warehouse %s: Request denied! Not enough free parking spots for all requested assets at the moment.", self.alias) + if _assetcategory==Group.Category.AIRPLANE or _assetcategory==Group.Category.HELICOPTER then + + if self.airbase and self.airbase:GetCoalition()==self:GetCoalition() then + + if self:IsRunwayOperational() or _assetairstart then + + if _assetairstart then + -- Airstart no need to check parking + else + + -- Check parking. + local Parking=self:_FindParkingForAssets(self.airbase,_assets) + + -- No parking? + if Parking==nil then + local text=string.format("Warehouse %s: Request denied! Not enough free parking spots for all requested assets at the moment.", self.alias) + self:_InfoMessage(text, 5) + return false + end + end + + else + -- Runway destroyed. + local text=string.format("Warehouse %s: Request denied! Runway is still destroyed", self.alias) + self:_InfoMessage(text, 5) + return false + end + + else + + -- No airbase! + local text=string.format("Warehouse %s: Request denied! No airbase", self.alias) self:_InfoMessage(text, 5) - - return false + return false + end end @@ -88815,14 +96746,37 @@ function WAREHOUSE:_CheckRequestNow(request) local _transportcategory=_transports[1].category -- Check available parking for transport units. - if self.airbase and (_transportcategory==Group.Category.AIRPLANE or _transportcategory==Group.Category.HELICOPTER) then - local Parking=self:_FindParkingForAssets(self.airbase,_transports) - if Parking==nil then - local text=string.format("Warehouse %s: Request denied! Not enough free parking spots for all transports at the moment.", self.alias) - self:_InfoMessage(text, 5) - - return false + if _transportcategory==Group.Category.AIRPLANE or _transportcategory==Group.Category.HELICOPTER then + + if self.airbase and self.airbase:GetCoalition()==self:GetCoalition() then + + if self:IsRunwayOperational() then + + local Parking=self:_FindParkingForAssets(self.airbase,_transports) + + -- No parking ==> return false + if Parking==nil then + local text=string.format("Warehouse %s: Request denied! Not enough free parking spots for all transports at the moment.", self.alias) + self:_InfoMessage(text, 5) + return false + end + + else + + -- Runway destroyed. + local text=string.format("Warehouse %s: Request denied! Runway is still destroyed", self.alias) + self:_InfoMessage(text, 5) + return false + + end + + else + -- No airbase + local text=string.format("Warehouse %s: Request denied! No airbase currently!", self.alias) + self:_InfoMessage(text, 5) + return false end + end else @@ -88836,7 +96790,9 @@ function WAREHOUSE:_CheckRequestNow(request) else - -- Self propelled case. Nothing to do for now. + --- + -- Self propelled case + --- -- Ground asset checks. if _assetcategory==Group.Category.GROUND then @@ -89096,7 +97052,7 @@ function WAREHOUSE:_CheckQueue() -- Check if request is possible now. local okay=false - if valid then + if valid then okay=self:_CheckRequestNow(qitem) else -- Remember invalid request and delete later in order not to confuse the loop. @@ -89136,7 +97092,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. @@ -89167,7 +97123,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. @@ -89225,7 +97181,7 @@ end function WAREHOUSE:_FindParkingForAssets(airbase, assets) -- Init default - local scanradius=100 + local scanradius=25 local scanunits=true local scanstatics=true local scanscenery=false @@ -89241,31 +97197,36 @@ 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 - --[[ - local airbase=coord:GetClosestAirbase() - local _,TermID, dist, spot=coord:GetClosestParkingSpot(airbase) - if dist<=10 then - env.info(string.format("Found client %s on parking spot %d at airbase %s", unit.name, TermID, airbase:GetName())) + 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 end -- Get parking spot data table. This contains all free and "non-free" spots. - local parkingdata=airbase:GetParkingSpotsTable() + local parkingdata=airbase.parking --airbase:GetParkingSpotsTable() + + --- + -- Find all obstacles + --- -- List of obstacles. local obstacles={} + + -- Check all clients. Clients dont change so we can put that out of the loop. + self.clientcoords=self.clientcoords or _clients() + for clientname,_coord in pairs(self.clientcoords) do + table.insert(obstacles, {coord=_coord, size=15, name=clientname, type="client"}) + end -- Loop over all parking spots and get the currently present obstacles. -- How long does this take on very large airbases, i.e. those with hundereds of parking spots? Seems to be okay! @@ -89281,22 +97242,18 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) -- Check all units. for _,_unit in pairs(_units) do local unit=_unit --Wrapper.Unit#UNIT - local _coord=unit:GetCoordinate() + 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"}) - end - - -- Check all clients. - local clientcoords=_clients() - for clientname,_coord in pairs(clientcoords) do - table.insert(obstacles, {coord=_coord, size=15, name=clientname, type="client"}) + if unit and unit:IsAlive() then + table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="unit"}) + end end -- Check all statics. for _,static in pairs(_statics) do - local _vec3=static:getPoint() - local _coord=COORDINATE:NewFromVec3(_vec3) + local _coord=static:getPoint() + --local _coord=COORDINATE:NewFromVec3(_vec3) local _name=static:getName() local _size=self:_GetObjectSize(static) table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="static"}) @@ -89304,14 +97261,18 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) -- Check all scenery. for _,scenery in pairs(_sceneries) do - local _vec3=scenery:getPoint() - local _coord=COORDINATE:NewFromVec3(_vec3) + local _coord=scenery:getPoint() + --local _coord=COORDINATE:NewFromVec3(_vec3) local _name=scenery:getTypeName() local _size=self:_GetObjectSize(scenery) - table.insert(obstacles,{coord=_coord, size=_size, name=_name, type="scenery"}) + table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="scenery"}) end end + + --- + -- Get Parking Spots + --- -- Parking data for all assets. local parking={} @@ -89319,96 +97280,123 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) -- Loop over all assets that need a parking psot. for _,asset in pairs(assets) do local _asset=asset --#WAREHOUSE.Assetitem + + if not _asset.spawned then - -- Get terminal type of this asset - local terminaltype=asset.terminalType or self:_GetTerminal(asset.attribute, self:GetAirbaseCategory()) - - -- Asset specific parking. - parking[_asset.uid]={} - - -- Loop over all units - each one needs a spot. - for i=1,_asset.nunits do - - -- Loop over all parking spots. - local gotit=false - for _,_parkingspot in pairs(parkingdata) do - 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 - - -- Coordinate of the parking spot. - local _spot=parkingspot.Coordinate -- Core.Point#COORDINATE - local _termid=parkingspot.TerminalID - local _toac=parkingspot.TOAC - - --env.info(string.format("FF asset=%s (id=%d): needs terminal type=%d, id=%d, #obstacles=%d", _asset.templatename, _asset.uid, terminaltype, _termid, #obstacles)) - - local free=true - local problem=nil - - -- Safe parking using TO_AC from DCS result. - self:I(self.lid..string.format("Parking spot %d TOAC=%s (safe park=%s).", _termid, tostring(_toac), tostring(self.safeparking))) - if self.safeparking and _toac then - free=false - self:I(self.lid..string.format("Parking spot %d is occupied by other aircraft taking off (TOAC).", _termid)) + -- Get terminal type of this asset + local terminaltype=asset.terminalType or self:_GetTerminal(asset.attribute, self:GetAirbaseCategory()) + + -- Asset specific parking. + parking[_asset.uid]={} + + -- 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 + for _,_parkingspot in pairs(parkingdata) do + local parkingspot=_parkingspot --Wrapper.Airbase#AIRBASE.ParkingSpot + + -- Parking valid? + local valid=true + + if asset.parkingIDs then + -- If asset has assigned parking spots, we take these no matter what. + valid=self:_CheckParkingAsset(parkingspot, asset) + else + + -- Valid terminal type depending on attribute. + local validTerminal=AIRBASE._CheckTerminalType(parkingspot.TerminalType, terminaltype) + + -- Valid parking list. + local validParking=self:_CheckParkingValid(parkingspot) + + -- Black and white list. + local validBWlist=airbase:_CheckParkingLists(parkingspot.TerminalID) + + -- Debug info. + --env.info(string.format("FF validTerminal = %s", tostring(validTerminal))) + --env.info(string.format("FF validParking = %s", tostring(validParking))) + --env.info(string.format("FF validBWlist = %s", tostring(validBWlist))) + + -- Check if all are true + valid=validTerminal and validParking and validBWlist end - - -- Loop over all obstacles. - for _,obstacle in pairs(obstacles) do - - -- Check if aircraft overlaps with any obstacle. - local dist=_spot:Get2DDistance(obstacle.coord) - local safe=_overlap(_asset.size, obstacle.size, dist) - - -- 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)) - free=false - problem=obstacle - problem.dist=dist + + + -- Check correct terminal type for asset. We don't want helos in shelters etc. + if valid then + + -- Coordinate of the parking spot. + local _spot=parkingspot.Coordinate -- Core.Point#COORDINATE + local _termid=parkingspot.TerminalID + local free=true + local problem=nil + + -- Loop over all obstacles. + for _,obstacle in pairs(obstacles) do + + -- Check if aircraft overlaps with any obstacle. + local dist=_spot:Get2DDistance(obstacle.coord) + local safe=_overlap(_asset.size, obstacle.size, dist) + + -- Spot is blocked. + if not safe then + 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", assetname, _asset.uid, _termid, dist)) + end + + end + + -- Check if spot is free + if free then + + -- Add parkingspot for this asset unit. + table.insert(parking[_asset.uid], parkingspot) + + -- Debug + 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=assetname, type="asset"}) + + gotit=true break + else - --env.info(string.format("FF asset=%s (id=%d): spot id=%d dist=%.1fm is SAFE", _asset.templatename, _asset.uid, _termid, dist)) + + -- Debug output for occupied spots. + if self.Debug then + local coord=problem.coord --Core.Point#COORDINATE + 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 - - end - - -- Check if spot is free - if free then - - -- Add parkingspot for this asset unit. - table.insert(parking[_asset.uid], parkingspot) - - self:I(self.lid..string.format("Parking spot %d is free for asset id=%d!", _termid, _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"}) - - gotit=true - break - + else - - -- Debug output for occupied spots. - self:I(self.lid..string.format("Parking spot %d is occupied or not big enough!", _termid)) - 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) - coord:MarkToAll(string.format(text)) - end - - end - - 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)) - return nil - end - end -- loop over asset units + 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 %s [id=%d]", assetname, _asset.uid)) + return nil + end + end -- loop over asset units + end -- Asset spawned check end -- loop over asset groups return parking @@ -89443,27 +97431,27 @@ end function WAREHOUSE:_GroupIsTransport(group, request) local asset=self:FindAssetInDB(group) - + if asset and asset.iscargo~=nil then return not asset.iscargo else -- Name of the group under question. local groupname=self:_GetNameWithOut(group) - + if request.transportgroupset then local transporters=request.transportgroupset:GetSetObjects() - + for _,transport in pairs(transporters) do if transport:GetName()==groupname then return true end end end - + if request.cargogroupset then local cargos=request.cargogroupset:GetSetObjects() - + for _,cargo in pairs(cargos) do if self:_GetNameWithOut(cargo)==groupname then return false @@ -89483,14 +97471,14 @@ end function WAREHOUSE:_GetNameWithOut(group) local groupname=type(group)=="string" and group or group:GetName() - + if groupname:find("CARGO") then local name=groupname:gsub("#CARGO", "") return name else return groupname end - + end @@ -89502,6 +97490,28 @@ end -- @return #number Request ID. function WAREHOUSE:_GetIDsFromGroup(group) + if group then + + -- Group name + local groupname=group:GetName() + + local wid, aid, rid=self:_GetIDsFromGroupName(groupname) + + return wid,aid,rid + else + self:E("WARNING: Group not found in GetIDsFromGroup() function!") + end + +end + +--- Get warehouse id, asset id and request id from group name (alias). +-- @param #WAREHOUSE self +-- @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:_GetIDsFromGroupName(groupname) + ---@param #string text The text to analyse. local function analyse(text) @@ -89531,93 +97541,26 @@ function WAREHOUSE:_GetIDsFromGroup(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(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))) + -- Get asset id from group name. + local wid,aid,rid=analyse(groupname) - return wid,aid,rid - else - self:E("WARNING: Group not found in GetIDsFromGroup() function!") - end - -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. --- @return #number Warehouse ID. --- @return #number Asset ID. --- @return #number Request ID. -function WAREHOUSE:_GetIDsFromGroupOLD(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 + -- Get Asset. + local asset=self:GetAssetByID(aid) - return _wid,_aid,_rid + -- Get warehouse and request id from asset table. + if asset then + wid=asset.wid + rid=asset.rid end - if group then - - -- Group name - local name=group:GetName() - - -- Get ids - local wid,aid,rid=analyse(name) - - -- 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!") - 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. @@ -89653,23 +97596,23 @@ function WAREHOUSE:_FilterStock(stock, descriptor, attribute, nmax, mobile) -- Filtered array. local filtered={} - + -- A specific list of assets was required. if descriptor==WAREHOUSE.Descriptor.ASSETLIST then -- Count total number in stock. local ntot=0 for _,_rasset in pairs(attribute) do - local rasset=_rasset --#WAREHOUSE.Assetitem + local rasset=_rasset --#WAREHOUSE.Assetitem for _,_asset in ipairs(stock) do - local asset=_asset --#WAREHOUSE.Assetitem + local asset=_asset --#WAREHOUSE.Assetitem if rasset.uid==asset.uid then table.insert(filtered, asset) break end - end + end end - + return filtered, #filtered, #filtered>=#attribute end @@ -89763,9 +97706,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") @@ -89802,6 +97746,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 @@ -90015,67 +97961,70 @@ end -- @param #string name Name of the queue for info reasons. function WAREHOUSE:_PrintQueue(queue, name) - local total="Empty" - if #queue>0 then - total=string.format("Total = %d", #queue) - end - - -- Init string. - local text=string.format("%s at %s: %s",name, self.alias, total) + if self.verbosity>=2 then - for i,qitem in ipairs(queue) do - local qitem=qitem --#WAREHOUSE.Pendingitem - - local uid=qitem.uid - local prio=qitem.prio - local clock="N/A" - if qitem.timestamp then - clock=tostring(UTILS.SecondsToClock(qitem.timestamp)) - end - local assignment=tostring(qitem.assignment) - local requestor=qitem.warehouse.alias - local airbasename=qitem.warehouse:GetAirbaseName() - local requestorAirbaseCat=qitem.warehouse:GetAirbaseCategory() - local assetdesc=qitem.assetdesc - local assetdescval=qitem.assetdescval - if assetdesc==WAREHOUSE.Descriptor.ASSETLIST then - assetdescval="Asset list" - end - local nasset=tostring(qitem.nasset) - local ndelivered=tostring(qitem.ndelivered) - local ncargogroupset="N/A" - if qitem.cargogroupset then - ncargogroupset=tostring(qitem.cargogroupset:Count()) - end - local transporttype="N/A" - if qitem.transporttype then - transporttype=qitem.transporttype + local total="Empty" + if #queue>0 then + total=string.format("Total = %d", #queue) end - local ntransport="N/A" - if qitem.ntransport then - ntransport=tostring(qitem.ntransport) - end - local ntransportalive="N/A" - if qitem.transportgroupset then - ntransportalive=tostring(qitem.transportgroupset:Count()) - end - local ntransporthome="N/A" - if qitem.ntransporthome then - ntransporthome=tostring(qitem.ntransporthome) + + -- Init string. + local text=string.format("%s at %s: %s",name, self.alias, total) + + for i,qitem in ipairs(queue) do + local qitem=qitem --#WAREHOUSE.Pendingitem + + local uid=qitem.uid + local prio=qitem.prio + local clock="N/A" + if qitem.timestamp then + clock=tostring(UTILS.SecondsToClock(qitem.timestamp)) + end + local assignment=tostring(qitem.assignment) + local requestor=qitem.warehouse.alias + local airbasename=qitem.warehouse:GetAirbaseName() + local requestorAirbaseCat=qitem.warehouse:GetAirbaseCategory() + local assetdesc=qitem.assetdesc + local assetdescval=qitem.assetdescval + if assetdesc==WAREHOUSE.Descriptor.ASSETLIST then + assetdescval="Asset list" + end + local nasset=tostring(qitem.nasset) + local ndelivered=tostring(qitem.ndelivered) + local ncargogroupset="N/A" + if qitem.cargogroupset then + ncargogroupset=tostring(qitem.cargogroupset:Count()) + end + local transporttype="N/A" + if qitem.transporttype then + transporttype=qitem.transporttype + end + local ntransport="N/A" + if qitem.ntransport then + ntransport=tostring(qitem.ntransport) + end + local ntransportalive="N/A" + if qitem.transportgroupset then + ntransportalive=tostring(qitem.transportgroupset:Count()) + end + local ntransporthome="N/A" + if qitem.ntransporthome then + ntransporthome=tostring(qitem.ntransporthome) + end + + -- Output text: + text=text..string.format( + "\n%d) UID=%d, Prio=%d, Clock=%s, Assignment=%s | Requestor=%s [Airbase=%s, category=%d] | Assets(%s)=%s: #requested=%s / #alive=%s / #delivered=%s | Transport=%s: #requested=%s / #alive=%s / #home=%s", + i, uid, prio, clock, assignment, requestor, airbasename, requestorAirbaseCat, assetdesc, assetdescval, nasset, ncargogroupset, ndelivered, transporttype, ntransport, ntransportalive, ntransporthome) + end - - -- Output text: - text=text..string.format( - "\n%d) UID=%d, Prio=%d, Clock=%s, Assignment=%s | Requestor=%s [Airbase=%s, category=%d] | Assets(%s)=%s: #requested=%s / #alive=%s / #delivered=%s | Transport=%s: #requested=%s / #alive=%s / #home=%s", - i, uid, prio, clock, assignment, requestor, airbasename, requestorAirbaseCat, assetdesc, assetdescval, nasset, ncargogroupset, ndelivered, transporttype, ntransport, ntransportalive, ntransporthome) - - end - - if #queue==0 then - self:T(self.lid..text) - else - if total~="Empty" then + + if #queue==0 then self:I(self.lid..text) + else + if total~="Empty" then + self:I(self.lid..text) + end end end end @@ -90083,17 +98032,19 @@ end --- Display status of warehouse. -- @param #WAREHOUSE self function WAREHOUSE:_DisplayStatus() - local text=string.format("\n------------------------------------------------------\n") - text=text..string.format("Warehouse %s status: %s\n", self.alias, self:GetState()) - text=text..string.format("------------------------------------------------------\n") - text=text..string.format("Coalition name = %s\n", self:GetCoalitionName()) - text=text..string.format("Country name = %s\n", self:GetCountryName()) - text=text..string.format("Airbase name = %s (category=%d)\n", self:GetAirbaseName(), self:GetAirbaseCategory()) - text=text..string.format("Queued requests = %d\n", #self.queue) - text=text..string.format("Pending requests = %d\n", #self.pending) - text=text..string.format("------------------------------------------------------\n") - text=text..self:_GetStockAssetsText() - self:T(text) + if self.verbosity>=3 then + local text=string.format("\n------------------------------------------------------\n") + text=text..string.format("Warehouse %s status: %s\n", self.alias, self:GetState()) + text=text..string.format("------------------------------------------------------\n") + text=text..string.format("Coalition name = %s\n", self:GetCoalitionName()) + text=text..string.format("Country name = %s\n", self:GetCountryName()) + text=text..string.format("Airbase name = %s (category=%d)\n", self:GetAirbaseName(), self:GetAirbaseCategory()) + text=text..string.format("Queued requests = %d\n", #self.queue) + text=text..string.format("Pending requests = %d\n", #self.pending) + text=text..string.format("------------------------------------------------------\n") + text=text..self:_GetStockAssetsText() + self:I(text) + end end --- Get text about warehouse stock. @@ -90126,36 +98077,56 @@ function WAREHOUSE:_GetStockAssetsText(messagetoall) end --- Create or update mark text at warehouse, which is displayed in F10 map showing how many assets of each type are in stock. --- Only the coaliton of the warehouse owner is able to see it. +-- Only the coalition of the warehouse owner is able to see it. -- @param #WAREHOUSE self -- @return #string Text about warehouse stock function WAREHOUSE:_UpdateWarehouseMarkText() if self.markerOn then - -- Create a mark with the current assets in stock. - if self.markerid~=nil then - trigger.action.removeMark(self.markerid) - end - - -- Get assets in stock. - local _data=self:GetStockInfo(self.stock) - - -- Text. + -- Marker text. local text=string.format("Warehouse state: %s\nTotal assets in stock %d:\n", self:GetState(), #self.stock) - - for _attribute,_count in pairs(_data) do + for _attribute,_count in pairs(self:GetStockInfo(self.stock) or {}) do if _count>0 then local attribute=tostring(UTILS.Split(_attribute, "_")[2]) text=text..string.format("%s=%d, ", attribute,_count) end end + + local coordinate=self:GetCoordinate() + local coalition=self:GetCoalition() + + if not self.markerWarehouse then + + -- Create a new marker. + self.markerWarehouse=MARKER:New(coordinate, text):ToCoalition(coalition) + + else - -- Create/update marker at warehouse in F10 map. - self.markerid=self:GetCoordinate():MarkToCoalition(text, self:GetCoalition(), true) + local refresh=false - end + if self.markerWarehouse.text~=text then + self.markerWarehouse.text=text + refresh=true + end + + if self.markerWarehouse.coordinate~=coordinate then + self.markerWarehouse.coordinate=coordinate + refresh=true + end + + if self.markerWarehouse.coalition~=coalition then + self.markerWarehouse.coalition=coalition + refresh=true + end + + if refresh then + self.markerWarehouse:Refresh() + end + end + end + end --- Display stock items of warehouse. @@ -90206,8 +98177,8 @@ end -- @param #number duration Message display duration in seconds. Default 20 sec. If duration is zero, no message is displayed. function WAREHOUSE:_InfoMessage(text, duration) duration=duration or 20 - if duration>0 then - MESSAGE:New(text, duration):ToCoalitionIf(self:GetCoalition(), self.Debug or self.Report) + if duration>0 and self.Debug or self.Report then + MESSAGE:New(text, duration):ToCoalition(self:GetCoalition()) end self:I(self.lid..text) end @@ -90219,7 +98190,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) @@ -90520,9 +98491,23 @@ function WAREHOUSE:_GetFlightplan(asset, departure, destination) local wp={} local c={} + -- Cold start (default). + local _type=COORDINATE.WaypointType.TakeOffParking + local _action=COORDINATE.WaypointAction.FromParkingArea + + -- Hot start. + if asset.takeoffType and asset.takeoffType==COORDINATE.WaypointType.TakeOffParkingHot then + --env.info("FF hot") + _type=COORDINATE.WaypointType.TakeOffParkingHot + _action=COORDINATE.WaypointAction.FromParkingAreaHot + else + --env.info("FF cold") + end + + --- Departure/Take-off c[#c+1]=Pdeparture - wp[#wp+1]=Pdeparture:WaypointAir("RADIO", COORDINATE.WaypointType.TakeOffParking, COORDINATE.WaypointAction.FromParkingArea, VxClimb*3.6, true, departure, nil, "Departure") + wp[#wp+1]=Pdeparture:WaypointAir("RADIO", _type, _action, VxClimb*3.6, true, departure, nil, "Departure") --- Begin of Cruise local Pcruise=Pdeparture:Translate(d_climb, heading) @@ -90561,11 +98546,10 @@ function WAREHOUSE:_GetFlightplan(asset, departure, destination) return wp,c end - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- **Functional** - (R2.5) - Yet Another Missile Trainer. +--- **Functional** - Yet Another Missile Trainer. -- -- -- Practice to evade missiles without being destroyed. @@ -90587,10 +98571,9 @@ end -- === -- -- ### Author: **funkyfranky** --- @module Functional.FOX +-- @module Functional.Fox -- @image Functional_FOX.png - --- FOX class. -- @type FOX -- @field #string ClassName Name of the class. @@ -90614,8 +98597,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! @@ -91061,7 +99043,6 @@ end --- Disable F10 menu for all players. -- @param #FOX self --- @param #boolean switch If true debug mode on. If false/nil debug mode off -- @return #FOX self function FOX:SetDisableF10Menu() @@ -91070,6 +99051,16 @@ function FOX:SetDisableF10Menu() return self end +--- Enable F10 menu for all players. +-- @param #FOX self +-- @return #FOX self +function FOX:SetEnableF10Menu() + + self.menudisabled=false + + return self +end + --- Set default player setting for missile destruction. -- @param #FOX self -- @param #boolean switch If true missiles are destroyed. If false/nil missiles are not destroyed. @@ -91361,7 +99352,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. @@ -91683,6 +99674,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 @@ -91724,7 +99722,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, self._AddF10Commands, self, _unitName) end -- Player data. @@ -91991,10 +99989,10 @@ function FOX:_AddF10Commands(_unitName) 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 or "unknown")) 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 or "unknown")) end end @@ -92380,35 +100378,36 @@ 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 --- +-- +-- ## Features: +-- +-- * Moose derived Modular, Automatic and Network capable Targeting and Interception System. +-- * 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. +-- -- === --- +-- -- ## Missions: -- -- ### [MANTIS - Modular, Automatic and Network capable Targeting and Interception System](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/MTS%20-%20Mantis/MTS-010%20-%20Basic%20Mantis%20Demo) --- +-- -- === --- +-- -- ### Author : **applevangelist ** --- +-- -- @module Functional.Mantis -- @image Functional.Mantis.jpg - --- Date: July 2021 +-- +-- Last Update: Oct 2022 ------------------------------------------------------------------------- ---- **MANTIS** class, extends #Core.Base#BASE +--- **MANTIS** class, extends Core.Base#BASE -- @type MANTIS --- @field #string Classname +-- @field #string ClassName -- @field #string name Name of this Mantis -- @field #string SAM_Templates_Prefix Prefix to build the #SET_GROUP for SAM sites -- @field Core.Set#SET_GROUP SAM_Group The SAM #SET_GROUP @@ -92442,105 +100441,214 @@ end -- @extends Core.Base#BASE ---- *The worst thing that can happen to a good cause is, not to be skillfully attacked, but to be ineptly defended.* - Frédéric Bastiat --- --- Simple Class for a more intelligent Air Defense System +--- *The worst thing that can happen to a good cause is, not to be skillfully attacked, but to be ineptly defended.* - Frédéric Bastiat +-- +-- Moose class for a more intelligent Air Defense System +-- +-- # MANTIS -- --- #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. +-- * Moose derived Modular, Automatic and Network capable Targeting and Interception System. +-- * 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. --- [optional] Set up your HQ. Can be any group, e.g. a command vehicle. +-- # 0. Base considerations and naming conventions -- --- # 1. Basic tactical considerations when setting up your SAM sites +-- **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! -- --- ## 1.1 Radar systems and AWACS +-- 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 +-- * From HDS (see note on HDS below): SA-2, SA-3, SA-10B, SA-10C, SA-12, SA-17, SA-20A, SA-20B, SA-23, HQ-2 +-- * From SMA: RBS98M, RBS70, RBS90, RBS90M, RBS103A, RBS103B, RBS103AM, RBS103BM, Lvkv9040M +-- **NOTE** If you are using the Swedish Military Assets (SMA), please note that the **group name** for RBS-SAM types also needs to contain the keyword "SMA" +-- +-- 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. --- +-- 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 --- 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, +-- +-- 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. --- +-- -- ## 1.3 Typical problems +-- +-- Often times, people complain because the detection cannot "see" oncoming targets and/or Mantis switches on too late. Three typial problems here are +-- +-- * 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 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 mission 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() +-- +-- Use +-- +-- * 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 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() -- --- Often times, people complain because the detection cannot "see" oncoming targets and/or Mantis switches on too late. Three typial problems here are --- --- * 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. +-- ## 2.1 Auto mode features -- --- 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.1.1 You can now add Accept-, Reject- and Conflict-Zones to your setup, e.g. to consider borders or de-militarized zones: -- --- # 2. Start up your MANTIS with a basic setting +-- -- 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: -- --- `myredmantis = MANTIS:New("myredmantis","Red SAM","Red EWR",nil,"red",false)` --- `myredmantis:Start()` --- --- [optional] Use +-- -- parameters are numbers. Defaults are 1,2,2,6 respectively +-- `mybluemantis:SetMaxActiveSAMs(Short,Mid,Long,Classic)` -- --- * `MANTIS:SetEWRGrouping(radius)` --- * `MANTIS:SetEWRRange(radius)` --- * `MANTIS:SetSAMRadius(radius)` --- * `MANTIS:SetDetectInterval(interval)` --- * `MANTIS:SetAutoRelocate(hq, ewr)` +-- ### 2.1.3 SHORAD will automatically be added from SAM sites of type "short-range" -- --- before starting #MANTIS to fine-tune your setup. +-- ### 2.1.4 Advanced features -- --- If you want to use a separate AWACS unit (default detection range: 250km) to support your EWR system, use e.g. the following setup: +-- -- 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` -- --- `mybluemantis = MANTIS:New("bluemantis","Blue SAM","Blue EWR",nil,"blue",false,"Blue Awacs")` --- `mybluemantis:Start()` +-- # 3. Default settings [both modes unless stated otherwise] -- --- # 3. Default settings --- -- 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)` --- * 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)` +-- * 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,coalition,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)` -- -- # 4. Advanced Mode --- --- 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. +-- +-- 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 ) +-- +-- Use this option if you want to make use of or allow advanced SEAD tactics. +-- +-- # 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() -- --- E.g. `mymantis:SetAdvancedMode( true, 90 )` +-- If you systematically name your SHORAD groups starting with "Blue SHORAD" you'll need exactly **one** SHORAD instance to manage all SHORAD groups. -- --- Use this option if you want to make use of or allow advanced SEAD tactics. +-- (Optionally) you can remove the link later on with +-- +-- mymantis:RemoveShorad() +-- +-- # 6. Integrated SEAD -- --- # 5. Integrate SHORAD +-- 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 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: +-- You can link into the SEAD driven events of 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()` --- --- and (optionally) remove the link later on with +-- 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 -- --- `mymantis:RemoveShorad()` --- -- @field #MANTIS MANTIS = { ClassName = "MANTIS", @@ -92550,9 +100658,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, @@ -92561,7 +100672,7 @@ MANTIS = { grouping = 5000, acceptrange = 80000, detectinterval = 30, - engagerange = 75, + engagerange = 95, autorelocate = false, advanced = false, adv_ratio = 100, @@ -92573,11 +100684,18 @@ MANTIS = { Shorad = nil, ShoradLink = false, ShoradTime = 600, - ShoradActDistance = 15000, + ShoradActDistance = 25000, UseEmOnOff = false, TimeStamp = 0, state2flag = false, SamStateTracker = {}, + DLink = false, + DLTimeStamp = 0, + Padding = 10, + SuppressedGroups = {}, + automode = true, + autoshorad = true, + ShoradGroupSet = nil, } --- Advanced state enumerator @@ -92588,6 +100706,95 @@ 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" }, +} + +--- SAM data SMA +-- @type MANTIS.SamDataSMA +-- @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.SamDataSMA = { + -- units from SMA Mod (Sweedish Military Assets) + -- https://forum.dcs.world/topic/295202-swedish-military-assets-for-dcs-by-currenthill/ + -- group name MUST contain SMA to ID launcher type correctly! + ["RBS98M SMA"] = { Range=20, Blindspot=0, Height=8, Type="Short", Radar="RBS-98" }, + ["RBS70 SMA"] = { Range=8, Blindspot=0, Height=5.5, Type="Short", Radar="RBS-70" }, + ["RBS70M SMA"] = { Range=8, Blindspot=0, Height=5.5, Type="Short", Radar="BV410_RBS70" }, + ["RBS90 SMA"] = { Range=8, Blindspot=0, Height=5.5, Type="Short", Radar="RBS-90" }, + ["RBS90M SMA"] = { Range=8, Blindspot=0, Height=5.5, Type="Short", Radar="BV410_RBS90" }, + ["RBS103A SMA"] = { Range=150, Blindspot=3, Height=24.5, Type="Long", Radar="LvS-103_Lavett103_Rb103A" }, + ["RBS103B SMA"] = { Range=35, Blindspot=0, Height=36, Type="Medium", Radar="LvS-103_Lavett103_Rb103B" }, + ["RBS103AM SMA"] = { Range=150, Blindspot=3, Height=24.5, Type="Long", Radar="LvS-103_Lavett103_HX_Rb103A" }, + ["RBS103BM SMA"] = { Range=35, Blindspot=0, Height=36, Type="Medium", Radar="LvS-103_Lavett103_HX_Rb103B" }, + ["Lvkv9040M SMA"] = { Range=4, Blindspot=0, Height=2.5, Type="Short", Radar="LvKv9040" }, +} + ----------------------------------------------------------------------- -- MANTIS System ----------------------------------------------------------------------- @@ -92599,56 +100806,58 @@ do --@param #string samprefix Prefixes for the SAM groups from the ME, e.g. all groups starting with "Red Sam..." --@param #string ewrprefix Prefixes for the EWR groups from the ME, e.g. all groups starting with "Red EWR..." --@param #string hq Group name of your HQ (optional) - --@param #string coaltion Coalition side of your setup, e.g. "blue", "red" or "neutral" + --@param #string coalition Coalition side of your setup, e.g. "blue", "red" or "neutral" --@param #boolean dynamic Use constant (true) filtering or just filter once (false, default) (optional) --@param #string awacs Group name of your Awacs (optional) - --@param #boolean EmOnOff Make MANTIS switch Emissions on and off instead of changing the alarm state between RED and GREEN + --@param #boolean EmOnOff Make MANTIS switch Emissions on and off instead of changing the alarm state between RED and GREEN (optional) + --@param #number Padding For #SEAD - Extra number of seconds to add to radar switch-back-on time (optional) --@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()` - -- - -- [optional] Use - -- - -- * `MANTIS:SetEWRGrouping(radius)` - -- * `MANTIS:SetEWRRange(radius)` - -- * `MANTIS:SetSAMRadius(radius)` - -- * `MANTIS:SetDetectInterval(interval)` - -- * `MANTIS:SetAutoRelocate(hq, ewr)` - -- + -- myredmantis = MANTIS:New("myredmantis","Red SAM","Red EWR",nil,"red",false) + -- myredmantis:Start() + -- + -- [optional] Use + -- + -- 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()` - -- - function MANTIS:New(name,samprefix,ewrprefix,hq,coaltion,dynamic,awacs, EmOnOff) - + -- + -- mybluemantis = MANTIS:New("bluemantis","Blue SAM","Blue EWR",nil,"blue",false,"Blue Awacs") + -- mybluemantis:Start() + -- + function MANTIS:New(name,samprefix,ewrprefix,hq,coalition,dynamic,awacs, EmOnOff, Padding) + -- DONE: Create some user functions for these -- DONE: Make HQ useful -- 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.Coalition = coalition 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 self.adv_ratio = 100 - self.adv_state = 0 + self.adv_state = 0 self.verbose = false self.Adv_EWR_Group = nil self.AWACS_Prefix = awacs or nil @@ -92656,32 +100865,49 @@ 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 + 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 EmOnOff then - if EmOnOff == false then - self.UseEmOnOff = false - else - self.UseEmOnOff = true - end - end - if type(awacs) == "string" then self.advAwacs = true else self.advAwacs = false end - + -- Inherit everything from BASE class. local self = BASE:Inherit(self, FSM:New()) -- #MANTIS - + -- Set the string id for output to DCS.log file. self.lid=string.format("MANTIS %s | ", self.name) - + -- Debug trace. if self.debug then BASE:TraceOnOff(true) @@ -92690,47 +100916,74 @@ do 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.5.2" + self.version="0.8.9" self:I(string.format("***** Starting MANTIS Version %s *****", self.version)) - + --- FSM Functions --- - + -- Start State. 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 --- ------------------------ - + --- Triggers the FSM event "Start". Starts the MANTIS. Initializes parameters and starts event handlers. -- @function [parent=#MANTIS] Start -- @param #MANTIS self @@ -92756,7 +101009,7 @@ do -- @function [parent=#MANTIS] __Status -- @param #MANTIS self -- @param #number delay Delay in seconds. - + --- On After "Relocating" event. HQ and/or EWR moved. -- @function [parent=#MANTIS] OnAfterRelocating -- @param #MANTIS self @@ -92764,7 +101017,7 @@ do -- @param #string Event The Event -- @param #string To The To State -- @return #MANTIS self - + --- On After "GreenState" event. A SAM group was switched to GREEN alert. -- @function [parent=#MANTIS] OnAfterGreenState -- @param #MANTIS self @@ -92773,7 +101026,7 @@ do -- @param #string To The To State -- @param Wrapper.Group#GROUP Group The GROUP object whose state was changed -- @return #MANTIS self - + --- On After "RedState" event. A SAM group was switched to RED alert. -- @function [parent=#MANTIS] OnAfterRedState -- @param #MANTIS self @@ -92782,7 +101035,7 @@ do -- @param #string To The To State -- @param Wrapper.Group#GROUP Group The GROUP object whose state was changed -- @return #MANTIS self - + --- On After "AdvStateChange" event. Advanced state changed, influencing detection speed. -- @function [parent=#MANTIS] OnAfterAdvStateChange -- @param #MANTIS self @@ -92793,7 +101046,7 @@ do -- @param #number Newstate New state - 0 = green, 1 = amber, 2 = red -- @param #number Interval Calculated detection interval based on state and advanced feature setting -- @return #MANTIS self - + --- On After "ShoradActivated" event. Mantis has activated a SHORAD. -- @function [parent=#MANTIS] OnAfterShoradActivated -- @param #MANTIS self @@ -92804,21 +101057,52 @@ do -- @param #number Radius Radius around the named group to find SHORAD groups -- @param #number Ontime Seconds the SHORAD will stay active - return self + --- 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 ----------------------------------------------------------------------- -- MANTIS helper functions ------------------------------------------------------------------------ - +----------------------------------------------------------------------- + --- [Internal] Function to get the self.SAM_Table -- @param #MANTIS self - -- @return #table table + -- @return #table table function MANTIS:_GetSAMTable() self:T(self.lid .. "GetSAMTable") return self.SAM_Table end - + --- [Internal] Function to set the self.SAM_Table -- @param #MANTIS self -- @return #MANTIS self @@ -92827,7 +101111,7 @@ do self.SAM_Table = table return self end - + --- Function to set the grouping radius of the detection in meters -- @param #MANTIS self -- @param #number radius Radius upon which detected objects will be grouped @@ -92837,55 +101121,93 @@ 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 self.checkradius = radius 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() self.mysead.EngagementRange = range return self end - + --- Function to set switch-on/off the debug state -- @param #MANTIS self -- @param #boolean onoff Set true to switch on @@ -92903,7 +101225,7 @@ do end return self end - + --- Function to get the HQ object for further use -- @param #MANTIS self -- @return Wrapper.GROUP#GROUP The HQ #GROUP object or *nil* if it doesn't exist @@ -92912,10 +101234,10 @@ do if self.HQ_CC then return self.HQ_CC else - return nil - end + return nil + end end - + --- Function to set separate AWACS detection instance -- @param #MANTIS self -- @param #string prefix Name of the AWACS group in the mission editor @@ -92939,7 +101261,7 @@ do self.awacsrange = range return self end - + --- Function to set the HQ object for further use -- @param #MANTIS self -- @param Wrapper.GROUP#GROUP group The #GROUP object to be set as HQ @@ -92957,7 +101279,7 @@ do end return self end - + --- Function to set the detection interval -- @param #MANTIS self -- @param #number interval The interval in seconds @@ -92966,8 +101288,8 @@ do local interval = interval or 30 self.detectinterval = interval return self - end - + end + --- Function to set Advanded Mode -- @param #MANTIS self -- @param #boolean onoff If true, will activate Advanced Mode @@ -92976,7 +101298,7 @@ do -- E.g. `mymantis:SetAdvancedMode(true, 90)` function MANTIS:SetAdvancedMode(onoff, ratio) self:T(self.lid .. "SetAdvancedMode") - self:T({onoff, ratio}) + --self:T({onoff, ratio}) local onoff = onoff or false local ratio = ratio or 100 if (type(self.HQ_Template_CC) == "string") and onoff and self.dynamic then @@ -92992,7 +101314,7 @@ do end return self end - + --- Set using Emissions on/off instead of changing alarm state -- @param #MANTIS self -- @param #boolean switch Decide if we are changing alarm state or Emission state @@ -93001,7 +101323,18 @@ do self.UseEmOnOff = switch or false return self end - + + --- 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) + self:T(self.lid .. "SetUsingDLink") + self.DLink = true + self.Detection = DLink + self.DLTimeStamp = timer.getAbsTime() + return self + end + --- [Internal] Function to check if HQ is alive -- @param #MANTIS self -- @return #boolean True if HQ is alive, else false @@ -93016,15 +101349,15 @@ do local hqgrp = GROUP:FindByName(hq) if hqgrp then if hqgrp:IsAlive() then -- ok we're on, hq exists and as alive - self:T(self.lid.." HQ is alive!") + --self:T(self.lid.." HQ is alive!") return true else - self:T(self.lid.." HQ is dead!") - return false + --self:T(self.lid.." HQ is dead!") + return false end end end - return self + return self end --- [Internal] Function to check if EWR is (at least partially) alive @@ -93033,7 +101366,7 @@ do function MANTIS:_CheckEWRState() self:T(self.lid .. "CheckEWRState") local text = self.lid.." Checking EWR State" - self:T(text) + --self:T(text) local m= MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) if self.verbose then self:I(text) end -- start check @@ -93049,14 +101382,14 @@ do end end end - self:T(self.lid..string.format(" No of EWR alive is %d", nalive)) + --self:T(self.lid..string.format(" No of EWR alive is %d", nalive)) if nalive > 0 then return true else return false end end - return self + return self end --- [Internal] Function to determine state of the advanced mode @@ -93065,10 +101398,8 @@ do -- @return #number Previous state for tracking 0, 1, or 2 function MANTIS:_CalcAdvState() self:T(self.lid .. "CalcAdvState") - local text = self.lid.." Calculating Advanced State" - self:T(text) - local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) - if self.verbose then self:I(text) end + local m=MESSAGE:New(self.lid.." Calculating Advanced State",10,"MANTIS"):ToAllIf(self.debug) + if self.verbose then self:I(self.lid.." Calculating Advanced State") end -- start check local currstate = self.adv_state -- save curr state for comparison later local EWR_State = self:_CheckEWRState() @@ -93086,32 +101417,35 @@ do local ratio = self.adv_ratio / 100 -- e.g. 80/100 = 0.8 ratio = ratio * self.adv_state -- e.g 0.8*2 = 1.6 local newinterval = interval + (interval * ratio) -- e.g. 30+(30*1.6) = 78 - local text = self.lid..string.format(" Calculated OldState/NewState/Interval: %d / %d / %d", currstate, self.adv_state, newinterval) - self:T(text) - local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) - if self.verbose then self:I(text) end + if self.debug or self.verbose then + local text = self.lid..string.format(" Calculated OldState/NewState/Interval: %d / %d / %d", currstate, self.adv_state, newinterval) + --self:T(text) + local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) + if self.verbose then self:I(text) end + end return newinterval, currstate end - + --- Function to set autorelocation for HQ and EWR objects. Note: Units must be actually mobile in DCS! -- @param #MANTIS self -- @param #boolean hq If true, will relocate HQ object -- @param #boolean ewr If true, will relocate EWR objects function MANTIS:SetAutoRelocate(hq, ewr) self:T(self.lid .. "SetAutoRelocate") - self:T({hq, ewr}) + --self:T({hq, ewr}) local hqrel = hq or false local ewrel = ewr or false if hqrel or ewrel then self.autorelocate = true self.autorelocateunits = { HQ = hqrel, EWR = ewrel } - self:T({self.autorelocate, self.autorelocateunits}) + --self:T({self.autorelocate, self.autorelocateunits}) end return self - end - + end + --- [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" @@ -93122,7 +101456,7 @@ do local HQGroup = self.HQ_CC if self.autorelocateunits.HQ and self.HQ_CC and HQGroup:IsAlive() then --only relocate if HQ exists local _hqgrp = self.HQ_CC - self:T(self.lid.." Relocating HQ") + --self:T(self.lid.." Relocating HQ") local text = self.lid.." Relocating HQ" --local m= MESSAGE:New(text,10,"MANTIS"):ToAll() _hqgrp:RelocateGroundRandomInRadius(20,500,true,true) @@ -93135,7 +101469,7 @@ do local EWR_Grps = EWR_GRP.Set --table of objects in SET_GROUP for _,_grp in pairs (EWR_Grps) do if _grp:IsAlive() and _grp:IsGround() then - self:T(self.lid.." Relocating EWR ".._grp:GetName()) + --self:T(self.lid.." Relocating EWR ".._grp:GetName()) local text = self.lid.." Relocating EWR ".._grp:GetName() local m= MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) if self.verbose then self:I(text) end @@ -93146,137 +101480,368 @@ 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 dectstring = coord:ToStringLLDMS() - local samstring = samcoordinate:ToStringLLDMS() local targetdistance = samcoordinate:DistanceFromPointVec2(coord) - local text = string.format("Checking SAM at % s - Distance %d m - Target %s", samstring, targetdistance, dectstring) - local m = MESSAGE:New(text,10,"Check"):ToAllIf(self.debug) - if self.verbose then self:I(self.lid..text) end + 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 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: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() self:T(self.lid.."Starting Detection") - + -- 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 + --local acceptrange = self.acceptrange or 80000 + local interval = self.detectinterval or 20 - --@param Functional.Detection #DETECTION_AREAS _MANTISdetection [Internal] The MANTIS detection object 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() - - 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 + MANTISdetection:__Start(2) + 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() self:T(self.lid.."Starting Awacs Detection") - + -- start detection local group = self.AWACS_Prefix local groupset = SET_GROUP:New():FilterPrefixes(group):FilterCoalitions(self.Coalition):FilterStart() 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 MANTISAwacs = DETECTION_AREAS:New( groupset, grouping ) --[Internal] Grouping detected objects to 5000m zones MANTISAwacs:FilterCategories({ Unit.Category.AIRPLANE, Unit.Category.HELICOPTER }) MANTISAwacs:SetAcceptRange(self.awacsrange) --250km 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 + -- @param #boolean sma SMA 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,sma) + 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 + elseif sma then + SAMData = self.SamDataSMA + 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 + local SMAMod = false + if string.find(grpname,"HDS",1,true) then + HDSmod = true + elseif string.find(grpname,"SMA",1,true) then + SMAMod = 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 or SMAMod then + range, height, type = self:_GetSAMDataFromUnits(grpname,HDSmod,SMAMod) + 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 ) + 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 - + --- [Internal] Function to update SAM table and SEAD state -- @param #MANTIS self -- @return #MANTIS self @@ -93287,20 +101852,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 @@ -93308,7 +101894,7 @@ do end return self end - + --- Function to link up #MANTIS with a #SHORAD installation -- @param #MANTIS self -- @param Functional.Shorad#SHORAD Shorad The #SHORAD object @@ -93325,7 +101911,7 @@ do end return self end - + --- Function to unlink #MANTIS from a #SHORAD installation -- @param #MANTIS self function MANTIS:RemoveShorad() @@ -93333,48 +101919,52 @@ do self.ShoradLink = false return self end - + ----------------------------------------------------------------------- -- 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 #number 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 @@ -93382,30 +101972,65 @@ do self:__ShoradActivated(1,name, radius, ontime) end -- debug output - local text = string.format("SAM %s switched to alarm state RED!", name) - local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) - if self.verbose then self:I(self.lid..text) end + 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 + else + 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 - local text = string.format("SAM %s switched to 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 + 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 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 end --end alive end --end check end --for for loop return self - end + 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 -- @return #MANTIS self @@ -93414,7 +102039,7 @@ do self:_RelocateGroups() return self end - + --- [Internal] Check advanced state -- @param #MANTIS self -- @return #MANTIS self @@ -93434,11 +102059,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 @@ -93449,7 +102075,21 @@ do end -- end newstate vs oldstate return self end - + + --- [Internal] Check DLink state + -- @param #MANTIS self + -- @return #MANTIS self + function MANTIS:_CheckDLinkState() + self:T(self.lid .. "_CheckDLinkState") + local dlink = self.Detection -- Ops.Intelligence#INTEL_DLINK + local TS = timer.getAbsTime() + if not dlink:Is("Running") and (TS - self.DLTimeStamp > 29) then + self.DLink = false + self.Detection = self:StartDetection() -- fall back + self:I(self.lid .. "Intel DLink not running - switching back to single detection!") + end + end + --- [Internal] Function to set start state -- @param #MANTIS self -- @param #string From The From State @@ -93460,14 +102100,26 @@ do self:T({From, Event, To}) self:T(self.lid.."Starting MANTIS") self:SetSAMStartState() - self.Detection = self:StartDetection() - if self.advAwacs then + if not INTEL then + self.Detection = self:StartDetection() + else + self.Detection = self:StartIntelDetection() + end + --[[ + if self.advAwacs and not self.automode then self.AWACS_Detection = self:StartAwacsDetection() end - self:__Status(self.detectinterval) + --]] + 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 - + --- [Internal] Before status function for MANTIS -- @param #MANTIS self -- @param #string From The From State @@ -93478,13 +102130,14 @@ 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 @@ -93493,21 +102146,26 @@ do local timepassed = thistime - self.TimeStamp local halfintv = math.floor(timepassed / relointerval) - + --self:T({timepassed=timepassed, halfintv=halfintv}) - + if halfintv >= 1 then self.TimeStamp = timer.getAbsTime() self:_Relocate() self:__Relocating(1) end end - - -- timer for advanced state check + + -- advanced state check if self.advanced then self:_CheckAdvState() end - + + -- check DLink state + if self.DLink then + self:_CheckDLinkState() + end + return self end @@ -93519,11 +102177,18 @@ do -- @return #MANTIS self function MANTIS:onafterStatus(From,Event,To) self:T({From, Event, To}) + -- Display some states + 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)) + end + end local interval = self.detectinterval * -1 self:__Status(interval) return self end - + --- [Internal] Function to stop MANTIS -- @param #MANTIS self -- @param #string From The From State @@ -93532,9 +102197,9 @@ do -- @return #MANTIS self function MANTIS:onafterStop(From, Event, To) self:T({From, Event, To}) - return self + return self end - + --- [Internal] Function triggered by Event Relocating -- @param #MANTIS self -- @param #string From The From State @@ -93543,9 +102208,9 @@ do -- @return #MANTIS self function MANTIS:onafterRelocating(From, Event, To) self:T({From, Event, To}) - return self + return self end - + --- [Internal] Function triggered by Event GreenState -- @param #MANTIS self -- @param #string From The From State @@ -93554,10 +102219,10 @@ 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}) - return self + self:T({From, Event, To, Group:GetName()}) + return self end - + --- [Internal] Function triggered by Event RedState -- @param #MANTIS self -- @param #string From The From State @@ -93566,10 +102231,10 @@ 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}) - return self + self:T({From, Event, To, Group:GetName()}) + return self end - + --- [Internal] Function triggered by Event AdvStateChange -- @param #MANTIS self -- @param #string From The From State @@ -93581,9 +102246,9 @@ do -- @return #MANTIS self function MANTIS:onafterAdvStateChange(From, Event, To, Oldstate, Newstate, Interval) self:T({From, Event, To, Oldstate, Newstate, Interval}) - return self + return self end - + --- [Internal] Function triggered by Event ShoradActivated -- @param #MANTIS self -- @param #string From The From State @@ -93594,18 +102259,70 @@ do -- @param #number Ontime Seconds the SHORAD will stay active function MANTIS:onafterShoradActivated(From, Event, To, Name, Radius, Ontime) self:T({From, Event, To, Name, Radius, Ontime}) - return self + 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 ----------------------------------------------------------------------- ---- **Functional** -- Short Range Air Defense System +--- **Functional** - Short Range Air Defense System. -- -- === +-- +-- ## Features: -- --- **SHORAD** - Short Range Air Defense System --- Controls a network of short range air/missile defense groups. +-- * Short Range Air Defense System +-- * Controls a network of short range air/missile defense groups. -- -- === -- @@ -93620,7 +102337,7 @@ end -- @module Functional.Shorad -- @image Functional.Shorad.jpg -- --- Date: July 2021 +-- Date: Nov 2021 ------------------------------------------------------------------------- --- **SHORAD** class, extends Core.Base#BASE @@ -93643,6 +102360,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 @@ -93715,7 +102433,7 @@ do ["AGM_122"] = "AGM_122", ["AGM_84"] = "AGM_84", ["AGM_45"] = "AGM_45", - ["ALARN"] = "ALARM", + ["ALARM"] = "ALARM", ["LD-10"] = "LD-10", ["X_58"] = "X_58", ["X_28"] = "X_28", @@ -93733,6 +102451,7 @@ do ["Kh29"] = "Kh29", ["Kh31"] = "Kh31", ["Kh66"] = "Kh66", + --["BGM_109"] = "BGM_109", } --- Instantiates a new SHORAD object @@ -93740,13 +102459,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() @@ -93764,11 +102483,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 @@ -93912,7 +102637,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 @@ -93928,7 +102653,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 @@ -93969,7 +102694,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 @@ -93990,7 +102715,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 @@ -94002,6 +102727,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 @@ -94025,7 +102751,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 @@ -94079,6 +102805,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 @@ -94106,13 +102902,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 @@ -94121,7 +102952,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 @@ -94142,6 +102977,7 @@ do end end end + return self end -- end @@ -94176,19 +103012,26 @@ 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 Tarawa](https://en.wikipedia.org/wiki/USS_Tarawa_(LHA-1)) (LHA-1) [**WIP**] +-- * [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) +-- * [HMS Invincible](https://en.wikipedia.org/wiki/HMS_Invincible_(R05\)) (R05) +-- * [USS Tarawa](https://en.wikipedia.org/wiki/USS_Tarawa_(LHA-1\)) (LHA-1) +-- * [USS America](https://en.wikipedia.org/wiki/USS_America_(LHA-6\)) (LHA-6) +-- * [Juan Carlos I](https://en.wikipedia.org/wiki/Spanish_amphibious_assault_ship_Juan_Carlos_I) (L61) +-- * [HMAS Canberra](https://en.wikipedia.org/wiki/HMAS_Canberra_(L02\)) (L02) -- -- **Supported Aircraft:** -- -- * [F/A-18C Hornet Lot 20](https://forums.eagle.ru/forumdisplay.php?f=557) (Player & AI) -- * [F-14A/B Tomcat](https://forums.eagle.ru/forumdisplay.php?f=395) (Player & AI) -- * [A-4E Skyhawk Community Mod](https://forums.eagle.ru/showthread.php?t=224989) (Player & AI) --- * [AV-8B N/A Harrier](https://forums.eagle.ru/forumdisplay.php?f=555) (Player & AI) [**WIP**] --- * [T-45C Goshawk](https://www.vnao-cvw-7.com/t-45-goshawk) (VNAO)(Player & AI) [**WIP**] +-- * [AV-8B N/A Harrier](https://forums.eagle.ru/forumdisplay.php?f=555) (Player & AI) +-- * [T-45C Goshawk](https://www.vnao-cvw-7.com/t-45-goshawk) (VNAO mod) (Player & AI) +-- * [FE/A-18E/F/G Superhornet](https://forum.dcs.world/topic/316971-cjs-super-hornet-community-mod-v20-official-thread/) (CJS mod) (Player & AI) -- * F/A-18C Hornet (AI) -- * F-14A Tomcat (AI) -- * E-2D Hawkeye (AI) @@ -94197,9 +103040,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 and the USS Tarawa are WIP. Those two can only be used together, i.e. the Tarawa is the only carrier the harrier is supposed to land on and --- the no other fixed wing aircraft (human or AI controlled) are supposed to land on the Tarawa. Currently only Case I is supported. Case II/III take slightly steps from the CVN carrier. --- However, the two Case II/III pattern are very similar so this is not a big drawback. +-- The AV-8B Harrier, HMS Hermes, HMS Invincible, 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, 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. -- @@ -94254,13 +103097,17 @@ 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=bTgJXZ9Mhdc&t=1s) +-- * [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. @@ -94282,7 +103129,7 @@ end -- @field Wrapper.Airbase#AIRBASE airbase Carrier airbase object. -- @field #table waypoints Waypoint coordinates of carrier. -- @field #number currentwp Current waypoint, i.e. the one that has been passed last. --- @field Core.Radio#BEACON beacon Carrier beacon for TACAN and ICLS. +-- @field Core.Beacon#BEACON beacon Carrier beacon for TACAN and ICLS. -- @field #boolean TACANon Automatic TACAN is activated. -- @field #number TACANchannel TACAN channel. -- @field #string TACANmode TACAN mode, i.e. "X" or "Y". @@ -94436,7 +103283,7 @@ end -- -- The flight that transitions form the holding pattern to the landing approach, it should leave the Marshal stack at the 3 position and make a left hand turn to the *Initial* -- position, which is 3 NM astern of the boat. Note that you need to be below 1300 feet to be registered in the initial zone. --- The altitude can be set via the function @{AIRBOSS.SetInitialMaxAlt}(*altitude*) function. +-- The altitude can be set via the function @{#AIRBOSS.SetInitialMaxAlt}(*altitude*) function. -- As described below, the initial zone can be smoked or flared via the AIRBOSS F10 Help radio menu. -- -- ### Landing Pattern @@ -94444,7 +103291,8 @@ end -- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case1_Landing.png) -- -- 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. When you stabalise over the landing spot LSO will call Stabalise to indicate you are centered at the correct spot. -- -- ## CASE III -- @@ -94777,12 +103625,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 -- @@ -94900,7 +103748,7 @@ end -- -- ## Save Results -- --- Saving asset data to file is achieved by the @{AIRBOSS.Save}(*path*, *filename*) function. +-- Saving asset data to file is achieved by the @{#AIRBOSS.Save}(*path*, *filename*) function. -- -- The parameter *path* specifies the path on the file system where the -- player grades are saved. If you do not specify a path, the file is saved your the DCS installation root directory if the **lfs** module is *not* desanizied or @@ -94921,7 +103769,7 @@ end -- -- ### Automatic Saving -- --- The player grades can be saved automatically after each graded player pass via the @{AIRBOSS.SetAutoSave}(*path*, *filename*) function. Again the parameters *path* and *filename* are optional. +-- The player grades can be saved automatically after each graded player pass via the @{#AIRBOSS.SetAutoSave}(*path*, *filename*) function. Again the parameters *path* and *filename* are optional. -- In the simplest case, you desanitize the **lfs** module and just add -- -- airbossStennis:SetAutoSave() @@ -94959,7 +103807,7 @@ end -- -- ## Load Results -- --- Loading player grades from file is achieved by the @{AIRBOSS.Load}(*path*, *filename*) function. The parameter *path* specifies the path on the file system where the +-- Loading player grades from file is achieved by the @{#AIRBOSS.Load}(*path*, *filename*) function. The parameter *path* specifies the path on the file system where the -- data is loaded from. If you do not specify a path, the file is loaded from your the DCS installation root directory or, if **lfs** was desanitized from you "Saved Games\DCS" directory. -- The parameter *filename* is optional and defines the name of the file to load. By default this is automatically generated from the AIBOSS carrier name/alias, for example -- "Airboss-USS Stennis_LSOgrades.csv". @@ -95007,7 +103855,7 @@ end -- * *Points*: Current points for the pass. -- * *Details*: Detailed grading analysis. -- ---## Lineup Error +-- ## Lineup Error -- -- ![Banner Image](..\Presentations\AIRBOSS\Airboss_TrapSheetLUE.png) -- @@ -95068,9 +103916,9 @@ end -- -- ## Sound Packs -- --- The AIRBOSS currently has two different "sound packs" for both LSO and Marshal radios. These contain voice overs by different actors. +-- The AIRBOSS currently has two different "sound packs" for LSO and three different "sound Packs" for Marshal radios. These contain voice overs by different actors. -- These can be set by @{#AIRBOSS.SetVoiceOversLSOByRaynor}() and @{#AIRBOSS.SetVoiceOversMarshalByRaynor}(). These are the default settings. --- The other sound files can be set by @{#AIRBOSS.SetVoiceOversLSOByFF}() and @{#AIRBOSS.SetVoiceOversMarshalByFF}(). +-- The other sound files can be set by @{#AIRBOSS.SetVoiceOversLSOByFF}(), @{#AIRBOSS.SetVoiceOversMarshalByGabriella}() and @{#AIRBOSS.SetVoiceOversMarshalByFF}(). -- Also combinations can be used, e.g. -- -- airbossStennis:SetVoiceOversLSOByFF() @@ -95100,8 +103948,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. @@ -95181,7 +104027,7 @@ end -- -- AI groups that enter the CCA are usually guided to Marshal stack. However, due to DCS limitations they might not obey the landing task if they have another airfield as departure and/or destination in -- their mission task. Therefore, AI groups can be respawned when detected in the CCA. This should clear all other airfields and allow the aircraft to land on the carrier. --- This is achieved by the @{AIRBOSS.SetRespawnAI}() function. +-- This is achieved by the @{#AIRBOSS.SetRespawnAI}() function. -- -- ## Known Issues -- @@ -95343,6 +104189,8 @@ AIRBOSS = { NmaxSection = nil, NmaxStack = nil, handleai = nil, + xtVoiceOvers = nil, + xtVoiceOversAI = nil, tanker = nil, Corientation = nil, Corientlast = nil, @@ -95405,7 +104253,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. +-- @field #string AV8B AV-8B Night Harrier. Works only with the HMS Hermes, HMS Invincible, 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. @@ -95416,7 +104264,10 @@ AIRBOSS = { -- @field #string S3BTANKER Lockheed S-3B Viking tanker. -- @field #string E2D Grumman E-2D Hawkeye AWACS. -- @field #string C2A Grumman C-2A Greyhound from Military Aircraft Mod. --- @field #string T45C T-45C by VNAO +-- @field #string T45C T-45C by VNAO. +-- @field #string RHINOE F/A-18E Superhornet (mod). +-- @field #string RHINOF F/A-18F Superhornet (mod). +-- @field #string GROWLER FEA-18G Superhornet (mod). AIRBOSS.AircraftCarrier={ AV8B="AV8BNA", HORNET="FA-18C_hornet", @@ -95430,6 +104281,9 @@ AIRBOSS.AircraftCarrier={ S3BTANKER="S-3B Tanker", E2D="E-2C", C2A="C2A_Greyhound", + RHINOE="FA-18E", + RHINOF="FA-18F", + GROWLER="EA-18G", } --- Carrier types. @@ -95439,18 +104293,30 @@ AIRBOSS.AircraftCarrier={ -- @field #string WASHINGTON USS George Washington (CVN-73) [Super Carrier Module] -- @field #string STENNIS USS John C. Stennis (CVN-74) -- @field #string TRUMAN USS Harry S. Truman (CVN-75) [Super Carrier Module] --- @field #string VINSON USS Carl Vinson (CVN-70) [Obsolete] --- @field #string TARAWA USS Tarawa (LHA-1) +-- @field #string FORRESTAL USS Forrestal (CV-59) [Heatblur Carrier Module] +-- @field #string VINSON USS Carl Vinson (CVN-70) [Deprecated!] +-- @field #string HERMES HMS Hermes (R12) [V/STOL Carrier] +-- @field #string INVINCIBLE HMS Invincible (R05) [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", - VINSON="VINSON", - TARAWA="LHA_Tarawa", - KUZNETSOV="KUZNECOW", +AIRBOSS.CarrierType = { + ROOSEVELT = "CVN_71", + LINCOLN = "CVN_72", + WASHINGTON = "CVN_73", + TRUMAN = "CVN_75", + STENNIS = "Stennis", + FORRESTAL = "Forrestal", + VINSON = "VINSON", + HERMES = "HERMES81", + INVINCIBLE = "hms_invincible", + TARAWA = "LHA_Tarawa", + AMERICA = "USS America LHA-6", + JCARLOS = "L61", + CANBERRA = "L02", + KUZNETSOV = "KUZNECOW", } --- Carrier specific parameters. @@ -95462,6 +104328,7 @@ AIRBOSS.CarrierType={ -- @field #number wire2 Distance in meters from carrier position to second wire. -- @field #number wire3 Distance in meters from carrier position to third wire. -- @field #number wire4 Distance in meters from carrier position to fourth wire. +-- @field #number landingdist Distance in meeters to the landing position. -- @field #number rwylength Length of the landing runway in meters. -- @field #number rwywidth Width of the landing runway in meters. -- @field #number totlength Total length of carrier. @@ -95498,7 +104365,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". @@ -95530,36 +104396,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. @@ -95569,18 +104435,18 @@ AIRBOSS.PatternStep={ -- @field #string IM "IM": In the middle. -- @field #string IC "IC": In close. -- @field #string AR "AR": At the ramp. --- @field #string AL "AL": Abeam landing position (Tarawa). --- @field #string LC "LC": Level crossing (Tarawa). +-- @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. @@ -95635,6 +104501,7 @@ AIRBOSS.GroovePos={ -- @field #AIRBOSS.RadioCall DEPARTANDREENTER "Depart and re-enter" call. -- @field #AIRBOSS.RadioCall EXPECTHEAVYWAVEOFF "Expect heavy wavoff" call. -- @field #AIRBOSS.RadioCall EXPECTSPOT75 "Expect spot 7.5" call. +-- @field #AIRBOSS.RadioCall EXPECTSPOT5 "Expect spot 5" call. -- @field #AIRBOSS.RadioCall FAST "You're fast" call. -- @field #AIRBOSS.RadioCall FOULDECK "Foul Deck" call. -- @field #AIRBOSS.RadioCall HIGH "You're high" call. @@ -95708,16 +104575,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. @@ -95854,16 +104720,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.1.6" - +AIRBOSS.version = "1.3.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -95936,56 +104801,56 @@ AIRBOSS.version="1.1.6" -- @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() @@ -96004,7 +104869,7 @@ function AIRBOSS:New(carriername, alias) self:SetLSOCallInterval() -- Radio scheduler. - self.radiotimer=SCHEDULER:New() + self.radiotimer = SCHEDULER:New() -- Set magnetic declination. self:SetMagneticDeclination() @@ -96033,6 +104898,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() @@ -96040,10 +104911,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() @@ -96064,7 +104935,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() @@ -96079,7 +104950,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() @@ -96092,38 +104963,56 @@ 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 + -- Stennis parameters were updated to match the other Super Carriers. 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.VINSON then - -- TODO: Carl Vinson parameters. + 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 + -- Carl Vinson is legacy now. 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.INVINCIBLE then + -- Invincible parameters. + self:_InitInvincible() + elseif self.carriertype == AIRBOSS.CarrierType.TARAWA then -- Tarawa parameters. self:_InitTarawa() - elseif self.carriertype==AIRBOSS.CarrierType.KUZNETSOV then + elseif self.carriertype == AIRBOSS.CarrierType.AMERICA then + -- Use America parameters. + self:_InitAmerica() + elseif self.carriertype == AIRBOSS.CarrierType.JCARLOS then + -- Use Juan Carlos parameters. + self:_InitJcarlos() + elseif self.carriertype == AIRBOSS.CarrierType.CANBERRA then + -- Use Juan Carlos parameters at this stage. + self:_InitCanberra() + 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 @@ -96136,48 +105025,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) + 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() @@ -96192,31 +105081,30 @@ function AIRBOSS:New(carriername, alias) bow:FlareYellow() -- Runway half width = 10 m. - local r1=stern:Translate(self.carrierparam.rwywidth*0.5, FB+90) - local r2=stern:Translate(self.carrierparam.rwywidth*0.5, FB-90) - 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) - 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) - cL:FlareYellow() - + local cL = stern:Translate( self.carrierparam.totwidthport, hdg - 90, true ) + -- cL:FlareYellow() -- Carrier specific. - if self.carrier:GetTypeName()~=AIRBOSS.CarrierType.TARAWA then + if self.carrier:GetTypeName() ~= AIRBOSS.CarrierType.INVINCIBLE or 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) - local w2=stern:Translate(self.carrierparam.wire2, FB) - local w3=stern:Translate(self.carrierparam.wire3, FB) - local w4=stern:Translate(self.carrierparam.wire4, FB) + 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() @@ -96225,27 +105113,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 ----------------------- @@ -96253,7 +105141,7 @@ function AIRBOSS:New(carriername, alias) ----------------------- -- Start State. - self:SetStartState("Stopped") + self:SetStartState( "Stopped" ) -- Add FSM transitions. -- From State --> Event --> To State @@ -96289,7 +105177,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 @@ -96299,7 +105186,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 @@ -96322,7 +105208,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 @@ -96339,7 +105224,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 @@ -96360,7 +105244,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 @@ -96374,7 +105257,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 @@ -96395,7 +105277,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 @@ -96418,7 +105299,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 @@ -96441,7 +105321,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 @@ -96464,7 +105343,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 @@ -96484,7 +105362,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 @@ -96503,26 +105380,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 @@ -96530,37 +105406,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 @@ -96569,37 +105445,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 @@ -96615,136 +105491,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 - - if uturn==nil or uturn==true then - recovery.UTURN=true + 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 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 @@ -96754,11 +105629,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 @@ -96768,25 +105643,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 @@ -96796,10 +105671,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 @@ -96807,145 +105682,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. +--- Set max altitude 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 altitude 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 @@ -96955,13 +105827,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.INVINCIBLE or 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 @@ -96976,118 +105864,135 @@ 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.INVINCIBLE or 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 self.lue.LeftMed=LeftMed or -2.0 self.lue.LEFT=LEFT or -3.0 self.lue.Right=Right or 1.0 - self.lue.RightMed=RightMed or 2.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 @@ -97096,79 +106001,77 @@ 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 @@ -97177,8 +106080,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 @@ -97186,8 +106089,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 @@ -97195,17 +106098,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 @@ -97213,34 +106115,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 @@ -97249,34 +106150,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 @@ -97285,11 +106185,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 @@ -97298,9 +106198,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 @@ -97308,11 +106208,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 @@ -97320,38 +106220,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 @@ -97359,8 +106275,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 @@ -97372,23 +106288,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 @@ -97399,10 +106315,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 @@ -97410,7 +106326,7 @@ end -- @param #AIRBOSS self -- @return #AIRBOSS self function AIRBOSS:SetDebugModeON() - self.Debug=true + self.Debug = true return self end @@ -97418,11 +106334,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 @@ -97431,8 +106347,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 @@ -97440,7 +106356,21 @@ end -- @param #AIRBOSS self -- @return #AIRBOSS self function AIRBOSS:SetDebugModeOFF() - self.Debug=false + self.Debug = false + return self +end + + +--- Set FunkMan socket. LSO grades and trap sheets will be send to your Discord bot. +-- **Requires running FunkMan program**. +-- @param #AIRBOSS self +-- @param #number Port Port. Default `10042`. +-- @param #string Host Host. Default `"127.0.0.1"`. +-- @return #AIRBOSS self +function AIRBOSS:SetFunkManOn(Port, Host) + + self.funkmanSocket=SOCKET:New(Port, Host) + return self end @@ -97449,12 +106379,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 @@ -97469,42 +106399,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 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -97516,63 +106446,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. @@ -97580,65 +106510,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 @@ -97647,24 +106576,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() @@ -97679,16 +106607,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 @@ -97709,27 +106637,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 ----------------------------------------------------------------------------------------------------------------- @@ -98022,48 +106951,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. @@ -98076,29 +107005,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 @@ -98110,15 +107039,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 @@ -98132,46 +107061,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 @@ -98186,19 +107115,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. @@ -98206,65 +107135,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() @@ -98278,9 +107206,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() @@ -98293,9 +107221,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". @@ -98303,9 +107231,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. @@ -98313,18 +107241,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 @@ -98338,146 +107266,148 @@ end function AIRBOSS:_InitStennis() -- Carrier Parameters. - self.carrierparam.sterndist =-153 - self.carrierparam.deckheight = 19.06 + self.carrierparam.sterndist = -153 + self.carrierparam.deckheight = 18.30 -- 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. + -- Landing distance. + self.carrierparam.landingdist = self.carrierparam.sterndist+self.carrierparam.wire3 -- 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 @@ -98489,24 +107419,151 @@ 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 + + -- 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 + + -- Wires. + self.carrierparam.wire1 = 55 -- Distance from stern to first wire. + self.carrierparam.wire2 = 67 + self.carrierparam.wire3 = 79 + self.carrierparam.wire4 = 92 + + -- Landing distance. + self.carrierparam.landingdist = self.carrierparam.sterndist+self.carrierparam.wire3 + +end + +--- Init parameters for Forrestal class super carriers. +-- @param #AIRBOSS self +function AIRBOSS:_InitForrestal() + + -- Init Nimitz as default. + 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 + + -- 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 + + -- 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 + + -- 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 + + -- Landing distance. + self.carrierparam.landingdist = self.carrierparam.sterndist+self.carrierparam.wire3 + +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 + + -- Distance to landing spot. + self.carrierparam.landingspot=69 + + -- Landing distance. + self.carrierparam.landingdist = self.carrierparam.sterndist+self.carrierparam.landingspot + + -- 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 + +end + +--- Init parameters for R05 HMS Invincible carrier. +-- @param #AIRBOSS self +function AIRBOSS:_InitInvincible() + + -- 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=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 = 228.19 + self.carrierparam.totwidthport = 20.5 + self.carrierparam.totwidthstarboard = 24.5 -- 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 = 0 + self.carrierparam.rwylength = 215 + self.carrierparam.rwywidth = 13 -- 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 = nil + self.carrierparam.wire2 = nil + self.carrierparam.wire3 = nil + self.carrierparam.wire4 = nil + + -- Distance to landing spot. + self.carrierparam.landingspot=69 + + -- Landing distance. + self.carrierparam.landingdist = self.carrierparam.sterndist+self.carrierparam.landingspot + + -- 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 end @@ -98518,337 +107575,442 @@ 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 + + -- Distance to landing spot. + self.carrierparam.landingspot=57 + + -- Landing distance. + self.carrierparam.landingdist = self.carrierparam.sterndist+self.carrierparam.landingspot + + -- 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 + +end + +--- Init parameters for LHA-6 America carrier. +-- @param #AIRBOSS self +function AIRBOSS:_InitAmerica() + + -- Init Stennis as default. + self:_InitStennis() + + -- Carrier Parameters. + 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 + + -- Landing runway. + self.carrierparam.rwyangle = 0 + self.carrierparam.rwylength = 240 + self.carrierparam.rwywidth = 15 + + -- Wires. + self.carrierparam.wire1 = nil + self.carrierparam.wire2 = nil + self.carrierparam.wire3 = nil + self.carrierparam.wire4 = nil + + -- Distance to landing spot. + self.carrierparam.landingspot=59 + + -- Landing distance. + self.carrierparam.landingdist = self.carrierparam.sterndist+self.carrierparam.landingspot + + -- 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 + +end + +--- Init parameters for L61 Juan Carlos carrier. +-- @param #AIRBOSS self +function AIRBOSS:_InitJcarlos() + + -- Init Stennis as default. + self:_InitStennis() + + -- Carrier Parameters. + 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 + + -- Landing runway. + self.carrierparam.rwyangle = 0 + self.carrierparam.rwylength = 202 + 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 + + -- Distance to landing spot. + self.carrierparam.landingspot=89 + + -- Landing distance. + self.carrierparam.landingdist = self.carrierparam.sterndist+self.carrierparam.landingspot -- 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 L02 Canberra carrier. +-- @param #AIRBOSS self +function AIRBOSS:_InitCanberra() + + -- Init Juan Carlos as default. + self:_InitJcarlos() 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.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: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 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.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: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 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.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.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: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 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.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.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: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 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.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: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 end @@ -98861,283 +108023,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, - }, - 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 }, } ----------------- @@ -99145,161 +108068,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 }, } ------------------- @@ -99307,309 +108097,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 @@ -99626,77 +108155,82 @@ 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 + or playerData.actype == AIRBOSS.AircraftCarrier.RHINOE + or playerData.actype == AIRBOSS.AircraftCarrier.RHINOF + or playerData.actype == AIRBOSS.AircraftCarrier.GROWLER + 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. This might need further tuning. - aoa.SLOW = 14.0 - aoa.Slow = 13.0 - aoa.OnSpeedMax = 12.0 - aoa.OnSpeed = 11.0 - aoa.OnSpeedMin = 10.0 - aoa.Fast = 9.0 - aoa.FAST = 8.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 @@ -99707,13 +108241,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 -- @@ -99725,20 +108259,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 @@ -99750,13 +108284,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 -- @@ -99767,20 +108301,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 @@ -99795,16 +108329,19 @@ 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 + or playerData.actype == AIRBOSS.AircraftCarrier.RHINOE + or playerData.actype == AIRBOSS.AircraftCarrier.RHINOF + or playerData.actype == AIRBOSS.AircraftCarrier.GROWLER + 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 @@ -99813,153 +108350,148 @@ 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 + if tomcat then + speed = UTILS.KnotsToMps( 150 ) + else + speed = UTILS.KnotsToMps( 250 ) + end - elseif step==AIRBOSS.PatternStep.ARCOUT then + elseif step == AIRBOSS.PatternStep.ARCOUT then - if tomcat then - speed=UTILS.KnotsToMps(150) - else - speed=UTILS.KnotsToMps(250) - end + if tomcat then + speed = UTILS.KnotsToMps( 150 ) + else + speed = UTILS.KnotsToMps( 250 ) + end - elseif step==AIRBOSS.PatternStep.DIRTYUP then + elseif step == AIRBOSS.PatternStep.DIRTYUP then - alt=UTILS.FeetToMeters(1200) + alt = UTILS.FeetToMeters( 1200 ) - --speed=UTILS.KnotsToMps(250) + -- speed=UTILS.KnotsToMps(250) - elseif step==AIRBOSS.PatternStep.BULLSEYE then + elseif step == AIRBOSS.PatternStep.BULLSEYE then - alt=UTILS.FeetToMeters(1200) + alt = UTILS.FeetToMeters( 1200 ) - dist=-UTILS.NMToMeters(3) + dist = -UTILS.NMToMeters( 3 ) - aoa=aoaac.OnSpeed + aoa = aoaac.OnSpeed - elseif step==AIRBOSS.PatternStep.INITIAL then + elseif step == AIRBOSS.PatternStep.INITIAL then if hornet or tomcat or harrier then - alt=UTILS.FeetToMeters(800) - speed=UTILS.KnotsToMps(350) + 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) + 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 + elseif step == AIRBOSS.PatternStep.BREAKENTRY then if hornet or tomcat or harrier then - alt=UTILS.FeetToMeters(800) - speed=UTILS.KnotsToMps(350) + 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) + 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 + elseif step == AIRBOSS.PatternStep.EARLYBREAK then if hornet or tomcat or harrier or goshawk then - alt=UTILS.FeetToMeters(800) + alt = UTILS.FeetToMeters( 800 ) elseif skyhawk then - alt=UTILS.FeetToMeters(600) + alt = UTILS.FeetToMeters( 600 ) end - elseif step==AIRBOSS.PatternStep.LATEBREAK then + elseif step == AIRBOSS.PatternStep.LATEBREAK then if hornet or tomcat or harrier or goshawk then - alt=UTILS.FeetToMeters(800) + alt = UTILS.FeetToMeters( 800 ) elseif skyhawk then - alt=UTILS.FeetToMeters(600) + alt = UTILS.FeetToMeters( 600 ) end - elseif step==AIRBOSS.PatternStep.ABEAM then + elseif step == AIRBOSS.PatternStep.ABEAM then if hornet or tomcat or harrier or goshawk then - alt=UTILS.FeetToMeters(600) + alt = UTILS.FeetToMeters( 600 ) elseif skyhawk then - alt=UTILS.FeetToMeters(500) + alt = UTILS.FeetToMeters( 500 ) end - aoa=aoaac.OnSpeed + aoa = aoaac.OnSpeed - if harrier then - -- 0.8 to 1.0 NM - dist=UTILS.NMToMeters(0.9) - else - dist=UTILS.NMToMeters(1.2) - end - - if goshawk then + 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) + alt=UTILS.FeetToMeters(312)-- 300-325 ft end - aoa=aoaac.OnSpeed + aoa = aoaac.OnSpeed end @@ -99976,30 +108508,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 @@ -100016,34 +108548,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. @@ -100061,40 +108593,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 @@ -100106,85 +108638,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 @@ -100195,25 +108732,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 @@ -100225,92 +108762,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 and false then --Disabled AI handling because of incorrect OPSGROUP reference! - 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 @@ -100318,36 +108852,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 @@ -100355,82 +108888,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 @@ -100440,61 +108972,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) + -- Holding altitude between angels 6 and 10 (random). + 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 @@ -100503,21 +109035,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 @@ -100527,69 +109059,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 @@ -100599,36 +109136,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 @@ -100639,27 +109176,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 @@ -100668,40 +109205,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. @@ -100712,6 +109249,9 @@ function AIRBOSS:_RefuelAI(flight) actype==AIRBOSS.AircraftCarrier.F14B or actype==AIRBOSS.AircraftCarrier.F14A_AI or actype==AIRBOSS.AircraftCarrier.HORNET or + actype==AIRBOSS.AircraftCarrier.RHINOE or + actype==AIRBOSS.AircraftCarrier.RHINOF or + actype==AIRBOSS.AircraftCarrier.GROWLER or actype==AIRBOSS.AircraftCarrier.FA18C or actype==AIRBOSS.AircraftCarrier.S3B or actype==AIRBOSS.AircraftCarrier.S3BTANKER then @@ -100719,25 +109259,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 @@ -100745,109 +109285,113 @@ 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) - - 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) + local Speed = UTILS.KnotsToKmph( 200 ) + + if flight.actype == AIRBOSS.AircraftCarrier.HORNET + or flight.actype == AIRBOSS.AircraftCarrier.FA18C + or flight.actype == AIRBOSS.AircraftCarrier.RHINOE + or flight.actype == AIRBOSS.AircraftCarrier.RHINOF + or flight.actype == AIRBOSS.AircraftCarrier.GROWLER 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. @@ -100857,83 +109401,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 Delta pattern. - if self.carriertype==AIRBOSS.CarrierType.TARAWA then + -- Tarawa,LHA,LHD Delta patterns. + if self.carriertype == AIRBOSS.CarrierType.INVINCIBLE or 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 @@ -100942,95 +109486,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 @@ -101039,51 +109583,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 @@ -101092,87 +109636,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 @@ -101184,28 +109728,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 @@ -101215,87 +109758,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 @@ -101305,60 +109848,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 @@ -101374,32 +109917,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 @@ -101407,19 +109950,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. @@ -101428,14 +109970,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 @@ -101444,17 +109986,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 @@ -101466,14 +110008,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 @@ -101487,47 +110029,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 - - 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) + 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 ) -- 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 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -101538,159 +110078,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) - end - self:T(self.lid..text) + 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 ) -- 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 @@ -101707,70 +110246,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)) - - 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 +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.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 @@ -101780,30 +110320,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 @@ -101814,16 +110354,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 @@ -101833,11 +110373,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 @@ -101851,29 +110391,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 @@ -101883,14 +110423,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 @@ -101901,31 +110441,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 @@ -101933,17 +110473,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 @@ -101952,29 +110492,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 @@ -101983,14 +110523,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 @@ -102002,44 +110542,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 @@ -102053,16 +110593,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 @@ -102073,41 +110613,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 @@ -102120,18 +110660,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 @@ -102145,37 +110685,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 @@ -102184,7 +110724,7 @@ function AIRBOSS:_UpdateFlightSection(flight) -------------------- -- Remove flight from its leaders section. - self:_RemoveFlightFromSection(flight) + self:_RemoveFlightFromSection( flight ) end @@ -102195,28 +110735,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 @@ -102224,36 +110764,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 @@ -102269,212 +110809,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 @@ -102482,28 +111021,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 @@ -102512,13 +111051,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 @@ -102527,19 +111066,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 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -102547,62 +111085,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 @@ -102610,51 +111148,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 @@ -102664,41 +111202,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 @@ -102706,57 +111244,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 then + if self.carriertype == AIRBOSS.CarrierType.INVINCIBLE or 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 @@ -102766,7 +111304,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 @@ -102776,31 +111314,31 @@ function AIRBOSS:OnEventLand(EventData) -- AI unit landed -- -------------------- - if self.carriertype~=AIRBOSS.CarrierType.TARAWA then + if self.carriertype ~= AIRBOSS.CarrierType.INVINCIBLE or 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 @@ -102809,62 +111347,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 @@ -102876,61 +111413,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 @@ -102941,48 +111477,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 @@ -102990,51 +111525,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 @@ -103042,51 +111576,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 @@ -103094,41 +111627,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 @@ -103139,8 +111671,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 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -103150,31 +111682,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 @@ -103183,28 +111715,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 @@ -103212,18 +111744,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 --------------------------- @@ -103231,99 +111763,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 @@ -103331,62 +111863,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 @@ -103395,24 +111926,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. @@ -103422,48 +111953,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 @@ -103472,40 +112003,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() + -- altitude of player in feet. + 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 @@ -103516,24 +112047,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 @@ -103541,60 +112072,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 @@ -103602,62 +112132,68 @@ 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 + or playerData.actype == AIRBOSS.AircraftCarrier.RHINOE + or playerData.actype == AIRBOSS.AircraftCarrier.RHINOF + or playerData.actype == AIRBOSS.AircraftCarrier.GROWLER + 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 @@ -103666,30 +112202,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 7.5 call - if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then - self:RadioTransmission(self.LSORadio, self.LSOCall.EXPECTSPOT75, nil, nil, nil, true) + -- 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 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 @@ -103697,150 +112237,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 @@ -103952,53 +112493,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. @@ -104209,110 +112754,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) - - -- 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 + 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 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 @@ -104321,17 +112884,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 @@ -104341,37 +112904,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 @@ -104389,61 +112952,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 @@ -104454,107 +113018,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 @@ -104568,29 +113129,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 then - -- Tarawa: Translate 8 meters port. + -- Stern coordinate (sterndist<0). --Pene testing Case III + if self.carriertype==AIRBOSS.CarrierType.INVINCIBLE or 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) + 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 ) 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 @@ -104600,73 +113170,73 @@ 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 @@ -104682,61 +113252,64 @@ 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 + or playerData.actype == AIRBOSS.AircraftCarrier.RHINOE + or playerData.actype == AIRBOSS.AircraftCarrier.RHINOF + or playerData.actype == AIRBOSS.AircraftCarrier.GROWLER 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 @@ -104747,47 +113320,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 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -104798,53 +113371,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 @@ -104853,70 +113426,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 @@ -104925,49 +113497,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 @@ -104976,22 +113548,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 @@ -105000,28 +113572,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 @@ -105030,79 +113602,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 @@ -105110,70 +113681,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 @@ -105183,167 +113752,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, HMS Invincible, 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 then - self.zoneHolding=ZONE_RADIUS:New("CASE I Holding Zone", self.carrier:GetVec2(), UTILS.NMToMeters(5)) + if self.carriertype == AIRBOSS.CarrierType.INVINCIBLE or 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 @@ -105354,75 +113921,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 then + if self.carriertype == AIRBOSS.CarrierType.INVINCIBLE or 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 @@ -105436,90 +114002,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 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 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 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. @@ -105527,34 +114093,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 @@ -105564,37 +114130,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 @@ -105604,126 +114170,131 @@ 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 ---- Get alitude of aircraft wrt carrier deck. Should give zero when the aircraft touched down. +--- Get altitude of aircraft wrt carrier deck. Should give zero when the aircraft touched down. -- @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 we take the abeam landing spot 120 ft abeam the 7.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, HMS Invincible, and HMS Hermes and Invincible 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()) - - -- Stern coordinate. - --local stern=self:_GetSternCoord() + self.landingcoord:UpdateFromCoordinate( self:_GetSternCoord() ) -- Final bearing. local FB=self:GetFinalBearing(false) - if self.carriertype==AIRBOSS.CarrierType.TARAWA then + -- Cse + local case=self.case + + -- set Case III V/STOL abeam landing spot over deck -- Pene Testing + if self.carriertype==AIRBOSS.CarrierType.INVINCIBLE or 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 + + -- Landing coordinate. + self.landingcoord:UpdateFromCoordinate(self:_GetLandingSpotCoordinate()) - -- Landing 100 ft abeam, 120 ft alt. - self.landingcoord:UpdateFromCoordinate(self:_GetLandingSpotCoordinate()):Translate(35, FB-90, true, true) - --stern=self:_GetLandingSpotCoordinate():Translate(35, FB-90) + -- Altitude 120ft -- is this corect for Case III? + self.landingcoord:SetAltitude(UTILS.FeetToMeters(120)) + + elseif case==2 or case==1 then - -- Alitude 120 ft. - self.landingcoord:SetAltitude(UTILS.FeetToMeters(120)) + -- Landing 100 ft abeam, 120 ft alt. + self.landingcoord:UpdateFromCoordinate(self:_GetLandingSpotCoordinate()):Translate(35, FB-90, true, true) + -- 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) + self.landingcoord:Translate( self.carrierparam.wire3, 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 return self.landingcoord end ---- Get landing spot on Tarawa. +--- Get landing spot on Tarawa and others. -- @param #AIRBOSS self -- @return Core.Point#COORDINATE Primary landing spot coordinate. function AIRBOSS:_GetLandingSpotCoordinate() - self.landingspotcoord:UpdateFromCoordinate(self:_GetSternCoord()) - - -- Stern coordinate. - --local stern=self:_GetSternCoord() - - if self.carriertype==AIRBOSS.CarrierType.TARAWA then + -- Start at stern coordinate. + self.landingspotcoord:UpdateFromCoordinate( self:_GetSternCoord() ) - -- Landing 100 ft abeam, 120 alt. - local hdg=self:GetHeading() + -- Landing 100 ft abeam, 100 alt. + local hdg = self:GetHeading() - -- Primary landing spot 7.5 - self.landingspotcoord:Translate(57, hdg, true, true):SetAltitude(self.carrierparam.deckheight) - - end + -- Primary landing spot. Different carriers handled via carrier parameter landingspot now. + self.landingspotcoord:Translate( self.carrierparam.landingspot, hdg, true, true ):SetAltitude( self.carrierparam.deckheight ) return self.landingspotcoord end @@ -105732,20 +114303,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 @@ -105756,30 +114327,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) + -- Wind direction and speed. By default at 18 meters ASL. + local Wdir, Wspeed = cv:GetWind( alt or 18 ) -- 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 @@ -105788,76 +114359,75 @@ 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. -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 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. - 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. +-- @param Core.Point#COORDINATE coord (Optional) Coordinate 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 @@ -105869,27 +114439,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 @@ -105907,56 +114476,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 @@ -105969,19 +114538,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 @@ -105993,24 +114562,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 @@ -106020,20 +114590,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. @@ -106041,42 +114611,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. @@ -106085,26 +114654,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 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -106114,93 +114681,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 24 seconds: No Grade "--" -- -- If you manage to be between 16.4 and and 16.6 seconds, you will even get and okay underline "\_OK\_". +-- No groove time for Harrier on LHA, LHD set to Tgroove Unicorn as starting point to allow possible _OK_ 5.0. +-- +-- If time in the AV-8B +-- +-- * < 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 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)" + 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 < 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 >= 60.0 and t <= 65.0) then + grade = "_OK_ V/STOL" end return grade @@ -106250,68 +114837,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 conver 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) + -- 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 ) -- Put everything together. - local G=GXX.." "..GIM.." ".." "..GIC.." "..GAR + local G = GXX .. " " .. GIM .. " " .. " " .. GIC .. " " .. GAR -- 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. + 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>=60.0 and Tgroove<=65.0)and playerData.actype==AIRBOSS.AircraftCarrier.AV8B or false local grade local points - if N==0 and TgrooveUnicorn then + if N == 0 and (TgrooveUnicorn or TgrooveVstolUnicorn or playerData.case==3) then -- No deviations, should be REALLY RARE! - grade="_OK_" - points=5.0 - G="Unicorn" + grade = "_OK_" + points = 5.0 + G = "Unicorn" else - if nL>0 then + + -- 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 > 1 and playerData.actype==AIRBOSS.AircraftCarrier.AV8B then -- Larger deviations ==> "No grade" 2.0 points. grade="--" points=2.0 - elseif nN>0 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 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 -- 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 -- 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 @@ -106321,55 +114927,65 @@ function AIRBOSS:_LSOgrade(playerData) 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 @@ -106380,175 +114996,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) - - --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 + 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 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 - end + 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. @@ -106556,69 +115168,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 - - 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 - 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 - 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 - 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 then - gp=AIRBOSS.GroovePos.AL + 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 + 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 + 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 + 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 + 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.INVINCIBLE or 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 @@ -106630,21 +115242,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) - - 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 +function AIRBOSS:_CheckAbort( X, Z, pos ) + + 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 @@ -106655,58 +115267,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 @@ -106719,33 +115331,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 @@ -106755,7 +115367,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 @@ -106763,195 +115375,196 @@ 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 + or playerData.actype == AIRBOSS.AircraftCarrier.RHINOE + or playerData.actype == AIRBOSS.AircraftCarrier.RHINOF + or playerData.actype == AIRBOSS.AircraftCarrier.GROWLER 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. @@ -106959,57 +115572,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 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 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", "" ) 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. @@ -107019,65 +115632,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", "") - else - --hint="You'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", "" ) 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. @@ -107087,54 +115700,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 - - 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", "") + -- 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", "" ) 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 @@ -107146,48 +115759,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 @@ -107201,122 +115814,124 @@ 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 - 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.finalscore = Points + end + mygrade.case = playerData.case + local windondeck = self:GetWindOnDeck() + mygrade.wind = UTILS.Round( UTILS.MpsToKnots( windondeck ), 1 ) + mygrade.modex = playerData.onboard + mygrade.airframe = playerData.actype + mygrade.carriertype = self.carriertype + mygrade.carriername = self.alias + mygrade.carrierrwy = self.carrierparam.rwyangle + mygrade.theatre = self.theatre + mygrade.mitime = UTILS.SecondsToClock( timer.getAbsTime(), true ) + 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 + -- Add last grade to playerdata for FunkMan. + playerData.grade=mygrade + -- 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 @@ -107337,41 +115952,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 @@ -107384,16 +115999,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 @@ -107406,18 +116021,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 -------------- @@ -107427,16 +116041,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 @@ -107449,7 +116063,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 @@ -107459,47 +116073,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 @@ -107513,80 +116127,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 @@ -107596,59 +116209,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. @@ -107657,70 +116268,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. @@ -107729,109 +116340,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 @@ -107843,50 +116453,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 @@ -107898,86 +116507,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 @@ -107987,31 +116593,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 @@ -108022,24 +116629,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. @@ -108051,23 +116658,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 --------------------------- @@ -108099,16 +116707,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 ---------------------------- @@ -108119,228 +116727,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)) - - local headingto=c0:HeadingTo(gotocoord) + -- gotocoord:MarkToAll(string.format("Goto waypoint speed=%.1f km/h", speedkmh)) - local hdg1=airboss:GetHeading() - local hdg2=c0:HeadingTo(gotocoord) - local delta=airboss:_GetDeltaHeading(hdg1, hdg2) + local headingto = c0:HeadingTo( gotocoord ) - --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 @@ -108352,23 +116959,27 @@ 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) - - 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" +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.RHINOE or actype == AIRBOSS.AircraftCarrier.RHINOF then + nickname = "Rhino" + elseif actype == AIRBOSS.AircraftCarrier.GROWLER then + nickname = "Growler" + elseif actype == AIRBOSS.AircraftCarrier.S3B or actype == AIRBOSS.AircraftCarrier.S3BTANKER then + nickname = "Viking" end return nickname @@ -108378,8 +116989,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. @@ -108387,60 +116998,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 @@ -108456,19 +117066,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 @@ -108478,32 +117088,32 @@ 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. - if aircrafttype==AIRBOSS.AircraftCarrier.AV8B then - if self.carriertype==AIRBOSS.CarrierType.TARAWA then + -- Special case for Harrier which can only land on Tarawa, LHA and LHD. + if aircrafttype == AIRBOSS.AircraftCarrier.AV8B then + if self.carriertype == AIRBOSS.CarrierType.INVINCIBLE or 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 end end - -- Also only Harriers can land on the Tarawa. - if self.carriertype==AIRBOSS.CarrierType.TARAWA then - if aircrafttype~=AIRBOSS.AircraftCarrier.AV8B then + -- Also only Harriers can land on the Tarawa, LHA and LHD. + if self.carriertype == AIRBOSS.CarrierType.INVINCIBLE 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 @@ -108516,10 +117126,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 @@ -108532,15 +117142,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 @@ -108553,31 +117163,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. +-- @param alt altitude 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 @@ -108592,25 +117202,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 @@ -108619,10 +117229,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 @@ -108630,15 +117240,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 @@ -108651,20 +117260,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. @@ -108672,13 +117281,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 @@ -108686,22 +117295,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 @@ -108710,7 +117319,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. @@ -108743,7 +117352,7 @@ end function AIRBOSS:_GetStaticWeather() -- Weather data from mission file. - local weather=env.mission.weather + local weather = env.mission.weather -- Clouds --[[ @@ -108755,19 +117364,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 @@ -108779,12 +117388,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 @@ -108795,9 +117403,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. @@ -108814,83 +117422,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 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 #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 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 @@ -108905,21 +117513,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 @@ -108934,76 +117542,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 @@ -109015,8 +117622,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 @@ -109026,72 +117633,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 @@ -109107,17 +117716,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 @@ -109132,23 +117741,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 @@ -109160,44 +117769,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 @@ -109210,30 +117819,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 @@ -109248,76 +117857,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 @@ -109325,7 +117934,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 @@ -109335,13 +117943,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 @@ -109354,13 +117962,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 @@ -109372,28 +117980,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 @@ -109403,27 +118011,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 @@ -109439,25 +118047,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 @@ -109474,55 +118082,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 @@ -109537,120 +118145,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 @@ -109658,48 +118346,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 @@ -109708,68 +118395,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 @@ -109778,195 +118464,193 @@ 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. +--- Compile a radio call when Marshal tells a flight the holding altitude. -- @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 ---- Compile a radio call when Marshal tells a flight the holding alitude. +--- Compile a radio call when Marshal tells a flight the holding altitude. -- @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. +--- Compile a radio call when Marshal tells a flight the holding altitude. -- @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 ---- Compile a radio call when Marshal tells a flight the holding alitude. +--- Compile a radio call when Marshal tells a flight the holding altitude. -- @param #AIRBOSS self -- @param #string modex Tail number. -- @param #number case Recovery case. -- @param #number brc Base recovery course. --- @param #number altitude Holding alitude. +-- @param #number altitude Holding altitude. -- @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 @@ -109977,28 +118661,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 -- @@ -110006,10 +118690,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 @@ -110018,116 +118702,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 @@ -110140,37 +118822,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 @@ -110179,25 +118861,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 @@ -110208,22 +118890,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 @@ -110232,22 +118914,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 @@ -110257,22 +118939,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 @@ -110280,28 +118962,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 ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -110309,33 +118990,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 @@ -110344,68 +119025,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 @@ -110414,8 +119100,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 @@ -110425,102 +119111,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 @@ -110530,60 +119216,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 @@ -110592,69 +119278,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 @@ -110662,60 +119353,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 @@ -110724,14 +119415,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 @@ -110740,72 +119431,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 @@ -110815,148 +119505,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 @@ -110968,33 +119660,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. @@ -111002,50 +119694,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 @@ -111054,74 +119748,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 @@ -111130,36 +119823,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 @@ -111173,134 +119866,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 @@ -111309,140 +120001,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) - 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) + 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 ) 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 @@ -111454,31 +120145,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 @@ -111487,31 +120178,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 @@ -111520,20 +120211,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 @@ -111542,28 +120233,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 @@ -111572,154 +120263,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 @@ -111728,92 +120417,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) + -- Pattern altitude. + 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 @@ -111823,55 +120511,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 landing spots. - if self.carriertype==AIRBOSS.CarrierType.TARAWA then - text=text.."\n* abeam landing stop with RED flares" + -- Tarawa, LHA and LHD landing spots. + if self.carriertype == AIRBOSS.CarrierType.INVINCIBLE or 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 @@ -111881,49 +120569,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 @@ -111932,18 +120620,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 @@ -111951,23 +120639,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 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ @@ -111976,94 +120663,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 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) + 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 + + -- 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. @@ -112073,17 +120758,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 @@ -112096,72 +120781,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. @@ -112169,13 +120851,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 @@ -112184,41 +120866,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. @@ -112226,102 +120907,155 @@ 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 + +--- On after "LSOGrade" event. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #AIRBOSS.PlayerData playerData Player Data. +-- @param #AIRBOSS.LSOgrade grade LSO grade. +function AIRBOSS:onafterLSOGrade(From, Event, To, playerData, grade) + + if self.funkmanSocket then + + -- Extract used info for FunkMan. We need to be careful with the amount of data send via UDP socket. + local trapsheet={} ; trapsheet.X={} ; trapsheet.Z={} ; trapsheet.AoA={} ; trapsheet.Alt={} + + -- Loop over trapsheet and extract used values. + for i = 1, #playerData.trapsheet do + local ts=playerData.trapsheet[i] --#AIRBOSS.GrooveData + table.insert(trapsheet.X, UTILS.Round(ts.X, 1)) + table.insert(trapsheet.Z, UTILS.Round(ts.Z, 1)) + table.insert(trapsheet.AoA, UTILS.Round(ts.AoA, 2)) + table.insert(trapsheet.Alt, UTILS.Round(ts.Alt, 1)) + end + + local result={} + result.command=SOCKET.DataType.LSOGRADE + result.name=playerData.name + result.trapsheet=trapsheet + result.airframe=grade.airframe + result.mitime=grade.mitime + result.midate=grade.midate + result.wind=grade.wind + result.carriertype=grade.carriertype + result.carriername=grade.carriername + result.carrierrwy=grade.carrierrwy + result.landingdist=self.carrierparam.landingdist + result.theatre=grade.theatre + result.case=playerData.case + result.Tgroove=grade.Tgroove + result.wire=grade.wire + result.grade=grade.grade + result.points=grade.points + result.details=grade.details + + -- Debug info. + self:T(self.lid.."Result onafterLSOGrade") + self:T(result) + + -- Send result. + self.funkmanSocket:SendTable(result) + end end @@ -112361,7 +121095,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". @@ -113114,10 +121848,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 @@ -113955,7 +122690,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 @@ -114828,7 +123562,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() @@ -115367,14 +124101,13 @@ end -- === -- -- ### Author: **funkyfranky** --- @module Ops.Atis +-- +-- @module Ops.ATIS -- @image OPS_ATIS.png - --- 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. @@ -115389,7 +124122,7 @@ end -- @field #string activerunway The active runway specified by the user. -- @field #number subduration Duration how long subtitles are displayed in seconds. -- @field #boolean metric If true, use metric units. If false, use imperial (default). --- @field #boolean PmmHg If true, give pressure in millimeters of Mercury. Default is inHg for imperial and hecto Pascal (=mili Bars) for metric units. +-- @field #boolean PmmHg If true, give pressure in millimeters of Mercury. Default is inHg for imperial and hectopascal (hPa, which is the same as millibar - mbar) for metric units. -- @field #boolean qnhonly If true, suppresses reporting QFE. Default is to report both QNH and QFE. -- @field #boolean TDegF If true, give temperature in degrees Fahrenheit. Default is in degrees Celsius independent of chosen unit system. -- @field #number zuludiff Time difference local vs. zulu in hours. @@ -115414,6 +124147,10 @@ end -- @field #boolean useSRS If true, use SRS for transmission. -- @field Sound.SRS#MSRS msrs Moose SRS object. -- @field #number dTQueueCheck Time interval to check the radio queue. Default 5 sec or 90 sec if SRS is used. +-- @field #boolean ReportmBar Report mBar/hpa even if not metric, i.e. for Mirage flights +-- @field #boolean TransmitOnlyWithPlayers For SRS - If true, only transmit if there are alive Players. +-- @field #string SRSText Text of the complete SRS message (if done at least once, else nil) +-- @field #boolean ATISforFARPs Will be set to true if the base given is a FARP/Helipad -- @extends Core.Fsm#FSM --- *It is a very sad thing that nowadays there is so little useless information.* - Oscar Wilde @@ -115443,12 +124180,12 @@ end -- The @{#ATIS.New}(*airbasename*, *frequency*) creates a new ATIS object. The parameter *airbasename* is the name of the airbase or airport. Note that this has to be spelled exactly as in the DCS mission editor. -- The parameter *frequency* is the frequency the ATIS broadcasts in MHz. -- --- Broadcasting is started via the @{#ATIS.Start}() function. The start can be delayed by useing @{#ATIS.__Start}(*delay*), where *delay* is the delay in seconds. +-- Broadcasting is started via the @{#ATIS.Start}() function. The start can be delayed by using @{#ATIS.__Start}(*delay*), where *delay* is the delay in seconds. -- -- ## Subtitles -- -- Currently, DCS allows for displaying subtitles of radio transmissions only from airborne units, *i.e.* airplanes and helicopters. Therefore, if you want to have subtitles, it is necessary to place an --- additonal aircraft on the ATIS airport and set it to uncontrolled. This unit can then function as a radio relay to transmit messages with subtitles. These subtitles will only be displayed, if the +-- additional aircraft on the ATIS airport and set it to uncontrolled. This unit can then function as a radio relay to transmit messages with subtitles. These subtitles will only be displayed, if the -- player has tuned in the correct ATIS frequency. -- -- Radio transmissions via an airborne unit can be set via the @{#ATIS.SetRadioRelayUnitName}(*unitname*) function, where the parameter *unitname* is the name of the unit passed as string, *e.g.* @@ -115464,7 +124201,7 @@ end -- -- ## Active Runway -- --- By default, the currently active runway is determined automatically by analysing the wind direction. Therefore, you should obviously set the wind speed to be greater zero in your mission. +-- By default, the currently active runway is determined automatically by analyzing the wind direction. Therefore, you should obviously set the wind speed to be greater zero in your mission. -- -- Note however, there are a few special cases, where automatic detection does not yield the correct or desired result. -- For example, there are airports with more than one runway facing in the same direction (usually denoted left and right). In this case, there is obviously no *unique* result depending on the wind vector. @@ -115492,7 +124229,7 @@ end -- -- ## Nav Aids -- --- Frequencies or channels of navigation aids can be specified by the user and are then provided as additional information. Unfortunately, it is **not possible** to aquire this information via the DCS API +-- Frequencies or channels of navigation aids can be specified by the user and are then provided as additional information. Unfortunately, it is **not possible** to acquire this information via the DCS API -- we have access to. -- -- As they say, all road lead to Rome but (for me) the easiest way to obtain the available nav aids data of an airport, is to start a mission and click on an airport symbol. @@ -115561,7 +124298,7 @@ end -- -- atisBatumi:SetMetricUnits() -- --- With this, wind speed is given in meters per second, pressure in hecto Pascal (mbar), visibility in kilometers etc. +-- With this, wind speed is given in meters per second, pressure in hectopascal (hPa, which is the same as millibar - mbar), visibility in kilometers etc. -- -- # Sound Files -- @@ -115578,17 +124315,19 @@ end -- # Marks on the F10 Map -- -- You can place marks on the F10 map via the @{#ATIS.SetMapMarks}() function. These will contain info about the ATIS frequency, the currently active runway and some basic info about the weather (wind, pressure and temperature). --- +-- -- # Text-To-Speech --- --- You can enable text-to-speech ATIS information with the @{#ATIS.SetSRS}() function. This uses [SRS](http://dcssimpleradio.com/) (Version >= 1.9.6.0) for broadcasing. +-- +-- You can enable text-to-speech ATIS information with the @{#ATIS.SetSRS}() function. This uses [SRS](http://dcssimpleradio.com/) (Version >= 1.9.6.0) for broadcasting. -- Advantages are that **no sound files** or radio relay units are necessary. Also the issue that FC3 aircraft hear all transmissions will be circumvented. --- +-- -- The @{#ATIS.SetSRS}() requires you to specify the path to the SRS install directory or more specifically the path to the DCS-SR-ExternalAudio.exe file. --- +-- -- Unfortunately, it is not possible to determine the duration of the complete transmission. So once the transmission is finished, there might be some radio silence before -- the next iteration begins. You can fine tune the time interval between transmissions with the @{#ATIS.SetQueueUpdateTime}() function. The default interval is 90 seconds. -- +-- An SRS Setup-Guide can be found here: [Moose TTS Setup Guide](https://github.com/FlightControl-Master/MOOSE_GUIDES/blob/master/documents/Moose%20TTS%20Setup%20Guide.pdf) +-- -- # Examples -- -- ## Caucasus: Batumi @@ -115619,19 +124358,31 @@ end -- atisAbuDhabi:SetTowerFrequencies({250.5, 119.2}) -- atisAbuDhabi:SetVOR(114.25) -- atisAbuDhabi:Start() --- +-- -- ## SRS --- +-- -- atis=ATIS:New("Batumi", 305, radio.modulation.AM) -- atis:SetSRS("D:\\DCS\\_SRS\\", "male", "en-US") -- atis:Start() -- -- This uses a male voice with US accent. It requires SRS to be installed in the `D:\DCS\_SRS\` directory. Not that backslashes need to be escaped or simply use slashes (as in linux). +-- +-- ## FARPS +-- +-- ATIS is working with FARPS, but this requires the usage of SRS. The airbase name for the `New()-method` is the UNIT name of the FARP: +-- +-- atis = ATIS:New("FARP Gold",119,radio.modulation.AM) +-- atis:SetMetricUnits() +-- atis:SetTransmitOnlyWithPlayers(true) +-- atis:SetReportmBar(true) +-- atis:SetTowerFrequencies(127.50) +-- atis:SetSRS("D:\\DCS\\_SRS\\", "male", "en-US",nil,5002) +-- atis:SetAdditionalInformation("Welcome to the Jungle!") +-- atis:__Start(3) -- -- @field #ATIS ATIS = { ClassName = "ATIS", - Debug = false, lid = nil, theatre = nil, airbasename = nil, @@ -115668,6 +124419,9 @@ ATIS = { usemarker = nil, markerid = nil, relHumidity = nil, + ReportmBar = false, + TransmitOnlyWithPlayers = false, + ATISforFARPs = false, } --- NATO alphabet. @@ -115712,14 +124466,14 @@ ATIS.Alphabet = { -- @field #number TheChannel -10° (West). -- @field #number Syria +5° (East). -- @field #number MarianaIslands +2° (East). -ATIS.RunwayM2T={ - Caucasus=0, - Nevada=12, - Normandy=-10, - PersianGulf=2, - TheChannel=-10, - Syria=5, - MarianaIslands=2, +ATIS.RunwayM2T = { + Caucasus = 0, + Nevada = 12, + Normandy = -10, + PersianGulf = 2, + TheChannel = -10, + Syria = 5, + MarianaIslands = 2, } --- Whether ICAO phraseology is used for ATIS broadcasts. @@ -115731,14 +124485,14 @@ ATIS.RunwayM2T={ -- @field #boolean TheChannel true. -- @field #boolean Syria true. -- @field #boolean MarianaIslands true. -ATIS.ICAOPhraseology={ - Caucasus=true, - Nevada=false, - Normandy=true, - PersianGulf=true, - TheChannel=true, - Syria=true, - MarianaIslands=true, +ATIS.ICAOPhraseology = { + Caucasus = true, + Nevada = false, + Normandy = true, + PersianGulf = true, + TheChannel = true, + Syria = true, + MarianaIslands = true } --- Nav point data. @@ -115827,99 +124581,101 @@ ATIS.ICAOPhraseology={ -- @field #ATIS.Soundfile TACANChannel -- @field #ATIS.Soundfile VORFrequency ATIS.Sound = { - ActiveRunway={filename="ActiveRunway.ogg", duration=0.99}, - AdviceOnInitial={filename="AdviceOnInitial.ogg", duration=3.00}, - Airport={filename="Airport.ogg", duration=0.66}, - Altimeter={filename="Altimeter.ogg", duration=0.68}, - At={filename="At.ogg", duration=0.41}, - CloudBase={filename="CloudBase.ogg", duration=0.82}, - CloudCeiling={filename="CloudCeiling.ogg", duration=0.61}, - CloudsBroken={filename="CloudsBroken.ogg", duration=1.07}, - CloudsFew={filename="CloudsFew.ogg", duration=0.99}, - CloudsNo={filename="CloudsNo.ogg", duration=1.01}, - CloudsNotAvailable={filename="CloudsNotAvailable.ogg", duration=2.35}, - CloudsOvercast={filename="CloudsOvercast.ogg", duration=0.83}, - CloudsScattered={filename="CloudsScattered.ogg", duration=1.18}, - Decimal={filename="Decimal.ogg", duration=0.54}, - DegreesCelsius={filename="DegreesCelsius.ogg", duration=1.27}, - DegreesFahrenheit={filename="DegreesFahrenheit.ogg", duration=1.23}, - DewPoint={filename="DewPoint.ogg", duration=0.65}, - Dust={filename="Dust.ogg", duration=0.54}, - Elevation={filename="Elevation.ogg", duration=0.78}, - EndOfInformation={filename="EndOfInformation.ogg", duration=1.15}, - Feet={filename="Feet.ogg", duration=0.45}, - Fog={filename="Fog.ogg", duration=0.47}, - Gusting={filename="Gusting.ogg", duration=0.55}, - HectoPascal={filename="HectoPascal.ogg", duration=1.15}, - Hundred={filename="Hundred.ogg", duration=0.47}, - InchesOfMercury={filename="InchesOfMercury.ogg", duration=1.16}, - Information={filename="Information.ogg", duration=0.85}, - Kilometers={filename="Kilometers.ogg", duration=0.78}, - Knots={filename="Knots.ogg", duration=0.59}, - Left={filename="Left.ogg", duration=0.54}, - MegaHertz={filename="MegaHertz.ogg", duration=0.87}, - Meters={filename="Meters.ogg", duration=0.59}, - MetersPerSecond={filename="MetersPerSecond.ogg", duration=1.14}, - Miles={filename="Miles.ogg", duration=0.60}, - MillimetersOfMercury={filename="MillimetersOfMercury.ogg", duration=1.53}, - Minus={filename="Minus.ogg", duration=0.64}, - N0={filename="N-0.ogg", duration=0.55}, - N1={filename="N-1.ogg", duration=0.41}, - N2={filename="N-2.ogg", duration=0.37}, - N3={filename="N-3.ogg", duration=0.41}, - N4={filename="N-4.ogg", duration=0.37}, - N5={filename="N-5.ogg", duration=0.43}, - N6={filename="N-6.ogg", duration=0.55}, - N7={filename="N-7.ogg", duration=0.43}, - N8={filename="N-8.ogg", duration=0.38}, - N9={filename="N-9.ogg", duration=0.55}, - NauticalMiles={filename="NauticalMiles.ogg", duration=1.04}, - None={filename="None.ogg", duration=0.43}, - QFE={filename="QFE.ogg", duration=0.63}, - QNH={filename="QNH.ogg", duration=0.71}, - Rain={filename="Rain.ogg", duration=0.41}, - Right={filename="Right.ogg", duration=0.44}, - Snow={filename="Snow.ogg", duration=0.48}, - SnowStorm={filename="SnowStorm.ogg", duration=0.82}, - StatuteMiles={filename="StatuteMiles.ogg", duration=1.15}, - SunriseAt={filename="SunriseAt.ogg", duration=0.92}, - SunsetAt={filename="SunsetAt.ogg", duration=0.95}, - Temperature={filename="Temperature.ogg", duration=0.64}, - Thousand={filename="Thousand.ogg", duration=0.55}, - ThunderStorm={filename="ThunderStorm.ogg", duration=0.81}, - TimeLocal={filename="TimeLocal.ogg", duration=0.90}, - TimeZulu={filename="TimeZulu.ogg", duration=0.86}, - TowerFrequency={filename="TowerFrequency.ogg", duration=1.19}, - Visibilty={filename="Visibility.ogg", duration=0.79}, - WeatherPhenomena={filename="WeatherPhenomena.ogg", duration=1.07}, - WindFrom={filename="WindFrom.ogg", duration=0.60}, - ILSFrequency={filename="ILSFrequency.ogg", duration=1.30}, - InnerNDBFrequency={filename="InnerNDBFrequency.ogg", duration=1.56}, - OuterNDBFrequency={filename="OuterNDBFrequency.ogg", duration=1.59}, - RunwayLength={filename="RunwayLength.ogg", duration=0.91}, - VORFrequency={filename="VORFrequency.ogg", duration=1.38}, - TACANChannel={filename="TACANChannel.ogg", duration=0.88}, - PRMGChannel={filename="PRMGChannel.ogg", duration=1.18}, - RSBNChannel={filename="RSBNChannel.ogg", duration=1.14}, - Zulu={filename="Zulu.ogg", duration=0.62}, + ActiveRunway = { filename = "ActiveRunway.ogg", duration = 0.99 }, + AdviceOnInitial = { filename = "AdviceOnInitial.ogg", duration = 3.00 }, + Airport = { filename = "Airport.ogg", duration = 0.66 }, + Altimeter = { filename = "Altimeter.ogg", duration = 0.68 }, + At = { filename = "At.ogg", duration = 0.41 }, + CloudBase = { filename = "CloudBase.ogg", duration = 0.82 }, + CloudCeiling = { filename = "CloudCeiling.ogg", duration = 0.61 }, + CloudsBroken = { filename = "CloudsBroken.ogg", duration = 1.07 }, + CloudsFew = { filename = "CloudsFew.ogg", duration = 0.99 }, + CloudsNo = { filename = "CloudsNo.ogg", duration = 1.01 }, + CloudsNotAvailable = { filename = "CloudsNotAvailable.ogg", duration = 2.35 }, + CloudsOvercast = { filename = "CloudsOvercast.ogg", duration = 0.83 }, + CloudsScattered = { filename = "CloudsScattered.ogg", duration = 1.18 }, + Decimal = { filename = "Decimal.ogg", duration = 0.54 }, + DegreesCelsius = { filename = "DegreesCelsius.ogg", duration = 1.27 }, + DegreesFahrenheit = { filename = "DegreesFahrenheit.ogg", duration = 1.23 }, + DewPoint = { filename = "DewPoint.ogg", duration = 0.65 }, + Dust = { filename = "Dust.ogg", duration = 0.54 }, + Elevation = { filename = "Elevation.ogg", duration = 0.78 }, + EndOfInformation = { filename = "EndOfInformation.ogg", duration = 1.15 }, + Feet = { filename = "Feet.ogg", duration = 0.45 }, + Fog = { filename = "Fog.ogg", duration = 0.47 }, + Gusting = { filename = "Gusting.ogg", duration = 0.55 }, + HectoPascal = { filename = "HectoPascal.ogg", duration = 1.15 }, + Hundred = { filename = "Hundred.ogg", duration = 0.47 }, + InchesOfMercury = { filename = "InchesOfMercury.ogg", duration = 1.16 }, + Information = { filename = "Information.ogg", duration = 0.85 }, + Kilometers = { filename = "Kilometers.ogg", duration = 0.78 }, + Knots = { filename = "Knots.ogg", duration = 0.59 }, + Left = { filename = "Left.ogg", duration = 0.54 }, + MegaHertz = { filename = "MegaHertz.ogg", duration = 0.87 }, + Meters = { filename = "Meters.ogg", duration = 0.59 }, + MetersPerSecond = { filename = "MetersPerSecond.ogg", duration = 1.14 }, + Miles = { filename = "Miles.ogg", duration = 0.60 }, + MillimetersOfMercury = { filename = "MillimetersOfMercury.ogg", duration = 1.53 }, + Minus = { filename = "Minus.ogg", duration = 0.64 }, + N0 = { filename = "N-0.ogg", duration = 0.55 }, + N1 = { filename = "N-1.ogg", duration = 0.41 }, + N2 = { filename = "N-2.ogg", duration = 0.37 }, + N3 = { filename = "N-3.ogg", duration = 0.41 }, + N4 = { filename = "N-4.ogg", duration = 0.37 }, + N5 = { filename = "N-5.ogg", duration = 0.43 }, + N6 = { filename = "N-6.ogg", duration = 0.55 }, + N7 = { filename = "N-7.ogg", duration = 0.43 }, + N8 = { filename = "N-8.ogg", duration = 0.38 }, + N9 = { filename = "N-9.ogg", duration = 0.55 }, + NauticalMiles = { filename = "NauticalMiles.ogg", duration = 1.04 }, + None = { filename = "None.ogg", duration = 0.43 }, + QFE = { filename = "QFE.ogg", duration = 0.63 }, + QNH = { filename = "QNH.ogg", duration = 0.71 }, + Rain = { filename = "Rain.ogg", duration = 0.41 }, + Right = { filename = "Right.ogg", duration = 0.44 }, + Snow = { filename = "Snow.ogg", duration = 0.48 }, + SnowStorm = { filename = "SnowStorm.ogg", duration = 0.82 }, + StatuteMiles = { filename = "StatuteMiles.ogg", duration = 1.15 }, + SunriseAt = { filename = "SunriseAt.ogg", duration = 0.92 }, + SunsetAt = { filename = "SunsetAt.ogg", duration = 0.95 }, + Temperature = { filename = "Temperature.ogg", duration = 0.64 }, + Thousand = { filename = "Thousand.ogg", duration = 0.55 }, + ThunderStorm = { filename = "ThunderStorm.ogg", duration = 0.81 }, + TimeLocal = { filename = "TimeLocal.ogg", duration = 0.90 }, + TimeZulu = { filename = "TimeZulu.ogg", duration = 0.86 }, + TowerFrequency = { filename = "TowerFrequency.ogg", duration = 1.19 }, + Visibilty = { filename = "Visibility.ogg", duration = 0.79 }, + WeatherPhenomena = { filename = "WeatherPhenomena.ogg", duration = 1.07 }, + WindFrom = { filename = "WindFrom.ogg", duration = 0.60 }, + ILSFrequency = { filename = "ILSFrequency.ogg", duration = 1.30 }, + InnerNDBFrequency = { filename = "InnerNDBFrequency.ogg", duration = 1.56 }, + OuterNDBFrequency = { filename = "OuterNDBFrequency.ogg", duration = 1.59 }, + RunwayLength = { filename = "RunwayLength.ogg", duration = 0.91 }, + VORFrequency = { filename = "VORFrequency.ogg", duration = 1.38 }, + TACANChannel = { filename = "TACANChannel.ogg", duration = 0.88 }, + PRMGChannel = { filename = "PRMGChannel.ogg", duration = 1.18 }, + RSBNChannel = { filename = "RSBNChannel.ogg", duration = 1.14 }, + Zulu = { filename = "Zulu.ogg", duration = 0.62 }, } - --- ATIS table containing all defined ATISes. -- @field #table _ATIS -_ATIS={} +_ATIS = {} --- ATIS class version. -- @field #string version -ATIS.version="0.9.6" +ATIS.version = "0.9.14" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Add new Normany airfields. +-- TODO: Add new Normandy airfields. -- TODO: Zulu time --> Zulu in output. -- TODO: Correct fog for elevation. +-- DONE: Use new AIRBASE system to set start/landing runway +-- DONE: SetILS doesn't work +-- DONE: Visibility reported twice over SRS -- DONE: Add text report for output. -- DONE: Add stop FMS functions. -- NOGO: Use local time. Not realisitc! @@ -115934,37 +124690,37 @@ ATIS.version="0.9.6" -- Constructor ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Create a new ATIS class object for a specific aircraft carrier unit. +--- Create a new ATIS class object for a specific airbase. -- @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 + 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 + self.theatre = env.mission.theatre -- Set some string id for output to DCS.log file. - self.lid=string.format("ATIS %s | ", self.airbasename) + self.lid = string.format( "ATIS %s | ", self.airbasename ) -- This is just to hinder the garbage collector deallocating the ATIS object. - _ATIS[#_ATIS+1]=self + _ATIS[#_ATIS + 1] = self -- Defaults: self:SetSoundfilesPath() @@ -115972,13 +124728,14 @@ function ATIS:New(airbasename, frequency, modulation) self:SetMagneticDeclination() self:SetRunwayCorrectionMagnetic2True() self:SetRadioPower() - self:SetAltimeterQNH(true) - self:SetMapMarks(false) + self:SetAltimeterQNH( true ) + self:SetMapMarks( false ) self:SetRelativeHumidity() self:SetQueueUpdateTime() + self:SetReportmBar(false) -- Start State. - self:SetStartState("Stopped") + self:SetStartState( "Stopped" ) -- Add FSM transitions. -- From State --> Event --> To State @@ -116002,7 +124759,6 @@ function ATIS:New(airbasename, frequency, modulation) -- @param #ATIS self -- @param #number delay Delay in seconds. - --- Triggers the FSM event "Stop". Stops the ATIS. -- @function [parent=#ATIS] Stop -- @param #ATIS self @@ -116012,7 +124768,6 @@ function ATIS:New(airbasename, frequency, modulation) -- @param #ATIS self -- @param #number delay Delay in seconds. - --- Triggers the FSM event "Status". -- @function [parent=#ATIS] Status -- @param #ATIS self @@ -116022,7 +124777,6 @@ function ATIS:New(airbasename, frequency, modulation) -- @param #ATIS self -- @param #number delay Delay in seconds. - --- Triggers the FSM event "Broadcast". -- @function [parent=#ATIS] Broadcast -- @param #ATIS self @@ -116032,7 +124786,6 @@ function ATIS:New(airbasename, frequency, modulation) -- @param #ATIS self -- @param #number delay Delay in seconds. - --- Triggers the FSM event "CheckQueue". -- @function [parent=#ATIS] CheckQueue -- @param #ATIS self @@ -116042,7 +124795,6 @@ function ATIS:New(airbasename, frequency, modulation) -- @param #ATIS self -- @param #number delay Delay in seconds. - --- Triggers the FSM event "Report". -- @function [parent=#ATIS] Report -- @param #ATIS self @@ -116062,15 +124814,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 @@ -116082,9 +124825,9 @@ end -- @param #ATIS self -- @param #string path Path for sound files. Default "ATIS Soundfiles/". Mind the slash "/" at the end! -- @return #ATIS self -function ATIS:SetSoundfilesPath(path) - self.soundpath=tostring(path or "ATIS Soundfiles/") - self:I(self.lid..string.format("Setting sound files path to %s", self.soundpath)) +function ATIS:SetSoundfilesPath( path ) + self.soundpath = tostring( path or "ATIS Soundfiles/" ) + self:T( self.lid .. string.format( "Setting sound files path to %s", self.soundpath ) ) return self end @@ -116093,9 +124836,9 @@ end -- @param #ATIS self -- @param #string unitname Name of the unit. -- @return #ATIS self -function ATIS:SetRadioRelayUnitName(unitname) - self.relayunitname=unitname - self:I(self.lid..string.format("Setting radio relay unit to %s", self.relayunitname)) +function ATIS:SetRadioRelayUnitName( unitname ) + self.relayunitname = unitname + self:T( self.lid .. string.format( "Setting radio relay unit to %s", self.relayunitname ) ) return self end @@ -116103,23 +124846,70 @@ end -- @param #ATIS self -- @param #table freqs Table of frequencies in MHz. A single frequency can be given as a plain number (*i.e.* must not be table). -- @return #ATIS self -function ATIS:SetTowerFrequencies(freqs) - if type(freqs)=="table" then +function ATIS:SetTowerFrequencies( freqs ) + if type( freqs ) == "table" then -- nothing to do else - freqs={freqs} + freqs = { freqs } end - self.towerfrequency=freqs + self.towerfrequency = freqs return self end ---- Set active runway. This can be used if the automatic runway determination via the wind direction gives incorrect results. +--- For SRS - Switch to only transmit if there are players on the server. +-- @param #ATIS self +-- @param #boolean Switch If true, only send SRS if there are alive Players. +-- @return #ATIS self +function ATIS:SetTransmitOnlyWithPlayers(Switch) + self.TransmitOnlyWithPlayers = Switch + if self.msrsQ then + self.msrsQ:SetTransmitOnlyWithPlayers(Switch) + end + return self +end + +--- Set active runway for **landing** operations. This can be used if the automatic runway determination via the wind direction gives incorrect results. -- For example, use this if there are two runways with the same directions. -- @param #ATIS self -- @param #string runway Active runway, *e.g.* "31L". -- @return #ATIS self -function ATIS:SetActiveRunway(runway) - self.activerunway=tostring(runway) +function ATIS:SetActiveRunway( runway ) + self.activerunway = tostring( runway ) + local prefer = nil + if string.find(string.lower(runway),"l") then + prefer = true + elseif string.find(string.lower(runway),"r") then + prefer = false + end + self.airbase:SetActiveRunway(runway,prefer) + return self +end + +--- Set the active runway for landing. +-- @param #ATIS self +-- @param #string runway : 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 #ATIS self +function ATIS:SetActiveRunwayLanding(runway, preferleft) + self.airbase:SetActiveRunwayLanding(runway,preferleft) + return self +end + +--- Set the active runway for take-off. +-- @param #ATIS self +-- @param #string runway : 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 #ATIS self +function ATIS:SetActiveRunwayTakeoff(runway,preferleft) + self.airbase:SetActiveRunwayTakeoff(runway,preferleft) + return self +end + +--- Give information on runway length. +-- @param #ATIS self +-- @return #ATIS self +function ATIS:SetRunwayLength() + self.rwylength = true return self end @@ -116131,11 +124921,12 @@ function ATIS:SetRunwayLength() return self end + --- Give information on airfield elevation -- @param #ATIS self -- @return #ATIS self function ATIS:SetElevation() - self.elevation=true + self.elevation = true return self end @@ -116143,8 +124934,8 @@ end -- @param #ATIS self -- @param #number power Radio power in Watts. Default 100 W. -- @return #ATIS self -function ATIS:SetRadioPower(power) - self.power=power or 100 +function ATIS:SetRadioPower( power ) + self.power = power or 100 return self end @@ -116152,59 +124943,66 @@ end -- @param #ATIS self -- @param #boolean switch If *true* or *nil*, marks are placed on F10 map. If *false* this feature is set to off (default). -- @return #ATIS self -function ATIS:SetMapMarks(switch) - if switch==nil or switch==true then - self.usemarker=true +function ATIS:SetMapMarks( switch ) + if switch == nil or switch == true then + self.usemarker = true else - self.usemarker=false + self.usemarker = false end return self end +--- Return the complete SRS Text block, if at least generated once. Else nil. +-- @param #ATIS self +-- @return #string SRSText +function ATIS:GetSRSText() + return self.SRSText +end + --- Set magnetic runway headings as depicted on the runway, *e.g.* "13" for 130° or "25L" for the left runway with magnetic heading 250°. -- @param #ATIS self -- @param #table headings Magnetic headings. Inverse (-180°) headings are added automatically. You only need to specify one heading per runway direction. "L"eft and "R" right can also be appended. -- @return #ATIS self -function ATIS:SetRunwayHeadingsMagnetic(headings) +function ATIS:SetRunwayHeadingsMagnetic( headings ) -- First make sure, we have a table. - if type(headings)=="table" then + if type( headings ) == "table" then -- nothing to do else - headings={headings} + headings = { headings } end - for _,heading in pairs(headings) do + for _, heading in pairs( headings ) do - if type(heading)=="number" then - heading=string.format("%02d", heading) + if type( heading ) == "number" then + heading = string.format( "%02d", heading ) end -- Add runway heading to table. - self:I(self.lid..string.format("Adding user specified magnetic runway heading %s", heading)) - table.insert(self.runwaymag, heading) + self:T( self.lid .. string.format( "Adding user specified magnetic runway heading %s", heading ) ) + table.insert( self.runwaymag, heading ) - local h=self:GetRunwayWithoutLR(heading) + local h = self:GetRunwayWithoutLR( heading ) - local head2=tonumber(h)-18 - if head2<0 then - head2=head2+36 + local head2 = tonumber( h ) - 18 + if head2 < 0 then + head2 = head2 + 36 end -- Convert to string. - head2=string.format("%02d", head2) + head2 = string.format( "%02d", head2 ) -- Append "L" or "R" if necessary. - local left=self:GetRunwayLR(heading) - if left==true then - head2=head2.."L" - elseif left==false then - head2=head2.."R" + local left = self:GetRunwayLR( heading ) + if left == true then + head2 = head2 .. "L" + elseif left == false then + head2 = head2 .. "R" end -- Add inverse runway heading to table. - self:I(self.lid..string.format("Adding user specified magnetic runway heading %s (inverse)", head2)) - table.insert(self.runwaymag, head2) + self:T( self.lid .. string.format( "Adding user specified magnetic runway heading %s (inverse)", head2 ) ) + table.insert( self.runwaymag, head2 ) end return self @@ -116214,8 +125012,8 @@ end -- @param #ATIS self -- @param #number duration Duration in seconds. Default 10 seconds. -- @return #ATIS self -function ATIS:SetSubtitleDuration(duration) - self.subduration=tonumber(duration or 10) +function ATIS:SetSubtitleDuration( duration ) + self.subduration = tonumber( duration or 10 ) return self end @@ -116223,7 +125021,7 @@ end -- @param #ATIS self -- @return #ATIS self function ATIS:SetMetricUnits() - self.metric=true + self.metric = true return self end @@ -116231,7 +125029,7 @@ end -- @param #ATIS self -- @return #ATIS self function ATIS:SetImperialUnits() - self.metric=false + self.metric = false return self end @@ -116240,7 +125038,7 @@ end -- @param #ATIS self -- @return #ATIS self function ATIS:SetPressureMillimetersMercury() - self.PmmHg=true + self.PmmHg = true return self end @@ -116248,7 +125046,7 @@ end -- @param #ATIS self -- @return #ATIS self function ATIS:SetTemperatureFahrenheit() - self.TDegF=true + self.TDegF = true return self end @@ -116257,8 +125055,8 @@ end -- @param #ATIS self -- @param #number Humidity Relative Humidity, i.e. a number between 0 and 100 %. Default is 50 %. -- @return #ATIS self -function ATIS:SetRelativeHumidity(Humidity) - self.relHumidity=Humidity or 50 +function ATIS:SetRelativeHumidity( Humidity ) + self.relHumidity = Humidity or 50 return self end @@ -116266,22 +125064,44 @@ end -- @param #ATIS self -- @param #boolean switch If true or nil, report altimeter QHN. If false, report QFF. -- @return #ATIS self -function ATIS:SetAltimeterQNH(switch) +function ATIS:SetAltimeterQNH( switch ) - if switch==true or switch==nil then - self.altimeterQNH=true + if switch == true or switch == nil then + self.altimeterQNH = true else - self.altimeterQNH=false + self.altimeterQNH = false end return self end +--- Additionally report altimeter QNH/QFE in hPa, even if not set to metric. +-- @param #ATIS self +-- @param #boolean switch If true or nil, report mBar/hPa in addition. +-- @return #ATIS self +function ATIS:SetReportmBar(switch) + if switch == true or switch == nil then + self.ReportmBar = true + else + self.ReportmBar = false + end + return self +end + +--- Additionally report free text, only working with SRS(!) +-- @param #ATIS self +-- @param #string text The text to report at the end of the ATIS message, e.g. runway closure, warnings, etc. +-- @return #ATIS self +function ATIS:SetAdditionalInformation(text) + self.AdditionalInformation = text + return self +end + --- Suppresses QFE readout. Default is to report both QNH and QFE. -- @param #ATIS self -- @return #ATIS self function ATIS:ReportQNHOnly() - self.qnhonly=true + self.qnhonly = true return self end @@ -116300,7 +125120,7 @@ end -- -- * 186° on the Caucaus map -- * 192° on the Nevada map --- * 170° on the Normany map +-- * 170° on the Normandy map -- * 182° on the Persian Gulf map -- -- Likewise, to convert *true* into *magnetic* heading, one has to substract easterly and add westerly variation. @@ -116308,10 +125128,10 @@ end -- Or you make your life simple and just include the sign so you don't have to bother about East/West. -- -- @param #ATIS self --- @param #number magvar Magnetic variation in degrees. Positive for easterly and negative for westerly variation. Default is magnatic declinaton of the used map, c.f. @{Utilities.UTils#UTILS.GetMagneticDeclination}. +-- @param #number magvar Magnetic variation in degrees. Positive for easterly and negative for westerly variation. Default is magnatic declinaton of the used map, c.f. @{Utilities.Utils#UTILS.GetMagneticDeclination}. -- @return #ATIS self -function ATIS:SetMagneticDeclination(magvar) - self.magvar=magvar or UTILS.GetMagneticDeclination() +function ATIS:SetMagneticDeclination( magvar ) + self.magvar = magvar or UTILS.GetMagneticDeclination() return self end @@ -116319,8 +125139,8 @@ end -- @param #ATIS self -- @param #number correction Correction of magnetic to true heading for runways in degrees. -- @return #ATIS self -function ATIS:SetRunwayCorrectionMagnetic2True(correction) - self.runwaym2t=correction or ATIS.RunwayM2T[UTILS.GetDCSMap()] +function ATIS:SetRunwayCorrectionMagnetic2True( correction ) + self.runwaym2t = correction or ATIS.RunwayM2T[UTILS.GetDCSMap()] return self end @@ -116328,7 +125148,7 @@ end -- @param #ATIS self -- @return #ATIS self function ATIS:SetReportWindTrue() - self.windtrue=true + self.windtrue = true return self end @@ -116344,8 +125164,8 @@ end -- @param #ATIS self -- @param #number delta Time difference in hours. -- @return #ATIS self -function ATIS:SetZuluTimeDifference(delta) - self.zuludiff=delta +function ATIS:SetZuluTimeDifference( delta ) + self.zuludiff = delta return self end @@ -116353,7 +125173,7 @@ end -- @param #ATIS self -- @return #ATIS self function ATIS:ReportZuluTimeOnly() - self.zulutimeonly=true + self.zulutimeonly = true return self end @@ -116362,11 +125182,11 @@ end -- @param #number frequency ILS frequency in MHz. -- @param #string runway (Optional) Runway for which the given ILS frequency applies. Default all (*nil*). -- @return #ATIS self -function ATIS:AddILS(frequency, runway) - local ils={} --#ATIS.NavPoint - ils.frequency=tonumber(frequency) - ils.runway=runway and tostring(runway) or nil - table.insert(self.ils, ils) +function ATIS:AddILS( frequency, runway ) + local ils = {} -- #ATIS.NavPoint + ils.frequency = tonumber( frequency ) + ils.runway = runway and tostring( runway ) or nil + table.insert( self.ils, ils ) return self end @@ -116374,8 +125194,8 @@ end -- @param #ATIS self -- @param #number frequency VOR frequency. -- @return #ATIS self -function ATIS:SetVOR(frequency) - self.vor=frequency +function ATIS:SetVOR( frequency ) + self.vor = frequency return self end @@ -116384,11 +125204,11 @@ end -- @param #number frequency NDB frequency in MHz. -- @param #string runway (Optional) Runway for which the given NDB frequency applies. Default all (*nil*). -- @return #ATIS self -function ATIS:AddNDBouter(frequency, runway) - local ndb={} --#ATIS.NavPoint - ndb.frequency=tonumber(frequency) - ndb.runway=runway and tostring(runway) or nil - table.insert(self.ndbouter, ndb) +function ATIS:AddNDBouter( frequency, runway ) + local ndb = {} -- #ATIS.NavPoint + ndb.frequency = tonumber( frequency ) + ndb.runway = runway and tostring( runway ) or nil + table.insert( self.ndbouter, ndb ) return self end @@ -116397,11 +125217,11 @@ end -- @param #number frequency NDB frequency in MHz. -- @param #string runway (Optional) Runway for which the given NDB frequency applies. Default all (*nil*). -- @return #ATIS self -function ATIS:AddNDBinner(frequency, runway) - local ndb={} --#ATIS.NavPoint - ndb.frequency=tonumber(frequency) - ndb.runway=runway and tostring(runway) or nil - table.insert(self.ndbinner, ndb) +function ATIS:AddNDBinner( frequency, runway ) + local ndb = {} -- #ATIS.NavPoint + ndb.frequency = tonumber( frequency ) + ndb.runway = runway and tostring( runway ) or nil + table.insert( self.ndbinner, ndb ) return self end @@ -116409,8 +125229,8 @@ end -- @param #ATIS self -- @param #number channel TACAN channel. -- @return #ATIS self -function ATIS:SetTACAN(channel) - self.tacan=channel +function ATIS:SetTACAN( channel ) + self.tacan = channel return self end @@ -116418,8 +125238,8 @@ end -- @param #ATIS self -- @param #number channel RSBN channel. -- @return #ATIS self -function ATIS:SetRSBN(channel) - self.rsbn=channel +function ATIS:SetRSBN( channel ) + self.rsbn = channel return self end @@ -116428,24 +125248,23 @@ end -- @param #number channel PRMG channel. -- @param #string runway (Optional) Runway for which the given PRMG channel applies. Default all (*nil*). -- @return #ATIS self -function ATIS:AddPRMG(channel, runway) - local ndb={} --#ATIS.NavPoint - ndb.frequency=tonumber(channel) - ndb.runway=runway and tostring(runway) or nil - table.insert(self.prmg, ndb) +function ATIS:AddPRMG( channel, runway ) + local ndb = {} -- #ATIS.NavPoint + ndb.frequency = tonumber( channel ) + ndb.runway = runway and tostring( runway ) or nil + table.insert( self.prmg, ndb ) return self end - --- Place marks with runway data on the F10 map. -- @param #ATIS self -- @param #boolean markall If true, mark all runways of the map. By default only the current ATIS runways are marked. -function ATIS:MarkRunways(markall) - local airbases=AIRBASE.GetAllAirbases() - for _,_airbase in pairs(airbases) do - local airbase=_airbase --Wrapper.Airbase#AIRBASE - if (not markall and airbase:GetName()==self.airbasename) or markall==true then - airbase:GetRunwayData(self.runwaym2t, true) +function ATIS:MarkRunways( markall ) + local airbases = AIRBASE.GetAllAirbases() + for _, _airbase in pairs( airbases ) do + local airbase = _airbase -- Wrapper.Airbase#AIRBASE + if (not markall and airbase:GetName() == self.airbasename) or markall == true then + airbase:GetRunwayData( self.runwaym2t, true ) end end end @@ -116457,17 +125276,26 @@ end -- @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 GoogleKey Path to Google JSON-Key. -- @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) +function ATIS:SetSRS(PathToSRS, Gender, Culture, Voice, Port, GoogleKey) + 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()) + self.msrs:SetLabel("ATIS") + self.msrs:SetGoogle(GoogleKey) + self.msrsQ = MSRSQUEUE:New("ATIS") + self.msrsQ:SetTransmitOnlyWithPlayers(self.TransmitOnlyWithPlayers) + 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 @@ -116476,15 +125304,15 @@ end -- @param #ATIS self -- @param #number TimeInterval Interval in seconds. Default 5 sec. -- @return #ATIS self -function ATIS:SetQueueUpdateTime(TimeInterval) - self.dTQueueCheck=TimeInterval or 5 +function ATIS:SetQueueUpdateTime( TimeInterval ) + self.dTQueueCheck = TimeInterval or 5 end --- Get the coalition of the associated airbase. -- @param #ATIS self -- @return #number Coalition of the associcated airbase. function ATIS:GetCoalition() - local coal=self.airbase and self.airbase:GetCoalition() or nil + local coal = self.airbase and self.airbase:GetCoalition() or nil return coal end @@ -116497,51 +125325,60 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -function ATIS:onafterStart(From, Event, To) +function ATIS:onafterStart( From, Event, To ) -- Check that this is an airdrome. - if self.airbase:GetAirbaseCategory()~=Airbase.Category.AIRDROME then - self:E(self.lid..string.format("ERROR: Cannot start ATIS for airbase %s! Only AIRDROMES are supported but NOT FARPS or SHIPS.", self.airbasename)) + if self.airbase:GetAirbaseCategory() == Airbase.Category.SHIP then + self:E( self.lid .. string.format( "ERROR: Cannot start ATIS for airbase %s! Only AIRDROMES are supported but NOT SHIPS.", self.airbasename ) ) return end - + + -- Check that if is a Helipad. + if self.airbase:GetAirbaseCategory() == Airbase.Category.HELIPAD then + self:E( self.lid .. string.format( "EXPERIMENTAL: Starting ATIS for Helipad %s! SRS must be ON", self.airbasename ) ) + self.ATISforFARPs = true + self.useSRS = true + end + -- Info. - self:I(self.lid..string.format("Starting ATIS v%s for airbase %s on %.3f MHz Modulation=%d", ATIS.version, self.airbasename, self.frequency, self.modulation)) + self:I( self.lid .. string.format( "Starting ATIS v%s for airbase %s on %.3f MHz Modulation=%d", ATIS.version, self.airbasename, self.frequency, self.modulation ) ) -- Start radio queue. - self.radioqueue=RADIOQUEUE:New(self.frequency, self.modulation, string.format("ATIS %s", self.airbasename)) + if not self.useSRS then + self.radioqueue = RADIOQUEUE:New( self.frequency, self.modulation, string.format( "ATIS %s", self.airbasename ) ) - -- Send coordinate is airbase coord. - self.radioqueue:SetSenderCoordinate(self.airbase:GetCoordinate()) + -- Send coordinate is airbase coord. + self.radioqueue:SetSenderCoordinate( self.airbase:GetCoordinate() ) - -- Set relay unit if we have one. - self.radioqueue:SetSenderUnitName(self.relayunitname) + -- Set relay unit if we have one. + self.radioqueue:SetSenderUnitName( self.relayunitname ) - -- Set radio power. - self.radioqueue:SetRadioPower(self.power) - - -- Init numbers. - self.radioqueue:SetDigit(0, ATIS.Sound.N0.filename, ATIS.Sound.N0.duration, self.soundpath) - self.radioqueue:SetDigit(1, ATIS.Sound.N1.filename, ATIS.Sound.N1.duration, self.soundpath) - self.radioqueue:SetDigit(2, ATIS.Sound.N2.filename, ATIS.Sound.N2.duration, self.soundpath) - self.radioqueue:SetDigit(3, ATIS.Sound.N3.filename, ATIS.Sound.N3.duration, self.soundpath) - self.radioqueue:SetDigit(4, ATIS.Sound.N4.filename, ATIS.Sound.N4.duration, self.soundpath) - self.radioqueue:SetDigit(5, ATIS.Sound.N5.filename, ATIS.Sound.N5.duration, self.soundpath) - self.radioqueue:SetDigit(6, ATIS.Sound.N6.filename, ATIS.Sound.N6.duration, self.soundpath) - self.radioqueue:SetDigit(7, ATIS.Sound.N7.filename, ATIS.Sound.N7.duration, self.soundpath) - self.radioqueue:SetDigit(8, ATIS.Sound.N8.filename, ATIS.Sound.N8.duration, self.soundpath) - self.radioqueue:SetDigit(9, ATIS.Sound.N9.filename, ATIS.Sound.N9.duration, self.soundpath) + -- Set radio power. + self.radioqueue:SetRadioPower( self.power ) + + -- Init numbers. + self.radioqueue:SetDigit( 0, ATIS.Sound.N0.filename, ATIS.Sound.N0.duration, self.soundpath ) + self.radioqueue:SetDigit( 1, ATIS.Sound.N1.filename, ATIS.Sound.N1.duration, self.soundpath ) + self.radioqueue:SetDigit( 2, ATIS.Sound.N2.filename, ATIS.Sound.N2.duration, self.soundpath ) + self.radioqueue:SetDigit( 3, ATIS.Sound.N3.filename, ATIS.Sound.N3.duration, self.soundpath ) + self.radioqueue:SetDigit( 4, ATIS.Sound.N4.filename, ATIS.Sound.N4.duration, self.soundpath ) + self.radioqueue:SetDigit( 5, ATIS.Sound.N5.filename, ATIS.Sound.N5.duration, self.soundpath ) + self.radioqueue:SetDigit( 6, ATIS.Sound.N6.filename, ATIS.Sound.N6.duration, self.soundpath ) + self.radioqueue:SetDigit( 7, ATIS.Sound.N7.filename, ATIS.Sound.N7.duration, self.soundpath ) + self.radioqueue:SetDigit( 8, ATIS.Sound.N8.filename, ATIS.Sound.N8.duration, self.soundpath ) + self.radioqueue:SetDigit( 9, ATIS.Sound.N9.filename, ATIS.Sound.N9.duration, self.soundpath ) + + -- Start radio queue. + self.radioqueue:Start( 1, 0.1 ) + end - -- Start radio queue. - self.radioqueue:Start(1, 0.1) - -- Handle airbase capture -- Handle events. - self:HandleEvent(EVENTS.BaseCaptured) + self:HandleEvent( EVENTS.BaseCaptured ) -- Init status updates. - self:__Status(-2) - self:__CheckQueue(-3) + self:__Status( -2 ) + self:__CheckQueue( -3 ) end --- Update status. @@ -116549,30 +125386,31 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -function ATIS:onafterStatus(From, Event, To) +function ATIS:onafterStatus( From, Event, To ) -- Get FSM state. - local fsmstate=self:GetState() + local fsmstate = self:GetState() - local relayunitstatus="N/A" + local relayunitstatus = "N/A" if self.relayunitname then - local ru=UNIT:FindByName(self.relayunitname) + local ru = UNIT:FindByName( self.relayunitname ) if ru then - relayunitstatus=tostring(ru:IsAlive()) + relayunitstatus = tostring( ru:IsAlive() ) end end - -- Info text. - local text=string.format("State %s: Freq=%.3f MHz %s", fsmstate, self.frequency, UTILS.GetModulationName(self.modulation)) + -- Info text. + local text = string.format( "State %s: Freq=%.3f MHz %s", fsmstate, self.frequency, UTILS.GetModulationName( self.modulation ) ) if self.useSRS then - text=text..string.format(", SRS path=%s (%s), gender=%s, culture=%s, voice=%s", - tostring(self.msrs.path), tostring(self.msrs.port), tostring(self.msrs.gender), tostring(self.msrs.culture), tostring(self.msrs.voice)) + text = text .. string.format( ", SRS path=%s (%s), gender=%s, culture=%s, voice=%s", tostring( self.msrs.path ), tostring( self.msrs.port ), tostring( self.msrs.gender ), tostring( self.msrs.culture ), tostring( self.msrs.voice ) ) else - text=text..string.format(", Relay unit=%s (alive=%s)", tostring(self.relayunitname), relayunitstatus) + text = text .. string.format( ", Relay unit=%s (alive=%s)", tostring( self.relayunitname ), relayunitstatus ) + end + self:T( self.lid .. text ) + + if not self:Is("Stopped") then + self:__Status( -60 ) end - self:I(self.lid..text) - - self:__Status(-60) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -116584,27 +125422,27 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -function ATIS:onafterCheckQueue(From, Event, To) +function ATIS:onafterCheckQueue( From, Event, To ) if self.useSRS then - + self:Broadcast() - + else - - if #self.radioqueue.queue==0 then - self:T(self.lid..string.format("Radio queue empty. Repeating message.")) + + if #self.radioqueue.queue == 0 then + self:T( self.lid .. string.format( "Radio queue empty. Repeating message." ) ) self:Broadcast() else - self:T2(self.lid..string.format("Radio queue %d transmissions queued.", #self.radioqueue.queue)) + self:T2( self.lid .. string.format( "Radio queue %d transmissions queued.", #self.radioqueue.queue ) ) end + + end - - + if not self:Is("Stopped") then + -- Check back in 5 seconds. + self:__CheckQueue( -math.abs( self.dTQueueCheck ) ) end - - -- Check back in 5 seconds. - self:__CheckQueue(-math.abs(self.dTQueueCheck)) end --- Broadcast ATIS radio message. @@ -116612,69 +125450,70 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -function ATIS:onafterBroadcast(From, Event, To) +function ATIS:onafterBroadcast( From, Event, To ) -- Get current coordinate. - local coord=self.airbase:GetCoordinate() + local coord = self.airbase:GetCoordinate() -- Get elevation. - local height=coord:GetLandHeight() + local height = coord:GetLandHeight() ---------------- --- Pressure --- ---------------- -- Pressure in hPa. - local qfe=coord:GetPressure(height) - local qnh=coord:GetPressure(0) + local qfe = coord:GetPressure( height ) + local qnh = coord:GetPressure( 0 ) if self.altimeterQNH then -- Some constants. - local L=-0.0065 --[K/m] - local R= 8.31446 --[J/mol/K] - local g= 9.80665 --[m/s^2] - local M= 0.0289644 --[kg/mol] - local T0=coord:GetTemperature(0)+273.15 --[K] Temp at sea level. - local TS=288.15 -- Standard Temperature assumed by Altimeter is 15°C - local q=qnh*100 + local L = -0.0065 -- [K/m] + local R = 8.31446 -- [J/mol/K] + local g = 9.80665 -- [m/s^2] + local M = 0.0289644 -- [kg/mol] + local T0 = coord:GetTemperature( 0 ) + 273.15 -- [K] Temp at sea level. + local TS = 288.15 -- Standard Temperature assumed by Altimeter is 15°C + local q = qnh * 100 -- Calculate Pressure. - local P=q*(1+L*height/T0)^(-g*M/(R*L)) -- Pressure at sea level - local Q=P/(1+L*height/TS)^(-g*M/(R*L)) -- Altimeter QNH - local A=(T0/L)*((P/q)^(((-R*L)/(g*M)))-1) -- Altitude check - + local P = q * (1 + L * height / T0) ^ (-g * M / (R * L)) -- Pressure at sea level + local Q = P / (1 + L * height / TS) ^ (-g * M / (R * L)) -- Altimeter QNH + local A = (T0 / L) * ((P / q) ^ (((-R * L) / (g * M))) - 1) -- Altitude check -- Debug aoutput - self:T2(self.lid..string.format("height=%.1f, A=%.1f, T0=%.1f, QFE=%.1f, QNH=%.1f, P=%.1f, Q=%.1f hPa = %.2f", height, A, T0-273.15, qfe, qnh, P/100, Q/100, UTILS.hPa2inHg(Q/100))) + self:T2( self.lid .. string.format( "height=%.1f, A=%.1f, T0=%.1f, QFE=%.1f, QNH=%.1f, P=%.1f, Q=%.1f hPa = %.2f", height, A, T0 - 273.15, qfe, qnh, P / 100, Q / 100, UTILS.hPa2inHg( Q / 100 ) ) ) -- Set QNH value in hPa. - qnh=Q/100 + qnh = Q / 100 end + local mBarqnh = qnh + local mBarqfe = qfe -- Convert to inHg. if self.PmmHg then - qfe=UTILS.hPa2mmHg(qfe) - qnh=UTILS.hPa2mmHg(qnh) + qfe = UTILS.hPa2mmHg( qfe ) + qnh = UTILS.hPa2mmHg( qnh ) else if not self.metric then - qfe=UTILS.hPa2inHg(qfe) - qnh=UTILS.hPa2inHg(qnh) + qfe = UTILS.hPa2inHg( qfe ) + qnh = UTILS.hPa2inHg( qnh ) end end - local QFE=UTILS.Split(string.format("%.2f", qfe), ".") - local QNH=UTILS.Split(string.format("%.2f", qnh), ".") + local QFE = UTILS.Split( string.format( "%.2f", qfe ), "." ) + local QNH = UTILS.Split( string.format( "%.2f", qnh ), "." ) if self.PmmHg then - QFE=UTILS.Split(string.format("%.1f", qfe), ".") - QNH=UTILS.Split(string.format("%.1f", qnh), ".") + QFE = UTILS.Split( string.format( "%.1f", qfe ), "." ) + QNH = UTILS.Split( string.format( "%.1f", qnh ), "." ) else if self.metric then - QFE=UTILS.Split(string.format("%.1f", qfe), ".") - QNH=UTILS.Split(string.format("%.1f", qnh), ".") + QFE = UTILS.Split( string.format( "%.1f", qfe ), "." ) + QNH = UTILS.Split( string.format( "%.1f", qnh ), "." ) end end @@ -116683,126 +125522,134 @@ function ATIS:onafterBroadcast(From, Event, To) ------------ -- Get wind direction and speed in m/s. - local windFrom, windSpeed=coord:GetWind(height+10) + local windFrom, windSpeed = coord:GetWind( height + 10 ) -- Wind in magnetic or true. - local magvar=self.magvar + local magvar = self.magvar if self.windtrue then - magvar=0 + magvar = 0 end - windFrom=windFrom-magvar - + windFrom = windFrom - magvar + -- Correct negative values. - if windFrom<0 then - windFrom=windFrom+360 + if windFrom < 0 then + windFrom = windFrom + 360 end - local WINDFROM=string.format("%03d", windFrom) - local WINDSPEED=string.format("%d", UTILS.MpsToKnots(windSpeed)) - + local WINDFROM = string.format( "%03d", windFrom ) + local WINDSPEED = string.format( "%d", UTILS.MpsToKnots( windSpeed ) ) + -- Report North as 0. - if WINDFROM=="000" then - WINDFROM="360" + if WINDFROM == "000" then + WINDFROM = "360" end if self.metric then - WINDSPEED=string.format("%d", windSpeed) + WINDSPEED = string.format( "%d", windSpeed ) end -------------- --- Runway --- -------------- - - local runway, rwyLeft=self:GetActiveRunway() - + + + local runwayLanding, rwyLandingLeft + local runwayTakeoff, rwyTakeoffLeft + + if self.airbase:GetAirbaseCategory() == Airbase.Category.HELIPAD then + runwayLanding, rwyLandingLeft="PAD 01",false + runwayTakeoff, rwyTakeoffLeft="PAD 02",false + else + runwayLanding, rwyLandingLeft=self:GetActiveRunway() + runwayTakeoff, rwyTakeoffLeft=self:GetActiveRunway(true) + end + ------------ --- Time --- ------------ - local time=timer.getAbsTime() + local time = timer.getAbsTime() -- Conversion to Zulu time. if self.zuludiff then -- User specified. - time=time-self.zuludiff*60*60 + time = time - self.zuludiff * 60 * 60 else - time=time-UTILS.GMTToLocalTimeDifference()*60*60 + time = time - UTILS.GMTToLocalTimeDifference() * 60 * 60 end if time < 0 then - time = 24*60*60 + time --avoid negative time around midnight - end - - local clock=UTILS.SecondsToClock(time) - local zulu=UTILS.Split(clock, ":") - local ZULU=string.format("%s%s", zulu[1], zulu[2]) - if self.useSRS then - ZULU=string.format("%s hours", zulu[1]) + time = 24 * 60 * 60 + time -- avoid negative time around midnight end + local clock = UTILS.SecondsToClock( time ) + local zulu = UTILS.Split( clock, ":" ) + local ZULU = string.format( "%s%s", zulu[1], zulu[2] ) + if self.useSRS then + ZULU = string.format( "%s hours", zulu[1] ) + end -- NATO time stamp. 0=Alfa, 1=Bravo, 2=Charlie, etc. - local NATO=ATIS.Alphabet[tonumber(zulu[1])+1] + local NATO = ATIS.Alphabet[tonumber( zulu[1] ) + 1] -- Debug. - self:T3(string.format("clock=%s", tostring(clock))) - self:T3(string.format("zulu1=%s", tostring(zulu[1]))) - self:T3(string.format("zulu2=%s", tostring(zulu[2]))) - self:T3(string.format("ZULU =%s", tostring(ZULU))) - self:T3(string.format("NATO =%s", tostring(NATO))) + self:T3( string.format( "clock=%s", tostring( clock ) ) ) + self:T3( string.format( "zulu1=%s", tostring( zulu[1] ) ) ) + self:T3( string.format( "zulu2=%s", tostring( zulu[2] ) ) ) + self:T3( string.format( "ZULU =%s", tostring( ZULU ) ) ) + self:T3( string.format( "NATO =%s", tostring( NATO ) ) ) -------------------------- --- Sunrise and Sunset --- -------------------------- - local sunrise=coord:GetSunrise() - sunrise=UTILS.Split(sunrise, ":") - local SUNRISE=string.format("%s%s", sunrise[1], sunrise[2]) + local sunrise = coord:GetSunrise() + sunrise = UTILS.Split( sunrise, ":" ) + local SUNRISE = string.format( "%s%s", sunrise[1], sunrise[2] ) if self.useSRS then - SUNRISE=string.format("%s %s hours", sunrise[1], sunrise[2]) - end + SUNRISE = string.format( "%s %s hours", sunrise[1], sunrise[2] ) + end - local sunset=coord:GetSunset() - sunset=UTILS.Split(sunset, ":") - local SUNSET=string.format("%s%s", sunset[1], sunset[2]) + local sunset = coord:GetSunset() + sunset = UTILS.Split( sunset, ":" ) + local SUNSET = string.format( "%s%s", sunset[1], sunset[2] ) if self.useSRS then - SUNSET=string.format("%s %s hours", sunset[1], sunset[2]) - end - + SUNSET = string.format( "%s %s hours", sunset[1], sunset[2] ) + end --------------------------------- --- Temperature and Dew Point --- --------------------------------- -- Temperature in °C. - local temperature=coord:GetTemperature(height+5) - + local temperature = coord:GetTemperature( height + 5 ) + -- Dew point in °C. - local dewpoint=temperature-(100-self.relHumidity)/5 + local dewpoint = temperature - (100 - self.relHumidity) / 5 -- 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)) - local DEWPOINT=string.format("%d", math.abs(dewpoint)) + local TEMPERATURE = string.format( "%d", math.abs( temperature ) ) + local DEWPOINT = string.format( "%d", math.abs( dewpoint ) ) --------------- --- Weather --- --------------- -- Get mission weather info. Most of this is static. - local clouds, visibility, turbulence, fog, dust, static=self:GetMissionWeather() + local clouds, visibility, turbulence, fog, dust, static = self:GetMissionWeather() -- Check that fog is actually "thick" enough to reach the airport. If an airport is in the mountains, fog might not affect it as it is measured from sea level. - if fog and fog.thicknessUTILS.FeetToMeters(1500) then - dust=nil + if dust and height + 25 > UTILS.FeetToMeters( 1500 ) then + dust = nil end ------------------ @@ -116810,214 +125657,214 @@ function ATIS:onafterBroadcast(From, Event, To) ------------------ -- Get min visibility. - local visibilitymin=visibility + local visibilitymin = visibility if fog then - if fog.visibility 10 then - reportedviz=10 + reportedviz = 10 end - VISIBILITY=string.format("%d", reportedviz) + VISIBILITY = string.format( "%d", reportedviz ) else -- max reported visibility 10 NM - local reportedviz=UTILS.Round(UTILS.MetersToSM(visibilitymin)) + local reportedviz = UTILS.Round( UTILS.MetersToSM( visibilitymin ) ) if reportedviz > 10 then - reportedviz=10 + reportedviz = 10 end - VISIBILITY=string.format("%d", reportedviz) + VISIBILITY = string.format( "%d", reportedviz ) end -------------- --- Clouds --- -------------- - local cloudbase=clouds.base - local cloudceil=clouds.base+clouds.thickness - local clouddens=clouds.density + local cloudbase = clouds.base + local cloudceil = clouds.base + clouds.thickness + local clouddens = clouds.density -- Cloud preset (DCS 2.7) - local cloudspreset=clouds.preset or "Nothing" - + local cloudspreset = clouds.preset or "Nothing" + -- Precepitation: 0=None, 1=Rain, 2=Thunderstorm, 3=Snow, 4=Snowstorm. - local precepitation=0 + local precepitation = 0 - if cloudspreset:find("Preset10") then + if cloudspreset:find( "Preset10" ) then -- Scattered 5 - clouddens=4 - elseif cloudspreset:find("Preset11") then + clouddens = 4 + elseif cloudspreset:find( "Preset11" ) then -- Scattered 6 - clouddens=4 - elseif cloudspreset:find("Preset12") then + clouddens = 4 + elseif cloudspreset:find( "Preset12" ) then -- Scattered 7 - clouddens=4 - elseif cloudspreset:find("Preset13") then + clouddens = 4 + elseif cloudspreset:find( "Preset13" ) then -- Broken 1 - clouddens=7 - elseif cloudspreset:find("Preset14") then + clouddens = 7 + elseif cloudspreset:find( "Preset14" ) then -- Broken 2 - clouddens=7 - elseif cloudspreset:find("Preset15") then + clouddens = 7 + elseif cloudspreset:find( "Preset15" ) then -- Broken 3 - clouddens=7 - elseif cloudspreset:find("Preset16") then + clouddens = 7 + elseif cloudspreset:find( "Preset16" ) then -- Broken 4 - clouddens=7 - elseif cloudspreset:find("Preset17") then + clouddens = 7 + elseif cloudspreset:find( "Preset17" ) then -- Broken 5 - clouddens=7 - elseif cloudspreset:find("Preset18") then + clouddens = 7 + elseif cloudspreset:find( "Preset18" ) then -- Broken 6 - clouddens=7 - elseif cloudspreset:find("Preset19") then + clouddens = 7 + elseif cloudspreset:find( "Preset19" ) then -- Broken 7 - clouddens=7 - elseif cloudspreset:find("Preset20") then + clouddens = 7 + elseif cloudspreset:find( "Preset20" ) then -- Broken 8 - clouddens=7 - elseif cloudspreset:find("Preset21") then + clouddens = 7 + elseif cloudspreset:find( "Preset21" ) then -- Overcast 1 - clouddens=9 - elseif cloudspreset:find("Preset22") then + clouddens = 9 + elseif cloudspreset:find( "Preset22" ) then -- Overcast 2 - clouddens=9 - elseif cloudspreset:find("Preset23") then + clouddens = 9 + elseif cloudspreset:find( "Preset23" ) then -- Overcast 3 - clouddens=9 - elseif cloudspreset:find("Preset24") then + clouddens = 9 + elseif cloudspreset:find( "Preset24" ) then -- Overcast 4 - clouddens=9 - elseif cloudspreset:find("Preset25") then + clouddens = 9 + elseif cloudspreset:find( "Preset25" ) then -- Overcast 5 - clouddens=9 - elseif cloudspreset:find("Preset26") then + clouddens = 9 + elseif cloudspreset:find( "Preset26" ) then -- Overcast 6 - clouddens=9 - elseif cloudspreset:find("Preset27") then + clouddens = 9 + elseif cloudspreset:find( "Preset27" ) then -- Overcast 7 - clouddens=9 - elseif cloudspreset:find("Preset1") then + clouddens = 9 + elseif cloudspreset:find( "Preset1" ) then -- Light Scattered 1 - clouddens=1 - elseif cloudspreset:find("Preset2") then + clouddens = 1 + elseif cloudspreset:find( "Preset2" ) then -- Light Scattered 2 - clouddens=1 - elseif cloudspreset:find("Preset3") then + clouddens = 1 + elseif cloudspreset:find( "Preset3" ) then -- High Scattered 1 - clouddens=4 - elseif cloudspreset:find("Preset4") then + clouddens = 4 + elseif cloudspreset:find( "Preset4" ) then -- High Scattered 2 - clouddens=4 - elseif cloudspreset:find("Preset5") then + clouddens = 4 + elseif cloudspreset:find( "Preset5" ) then -- Scattered 1 - clouddens=4 - elseif cloudspreset:find("Preset6") then + clouddens = 4 + elseif cloudspreset:find( "Preset6" ) then -- Scattered 2 - clouddens=4 - elseif cloudspreset:find("Preset7") then + clouddens = 4 + elseif cloudspreset:find( "Preset7" ) then -- Scattered 3 - clouddens=4 - elseif cloudspreset:find("Preset8") then + clouddens = 4 + elseif cloudspreset:find( "Preset8" ) then -- High Scattered 3 - clouddens=4 - elseif cloudspreset:find("Preset9") then + clouddens = 4 + elseif cloudspreset:find( "Preset9" ) then -- Scattered 4 - clouddens=4 - elseif cloudspreset:find("RainyPreset") then - -- Overcast + Rain - clouddens=9 - if temperature>5 then - precepitation=1 -- rain + clouddens = 4 + elseif cloudspreset:find( "RainyPreset" ) then + -- Overcast + Rain + clouddens = 9 + if temperature > 5 then + precepitation = 1 -- rain else - precepitation=3 -- snow + precepitation = 3 -- snow end - elseif cloudspreset:find("RainyPreset1") then - -- Overcast + Rain - clouddens=9 - if temperature>5 then - precepitation=1 -- rain + elseif cloudspreset:find( "RainyPreset1" ) then + -- Overcast + Rain + clouddens = 9 + if temperature > 5 then + precepitation = 1 -- rain else - precepitation=3 -- snow - end - elseif cloudspreset:find("RainyPreset2") then - -- Overcast + Rain - clouddens=9 - if temperature>5 then - precepitation=1 -- rain + precepitation = 3 -- snow + end + elseif cloudspreset:find( "RainyPreset2" ) then + -- Overcast + Rain + clouddens = 9 + if temperature > 5 then + precepitation = 1 -- rain else - precepitation=3 -- snow + precepitation = 3 -- snow end - elseif cloudspreset:find("RainyPreset3") then + elseif cloudspreset:find( "RainyPreset3" ) then -- Overcast + Rain - clouddens=9 - if temperature>5 then - precepitation=1 -- rain + clouddens = 9 + if temperature > 5 then + precepitation = 1 -- rain else - precepitation=3 -- snow + precepitation = 3 -- snow end end - - local CLOUDBASE=string.format("%d", UTILS.MetersToFeet(cloudbase)) - local CLOUDCEIL=string.format("%d", UTILS.MetersToFeet(cloudceil)) + + local CLOUDBASE = string.format( "%d", UTILS.MetersToFeet( cloudbase ) ) + local CLOUDCEIL = string.format( "%d", UTILS.MetersToFeet( cloudceil ) ) if self.metric then - CLOUDBASE=string.format("%d", cloudbase) - CLOUDCEIL=string.format("%d", cloudceil) + CLOUDBASE = string.format( "%d", cloudbase ) + CLOUDCEIL = string.format( "%d", cloudceil ) end -- Cloud base/ceiling in thousands and hundrets of ft/meters. - local CLOUDBASE1000, CLOUDBASE0100=self:_GetThousandsAndHundreds(UTILS.MetersToFeet(cloudbase)) - local CLOUDCEIL1000, CLOUDCEIL0100=self:_GetThousandsAndHundreds(UTILS.MetersToFeet(cloudceil)) + local CLOUDBASE1000, CLOUDBASE0100 = self:_GetThousandsAndHundreds( UTILS.MetersToFeet( cloudbase ) ) + local CLOUDCEIL1000, CLOUDCEIL0100 = self:_GetThousandsAndHundreds( UTILS.MetersToFeet( cloudceil ) ) if self.metric then - CLOUDBASE1000, CLOUDBASE0100=self:_GetThousandsAndHundreds(cloudbase) - CLOUDCEIL1000, CLOUDCEIL0100=self:_GetThousandsAndHundreds(cloudceil) + CLOUDBASE1000, CLOUDBASE0100 = self:_GetThousandsAndHundreds( cloudbase ) + CLOUDCEIL1000, CLOUDCEIL0100 = self:_GetThousandsAndHundreds( cloudceil ) end -- No cloud info for dynamic weather. - local CloudCover={} --#ATIS.Soundfile - CloudCover=ATIS.Sound.CloudsNotAvailable - local CLOUDSsub="Cloud coverage information not available" + local CloudCover = {} -- #ATIS.Soundfile + CloudCover = ATIS.Sound.CloudsNotAvailable + local CLOUDSsub = "Cloud coverage information not available" -- Only valid for static weather. if static then - if clouddens>=9 then + if clouddens >= 9 then -- Overcast 9,10 - CloudCover=ATIS.Sound.CloudsOvercast - CLOUDSsub="Overcast" - elseif clouddens>=7 then + CloudCover = ATIS.Sound.CloudsOvercast + CLOUDSsub = "Overcast" + elseif clouddens >= 7 then -- Broken 7,8 - CloudCover=ATIS.Sound.CloudsBroken - CLOUDSsub="Broken clouds" - elseif clouddens>=4 then + CloudCover = ATIS.Sound.CloudsBroken + CLOUDSsub = "Broken clouds" + elseif clouddens >= 4 then -- Scattered 4,5,6 - CloudCover=ATIS.Sound.CloudsScattered - CLOUDSsub="Scattered clouds" - elseif clouddens>=1 then + CloudCover = ATIS.Sound.CloudsScattered + CLOUDSsub = "Scattered clouds" + elseif clouddens >= 1 then -- Few 1,2,3 - CloudCover=ATIS.Sound.CloudsFew - CLOUDSsub="Few clouds" + CloudCover = ATIS.Sound.CloudsFew + CLOUDSsub = "Few clouds" else -- No clouds - CLOUDBASE=nil - CLOUDCEIL=nil - CloudCover=ATIS.Sound.CloudsNo - CLOUDSsub="No clouds" + CLOUDBASE = nil + CLOUDCEIL = nil + CloudCover = ATIS.Sound.CloudsNo + CLOUDSsub = "No clouds" end end @@ -117026,558 +125873,579 @@ function ATIS:onafterBroadcast(From, Event, To) -------------------- -- Subtitle - local subtitle="" + local subtitle = "" - --Airbase name - subtitle=string.format("%s", self.airbasename) - if self.airbasename:find("AFB")==nil and self.airbasename:find("Airport")==nil and self.airbasename:find("Airstrip")==nil and self.airbasename:find("airfield")==nil and self.airbasename:find("AB")==nil then - subtitle=subtitle.." Airport" + -- Airbase name + subtitle = string.format( "%s", self.airbasename ) + if (not self.ATISforFARPs) and self.airbasename:find( "AFB" ) == nil and self.airbasename:find( "Airport" ) == nil and self.airbasename:find( "Airstrip" ) == nil and self.airbasename:find( "airfield" ) == nil and self.airbasename:find( "AB" ) == nil then + subtitle = subtitle .. " Airport" end if not self.useSRS then - self.radioqueue:NewTransmission(string.format("%s/%s.ogg", self.theatre, self.airbasename), 3.0, self.soundpath, nil, nil, subtitle, self.subduration) + self.radioqueue:NewTransmission( string.format( "%s/%s.ogg", self.theatre, self.airbasename ), 3.0, self.soundpath, nil, nil, subtitle, self.subduration ) end - local alltext=subtitle + local alltext = subtitle -- Information tag - subtitle=string.format("Information %s", NATO) - local _INFORMATION=subtitle + subtitle = string.format( "Information %s", NATO ) + local _INFORMATION = subtitle if not self.useSRS then - self:Transmission(ATIS.Sound.Information, 0.5, subtitle) - self.radioqueue:NewTransmission(string.format("NATO Alphabet/%s.ogg", NATO), 0.75, self.soundpath) + self:Transmission( ATIS.Sound.Information, 0.5, subtitle ) + self.radioqueue:NewTransmission( string.format( "NATO Alphabet/%s.ogg", NATO ), 0.75, self.soundpath ) end - alltext=alltext..";\n"..subtitle + alltext = alltext .. ";\n" .. subtitle -- Zulu Time - subtitle=string.format("%s Zulu", ZULU) + subtitle = string.format( "%s Zulu", ZULU ) if not self.useSRS then - self.radioqueue:Number2Transmission(ZULU, nil, 0.5) - self:Transmission(ATIS.Sound.Zulu, 0.2, subtitle) + self.radioqueue:Number2Transmission( ZULU, nil, 0.5 ) + self:Transmission( ATIS.Sound.Zulu, 0.2, subtitle ) end - alltext=alltext..";\n"..subtitle - + alltext = alltext .. ";\n" .. subtitle + if not self.zulutimeonly then -- Sunrise Time - subtitle=string.format("Sunrise at %s local time", SUNRISE) + subtitle = string.format( "Sunrise at %s local time", SUNRISE ) if not self.useSRS then - self:Transmission(ATIS.Sound.SunriseAt, 0.5, subtitle) - self.radioqueue:Number2Transmission(SUNRISE, nil, 0.2) - self:Transmission(ATIS.Sound.TimeLocal, 0.2) + self:Transmission( ATIS.Sound.SunriseAt, 0.5, subtitle ) + self.radioqueue:Number2Transmission( SUNRISE, nil, 0.2 ) + self:Transmission( ATIS.Sound.TimeLocal, 0.2 ) end - alltext=alltext..";\n"..subtitle - + alltext = alltext .. ";\n" .. subtitle + -- Sunset Time - subtitle=string.format("Sunset at %s local time", SUNSET) + subtitle = string.format( "Sunset at %s local time", SUNSET ) if not self.useSRS then - self:Transmission(ATIS.Sound.SunsetAt, 0.5, subtitle) - self.radioqueue:Number2Transmission(SUNSET, nil, 0.5) - self:Transmission(ATIS.Sound.TimeLocal, 0.2) + self:Transmission( ATIS.Sound.SunsetAt, 0.5, subtitle ) + self.radioqueue:Number2Transmission( SUNSET, nil, 0.5 ) + self:Transmission( ATIS.Sound.TimeLocal, 0.2 ) end - alltext=alltext..";\n"..subtitle + alltext = alltext .. ";\n" .. subtitle 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) + subtitle = string.format( "Wind from %s at %s m/s", WINDFROM, WINDSPEED ) else - subtitle=string.format("Wind from %s at %s knots", WINDFROM, WINDSPEED) + subtitle = string.format( "Wind from %s at %s knots", WINDFROM, WINDSPEED ) end - if turbulence>0 then - subtitle=subtitle..", gusting" + if turbulence > 0 then + subtitle = subtitle .. ", gusting" end - local _WIND=subtitle + local _WIND = subtitle if not self.useSRS then - self:Transmission(ATIS.Sound.WindFrom, 1.0, subtitle) - self.radioqueue:Number2Transmission(WINDFROM) - self:Transmission(ATIS.Sound.At, 0.2) - self.radioqueue:Number2Transmission(WINDSPEED) + self:Transmission( ATIS.Sound.WindFrom, 1.0, subtitle ) + self.radioqueue:Number2Transmission( WINDFROM ) + self:Transmission( ATIS.Sound.At, 0.2 ) + self.radioqueue:Number2Transmission( WINDSPEED ) if self.metric then - self:Transmission(ATIS.Sound.MetersPerSecond, 0.2) + self:Transmission( ATIS.Sound.MetersPerSecond, 0.2 ) else - self:Transmission(ATIS.Sound.Knots, 0.2) + self:Transmission( ATIS.Sound.Knots, 0.2 ) end - if turbulence>0 then - self:Transmission(ATIS.Sound.Gusting, 0.2) + if turbulence > 0 then + self:Transmission( ATIS.Sound.Gusting, 0.2 ) end end - alltext=alltext..";\n"..subtitle - + alltext = alltext .. ";\n" .. subtitle + -- Visibility if self.metric then - subtitle=string.format("Visibility %s km", VISIBILITY) + subtitle = string.format( "Visibility %s km", VISIBILITY ) else - subtitle=string.format("Visibility %s SM", VISIBILITY) + subtitle = string.format( "Visibility %s SM", VISIBILITY ) end if not self.useSRS then - self:Transmission(ATIS.Sound.Visibilty, 1.0, subtitle) - self.radioqueue:Number2Transmission(VISIBILITY) + self:Transmission( ATIS.Sound.Visibilty, 1.0, subtitle ) + self.radioqueue:Number2Transmission( VISIBILITY ) if self.metric then - self:Transmission(ATIS.Sound.Kilometers, 0.2) + self:Transmission( ATIS.Sound.Kilometers, 0.2 ) else - self:Transmission(ATIS.Sound.StatuteMiles, 0.2) + self:Transmission( ATIS.Sound.StatuteMiles, 0.2 ) end end - alltext=alltext..";\n"..subtitle + alltext = alltext .. ";\n" .. subtitle + subtitle = "" -- Weather phenomena - local wp=false - local wpsub="" - if precepitation==1 then - wp=true - wpsub=wpsub.." rain" - elseif precepitation==2 then + local wp = false + local wpsub = "" + if precepitation == 1 then + wp = true + wpsub = wpsub .. " rain" + elseif precepitation == 2 then if wp then - wpsub=wpsub.."," + wpsub = wpsub .. "," end - wpsub=wpsub.." thunderstorm" - wp=true - elseif precepitation==3 then - wpsub=wpsub.." snow" - wp=true - elseif precepitation==4 then - wpsub=wpsub.." snowstorm" - wp=true + wpsub = wpsub .. " thunderstorm" + wp = true + elseif precepitation == 3 then + wpsub = wpsub .. " snow" + wp = true + elseif precepitation == 4 then + wpsub = wpsub .. " snowstorm" + wp = true end if fog then if wp then - wpsub=wpsub.."," + wpsub = wpsub .. "," end - wpsub=wpsub.." fog" - wp=true + wpsub = wpsub .. " fog" + wp = true end if dust then if wp then - wpsub=wpsub.."," + wpsub = wpsub .. "," end - wpsub=wpsub.." dust" - wp=true + wpsub = wpsub .. " dust" + wp = true end -- Actual output if wp then - subtitle=string.format("Weather phenomena:%s", wpsub) + subtitle = string.format( "Weather phenomena:%s", wpsub ) if not self.useSRS then - self:Transmission(ATIS.Sound.WeatherPhenomena, 1.0, subtitle) - if precepitation==1 then - self:Transmission(ATIS.Sound.Rain, 0.5) - elseif precepitation==2 then - self:Transmission(ATIS.Sound.ThunderStorm, 0.5) - elseif precepitation==3 then - self:Transmission(ATIS.Sound.Snow, 0.5) - elseif precepitation==4 then - self:Transmission(ATIS.Sound.SnowStorm, 0.5) + self:Transmission( ATIS.Sound.WeatherPhenomena, 1.0, subtitle ) + if precepitation == 1 then + self:Transmission( ATIS.Sound.Rain, 0.5 ) + elseif precepitation == 2 then + self:Transmission( ATIS.Sound.ThunderStorm, 0.5 ) + elseif precepitation == 3 then + self:Transmission( ATIS.Sound.Snow, 0.5 ) + elseif precepitation == 4 then + self:Transmission( ATIS.Sound.SnowStorm, 0.5 ) end if fog then - self:Transmission(ATIS.Sound.Fog, 0.5) + self:Transmission( ATIS.Sound.Fog, 0.5 ) end if dust then - self:Transmission(ATIS.Sound.Dust, 0.5) + self:Transmission( ATIS.Sound.Dust, 0.5 ) end end - alltext=alltext..";\n"..subtitle + alltext = alltext .. ";\n" .. subtitle end -- Cloud base if not self.useSRS then - self:Transmission(CloudCover, 1.0, CLOUDSsub) + self:Transmission( CloudCover, 1.0, CLOUDSsub ) end if CLOUDBASE and static then -- Base - local cbase=tostring(tonumber(CLOUDBASE1000)*1000+tonumber(CLOUDBASE0100)*100) - local cceil=tostring(tonumber(CLOUDCEIL1000)*1000+tonumber(CLOUDCEIL0100)*100) + local cbase = tostring( tonumber( CLOUDBASE1000 ) * 1000 + tonumber( CLOUDBASE0100 ) * 100 ) + local cceil = tostring( tonumber( CLOUDCEIL1000 ) * 1000 + tonumber( CLOUDCEIL0100 ) * 100 ) if self.metric then - --subtitle=string.format("Cloud base %s, ceiling %s meters", CLOUDBASE, CLOUDCEIL) - subtitle=string.format("Cloud base %s, ceiling %s meters", cbase, cceil) + -- subtitle=string.format("Cloud base %s, ceiling %s meters", CLOUDBASE, CLOUDCEIL) + subtitle = string.format( "Cloud base %s, ceiling %s meters", cbase, cceil ) else - --subtitle=string.format("Cloud base %s, ceiling %s feet", CLOUDBASE, CLOUDCEIL) - subtitle=string.format("Cloud base %s, ceiling %s feet", cbase, cceil) + -- subtitle=string.format("Cloud base %s, ceiling %s feet", CLOUDBASE, CLOUDCEIL) + subtitle = string.format( "Cloud base %s, ceiling %s feet", cbase, cceil ) end if not self.useSRS then - self:Transmission(ATIS.Sound.CloudBase, 1.0, subtitle) - if tonumber(CLOUDBASE1000)>0 then - self.radioqueue:Number2Transmission(CLOUDBASE1000) - self:Transmission(ATIS.Sound.Thousand, 0.1) + self:Transmission( ATIS.Sound.CloudBase, 1.0, subtitle ) + if tonumber( CLOUDBASE1000 ) > 0 then + self.radioqueue:Number2Transmission( CLOUDBASE1000 ) + self:Transmission( ATIS.Sound.Thousand, 0.1 ) end - if tonumber(CLOUDBASE0100)>0 then - self.radioqueue:Number2Transmission(CLOUDBASE0100) - self:Transmission(ATIS.Sound.Hundred, 0.1) + if tonumber( CLOUDBASE0100 ) > 0 then + self.radioqueue:Number2Transmission( CLOUDBASE0100 ) + self:Transmission( ATIS.Sound.Hundred, 0.1 ) end -- Ceiling - self:Transmission(ATIS.Sound.CloudCeiling, 0.5) - if tonumber(CLOUDCEIL1000)>0 then - self.radioqueue:Number2Transmission(CLOUDCEIL1000) - self:Transmission(ATIS.Sound.Thousand, 0.1) + self:Transmission( ATIS.Sound.CloudCeiling, 0.5 ) + if tonumber( CLOUDCEIL1000 ) > 0 then + self.radioqueue:Number2Transmission( CLOUDCEIL1000 ) + self:Transmission( ATIS.Sound.Thousand, 0.1 ) end - if tonumber(CLOUDCEIL0100)>0 then - self.radioqueue:Number2Transmission(CLOUDCEIL0100) - self:Transmission(ATIS.Sound.Hundred, 0.1) + if tonumber( CLOUDCEIL0100 ) > 0 then + self.radioqueue:Number2Transmission( CLOUDCEIL0100 ) + self:Transmission( ATIS.Sound.Hundred, 0.1 ) end if self.metric then - self:Transmission(ATIS.Sound.Meters, 0.1) + self:Transmission( ATIS.Sound.Meters, 0.1 ) else - self:Transmission(ATIS.Sound.Feet, 0.1) + self:Transmission( ATIS.Sound.Feet, 0.1 ) end end end - alltext=alltext..";\n"..subtitle - + + alltext = alltext .. ";\n" .. subtitle + subtitle = "" -- Temperature if self.TDegF then - if temperature<0 then - subtitle=string.format("Temperature -%s °F", TEMPERATURE) + if temperature < 0 then + subtitle = string.format( "Temperature -%s °F", TEMPERATURE ) else - subtitle=string.format("Temperature %s °F", TEMPERATURE) + subtitle = string.format( "Temperature %s °F", TEMPERATURE ) end else - if temperature<0 then - subtitle=string.format("Temperature -%s °C", TEMPERATURE) + if temperature < 0 then + subtitle = string.format( "Temperature -%s °C", TEMPERATURE ) else - subtitle=string.format("Temperature %s °C", TEMPERATURE) + subtitle = string.format( "Temperature %s °C", TEMPERATURE ) end end - local _TEMPERATURE=subtitle + local _TEMPERATURE = subtitle if not self.useSRS then - self:Transmission(ATIS.Sound.Temperature, 1.0, subtitle) - if temperature<0 then - self:Transmission(ATIS.Sound.Minus, 0.2) + self:Transmission( ATIS.Sound.Temperature, 1.0, subtitle ) + if temperature < 0 then + self:Transmission( ATIS.Sound.Minus, 0.2 ) end - self.radioqueue:Number2Transmission(TEMPERATURE) + self.radioqueue:Number2Transmission( TEMPERATURE ) if self.TDegF then - self:Transmission(ATIS.Sound.DegreesFahrenheit, 0.2) + self:Transmission( ATIS.Sound.DegreesFahrenheit, 0.2 ) else - self:Transmission(ATIS.Sound.DegreesCelsius, 0.2) + self:Transmission( ATIS.Sound.DegreesCelsius, 0.2 ) end end - alltext=alltext..";\n"..subtitle - + alltext = alltext .. ";\n" .. subtitle + -- Dew point if self.TDegF then - if dewpoint<0 then - subtitle=string.format("Dew point -%s °F", DEWPOINT) + if dewpoint < 0 then + subtitle = string.format( "Dew point -%s °F", DEWPOINT ) else - subtitle=string.format("Dew point %s °F", DEWPOINT) + subtitle = string.format( "Dew point %s °F", DEWPOINT ) end else - if dewpoint<0 then - subtitle=string.format("Dew point -%s °C", DEWPOINT) + if dewpoint < 0 then + subtitle = string.format( "Dew point -%s °C", DEWPOINT ) else - subtitle=string.format("Dew point %s °C", DEWPOINT) + subtitle = string.format( "Dew point %s °C", DEWPOINT ) end end - local _DEWPOINT=subtitle + local _DEWPOINT = subtitle if not self.useSRS then - self:Transmission(ATIS.Sound.DewPoint, 1.0, subtitle) - if dewpoint<0 then - self:Transmission(ATIS.Sound.Minus, 0.2) + self:Transmission( ATIS.Sound.DewPoint, 1.0, subtitle ) + if dewpoint < 0 then + self:Transmission( ATIS.Sound.Minus, 0.2 ) end - self.radioqueue:Number2Transmission(DEWPOINT) + self.radioqueue:Number2Transmission( DEWPOINT ) if self.TDegF then - self:Transmission(ATIS.Sound.DegreesFahrenheit, 0.2) + self:Transmission( ATIS.Sound.DegreesFahrenheit, 0.2 ) else - self:Transmission(ATIS.Sound.DegreesCelsius, 0.2) + self:Transmission( ATIS.Sound.DegreesCelsius, 0.2 ) end end - alltext=alltext..";\n"..subtitle + alltext = alltext .. ";\n" .. subtitle -- Altimeter QNH/QFE. if self.PmmHg then if self.qnhonly then - subtitle=string.format("Altimeter %s.%s mmHg", QNH[1], QNH[2]) + subtitle = string.format( "Altimeter %s.%s mmHg", QNH[1], QNH[2] ) else - subtitle=string.format("Altimeter: QNH %s.%s, QFE %s.%s mmHg", QNH[1], QNH[2], QFE[1], QFE[2]) + subtitle = string.format( "Altimeter: QNH %s.%s, QFE %s.%s mmHg", QNH[1], QNH[2], QFE[1], QFE[2] ) end else if self.metric then if self.qnhonly then - subtitle=string.format("Altimeter %s.%s hPa", QNH[1], QNH[2]) + subtitle = string.format( "Altimeter %s.%s hPa", QNH[1], QNH[2] ) else - subtitle=string.format("Altimeter: QNH %s.%s, QFE %s.%s hPa", QNH[1], QNH[2], QFE[1], QFE[2]) + subtitle = string.format( "Altimeter: QNH %s.%s, QFE %s.%s hPa", QNH[1], QNH[2], QFE[1], QFE[2] ) end else if self.qnhonly then - subtitle=string.format("Altimeter %s.%s inHg", QNH[1], QNH[2]) + subtitle = string.format( "Altimeter %s.%s inHg", QNH[1], QNH[2] ) else - subtitle=string.format("Altimeter: QNH %s.%s, QFE %s.%s inHg", QNH[1], QNH[2], QFE[1], QFE[2]) + subtitle = string.format( "Altimeter: QNH %s.%s, QFE %s.%s inHg", QNH[1], QNH[2], QFE[1], QFE[2] ) end end end - local _ALTIMETER=subtitle + + if self.ReportmBar and not self.metric then + if self.qnhonly then + subtitle = string.format( "%s;\nAltimeter %d hPa", subtitle, mBarqnh ) + else + subtitle = string.format( "%s;\nAltimeter: QNH %d, QFE %d hPa", subtitle, mBarqnh, mBarqfe) + end + end + + local _ALTIMETER = subtitle if not self.useSRS then - self:Transmission(ATIS.Sound.Altimeter, 1.0, subtitle) + self:Transmission( ATIS.Sound.Altimeter, 1.0, subtitle ) if not self.qnhonly then - self:Transmission(ATIS.Sound.QNH, 0.5) + self:Transmission( ATIS.Sound.QNH, 0.5 ) end - self.radioqueue:Number2Transmission(QNH[1]) + self.radioqueue:Number2Transmission( QNH[1] ) if ATIS.ICAOPhraseology[UTILS.GetDCSMap()] then - self:Transmission(ATIS.Sound.Decimal, 0.2) + self:Transmission( ATIS.Sound.Decimal, 0.2 ) end - self.radioqueue:Number2Transmission(QNH[2]) - + self.radioqueue:Number2Transmission( QNH[2] ) + if not self.qnhonly then - self:Transmission(ATIS.Sound.QFE, 0.75) - self.radioqueue:Number2Transmission(QFE[1]) - if ATIS.ICAOPhraseology[UTILS.GetDCSMap()] then - self:Transmission(ATIS.Sound.Decimal, 0.2) - end - self.radioqueue:Number2Transmission(QFE[2]) - end - + self:Transmission( ATIS.Sound.QFE, 0.75 ) + self.radioqueue:Number2Transmission( QFE[1] ) + if ATIS.ICAOPhraseology[UTILS.GetDCSMap()] then + self:Transmission( ATIS.Sound.Decimal, 0.2 ) + end + self.radioqueue:Number2Transmission( QFE[2] ) + end + if self.PmmHg then - self:Transmission(ATIS.Sound.MillimetersOfMercury, 0.1) + self:Transmission( ATIS.Sound.MillimetersOfMercury, 0.1 ) else if self.metric then - self:Transmission(ATIS.Sound.HectoPascal, 0.1) + self:Transmission( ATIS.Sound.HectoPascal, 0.1 ) else - self:Transmission(ATIS.Sound.InchesOfMercury, 0.1) - end - end - end - alltext=alltext..";\n"..subtitle - - -- Active runway. - local subtitle=string.format("Active runway %s", runway) - if rwyLeft==true then - subtitle=subtitle.." Left" - elseif rwyLeft==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:Transmission(ATIS.Sound.Left, 0.2) - elseif rwyLeft==false then - self:Transmission(ATIS.Sound.Right, 0.2) + self:Transmission( ATIS.Sound.InchesOfMercury, 0.1 ) + end end end - alltext=alltext..";\n"..subtitle - - -- Runway length. - if self.rwylength then + alltext = alltext .. ";\n" .. subtitle - local runact=self.airbase:GetActiveRunway(self.runwaym2t) - local length=runact.length - if not self.metric then - length=UTILS.MetersToFeet(length) + if not self.ATISforFARPs then + -- Active runway. + local subtitle=string.format("Active runway %s", runwayLanding) + if rwyLandingLeft==true then + subtitle=subtitle.." Left" + elseif rwyLandingLeft==false then + subtitle=subtitle.." Right" end - - -- Length in thousands and hundrets of ft/meters. - local L1000, L0100=self:_GetThousandsAndHundreds(length) - - -- Subtitle. - local subtitle=string.format("Runway length %d", length) - if self.metric then - subtitle=subtitle.." meters" - else - subtitle=subtitle.." feet" - end - - -- Transmit. + local _RUNACT = subtitle if not self.useSRS then - self:Transmission(ATIS.Sound.RunwayLength, 1.0, subtitle) - if tonumber(L1000)>0 then - self.radioqueue:Number2Transmission(L1000) - self:Transmission(ATIS.Sound.Thousand, 0.1) + self:Transmission(ATIS.Sound.ActiveRunway, 1.0, subtitle) + self.radioqueue:Number2Transmission(runwayLanding) + if rwyLandingLeft==true then + self:Transmission(ATIS.Sound.Left, 0.2) + elseif rwyLandingLeft==false then + self:Transmission(ATIS.Sound.Right, 0.2) end - if tonumber(L0100)>0 then - self.radioqueue:Number2Transmission(L0100) - self:Transmission(ATIS.Sound.Hundred, 0.1) + end + alltext = alltext .. ";\n" .. subtitle + + -- Runway length. + if self.rwylength then + + local runact = self.airbase:GetActiveRunway( self.runwaym2t ) + local length = runact.length + if not self.metric then + length = UTILS.MetersToFeet( length ) end + + -- Length in thousands and hundrets of ft/meters. + local L1000, L0100 = self:_GetThousandsAndHundreds( length ) + + -- Subtitle. + local subtitle = string.format( "Runway length %d", length ) if self.metric then - self:Transmission(ATIS.Sound.Meters, 0.1) + subtitle = subtitle .. " meters" else - self:Transmission(ATIS.Sound.Feet, 0.1) + subtitle = subtitle .. " feet" end + + -- Transmit. + if not self.useSRS then + self:Transmission( ATIS.Sound.RunwayLength, 1.0, subtitle ) + if tonumber( L1000 ) > 0 then + self.radioqueue:Number2Transmission( L1000 ) + self:Transmission( ATIS.Sound.Thousand, 0.1 ) + end + if tonumber( L0100 ) > 0 then + self.radioqueue:Number2Transmission( L0100 ) + self:Transmission( ATIS.Sound.Hundred, 0.1 ) + end + if self.metric then + self:Transmission( ATIS.Sound.Meters, 0.1 ) + else + self:Transmission( ATIS.Sound.Feet, 0.1 ) + end + end + alltext = alltext .. ";\n" .. subtitle end - alltext=alltext..";\n"..subtitle end - -- Airfield elevation if self.elevation then - local elevation=self.airbase:GetHeight() + local elevation = self.airbase:GetHeight() if not self.metric then - elevation=UTILS.MetersToFeet(elevation) + elevation = UTILS.MetersToFeet( elevation ) end -- Length in thousands and hundrets of ft/meters. - local L1000, L0100=self:_GetThousandsAndHundreds(elevation) + local L1000, L0100 = self:_GetThousandsAndHundreds( elevation ) -- Subtitle. - local subtitle=string.format("Elevation %d", elevation) + local subtitle = string.format( "Elevation %d", elevation ) if self.metric then - subtitle=subtitle.." meters" + subtitle = subtitle .. " meters" else - subtitle=subtitle.." feet" + subtitle = subtitle .. " feet" end -- Transmit. if not self.useSRS then - self:Transmission(ATIS.Sound.Elevation, 1.0, subtitle) - if tonumber(L1000)>0 then - self.radioqueue:Number2Transmission(L1000) - self:Transmission(ATIS.Sound.Thousand, 0.1) + self:Transmission( ATIS.Sound.Elevation, 1.0, subtitle ) + if tonumber( L1000 ) > 0 then + self.radioqueue:Number2Transmission( L1000 ) + self:Transmission( ATIS.Sound.Thousand, 0.1 ) end - if tonumber(L0100)>0 then - self.radioqueue:Number2Transmission(L0100) - self:Transmission(ATIS.Sound.Hundred, 0.1) + if tonumber( L0100 ) > 0 then + self.radioqueue:Number2Transmission( L0100 ) + self:Transmission( ATIS.Sound.Hundred, 0.1 ) end if self.metric then - self:Transmission(ATIS.Sound.Meters, 0.1) + self:Transmission( ATIS.Sound.Meters, 0.1 ) else - self:Transmission(ATIS.Sound.Feet, 0.1) + self:Transmission( ATIS.Sound.Feet, 0.1 ) end end - alltext=alltext..";\n"..subtitle + alltext = alltext .. ";\n" .. subtitle end -- Tower frequency. if self.towerfrequency then - local freqs="" - for i,freq in pairs(self.towerfrequency) do - freqs=freqs..string.format("%.3f MHz", freq) - if i<#self.towerfrequency then - freqs=freqs..", " + local freqs = "" + for i, freq in pairs( self.towerfrequency ) do + freqs = freqs .. string.format( "%.3f MHz", freq ) + if i < #self.towerfrequency then + freqs = freqs .. ", " end end - subtitle=string.format("Tower frequency %s", freqs) + subtitle = string.format( "Tower frequency %s", freqs ) if not self.useSRS then - self:Transmission(ATIS.Sound.TowerFrequency, 1.0, subtitle) - for _,freq in pairs(self.towerfrequency) do - local f=string.format("%.3f", freq) - f=UTILS.Split(f, ".") - self.radioqueue:Number2Transmission(f[1], nil, 0.5) - if tonumber(f[2])>0 then - self:Transmission(ATIS.Sound.Decimal, 0.2) - self.radioqueue:Number2Transmission(f[2]) + self:Transmission( ATIS.Sound.TowerFrequency, 1.0, subtitle ) + for _, freq in pairs( self.towerfrequency ) do + local f = string.format( "%.3f", freq ) + f = UTILS.Split( f, "." ) + self.radioqueue:Number2Transmission( f[1], nil, 0.5 ) + if tonumber( f[2] ) > 0 then + self:Transmission( ATIS.Sound.Decimal, 0.2 ) + self.radioqueue:Number2Transmission( f[2] ) end - self:Transmission(ATIS.Sound.MegaHertz, 0.2) + self:Transmission( ATIS.Sound.MegaHertz, 0.2 ) end end - alltext=alltext..";\n"..subtitle + alltext = alltext .. ";\n" .. subtitle 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 - self:Transmission(ATIS.Sound.ILSFrequency, 1.0, subtitle) - local f=string.format("%.2f", ils.frequency) - f=UTILS.Split(f, ".") - self.radioqueue:Number2Transmission(f[1], nil, 0.5) - if tonumber(f[2])>0 then - self:Transmission(ATIS.Sound.Decimal, 0.2) - self.radioqueue:Number2Transmission(f[2]) - end - self:Transmission(ATIS.Sound.MegaHertz, 0.2) - end - alltext=alltext..";\n"..subtitle + subtitle = string.format( "ILS frequency %.2f MHz", ils.frequency ) + if not self.useSRS then + self:Transmission( ATIS.Sound.ILSFrequency, 1.0, subtitle ) + local f = string.format( "%.2f", ils.frequency ) + f = UTILS.Split( f, "." ) + self.radioqueue:Number2Transmission( f[1], nil, 0.5 ) + if tonumber( f[2] ) > 0 then + self:Transmission( ATIS.Sound.Decimal, 0.2 ) + self.radioqueue:Number2Transmission( f[2] ) + end + self:Transmission( ATIS.Sound.MegaHertz, 0.2 ) + end + alltext = alltext .. ";\n" .. subtitle 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) + subtitle = string.format( "Outer NDB frequency %.2f MHz", ndb.frequency ) if not self.useSRS then - self:Transmission(ATIS.Sound.OuterNDBFrequency, 1.0, subtitle) - local f=string.format("%.2f", ndb.frequency) - f=UTILS.Split(f, ".") - self.radioqueue:Number2Transmission(f[1], nil, 0.5) - if tonumber(f[2])>0 then - self:Transmission(ATIS.Sound.Decimal, 0.2) - self.radioqueue:Number2Transmission(f[2]) + self:Transmission( ATIS.Sound.OuterNDBFrequency, 1.0, subtitle ) + local f = string.format( "%.2f", ndb.frequency ) + f = UTILS.Split( f, "." ) + self.radioqueue:Number2Transmission( f[1], nil, 0.5 ) + if tonumber( f[2] ) > 0 then + self:Transmission( ATIS.Sound.Decimal, 0.2 ) + self.radioqueue:Number2Transmission( f[2] ) end - self:Transmission(ATIS.Sound.MegaHertz, 0.2) + self:Transmission( ATIS.Sound.MegaHertz, 0.2 ) end - alltext=alltext..";\n"..subtitle + alltext = alltext .. ";\n" .. subtitle 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) + subtitle = string.format( "Inner NDB frequency %.2f MHz", ndb.frequency ) if not self.useSRS then - self:Transmission(ATIS.Sound.InnerNDBFrequency, 1.0, subtitle) - local f=string.format("%.2f", ndb.frequency) - f=UTILS.Split(f, ".") - self.radioqueue:Number2Transmission(f[1], nil, 0.5) - if tonumber(f[2])>0 then - self:Transmission(ATIS.Sound.Decimal, 0.2) - self.radioqueue:Number2Transmission(f[2]) - end - self:Transmission(ATIS.Sound.MegaHertz, 0.2) - end - alltext=alltext..";\n"..subtitle + self:Transmission( ATIS.Sound.InnerNDBFrequency, 1.0, subtitle ) + local f = string.format( "%.2f", ndb.frequency ) + f = UTILS.Split( f, "." ) + self.radioqueue:Number2Transmission( f[1], nil, 0.5 ) + if tonumber( f[2] ) > 0 then + self:Transmission( ATIS.Sound.Decimal, 0.2 ) + self.radioqueue:Number2Transmission( f[2] ) + end + self:Transmission( ATIS.Sound.MegaHertz, 0.2 ) + end + alltext = alltext .. ";\n" .. subtitle end -- VOR if self.vor then - subtitle=string.format("VOR frequency %.2f MHz", self.vor) + subtitle = string.format( "VOR frequency %.2f MHz", self.vor ) if self.useSRS then - subtitle=string.format("V O R frequency %.2f MHz", self.vor) + subtitle = string.format( "V O R frequency %.2f MHz", self.vor ) end if not self.useSRS then - self:Transmission(ATIS.Sound.VORFrequency, 1.0, subtitle) - local f=string.format("%.2f", self.vor) - f=UTILS.Split(f, ".") - self.radioqueue:Number2Transmission(f[1], nil, 0.5) - if tonumber(f[2])>0 then - self:Transmission(ATIS.Sound.Decimal, 0.2) - self.radioqueue:Number2Transmission(f[2]) + self:Transmission( ATIS.Sound.VORFrequency, 1.0, subtitle ) + local f = string.format( "%.2f", self.vor ) + f = UTILS.Split( f, "." ) + self.radioqueue:Number2Transmission( f[1], nil, 0.5 ) + if tonumber( f[2] ) > 0 then + self:Transmission( ATIS.Sound.Decimal, 0.2 ) + self.radioqueue:Number2Transmission( f[2] ) end - self:Transmission(ATIS.Sound.MegaHertz, 0.2) + self:Transmission( ATIS.Sound.MegaHertz, 0.2 ) end - alltext=alltext..";\n"..subtitle + alltext = alltext .. ";\n" .. subtitle end -- 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) - self.radioqueue:NewTransmission("NATO Alphabet/Xray.ogg", 0.75, self.soundpath, nil, 0.2) + self:Transmission( ATIS.Sound.TACANChannel, 1.0, subtitle ) + self.radioqueue:Number2Transmission( tostring( self.tacan ), nil, 0.2 ) + self.radioqueue:NewTransmission( "NATO Alphabet/Xray.ogg", 0.75, self.soundpath, nil, 0.2 ) end - alltext=alltext..";\n"..subtitle + alltext = alltext .. ";\n" .. subtitle end -- RSBN if self.rsbn then - subtitle=string.format("RSBN channel %d", self.rsbn) + subtitle = string.format( "RSBN channel %d", self.rsbn ) if not self.useSRS then - self:Transmission(ATIS.Sound.RSBNChannel, 1.0, subtitle) - self.radioqueue:Number2Transmission(tostring(self.rsbn), nil, 0.2) + self:Transmission( ATIS.Sound.RSBNChannel, 1.0, subtitle ) + self.radioqueue:Number2Transmission( tostring( self.rsbn ), nil, 0.2 ) end - alltext=alltext..";\n"..subtitle + alltext = alltext .. ";\n" .. subtitle 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) + subtitle = string.format( "PRMG channel %d", ndb.frequency ) if not self.useSRS then - self:Transmission(ATIS.Sound.PRMGChannel, 1.0, subtitle) - self.radioqueue:Number2Transmission(tostring(ndb.frequency), nil, 0.5) - end - alltext=alltext..";\n"..subtitle + self:Transmission( ATIS.Sound.PRMGChannel, 1.0, subtitle ) + self.radioqueue:Number2Transmission( tostring( ndb.frequency ), nil, 0.5 ) + end + alltext = alltext .. ";\n" .. subtitle + end + + -- additional info, if any + if self.useSRS and self.AdditionalInformation then + alltext = alltext .. ";\n"..self.AdditionalInformation end -- Advice on initial... - subtitle=string.format("Advise on initial contact, you have information %s", NATO) + subtitle = string.format( "Advise on initial contact, you have information %s", NATO ) if not self.useSRS then - self:Transmission(ATIS.Sound.AdviceOnInitial, 0.5, subtitle) - self.radioqueue:NewTransmission(string.format("NATO Alphabet/%s.ogg", NATO), 0.75, self.soundpath) - end - alltext=alltext..";\n"..subtitle - + self:Transmission( ATIS.Sound.AdviceOnInitial, 0.5, subtitle ) + self.radioqueue:NewTransmission( string.format( "NATO Alphabet/%s.ogg", NATO ), 0.75, self.soundpath ) + end + alltext = alltext .. ";\n" .. subtitle + -- Report ATIS text. - self:Report(alltext) + self:Report( alltext ) -- Update F10 marker. if self.usemarker then - self:UpdateMarker(_INFORMATION, _RUNACT, _WIND, _ALTIMETER, _TEMPERATURE) + self:UpdateMarker( _INFORMATION, _RUNACT, _WIND, _ALTIMETER, _TEMPERATURE ) end end @@ -117588,34 +126456,39 @@ end -- @param #string Event Event. -- @param #string To To state. -- @param #string Text Report text. -function ATIS:onafterReport(From, Event, To, Text) - self:T(self.lid..string.format("Report:\n%s", Text)) - +function ATIS:onafterReport( From, Event, To, Text ) + self:T( self.lid .. string.format( "Report:\n%s", Text ) ) + if self.useSRS and self.msrs then - + -- Remove line breaks - local text=string.gsub(Text, "[\r\n]", "") - + local text = string.gsub( Text, "[\r\n]", "" ) + -- Replace other stuff. - local text=string.gsub(text, "SM", "statute miles") - local text=string.gsub(text, "°C", "degrees Celsius") - local text=string.gsub(text, "°F", "degrees Fahrenheit") - local text=string.gsub(text, "inHg", "inches of Mercury") - local text=string.gsub(text, "mmHg", "millimeters of Mercury") - local text=string.gsub(text, "hPa", "hecto Pascals") - local text=string.gsub(text, "m/s", "meters per second") - + local text = string.gsub( text, "SM", "statute miles" ) + local text = string.gsub( text, "°C", "degrees Celsius" ) + local text = string.gsub( text, "°F", "degrees Fahrenheit" ) + local text = string.gsub( text, "inHg", "inches of Mercury" ) + local text = string.gsub( text, "mmHg", "millimeters of Mercury" ) + local text = string.gsub( text, "hPa", "hectopascals" ) + local text = string.gsub( text, "m/s", "meters per second" ) + local text = string.gsub( text, "TACAN", "tackan" ) + local text = string.gsub( text, "FARP", "farp" ) + -- Replace ";" by "." - local text=string.gsub(text, ";", " . ") - - --Debug output. - self:T("SRS TTS: "..text) - - -- Play text-to-speech report. - self.msrs:PlayText(text) + local text = string.gsub( text, ";", " . " ) + + -- Debug output. + self:T( "SRS TTS: " .. text ) + + -- Play text-to-speech report. + local duration = STTS.getSpeechTime(text,0.95) + self.msrsQ:NewTransmission(text,duration,self.msrs,nil,2) + --self.msrs:PlayText( text ) + self.SRSText = text end - + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -117625,25 +126498,25 @@ end --- Base captured -- @param #ATIS self -- @param Core.Event#EVENTDATA EventData Event data. -function ATIS:OnEventBaseCaptured(EventData) - +function ATIS:OnEventBaseCaptured( EventData ) + if EventData and EventData.Place then -- Place is the airbase that was captured. - local airbase=EventData.Place --Wrapper.Airbase#AIRBASE + local airbase = EventData.Place -- Wrapper.Airbase#AIRBASE -- Check that this airbase belongs or did belong to this warehouse. - if EventData.PlaceName==self.airbasename then + if EventData.PlaceName == self.airbasename then -- New coalition of airbase after it was captured. - local NewCoalitionAirbase=airbase:GetCoalition() - - if self.useSRS and self.msrs and self.msrs.coalition~=NewCoalitionAirbase then - self.msrs:SetCoalition(NewCoalitionAirbase) + local NewCoalitionAirbase = airbase:GetCoalition() + + if self.useSRS and self.msrs and self.msrs.coalition ~= NewCoalitionAirbase then + self.msrs:SetCoalition( NewCoalitionAirbase ) end - + end - + end end @@ -117660,78 +126533,58 @@ end -- @param #string altimeter Altimeter text. -- @param #string temperature Temperature text. -- @return #number Marker ID. -function ATIS:UpdateMarker(information, runact, wind, altimeter, temperature) +function ATIS:UpdateMarker( information, runact, wind, altimeter, temperature ) if self.markerid then - self.airbase:GetCoordinate():RemoveMark(self.markerid) + self.airbase:GetCoordinate():RemoveMark( self.markerid ) end - local text=string.format("ATIS on %.3f %s, %s:\n", self.frequency, UTILS.GetModulationName(self.modulation), tostring(information)) - text=text..string.format("%s\n", tostring(runact)) - text=text..string.format("%s\n", tostring(wind)) - text=text..string.format("%s\n", tostring(altimeter)) - text=text..string.format("%s", tostring(temperature)) + local text = string.format( "ATIS on %.3f %s, %s:\n", self.frequency, UTILS.GetModulationName( self.modulation ), tostring( information ) ) + text = text .. string.format( "%s\n", tostring( runact ) ) + text = text .. string.format( "%s\n", tostring( wind ) ) + text = text .. string.format( "%s\n", tostring( altimeter ) ) + text = text .. string.format( "%s", tostring( temperature ) ) -- More info is not displayed on the marker! -- Place new mark - self.markerid=self.airbase:GetCoordinate():MarkToAll(text, true) + self.markerid = self.airbase:GetCoordinate():MarkToAll( text, true ) return self.markerid 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. -- @param #ATIS self -- @param #number windfrom Wind direction (from) in degrees. -- @return #string Runway magnetic heading divided by ten (and rounded). Eg, "13" for 130°. -function ATIS:GetMagneticRunway(windfrom) +function ATIS:GetMagneticRunway( windfrom ) - local diffmin=nil - local runway=nil - for _,heading in pairs(self.runwaymag) do + local diffmin = nil + local runway = nil + for _, heading in pairs( self.runwaymag ) do - local hdg=self:GetRunwayWithoutLR(heading) + local hdg = self:GetRunwayWithoutLR( heading ) - local diff=UTILS.HdgDiff(windfrom, tonumber(hdg)*10) - if diffmin==nil or diff data is valid for all runways. return nav else - local navy=tonumber(self:GetRunwayWithoutLR(nav.runway))*10 - local rwyy=tonumber(self:GetRunwayWithoutLR(runway))*10 + local navy = tonumber( self:GetRunwayWithoutLR( nav.runway ) ) * 10 + local rwyy = tonumber( self:GetRunwayWithoutLR( runway ) ) * 10 - local navL=self:GetRunwayLR(nav.runway) - local hdgD=UTILS.HdgDiff(navy,rwyy) + local navL = self:GetRunwayLR( nav.runway ) + local hdgD = UTILS.HdgDiff( navy, rwyy ) - if hdgD<=15 then --We allow an error of +-15° here. - if navL==nil or (navL==true and left==true) or (navL==false and left==false) then + if hdgD <= 15 then -- We allow an error of +-15° here. + if navL == nil or (navL == true and left == true) or (navL == false and left == false) then return nav end end @@ -117777,9 +126630,9 @@ end -- @param #ATIS self -- @param #string runway Runway heading, *e.g.* "31L". -- @return #string Runway heading without left or right, *e.g.* "31". -function ATIS:GetRunwayWithoutLR(runway) - local rwywo=runway:gsub("%D+", "") - --self:I(string.format("FF runway=%s ==> rwywo=%s", runway, rwywo)) +function ATIS:GetRunwayWithoutLR( runway ) + local rwywo = runway:gsub( "%D+", "" ) + -- self:T(string.format("FF runway=%s ==> rwywo=%s", runway, rwywo)) return rwywo end @@ -117787,11 +126640,11 @@ end -- @param #ATIS self -- @param #string runway Runway heading, *e.g.* "31L". -- @return #boolean If *true*, left runway is active. If *false*, right runway. If *nil*, neither applies. -function ATIS:GetRunwayLR(runway) +function ATIS:GetRunwayLR( runway ) -- Get left/right if specified. - local rwyL=runway:lower():find("l") - local rwyR=runway:lower():find("r") + local rwyL = runway:lower():find( "l" ) + local rwyR = runway:lower():find( "r" ) if rwyL then return true @@ -117809,19 +126662,19 @@ end -- @param #number interval Interval in seconds after the last transmission finished. -- @param #string subtitle Subtitle of the transmission. -- @param #string path Path to sound file. Default self.soundpath. -function ATIS:Transmission(sound, interval, subtitle, path) - self.radioqueue:NewTransmission(sound.filename, sound.duration, path or self.soundpath, nil, interval, subtitle, self.subduration) +function ATIS:Transmission( sound, interval, subtitle, path ) + self.radioqueue:NewTransmission( sound.filename, sound.duration, path or self.soundpath, nil, interval, subtitle, self.subduration ) end --- Play all audio files. -- @param #ATIS self function ATIS:SoundCheck() - for _,_sound in pairs(ATIS.Sound) do - local sound=_sound --#ATIS.Soundfile - local subtitle=string.format("Playing sound file %s, duration %.2f sec", sound.filename, sound.duration) - self:Transmission(sound, nil, subtitle) - MESSAGE:New(subtitle, 5, "ATIS"):ToAll() + for _, _sound in pairs( ATIS.Sound ) do + local sound = _sound -- #ATIS.Soundfile + local subtitle = string.format( "Playing sound file %s, duration %.2f sec", sound.filename, sound.duration ) + self:Transmission( sound, nil, subtitle ) + MESSAGE:New( subtitle, 5, "ATIS" ):ToAll() end end @@ -117837,7 +126690,7 @@ end function ATIS:GetMissionWeather() -- Weather data from mission file. - local weather=env.mission.weather + local weather = env.mission.weather -- Clouds --[[ @@ -117849,25 +126702,25 @@ function ATIS:GetMissionWeather() ["iprecptns"] = 1, }, -- end of ["clouds"] ]] - local clouds=weather.clouds + local clouds = weather.clouds -- 0=static, 1=dynamic - local static=weather.atmosphere_type==0 + local static = weather.atmosphere_type == 0 -- Visibilty distance in meters. - local visibility=weather.visibility.distance + local visibility = weather.visibility.distance -- Ground turbulence. - local turbulence=weather.groundTurbulence + local turbulence = weather.groundTurbulence -- 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 @@ -117879,35 +126732,34 @@ function ATIS:GetMissionWeather() ["visibility"] = 25, }, -- end of ["fog"] ]] - local fog=nil - if weather.enable_fog==true then - fog=weather.fog - end - - self:T("FF weather:") - self:T({clouds=clouds}) - self:T({visibility=visibility}) - self:T({turbulence=turbulence}) - self:T({fog=fog}) - self:T({dust=dust}) - self:T({static=static}) + local fog = nil + if weather.enable_fog == true then + fog = weather.fog + end + + self:T( "FF weather:" ) + self:T( { clouds = clouds } ) + self:T( { visibility = visibility } ) + self:T( { turbulence = turbulence } ) + self:T( { fog = fog } ) + self:T( { dust = dust } ) + self:T( { static = static } ) return clouds, visibility, turbulence, fog, dust, static end - --- Get thousands of a number. -- @param #ATIS self -- @param #number n Number, *e.g.* 4359. -- @return #string Thousands of n, *e.g.* "4" for 4359. -- @return #string Hundreds of n, *e.g.* "4" for 4359 because its rounded. -function ATIS:_GetThousandsAndHundreds(n) +function ATIS:_GetThousandsAndHundreds( n ) - local N=UTILS.Round(n/1000, 1) + local N = UTILS.Round( n / 1000, 1 ) - local S=UTILS.Split(string.format("%.1f", N), ".") + local S = UTILS.Split( string.format( "%.1f", N ), "." ) - local t=S[1] - local h=S[2] + local t = S[1] + local h = S[2] return t, h end @@ -117915,7 +126767,7 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- **Ops** -- Combat Troops & Logistics Department. +--- **Ops** - Combat Troops & Logistics Department. -- -- === -- @@ -117939,23 +126791,293 @@ end -- @module Ops.CTLD -- @image OPS_CTLD.jpg --- Date: Aug 2021 +-- Last Update Jan 2023 do + +------------------------------------------------------ +--- **CTLD_ENGINEERING** class, extends Core.Base#BASE +-- @type CTLD_ENGINEERING +-- @field #string ClassName +-- @field #string lid +-- @field #string Name +-- @field Wrapper.Group#GROUP Group +-- @field Wrapper.Unit#UNIT Unit +-- @field Wrapper.Group#GROUP HeliGroup +-- @field Wrapper.Unit#UNIT HeliUnit +-- @field #string State +-- @extends Core.Base#BASE + +--- +-- @field #CTLD_ENGINEERING +CTLD_ENGINEERING = { + ClassName = "CTLD_ENGINEERING", + lid = "", + Name = "none", + Group = nil, + Unit = nil, + --C_Ops = nil, + HeliGroup = nil, + HeliUnit = nil, + State = "", + } + + --- CTLD_ENGINEERING class version. + -- @field #string version + CTLD_ENGINEERING.Version = "0.0.3" + + --- Create a new instance. + -- @param #CTLD_ENGINEERING self + -- @param #string Name + -- @param #string GroupName Name of Engineering #GROUP object + -- @param Wrapper.Group#GROUP HeliGroup HeliGroup + -- @param Wrapper.Unit#UNIT HeliUnit HeliUnit + -- @return #CTLD_ENGINEERING self + function CTLD_ENGINEERING:New(Name, GroupName, HeliGroup, HeliUnit) + + -- Inherit everything from BASE class. + local self=BASE:Inherit(self, BASE:New()) -- #CTLD_ENGINEERING + + --BASE:I({Name, GroupName}) + + self.Name = Name or "Engineer Squad" -- #string + self.Group = GROUP:FindByName(GroupName) -- Wrapper.Group#GROUP + self.Unit = self.Group:GetUnit(1) -- Wrapper.Unit#UNIT + self.HeliGroup = HeliGroup -- Wrapper.Group#GROUP + self.HeliUnit = HeliUnit -- Wrapper.Unit#UNIT + self.currwpt = nil -- Core.Point#COORDINATE + self.lid = string.format("%s (%s) | ",self.Name, self.Version) + -- Start State. + self.State = "Stopped" + self.marktimer = 300 -- wait this many secs before trying a crate again + self:Start() + local parent = self:GetParent(self) + return self + end + + --- (Internal) Set the status + -- @param #CTLD_ENGINEERING self + -- @param #string State + -- @return #CTLD_ENGINEERING self + function CTLD_ENGINEERING:SetStatus(State) + self.State = State + return self + end + + --- (Internal) Get the status + -- @param #CTLD_ENGINEERING self + -- @return #string State + function CTLD_ENGINEERING:GetStatus() + return self.State + end + + --- (Internal) Check the status + -- @param #CTLD_ENGINEERING self + -- @param #string State + -- @return #boolean Outcome + function CTLD_ENGINEERING:IsStatus(State) + return self.State == State + end + + --- (Internal) Check the negative status + -- @param #CTLD_ENGINEERING self + -- @param #string State + -- @return #boolean Outcome + function CTLD_ENGINEERING:IsNotStatus(State) + return self.State ~= State + end + + --- (Internal) Set start status. + -- @param #CTLD_ENGINEERING self + -- @return #CTLD_ENGINEERING self + function CTLD_ENGINEERING:Start() + self:T(self.lid.."Start") + self:SetStatus("Running") + return self + end + + --- (Internal) Set stop status. + -- @param #CTLD_ENGINEERING self + -- @return #CTLD_ENGINEERING self + function CTLD_ENGINEERING:Stop() + self:T(self.lid.."Stop") + self:SetStatus("Stopped") + return self + end + + --- (Internal) Set build status. + -- @param #CTLD_ENGINEERING self + -- @return #CTLD_ENGINEERING self + function CTLD_ENGINEERING:Build() + self:T(self.lid.."Build") + self:SetStatus("Building") + return self + end + + --- (Internal) Set done status. + -- @param #CTLD_ENGINEERING self + -- @return #CTLD_ENGINEERING self + function CTLD_ENGINEERING:Done() + self:T(self.lid.."Done") + local grp = self.Group -- Wrapper.Group#GROUP + grp:RelocateGroundRandomInRadius(7,100,false,false,"Diamond") + self:SetStatus("Running") + return self + end + + --- (Internal) Search for crates in reach. + -- @param #CTLD_ENGINEERING self + -- @param #table crates Table of found crate Ops.CTLD#CTLD_CARGO objects. + -- @param #number number Number of crates found. + -- @return #CTLD_ENGINEERING self + function CTLD_ENGINEERING:Search(crates,number) + self:T(self.lid.."Search") + self:SetStatus("Searching") + -- find crates close by + --local COps = self.C_Ops -- Ops.CTLD#CTLD + local dist = self.distance -- #number + local group = self.Group -- Wrapper.Group#GROUP + --local crates,number = COps:_FindCratesNearby(group,nil, dist) -- #table + local ctable = {} + local ind = 0 + if number > 0 then + -- get set of dropped only + for _,_cargo in pairs (crates) do + local cgotype = _cargo:GetType() + if _cargo:WasDropped() and cgotype ~= CTLD_CARGO.Enum.STATIC then + local ok = false + local chalk = _cargo:GetMark() + if chalk == nil then + ok = true + else + -- have we tried this cargo recently? + local tag = chalk.tag or "none" + local timestamp = chalk.timestamp or 0 + -- enough time gone? + local gone = timer.getAbsTime() - timestamp + if gone >= self.marktimer then + ok = true + _cargo:WipeMark() + end -- end time check + end -- end chalk + if ok then + local chalk = {} + chalk.tag = "Engineers" + chalk.timestamp = timer.getAbsTime() + _cargo:AddMark(chalk) + ind = ind + 1 + table.insert(ctable,ind,_cargo) + end + end -- end dropped + end -- end for + end -- end number + + if ind > 0 then + local crate = ctable[1] -- Ops.CTLD#CTLD_CARGO + local static = crate:GetPositionable() -- Wrapper.Static#STATIC + local crate_pos = static:GetCoordinate() -- Core.Point#COORDINATE + local gpos = group:GetCoordinate() -- Core.Point#COORDINATE + -- see how far we are from the crate + local distance = self:_GetDistance(gpos,crate_pos) + self:T(string.format("%s Distance to crate: %d", self.lid, distance)) + -- move there + if distance > 30 and distance ~= -1 and self:IsStatus("Searching") then + group:RouteGroundTo(crate_pos,15,"Line abreast",1) + self.currwpt = crate_pos -- Core.Point#COORDINATE + self:Move() + elseif distance <= 30 and distance ~= -1 then + -- arrived + self:Arrive() + end + else + self:T(self.lid.."No crates in reach!") + end + return self + end + + --- (Internal) Move towards crates in reach. + -- @param #CTLD_ENGINEERING self + -- @return #CTLD_ENGINEERING self + function CTLD_ENGINEERING:Move() + self:T(self.lid.."Move") + self:SetStatus("Moving") + -- check if we arrived on target + --local COps = self.C_Ops -- Ops.CTLD#CTLD + local group = self.Group -- Wrapper.Group#GROUP + local tgtpos = self.currwpt -- Core.Point#COORDINATE + local gpos = group:GetCoordinate() -- Core.Point#COORDINATE + -- see how far we are from the crate + local distance = self:_GetDistance(gpos,tgtpos) + self:T(string.format("%s Distance remaining: %d", self.lid, distance)) + if distance <= 30 and distance ~= -1 then + -- arrived + self:Arrive() + end + return self + end + + --- (Internal) Arrived at crates in reach. Stop group. + -- @param #CTLD_ENGINEERING self + -- @return #CTLD_ENGINEERING self + function CTLD_ENGINEERING:Arrive() + self:T(self.lid.."Arrive") + self:SetStatus("Arrived") + self.currwpt = nil + local Grp = self.Group -- Wrapper.Group#GROUP + Grp:RouteStop() + return self + end + + --- (Internal) Return distance in meters between two coordinates. + -- @param #CTLD_ENGINEERING self + -- @param Core.Point#COORDINATE _point1 Coordinate one + -- @param Core.Point#COORDINATE _point2 Coordinate two + -- @return #number Distance in meters or -1 + function CTLD_ENGINEERING:_GetDistance(_point1, _point2) + self:T(self.lid .. " _GetDistance") + if _point1 and _point2 then + local distance1 = _point1:Get2DDistance(_point2) + local distance2 = _point1:DistanceFromPointVec2(_point2) + 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 + end + else + self:E("******Cannot calculate distance!") + self:E({_point1,_point2}) + return -1 + end + end + +end + + +do ------------------------------------------------------ ---- **CTLD_CARGO** class, extends #Core.Base#BASE +--- **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. --- @field #CTLD_CARGO.Enum Type Enumerator of Type. +-- @field #string CargoType Enumerator of Type. -- @field #boolean HasBeenMoved Flag for moving. -- @field #boolean LoadDirectly Flag for direct loading. -- @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 --- @extends Core.Fsm#FSM +-- @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 CTLD_CARGO = { ClassName = "CTLD_CARGO", ID = 0, @@ -117967,18 +127089,28 @@ CTLD_CARGO = { CratesNeeded = 0, Positionable = nil, HasBeenDropped = false, - PerCrateMass = 0 + PerCrateMass = 0, + Stock = nil, + Mark = nil, } --- Define cargo types. -- @type CTLD_CARGO.Enum - -- @field #string Type Type of Cargo. + -- @field #string VEHICLE + -- @field #string TROOPS + -- @field #string FOB + -- @field #string CRATE + -- @field #string REPAIR + -- @field #string ENGINEERS + -- @field #string STATIC CTLD_CARGO.Enum = { - ["VEHICLE"] = "Vehicle", -- #string vehicles - ["TROOPS"] = "Troops", -- #string troops - ["FOB"] = "FOB", -- #string FOB - ["CRATE"] = "Crate", -- #string crate - ["REPAIR"] = "Repair", -- #string repair + 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 statics } --- Function to create new CTLD_CARGO object. @@ -117993,8 +127125,10 @@ CTLD_CARGO = { -- @param Wrapper.Positionable#POSITIONABLE Positionable Representation of cargo in the mission. -- @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) + 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}) @@ -118008,6 +127142,9 @@ CTLD_CARGO = { self.Positionable = Positionable or nil -- Wrapper.Positionable#POSITIONABLE self.HasBeenDropped = Dropped or false --#boolean self.PerCrateMass = PerCrateMass or 0 -- #number + self.Stock = Stock or nil --#number + self.Mark = nil + self.Subcategory = Subcategory or "Other" return self end @@ -118018,6 +127155,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 @@ -118099,6 +127250,42 @@ CTLD_CARGO = { self.HasBeenDropped = dropped or false end + --- Get Stock. + -- @param #CTLD_CARGO self + -- @return #number Stock + function CTLD_CARGO:GetStock() + if self.Stock then + return self.Stock + else + return -1 + end + end + + --- Add Stock. + -- @param #CTLD_CARGO self + -- @param #number Number to add, one if nil. + -- @return #CTLD_CARGO self + function CTLD_CARGO:AddStock(Number) + if self.Stock then -- Stock nil? + local number = Number or 1 + self.Stock = self.Stock + number + end + return self + end + + --- Remove Stock. + -- @param #CTLD_CARGO self + -- @param #number Number to reduce, one if nil. + -- @return #CTLD_CARGO self + function CTLD_CARGO:RemoveStock(Number) + if self.Stock then -- Stock nil? + local number = Number or 1 + self.Stock = self.Stock - number + if self.Stock < 0 then self.Stock = 0 end + end + return self + end + --- Query crate type for REPAIR -- @param #CTLD_CARGO self -- @param #boolean @@ -118109,10 +127296,55 @@ CTLD_CARGO = { return false end end + + --- Query crate type for STATIC + -- @param #CTLD_CARGO self + -- @return #boolean + function CTLD_CARGO:IsStatic() + if self.CargoType == "Static" then + return true + else + return false + end + end + + --- Add mark + -- @param #CTLD_CARGO self + -- @return #CTLD_CARGO self + function CTLD_CARGO:AddMark(Mark) + self.Mark = Mark + return self + end + + --- Get mark + -- @param #CTLD_CARGO self + -- @return #string Mark + function CTLD_CARGO:GetMark(Mark) + return self.Mark + end + + --- Wipe mark + -- @param #CTLD_CARGO self + -- @return #CTLD_CARGO self + function CTLD_CARGO:WipeMark() + self.Mark = nil + return self + end + + --- Get overall mass of a cargo object, i.e. crates needed x mass per crate + -- @param #CTLD_CARGO self + -- @return #number mass + function CTLD_CARGO:GetNetMass() + return self.CratesNeeded * self.PerCrateMass + end end do +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +-- TODO CTLD +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + ------------------------------------------------------------------------- --- **CTLD** class, extends Core.Base#BASE, Core.Fsm#FSM -- @type CTLD @@ -118120,6 +127352,7 @@ do -- @field #number verbose Verbosity level. -- @field #string lid Class id string for output to DCS log file. -- @field #number coalition Coalition side number, e.g. `coalition.side.RED`. +-- @field #boolean debug -- @extends Core.Fsm#FSM --- *Combat Troop & Logistics Deployment (CTLD): Everyone wants to be a POG, until there\'s POG stuff to be done.* (Mil Saying) @@ -118134,12 +127367,14 @@ do -- * Object oriented refactoring of Ciribob\'s fantastic CTLD script. -- * No need for extra MIST loading. -- * Additional events to tailor your mission. --- * ANY late activated group can serve as cargo, either as troops or crates, which have to be build on-location. +-- * 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 -- @@ -118161,26 +127396,40 @@ do -- -- if you want to add weight to your Heli, troops can have a weight in kg **per person**. Currently no max weight checked. Fly carefully. -- my_ctld:AddTroopsCargo("Anti-Tank Small",{"ATS"},CTLD_CARGO.Enum.TROOPS,3,80) -- --- -- add infantry unit called "Anti-Tank" using templates "AA" and "AA"", of type TROOP with size 4 --- my_ctld:AddTroopsCargo("Anti-Air",{"AA","AA2"},CTLD_CARGO.Enum.TROOPS,4) +-- -- add infantry unit called "Anti-Tank" using templates "AA" and "AA"", of type TROOP with size 4. No weight. We only have 2 in stock: +-- my_ctld:AddTroopsCargo("Anti-Air",{"AA","AA2"},CTLD_CARGO.Enum.TROOPS,4,nil,2) +-- +-- -- add an engineers unit called "Wrenches" using template "Engineers", of type ENGINEERS with size 2. Engineers can be loaded, dropped, +-- -- and extracted like troops. However, they will seek to build and/or repair crates found in a given radius. Handy if you can\'t stay +-- -- to build or repair or under fire. +-- my_ctld:AddTroopsCargo("Wrenches",{"Engineers"},CTLD_CARGO.Enum.ENGINEERS,4) +-- myctld.EngineerSearch = 2000 -- teams will search for crates in this radius. -- -- -- 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) -- -- -- 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 +-- +-- -- add static cargo objects, e.g ammo chests - the name needs to refer to a STATIC object in the mission editor, +-- -- here: it\'s the UNIT name (not the GROUP name!), the second parameter is the weight in kg. +-- my_ctld:AddStaticsCargo("Ammunition",500) -- -- ## 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) @@ -118199,9 +127448,8 @@ do -- -- Add a zone of type SHIP to our setup. Players can load troops and crates from this ship -- -- "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 smoke, flares, beacons don't work for this type of loadzone (yet). Also, 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. +-- -- 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, avoid forcing hoverload. -- my_ctld:AddCTLDZone("Tarawa",CTLD.CargoZoneType.SHIP,SMOKECOLOR.Blue,true,true,240,20) -- -- ## 2. Options @@ -118209,11 +127457,11 @@ do -- The following options are available (with their defaults). Only set the ones you want changed: -- -- my_ctld.useprefix = true -- (DO NOT SWITCH THIS OFF UNLESS YOU KNOW WHAT YOU ARE DOING!) Adjust **before** starting CTLD. If set to false, *all* choppers of the coalition side will be enabled for CTLD. --- my_ctld.CrateDistance = 30 -- List and Load crates in this radius only. +-- my_ctld.CrateDistance = 35 -- List and Load crates in this radius only. -- my_ctld.dropcratesanywhere = false -- Option to allow crates to be dropped anywhere. -- my_ctld.maximumHoverHeight = 15 -- Hover max this high to load. -- my_ctld.minimumHoverHeight = 4 -- Hover min this low to load. --- my_ctld.forcehoverload = true -- Crates (not: troops) can only be loaded while hovering. +-- my_ctld.forcehoverload = true -- Crates (not: troops) can **only** be loaded while hovering. -- my_ctld.hoverautoloading = true -- Crates in CrateDistance in a LOAD zone will be loaded automatically if space allows. -- my_ctld.smokedistance = 2000 -- Smoke or flares can be request for zones this far away (in meters). -- my_ctld.movetroopstowpzone = true -- Troops and vehicles will move to the nearest MOVE zone... @@ -118221,7 +127469,22 @@ do -- my_ctld.smokedistance = 2000 -- Only smoke or flare zones if requesting player unit is this far away (in meters) -- my_ctld.suppressmessages = false -- Set to true if you want to script your own messages. -- my_ctld.repairtime = 300 -- Number of seconds it takes to repair a unit. --- +-- my_ctld.buildtime = 300 -- Number of seconds it takes to build a unit. Set to zero or nil to build instantly. +-- 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.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. +-- my_ctld.placeCratesAhead = false -- place crates straight ahead of the helicopter, in a random way. If true, crates are more neatly sorted. +-- my_ctld.nobuildinloadzones = true -- forbid players to build stuff in LOAD zones if set to `true` +-- my_ctld.movecratesbeforebuild = true -- crates must be moved once before they can be build. Set to false for direct builds. +-- my_ctld.surfacetypes = {land.SurfaceType.LAND,land.SurfaceType.ROAD,land.SurfaceType.RUNWAY,land.SurfaceType.SHALLOW_WATER} -- surfaces for loading back objects. +-- my_ctld.nobuildmenu = false -- if set to true effectively enforces to have engineers build/repair stuff for you. +-- -- ## 2.1 User functions -- -- ### 2.1.1 Adjust or add chopper unit-type capabilities @@ -118229,21 +127492,23 @@ 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) +-- -- 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}, --- ["SA342L"] = {type="SA342L", crates=false, troops=true, cratelimit = 0, trooplimit = 2}, --- ["SA342M"] = {type="SA342M", crates=false, troops=true, cratelimit = 0, trooplimit = 4}, --- ["SA342Minigun"] = {type="SA342Minigun", crates=false, troops=true, cratelimit = 0, trooplimit = 2}, --- ["UH-1H"] = {type="UH-1H", crates=true, troops=true, cratelimit = 1, trooplimit = 8}, --- ["Mi-8MT"] = {type="Mi-8MT", crates=true, troops=true, cratelimit = 2, trooplimit = 12}, --- ["Ka-50"] = {type="Ka-50", crates=false, troops=false, cratelimit = 0, trooplimit = 0}, --- ["Mi-24P"] = {type="Mi-24P", crates=true, troops=true, cratelimit = 1, trooplimit = 8}, --- ["Mi-24V"] = {type="Mi-24V", crates=true, troops=true, cratelimit = 1, trooplimit = 8}, --- +-- -- 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}, +-- ["Mi-8MTV2"] = {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}, +-- ["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 -- @@ -118257,6 +127522,26 @@ do -- -- Deactivate zone called Name of type #CTLD.CargoZoneType ZoneType: -- my_ctld:DeactivateZone(Name,CTLD.CargoZoneType.DROP) -- +-- ## 2.1.3 Limit and manage available resources +-- +-- When adding generic cargo types, you can effectively limit how many units can be dropped/build by the players, e.g. +-- +-- -- 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) +-- +-- You can manually add or remove the available stock like so: +-- +-- -- Crates +-- my_ctld:AddStockCrates("Humvee", 2) +-- my_ctld:RemoveStockCrates("Humvee", 2) +-- +-- -- Troops +-- my_ctld:AddStockTroops("Anti-Air", 2) +-- my_ctld:RemoveStockTroops("Anti-Air", 2) +-- +-- Notes: +-- Troops dropped back into a LOAD zone will effectively be added to the stock. Crates lost in e.g. a heli crash are just that - lost. +-- -- ## 3. Events -- -- The class comes with a number of FSM-based events that missions designers can use to shape their mission. @@ -118318,18 +127603,24 @@ do -- -- To award player with points, using the SCORING Class (SCORING: my_Scoring, CTLD: CTLD_Cargotransport) -- +-- my_scoring = SCORING:New("Combat Transport") +-- -- function CTLD_Cargotransport:OnAfterCratesDropped(From, Event, To, Group, Unit, Cargotable) -- local points = 10 --- local PlayerName = Unit:GetPlayerName() --- my_scoring:_AddPlayerFromUnit( Unit ) --- my_scoring:AddGoalScore(Unit, "CTLD", string.format("Pilot %s has been awarded %d points for transporting cargo crates!", PlayerName, points), points) +-- if Unit then +-- local PlayerName = Unit:GetPlayerName() +-- my_scoring:_AddPlayerFromUnit( Unit ) +-- my_scoring:AddGoalScore(Unit, "CTLD", string.format("Pilot %s has been awarded %d points for transporting cargo crates!", PlayerName, points), points) +-- end -- end -- -- function CTLD_Cargotransport:OnAfterCratesBuild(From, Event, To, Group, Unit, Vehicle) -- local points = 5 --- local PlayerName = Unit:GetPlayerName() --- my_scoring:_AddPlayerFromUnit( Unit ) --- my_scoring:AddGoalScore(Unit, "CTLD", string.format("Pilot %s has been awarded %d points for the construction of Units!", PlayerName, points), points) +-- if Unit then + -- local PlayerName = Unit:GetPlayerName() + -- my_scoring:_AddPlayerFromUnit( Unit ) + -- my_scoring:AddGoalScore(Unit, "CTLD", string.format("Pilot %s has been awarded %d points for the construction of Units!", PlayerName, points), points) +-- end -- end -- -- ## 4. F10 Menu structure @@ -118338,7 +127629,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 -- @@ -118349,7 +127640,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. -- @@ -118360,11 +127651,20 @@ do -- ## 4.6 Show hover parameters -- -- Lists hover parameters and indicates if these are curently fulfilled. Also @see options on hover heights. --- +-- +-- ## 4.7 List Inventory +-- +-- Lists invetory of available units to drop or build. +-- -- ## 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") -- @@ -118375,19 +127675,180 @@ 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, use either the above OR this method, NOT both!) +-- +-- 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). +-- +-- DO NOT use the "splash damage" script together with this method! Your cargo will explode on the ground! +-- +-- 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 -- +-- ## 6. Save and load back units - persistance +-- +-- You can save and later load back units dropped or build to make your mission persistent. +-- For this to work, you need to de-sanitize **io** and **lfs** in your MissionScripting.lua, which is located in your DCS installtion folder under Scripts. +-- There is a risk involved in doing that; if you do not know what that means, this is possibly not for you. +-- +-- Use the following options to manage your saves: +-- +-- my_ctld.enableLoadSave = true -- allow auto-saving and loading of files +-- my_ctld.saveinterval = 600 -- save every 10 minutes +-- my_ctld.filename = "missionsave.csv" -- example filename +-- my_ctld.filepath = "C:\\Users\\myname\\Saved Games\\DCS\Missions\\MyMission" -- example path +-- my_ctld.eventoninject = true -- fire OnAfterCratesBuild and OnAfterTroopsDeployed events when loading (uses Inject functions) +-- +-- Then use an initial load at the beginning of your mission: +-- +-- my_ctld:__Load(10) +-- +-- **Caveat:** +-- If you use units build by multiple templates, they will effectively double on loading. Dropped crates are not saved. Current stock is not saved. +-- +-- ## 7. Complex example - Build a complete FARP from a CTLD crate drop +-- +-- Prerequisites - you need to add a cargo of type FOB to your CTLD instance, for simplification reasons we call it FOB: +-- +-- my_ctld:AddCratesCargo("FARP",{"FOB"},CTLD_CARGO.Enum.FOB,2) +-- +-- Also, you need to have **all statics with the fitting names** as per the script in your mission already, as we're going to copy them, and a template +-- for FARP vehicles, so -- services are goin to work (e.g. for the blue side: an unarmed humvee, two trucks and a fuel truck. Optionally add a fire fighter). +-- +-- The following code will build a FARP at the coordinate the FOB was dropped and built: +-- +-- -- FARP Radio. First one has 130AM, next 131 and for forth +-- local FARPFreq = 130 +-- local FARPName = 1 -- numbers 1..10 +-- +-- local FARPClearnames = { +-- [1]="London", +-- [2]="Dallas", +-- [3]="Paris", +-- [4]="Moscow", +-- [5]="Berlin", +-- [6]="Rome", +-- [7]="Madrid", +-- [8]="Warsaw", +-- [9]="Dublin", +-- [10]="Perth", +-- } +-- +-- function BuildAFARP(Coordinate) +-- local coord = Coordinate -- Core.Point#COORDINATE +-- +-- local FarpName = ((FARPName-1)%10)+1 +-- local FName = FARPClearnames[FarpName] +-- +-- FARPFreq = FARPFreq + 1 +-- FARPName = FARPName + 1 +-- +-- -- Create a SPAWNSTATIC object from a template static FARP object. +-- local SpawnStaticFarp=SPAWNSTATIC:NewFromStatic("Static Invisible FARP-1", country.id.USA) +-- +-- -- Spawning FARPs is special in DCS. Therefore, we need to specify that this is a FARP. We also set the callsign and the frequency. +-- SpawnStaticFarp:InitFARP(FARPName, FARPFreq, 0) +-- SpawnStaticFarp:InitDead(false) +-- +-- -- Spawn FARP +-- local ZoneSpawn = ZONE_RADIUS:New("FARP "..FName,Coordinate:GetVec2(),160,false) +-- local Heading = 0 +-- local FarpBerlin=SpawnStaticFarp:SpawnFromZone(ZoneSpawn, Heading, "FARP "..FName) +-- +-- -- ATC and services - put them 125m from the center of the zone towards North +-- local FarpVehicles = SPAWN:NewWithAlias("FARP Vehicles Template","FARP "..FName.." Technicals") +-- FarpVehicles:InitHeading(180) +-- local FarpVCoord = coord:Translate(125,0) +-- FarpVehicles:SpawnFromCoordinate(FarpVCoord) +-- +-- -- We will put the rest of the statics in a nice circle around the center +-- local base = 330 +-- local delta = 30 +-- +-- local windsock = SPAWNSTATIC:NewFromStatic("Static Windsock-1",country.id.USA) +-- local sockcoord = coord:Translate(125,base) +-- windsock:SpawnFromCoordinate(sockcoord,Heading,"Windsock "..FName) +-- base=base-delta +-- +-- local fueldepot = SPAWNSTATIC:NewFromStatic("Static FARP Fuel Depot-1",country.id.USA) +-- local fuelcoord = coord:Translate(125,base) +-- fueldepot:SpawnFromCoordinate(fuelcoord,Heading,"Fueldepot "..FName) +-- base=base-delta +-- +-- local ammodepot = SPAWNSTATIC:NewFromStatic("Static FARP Ammo Storage-2-1",country.id.USA) +-- local ammocoord = coord:Translate(125,base) +-- ammodepot:SpawnFromCoordinate(ammocoord,Heading,"Ammodepot "..FName) +-- base=base-delta +-- +-- local CommandPost = SPAWNSTATIC:NewFromStatic("Static FARP Command Post-1",country.id.USA) +-- local CommandCoord = coord:Translate(125,base) +-- CommandPost:SpawnFromCoordinate(CommandCoord,Heading,"Command Post "..FName) +-- base=base-delta +-- +-- local Tent1 = SPAWNSTATIC:NewFromStatic("Static FARP Tent-11",country.id.USA) +-- local Tent1Coord = coord:Translate(125,base) +-- Tent1:SpawnFromCoordinate(Tent1Coord,Heading,"Command Tent "..FName) +-- base=base-delta +-- +-- local Tent2 = SPAWNSTATIC:NewFromStatic("Static FARP Tent-11",country.id.USA) +-- local Tent2Coord = coord:Translate(125,base) +-- Tent2:SpawnFromCoordinate(Tent2Coord,Heading,"Command Tent2 "..FName) +-- +-- -- add a loadzone to CTLD +-- my_ctld:AddCTLDZone("FARP "..FName,CTLD.CargoZoneType.LOAD,SMOKECOLOR.Blue,true,true) +-- local m = MESSAGE:New(string.format("FARP %s in operation!",FName),15,"CTLD"):ToBlue() +-- end +-- +-- function my_ctld:OnAfterCratesBuild(From,Event,To,Group,Unit,Vehicle) +-- local name = Vehicle:GetName() +-- if string.match(name,"FOB",1,true) then +-- local Coord = Vehicle:GetCoordinate() +-- Vehicle:Destroy(false) +-- BuildAFARP(Coord) +-- end +-- end +-- +-- -- @field #CTLD CTLD = { ClassName = "CTLD", - verbose = 0, - lid = "", + verbose = 0, + lid = "", coalition = 1, coalitiontxt = "blue", PilotGroups = {}, -- #GROUP_SET of heli pilots @@ -118396,18 +127857,16 @@ CTLD = { FreeUHFFrequencies = {}, -- Table of UHF FreeFMFrequencies = {}, -- Table of FM CargoCounter = 0, - dropOffZones = {}, - wpZones = {}, Cargo_Troops = {}, -- generic troops objects Cargo_Crates = {}, -- generic crate objects Loaded_Cargo = {}, -- cargo aboard units Spawned_Crates = {}, -- Holds objects for crates spawned generally Spawned_Cargo = {}, -- Binds together spawned_crates and their CTLD_CARGO objects - CrateDistance = 30, -- list crates in this radius + CrateDistance = 35, -- list crates in this radius debug = false, wpZones = {}, - pickupZones = {}, dropOffZones = {}, + pickupZones = {}, } ------------------------------ @@ -118415,20 +127874,34 @@ CTLD = { -- DONE: TEST Hover load and unload -- DONE: Crate unload -- DONE: Hover (auto-)load --- TODO: (More) Housekeeping +-- DONE: (More) Housekeeping -- DONE: Troops running to WP Zone -- DONE: Zone Radio Beacons -- DONE: Stats Running -- DONE: Added support for Hercules -- TODO: Possibly - either/or loading crates and troops --- TODO: Limit of troops, crates buildable? +-- DONE: Make inject respect existing cargo types +-- DONE: Drop beacons or flares/smoke +-- DONE: Add statics as cargo +-- DONE: List cargo in stock +-- DONE: Limit of troops, crates buildable? +-- DONE: Allow saving of Troops & Vehicles ------------------------------ --- Radio Beacons -- @type CTLD.ZoneBeacon -- @field #string name -- Name of zone for the coordinate -- @field #number frequency -- in mHz --- @field #number modulation -- i.e.radio.modulation.FM or radio.modulation.AM +-- @field #number modulation -- i.e.CTLD.RadioModulation.FM or CTLD.RadioModulation.AM + +--- Radio Modulation +-- @type CTLD.RadioModulation +-- @field #number AM +-- @field #number FM +CTLD.RadioModulation = { + AM = 0, + FM = 1, +} --- Zone Info. -- @type CTLD.CargoZone @@ -118442,6 +127915,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 @@ -118450,6 +127924,7 @@ CTLD.CargoZoneType = { DROP = "drop", MOVE = "move", SHIP = "ship", + BEACON = "beacon", } --- Buildable table info. @@ -118468,23 +127943,29 @@ 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}, - ["SA342L"] = {type="SA342L", crates=false, troops=true, cratelimit = 0, trooplimit = 2}, - ["SA342M"] = {type="SA342M", crates=false, troops=true, cratelimit = 0, trooplimit = 4}, - ["SA342Minigun"] = {type="SA342Minigun", crates=false, troops=true, cratelimit = 0, trooplimit = 2}, - ["UH-1H"] = {type="UH-1H", crates=true, troops=true, cratelimit = 1, trooplimit = 8}, - ["Mi-8MTV2"] = {type="Mi-8MTV2", crates=true, troops=true, cratelimit = 2, trooplimit = 12}, - ["Mi-8MT"] = {type="Mi-8MTV2", crates=true, troops=true, cratelimit = 2, trooplimit = 12}, - ["Ka-50"] = {type="Ka-50", crates=false, troops=false, cratelimit = 0, trooplimit = 0}, - ["Mi-24P"] = {type="Mi-24P", crates=true, troops=true, cratelimit = 2, trooplimit = 8}, - ["Mi-24V"] = {type="Mi-24V", crates=true, troops=true, cratelimit = 2, trooplimit = 8}, - ["Hercules"] = {type="Hercules", crates=true, troops=true, cratelimit = 7, trooplimit = 64}, -- 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-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}, + ["Ka-50_3"] = {type="Ka-50_3", 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 + ["Bronco-OV-10A"] = {type="Bronco-OV-10A", crates= false, troops=true, cratelimit = 0, trooplimit = 5, length = 13, cargoweightlimit = 1450}, } --- CTLD class version. -- @field #string version -CTLD.version="0.1.5a1" +CTLD.version="1.0.29" --- Instantiate a new CTLD. -- @param #CTLD self @@ -118538,7 +128019,7 @@ function CTLD:New(Coalition, Prefixes, Alias) self:SetStartState("Stopped") -- Add FSM transitions. - -- From State --> Event --> To State + -- From State --> Event --> To State self:AddTransition("Stopped", "Start", "Running") -- Start FSM. self:AddTransition("*", "Status", "*") -- CTLD status update. self:AddTransition("*", "TroopsPickedUp", "*") -- CTLD pickup event. @@ -118548,7 +128029,9 @@ function CTLD:New(Coalition, Prefixes, Alias) self:AddTransition("*", "TroopsRTB", "*") -- CTLD deploy event. self:AddTransition("*", "CratesDropped", "*") -- CTLD deploy event. self:AddTransition("*", "CratesBuild", "*") -- CTLD build event. - self:AddTransition("*", "CratesRepaired", "*") -- CTLD repair event. + self:AddTransition("*", "CratesRepaired", "*") -- CTLD repair event. + self:AddTransition("*", "Load", "*") -- CTLD load event. + self:AddTransition("*", "Save", "*") -- CTLD save event. self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. -- tables @@ -118565,16 +128048,21 @@ function CTLD:New(Coalition, Prefixes, Alias) -- radio beacons self.RadioSound = "beacon.ogg" + self.RadioPath = "l10n/DEFAULT/" -- zones stuff self.pickupZones = {} self.dropOffZones = {} self.wpZones = {} self.shipZones = {} + self.droppedBeacons = {} + self.droppedbeaconref = {} + self.droppedbeacontimeout = 600 -- Cargo self.Cargo_Crates = {} self.Cargo_Troops = {} + self.Cargo_Statics = {} self.Loaded_Cargo = {} self.Spawned_Crates = {} self.Spawned_Cargo = {} @@ -118585,11 +128073,16 @@ function CTLD:New(Coalition, Prefixes, Alias) self.CrateCounter = 0 self.TroopCounter = 0 + -- added engineering + self.Engineers = 0 -- #number use as counter + self.EngineersInField = {} -- #table holds #CTLD_ENGINEERING objects + self.EngineerSearch = 2000 -- #number search distance for crates to build or repair + self.nobuildmenu = false -- enfore engineer build only? + -- setup - self.CrateDistance = 30 -- list/load crates in this radius + self.CrateDistance = 35 -- list/load crates in this radius self.ExtractFactor = 3.33 -- factor for troops extraction, i.e. CrateDistance * Extractfactor self.prefixes = Prefixes or {"Cargoheli"} - --self.I({prefixes = self.prefixes}) self.useprefix = true self.maximumHoverHeight = 15 @@ -118601,6 +128094,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 @@ -118611,8 +128105,51 @@ function CTLD:New(Coalition, Prefixes, Alias) -- message suppression self.suppressmessages = false - -- time to repair a unit/group + -- time to repairor build a unit/group self.repairtime = 300 + self.buildtime = 300 + + -- place spawned crates in front of aircraft + self.placeCratesAhead = false + + -- country of crates spawned + self.cratecountry = country.id.GERMANY + + -- for opening doors + self.pilotmustopendoors = false + + if self.coalition == coalition.side.RED then + self.cratecountry = country.id.RUSSIA + end + + -- load and save dropped TROOPS + self.enableLoadSave = false + self.filepath = nil + self.saveinterval = 600 + self.eventoninject = true + + -- sub categories + self.usesubcats = false + self.subcats = {} + + -- disallow building in loadzones + self.nobuildinloadzones = true + self.movecratesbeforebuild = true + self.surfacetypes = {land.SurfaceType.LAND,land.SurfaceType.ROAD,land.SurfaceType.RUNWAY,land.SurfaceType.SHALLOW_WATER} + + local AliaS = string.gsub(self.alias," ","_") + self.filename = string.format("CTLD_%s_Persist.csv",AliaS) + + -- allow re-pickup crates + self.allowcratepickupagain = true + + -- 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() @@ -118622,7 +128159,7 @@ function CTLD:New(Coalition, Prefixes, Alias) self:_GenerateUHFrequencies() self:_GenerateFMFrequencies() - ------------------------ + ------------------------ --- Pseudo Functions --- ------------------------ @@ -118652,6 +128189,110 @@ function CTLD:New(Coalition, Prefixes, Alias) -- @param #CTLD self -- @param #number delay Delay in seconds. + --- Triggers the FSM event "Load". + -- @function [parent=#CTLD] Load + -- @param #CTLD self + + --- Triggers the FSM event "Load" after a delay. + -- @function [parent=#CTLD] __Load + -- @param #CTLD self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Save". + -- @function [parent=#CTLD] Load + -- @param #CTLD self + + --- Triggers the FSM event "Save" after a delay. + -- @function [parent=#CTLD] __Save + -- @param #CTLD self + -- @param #number delay Delay in seconds. + + --- FSM Function OnBeforeTroopsPickedUp. + -- @function [parent=#CTLD] OnBeforeTroopsPickedUp + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param #CTLD_CARGO Cargo Cargo troops. + -- @return #CTLD self + + --- FSM Function OnBeforeTroopsExtracted. + -- @function [parent=#CTLD] OnBeforeTroopsExtracted + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param #CTLD_CARGO Cargo Cargo troops. + -- @return #CTLD self + + --- FSM Function OnBeforeCratesPickedUp. + -- @function [parent=#CTLD] OnBeforeCratesPickedUp + -- @param #CTLD self + -- @param #string From State . + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param #CTLD_CARGO Cargo Cargo crate. + -- @return #CTLD self + + --- FSM Function OnBeforeTroopsDeployed. + -- @function [parent=#CTLD] OnBeforeTroopsDeployed + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param Wrapper.Group#GROUP Troops Troops #GROUP Object. + -- @return #CTLD self + + --- FSM Function OnBeforeCratesDropped. + -- @function [parent=#CTLD] OnBeforeCratesDropped + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param #table Cargotable Table of #CTLD_CARGO objects dropped. + -- @return #CTLD self + + --- FSM Function OnBeforeCratesBuild. + -- @function [parent=#CTLD] OnBeforeCratesBuild + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param Wrapper.Group#GROUP Vehicle The #GROUP object of the vehicle or FOB build. + -- @return #CTLD self + + --- FSM Function OnBeforeCratesRepaired. + -- @function [parent=#CTLD] OnBeforeCratesRepaired + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param Wrapper.Group#GROUP Vehicle The #GROUP object of the vehicle or FOB repaired. + -- @return #CTLD self + + --- FSM Function OnBeforeTroopsRTB. + -- @function [parent=#CTLD] OnBeforeTroopsRTB + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + --- FSM Function OnAfterTroopsPickedUp. -- @function [parent=#CTLD] OnAfterTroopsPickedUp -- @param #CTLD self @@ -118738,6 +128379,24 @@ function CTLD:New(Coalition, Prefixes, Alias) -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. + --- FSM Function OnAfterLoad. + -- @function [parent=#CTLD] OnAfterLoad + -- @param #CTLD self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string path (Optional) Path where the file is located. 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 loading. Default is "CTLD__Persist.csv". + + --- FSM Function OnAfterSave. + -- @function [parent=#CTLD] OnAfterSave + -- @param #CTLD self + -- @param #string From From state. + -- @param #string Event Event. + -- @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. Default is "CTLD__Persist.csv". + return self end @@ -118762,6 +128421,8 @@ function CTLD:_GetUnitCapabilities(Unit) capabilities.cratelimit = 0 capabilities.trooplimit = 0 capabilities.type = "generic" + capabilities.length = 20 + capabilities.cargoweightlimit = 0 end return capabilities end @@ -118795,6 +128456,30 @@ 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 + +--- (User) Add a PLAYERTASK - FSM events will check success +-- @param #CTLD self +-- @param Ops.PlayerTask#PLAYERTASK PlayerTask +-- @return #CTLD self +function CTLD:AddPlayerTask(PlayerTask) + self:T(self.lid .. " AddPlayerTask") + if not self.PlayerTaskQueue then + self.PlayerTaskQueue = FIFO:New() + end + self.PlayerTaskQueue:Push(PlayerTask,PlayerTask.PlayerTaskNr) + return self +end + --- (Internal) Event handler function -- @param #CTLD self -- @param Core.Event#EVENTDATA EventData @@ -118815,8 +128500,8 @@ function CTLD:_EventHandler(EventData) self:_RefreshF10Menus() end -- Herc support - --self:T_unit:GetTypeName()) - if _unit:GetTypeName() == "Hercules" and self.enableHercules then + if self:IsHercules(_unit) and self.enableHercules then + local unitname = event.IniUnitName or "none" self.Loaded_Cargo[unitname] = nil self:_RefreshF10Menus() end @@ -118844,13 +128529,66 @@ function CTLD:_SendMessage(Text, Time, Clearscreen, Group) return self end +--- (Internal) Find a troops CTLD_CARGO object in stock +-- @param #CTLD self +-- @param #string Name of the object +-- @return #CTLD_CARGO Cargo object, nil if it cannot be found +function CTLD:_FindTroopsCargoObject(Name) + self:T(self.lid .. " _FindTroopsCargoObject") + local cargo = nil + for _,_cargo in pairs(self.Cargo_Troops)do + local cargo = _cargo -- #CTLD_CARGO + if cargo.Name == Name then + return cargo + end + end + return nil +end + +--- (User) Pre-load troops into a helo, e.g. for airstart. Unit **must** be alive in-game, i.e. player has taken the slot! +-- @param #CTLD self +-- @param Wrapper.Unit#UNIT Unit The unit to load into, can be handed as Wrapper.Client#CLIENT object +-- @param #string Troopname The name of the Troops to be loaded. Must be created prior in the CTLD setup! +-- @return #CTLD self +-- @usage +-- local client = UNIT:FindByName("Helo-1-1") +-- if client and client:IsAlive() then +-- myctld:PreloadTroops(client,"Infantry") +-- end +function CTLD:PreloadTroops(Unit,Troopname) + self:T(self.lid .. " PreloadTroops") + local name = Troopname or "Unknown" + if Unit and Unit:IsAlive() then + local cargo = self:_FindTroopsCargoObject(name) + local group = Unit:GetGroup() + if cargo then + self:_LoadTroops(group,Unit,cargo,true) + else + self:E(self.lid.." Troops preload - Cargo Object "..name.." not found!") + end + end + return self +end + --- (Internal) Function to load troops into a heli. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Unit#UNIT Unit -- @param #CTLD_CARGO Cargotype -function CTLD:_LoadTroops(Group, Unit, Cargotype) +-- @param #boolean Inject +function CTLD:_LoadTroops(Group, Unit, Cargotype, Inject) self:T(self.lid .. " _LoadTroops") + -- check if we have stock + local instock = Cargotype:GetStock() + local cgoname = Cargotype:GetName() + local cgotype = Cargotype:GetType() + local cgonetmass = Cargotype:GetNetMass() + local maxloadable = self:_GetMaxLoadableMass(Unit) + if type(instock) == "number" and tonumber(instock) <= 0 and tonumber(instock) ~= -1 and not Inject then + -- nothing left over + self:_SendMessage(string.format("Sorry, all %s are gone!", cgoname), 10, false, Group) + return self + end -- landed or hovering over load zone? local grounded = not self:IsUnitInAir(Unit) local hoverload = self:CanHoverLoad(Unit) @@ -118859,12 +128597,17 @@ function CTLD:_LoadTroops(Group, Unit, Cargotype) if not inzone then inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP) end - if not inzone then - self:_SendMessage("You are not close enough to a logistics zone!", 10, false, Group) - if not self.debug then return self end - elseif not grounded and not hoverload then - self:_SendMessage("You need to land or hover in position to load!", 10, false, Group) - if not self.debug then return self end + if not Inject then + if not inzone then + self:_SendMessage("You are not close enough to a logistics zone!", 10, false, Group) + if not self.debug then return self end + elseif not grounded and not hoverload then + self:_SendMessage("You need to land or hover in position to load!", 10, false, Group) + if not self.debug then return self end + elseif self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then + self:_SendMessage("You need to open the door(s) to load troops!", 10, false, Group) + if not self.debug then return self end + end end -- load troops into heli local group = Group -- Wrapper.Group#GROUP @@ -118893,9 +128636,12 @@ function CTLD:_LoadTroops(Group, Unit, Cargotype) if troopsize + numberonboard > trooplimit then self:_SendMessage("Sorry, we\'re crammed already!", 10, false, Group) return + elseif maxloadable < cgonetmass then + self:_SendMessage("Sorry, that\'s too heavy to load!", 10, false, Group) + return else self.CargoCounter = self.CargoCounter + 1 - local loadcargotype = CTLD_CARGO:New(self.CargoCounter, Cargotype.Name, Cargotype.Templates, CTLD_CARGO.Enum.TROOPS, true, true, Cargotype.CratesNeeded,nil,nil,Cargotype.PerCrateMass) + local loadcargotype = CTLD_CARGO:New(self.CargoCounter, Cargotype.Name, Cargotype.Templates, cgotype, true, true, Cargotype.CratesNeeded,nil,nil,Cargotype.PerCrateMass) self:T({cargotype=loadcargotype}) loaded.Troopsloaded = loaded.Troopsloaded + troopsize table.insert(loaded.Cargo,loadcargotype) @@ -118903,6 +128649,7 @@ function CTLD:_LoadTroops(Group, Unit, Cargotype) self:_SendMessage("Troops boarded!", 10, false, Group) self:__TroopsPickedUp(1,Group, Unit, Cargotype) self:_UpdateUnitCargoMass(Unit) + Cargotype:RemoveStock() end return self end @@ -118914,24 +128661,25 @@ function CTLD:_FindRepairNearby(Group, Unit, Repairtype) -- find nearest group of deployed groups local nearestGroup = nil local nearestGroupIndex = -1 - local nearestDistance = 10000000 + local nearestDistance = 10000 for k,v in pairs(self.DroppedTroops) do local distance = self:_GetDistance(v:GetCoordinate(),unitcoord) - if distance < nearestDistance and distance ~= -1 then + local unit = v:GetUnit(1) -- Wrapper.Unit#UNIT + local desc = unit:GetDesc() or nil + if distance < nearestDistance and distance ~= -1 and not desc.attributes.Infantry then nearestGroup = v nearestGroupIndex = k nearestDistance = distance end end - + -- found one and matching distance? - if nearestGroup == nil or nearestDistance > 1000 then + if nearestGroup == nil or nearestDistance > self.EngineerSearch then self:_SendMessage("No unit close enough to repair!", 10, false, Group) return nil, nil end local groupname = nearestGroup:GetName() - --self:I(string.format("***** Found Group %s",groupname)) -- helper to find matching template local function matchstring(String,Table) @@ -118954,7 +128702,6 @@ function CTLD:_FindRepairNearby(Group, Unit, Repairtype) -- walk through generics and find matching type local Cargotype = nil for k,v in pairs(self.Cargo_Crates) do - --self:I({groupname,v.Templates}) if matchstring(groupname,v.Templates) and matchstring(groupname,Repairtype) then Cargotype = v -- #CTLD_CARGO break @@ -118962,7 +128709,6 @@ function CTLD:_FindRepairNearby(Group, Unit, Repairtype) end if Cargotype == nil then - --self:_SendMessage("Can't find a matching group for " .. Repairtype, 10, false, Group) return nil, nil else return nearestGroup, Cargotype @@ -118977,18 +128723,18 @@ end -- @param #table Crates Table of #CTLD_CARGO objects near the unit. -- @param #CTLD.Buildable Build Table build object. -- @param #number Number Number of objects in Crates (found) to limit search. -function CTLD:_RepairObjectFromCrates(Group,Unit,Crates,Build,Number) +-- @param #boolean Engineering If true it is an Engineering repair. +function CTLD:_RepairObjectFromCrates(Group,Unit,Crates,Build,Number,Engineering) self:T(self.lid .. " _RepairObjectFromCrates") local build = Build -- -- #CTLD.Buildable - --self:I({Build=Build}) local Repairtype = build.Template -- #string local NearestGroup, CargoType = self:_FindRepairNearby(Group,Unit,Repairtype) -- Wrapper.Group#GROUP, #CTLD_CARGO - --self:I({Repairtype=Repairtype, CargoType=CargoType, NearestGroup=NearestGroup}) if NearestGroup ~= nil then if self.repairtime < 2 then self.repairtime = 30 end -- noob catch - self:_SendMessage(string.format("Repair started using %s taking %d secs", build.Name, self.repairtime), 10, false, Group) + if not Engineering then + self:_SendMessage(string.format("Repair started using %s taking %d secs", build.Name, self.repairtime), 10, false, Group) + end -- now we can build .... - --NearestGroup:Destroy(false) local name = CargoType:GetName() local required = CargoType:GetCratesNeeded() local template = CargoType:GetTemplates() @@ -119005,9 +128751,12 @@ function CTLD:_RepairObjectFromCrates(Group,Unit,Crates,Build,Number) desttimer:Start(self.repairtime - 1) local buildtimer = TIMER:New(self._BuildObjectFromCrates,self,Group,Unit,object,true,NearestGroup:GetCoordinate()) buildtimer:Start(self.repairtime) - --self:_BuildObjectFromCrates(Group,Unit,object) else - self:_SendMessage("Can't repair this unit with " .. build.Name, 10, false, Group) + if not Engineering then + self:_SendMessage("Can't repair this unit with " .. build.Name, 10, false, Group) + else + self:T("Can't repair this unit with " .. build.Name) + end end return self end @@ -119026,6 +128775,10 @@ end self:_SendMessage("You need to land or hover in position to load!", 10, false, Group) if not self.debug then return self end end + if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then + self:_SendMessage("You need to open the door(s) to extract troops!", 10, false, Group) + if not self.debug then return self end + end -- load troops into heli local unit = Unit -- Wrapper.Unit#UNIT local unitname = unit:GetName() @@ -119040,67 +128793,96 @@ end local nearestGroup = nil local nearestGroupIndex = -1 local nearestDistance = 10000000 + local nearestList = {} + local distancekeys = {} + local extractdistance = self.CrateDistance * self.ExtractFactor for k,v in pairs(self.DroppedTroops) do local distance = self:_GetDistance(v:GetCoordinate(),unitcoord) - if distance < nearestDistance and distance ~= -1 then + if distance <= extractdistance and distance ~= -1 then nearestGroup = v nearestGroupIndex = k nearestDistance = distance + table.insert(nearestList, math.floor(distance), v) + distancekeys[#distancekeys+1] = math.floor(distance) end end - local extractdistance = self.CrateDistance * self.ExtractFactor - if nearestGroup == nil or nearestDistance > extractdistance then self:_SendMessage("No units close enough to extract!", 10, false, Group) return self end - -- find matching cargo type - local groupType = string.match(nearestGroup:GetName(), "(.+)-(.+)$") - local Cargotype = nil - for k,v in pairs(self.Cargo_Troops) do - if v.Name == groupType then - Cargotype = v - break + + -- sort reference keys + table.sort(distancekeys) + + local secondarygroups = {} + + for i=1,#distancekeys do + local nearestGroup = nearestList[distancekeys[i]] + -- find matching cargo type + local groupType = string.match(nearestGroup:GetName(), "(.+)-(.+)$") + local Cargotype = nil + for k,v in pairs(self.Cargo_Troops) do + local comparison = "" + if type(v.Templates) == "string" then comparison = v.Templates else comparison = v.Templates[1] end + if comparison == groupType then + Cargotype = v + break + end + end + if Cargotype == nil then + self:_SendMessage("Can't onboard " .. groupType, 10, false, Group) + else + + local troopsize = Cargotype:GetCratesNeeded() -- #number + -- have we loaded stuff already? + local numberonboard = 0 + local loaded = {} + if self.Loaded_Cargo[unitname] then + loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo + numberonboard = loaded.Troopsloaded or 0 + else + loaded = {} -- #CTLD.LoadedCargo + loaded.Troopsloaded = 0 + loaded.Cratesloaded = 0 + loaded.Cargo = {} + end + if troopsize + numberonboard > trooplimit then + self:_SendMessage("Sorry, we\'re crammed already!", 10, false, Group) + --return self + else + self.CargoCounter = self.CargoCounter + 1 + local loadcargotype = CTLD_CARGO:New(self.CargoCounter, Cargotype.Name, Cargotype.Templates, Cargotype.CargoType, true, true, Cargotype.CratesNeeded,nil,nil,Cargotype.PerCrateMass) + self:T({cargotype=loadcargotype}) + loaded.Troopsloaded = loaded.Troopsloaded + troopsize + table.insert(loaded.Cargo,loadcargotype) + self.Loaded_Cargo[unitname] = loaded + self:_SendMessage("Troops boarded!", 10, false, Group) + self:_UpdateUnitCargoMass(Unit) + self:__TroopsExtracted(1,Group, Unit, nearestGroup) + + -- clean up: + if type(Cargotype.Templates) == "table" and Cargotype.Templates[2] then + for _,_key in pairs (Cargotype.Templates) do + table.insert(secondarygroups,_key) + end + end + nearestGroup:Destroy(false) + end end end - - if Cargotype == nil then - self:_SendMessage("Can't find a matching cargo type for " .. groupType, 10, false, Group) - return self - end - - local troopsize = Cargotype:GetCratesNeeded() -- #number - -- have we loaded stuff already? - local numberonboard = 0 - local loaded = {} - if self.Loaded_Cargo[unitname] then - loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo - numberonboard = loaded.Troopsloaded or 0 - else - loaded = {} -- #CTLD.LoadedCargo - loaded.Troopsloaded = 0 - loaded.Cratesloaded = 0 - loaded.Cargo = {} - end - if troopsize + numberonboard > trooplimit then - self:_SendMessage("Sorry, we\'re crammed already!", 10, false, Group) - return - else - self.CargoCounter = self.CargoCounter + 1 - local loadcargotype = CTLD_CARGO:New(self.CargoCounter, Cargotype.Name, Cargotype.Templates, CTLD_CARGO.Enum.TROOPS, true, true, Cargotype.CratesNeeded,nil,nil,Cargotype.PerCrateMass) - self:T({cargotype=loadcargotype}) - loaded.Troopsloaded = loaded.Troopsloaded + troopsize - table.insert(loaded.Cargo,loadcargotype) - self.Loaded_Cargo[unitname] = loaded - self:_SendMessage("Troops boarded!", 10, false, Group) - self:_UpdateUnitCargoMass(Unit) - self:__TroopsExtracted(1,Group, Unit, nearestGroup) - - -- clean up: - table.remove(self.DroppedTroops, nearestGroupIndex) - nearestGroup:Destroy(false) + -- clean up secondary groups + for _,_name in pairs(secondarygroups) do + for _,_group in pairs(nearestList) do + if _group and _group:IsAlive() then + local groupname = string.match(_group:GetName(), "(.+)-(.+)$") + if _name == groupname then + _group:Destroy(false) + end + end + end end + self:CleanDroppedTroops() return self end @@ -119113,12 +128895,23 @@ end -- @param #boolean drop If true we\'re dropping from heli rather than loading. function CTLD:_GetCrates(Group, Unit, Cargo, number, drop) self:T(self.lid .. " _GetCrates") - local cgoname = Cargo:GetName() - -- check if we are in LOAD zone + if not drop then + local cgoname = Cargo:GetName() + -- check if we have stock + local instock = Cargo:GetStock() + if type(instock) == "number" and tonumber(instock) <= 0 and tonumber(instock) ~= -1 then + -- nothing left over + self:_SendMessage(string.format("Sorry, we ran out of %s", cgoname), 10, false, Group) + return self + end + end + -- check if we are in LOAD zone local inzone = false 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 @@ -119140,43 +128933,76 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop) -- avoid crate spam local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitCapabilities local canloadcratesno = capabilities.cratelimit - local loaddist = self.CrateDistance or 30 - local nearcrates, numbernearby = self:_FindCratesNearby(Group,Unit,loaddist) + local loaddist = self.CrateDistance or 35 + 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 end -- spawn crates in front of helicopter local IsHerc = self:IsHercules(Unit) -- Herc - local cargotype = Cargo -- #CTLD_CARGO + local cargotype = Cargo -- Ops.CTLD#CTLD_CARGO local number = number or cargotype:GetCratesNeeded() --#number local cratesneeded = cargotype:GetCratesNeeded() --#number local cratename = cargotype:GetName() local cratetemplate = "Container"-- #string + local cgotype = cargotype:GetType() + local cgomass = cargotype:GetMass() + local isstatic = false + if cgotype == CTLD_CARGO.Enum.STATIC then + cratetemplate = cargotype:GetTemplates() + isstatic = true + end -- get position and heading of heli local position = Unit:GetCoordinate() local heading = Unit:GetHeading() + 1 local height = Unit:GetHeight() local droppedcargo = {} + local cratedistance = 0 + local rheading = 0 + local angleOffNose = 0 + local addon = 0 + if IsHerc then + -- spawn behind the Herc + addon = 180 + end -- loop crates needed for i=1,number do local cratealias = string.format("%s-%d", cratetemplate, math.random(1,100000)) - local cratedistance = i*4 + 8 - if IsHerc then - -- wider radius - cratedistance = i*4 + 12 - end - for i=1,50 do - math.random(90,270) - end - local rheading = math.floor(((math.random(90,270) * heading) + 1) / 360) - if not IsHerc then - rheading = rheading + 180 -- mirror for Helis + if not self.placeCratesAhead then + cratedistance = (i-1)*2.5 + capabilities.length + if cratedistance > self.CrateDistance then cratedistance = self.CrateDistance end + -- altered heading logic + -- DONE: right standard deviation? + rheading = UTILS.RandomGaussian(0,30,-90,90,100) + rheading = math.fmod((heading + rheading + addon), 360) + else + local initialSpacing = IsHerc and 16 or 12 -- initial spacing of the first crates + local crateSpacing = 4 -- further spacing of remaining crates + local lateralSpacing = 4 -- lateral spacing of crates + local nrSideBySideCrates = 3 -- number of crates that are placed side-by-side + + if cratesneeded == 1 then + -- single crate needed spawns straight ahead + cratedistance = initialSpacing + rheading = heading + else + if (i - 1) % nrSideBySideCrates == 0 then + cratedistance = i == 1 and initialSpacing or cratedistance + crateSpacing + angleOffNose = math.ceil(math.deg(math.atan(lateralSpacing / cratedistance))) + rheading = heading - angleOffNose + else + rheading = rheading + angleOffNose + end + end end - if rheading > 360 then rheading = rheading - 360 end -- catch > 360 local cratecoord = position:Translate(cratedistance,rheading) local cratevec2 = cratecoord:GetVec2() self.CrateCounter = self.CrateCounter + 1 + local basetype = self.basetype or "container_cargo" + if isstatic then + basetype = cratetemplate + end if type(ship) == "string" then self:T("Spawning on ship "..ship) local Ship = UNIT:FindByName(ship) @@ -119186,35 +129012,100 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop) dist = dist - (20 + math.random(1,10)) local width = width / 2 local Offy = math.random(-width,width) - self.Spawned_Crates[self.CrateCounter] = SPAWNSTATIC:NewFromType("container_cargo","Cargos",country.id.GERMANY) - --:InitCoordinate(cratecoord) + self.Spawned_Crates[self.CrateCounter] = SPAWNSTATIC:NewFromType(basetype,"Cargos",self.cratecountry) + :InitCargoMass(cgomass) + :InitCargo(self.enableslingload) :InitLinkToUnit(Ship,dist,Offy,0) :Spawn(270,cratealias) else - self.Spawned_Crates[self.CrateCounter] = SPAWNSTATIC:NewFromType("container_cargo","Cargos",country.id.GERMANY) - :InitCoordinate(cratecoord) - --:InitLinkToUnit(Unit,OffsetX,OffsetY,OffsetAngle) - :Spawn(270,cratealias) + self.Spawned_Crates[self.CrateCounter] = SPAWNSTATIC:NewFromType(basetype,"Cargos",self.cratecountry) + :InitCoordinate(cratecoord) + :InitCargoMass(cgomass) + :InitCargo(self.enableslingload) + :Spawn(270,cratealias) end local templ = cargotype:GetTemplates() local sorte = cargotype:GetType() - self.CargoCounter = self.CargoCounter +1 + 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) + --CTLD_CARGO:New(ID, Name, Templates, Sorte, HasBeenMoved, LoadDirectly, CratesNeeded, Positionable, Dropped, PerCrateMass, Stock, Subcategory) + realcargo = CTLD_CARGO:New(self.CargoCounter,cratename,templ,sorte,true,false,cratesneeded,self.Spawned_Crates[self.CrateCounter],true,cargotype.PerCrateMass,nil,subcat) table.insert(droppedcargo,realcargo) else - realcargo = CTLD_CARGO:New(self.CargoCounter,cratename,templ,sorte,false,false,cratesneeded,self.Spawned_Crates[self.CrateCounter],nil,cargotype.PerCrateMass) + realcargo = CTLD_CARGO:New(self.CargoCounter,cratename,templ,sorte,false,false,cratesneeded,self.Spawned_Crates[self.CrateCounter],false,cargotype.PerCrateMass,nil,subcat) + Cargo:RemoveStock() end table.insert(self.Spawned_Cargo, realcargo) end - local text = string.format("Crates for %s have been positioned near you!",cratename) - if drop then - text = string.format("Crates for %s have been dropped!",cratename) - self:__CratesDropped(1, Group, Unit, droppedcargo) - end - self:_SendMessage(text, 10, false, Group) + local text = string.format("Crates for %s have been positioned near you!",cratename) + if drop then + text = string.format("Crates for %s have been dropped!",cratename) + self:__CratesDropped(1, Group, Unit, droppedcargo) + end + self:_SendMessage(text, 10, false, Group) + return self +end + +--- (Internal) Inject crates and static cargo objects. +-- @param #CTLD self +-- @param Core.Zone#ZONE Zone Zone to spawn in. +-- @param #CTLD_CARGO Cargo The cargo type to spawn. +-- @param #boolean RandomCoord Randomize coordinate. +-- @return #CTLD self +function CTLD:InjectStatics(Zone, Cargo, RandomCoord) + self:T(self.lid .. " InjectStatics") + local cratecoord = Zone:GetCoordinate() + if RandomCoord then + cratecoord = Zone:GetRandomCoordinate(5,20) + end + local surface = cratecoord:GetSurfaceType() + if surface == land.SurfaceType.WATER then return self + end + local cargotype = Cargo -- #CTLD_CARGO + --local number = 1 + local cratesneeded = cargotype:GetCratesNeeded() --#number + local cratetemplate = "Container"-- #string + local cratealias = string.format("%s-%d", cratetemplate, math.random(1,100000)) + local cratename = cargotype:GetName() + local cgotype = cargotype:GetType() + local cgomass = cargotype:GetMass() + local isstatic = false + if cgotype == CTLD_CARGO.Enum.STATIC then + cratetemplate = cargotype:GetTemplates() + isstatic = true + end + local basetype = self.basetype or "container_cargo" + if isstatic then + basetype = cratetemplate + end + self.CrateCounter = self.CrateCounter + 1 + self.Spawned_Crates[self.CrateCounter] = SPAWNSTATIC:NewFromType(basetype,"Cargos",self.cratecountry) + :InitCargoMass(cgomass) + :InitCargo(self.enableslingload) + :InitCoordinate(cratecoord) + :Spawn(270,cratealias) + local templ = cargotype:GetTemplates() + local sorte = cargotype:GetType() + self.CargoCounter = self.CargoCounter + 1 + cargotype.Positionable = self.Spawned_Crates[self.CrateCounter] + table.insert(self.Spawned_Cargo, cargotype) + return self +end + +--- (User) Inject static cargo objects. +-- @param #CTLD self +-- @param Core.Zone#ZONE Zone Zone to spawn in. Will be a somewhat random coordinate. +-- @param #string Template Unit(!) name of the static cargo object to be used as template. +-- @param #number Mass Mass of the static in kg. +-- @return #CTLD self +function CTLD:InjectStaticFromTemplate(Zone, Template, Mass) + self:T(self.lid .. " InjectStaticFromTemplate") + local cargotype = self:GetStaticsCargoFromTemplate(Template,Mass) -- #CTLD_CARGO + self:InjectStatics(Zone,cargotype,true) + return self end --- (Internal) Function to find and list nearby crates. @@ -119224,8 +129115,8 @@ end -- @return #CTLD self function CTLD:_ListCratesNearby( _group, _unit) self:T(self.lid .. " _ListCratesNearby") - local finddist = self.CrateDistance or 30 - local crates,number = self:_FindCratesNearby(_group,_unit, finddist) -- #table + local finddist = self.CrateDistance or 35 + local crates,number = self:_FindCratesNearby(_group,_unit, finddist,true) -- #table if number > 0 then local text = REPORT:New("Crates Found Nearby:") text:Add("------------------------------------------------------------") @@ -119234,9 +129125,9 @@ function CTLD:_ListCratesNearby( _group, _unit) local name = entry:GetName() --#string local dropped = entry:WasDropped() if dropped then - text:Add(string.format("Dropped crate for %s",name)) + text:Add(string.format("Dropped crate for %s, %dkg",name, entry.PerCrateMass)) else - text:Add(string.format("Crate for %s, %d Kg",name, entry.PerCrateMass)) + text:Add(string.format("Crate for %s, %dkg",name, entry.PerCrateMass)) end end if text:GetCount() == 1 then @@ -119258,9 +129149,20 @@ end function CTLD:_GetDistance(_point1, _point2) self:T(self.lid .. " _GetDistance") if _point1 and _point2 then - local distance = _point1:DistanceFromPointVec2(_point2) - return distance + local distance1 = _point1:Get2DDistance(_point2) + local distance2 = _point1:DistanceFromPointVec2(_point2) + 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 + end else + self:E("******Cannot calculate distance!") + self:E({_point1,_point2}) return -1 end end @@ -119270,9 +129172,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() @@ -119280,16 +129183,28 @@ 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 + maxloadable = self:_GetMaxLoadableMass(_unit) + 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 @@ -119309,7 +129224,7 @@ function CTLD:_LoadCratesNearby(Group, Unit) local unitname = unit:GetName() -- see if this heli can load crates local unittype = unit:GetTypeName() - local capabilities = self:_GetUnitCapabilities(Unit) + local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitCapabilities --local capabilities = self.UnitTypes[unittype] -- #CTLD.UnitCapabilities local cancrates = capabilities.crates -- #boolean local cratelimit = capabilities.cratelimit -- #number @@ -119335,6 +129250,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 @@ -119342,16 +129258,17 @@ function CTLD:_LoadCratesNearby(Group, Unit) loaded.Cargo = {} end -- get nearby crates - local finddist = self.CrateDistance or 30 - local nearcrates,number = self:_FindCratesNearby(Group,Unit,finddist) -- #table + local finddist = self.CrateDistance or 35 + 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 -- exit + return self -- exit elseif number == 0 then - self:_SendMessage("Sorry no loadable crates nearby!", 10, false, Group) - return -- exit + 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) - return -- exit + return self -- exit else -- go through crates and load local capacity = cratelimit - numberonboard @@ -119362,8 +129279,14 @@ function CTLD:_LoadCratesNearby(Group, Unit) local crateind = 0 -- get crate with largest index for _ind,_crate in pairs (nearcrates) do - if not _crate:HasMoved() and not _crate:WasDropped() and _crate:GetID() > crateind then - crateind = _crate:GetID() + if self.allowcratepickupagain then + if _crate:GetID() > crateind and _crate.Positionable ~= nil then + crateind = _crate:GetID() + end + else + if not _crate:HasMoved() and _crate:WasDropped() and _crate:GetID() > crateind then + crateind = _crate:GetID() + end end end -- load one if we found one @@ -119371,33 +129294,50 @@ function CTLD:_LoadCratesNearby(Group, Unit) local crate = nearcrates[crateind] -- #CTLD_CARGO loaded.Cratesloaded = loaded.Cratesloaded + 1 crate:SetHasMoved(true) + crate:SetWasDropped(false) table.insert(loaded.Cargo, crate) table.insert(crateidsloaded,crate:GetID()) -- destroy crate crate:GetPositionable():Destroy(false) crate.Positionable = nil self:_SendMessage(string.format("Crate ID %d for %s loaded!",crate:GetID(),crate:GetName()), 10, false, Group) + table.remove(nearcrates,crate:GetID()) self:__CratesPickedUp(1, Group, Unit, crate) end end 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 + 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 - self.Spawned_Cargo = nil - self.Spawned_Cargo = newexcrates + 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 @@ -119407,6 +129347,7 @@ end -- @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 @@ -119415,10 +129356,10 @@ function CTLD:_GetUnitCargoMass(Unit) for _,_cargo in pairs(cargotable) do local cargo = _cargo -- #CTLD_CARGO local type = cargo:GetType() -- #CTLD_CARGO.Enum - if type == CTLD_CARGO.Enum.TROOPS and not cargo:WasDropped() then + if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) and not cargo:WasDropped() then loadedmass = loadedmass + (cargo.PerCrateMass * cargo:GetCratesNeeded()) end - if type ~= CTLD_CARGO.Enum.TROOPS and not cargo:WasDropped() then + if type ~= CTLD_CARGO.Enum.TROOPS and type ~= CTLD_CARGO.Enum.ENGINEERS and not cargo:WasDropped() then loadedmass = loadedmass + cargo.PerCrateMass end end @@ -119426,6 +129367,21 @@ function CTLD:_GetUnitCargoMass(Unit) return loadedmass end +--- (Internal) Function to calculate max loadable mass left over. +-- @param #CTLD self +-- @param Wrapper.Unit#UNIT Unit +-- @return #number maxloadable Max loadable mass in kg +function CTLD:_GetMaxLoadableMass(Unit) + self:T(self.lid .. " _GetMaxLoadableMass") + if not Unit then return 0 end + local loadable = 0 + local loadedmass = self:_GetUnitCargoMass(Unit) + local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitCapabilities + local maxmass = capabilities.cargoweightlimit or 2000 -- max 2 tons + loadable = maxmass - loadedmass + return loadable +end + --- (Internal) Function to calculate and set Unit internal cargo mass -- @param #CTLD self -- @param Wrapper.Unit#UNIT Unit @@ -119433,9 +129389,6 @@ function CTLD:_UpdateUnitCargoMass(Unit) self:T(self.lid .. " _UpdateUnitCargoMass") local calculatedMass = self:_GetUnitCargoMass(Unit) Unit:SetUnitInternalCargo(calculatedMass) - --local report = REPORT:New("Loadmaster report") - --report:Add("Carrying " .. calculatedMass .. "Kg") - --self:_SendMessage(report:Text(),10,false,Unit:GetGroup()) return self end @@ -119453,6 +129406,7 @@ function CTLD:_ListCargo(Group, Unit) local cratelimit = capabilities.cratelimit -- #number local loadedcargo = self.Loaded_Cargo[unitname] or {} -- #CTLD.LoadedCargo local loadedmass = self:_GetUnitCargoMass(Unit) -- #number + local maxloadable = self:_GetMaxLoadableMass(Unit) if self.Loaded_Cargo[unitname] then local no_troops = loadedcargo.Troopsloaded or 0 local no_crates = loadedcargo.Cratesloaded or 0 @@ -119465,7 +129419,7 @@ function CTLD:_ListCargo(Group, Unit) for _,_cargo in pairs(cargotable) do local cargo = _cargo -- #CTLD_CARGO local type = cargo:GetType() -- #CTLD_CARGO.Enum - if type == CTLD_CARGO.Enum.TROOPS and not cargo:WasDropped() then + if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) and (not cargo:WasDropped() or self.allowcratepickupagain) then report:Add(string.format("Troop: %s size %d",cargo:GetName(),cargo:GetCratesNeeded())) end end @@ -119478,7 +129432,7 @@ function CTLD:_ListCargo(Group, Unit) for _,_cargo in pairs(cargotable) do local cargo = _cargo -- #CTLD_CARGO local type = cargo:GetType() -- #CTLD_CARGO.Enum - if type ~= CTLD_CARGO.Enum.TROOPS and not cargo:WasDropped() then + if (type ~= CTLD_CARGO.Enum.TROOPS and type ~= CTLD_CARGO.Enum.ENGINEERS) and (not cargo:WasDropped() or self.allowcratepickupagain) then report:Add(string.format("Crate: %s size 1",cargo:GetName())) cratecount = cratecount + 1 end @@ -119487,11 +129441,105 @@ function CTLD:_ListCargo(Group, Unit) report:Add(" N O N E") end report:Add("------------------------------------------------------------") - report:Add("Total Mass: ".. loadedmass .. " kg") + report:Add("Total Mass: ".. loadedmass .. " kg. Loadable: "..maxloadable.." kg.") local text = report:Text() self:_SendMessage(text, 30, true, Group) else - self:_SendMessage(string.format("Nothing loaded!\nTroop limit: %d | Crate limit %d",trooplimit,cratelimit), 10, false, Group) + self:_SendMessage(string.format("Nothing loaded!\nTroop limit: %d | Crate limit %d | Weight limit %d kgs",trooplimit,cratelimit,maxloadable), 10, false, Group) + end + return self +end + +--- (Internal) Function to list loaded cargo. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Unit#UNIT Unit +-- @return #CTLD self +function CTLD:_ListInventory(Group, Unit) + self:T(self.lid .. " _ListInventory") + local unitname = Unit:GetName() + local unittype = Unit:GetTypeName() + local cgotypes = self.Cargo_Crates + local trptypes = self.Cargo_Troops + local stctypes = self.Cargo_Statics + + local function countcargo(cgotable) + local counter = 0 + for _,_cgo in pairs(cgotable) do + counter = counter + 1 + end + return counter + end + + local crateno = countcargo(cgotypes) + local troopno = countcargo(trptypes) + local staticno = countcargo(stctypes) + + if (crateno > 0 or troopno > 0 or staticno > 0) then + + local report = REPORT:New("Inventory Sheet") + report:Add("------------------------------------------------------------") + report:Add(string.format("Troops: %d, Cratetypes: %d",troopno,crateno+staticno)) + report:Add("------------------------------------------------------------") + report:Add(" -- TROOPS --") + for _,_cargo in pairs(trptypes) do + local cargo = _cargo -- #CTLD_CARGO + local type = cargo:GetType() -- #CTLD_CARGO.Enum + if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) and not cargo:WasDropped() then + local stockn = cargo:GetStock() + local stock = "none" + if stockn == -1 then + stock = "unlimited" + elseif stockn > 0 then + stock = tostring(stockn) + end + report:Add(string.format("Unit: %s | Soldiers: %d | Stock: %s",cargo:GetName(),cargo:GetCratesNeeded(),stock)) + end + end + if report:GetCount() == 4 then + report:Add(" N O N E") + end + report:Add("------------------------------------------------------------") + report:Add(" -- CRATES --") + local cratecount = 0 + for _,_cargo in pairs(cgotypes) do + local cargo = _cargo -- #CTLD_CARGO + local type = cargo:GetType() -- #CTLD_CARGO.Enum + if (type ~= CTLD_CARGO.Enum.TROOPS and type ~= CTLD_CARGO.Enum.ENGINEERS) and not cargo:WasDropped() then + local stockn = cargo:GetStock() + local stock = "none" + if stockn == -1 then + stock = "unlimited" + elseif stockn > 0 then + stock = tostring(stockn) + end + report:Add(string.format("Type: %s | Crates per Set: %d | Stock: %s",cargo:GetName(),cargo:GetCratesNeeded(),stock)) + cratecount = cratecount + 1 + end + end + -- Statics + for _,_cargo in pairs(stctypes) do + local cargo = _cargo -- #CTLD_CARGO + local type = cargo:GetType() -- #CTLD_CARGO.Enum + if (type == CTLD_CARGO.Enum.STATIC) and not cargo:WasDropped() then + local stockn = cargo:GetStock() + local stock = "none" + if stockn == -1 then + stock = "unlimited" + elseif stockn > 0 then + stock = tostring(stockn) + end + report:Add(string.format("Type: %s | Stock: %s",cargo:GetName(),stock)) + cratecount = cratecount + 1 + end + end + if cratecount == 0 then + report:Add(" N O N E") + end + local text = report:Text() + self:_SendMessage(text, 30, true, Group) + else + self:_SendMessage(string.format("Nothing in stock!"), 10, false, Group) end return self end @@ -119501,7 +129549,7 @@ end -- @param Wrapper.Unit#UNIT Unit -- @return #boolean Outcome function CTLD:IsHercules(Unit) - if Unit:GetTypeName() == "Hercules" then + if Unit:GetTypeName() == "Hercules" or string.find(Unit:GetTypeName(),"Bronco") then return true else return false @@ -119516,6 +129564,11 @@ function CTLD:_UnloadTroops(Group, Unit) self:T(self.lid .. " _UnloadTroops") -- check if we are in LOAD zone local droppingatbase = false + local canunload = true + if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then + self:_SendMessage("You need to open the door(s) to unload troops!", 10, false, Group) + if not self.debug then return self end + end local inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) if not inzone then inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP) @@ -119542,12 +129595,12 @@ function CTLD:_UnloadTroops(Group, Unit) for _,_cargo in pairs (cargotable) do local cargo = _cargo -- #CTLD_CARGO local type = cargo:GetType() -- #CTLD_CARGO.Enum - if type == CTLD_CARGO.Enum.TROOPS and not cargo:WasDropped() then + if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) and not cargo:WasDropped() then -- unload troops 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 @@ -119562,13 +129615,18 @@ function CTLD:_UnloadTroops(Group, Unit) :InitRandomizeUnits(true,20,2) :InitDelayOff() :SpawnFromVec2(randomcoord) - if self.movetroopstowpzone then - self:_MoveGroupToZone(self.DroppedTroops[self.TroopCounter]) - end + self:__TroopsDeployed(1, Group, Unit, self.DroppedTroops[self.TroopCounter],type) end -- template loop cargo:SetWasDropped(true) - self:_SendMessage(string.format("Dropped Troops %s into action!",name), 10, false, Group) - self:__TroopsDeployed(1, Group, Unit, self.DroppedTroops[self.TroopCounter]) + -- engineering group? + if type == CTLD_CARGO.Enum.ENGINEERS then + self.Engineers = self.Engineers + 1 + local grpname = self.DroppedTroops[self.TroopCounter]:GetName() + self.EngineersInField[self.Engineers] = CTLD_ENGINEERING:New(name, grpname) + self:_SendMessage(string.format("Dropped Engineers %s into action!",name), 10, false, Group) + else + self:_SendMessage(string.format("Dropped Troops %s into action!",name), 10, false, Group) + end end -- if type end end -- cargotable loop else -- droppingatbase @@ -119586,9 +129644,23 @@ function CTLD:_UnloadTroops(Group, Unit) local cargo = _cargo -- #CTLD_CARGO local type = cargo:GetType() -- #CTLD_CARGO.Enum local dropped = cargo:WasDropped() - if type ~= CTLD_CARGO.Enum.TROOPS and not dropped then + if type ~= CTLD_CARGO.Enum.TROOPS and type ~= CTLD_CARGO.Enum.ENGINEERS and not dropped then table.insert(loaded.Cargo,_cargo) loaded.Cratesloaded = loaded.Cratesloaded + 1 + else + -- add troops back to stock + if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) and droppingatbase then + -- find right generic type + local name = cargo:GetName() + local gentroops = self.Cargo_Troops + for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO + if _troop.Name == name then + local stock = _troop:GetStock() + -- avoid making unlimited stock limited + if stock and tonumber(stock) >= 0 then _troop:AddStock() end + end + end + end end end self.Loaded_Cargo[unitname] = nil @@ -119607,7 +129679,7 @@ end --- (Internal) Function to unload crates from heli. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group --- @param Wrappe.Unit#UNIT Unit +-- @param Wrapper.Unit#UNIT Unit function CTLD:_UnloadCrates(Group, Unit) self:T(self.lid .. " _UnloadCrates") @@ -119639,7 +129711,7 @@ function CTLD:_UnloadCrates(Group, Unit) for _,_cargo in pairs (cargotable) do local cargo = _cargo -- #CTLD_CARGO local type = cargo:GetType() -- #CTLD_CARGO.Enum - if type ~= CTLD_CARGO.Enum.TROOPS and not cargo:WasDropped() then + if type ~= CTLD_CARGO.Enum.TROOPS and type ~= CTLD_CARGO.Enum.ENGINEERS and (not cargo:WasDropped() or self.allowcratepickupagain) then -- unload crates self:_GetCrates(Group, Unit, cargo, 1, true) cargo:SetWasDropped(true) @@ -119656,7 +129728,7 @@ function CTLD:_UnloadCrates(Group, Unit) local cargo = _cargo -- #CTLD_CARGO local type = cargo:GetType() -- #CTLD_CARGO.Enum local size = cargo:GetCratesNeeded() - if type == CTLD_CARGO.Enum.TROOPS then + if type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS then table.insert(loaded.Cargo,_cargo) loaded.Troopsloaded = loaded.Troopsloaded + size end @@ -119678,12 +129750,29 @@ end --- (Internal) Function to build nearby crates. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group --- @param Wrappe.Unit#UNIT Unit -function CTLD:_BuildCrates(Group, Unit) +-- @param Wrapper.Unit#UNIT Unit +-- @param #boolean Engineering If true build is by an engineering team. +function CTLD:_BuildCrates(Group, Unit,Engineering) self:T(self.lid .. " _BuildCrates") + -- avoid users trying to build from flying Hercs + if self:IsHercules(Unit) and self.enableHercules and not Engineering then + local speed = Unit:GetVelocityKMH() + if speed > 1 then + self:_SendMessage("You need to land / stop to build something, Pilot!", 10, false, Group) + return self + end + end + if not Engineering and self.nobuildinloadzones then + -- are we in a load zone? + local inloadzone = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) + if inloadzone then + self:_SendMessage("You cannot build in a loading area, Pilot!", 10, false, Group) + return self + end + end -- get nearby crates - local finddist = self.CrateDistance or 30 - local crates,number = self:_FindCratesNearby(Group,Unit, finddist) -- #table + local finddist = self.CrateDistance or 35 + local crates,number = self:_FindCratesNearby(Group,Unit, finddist,true) -- #table local buildables = {} local foundbuilds = false local canbuild = false @@ -119691,12 +129780,14 @@ function CTLD:_BuildCrates(Group, Unit) -- get dropped crates for _,_crate in pairs(crates) do local Crate = _crate -- #CTLD_CARGO - if Crate:WasDropped() and not Crate:IsRepair() then + if (Crate:WasDropped() or not self.movecratesbeforebuild) and not Crate:IsRepair() and not Crate:IsStatic() then -- we can build these - maybe local name = Crate:GetName() 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 @@ -119705,6 +129796,7 @@ function CTLD:_BuildCrates(Group, Unit) object.Template = template object.CanBuild = false object.Type = ctype -- #CTLD_CARGO.Enum + object.Coord = ccoord:GetVec2() buildables[name] = object foundbuilds = true else @@ -119733,10 +129825,19 @@ function CTLD:_BuildCrates(Group, Unit) local text = string.format("Type: %s | Required %d | Found %d | Can Build %s", name, needed, found, txtok) report:Add(text) end -- end list buildables - if not foundbuilds then report:Add(" --- None Found ---") end + if not foundbuilds then + report:Add(" --- None found! ---") + if self.movecratesbeforebuild then + report:Add("*** Crates need to be moved before building!") + end + end report:Add("------------------------------------------------------------") local text = report:Text() - self:_SendMessage(text, 30, true, Group) + if not Engineering then + self:_SendMessage(text, 30, true, Group) + else + self:T(text) + end -- let\'s get going if canbuild then -- loop again @@ -119744,12 +129845,18 @@ function CTLD:_BuildCrates(Group, Unit) local build = _build -- #CTLD.Buildable if build.CanBuild then self:_CleanUpCrates(crates,build,number) - self:_BuildObjectFromCrates(Group,Unit,build) + if self.buildtime and self.buildtime > 0 then + local buildtimer = TIMER:New(self._BuildObjectFromCrates,self,Group,Unit,build,false,Group:GetCoordinate()) + buildtimer:Start(self.buildtime) + self:_SendMessage(string.format("Build started, ready in %d seconds!",self.buildtime),15,false,Group) + else + self:_BuildObjectFromCrates(Group,Unit,build) + end end end end else - self:_SendMessage(string.format("No crates within %d meters!",finddist), 10, false, Group) + if not Engineering then self:_SendMessage(string.format("No crates within %d meters!",finddist), 10, false, Group) end end -- number > 0 return self end @@ -119757,12 +129864,13 @@ end --- (Internal) Function to repair nearby vehicles / FOBs -- @param #CTLD self -- @param Wrapper.Group#GROUP Group --- @param Wrappe.Unit#UNIT Unit -function CTLD:_RepairCrates(Group, Unit) +-- @param Wrapper.Unit#UNIT Unit +-- @param #boolean Engineering If true, this is an engineering role +function CTLD:_RepairCrates(Group, Unit, Engineering) self:T(self.lid .. " _RepairCrates") -- get nearby crates - local finddist = self.CrateDistance or 30 - local crates,number = self:_FindCratesNearby(Group,Unit, finddist) -- #table + local finddist = self.CrateDistance or 35 + local crates,number = self:_FindCratesNearby(Group,Unit,finddist,true) -- #table local buildables = {} local foundbuilds = false local canbuild = false @@ -119770,7 +129878,7 @@ function CTLD:_RepairCrates(Group, Unit) -- get dropped crates for _,_crate in pairs(crates) do local Crate = _crate -- #CTLD_CARGO - if Crate:WasDropped() and Crate:IsRepair() then + if Crate:WasDropped() and Crate:IsRepair() and not Crate:IsStatic() then -- we can build these - maybe local name = Crate:GetName() local required = Crate:GetCratesNeeded() @@ -119815,19 +129923,23 @@ function CTLD:_RepairCrates(Group, Unit) if not foundbuilds then report:Add(" --- None Found ---") end report:Add("------------------------------------------------------------") local text = report:Text() - self:_SendMessage(text, 30, true, Group) + if not Engineering then + self:_SendMessage(text, 30, true, Group) + else + self:T(text) + end -- let\'s get going if canbuild then -- loop again for _,_build in pairs(buildables) do local build = _build -- #CTLD.Buildable if build.CanBuild then - self:_RepairObjectFromCrates(Group,Unit,crates,build,number) + self:_RepairObjectFromCrates(Group,Unit,crates,build,number,Engineering) end end end else - self:_SendMessage(string.format("No crates within %d meters!",finddist), 10, false, Group) + if not Engineering then self:_SendMessage(string.format("No crates within %d meters!",finddist), 10, false, Group) end end -- number > 0 return self end @@ -119838,44 +129950,58 @@ end -- @param Wrapper.Group#UNIT Unit -- @param #CTLD.Buildable Build -- @param #boolean Repair If true this is a repair and not a new build --- @param Core.Point#COORDINATE Coordinate Location for repair (e.g. where the destroyed unit was) +-- @param Core.Point#COORDINATE RepairLocation Location for repair (e.g. where the destroyed unit was) function CTLD:_BuildObjectFromCrates(Group,Unit,Build,Repair,RepairLocation) self:T(self.lid .. " _BuildObjectFromCrates") -- Spawn-a-crate-content - local position = Unit:GetCoordinate() or Group:GetCoordinate() - local unitname = Unit:GetName() or Group:GetName() - local name = Build.Name - local type = Build.Type -- #CTLD_CARGO.Enum - local canmove = false - if type == CTLD_CARGO.Enum.VEHICLE then canmove = true end - local temptable = Build.Template or {} - local zone = ZONE_GROUP:New(string.format("Unload zone-%s",unitname),Group,100) - local randomcoord = zone:GetRandomCoordinate(35):GetVec2() - if Repair then - randomcoord = RepairLocation:GetVec2() - end - for _,_template in pairs(temptable) do - self.TroopCounter = self.TroopCounter + 1 - 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) - :InitDelayOff() - :SpawnFromVec2(randomcoord) - else -- don't random position of e.g. SAM units build as FOB - self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) - :InitDelayOff() - :SpawnFromVec2(randomcoord) - end - if self.movetroopstowpzone and canmove then - self:_MoveGroupToZone(self.DroppedTroops[self.TroopCounter]) + if Group and Group:IsAlive() or (RepairLocation and not Repair) then + --local position = Unit:GetCoordinate() or Group:GetCoordinate() + --local unitname = Unit:GetName() or Group:GetName() or "Unknown" + local name = Build.Name + local ctype = Build.Type -- #CTLD_CARGO.Enum + local canmove = false + if ctype == CTLD_CARGO.Enum.VEHICLE then canmove = true end + if ctype == CTLD_CARGO.Enum.STATIC then + return self + end + local temptable = Build.Template or {} + if type(temptable) == "string" then + temptable = {temptable} end - if Repair then - self:__CratesRepaired(1,Group,Unit,self.DroppedTroops[self.TroopCounter]) + local zone = nil + if RepairLocation and not Repair then + -- timed build + zone = ZONE_RADIUS:New(string.format("Build zone-%d",math.random(1,10000)),RepairLocation:GetVec2(),100) else - self:__CratesBuild(1,Group,Unit,self.DroppedTroops[self.TroopCounter]) + zone = ZONE_GROUP:New(string.format("Unload zone-%d",math.random(1,10000)),Group,100) end - end -- template loop + --local randomcoord = zone:GetRandomCoordinate(35):GetVec2() + local randomcoord = Build.Coord or zone:GetRandomCoordinate(35):GetVec2() + if Repair then + randomcoord = RepairLocation:GetVec2() + end + for _,_template in pairs(temptable) do + self.TroopCounter = self.TroopCounter + 1 + 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) + :InitDelayOff() + :SpawnFromVec2(randomcoord) + else -- don't random position of e.g. SAM units build as FOB + self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) + :InitDelayOff() + :SpawnFromVec2(randomcoord) + end + if Repair then + self:__CratesRepaired(1,Group,Unit,self.DroppedTroops[self.TroopCounter]) + else + self:__CratesBuild(1,Group,Unit,self.DroppedTroops[self.TroopCounter]) + end + end -- template loop + else + self:T(self.lid.."Group KIA while building!") + end return self end @@ -119888,7 +130014,6 @@ function CTLD:_MoveGroupToZone(Group) local groupcoord = Group:GetCoordinate() -- Get closest zone of type local outcome, name, zone, distance = self:IsUnitInZone(Group,CTLD.CargoZoneType.MOVE) - --self:Tstring.format("Closest WP zone %s is %d meters",name,distance)) if (distance <= self.movetroopsdistance) and zone then -- yes, we can ;) local groupname = Group:GetName() @@ -119931,23 +130056,12 @@ function CTLD:_CleanUpCrates(Crates,Build,Number) found = found + 1 nowcrate:GetPositionable():Destroy(false) nowcrate.Positionable = nil + nowcrate.HasBeenDropped = false end 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 @@ -119964,7 +130078,7 @@ function CTLD:_RefreshF10Menus() local _unit = _group:GetUnit(1) -- Wrapper.Unit#UNIT Asume that there is only one unit in the flight for players if _unit then if _unit:IsAlive() and _unit:IsPlayer() then - if _unit:IsHelicopter() or (_unit:GetTypeName() == "Hercules" and self.enableHercules) then --ensure no stupid unit entries here + if _unit:IsHelicopter() or (self:IsHercules(_unit) and self.enableHercules) then --ensure no stupid unit entries here local unitName = _unit:GetName() _UnitList[unitName] = unitName end @@ -119973,6 +130087,22 @@ 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 + for _id,_cargo in pairs(self.Cargo_Statics) 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 = {} @@ -119989,27 +130119,23 @@ function CTLD:_RefreshF10Menus() local cancrates = capabilities.crates -- top menu local topmenu = MENU_GROUP:New(_group,"CTLD",nil) - local topcrates = MENU_GROUP:New(_group,"Manage Crates",topmenu) local toptroops = MENU_GROUP:New(_group,"Manage Troops",topmenu) + local topcrates = MENU_GROUP:New(_group,"Manage Crates",topmenu) local listmenu = MENU_GROUP_COMMAND:New(_group,"List boarded cargo",topmenu, self._ListCargo, 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 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 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 crates management - 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("Get crate for %s",entry.Name) - menus[menucount] = MENU_GROUP_COMMAND:New(_group,menutext,cratesmenu,self._GetCrates, self, _group, _unit, entry) - end - listmenu = MENU_GROUP_COMMAND:New(_group,"List crates nearby",topcrates, self._ListCratesNearby, self, _group, _unit) - local unloadmenu = MENU_GROUP_COMMAND:New(_group,"Drop crates",topcrates, self._UnloadCrates, self, _group, _unit) - local buildmenu = MENU_GROUP_COMMAND:New(_group,"Build crates",topcrates, self._BuildCrates, self, _group, _unit) - local repairmenu = MENU_GROUP_COMMAND:New(_group,"Repair",topcrates, self._RepairCrates, self, _group, _unit):Refresh() - end -- sub menu troops management if cantroops then local troopsmenu = MENU_GROUP:New(_group,"Load troops",toptroops) @@ -120021,8 +130147,54 @@ function CTLD:_RefreshF10Menus() local unloadmenu1 = MENU_GROUP_COMMAND:New(_group,"Drop troops",toptroops, self._UnloadTroops, self, _group, _unit):Refresh() local extractMenu1 = MENU_GROUP_COMMAND:New(_group, "Extract troops", toptroops, self._ExtractTroops, self, _group, _unit):Refresh() end - local rbcns = MENU_GROUP_COMMAND:New(_group,"List active zone beacons",topmenu, self._ListRadioBeacons, self, _group, _unit) - if unittype == "Hercules" then + -- sub menu crates management + 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) + + 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 + for _,_entry in pairs(self.Cargo_Statics) 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 + for _,_entry in pairs(self.Cargo_Statics) 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 + listmenu = MENU_GROUP_COMMAND:New(_group,"List crates nearby",topcrates, self._ListCratesNearby, self, _group, _unit) + local unloadmenu = MENU_GROUP_COMMAND:New(_group,"Drop crates",topcrates, self._UnloadCrates, self, _group, _unit) + if not self.nobuildmenu then + local buildmenu = MENU_GROUP_COMMAND:New(_group,"Build crates",topcrates, self._BuildCrates, self, _group, _unit) + local repairmenu = MENU_GROUP_COMMAND:New(_group,"Repair",topcrates, self._RepairCrates, self, _group, _unit):Refresh() + else + unloadmenu:Refresh() + end + end + if self:IsHercules(_unit) then local hoverpars = MENU_GROUP_COMMAND:New(_group,"Show flight parameters",topmenu, self._ShowFlightParams, self, _group, _unit):Refresh() else local hoverpars = MENU_GROUP_COMMAND:New(_group,"Show hover parameters",topmenu, self._ShowHoverParams, self, _group, _unit):Refresh() @@ -120037,6 +130209,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 #boolean 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". @@ -120044,11 +130235,17 @@ function CTLD:_RefreshF10Menus() -- @param #CTLD_CARGO.Enum Type Type of cargo, here TROOPS - these will move to a nearby destination zone when dropped/build. -- @param #number NoTroops Size of the group in number of Units across combined templates (for loading). -- @param #number PerTroopMass Mass in kg of each soldier -function CTLD:AddTroopsCargo(Name,Templates,Type,NoTroops,PerTroopMass) +-- @param #number Stock Number of groups in stock. Nil for unlimited. +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) + local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Templates,Type,false,true,NoTroops,nil,nil,PerTroopMass,Stock) table.insert(self.Cargo_Troops,cargo) return self end @@ -120060,27 +130257,72 @@ end -- @param #CTLD_CARGO.Enum Type Type of cargo. I.e. VEHICLE or FOB. VEHICLE will move to destination zones when dropped/build, FOB stays put. -- @param #number NoCrates Number of crates needed to build this cargo. -- @param #number PerCrateMass Mass in kg of each crate -function CTLD:AddCratesCargo(Name,Templates,Type,NoCrates,PerCrateMass) +-- @param #number Stock Number of groups in stock. Nil for unlimited. +-- @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) + 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 +--- User function - Add *generic* static-type loadable as cargo. This type will create cargo that needs to be loaded, moved and dropped. +-- @param #CTLD self +-- @param #string Name Unique name of this type of cargo as set in the mission editor (note: UNIT name!), e.g. "Ammunition-1". +-- @param #number Mass Mass in kg of each static in kg, e.g. 100. +-- @param #number Stock Number of groups in stock. Nil for unlimited. +-- @param #string SubCategory Name of sub-category (optional). +function CTLD:AddStaticsCargo(Name,Mass,Stock,SubCategory) + self:T(self.lid .. " AddStaticsCargo") + self.CargoCounter = self.CargoCounter + 1 + local type = CTLD_CARGO.Enum.STATIC + local template = STATIC:FindByName(Name,true):GetTypeName() + -- Crates are not directly loadable + local cargo = CTLD_CARGO:New(self.CargoCounter,Name,template,type,false,false,1,nil,nil,Mass,Stock,SubCategory) + table.insert(self.Cargo_Statics,cargo) + return self +end + +--- User function - Get a *generic* static-type loadable as #CTLD_CARGO object. +-- @param #CTLD self +-- @param #string Name Unique Unit(!) name of this type of cargo as set in the mission editor (not: GROUP name!), e.g. "Ammunition-1". +-- @param #number Mass Mass in kg of each static in kg, e.g. 100. +-- @return #CTLD_CARGO Cargo object +function CTLD:GetStaticsCargoFromTemplate(Name,Mass) + self:T(self.lid .. " GetStaticsCargoFromTemplate") + self.CargoCounter = self.CargoCounter + 1 + local type = CTLD_CARGO.Enum.STATIC + local template = STATIC:FindByName(Name,true):GetTypeName() + -- Crates are not directly loadable + local cargo = CTLD_CARGO:New(self.CargoCounter,Name,template,type,false,false,1,nil,nil,Mass,1) + --table.insert(self.Cargo_Statics,cargo) + return cargo +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 -function CTLD:AddCratesRepair(Name,Template,Type,NoCrates, PerCrateMass) +-- @param #number Stock Number of groups in stock. Nil for unlimited. +-- @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) + 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 @@ -120096,7 +130338,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 @@ -120106,10 +130350,10 @@ end --- User function - Activate Name #CTLD.CargoZone.Type ZoneType for this CTLD instance. -- @param #CTLD self -- @param #string Name Name of the zone to change in the ME. --- @param #CTLD.CargoZoneTyp ZoneType Type of zone this belongs to. +-- @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 @@ -120142,9 +130386,9 @@ end --- User function - Deactivate Name #CTLD.CargoZoneType ZoneType for this CTLD instance. -- @param #CTLD self -- @param #string Name Name of the zone to change in the ME. --- @param #CTLD.CargoZoneTyp ZoneType Type of zone this belongs to. +-- @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 @@ -120165,7 +130409,7 @@ function CTLD:_GetFMBeacon(Name) table.insert(self.UsedFMFrequencies, FM) beacon.name = Name beacon.frequency = FM / 1000000 - beacon.modulation = radio.modulation.FM + beacon.modulation = CTLD.RadioModulation.FM return beacon end @@ -120185,7 +130429,7 @@ function CTLD:_GetUHFBeacon(Name) table.insert(self.UsedUHFFrequencies, UHF) beacon.name = Name beacon.frequency = UHF / 1000000 - beacon.modulation = radio.modulation.AM + beacon.modulation = CTLD.RadioModulation.AM return beacon end @@ -120206,12 +130450,12 @@ function CTLD:_GetVHFBeacon(Name) table.insert(self.UsedVHFFrequencies, VHF) beacon.name = Name beacon.frequency = VHF / 1000000 - beacon.modulation = radio.modulation.FM + beacon.modulation = CTLD.RadioModulation.FM return beacon 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). @@ -120226,14 +130470,33 @@ end -- @return #CTLD self function CTLD:AddCTLDZone(Name, Type, Color, Active, HasBeacon, Shiplength, Shipwidth) self:T(self.lid .. " AddCTLDZone") - + + local zone = ZONE:FindByName(Name) + if not zone and Type ~= CTLD.CargoZoneType.SHIP then + self:E(self.lid.."**** Zone does not exist: "..Name) + return self + end + + if Type == CTLD.CargoZoneType.SHIP then + local Ship = UNIT:FindByName(Name) + if not Ship then + self:E(self.lid.."**** Ship does not exist: "..Name) + return self + end + end + local ctldzone = {} -- #CTLD.CargoZone ctldzone.active = Active or false ctldzone.color = Color or SMOKECOLOR.Red ctldzone.name = Name or "NONE" ctldzone.type = Type or CTLD.CargoZoneType.MOVE -- #CTLD.CargoZoneType ctldzone.hasbeacon = HasBeacon or false - + + if Type == CTLD.CargoZoneType.BEACON then + self.droppedbeaconref[ctldzone.name] = zone:GetCoordinate() + ctldzone.timestamp = timer.getTime() + end + if HasBeacon then ctldzone.fmbeacon = self:_GetFMBeacon(Name) ctldzone.uhfbeacon = self:_GetUHFBeacon(Name) @@ -120253,6 +130516,92 @@ 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 + if not beacon.timestamp then beacon.timestamp = timer.getTime() + timeout end + 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 @@ -120261,8 +130610,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} - for i=1,3 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 @@ -120292,17 +130641,66 @@ end -- @param #string Sound Name of soundfile. -- @param #number Mhz Frequency in Mhz. -- @param #number Modulation Modulation AM or FM. -function CTLD:_AddRadioBeacon(Name, Sound, Mhz, Modulation) +-- @param #boolean IsShip If true zone is a ship. +-- @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 = ZONE:FindByName(Name) + 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 - local ZoneCoord = Zone:GetCoordinate() - local ZoneVec3 = ZoneCoord:GetVec3() + if IsDropped then + local ZoneCoord = Zone + local ZoneVec3 = ZoneCoord:GetVec3() or {x=0,y=0,z=0} 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 + local Sound = self.RadioPath..Sound + trigger.action.radioTransmission(Sound, ZoneVec3, Modulation, false, Frequency, 1000, Name..math.random(1,10000)) -- Beacon in MP only runs for 30secs straight + self:T2(string.format("Beacon added | Name = %s | Sound = %s | Vec3 = %d %d %d | Freq = %f | Modulation = %d (0=AM/1=FM)",Name,Sound,ZoneVec3.x,ZoneVec3.y,ZoneVec3.z,Mhz,Modulation)) + else + local ZoneCoord = Zone:GetCoordinate() + local ZoneVec3 = ZoneCoord:GetVec3() or {x=0,y=0,z=0} + local Frequency = Mhz * 1000000 -- Freq in Hert + local Sound = self.RadioPath..Sound + trigger.action.radioTransmission(Sound, ZoneVec3, Modulation, false, Frequency, 1000, Name..math.random(1,10000)) -- Beacon in MP only runs for 30secs straightt + self:T2(string.format("Beacon added | Name = %s | Sound = %s | Vec3 = {x=%d, y=%d, z=%d} | Freq = %f | Modulation = %d (0=AM/1=FM)",Name,Sound,ZoneVec3.x,ZoneVec3.y,ZoneVec3.z,Mhz,Modulation)) + end + else + self:E(self.lid.."***** _AddRadioBeacon: Zone does not exist: "..Name) + end + return self +end + +--- Set folder path where the CTLD 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 #CTLD self +-- @param #string FolderPath The path to the sound files, e.g. "CTLD_Soundfiles/". +-- @return #CTLD self +function CTLD:SetSoundfilesFolder( FolderPath ) + self:T(self.lid .. " SetSoundfilesFolder") + -- Check that it ends with / + if FolderPath then + local lastchar = string.sub( FolderPath, -1 ) + if lastchar ~= "/" then + FolderPath = FolderPath .. "/" + end end + + -- Folderpath. + self.RadioPath = FolderPath + + -- Info message. + self:I( self.lid .. string.format( "Setting sound files folder to: %s", self.RadioPath ) ) + return self end @@ -120311,8 +130709,12 @@ end function CTLD:_RefreshRadioBeacons() self:T(self.lid .. " _RefreshRadioBeacons") - local zones = {[1] = self.pickupZones, [2] = self.wpZones, [3] = self.dropOffZones} - for i=1,3 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 @@ -120324,10 +130726,10 @@ function CTLD:_RefreshRadioBeacons() local Name = czone.name local FM = FMbeacon.frequency -- MHz local VHF = VHFbeacon.frequency -- KHz - local UHF = UHFbeacon.frequency -- MHz - self:_AddRadioBeacon(Name,Sound,FM,radio.modulation.FM) - self:_AddRadioBeacon(Name,Sound,VHF,radio.modulation.FM) - self:_AddRadioBeacon(Name,Sound,UHF,radio.modulation.AM) + local UHF = UHFbeacon.frequency -- MHz + self:_AddRadioBeacon(Name,Sound,FM, CTLD.RadioModulation.FM, IsShip, IsDropped) + self:_AddRadioBeacon(Name,Sound,VHF,CTLD.RadioModulation.FM, IsShip, IsDropped) + self:_AddRadioBeacon(Name,Sound,UHF,CTLD.RadioModulation.AM, IsShip, IsDropped) end end end @@ -120365,9 +130767,10 @@ function CTLD:IsUnitInZone(Unit,Zonetype) local zoneret = nil local zonewret = nil local zonenameret = nil + local unitcoord = Unit:GetCoordinate() + local unitVec2 = unitcoord:GetVec2() for _,_cargozone in pairs(zonetable) do local czone = _cargozone -- #CTLD.CargoZone - local unitcoord = Unit:GetCoordinate() local zonename = czone.name local active = czone.active local color = czone.color @@ -120376,18 +130779,26 @@ function CTLD:IsUnitInZone(Unit,Zonetype) local zonewidth = 20 if Zonetype == CTLD.CargoZoneType.SHIP then self:T("Checking Type Ship: "..zonename) - zone = UNIT:FindByName(zonename) - zonecoord = zone:GetCoordinate() + local ZoneUNIT = UNIT:FindByName(zonename) + zonecoord = ZoneUNIT:GetCoordinate() zoneradius = czone.shiplength zonewidth = czone.shipwidth - else + zone = ZONE_UNIT:New( ZoneUNIT:GetName(), ZoneUNIT, zoneradius/2) + elseif ZONE:FindByName(zonename) then zone = ZONE:FindByName(zonename) + self:T("Checking Zone: "..zonename) zonecoord = zone:GetCoordinate() - zoneradius = zone:GetRadius() + --zoneradius = 1500 + zonewidth = zoneradius + elseif AIRBASE:FindByName(zonename) then + zone = AIRBASE:FindByName(zonename):GetZone() + self:T("Checking Zone: "..zonename) + zonecoord = zone:GetCoordinate() + zoneradius = 2000 zonewidth = zoneradius end local distance = self:_GetDistance(zonecoord,unitcoord) - if distance <= zoneradius and active then + if zone:IsVec2InZone(unitVec2) and active then outcome = true end if maxdist > distance then @@ -120405,7 +130816,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. @@ -120416,12 +130852,20 @@ function CTLD:SmokeZoneNearBy(Unit, Flare) local Group = Unit:GetGroup() local smokedistance = self.smokedistance local smoked = false - local zones = {[1] = self.pickupZones, [2] = self.wpZones, [3] = self.dropOffZones} - for i=1,3 do + local zones = {[1] = self.pickupZones, [2] = self.wpZones, [3] = self.dropOffZones, [4] = self.shipZones} + for i=1,4 do for index,cargozone in pairs(zones[i]) do local CZone = cargozone --#CTLD.CargoZone local zonename = CZone.name - local zone = ZONE:FindByName(zonename) + local zone = nil + if i == 4 then + 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 local color = CZone.color @@ -120451,11 +130895,13 @@ end --- User - Function to add/adjust unittype capabilities. -- @param #CTLD self -- @param #string Unittype The unittype to adjust. If passed as Wrapper.Unit#UNIT, it will search for the unit in the mission. - -- @param #boolean Cancrates Unit can load crates. - -- @param #boolean Cantroops Unit can load troops. - -- @param #number Cratelimit Unit can carry number of crates. - -- @param #number Trooplimit Unit can carry number of troops. - function CTLD:UnitCapabilities(Unittype, Cancrates, Cantroops, Cratelimit, Trooplimit) + -- @param #boolean Cancrates Unit can load crates. Default false. + -- @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 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 @@ -120467,6 +130913,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 @@ -120474,6 +130927,8 @@ end capabilities.troops = Cantroops or false capabilities.cratelimit = Cratelimit or 0 capabilities.trooplimit = Trooplimit or 0 + capabilities.length = Length or length + capabilities.cargoweightlimit = Maxcargoweight or maxcargo self.UnitTypes[unittype] = capabilities return self end @@ -120519,8 +130974,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 @@ -120548,7 +131003,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 @@ -120575,8 +131030,7 @@ end self:_SendMessage(text, 10, false, Group) return self end - - + --- (Internal) Check if a unit is in a load zone and is hovering in parameters. -- @param #CTLD self -- @param Wrapper.Unit#UNIT Unit @@ -120598,7 +131052,7 @@ end function CTLD:IsUnitInAir(Unit) -- get speed and height local minheight = self.minimumHoverHeight - if self.enableHercules and Unit:GetTypeName() == "Hercules" then + if self.enableHercules and self:IsHercules(Unit) then minheight = 5.1 -- herc is 5m AGL on the ground end local uheight = Unit:GetHeight() @@ -120659,18 +131113,276 @@ end -- @param #CTLD self -- @return #CTLD self function CTLD:CleanDroppedTroops() + -- Troops local troops = self.DroppedTroops local newtable = {} for _index, _group in pairs (troops) do - if _group and _group:IsAlive() then - newtable[_index] = _group + self:T({_group.ClassName}) + if _group and _group.ClassName == "GROUP" then + if _group:IsAlive() then + newtable[_index] = _group + end end end self.DroppedTroops = newtable + -- Engineers + local engineers = self.EngineersInField + local engtable = {} + for _index, _group in pairs (engineers) do + self:T({_group.ClassName}) + if _group and _group:IsNotStatus("Stopped") then + engtable[_index] = _group + end + end + self.EngineersInField = engtable + return self + end + + --- User - function to add stock of a certain troops type + -- @param #CTLD self + -- @param #string Name Name as defined in the generic cargo. + -- @param #number Number Number of units/groups to add. + -- @return #CTLD self + function CTLD:AddStockTroops(Name, Number) + local name = Name or "none" + local number = Number or 1 + -- find right generic type + local gentroops = self.Cargo_Troops + for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO + if _troop.Name == name then + _troop:AddStock(number) + end + end + end + + --- User - function to add stock of a certain crates type + -- @param #CTLD self + -- @param #string Name Name as defined in the generic cargo. + -- @param #number Number Number of units/groups to add. + -- @return #CTLD self + function CTLD:AddStockCrates(Name, Number) + local name = Name or "none" + local number = Number or 1 + -- find right generic type + local gentroops = self.Cargo_Crates + for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO + if _troop.Name == name then + _troop:AddStock(number) + end + end + end + + --- User - function to remove stock of a certain troops type + -- @param #CTLD self + -- @param #string Name Name as defined in the generic cargo. + -- @param #number Number Number of units/groups to add. + -- @return #CTLD self + function CTLD:RemoveStockTroops(Name, Number) + local name = Name or "none" + local number = Number or 1 + -- find right generic type + local gentroops = self.Cargo_Troops + for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO + if _troop.Name == name then + _troop:RemoveStock(number) + end + end + end + + --- User - function to remove stock of a certain crates type + -- @param #CTLD self + -- @param #string Name Name as defined in the generic cargo. + -- @param #number Number Number of units/groups to add. + -- @return #CTLD self + function CTLD:RemoveStockCrates(Name, Number) + local name = Name or "none" + local number = Number or 1 + -- find right generic type + local gentroops = self.Cargo_Crates + for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO + if _troop.Name == name then + _troop:RemoveStock(number) + end + end + return self + end + + --- (Internal) Check on engineering teams + -- @param #CTLD self + -- @return #CTLD self + function CTLD:_CheckEngineers() + self:T(self.lid.." CheckEngineers") + local engtable = self.EngineersInField + for _ind,_engineers in pairs (engtable) do + local engineers = _engineers -- #CTLD_ENGINEERING + local wrenches = engineers.Group -- Wrapper.Group#GROUP + 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,true) -- #table + engineers:Search(crates,number) + elseif engineers:IsStatus("Moving") then + engineers:Move() + elseif engineers:IsStatus("Arrived") then + engineers:Build() + local unit = wrenches:GetUnit(1) + self:_BuildCrates(wrenches,unit,true) + self:_RepairCrates(wrenches,unit,true) + engineers:Done() + end + else + engineers:Stop() + end + end return self end + + --- (User) Pre-populate troops in the field. + -- @param #CTLD self + -- @param Core.Zone#ZONE Zone The zone where to drop the troops. + -- @param Ops.CTLD#CTLD_CARGO Cargo The #CTLD_CARGO object to spawn. + -- @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! + -- @param #boolean PreciseLocation (Optional) Don't try to get a random position in the zone but use the dead center. Caution not to stack up stuff on another! + -- @return #CTLD self + -- @usage Use this function to pre-populate the field with Troops or Engineers at a random coordinate in a zone: + -- -- create a matching #CTLD_CARGO type + -- local InjectTroopsType = CTLD_CARGO:New(nil,"Infantry",{"Inf12"},CTLD_CARGO.Enum.TROOPS,true,true,12,nil,false,80) + -- -- get a #ZONE object + -- local dropzone = ZONE:New("InjectZone") -- Core.Zone#ZONE + -- -- and go: + -- my_ctld:InjectTroops(dropzone,InjectTroopsType,{land.SurfaceType.LAND}) + function CTLD:InjectTroops(Zone,Cargo,Surfacetypes,PreciseLocation) + self:T(self.lid.." InjectTroops") + local cargo = Cargo -- #CTLD_CARGO + + local function IsTroopsMatch(cargo) + local match = false + local cgotbl = self.Cargo_Troops + local name = cargo:GetName() + for _,_cgo in pairs (cgotbl) do + local cname = _cgo:GetName() + if name == cname then + match = true + break + end + end + return match + end + + if not IsTroopsMatch(cargo) then + self.CargoCounter = self.CargoCounter + 1 + cargo.ID = self.CargoCounter + cargo.Stock = 1 + table.insert(self.Cargo_Troops,cargo) + end + + local type = cargo:GetType() -- #CTLD_CARGO.Enum + if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) then + -- unload + local name = cargo:GetName() or "none" + local temptable = cargo:GetTemplates() or {} + local factor = 1.5 + local zone = Zone + local randomcoord = zone:GetRandomCoordinate(10,30*factor,Surfacetypes):GetVec2() + if PreciseLocation then + randomcoord = zone:GetCoordinate():GetVec2() + end + for _,_template in pairs(temptable) do + self.TroopCounter = self.TroopCounter + 1 + local alias = string.format("%s-%d", _template, math.random(1,100000)) + self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) + :InitRandomizeUnits(true,20,2) + :InitDelayOff() + :SpawnFromVec2(randomcoord) + if self.movetroopstowpzone and type ~= CTLD_CARGO.Enum.ENGINEERS then + self:_MoveGroupToZone(self.DroppedTroops[self.TroopCounter]) + end + end -- template loop + cargo:SetWasDropped(true) + -- engineering group? + if type == CTLD_CARGO.Enum.ENGINEERS then + self.Engineers = self.Engineers + 1 + local grpname = self.DroppedTroops[self.TroopCounter]:GetName() + self.EngineersInField[self.Engineers] = CTLD_ENGINEERING:New(name, grpname) + end + if self.eventoninject then + self:__TroopsDeployed(1,nil,nil,self.DroppedTroops[self.TroopCounter],type) + end + end -- if type end + return self + end + + --- (User) Pre-populate vehicles in the field. + -- @param #CTLD self + -- @param Core.Zone#ZONE Zone The zone where to drop the troops. + -- @param Ops.CTLD#CTLD_CARGO Cargo The #CTLD_CARGO object to spawn. + -- @return #CTLD self + -- @usage Use this function to pre-populate the field with Vehicles or FOB at a random coordinate in a zone: + -- -- create a matching #CTLD_CARGO type + -- local InjectVehicleType = CTLD_CARGO:New(nil,"Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE,true,true,1,nil,false,1000) + -- -- get a #ZONE object + -- local dropzone = ZONE:New("InjectZone") -- Core.Zone#ZONE + -- -- and go: + -- my_ctld:InjectVehicles(dropzone,InjectVehicleType) + function CTLD:InjectVehicles(Zone,Cargo) + self:T(self.lid.." InjectVehicles") + local cargo = Cargo -- #CTLD_CARGO + + local function IsVehicMatch(cargo) + local match = false + local cgotbl = self.Cargo_Crates + local name = cargo:GetName() + for _,_cgo in pairs (cgotbl) do + local cname = _cgo:GetName() + if name == cname then + match = true + break + end + end + return match + end + + if not IsVehicMatch(cargo) then + self.CargoCounter = self.CargoCounter + 1 + cargo.ID = self.CargoCounter + cargo.Stock = 1 + table.insert(self.Cargo_Crates,cargo) + end + + local type = cargo:GetType() -- #CTLD_CARGO.Enum + if (type == CTLD_CARGO.Enum.VEHICLE or type == CTLD_CARGO.Enum.FOB) then + -- unload + local name = cargo:GetName() or "none" + local temptable = cargo:GetTemplates() or {} + local factor = 1.5 + local zone = Zone + local randomcoord = zone:GetRandomCoordinate(10,30*factor):GetVec2() + cargo:SetWasDropped(true) + local canmove = false + if type == CTLD_CARGO.Enum.VEHICLE then canmove = true end + for _,_template in pairs(temptable) do + self.TroopCounter = self.TroopCounter + 1 + 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) + :InitDelayOff() + :SpawnFromVec2(randomcoord) + else -- don't random position of e.g. SAM units build as FOB + self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) + :InitDelayOff() + :SpawnFromVec2(randomcoord) + end + if self.eventoninject then + self:__CratesBuild(1,nil,nil,self.DroppedTroops[self.TroopCounter]) + end + end -- end loop + end -- if type end + return self + end + ------------------------------------------------------------------- --- FSM functions +-- TODO FSM functions ------------------------------------------------------------------- --- (Internal) FSM Function onafterStart. @@ -120697,6 +131409,14 @@ end self:HandleEvent(EVENTS.PlayerEnterUnit, self._EventHandler) self:HandleEvent(EVENTS.PlayerLeaveUnit, self._EventHandler) self:__Status(-5) + + -- AutoSave + if self.enableLoadSave then + local interval = self.saveinterval + local filename = self.filename + local filepath = self.filepath + self:__Save(interval,filepath,filename) + end return self end @@ -120710,8 +131430,10 @@ end self:T({From, Event, To}) self:CleanDroppedTroops() self:_RefreshF10Menus() + self:CheckDroppedBeacons() self:_RefreshRadioBeacons() self:CheckAutoHoverload() + self:_CheckEngineers() return self end @@ -120741,7 +131463,25 @@ end if self.debug or self.verbose > 0 then local text = string.format("%s Pilots %d | Live Crates %d |\nCargo Counter %d | Troop Counter %d", self.lid, pilots, boxes, cc, tc) - local m = MESSAGE:New(text,10,"CTLD"):ToAll() + local m = MESSAGE:New(text,10,"CTLD"):ToAll() + if self.verbose > 0 then + self:I(self.lid.."Cargo and Troops in Stock:") + for _,_troop in pairs (self.Cargo_Crates) do + local name = _troop:GetName() + local stock = _troop:GetStock() + self:I(string.format("-- %s \t\t\t %d", name, stock)) + end + for _,_troop in pairs (self.Cargo_Statics) do + local name = _troop:GetName() + local stock = _troop:GetStock() + self:I(string.format("-- %s \t\t\t %d", name, stock)) + end + for _,_troop in pairs (self.Cargo_Troops) do + local name = _troop:GetName() + local stock = _troop:GetStock() + self:I(string.format("-- %s \t\t %d", name, stock)) + end + end end self:__Status(-30) return self @@ -120815,6 +131555,45 @@ end -- @return #CTLD self function CTLD:onbeforeTroopsDeployed(From, Event, To, Group, Unit, Troops) self:T({From, Event, To}) + if Unit and Unit:IsPlayer() and self.PlayerTaskQueue then + local playername = Unit:GetPlayerName() + local dropcoord = Troops:GetCoordinate() or COORDINATE:New(0,0,0) + local dropvec2 = dropcoord:GetVec2() + self.PlayerTaskQueue:ForEach( + function (Task) + local task = Task -- Ops.PlayerTask#PLAYERTASK + local subtype = task:GetSubType() + -- right subtype? + if Event == subtype and not task:IsDone() then + local targetzone = task.Target:GetObject() -- Core.Zone#ZONE should be a zone in this case .... + if targetzone and targetzone.ClassName and string.match(targetzone.ClassName,"ZONE") and targetzone:IsVec2InZone(dropvec2) then + if task.Clients:HasUniqueID(playername) then + -- success + task:__Success(-1) + end + end + end + end + ) + end + return self + end + + --- (Internal) FSM Function onafterTroopsDeployed. + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param Wrapper.Group#GROUP Troops Troops #GROUP Object. + -- @param #CTLD.CargoZoneType Type Type of Cargo deployed + -- @return #CTLD self + function CTLD:onafterTroopsDeployed(From, Event, To, Group, Unit, Troops, Type) + self:T({From, Event, To}) + if self.movetroopstowpzone and Type ~= CTLD_CARGO.Enum.ENGINEERS then + self:_MoveGroupToZone(Troops) + end return self end @@ -120843,6 +131622,44 @@ end -- @return #CTLD self function CTLD:onbeforeCratesBuild(From, Event, To, Group, Unit, Vehicle) self:T({From, Event, To}) + if Unit and Unit:IsPlayer() and self.PlayerTaskQueue then + local playername = Unit:GetPlayerName() + local dropcoord = Vehicle:GetCoordinate() or COORDINATE:New(0,0,0) + local dropvec2 = dropcoord:GetVec2() + self.PlayerTaskQueue:ForEach( + function (Task) + local task = Task -- Ops.PlayerTask#PLAYERTASK + local subtype = task:GetSubType() + -- right subtype? + if Event == subtype and not task:IsDone() then + local targetzone = task.Target:GetObject() -- Core.Zone#ZONE should be a zone in this case .... + if targetzone and targetzone.ClassName and string.match(targetzone.ClassName,"ZONE") and targetzone:IsVec2InZone(dropvec2) then + if task.Clients:HasUniqueID(playername) then + -- success + task:__Success(-1) + end + end + end + end + ) + end + return self + end + + --- (Internal) FSM Function onafterCratesBuild. + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param Wrapper.Group#GROUP Vehicle The #GROUP object of the vehicle or FOB build. + -- @return #CTLD self + function CTLD:onafterCratesBuild(From, Event, To, Group, Unit, Vehicle) + self:T({From, Event, To}) + if self.movetroopstowpzone then + self:_MoveGroupToZone(Vehicle) + end return self end @@ -120859,11 +131676,986 @@ end return self end + --- On before "Save" event. Checks if io and lfs are available. + -- @param #CTLD self + -- @param #string From From state. + -- @param #string Event Event. + -- @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. Default is "CTLD__Persist.csv". + function CTLD:onbeforeSave(From, Event, To, path, filename) + self:T({From, Event, To, path, filename}) + if not self.enableLoadSave then + return self + end + -- Thanks to @FunkyFranky + -- Check io module is available. + if not io then + self:E(self.lid.."ERROR: io not desanitized. Can't save current state.") + return false + end + + -- Check default path. + if path==nil and not lfs then + self:E(self.lid.."WARNING: lfs not desanitized. State will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.") + end + + return true + end + + --- On after "Save" event. Player data is saved to file. + -- @param #CTLD self + -- @param #string From From state. + -- @param #string Event Event. + -- @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. Default is Default is "CTLD__Persist.csv". + function CTLD:onafterSave(From, Event, To, path, filename) + self:T({From, Event, To, path, filename}) + -- Thanks to @FunkyFranky + if not self.enableLoadSave then + return self + end + --- Function that saves data to file + 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=self.filepath or lfs.writedir() + end + + -- Set file name. + filename=filename or self.filename + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + local grouptable = self.DroppedTroops -- #table + local cgovehic = self.Cargo_Crates + local cgotable = self.Cargo_Troops + local stcstable = self.Spawned_Cargo + + local statics = nil + local statics = {} + 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 + if object and object:IsAlive() and (cargo:WasDropped() or not cargo:HasMoved()) then + statics[#statics+1] = cargo + end + end + + -- find matching cargo + local function FindCargoType(name,table) + -- name matching a template in the table + local match = false + local cargo = nil + for _ind,_cargo in pairs (table) do + local thiscargo = _cargo -- #CTLD_CARGO + local template = thiscargo:GetTemplates() + if type(template) == "string" then + template = { template } + end + for _,_name in pairs (template) do + if string.find(name,_name) and _cargo:GetType() ~= CTLD_CARGO.Enum.REPAIR then + match = true + cargo = thiscargo + end + end + if match then break end + end + return match, cargo + end + + + --local data = "LoadedData = {\n" + local data = "Group,x,y,z,CargoName,CargoTemplates,CargoType,CratesNeeded,CrateMass\n" + local n = 0 + for _,_grp in pairs(grouptable) do + local group = _grp -- Wrapper.Group#GROUP + if group and group:IsAlive() then + -- get template name + local name = group:GetName() + local template = string.gsub(name,"-(.+)$","") + if string.find(template,"#") then + template = string.gsub(name,"#(%d+)$","") + end + + local match, cargo = FindCargoType(template,cgotable) + if not match then + match, cargo = FindCargoType(template,cgovehic) + end + if match then + n = n + 1 + local cargo = cargo -- #CTLD_CARGO + local cgoname = cargo.Name + local cgotemp = cargo.Templates + local cgotype = cargo.CargoType + local cgoneed = cargo.CratesNeeded + local cgomass = cargo.PerCrateMass + + if type(cgotemp) == "table" then + local templates = "{" + for _,_tmpl in pairs(cgotemp) do + templates = templates .. _tmpl .. ";" + end + templates = templates .. "}" + cgotemp = templates + end + + local location = group:GetVec3() + local txt = string.format("%s,%d,%d,%d,%s,%s,%s,%d,%d\n" + ,template,location.x,location.y,location.z,cgoname,cgotemp,cgotype,cgoneed,cgomass) + data = data .. txt + end + end + end + + for _,_cgo in pairs(statics) do + local object = _cgo -- #CTLD_CARGO + local cgoname = object.Name + local cgotemp = object.Templates + + if type(cgotemp) == "table" then + local templates = "{" + for _,_tmpl in pairs(cgotemp) do + templates = templates .. _tmpl .. ";" + end + templates = templates .. "}" + cgotemp = templates + end + + local cgotype = object.CargoType + local cgoneed = object.CratesNeeded + local cgomass = object.PerCrateMass + local crateobj = object.Positionable + local location = crateobj:GetVec3() + local txt = string.format("%s,%d,%d,%d,%s,%s,%s,%d,%d\n" + ,"STATIC",location.x,location.y,location.z,cgoname,cgotemp,cgotype,cgoneed,cgomass) + data = data .. txt + end + + _savefile(filename, data) + + -- AutoSave + if self.enableLoadSave then + local interval = self.saveinterval + local filename = self.filename + local filepath = self.filepath + self:__Save(interval,filepath,filename) + end + return self + end + + --- On before "Load" event. Checks if io and lfs and the file are available. + -- @param #CTLD self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string path (Optional) Path where the file is located. 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 loading. Default is "CTLD__Persist.csv". + function CTLD:onbeforeLoad(From, Event, To, path, filename) + self:T({From, Event, To, path, filename}) + if not self.enableLoadSave then + return self + end + --- 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 + + -- Set file name and path + filename=filename or self.filename + path = path or self.filepath + + -- Check io module is available. + if not io then + self:E(self.lid.."WARNING: io not desanitized. Cannot load file.") + return false + end + + -- Check default path. + if path==nil and not lfs then + self:E(self.lid.."WARNING: lfs not desanitized. State 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() + end + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + -- Check if file exists. + local exists=_fileexists(filename) + + if exists then + return true + else + self:E(self.lid..string.format("WARNING: State file %s might not exist.", filename)) + return false + --return self + end + + end + + --- On after "Load" event. Loads dropped units from file. + -- @param #CTLD self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string path (Optional) Path where the file is located. 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 loading. Default is "CTLD__Persist.csv". + function CTLD:onafterLoad(From, Event, To, path, filename) + self:T({From, Event, To, path, filename}) + if not self.enableLoadSave then + return self + end + --- Function that loads data from a file. + local function _loadfile(filename) + local f=assert(io.open(filename, "rb")) + local data=f:read("*all") + f:close() + return data + end + + -- Set file name and path + filename=filename or self.filename + path = path or self.filepath + + -- Set path or default. + if lfs then + path=path or lfs.writedir() + end + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + -- Info message. + local text=string.format("Loading CTLD state from file %s", filename) + MESSAGE:New(text,10):ToAllIf(self.Debug) + self:I(self.lid..text) + + local file=assert(io.open(filename, "rb")) + + local loadeddata = {} + for line in file:lines() do + loadeddata[#loadeddata+1] = line + end + file:close() + + -- remove header + table.remove(loadeddata, 1) + + 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,10=SubCategory + local groupname = dataset[1] + local vec2 = {} + vec2.x = tonumber(dataset[2]) + vec2.y = tonumber(dataset[4]) + local cargoname = dataset[5] + local cargotype = dataset[7] + if type(groupname) == "string" and groupname ~= "STATIC" then + local cargotemplates = dataset[6] + cargotemplates = string.gsub(cargotemplates,"{","") + cargotemplates = string.gsub(cargotemplates,"}","") + cargotemplates = UTILS.Split(cargotemplates,";") + local size = tonumber(dataset[8]) + local mass = tonumber(dataset[9]) + -- inject at Vec2 + local dropzone = ZONE_RADIUS:New("DropZone",vec2,20) + if cargotype == CTLD_CARGO.Enum.VEHICLE or cargotype == CTLD_CARGO.Enum.FOB then + local injectvehicle = CTLD_CARGO:New(nil,cargoname,cargotemplates,cargotype,true,true,size,nil,true,mass) + self:InjectVehicles(dropzone,injectvehicle) + elseif cargotype == CTLD_CARGO.Enum.TROOPS or cargotype == CTLD_CARGO.Enum.ENGINEERS then + local injecttroops = CTLD_CARGO:New(nil,cargoname,cargotemplates,cargotype,true,true,size,nil,true,mass) + self:InjectTroops(dropzone,injecttroops,self.surfacetypes) + end + elseif (type(groupname) == "string" and groupname == "STATIC") or cargotype == CTLD_CARGO.Enum.REPAIR then + local cargotemplates = dataset[6] + local size = tonumber(dataset[8]) + local mass = tonumber(dataset[9]) + local dropzone = ZONE_RADIUS:New("DropZone",vec2,20) + local injectstatic = nil + if cargotype == CTLD_CARGO.Enum.VEHICLE or cargotype == CTLD_CARGO.Enum.FOB then + cargotemplates = string.gsub(cargotemplates,"{","") + cargotemplates = string.gsub(cargotemplates,"}","") + cargotemplates = UTILS.Split(cargotemplates,";") + injectstatic = CTLD_CARGO:New(nil,cargoname,cargotemplates,cargotype,true,true,size,nil,true,mass) + elseif cargotype == CTLD_CARGO.Enum.STATIC or cargotype == CTLD_CARGO.Enum.REPAIR then + injectstatic = CTLD_CARGO:New(nil,cargoname,cargotemplates,cargotype,true,true,size,nil,true,mass) + end + if injectstatic then + self:InjectStatics(dropzone,injectstatic) + end + end + end + + return self + end end -- end do + +do +--- **Hercules Cargo AIR Drop Events** by Anubis Yinepu +-- Moose CTLD OO refactoring by Applevangelist +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +-- TODO CTLD_HERCULES +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +-- 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.2", +} + +--- 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}, +} + +--- 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. +-- **Do not use** the **splash damage** script together with this, your cargo will just explode when reaching the ground! +-- +-- ### Airdrops +-- +-- 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) +-- +-- ### General +-- +-- Use either this method to integrate the Hercules **or** the one from the "normal" CTLD. Never both! +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.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 + 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) + 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 + + 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 + + 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 + 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) + 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_] or initiator:GetVec3() -- 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(1,1,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(1,1,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) + return self +end + +end + ------------------------------------------------------------------- -- End Ops.CTLD.lua ------------------------------------------------------------------- ---- **Ops** -- Combat Search and Rescue. +--- **Ops** - Combat Search and Rescue. +-- +-- === +-- +-- **CSAR** - MOOSE based Helicopter CSAR Operations. +-- +-- === +-- +-- ## Missions:--- **Ops** -- Combat Search and Rescue. -- -- === -- @@ -120883,11 +132675,11 @@ end -- end do -- -- === -- --- ### Author: **Applevangelist** (Moose Version), ***Ciribob*** (original), Thanks to: Shadowze, Cammel (testing) +-- ### Author: **Applevangelist** (Moose Version), ***Ciribob*** (original), Thanks to: Shadowze, Cammel (testing), The Chosen One (Persistence) -- @module Ops.CSAR -- @image OPS_CSAR.jpg --- Date: Aug 2021 +-- Date: January 2023 ------------------------------------------------------------------------- --- **CSAR** class, extends Core.Base#BASE, Core.Fsm#FSM @@ -120896,6 +132688,7 @@ end -- end do -- @field #number verbose Verbosity level. -- @field #string lid Class id string for output to DCS log file. -- @field #number coalition Coalition side number, e.g. `coalition.side.RED`. +-- @field Core.Set#SET_GROUP allheligroupset Set of CSAR heli groups. -- @extends Core.Fsm#FSM --- *Combat search and rescue (CSAR) are search and rescue operations that are carried out during war that are within or near combat zones.* (Wikipedia) @@ -120910,6 +132703,7 @@ end -- end do -- * 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 -- @@ -120932,53 +132726,71 @@ end -- end do -- -- 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. --- -- (added 0.1.4) limit amount of downed pilots spawned by **ejection** events --- self.limitmaxdownedpilots = true --- self.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 --- -- (added 0.1.9) --- self.suppressmessages = false -- switch off all messaging if you want to do your own --- +-- 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. +-- -- limit amount of downed pilots spawned by **ejection** events +-- mycsar.limitmaxdownedpilots = true +-- mycsar.maxdownedpilots = 10 +-- -- allow to set far/near distance for approach and optionally pilot must 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 +-- mycsar.suppressmessages = false -- switch off all messaging if you want to do your own +-- mycsar.rescuehoverheight = 20 -- max height for a hovering rescue in meters +-- mycsar.rescuehoverdistance = 10 -- max distance for a hovering rescue in meters +-- -- Country codes for spawned pilots +-- mycsar.countryblue= country.id.USA +-- mycsar.countryred = country.id.RUSSIA +-- mycsar.countryneutral = country.id.UN_PEACEKEEPERS +-- mycsar.topmenuname = "CSAR" -- set the menu entry name +-- mycsar.ADFRadioPwr = 1000 -- ADF Beacons sending with 1KW as default +-- mycsar.PilotWeight = 80 -- Loaded pilots weigh 80kgs each +-- -- ## 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.SRSGender = "male" -- male or female voice +-- -- +-- 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. +-- mycsar.allowbronco = false -- set to true to use the Bronco mod as a CSAR plane +-- -- ## 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 -- @@ -121005,7 +132817,7 @@ end -- end do -- -- 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 -- @@ -121032,6 +132844,28 @@ end -- end do -- -- 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) +-- +-- ## 6. Save and load downed pilots - Persistance +-- +-- You can save and later load back downed pilots to make your mission persistent. +-- For this to work, you need to de-sanitize **io** and **lfs** in your MissionScripting.lua, which is located in your DCS installtion folder under Scripts. +-- There is a risk involved in doing that; if you do not know what that means, this is possibly not for you. +-- +-- Use the following options to manage your saves: +-- +-- mycsar.enableLoadSave = true -- allow auto-saving and loading of files +-- mycsar.saveinterval = 600 -- save every 10 minutes +-- mycsar.filename = "missionsave.csv" -- example filename +-- mycsar.filepath = "C:\\Users\\myname\\Saved Games\\DCS\Missions\\MyMission" -- example path +-- +-- Then use an initial load at the beginning of your mission: +-- +-- mycsar:__Load(10) +-- +-- **Caveat:** +-- Dropped troop noMessage and forcedesc parameters aren't saved. -- -- @field #CSAR CSAR = { @@ -121053,7 +132887,7 @@ CSAR = { smokeMarkers = {}, -- tracks smoke markers for groups heliVisibleMessage = {}, -- tracks if the first message has been sent of the heli being visible heliCloseMessage = {}, -- tracks heli close message ie heli < 500m distance - max_units = 6, --number of pilots that can be carried + max_units = 6, -- number of pilots that can be carried hoverStatus = {}, -- tracks status of a helis hover above a downed pilot pilotDisabled = {}, -- tracks what aircraft a pilot is disabled for pilotLives = {}, -- tracks how many lives a pilot has @@ -121066,6 +132900,10 @@ CSAR = { rescuedpilots = 0, limitmaxdownedpilots = true, maxdownedpilots = 10, + allheligroupset = nil, + topmenuname = "CSAR", + ADFRadioPwr = 1000, + PilotWeight = 80, } --- Downed pilots info. @@ -121079,8 +132917,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 @@ -121095,10 +132934,14 @@ 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["UH-60L"] = 10 +CSAR.AircraftType["AH-64D_BLK_II"] = 2 +CSAR.AircraftType["Bronco-OV-10A"] = 2 --- CSAR class version. -- @field #string version -CSAR.version="0.1.10r3" +CSAR.version="1.0.17" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list @@ -121106,7 +132949,7 @@ CSAR.version="0.1.10r3" -- DONE: SRS Integration (to be tested) -- TODO: Maybe - add option to smoke/flare closest MASH - +-- DONE: shagrat Add cargoWeight to helicopter when pilot boarded ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -121122,6 +132965,8 @@ function CSAR:New(Coalition, Template, Alias) -- Inherit everything from FSM class. local self=BASE:Inherit(self, FSM:New()) -- #CSAR + BASE:T({Coalition, Prefixes, Alias}) + --set Coalition if Coalition and type(Coalition)=="string" then if Coalition=="blue" then @@ -121167,10 +133012,13 @@ 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. self:AddTransition("*", "KIA", "*") -- Pilot killed in action. + self:AddTransition("*", "Load", "*") -- CSAR load event. + self:AddTransition("*", "Save", "*") -- CSAR save event. self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. -- tables, mainly for tracking actions @@ -121207,6 +133055,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 @@ -121214,7 +133063,7 @@ function CSAR:New(Coalition, Template, Alias) self.csarPrefix = { "helicargo", "MEDEVAC"} -- prefixes used for useprefix=true - DON\'T use # in names! self.template = Template or "generic" -- template for downed pilot self.mashprefix = {"MASH"} -- prefixes used to find MASHes - self.mash = SET_GROUP:New():FilterCoalitions(self.coalition):FilterPrefixes(self.mashprefix):FilterOnce() -- currently only GROUP objects, maybe support STATICs also? + self.autosmoke = false -- automatically smoke location when heli is near self.autosmokedistance = 2000 -- distance for autosmoke -- added 0.1.4 @@ -121227,14 +133076,52 @@ function CSAR:New(Coalition, Template, Alias) self.approachdist_near = 3000 -- switch to 5 sec interval approach mode, meters self.pilotmustopendoors = false -- switch to true to enable check on open doors self.suppressmessages = false - + + -- added 0.1.11r1 + self.rescuehoverheight = 20 + self.rescuehoverdistance = 10 + + -- added 0.1.12 + 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 + + -- added 1.0.15 + self.allowbronco = false -- set to true to use the Bronco mod as a CSAR plane + + self.ADFRadioPwr = 1000 + + -- added 1.0.16 + self.PilotWeight = 80 + -- 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) self.useSRS = false -- Use FF\'s SRS integration - self.SRSPath = "E:\\Progra~1\\DCS-SimpleRadio-Standalone\\" -- adjust your own path in your server(!) + self.SRSPath = "E:\\Program Files\\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.0 -- volume 0.0 to 1.0 + self.SRSGender = "male" -- male or female + + local AliaS = string.gsub(self.alias," ","_") + self.filename = string.format("CSAR_%s_Persist.csv",AliaS) + + -- load and save downed pilots + self.enableLoadSave = false + self.filepath = nil + self.saveinterval = 600 ------------------------ --- Pseudo Functions --- @@ -121265,6 +133152,24 @@ function CSAR:New(Coalition, Template, Alias) -- @function [parent=#CSAR] __Status -- @param #CSAR self -- @param #number delay Delay in seconds. + -- + -- --- Triggers the FSM event "Load". + -- @function [parent=#CSAR] Load + -- @param #CSAR self + + --- Triggers the FSM event "Load" after a delay. + -- @function [parent=#CSAR] __Load + -- @param #CSAR self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Save". + -- @function [parent=#CSAR] Load + -- @param #CSAR self + + --- Triggers the FSM event "Save" after a delay. + -- @function [parent=#CSAR] __Save + -- @param #CSAR self + -- @param #number delay Delay in seconds. --- On After "PilotDown" event. Downed Pilot detected. -- @function [parent=#CSAR] OnAfterPilotDown @@ -121286,7 +133191,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. @@ -121294,6 +133208,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 @@ -121322,6 +133237,24 @@ function CSAR:New(Coalition, Template, Alias) -- @param #string To To state. -- @param #string Pilotname Name of the pilot KIA. + --- FSM Function OnAfterLoad. + -- @function [parent=#CSAR] OnAfterLoad + -- @param #CSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string path (Optional) Path where the file is located. 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 loading. Default is "CSAR__Persist.csv". + + --- FSM Function OnAfterSave. + -- @function [parent=#CSAR] OnAfterSave + -- @param #CSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @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. Default is "CSAR__Persist.csv". + return self end @@ -121339,8 +133272,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 @@ -121356,6 +133290,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 @@ -121400,21 +133335,41 @@ function CSAR:_DoubleEjection(_unitname) return false end +--- (User) Add a PLAYERTASK - FSM events will check success +-- @param #CSAR self +-- @param Ops.PlayerTask#PLAYERTASK PlayerTask +-- @return #CSAR self +function CSAR:AddPlayerTask(PlayerTask) + self:T(self.lid .. " AddPlayerTask") + if not self.PlayerTaskQueue then + self.PlayerTaskQueue = FIFO:New() + end + self.PlayerTaskQueue:Push(PlayerTask,PlayerTask.PlayerTaskNr) + return self +end + --- (Internal) Spawn a downed pilot -- @param #CSAR self -- @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() 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? @@ -121480,21 +133435,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 @@ -121503,25 +133468,33 @@ function CSAR:_AddCsar(_coalition , _country, _point, _typeName, _unitName, _pla local _text = _description if not forcedesc then if _playerName ~= nil then - _text = "Pilot " .. _playerName + if _freq ~= 0 then --shagrat + _text = "Pilot " .. _playerName + else + _text = "TIC - " .. _playerName + end elseif _unitName ~= nil then - _text = "AI Pilot of " .. _unitName + 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. @@ -121532,7 +133505,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 @@ -121552,11 +133534,11 @@ function CSAR:_SpawnCsarAtZone( _zone, _coalition, _description, _randomPoint, _ local _country = 0 if _coalition == coalition.side.BLUE then - _country = country.id.USA + _country = self.countryblue elseif _coalition == coalition.side.RED then - _country = country.id.RUSSIA + _country = self.countryred else - _country = country.id.UN_PEACEKEEPERS + _country = self.countryneutral end self:_AddCsar(_coalition, _country, pos, typename, unitname, _description, freq, _nomessage, _description, forcedesc) @@ -121566,7 +133548,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. @@ -121583,7 +133565,58 @@ function CSAR:SpawnCSARAtZone(Zone, Coalition, Description, RandomPoint, Nomessa return self end --- TODO: Split in functions per Event type +--- (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 Core.Point#COORDINATE _Point +-- @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 Core.Point#COORDINATE Point +-- @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 coordinate Core.Point#COORDINATE for the blue coalition. +-- my_csar:SpawnCASEVAC( coordinate, 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) @@ -121592,9 +133625,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 @@ -121602,35 +133640,52 @@ 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 + + local function IsBronco(Group) + local grp = Group -- Wrapper.Group#GROUP + local typename = grp:GetTypeName() + self:T(typename) + if typename == "Bronco-OV-10A" then return true end + return false + end + + if _unit:IsHelicopter() or _group:IsHelicopter() or IsBronco(_group) then self:_AddMedevacMenuItem() end - return true + return self elseif (_event.id == EVENTS.PilotDead and self.csarOncrash == false) then -- Pilot dead @@ -121642,29 +133697,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 - self:_DisplayToAllSAR("MAYDAY MAYDAY! " .. _unit:GetTypeName() .. " shot down. No Chute!", self.coalition, self.messageTime) + 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") @@ -121672,38 +133727,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! - end - - if self.enableForAI == false and _event.IniPlayerName == nil then - return + self:T("Wrong coalition! Coalition = "..UTILS.GetCoalitionName(_coalition)) + return self --ignore! 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") @@ -121718,45 +133806,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) - --[[ - if self.pilotmustopendoors and not self:_IsLoadingDoorOpen(_event.IniUnitName) then - self:_DisplayMessageToSAR(_unit, "Open the door to let me out!", self.messageTime, true) - else - self:_RescuePilots(_unit) - end - --]] 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 @@ -121769,15 +133872,19 @@ end function CSAR:_InitSARForPilot(_downedGroup, _GroupName, _freq, _nomessage) self:T(self.lid .. " _InitSARForPilot") local _leader = _downedGroup:GetUnit(1) - --local _groupName = _downedGroup:GetName() local _groupName = _GroupName local _freqk = _freq / 1000 local _coordinatesText = self:_GetPositionOfWounded(_downedGroup) 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 @@ -121785,7 +133892,7 @@ function CSAR:_InitSARForPilot(_downedGroup, _GroupName, _freq, _nomessage) end -- trigger FSM event - self:__PilotDown(2,_downedGroup, _freqk, _leadername, _coordinatesText) + self:__PilotDown(2,_downedGroup, _freqk, _groupName, _coordinatesText) return self end @@ -121825,6 +133932,38 @@ function CSAR:_RemoveNameFromDownedPilots(name,force) return found end +--- [User] Set callsign options for TTS output. See @{Wrapper.Group#GROUP.GetCustomCallSign}() on how to set customized callsigns. +-- @param #CSAR self +-- @param #boolean ShortCallsign If true, only call out the major flight number +-- @param #boolean Keepnumber If true, keep the **customized callsign** in the #GROUP name for players as-is, no amendments or numbers. +-- @param #table CallsignTranslations (optional) Table to translate between DCS standard callsigns and bespoke ones. Does not apply if using customized +-- callsigns from playername or group name. +-- @return #CSAR self +function CSAR:SetCallSignOptions(ShortCallsign,Keepnumber,CallsignTranslations) + if not ShortCallsign or ShortCallsign == false then + self.ShortCallsign = false + else + self.ShortCallsign = true + end + self.Keepnumber = Keepnumber or false + self.CallsignTranslations = CallsignTranslations + return self +end + +--- (Internal) Check if a name is in downed pilot table and remove it. +-- @param #CSAR self +-- @param #string UnitName +-- @return #string CallSign +function CSAR:_GetCustomCallSign(UnitName) + local callsign = Unitname + local unit = UNIT:FindByName(UnitName) + if unit and unit:IsAlive() then + local group = unit:GetGroup() + callsign = group:GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations) + end + return callsign +end + --- (Internal) Check state of wounded group. -- @param #CSAR self -- @param #string heliname heliname @@ -121852,7 +133991,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 @@ -121881,9 +134020,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.", self:_GetCustomCallSign(_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.", self:_GetCustomCallSign(_heliName), _pilotName), self.messageTime,false,true) end --mark as shown for THIS heli and THIS group self.heliVisibleMessage[_lookupKeyHeli] = true @@ -121915,7 +134054,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 @@ -121947,15 +134086,14 @@ 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, self:_GetCustomCallSign(_heliName), _unitsInHelicopter, _unitsInHelicopter), self.messageTime,false,false,true) + return self end local found,downedgrouptable = self:_CheckNameInDownedPilots(_woundedGroupName) local grouptable = downedgrouptable --#CSAR.DownedPilot self.inTransitGroups[_heliName][_woundedGroupName] = { - -- DONE: Fix with #CSAR.DownedPilot originalUnit = grouptable.originalUnit, woundedGroup = _woundedGroupName, side = self.coalition, @@ -121966,11 +134104,27 @@ function CSAR:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupNam _woundedGroup:Destroy(false) self:_RemoveNameFromDownedPilots(_woundedGroupName,true) - self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s I\'m in! Get to the MASH ASAP! ", _heliName, _pilotName), self.messageTime,true,true) + self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s I\'m in! Get to the MASH ASAP! ", self:_GetCustomCallSign(_heliName), _pilotName), self.messageTime,true,true) - self:__Boarded(5,_heliName,_woundedGroupName) + self:_UpdateUnitCargoMass(_heliName) - return true + self:__Boarded(5,_heliName,_woundedGroupName,grouptable.desc) + + return self +end + +--- (Internal) Function to calculate and set Unit internal cargo mass +-- @param #CSAR self +-- @param #string _heliName Unit name +-- @return #CSAR self +function CSAR:_UpdateUnitCargoMass(_heliName) + self:T(self.lid .. " _UpdateUnitCargoMass") + local calculatedMass = self:_PilotsOnboard(_heliName)*(self.PilotWeight or 80) + local Unit = UNIT:FindByName(_heliName) + if Unit then + Unit:SetUnitInternalCargo(calculatedMass) + end + return self end --- (Internal) Move group to destination. @@ -121987,49 +134141,13 @@ function CSAR:_OrderGroupToMoveToPoint(_leader, _destination) return self end - --- (internal) Function to check if the heli door(s) are open. Thanks to Shadowze. -- @param #CSAR self -- @param #string unit_name Name of unit. -- @return #boolean outcome The outcome. function CSAR:_IsLoadingDoorOpen( unit_name ) self:T(self.lid .. " _IsLoadingDoorOpen") - - --[[ - local ret_val = false - local unit = Unit.getByName(unit_name) - if unit ~= nil then - local type_name = unit:getTypeName() - - if type_name == "Mi-8MT" and unit:getDrawArgumentValue(86) == 1 or unit:getDrawArgumentValue(250) == 1 then - self:T(unit_name .. " Cargo doors are open or cargo door not present") - ret_val = true - end - - if type_name == "Mi-24P" and unit:getDrawArgumentValue(38) == 1 or unit:getDrawArgumentValue(86) == 1 then - self:T(unit_name .. " a side door is open") - ret_val = true - end - - if type_name == "UH-1H" and unit:getDrawArgumentValue(43) == 1 or unit:getDrawArgumentValue(44) == 1 then - self:T(unit_name .. " a side door is open ") - ret_val = true - end - - if string.find(type_name, "SA342" ) and unit:getDrawArgumentValue(34) == 1 or unit:getDrawArgumentValue(38) == 1 then - self:T(unit_name .. " front door(s) are open") - ret_val = true - end - - if ret_val == false then - self:T(unit_name .. " all doors are closed") - end - return ret_val - - end -- nil - --]] return UTILS.IsLoadingDoorOpen(unit_name) - end --- (Internal) Function to check if heli is close to group. @@ -122056,49 +134174,49 @@ function CSAR:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedG if self.heliCloseMessage[_lookupKeyHeli] == nil then if self.autosmoke == true then - self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. You\'re close now! Land or hover at the smoke.", _heliName, _pilotName), self.messageTime,false,true) + self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. You\'re close now! Land or hover at the smoke.", self:_GetCustomCallSign(_heliName), _pilotName), self.messageTime,false,true) else - self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. You\'re close now! Land in a safe place, I will go there ", _heliName, _pilotName), self.messageTime,false,true) + self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. You\'re close now! Land in a safe place, I will go there ", self:_GetCustomCallSign(_heliName), _pilotName), self.messageTime,false,true) end - --mark as shown for THIS heli and THIS group self.heliCloseMessage[_lookupKeyHeli] = true end -- have we landed close enough? if not _heliUnit:InAir() then - -- if you land on them, doesnt matter if they were heading to someone else as you\'re closer, you win! :) if self.pilotRuntoExtractPoint == true then if (_distance < self.extractDistance) then 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 @@ -122111,15 +134229,16 @@ function CSAR:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedG end if _heliUnit:InAir() and _unitsInHelicopter + 1 <= _maxUnits then - - if _distance < 8.0 then + -- DONE - make variable + if _distance < self.rescuehoverdistance then --check height! local leaderheight = _woundedLeader:GetHeight() if leaderheight < 0 then leaderheight = 0 end local _height = _heliUnit:GetHeight() - leaderheight - - if _height <= 20.0 then + + -- DONE - make variable + if _height <= self.rescuehoverheight then local _time = self.hoverStatus[_lookupKeyHeli] @@ -122134,18 +134253,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 @@ -122194,7 +134314,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 @@ -122218,15 +134338,17 @@ function CSAR:_RescuePilots(_heliUnit) -- Groups already rescued return end - - -- DONE: count saved units? + local PilotsSaved = self:_PilotsOnboard(_heliName) self.inTransitGroups[_heliName] = nil - local _txt = string.format("%s: The %d pilot(s) have been taken to the\nmedical clinic. Good job!", _heliName, PilotsSaved) + local _txt = string.format("%s: The %d pilot(s) have been taken to the\nmedical clinic. Good job!", self:_GetCustomCallSign(_heliName), PilotsSaved) self:_DisplayMessageToSAR(_heliUnit, _txt, self.messageTime) + + self:_UpdateUnitCargoMass(_heliName) + -- trigger event self:__Rescued(-1,_heliUnit,_heliName, PilotsSaved) return self @@ -122253,22 +134375,18 @@ 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 - local m = MESSAGE:New(_text,_time,"Info",_clear):ToGroup(group) + if _override or not self.suppressmessages then + local m = MESSAGE:New(_text,_time,"CSAR",_clear):ToGroup(group) end -- integrate SRS if _speak and self.useSRS then - local srstext = SOUNDTEXT:New(_text) - local path = self.SRSPath - local modulation = self.SRSModulation - local channel = self.SRSchannel - local msrs = MSRS:New(path,channel,modulation) - msrs:PlaySoundText(srstext, 2) + self.SRSQueue:NewTransmission(_text,nil,self.msrs,nil,2) end return self end @@ -122315,7 +134433,6 @@ function CSAR:_DisplayActiveSAR(_unitName) local _groupName = _value.name self:T(string.format("Display Active Pilot: %s", tostring(_groupName))) self:T({Table=_value}) - --local _woundedGroup = GROUP:FindByName(_groupName) local _woundedGroup = _value.group if _woundedGroup and _value.alive then local _coordinatesText = self:_GetPositionOfWounded(_woundedGroup) @@ -122329,7 +134446,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 @@ -122343,7 +134464,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 @@ -122401,7 +134522,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 @@ -122410,8 +134531,8 @@ function CSAR:_SignalFlare(_unitName) else _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) + local _msg = string.format("%s - Popping signal flare at your %s o\'clock. Distance %s", self:_GetCustomCallSign(_unitName), _clockDir, _distance) + self:_DisplayMessageToSAR(_heli, _msg, self.messageTime, false, true, true) local _coord = _closest.pilot:GetCoordinate() _coord:FlareRed(_clockDir) @@ -122422,7 +134543,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 @@ -122456,16 +134577,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", self:_GetCustomCallSign(_unitName), _clockDir, _distance) + self:_DisplayMessageToSAR(_heli, _msg, self.messageTime, false, true, true) local _coord = _closest.pilot:GetCoordinate() local color = self.smokecolor _coord:Smoke(color) @@ -122476,7 +134597,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 @@ -122515,7 +134636,7 @@ function CSAR:_GetClosestMASH(_heli) if self.allowFARPRescue then local position = _heli:GetCoordinate() - local afb,distance = position:GetClosestAirbase2(nil,self.coalition) + local afb,distance = position:GetClosestAirbase(nil,self.coalition) _shortestDistance = distance end @@ -122548,13 +134669,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 @@ -122565,7 +134686,7 @@ function CSAR:_AddMedevacMenuItem() self:T(self.lid .. " _AddMedevacMenuItem") local coalition = self.coalition - local allheligroupset = self.allheligroupset + local allheligroupset = self.allheligroupset -- Core.Set#SET_GROUP local _allHeliGroups = allheligroupset:GetSetObjects() -- rebuild units table @@ -122590,7 +134711,8 @@ function CSAR:_AddMedevacMenuItem() local groupname = _group:GetName() if self.addedTo[groupname] == nil then self.addedTo[groupname] = true - local _rootPath = MENU_GROUP:New(_group,"CSAR") + local menuname = self.topmenuname or "CSAR" + local _rootPath = MENU_GROUP:New(_group,menuname) local _rootMenu1 = MENU_GROUP_COMMAND:New(_group,"List Active CSAR",_rootPath, self._DisplayActiveSAR,self,_unitName) local _rootMenu2 = MENU_GROUP_COMMAND:New(_group,"Check Onboard",_rootPath, self._CheckOnboard,self,_unitName) local _rootMenu3 = MENU_GROUP_COMMAND:New(_group,"Request Signal Flare",_rootPath, self._SignalFlare,self,_unitName) @@ -122612,7 +134734,6 @@ function CSAR:_GetDistance(_point1, _point2) if _point1 and _point2 then local distance1 = _point1:Get2DDistance(_point2) local distance2 = _point1:DistanceFromPointVec2(_point2) - self:I({dist1=distance1, dist2=distance2}) if distance1 and type(distance1) == "number" then return distance1 elseif distance2 and type(distance2) == "number" then @@ -122633,7 +134754,6 @@ end -- @param #CSAR self function CSAR:_GenerateVHFrequencies() self:T(self.lid .. " _GenerateVHFrequencies") - --local _skipFrequencies = self.SkipFrequencies local FreeVHFFrequencies = {} FreeVHFFrequencies = UTILS.GenerateVHFrequencies() @@ -122669,14 +134789,17 @@ function CSAR:_GetClockDirection(_heli, _group) local DirectionVec3 = _playerPosition:GetDirectionVec3( _targetpostions ) local Angle = _playerPosition:GetAngleDegrees( DirectionVec3 ) self:T(self.lid .. " _GetClockDirection"..tostring(Angle).." "..tostring(_heading)) - local clock = 12 - if _heading then - local Aspect = Angle - _heading - if Aspect == 0 then Aspect = 360 end - --clock = math.floor(Aspect / 30) - clock = math.abs(UTILS.Round((Aspect / 30),0)) - if clock == 0 then clock = 12 end - end + local hours = 0 + local clock = 12 + if _heading and Angle then + clock = 12 + --if angle == 0 then angle = 360 end + clock = _heading-Angle + hours = (clock/30)*-1 + clock = 12+hours + clock = UTILS.Round(clock,0) + if clock > 12 then clock = clock-12 end + end return clock end @@ -122699,10 +134822,14 @@ function CSAR:_AddBeaconToGroup(_group, _freq) end if _group:IsAlive() then - local _radioUnit = _group:GetUnit(1) - local Frequency = _freq -- Freq in Hertz - local Sound = "l10n/DEFAULT/"..self.radioSound - trigger.action.radioTransmission(Sound, _radioUnit:GetPositionVec3(), 0, false, Frequency, 1000) -- Beacon in MP only runs for exactly 30secs straight + local _radioUnit = _group:GetUnit(1) + if _radioUnit then + local Frequency = _freq -- Freq in Hertz + local name = _radioUnit:GetName() + local Sound = "l10n/DEFAULT/"..self.radioSound + local vec3 = _radioUnit:GetVec3() or _radioUnit:GetPositionVec3() or {x=0,y=0,z=0} + trigger.action.radioTransmission(Sound, vec3, 0, false, Frequency, self.ADFRadioPwr or 1000,name..math.random(1,10000)) -- Beacon in MP only runs for exactly 30secs straight + end end return self end @@ -122768,21 +134895,57 @@ 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) - if self.useprefix then + + if self.allowbronco then + local prefixes = self.csarPrefix or {} + self.allheligroupset = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(prefixes):FilterStart() + elseif self.useprefix then local prefixes = self.csarPrefix or {} self.allheligroupset = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(prefixes):FilterCategoryHelicopter():FilterStart() else 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 + if self.useSRS then + local path = self.SRSPath + local modulation = self.SRSModulation + local channel = self.SRSchannel + self.msrs = MSRS:New(path,channel,modulation) + self.msrs:SetPort(self.SRSport) + self.msrs:SetLabel("CSAR") + self.msrs:SetCulture(self.SRSCulture) + self.msrs:SetCoalition(self.coalition) + self.msrs:SetVoice(self.SRSVoice) + self.msrs:SetGender(self.SRSGender) + if self.SRSGPathToCredentials then + self.msrs:SetGoogle(self.SRSGPathToCredentials) + end + self.msrs:SetVolume(self.SRSVolume) + self.msrs:SetLabel("CSAR") + self.SRSQueue = MSRSQUEUE:New("CSAR") + end + self:__Status(-10) + + if self.enableLoadSave then + local interval = self.saveinterval + local filename = self.filename + local filepath = self.filepath + self:__Save(interval,filepath,filename) + end + return self end @@ -122815,7 +134978,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 @@ -122882,6 +135050,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) @@ -122912,6 +135081,29 @@ end function CSAR:onbeforeBoarded(From, Event, To, Heliname, Woundedgroupname) self:T({From, Event, To, Heliname, Woundedgroupname}) self:_ScheduledSARFlight(Heliname,Woundedgroupname) + local Unit = UNIT:FindByName(Heliname) + if Unit and Unit:IsPlayer() and self.PlayerTaskQueue then + local playername = Unit:GetPlayerName() + local dropcoord = Unit:GetCoordinate() or COORDINATE:New(0,0,0) + local dropvec2 = dropcoord:GetVec2() + self.PlayerTaskQueue:ForEach( + function (Task) + local task = Task -- Ops.PlayerTask#PLAYERTASK + local subtype = task:GetSubType() + -- right subtype? + if Event == subtype and not task:IsDone() then + local targetzone = task.Target:GetObject() -- Core.Zone#ZONE should be a zone in this case .... + if (targetzone and targetzone.ClassName and string.match(targetzone.ClassName,"ZONE") and targetzone:IsVec2InZone(dropvec2)) + or (string.find(task.CSARPilotName,Woundedgroupname)) then + if task.Clients:HasUniqueID(playername) then + -- success + task:__Success(-1) + end + end + end + end + ) + end return self end @@ -122941,6 +135133,23 @@ function CSAR:onbeforeRescued(From, Event, To, HeliUnit, HeliName, PilotsSaved) self:T({From, Event, To, HeliName, HeliUnit}) self.rescues = self.rescues + 1 self.rescuedpilots = self.rescuedpilots + PilotsSaved + local Unit = HeliUnit or UNIT:FindByName(HeliName) + if Unit and Unit:IsPlayer() and self.PlayerTaskQueue then + local playername = Unit:GetPlayerName() + self.PlayerTaskQueue:ForEach( + function (Task) + local task = Task -- Ops.PlayerTask#PLAYERTASK + local subtype = task:GetSubType() + -- right subtype? + if Event == subtype and not task:IsDone() then + if task.Clients:HasUniqueID(playername) then + -- success + task:__Success(-1) + end + end + end + ) + end return self end @@ -122957,10 +135166,256 @@ 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 + +--- On before "Save" event. Checks if io and lfs are available. +-- @param #CSAR self +-- @param #string From From state. +-- @param #string Event Event. +-- @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. Default is "CSAR__Persist.csv". +function CSAR:onbeforeSave(From, Event, To, path, filename) + self:T({From, Event, To, path, filename}) + if not self.enableLoadSave then + return self + end + -- Thanks to @FunkyFranky + -- Check io module is available. + if not io then + self:E(self.lid.."ERROR: io not desanitized. Can't save current state.") + return false + end + + -- Check default path. + if path==nil and not lfs then + self:E(self.lid.."WARNING: lfs not desanitized. State will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.") + end + + return true +end + +--- On after "Save" event. Player data is saved to file. +-- @param #CSAR self +-- @param #string From From state. +-- @param #string Event Event. +-- @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. Default is Default is "CSAR__Persist.csv". +function CSAR:onafterSave(From, Event, To, path, filename) + self:T({From, Event, To, path, filename}) + -- Thanks to @FunkyFranky + if not self.enableLoadSave then + return self + end + --- Function that saves data to file + 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=self.filepath or lfs.writedir() + end + + -- Set file name. + filename=filename or self.filename + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + local pilots = self.downedPilots + + --local data = "LoadedData = {\n" + local data = "playerName,x,y,z,coalition,country,description,typeName,unitName,freq\n" + local n = 0 + for _,_grp in pairs(pilots) do + local DownedPilot = _grp -- Wrapper.Group#GROUP + if DownedPilot and DownedPilot.alive then + -- get downed pilot data for saving + local playerName = DownedPilot.player + local group = DownedPilot.group + local coalition = group:GetCoalition() + local country = group:GetCountry() + local description = DownedPilot.desc + local typeName = DownedPilot.typename + local freq = DownedPilot.frequency + local location = group:GetVec3() + local unitName = DownedPilot.originalUnit + local txt = string.format("%s,%d,%d,%d,%s,%s,%s,%s,%s,%d\n",playerName,location.x,location.y,location.z,coalition,country,description,typeName,unitName,freq) + + self:I(self.lid.."Saving to CSAR File: " .. txt) + + data = data .. txt + end + end + + _savefile(filename, data) + + -- AutoSave + if self.enableLoadSave then + local interval = self.saveinterval + local filename = self.filename + local filepath = self.filepath + self:__Save(interval,filepath,filename) + end + return self +end + +--- On before "Load" event. Checks if io and lfs and the file are available. +-- @param #CSAR self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string path (Optional) Path where the file is located. 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 loading. Default is "CSAR__Persist.csv". +function CSAR:onbeforeLoad(From, Event, To, path, filename) + self:T({From, Event, To, path, filename}) + if not self.enableLoadSave then + return self + end + --- 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 + + -- Set file name and path + filename=filename or self.filename + path = path or self.filepath + + -- Check io module is available. + if not io then + self:E(self.lid.."WARNING: io not desanitized. Cannot load file.") + return false + end + + -- Check default path. + if path==nil and not lfs then + self:E(self.lid.."WARNING: lfs not desanitized. State 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() + end + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + -- Check if file exists. + local exists=_fileexists(filename) + + if exists then + return true + else + self:E(self.lid..string.format("WARNING: State file %s might not exist.", filename)) + return false + --return self + end + +end + +--- On after "Load" event. Loads dropped units from file. +-- @param #CSAR self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string path (Optional) Path where the file is located. 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 loading. Default is "CSAR__Persist.csv". +function CSAR:onafterLoad(From, Event, To, path, filename) + self:T({From, Event, To, path, filename}) + if not self.enableLoadSave then + return self + end + --- Function that loads data from a file. + local function _loadfile(filename) + local f=assert(io.open(filename, "rb")) + local data=f:read("*all") + f:close() + return data + end + + -- Set file name and path + filename=filename or self.filename + path = path or self.filepath + + -- Set path or default. + if lfs then + path=path or lfs.writedir() + end + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + -- Info message. + local text=string.format("Loading CSAR state from file %s", filename) + MESSAGE:New(text,10):ToAllIf(self.Debug) + self:I(self.lid..text) + + local file=assert(io.open(filename, "rb")) + + local loadeddata = {} + for line in file:lines() do + loadeddata[#loadeddata+1] = line + end + file:close() + + -- remove header + table.remove(loadeddata, 1) + + for _id,_entry in pairs (loadeddata) do + local dataset = UTILS.Split(_entry,",") + -- 1=playerName,2=x,3=y,4=z,5=coalition,6=country,7=description,8=typeName,9=unitName,10=freq\n + local playerName = dataset[1] + + local vec3 = {} + vec3.x = tonumber(dataset[2]) + vec3.y = tonumber(dataset[3]) + vec3.z = tonumber(dataset[4]) + local point = COORDINATE:NewFromVec3(vec3) + + local coalition = dataset[5] + local country = dataset[6] + local description = dataset[7] + local typeName = dataset[8] + local unitName = dataset[9] + local freq = dataset[10] + + self:_AddCsar(coalition, country, point, typeName, unitName, playerName, freq, nil, description, nil) + end + + return self +end + -------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- End Ops.CSAR -------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- **AI** -- Balance player slots with AI to create an engaging simulation environment, independent of the amount of players. +--- **AI** - Balance player slots with AI to create an engaging simulation environment, independent of the amount of players. -- -- **Features:** -- @@ -122971,7 +135426,7 @@ end -- -- === -- --- ### [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) -- -- === -- @@ -123522,6 +135977,9 @@ function AI_AIR:New( AIGroup ) self.IdleCount = 0 + self.RTBSpeedMaxFactor = 0.6 + self.RTBSpeedMinFactor = 0.5 + return self end @@ -123639,11 +136097,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 targeted 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 ) @@ -123656,14 +136114,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 ) @@ -123675,7 +136133,7 @@ end ---- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. +--- Defines a new patrol route using the @{AI.AI_Patrol#AI_PATROL_ZONE} parameters and settings. -- @param #AI_AIR self -- @return #AI_AIR self -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. @@ -123739,27 +136197,27 @@ function AI_AIR:onafterStatus() -- self:Home( "Destroy" ) -- end -- end - + if not self:Is( "Fuel" ) and not self:Is( "Home" ) and not self:is( "Refuelling" )then - + 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 - + if self.TankerName then self:I( self.Controllable:GetName() .. " is out of fuel: " .. Fuel .. " ... Refuelling at Tanker!" ) self:Refuel() else self:I( self.Controllable:GetName() .. " is out of fuel: " .. Fuel .. " ... RTB!" ) local OldAIControllable = self.Controllable - + local OrbitTask = OldAIControllable:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed ) local TimedOrbitTask = OldAIControllable:TaskControlled( OrbitTask, OldAIControllable:TaskCondition(nil,nil,nil,nil,self.OutOfFuelOrbitTime,nil ) ) OldAIControllable:SetTask( TimedOrbitTask, 10 ) - + self:Fuel() RTB = true end @@ -123770,11 +136228,11 @@ function AI_AIR:onafterStatus() if self:Is( "Fuel" ) and not self:Is( "Home" ) and not self:is( "Refuelling" ) then RTB = true end - + -- TODO: Check GROUP damage function. local Damage = self.Controllable:GetLife() local InitialLife = self.Controllable:GetLife0() - + -- If the group is damaged, then RTB. -- Note that a group can consist of more units, so if one unit is damaged of a group, the mission may continue. -- The damaged unit will RTB due to DCS logic, and the others will continue to engage. @@ -123784,7 +136242,7 @@ function AI_AIR:onafterStatus() RTB = true self:SetStatusOff() end - + -- Check if planes went RTB and are out of control. -- We only check if planes are out of control, when they are in duty. if self.Controllable:HasTask() == false then @@ -123798,7 +136256,7 @@ function AI_AIR:onafterStatus() self:Damaged() else self:I( self.Controllable:GetName() .. " control lost! " ) - + self:LostControl() end else @@ -123816,7 +136274,7 @@ function AI_AIR:onafterStatus() if not self:Is("Home") then self:__Status( 10 ) end - + end end @@ -123825,11 +136283,11 @@ end function AI_AIR.RTBRoute( AIGroup, Fsm ) AIGroup:F( { "AI_AIR.RTBRoute:", AIGroup:GetName() } ) - + if AIGroup:IsAlive() then Fsm:RTB() end - + end --- @param Wrapper.Group#GROUP AIGroup @@ -123842,7 +136300,20 @@ function AI_AIR.RTBHold( AIGroup, Fsm ) local Task = AIGroup:TaskOrbitCircle( 4000, 400 ) AIGroup:SetTask( Task ) end - + +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 @@ -123851,50 +136322,53 @@ end function AI_AIR:onafterRTB( AIGroup, From, Event, To ) self:F( { AIGroup, From, Event, To } ) - - if AIGroup and AIGroup:IsAlive() then + if AIGroup and AIGroup:IsAlive() then + + self:T( "Group " .. AIGroup:GetName() .. " ... RTB! ( " .. self:GetState() .. " )" ) - self:I( "Group " .. AIGroup:GetName() .. " ... RTB! ( " .. self:GetState() .. " )" ) - self:ClearTargetDistance() --AIGroup:ClearTasks() + AIGroup:OptionProhibitAfterburner(true) + local EngageRoute = {} --- Calculate the target route point. - + 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 ) --local ToAirbaseAngle = FromCoord:GetAngleDegrees( FromCoord:GetDirectionVec3( ToTargetCoord2 ) ) local Distance = FromCoord:Get2DDistance( ToTargetCoord2 ) - + --local ToAirbaseCoord = FromCoord:Translate( 5000, ToAirbaseAngle ) local ToAirbaseCoord = ToTargetCoord2 - + if Distance < 5000 then self:I( "RTB and near the airbase!" ) self:Home() return end - + if not AIGroup:InAir() == true then self:I( "Not anymore in the air, considered Home." ) self:Home() return end - - + + --- Create a route point of type air. local FromRTBRoutePoint = FromCoord:WaypointAir( self.PatrolAltType, @@ -123915,10 +136389,10 @@ function AI_AIR:onafterRTB( AIGroup, From, Event, To ) EngageRoute[#EngageRoute+1] = FromRTBRoutePoint EngageRoute[#EngageRoute+1] = ToRTBRoutePoint - + local Tasks = {} Tasks[#Tasks+1] = AIGroup:TaskFunction( "AI_AIR.RTBRoute", self ) - + EngageRoute[#EngageRoute].task = AIGroup:TaskCombo( Tasks ) AIGroup:OptionROEHoldFire() @@ -123926,9 +136400,9 @@ function AI_AIR:onafterRTB( AIGroup, From, Event, To ) --- NOW ROUTE THE GROUP! AIGroup:Route( EngageRoute, self.TaskDelay ) - + end - + end --- @param #AI_AIR self @@ -124077,7 +136551,7 @@ function AI_AIR:OnPilotDead( EventData ) self:__PilotDead( self.TaskDelay, EventData ) end end ---- **AI** -- Models the process of A2G patrolling and engaging ground targets for airplanes and helicopters. +--- **AI** - Models the process of A2G patrolling and engaging ground targets for airplanes and helicopters. -- -- === -- @@ -124091,8 +136565,7 @@ end --- @type AI_AIR_PATROL -- @extends AI.AI_Air#AI_AIR - ---- The AI_AIR_PATROL class implements the core functions to patrol a @{Zone} by an AI @{Wrapper.Group} or @{Wrapper.Group} +--- The AI_AIR_PATROL class implements the core functions to patrol a @{Core.Zone} by an AI @{Wrapper.Group} -- and automatically engage any airborne enemies that are within a certain range or within a certain zone. -- -- ![Process](..\Presentations\AI_CAP\Dia3.JPG) @@ -124118,8 +136591,8 @@ end -- -- ![Process](..\Presentations\AI_CAP\Dia10.JPG) -- --- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. --- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- Until a fuel or damage threshold has been reached by the AI, or when the AI is commanded to RTB. +-- When the fuel threshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. -- -- ![Process](..\Presentations\AI_CAP\Dia13.JPG) -- @@ -124149,7 +136622,7 @@ end -- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. -- * **@{#AI_AIR_PATROL.Destroy}**: The AI has destroyed a bogey @{Wrapper.Unit}. -- * **@{#AI_AIR_PATROL.Destroyed}**: The AI has destroyed all bogeys @{Wrapper.Unit}s assigned in the CAS task. --- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. +-- * **Status** ( Group ): The AI is checking status (fuel and damage). When the thresholds have been reached, the AI will RTB. -- -- ## 3. Set the Range of Engagement -- @@ -124165,9 +136638,9 @@ end -- -- ![Zone](..\Presentations\AI_CAP\Dia12.JPG) -- --- An optional @{Zone} can be set, +-- An optional @{Core.Zone} can be set, -- that will define when the AI will engage with the detected airborne enemy targets. --- Use the method @{AI.AI_Cap#AI_AIR_PATROL.SetEngageZone}() to define that Zone. +-- Use the method @{AI.AI_CAP#AI_AIR_PATROL.SetEngageZone}() to define that Zone. -- -- === -- @@ -124180,7 +136653,7 @@ AI_AIR_PATROL = { -- @param #AI_AIR_PATROL self -- @param AI.AI_Air#AI_AIR AI_Air The AI_AIR FSM. -- @param Wrapper.Group#GROUP AIGroup The AI group. --- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Core.Zone} where the patrol needs to be executed. -- @param DCS#Altitude PatrolFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the patrol. -- @param DCS#Altitude PatrolCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the patrol. -- @param DCS#Speed PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed of the @{Wrapper.Group} in km/h. @@ -124193,17 +136666,17 @@ function AI_AIR_PATROL:New( AI_Air, AIGroup, PatrolZone, PatrolFloorAltitude, Pa local self = BASE:Inherit( self, AI_Air ) -- #AI_AIR_PATROL local SpeedMax = AIGroup:GetSpeedMax() - + self.PatrolZone = PatrolZone - + self.PatrolFloorAltitude = PatrolFloorAltitude or 1000 self.PatrolCeilingAltitude = PatrolCeilingAltitude or 1500 self.PatrolMinSpeed = PatrolMinSpeed or SpeedMax * 0.5 self.PatrolMaxSpeed = PatrolMaxSpeed or SpeedMax * 0.75 - + -- defafult PatrolAltType to "RADIO" if not specified self.PatrolAltType = PatrolAltType or "RADIO" - + self:AddTransition( { "Started", "Airborne", "Refuelling" }, "Patrol", "Patrolling" ) --- OnBefore Transition Handler for Event Patrol. @@ -124214,7 +136687,7 @@ function AI_AIR_PATROL:New( AI_Air, AIGroup, PatrolZone, PatrolFloorAltitude, Pa -- @param #string Event The Event string. -- @param #string To The To State string. -- @return #boolean Return false to cancel Transition. - + --- OnAfter Transition Handler for Event Patrol. -- @function [parent=#AI_AIR_PATROL] OnAfterPatrol -- @param #AI_AIR_PATROL self @@ -124222,16 +136695,16 @@ function AI_AIR_PATROL:New( AI_Air, AIGroup, PatrolZone, PatrolFloorAltitude, Pa -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. - + --- Synchronous Event Trigger for Event Patrol. -- @function [parent=#AI_AIR_PATROL] Patrol -- @param #AI_AIR_PATROL self - + --- Asynchronous Event Trigger for Event Patrol. -- @function [parent=#AI_AIR_PATROL] __Patrol -- @param #AI_AIR_PATROL self -- @param #number Delay The delay in seconds. - + --- OnLeave Transition Handler for State Patrolling. -- @function [parent=#AI_AIR_PATROL] OnLeavePatrolling -- @param #AI_AIR_PATROL self @@ -124240,7 +136713,7 @@ function AI_AIR_PATROL:New( AI_Air, AIGroup, PatrolZone, PatrolFloorAltitude, Pa -- @param #string Event The Event string. -- @param #string To The To State string. -- @return #boolean Return false to cancel Transition. - + --- OnEnter Transition Handler for State Patrolling. -- @function [parent=#AI_AIR_PATROL] OnEnterPatrolling -- @param #AI_AIR_PATROL self @@ -124248,9 +136721,9 @@ function AI_AIR_PATROL:New( AI_Air, AIGroup, PatrolZone, PatrolFloorAltitude, Pa -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. - + self:AddTransition( "Patrolling", "PatrolRoute", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR_PATROL. - + --- OnBefore Transition Handler for Event PatrolRoute. -- @function [parent=#AI_AIR_PATROL] OnBeforePatrolRoute -- @param #AI_AIR_PATROL self @@ -124259,7 +136732,7 @@ function AI_AIR_PATROL:New( AI_Air, AIGroup, PatrolZone, PatrolFloorAltitude, Pa -- @param #string Event The Event string. -- @param #string To The To State string. -- @return #boolean Return false to cancel Transition. - + --- OnAfter Transition Handler for Event PatrolRoute. -- @function [parent=#AI_AIR_PATROL] OnAfterPatrolRoute -- @param #AI_AIR_PATROL self @@ -124267,23 +136740,21 @@ function AI_AIR_PATROL:New( AI_Air, AIGroup, PatrolZone, PatrolFloorAltitude, Pa -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. - + --- Synchronous Event Trigger for Event PatrolRoute. -- @function [parent=#AI_AIR_PATROL] PatrolRoute -- @param #AI_AIR_PATROL self - + --- Asynchronous Event Trigger for Event PatrolRoute. -- @function [parent=#AI_AIR_PATROL] __PatrolRoute -- @param #AI_AIR_PATROL self -- @param #number Delay The delay in seconds. - self:AddTransition( "*", "Reset", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR_PATROL. return self end - --- Set the Engage Range when the AI will engage with airborne enemies. -- @param #AI_AIR_PATROL self -- @param #number EngageRange The Engage Range. @@ -124309,7 +136780,7 @@ end -- @param #table CapCoordinates Table of coordinates of first race track point. Second point is determined by leg length and heading. -- @return #AI_AIR_PATROL self function AI_AIR_PATROL:SetRaceTrackPattern(LegMin, LegMax, HeadingMin, HeadingMax, DurationMin, DurationMax, CapCoordinates) - + self.racetrack=true self.racetracklegmin=LegMin or 10000 self.racetracklegmax=LegMax or 15000 @@ -124317,18 +136788,16 @@ function AI_AIR_PATROL:SetRaceTrackPattern(LegMin, LegMax, HeadingMin, HeadingMa self.racetrackheadingmax=HeadingMax or 180 self.racetrackdurationmin=DurationMin self.racetrackdurationmax=DurationMax - + if self.racetrackdurationmax and not self.racetrackdurationmin then self.racetrackdurationmin=self.racetrackdurationmax end - - self.racetrackcapcoordinates=CapCoordinates - -end + self.racetrackcapcoordinates=CapCoordinates +end ---- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. +--- Defines a new patrol route using the @{AI.AI_Patrol#AI_PATROL_ZONE} parameters and settings. -- @param #AI_AIR_PATROL self -- @return #AI_AIR_PATROL self -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. @@ -124341,7 +136810,7 @@ function AI_AIR_PATROL:onafterPatrol( AIPatrol, From, Event, To ) self:ClearTargetDistance() self:__PatrolRoute( self.TaskDelay ) - + AIPatrol:OnReSpawn( function( PatrolGroup ) self:__Reset( self.TaskDelay ) @@ -124350,7 +136819,7 @@ function AI_AIR_PATROL:onafterPatrol( AIPatrol, From, Event, To ) ) end ---- This statis method is called from the route path within the last task at the last waaypoint of the AIPatrol. +--- This static method is called from the route path within the last task at the last waypoint of the AIPatrol. -- Note that this method is required, as triggers the next route when patrolling for the AIPatrol. -- @param Wrapper.Group#GROUP AIPatrol The AI group. -- @param #AI_AIR_PATROL Fsm The FSM. @@ -124361,10 +136830,10 @@ function AI_AIR_PATROL.___PatrolRoute( AIPatrol, Fsm ) if AIPatrol and AIPatrol:IsAlive() then Fsm:PatrolRoute() end - + end ---- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. +--- Defines a new patrol route using the @{AI.AI_Patrol#AI_PATROL_ZONE} parameters and settings. -- @param #AI_AIR_PATROL self -- @param Wrapper.Group#GROUP AIPatrol The Group managed by the FSM. -- @param #string From The From State string. @@ -124379,21 +136848,20 @@ function AI_AIR_PATROL:onafterPatrolRoute( AIPatrol, From, Event, To ) return end - if AIPatrol and AIPatrol:IsAlive() then - + local PatrolRoute = {} --- Calculate the target route point. - + local CurrentCoord = AIPatrol:GetCoordinate() - + local altitude= math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ) - + local ToTargetCoord = self.PatrolZone:GetRandomPointVec2() ToTargetCoord:SetAlt( altitude ) self:SetTargetDistance( ToTargetCoord ) -- For RTB status check - + local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) local speedkmh=ToTargetSpeed @@ -124401,31 +136869,31 @@ function AI_AIR_PATROL:onafterPatrolRoute( AIPatrol, From, Event, To ) PatrolRoute[#PatrolRoute+1] = FromWP if self.racetrack then - + -- Random heading. local heading = math.random(self.racetrackheadingmin, self.racetrackheadingmax) - + -- Random leg length. local leg=math.random(self.racetracklegmin, self.racetracklegmax) - + -- Random duration if any. local duration = self.racetrackdurationmin if self.racetrackdurationmax then duration=math.random(self.racetrackdurationmin, self.racetrackdurationmax) end - + -- CAP coordinate. local c0=self.PatrolZone:GetRandomCoordinate() if self.racetrackcapcoordinates and #self.racetrackcapcoordinates>0 then c0=self.racetrackcapcoordinates[math.random(#self.racetrackcapcoordinates)] end - + -- Race track points. local c1=c0:SetAltitude(altitude) --Core.Point#COORDINATE local c2=c1:Translate(leg, heading):SetAltitude(altitude) - + self:SetTargetDistance(c0) -- For RTB status check - + -- Debug: self:T(string.format("Patrol zone race track: v=%.1f knots, h=%.1f ft, heading=%03d, leg=%d m, t=%s sec", UTILS.KmphToKnots(speedkmh), UTILS.MetersToFeet(altitude), heading, leg, tostring(duration))) --c1:MarkToAll("Race track c1") @@ -124433,39 +136901,41 @@ function AI_AIR_PATROL:onafterPatrolRoute( AIPatrol, From, Event, To ) -- Task to orbit. local taskOrbit=AIPatrol:TaskOrbit(c1, altitude, UTILS.KmphToMps(speedkmh), c2) - + -- Task function to redo the patrol at other random position. local taskPatrol=AIPatrol:TaskFunction("AI_AIR_PATROL.___PatrolRoute", self) - + -- Controlled task with task condition. local taskCond=AIPatrol:TaskCondition(nil, nil, nil, nil, duration, nil) local taskCont=AIPatrol:TaskControlled(taskOrbit, taskCond) - + -- Second waypoint PatrolRoute[2]=c1:WaypointAirTurningPoint(self.PatrolAltType, speedkmh, {taskCont, taskPatrol}, "CAP Orbit") else - + --- Create a route point of type air. local ToWP = ToTargetCoord:WaypointAir(self.PatrolAltType, POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, ToTargetSpeed, true) PatrolRoute[#PatrolRoute+1] = ToWP - + local Tasks = {} Tasks[#Tasks+1] = AIPatrol:TaskFunction("AI_AIR_PATROL.___PatrolRoute", self) PatrolRoute[#PatrolRoute].task = AIPatrol:TaskCombo( Tasks ) - + end - + AIPatrol:OptionROEReturnFire() AIPatrol:OptionROTEvadeFire() - + AIPatrol:Route( PatrolRoute, self.TaskDelay ) - + end end ---- @param Wrapper.Group#GROUP AIPatrol +--- Resumes the AIPatrol +-- @param Wrapper.Group#GROUP AIPatrol +-- @param Core.Fsm#FSM Fsm function AI_AIR_PATROL.Resume( AIPatrol, Fsm ) AIPatrol:F( { "AI_AIR_PATROL.Resume:", AIPatrol:GetName() } ) @@ -124473,11 +136943,11 @@ function AI_AIR_PATROL.Resume( AIPatrol, Fsm ) Fsm:__Reset( Fsm.TaskDelay ) Fsm:__PatrolRoute( Fsm.TaskDelay ) end - + end ---- **AI** -- Models the process of air to ground engagement for airplanes and helicopters. +--- **AI** - Models the process of air to ground engagement for airplanes and helicopters. -- --- This is a class used in the @{AI_A2G_Dispatcher}. +-- This is a class used in the @{AI.AI_A2G_Dispatcher}. -- -- === -- @@ -124519,8 +136989,8 @@ end -- -- ![Process](..\Presentations\AI_GCI\Dia10.JPG) -- --- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. --- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- Until a fuel or damage threshold has been reached by the AI, or when the AI is commanded to RTB. +-- When the fuel threshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. -- -- ![Process](..\Presentations\AI_GCI\Dia13.JPG) -- @@ -124542,9 +137012,9 @@ end -- -- ![Zone](..\Presentations\AI_GCI\Dia12.JPG) -- --- An optional @{Zone} can be set, +-- An optional @{Core.Zone} can be set, -- that will define when the AI will engage with the detected airborne enemy targets. --- Use the method @{AI.AI_Cap#AI_AIR_ENGAGE.SetEngageZone}() to define that Zone. +-- Use the method @{AI.AI_CAP#AI_AIR_ENGAGE.SetEngageZone}() to define that Zone. -- -- === -- @@ -125010,6 +137480,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 ) @@ -125074,7 +137548,7 @@ function AI_AIR_ENGAGE.Resume( AIEngage, Fsm ) end end ---- **AI** -- (R2.2) - Models the process of air patrol of airplanes. +--- **AI** - Models the process of air patrol of airplanes. -- -- === -- @@ -125089,7 +137563,7 @@ end --- @type AI_A2A_PATROL -- @extends AI.AI_A2A#AI_A2A ---- Implements the core functions to patrol a @{Zone} by an AI @{Wrapper.Group} or @{Wrapper.Group}. +--- Implements the core functions to patrol a @{Core.Zone} by an AI @{Wrapper.Group} or @{Wrapper.Group}. -- -- ![Process](..\Presentations\AI_PATROL\Dia3.JPG) -- @@ -125115,8 +137589,8 @@ end -- -- ![Process](..\Presentations\AI_PATROL\Dia10.JPG) -- --- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. --- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- Until a fuel or damage threshold has been reached by the AI, or when the AI is commanded to RTB. +-- When the fuel threshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. -- -- ![Process](..\Presentations\AI_PATROL\Dia11.JPG) -- @@ -125144,7 +137618,7 @@ end -- * **RTB** ( Group ): Route the AI to the home base. -- * **Detect** ( Group ): The AI is detecting targets. -- * **Detected** ( Group ): The AI has detected new targets. --- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. +-- * **Status** ( Group ): The AI is checking status (fuel and damage). When the thresholds have been reached, the AI will RTB. -- -- ## 3. Set or Get the AI controllable -- @@ -125176,16 +137650,16 @@ end -- ## 6. Manage the "out of fuel" in the AI_A2A_PATROL -- -- 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 AI is targetted to the AI_A2A_PATROL. +-- 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 AI is targeted to the AI_A2A_PATROL. -- Once the time is finished, the old AI will return to the base. -- Use the method @{#AI_A2A_PATROL.ManageFuel}() to have this proces in place. -- -- ## 7. Manage "damage" behaviour of the AI in the AI_A2A_PATROL -- -- When the AI is damaged, it is required that a new Patrol is started. However, damage cannon be foreseen early on. --- Therefore, when the damage treshold is reached, the AI will return immediately to the home base (RTB). +-- Therefore, when the damage threshold is reached, the AI will return immediately to the home base (RTB). -- Use the method @{#AI_A2A_PATROL.ManageDamage}() to have this proces in place. -- -- === @@ -125198,7 +137672,7 @@ AI_A2A_PATROL = { --- Creates a new AI_A2A_PATROL object -- @param #AI_A2A_PATROL self -- @param Wrapper.Group#GROUP AIPatrol The patrol group object. --- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Core.Zone} where the patrol needs to be executed. -- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. -- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. -- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. @@ -125340,7 +137814,7 @@ function AI_A2A_PATROL:SetAltitude( PatrolFloorAltitude, PatrolCeilingAltitude ) end ---- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. +--- Defines a new patrol route using the @{AI.AI_Patrol#AI_PATROL_ZONE} parameters and settings. -- @param #AI_A2A_PATROL self -- @return #AI_A2A_PATROL self -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. @@ -125363,7 +137837,7 @@ function AI_A2A_PATROL:onafterPatrol( AIPatrol, From, Event, To ) end ---- This statis method is called from the route path within the last task at the last waaypoint of the AIPatrol. +--- This static method is called from the route path within the last task at the last waypoint of the AIPatrol. -- Note that this method is required, as triggers the next route when patrolling for the AIPatrol. -- @param Wrapper.Group#GROUP AIPatrol The AI group. -- @param #AI_A2A_PATROL Fsm The FSM. @@ -125378,7 +137852,7 @@ function AI_A2A_PATROL.PatrolRoute( AIPatrol, Fsm ) end ---- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. +--- Defines a new patrol route using the @{AI.AI_Patrol#AI_PATROL_ZONE} parameters and settings. -- @param #AI_A2A_PATROL self -- @param Wrapper.Group#GROUP AIPatrol The Group managed by the FSM. -- @param #string From The From State string. @@ -125478,7 +137952,9 @@ function AI_A2A_PATROL:onafterRoute( AIPatrol, From, Event, To ) end ---- **AI** -- (R2.2) - Models the process of Combat Air Patrol (CAP) for airplanes. +--- **AI** - Models the process of Combat Air Patrol (CAP) for airplanes. +-- +-- This is a class used in the @{AI.AI_A2A_Dispatcher}. -- -- === -- @@ -125493,8 +137969,7 @@ end -- @extends AI.AI_Air_Patrol#AI_AIR_PATROL -- @extends AI.AI_Air_Engage#AI_AIR_ENGAGE - ---- The AI_A2A_CAP class implements the core functions to patrol a @{Zone} by an AI @{Wrapper.Group} or @{Wrapper.Group} +--- The AI_A2A_CAP class implements the core functions to patrol a @{Core.Zone} by an AI @{Wrapper.Group} or @{Wrapper.Group} -- and automatically engage any airborne enemies that are within a certain range or within a certain zone. -- -- ![Process](..\Presentations\AI_CAP\Dia3.JPG) @@ -125520,8 +137995,8 @@ end -- -- ![Process](..\Presentations\AI_CAP\Dia10.JPG) -- --- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. --- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- Until a fuel or damage threshold has been reached by the AI, or when the AI is commanded to RTB. +-- When the fuel threshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. -- -- ![Process](..\Presentations\AI_CAP\Dia13.JPG) -- @@ -125551,7 +138026,7 @@ end -- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. -- * **@{#AI_A2A_CAP.Destroy}**: The AI has destroyed a bogey @{Wrapper.Unit}. -- * **@{#AI_A2A_CAP.Destroyed}**: The AI has destroyed all bogeys @{Wrapper.Unit}s assigned in the CAS task. --- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. +-- * **Status** ( Group ): The AI is checking status (fuel and damage). When the thresholds have been reached, the AI will RTB. -- -- ## 3. Set the Range of Engagement -- @@ -125561,15 +138036,15 @@ end -- that will define when the AI will engage with the detected airborne enemy targets. -- The range can be beyond or smaller than the range of the Patrol Zone. -- The range is applied at the position of the AI. --- Use the method @{AI.AI_CAP#AI_A2A_CAP.SetEngageRange}() to define that range. +-- Use the method @{#AI_A2A_CAP.SetEngageRange}() to define that range. -- -- ## 4. Set the Zone of Engagement -- -- ![Zone](..\Presentations\AI_CAP\Dia12.JPG) -- --- An optional @{Zone} can be set, +-- An optional @{Core.Zone} can be set, -- that will define when the AI will engage with the detected airborne enemy targets. --- Use the method @{AI.AI_Cap#AI_A2A_CAP.SetEngageZone}() to define that Zone. +-- Use the method @{#AI_A2A_CAP.SetEngageZone}() to define that Zone. -- -- === -- @@ -125586,7 +138061,7 @@ AI_A2A_CAP = { -- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. -- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. -- @param DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO". --- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Core.Zone} where the patrol needs to be executed. -- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. -- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. -- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. @@ -125612,7 +138087,7 @@ end --- Creates a new AI_A2A_CAP object -- @param #AI_A2A_CAP self -- @param Wrapper.Group#GROUP AICap --- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Core.Zone} where the patrol needs to be executed. -- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. -- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. -- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. @@ -125671,7 +138146,7 @@ end --- Evaluate the attack and create an AttackUnitTask list. -- @param #AI_A2A_CAP self -- @param Core.Set#SET_UNIT AttackSetUnit The set of units to attack. --- @param Wrappper.Group#GROUP DefenderGroup The group of defenders. +-- @param Wrapper.Group#GROUP DefenderGroup The group of defenders. -- @param #number EngageAltitude The altitude to engage the targets. -- @return #AI_A2A_CAP self function AI_A2A_CAP:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageAltitude ) @@ -125690,9 +138165,9 @@ function AI_A2A_CAP:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageA return AttackUnitTasks end ---- **AI** -- (R2.2) - Models the process of Ground Controlled Interception (GCI) for airplanes. +--- **AI** - Models the process of Ground Controlled Interception (GCI) for airplanes. -- --- This is a class used in the @{AI_A2A_Dispatcher}. +-- This is a class used in the @{AI.AI_A2A_Dispatcher}. -- -- === -- @@ -125700,7 +138175,7 @@ end -- -- === -- --- @module AI.AI_A2A_GCI +-- @module AI.AI_A2A_Gci -- @image AI_Ground_Control_Intercept.JPG @@ -125734,8 +138209,8 @@ end -- -- ![Process](..\Presentations\AI_GCI\Dia10.JPG) -- --- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. --- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- Until a fuel or damage threshold has been reached by the AI, or when the AI is commanded to RTB. +-- When the fuel threshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. -- -- ![Process](..\Presentations\AI_GCI\Dia13.JPG) -- @@ -125765,7 +138240,7 @@ end -- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. -- * **@{#AI_A2A_GCI.Destroy}**: The AI has destroyed a bogey @{Wrapper.Unit}. -- * **@{#AI_A2A_GCI.Destroyed}**: The AI has destroyed all bogeys @{Wrapper.Unit}s assigned in the CAS task. --- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. +-- * **Status** ( Group ): The AI is checking status (fuel and damage). When the thresholds have been reached, the AI will RTB. -- -- ## 3. Set the Range of Engagement -- @@ -125781,9 +138256,9 @@ end -- -- ![Zone](..\Presentations\AI_GCI\Dia12.JPG) -- --- An optional @{Zone} can be set, +-- An optional @{Core.Zone} can be set, -- that will define when the AI will engage with the detected airborne enemy targets. --- Use the method @{AI.AI_Cap#AI_A2A_GCI.SetEngageZone}() to define that Zone. +-- Use the method @{AI.AI_CAP#AI_CAP_ZONE.SetEngageZone}() to define that Zone. -- -- === -- @@ -125845,7 +138320,7 @@ end --- Evaluate the attack and create an AttackUnitTask list. -- @param #AI_A2A_GCI self -- @param Core.Set#SET_UNIT AttackSetUnit The set of units to attack. --- @param Wrappper.Group#GROUP DefenderGroup The group of defenders. +-- @param Wrapper.Group#GROUP DefenderGroup The group of defenders. -- @param #number EngageAltitude The altitude to engage the targets. -- @return #AI_A2A_GCI self function AI_A2A_GCI:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageAltitude ) @@ -125864,7 +138339,7 @@ function AI_A2A_GCI:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageA return AttackUnitTasks end ---- **AI** - (R2.2) - Manages the process of an automatic A2A defense system based on an EWR network targets and coordinating CAP and GCI. +--- **AI** - Manages the process of an automatic A2A defense system based on an EWR network targets and coordinating CAP and GCI. -- -- === -- @@ -125906,7 +138381,7 @@ end -- AI\_A2A\_DISPATCHER is the main A2A defense class that models the A2A defense system. -- AI\_A2A\_GCICAP derives or inherits from AI\_A2A\_DISPATCHER and is a more **noob** user friendly class, but is less flexible. -- --- Before you start using the AI\_A2A\_DISPATCHER or AI\_A2A\_GCICAP ask youself the following questions. +-- Before you start using the AI\_A2A\_DISPATCHER or AI\_A2A\_GCICAP ask yourself the following questions. -- -- ## 0. Do I need AI\_A2A\_DISPATCHER or do I need AI\_A2A\_GCICAP? -- @@ -125923,8 +138398,8 @@ end -- -- ## 2. Which type of EWR will I setup? Grouping based per AREA, per TYPE or per UNIT? (Later others will follow). -- --- The MOOSE framework leverages the @{Detection} classes to perform the EWR detection. --- Several types of @{Detection} classes exist, and the most common characteristics of these classes is that they: +-- The MOOSE framework leverages the @{Functional.Detection} classes to perform the EWR detection. +-- Several types of @{Functional.Detection} classes exist, and the most common characteristics of these classes is that they: -- -- * Perform detections from multiple FACs as one co-operating entity. -- * Communicate with a Head Quarters, which consolidates each detection. @@ -125948,7 +138423,7 @@ end -- -- A good functioning defense will have a "maximum range" evaluated to the enemy when CAP will be engaged or GCI will be spawned. -- --- ## 6. Which Airbases, Carrier Ships, Farps will take part in the defense system for the Coalition? +-- ## 6. Which Airbases, Carrier Ships, FARPs will take part in the defense system for the Coalition? -- -- Carefully plan which airbases will take part in the coalition. Color each airbase in the color of the coalition. -- @@ -125957,7 +138432,7 @@ end -- The defense system works with Squadrons. Each Squadron must be given a unique name, that forms the **key** to the defense system. -- Several options and activities can be set per Squadron. -- --- ## 8. Where will the Squadrons be located? On Airbases? On Carrier Ships? On Farps? +-- ## 8. Where will the Squadrons be located? On Airbases? On Carrier Ships? On FARPs? -- -- Squadrons are placed as the "home base" on an airfield, carrier or farp. -- Carefully plan where each Squadron will be located as part of the defense system. @@ -125992,7 +138467,7 @@ end -- * polygon zones -- * moving zones -- --- Depending on the type of zone selected, a different @{Zone} object needs to be created from a ZONE_ class. +-- Depending on the type of zone selected, a different @{Core.Zone} object needs to be created from a ZONE_ class. -- -- ## 14. For each Squadron doing CAP, what are the time intervals and CAP amounts to be performed? -- @@ -126015,7 +138490,7 @@ end -- * From a parking spot with running engines -- * From a parking spot with cold engines -- --- **The default takeoff method is staight in the air.** +-- **The default takeoff method is straight in the air.** -- -- ## 17. For each Squadron, which landing method will I use? -- @@ -126051,8 +138526,6 @@ end -- @module AI.AI_A2A_Dispatcher -- @image AI_Air_To_Air_Dispatching.JPG - - do -- AI_A2A_DISPATCHER --- AI_A2A_DISPATCHER class. @@ -126132,7 +138605,7 @@ do -- AI_A2A_DISPATCHER -- **DetectionSetGroup** is then being configured to filter all active groups with a group name starting with **DF CCCP AWACS** or **DF CCCP EWR** to be included in the Set. -- **DetectionSetGroup** is then being ordered to start the dynamic filtering. Note that any destroy or new spawn of a group with the above names will be removed or added to the Set. -- - -- Then a new Detection object is created from the class DETECTION_AREAS. A grouping radius of 30000 is choosen, which is 30km. + -- Then a new Detection object is created from the class DETECTION_AREAS. A grouping radius of 30000 is chosen, which is 30km. -- The **Detection** object is then passed to the @{#AI_A2A_DISPATCHER.New}() method to indicate the EWR network configuration and setup the A2A defense detection mechanism. -- -- You could build a **mutual defense system** like this: @@ -126224,7 +138697,7 @@ do -- AI_A2A_DISPATCHER -- -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia9.JPG) -- - -- If it's a cold war then the **borders of red and blue territory** need to be defined using a @{zone} object derived from @{Core.Zone#ZONE_BASE}. + -- If it's a cold war then the **borders of red and blue territory** need to be defined using a @{Core.Zone} object derived from @{Core.Zone#ZONE_BASE}. -- If a hot war is chosen then **no borders** actually need to be defined using the helicopter units other than -- it makes it easier sometimes for the mission maker to envisage where the red and blue territories roughly are. -- In a hot war the borders are effectively defined by the ground based radar coverage of a coalition. @@ -126293,7 +138766,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: @@ -126340,7 +138813,7 @@ do -- AI_A2A_DISPATCHER -- * @{#AI_A2A_DISPATCHER.SetSquadronLandingAtRunway}() will despawn the returning aircraft directly after landing at the runway. -- * @{#AI_A2A_DISPATCHER.SetSquadronLandingAtEngineShutdown}() will despawn the returning aircraft when the aircraft has returned to its parking spot and has turned off its engines. -- - -- You can use these methods to minimize the airbase coodination overhead and to increase the airbase efficiency. + -- You can use these methods to minimize the airbase coordination overhead and to increase the airbase efficiency. -- When there are lots of aircraft returning for landing, at the same airbase, the takeoff process will be halted, which can cause a complete failure of the -- A2A defense system, as no new CAP or GCI planes can takeoff. -- Note that the method @{#AI_A2A_DISPATCHER.SetSquadronLandingNearAirbase}() will only work for returning aircraft, not for damaged or out of fuel aircraft. @@ -126368,7 +138841,7 @@ do -- AI_A2A_DISPATCHER -- -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia12.JPG) -- - -- In the case of GCI, the @{#AI_A2A_DISPATCHER.SetSquadronGrouping}() method has additional behaviour. When there aren't enough CAP flights airborne, a GCI will be initiated for the remaining + -- In the case of GCI, the @{#AI_A2A_DISPATCHER.SetSquadronGrouping}() method has additional behavior. When there aren't enough CAP flights airborne, a GCI will be initiated for the remaining -- targets to be engaged. Depending on the grouping parameter, the spawned flights for GCI are grouped into this setting. -- For example with a group setting of 2, if 3 targets are detected and cannot be engaged by CAP or any airborne flight, -- a GCI needs to be started, the GCI flights will be grouped as follows: Group 1 of 2 flights and Group 2 of one flight! @@ -126403,13 +138876,13 @@ do -- AI_A2A_DISPATCHER -- -- The **overhead value is set for a Squadron**, and can be **dynamically adjusted** during mission execution, so to adjust the defense overhead when the tactical situation changes. -- - -- ## 6.5. Squadron fuel treshold. + -- ## 6.5. Squadron fuel threshold. -- - -- When an airplane gets **out of fuel** to a certain %-tage, which is by default **15% (0.15)**, there are two possible actions that can be taken: + -- When an airplane gets **out of fuel** to a certain %, which is by default **15% (0.15)**, there are two possible actions that can be taken: -- - The defender will go RTB, and will be replaced with a new defender if possible. -- - The defender will refuel at a tanker, if a tanker has been specified for the squadron. -- - -- Use the method @{#AI_A2A_DISPATCHER.SetSquadronFuelThreshold}() to set the **squadron fuel treshold** of spawned airplanes for all squadrons. + -- Use the method @{#AI_A2A_DISPATCHER.SetSquadronFuelThreshold}() to set the **squadron fuel threshold** of spawned airplanes for all squadrons. -- -- ## 7. Setup a squadron for CAP -- @@ -126460,7 +138933,7 @@ do -- AI_A2A_DISPATCHER -- A2ADispatcher:SetSquadronCap( "Maykop", CAPZoneMiddle, 4000, 8000, 600, 800, 800, 1200, "RADIO" ) -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) -- - -- Note the different @{Zone} MOOSE classes being used to create zones of different types. Please click the @{Zone} link for more information about the different zone types. + -- Note the different @{Core.Zone} MOOSE classes being used to create zones of different types. Please click the @{Core.Zone} link for more information about the different zone types. -- Zones can be circles, can be setup in the mission editor using trigger zones, but can also be setup in the mission editor as polygons and in this case GROUP objects are being used! -- -- ## 7.2. Set the squadron to execute CAP: @@ -126474,7 +138947,7 @@ do -- AI_A2A_DISPATCHER -- * The minimum and maximum engage speed -- * The type of altitude measurement -- - -- These define how the squadron will perform the CAP while partrolling. Different terrain types requires different types of CAP. + -- These define how the squadron will perform the CAP while patrolling. Different terrain types requires different types of CAP. -- -- The @{#AI_A2A_DISPATCHER.SetSquadronCapInterval}() method specifies **how much** and **when** CAP flights will takeoff. -- @@ -126542,7 +139015,7 @@ do -- AI_A2A_DISPATCHER -- Essentially this controls how many flights of GCI aircraft can be active at any time. -- Note allowing large numbers of active GCI flights can adversely impact mission performance on low or medium specification hosts/servers. -- GCI needs to be setup at strategic airbases. Too far will mean that the aircraft need to fly a long way to reach the intruders, - -- too short will mean that the intruders may have alraedy passed the ideal interception point! + -- too short will mean that the intruders may have already passed the ideal interception point! -- -- For example, the following setup will create a GCI for squadron "Sochi": -- @@ -126600,17 +139073,17 @@ do -- AI_A2A_DISPATCHER -- -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultGrouping}() to set the **default grouping** of spawned airplanes for all squadrons. -- - -- ## 10.5. Default RTB fuel treshold. + -- ## 10.5. Default RTB fuel threshold. -- - -- When an airplane gets **out of fuel** to a certain %-tage, which is **15% (0.15)**, it will go RTB, and will be replaced with a new airplane when applicable. + -- When an airplane gets **out of fuel** to a certain %, which is **15% (0.15)**, it will go RTB, and will be replaced with a new airplane when applicable. -- - -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultFuelThreshold}() to set the **default fuel treshold** of spawned airplanes for all squadrons. + -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultFuelThreshold}() to set the **default fuel threshold** of spawned airplanes for all squadrons. -- - -- ## 10.6. Default RTB damage treshold. + -- ## 10.6. Default RTB damage threshold. -- - -- When an airplane is **damaged** to a certain %-tage, which is **40% (0.40)**, it will go RTB, and will be replaced with a new airplane when applicable. + -- When an airplane is **damaged** to a certain %, which is **40% (0.40)**, it will go RTB, and will be replaced with a new airplane when applicable. -- - -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultDamageThreshold}() to set the **default damage treshold** of spawned airplanes for all squadrons. + -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultDamageThreshold}() to set the **default damage threshold** of spawned airplanes for all squadrons. -- -- ## 10.7. Default settings for CAP. -- @@ -126639,7 +139112,7 @@ do -- AI_A2A_DISPATCHER -- -- In the mission editor, setup a group with task Refuelling. A tanker unit of the correct coalition will be automatically selected. -- Then, use the method @{#AI_A2A_DISPATCHER.SetDefaultTanker}() to set the tanker for the dispatcher. - -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultFuelThreshold}() to set the %-tage left in the defender airplane tanks when a refuel action is needed. + -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultFuelThreshold}() to set the % left in the defender airplane tanks when a refuel action is needed. -- -- When the tanker specified is alive and in the air, the tanker will be used for refuelling. -- @@ -126653,7 +139126,7 @@ do -- AI_A2A_DISPATCHER -- A2ADispatcher:SetSquadronCapInterval("Sochi", 2, 30, 600, 1 ) -- A2ADispatcher:SetSquadronGci( "Sochi", 900, 1200 ) -- - -- -- Set the default tanker for refuelling to "Tanker", when the default fuel treshold has reached 90% fuel left. + -- -- Set the default tanker for refuelling to "Tanker", when the default fuel threshold has reached 90% fuel left. -- A2ADispatcher:SetDefaultFuelThreshold( 0.9 ) -- A2ADispatcher:SetDefaultTanker( "Tanker" ) -- @@ -126718,7 +139191,6 @@ do -- AI_A2A_DISPATCHER Detection = nil, } - --- Squadron data structure. -- @type AI_A2A_DISPATCHER.Squadron -- @field #string Name Name of the squadron. @@ -126737,7 +139209,7 @@ do -- AI_A2A_DISPATCHER -- @field #number FuelThreshold Fuel threshold [0,1] for RTB. -- @field #string TankerName Name of the refuelling tanker. -- @field #table Table of template group names of the squadron. - -- @field #table Spawn Table of spaws Core.Spawn#SPAWN. + -- @field #table Spawn Table of spawns Core.Spawn#SPAWN. -- @field #table TemplatePrefixes -- @field #boolean Racetrack If true, CAP flights will perform a racetrack pattern rather than randomly patrolling the zone. -- @field #number RacetrackLengthMin Min Length of race track in meters. Default 10,000 m. @@ -126750,11 +139222,12 @@ 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. + --- Defines Landing type/location. -- @field Landing AI_A2A_DISPATCHER.Landing = { NearAirbase = 1, @@ -126763,26 +139236,26 @@ do -- AI_A2A_DISPATCHER } --- AI_A2A_DISPATCHER constructor. - -- This is defining the A2A DISPATCHER for one coaliton. + -- This is defining the A2A DISPATCHER for one coalition. -- The Dispatcher works with a @{Functional.Detection#DETECTION_BASE} object that is taking of the detection of targets using the EWR units. - -- The Detection object is polymorphic, depending on the type of detection object choosen, the detection will work differently. + -- The Detection object is polymorphic, depending on the type of detection object chosen, the detection will work differently. -- @param #AI_A2A_DISPATCHER self -- @param Functional.Detection#DETECTION_BASE Detection The DETECTION object that will detects targets using the the Early Warning Radar network. -- @return #AI_A2A_DISPATCHER self -- @usage -- - -- -- Setup the Detection, using DETECTION_AREAS. - -- -- First define the SET of GROUPs that are defining the EWR network. - -- -- Here with prefixes DF CCCP AWACS, DF CCCP EWR. - -- DetectionSetGroup = SET_GROUP:New() - -- DetectionSetGroup:FilterPrefixes( { "DF CCCP AWACS", "DF CCCP EWR" } ) - -- DetectionSetGroup:FilterStart() + -- -- Setup the Detection, using DETECTION_AREAS. + -- -- First define the SET of GROUPs that are defining the EWR network. + -- -- Here with prefixes DF CCCP AWACS, DF CCCP EWR. + -- DetectionSetGroup = SET_GROUP:New() + -- DetectionSetGroup:FilterPrefixes( { "DF CCCP AWACS", "DF CCCP EWR" } ) + -- DetectionSetGroup:FilterStart() -- - -- -- Define the DETECTION_AREAS, using the DetectionSetGroup, with a 30km grouping radius. - -- Detection = DETECTION_AREAS:New( DetectionSetGroup, 30000 ) + -- -- Define the DETECTION_AREAS, using the DetectionSetGroup, with a 30km grouping radius. + -- Detection = DETECTION_AREAS:New( DetectionSetGroup, 30000 ) -- - -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) -- + -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) -- function AI_A2A_DISPATCHER:New( Detection ) @@ -126797,14 +139270,16 @@ do -- AI_A2A_DISPATCHER self.DefenderTasks = {} -- The Defenders Tasks. self.DefenderDefault = {} -- The Defender Default Settings over all Squadrons. + self.SetSendPlayerMessages = false --#boolean Flash messages to player + -- TODO: Check detection through radar. self.Detection:FilterCategories( { Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } ) - --self.Detection:InitDetectRadar( true ) + -- self.Detection:InitDetectRadar( true ) self.Detection:SetRefreshTimeInterval( 30 ) self:SetEngageRadius() self:SetGciRadius() - self:SetIntercept( 300 ) -- A default intercept delay time of 300 seconds. + self:SetIntercept( 300 ) -- A default intercept delay time of 300 seconds. self:SetDisengageRadius( 300000 ) -- The default Disengage Radius is 300 km. self:SetDefaultTakeoff( AI_A2A_DISPATCHER.Takeoff.Air ) @@ -126817,7 +139292,6 @@ do -- AI_A2A_DISPATCHER self:SetDefaultCapTimeInterval( 180, 600 ) -- Between 180 and 600 seconds. self:SetDefaultCapLimit( 1 ) -- Maximum one CAP per squadron. - self:AddTransition( "Started", "Assign", "Started" ) --- OnAfter Transition Handler for Event Assign. @@ -126925,17 +139399,14 @@ do -- AI_A2A_DISPATCHER -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. -- @param #table Defenders Defenders table. - -- Subscribe to the CRASH event so that when planes are shot -- by a Unit from the dispatcher, they will be removed from the detection... -- This will avoid the detection to still "know" the shot unit until the next detection. -- Otherwise, a new intercept or engage may happen for an already shot plane! - self:HandleEvent( EVENTS.Crash, self.OnEventCrashOrDead ) self:HandleEvent( EVENTS.Dead, self.OnEventCrashOrDead ) - --self:HandleEvent( EVENTS.RemoveUnit, self.OnEventCrashOrDead ) - + -- self:HandleEvent( EVENTS.RemoveUnit, self.OnEventCrashOrDead ) self:HandleEvent( EVENTS.Land ) self:HandleEvent( EVENTS.EngineShutdown ) @@ -126952,7 +139423,6 @@ do -- AI_A2A_DISPATCHER return self end - --- On after "Start" event. -- @param #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:onafterStart( From, Event, To ) @@ -126960,8 +139430,8 @@ do -- AI_A2A_DISPATCHER self:GetParent( self, AI_A2A_DISPATCHER ).onafterStart( self, From, Event, To ) -- Spawn the resources. - for SquadronName,_DefenderSquadron in pairs( self.DefenderSquadrons ) do - local DefenderSquadron=_DefenderSquadron --#AI_A2A_DISPATCHER.Squadron + for SquadronName, _DefenderSquadron in pairs( self.DefenderSquadrons ) do + local DefenderSquadron = _DefenderSquadron -- #AI_A2A_DISPATCHER.Squadron DefenderSquadron.Resources = {} if DefenderSquadron.ResourceCount then for Resource = 1, DefenderSquadron.ResourceCount do @@ -126971,7 +139441,6 @@ do -- AI_A2A_DISPATCHER end end - --- Park defender. -- @param #AI_A2A_DISPATCHER self -- @param #AI_A2A_DISPATCHER.Squadron DefenderSquadron The squadron. @@ -126979,7 +139448,7 @@ do -- AI_A2A_DISPATCHER local TemplateID = math.random( 1, #DefenderSquadron.Spawn ) - local Spawn = DefenderSquadron.Spawn[ TemplateID ] -- Core.Spawn#SPAWN + local Spawn = DefenderSquadron.Spawn[TemplateID] -- Core.Spawn#SPAWN Spawn:InitGrouping( 1 ) @@ -126987,11 +139456,11 @@ do -- AI_A2A_DISPATCHER if self:IsSquadronVisible( DefenderSquadron.Name ) then - local Grouping=DefenderSquadron.Grouping or self.DefenderDefault.Grouping + local Grouping = DefenderSquadron.Grouping or self.DefenderDefault.Grouping - Grouping=1 + Grouping = 1 - Spawn:InitGrouping(Grouping) + Spawn:InitGrouping( Grouping ) SpawnGroup = Spawn:SpawnAtAirbase( DefenderSquadron.Airbase, SPAWN.Takeoff.Cold ) @@ -127003,15 +139472,14 @@ do -- AI_A2A_DISPATCHER DefenderSquadron.Resources[TemplateID][GroupName] = {} DefenderSquadron.Resources[TemplateID][GroupName] = SpawnGroup - self.uncontrolled=self.uncontrolled or {} - self.uncontrolled[DefenderSquadron.Name]=self.uncontrolled[DefenderSquadron.Name] or {} + self.uncontrolled = self.uncontrolled or {} + self.uncontrolled[DefenderSquadron.Name] = self.uncontrolled[DefenderSquadron.Name] or {} - table.insert(self.uncontrolled[DefenderSquadron.Name], {group=SpawnGroup, name=GroupName, grouping=Grouping}) + table.insert( self.uncontrolled[DefenderSquadron.Name], { group = SpawnGroup, name = GroupName, grouping = Grouping } ) end end - --- Event base captured. -- @param #AI_A2A_DISPATCHER self -- @param Core.Event#EVENTDATA EventData @@ -127021,7 +139489,7 @@ do -- AI_A2A_DISPATCHER self:I( "Captured " .. AirbaseName ) - -- Now search for all squadrons located at the airbase, and sanatize them. + -- Now search for all squadrons located at the airbase, and sanitize them. for SquadronName, Squadron in pairs( self.DefenderSquadrons ) do if Squadron.AirbaseName == AirbaseName then Squadron.ResourceCount = -999 -- The base has been captured, and the resources are eliminated. No more spawning. @@ -127076,8 +139544,7 @@ do -- AI_A2A_DISPATCHER if Squadron then self:F( { SquadronName = Squadron.Name } ) local LandingMethod = self:GetSquadronLanding( Squadron.Name ) - if LandingMethod == AI_A2A_DISPATCHER.Landing.AtEngineShutdown and - not DefenderUnit:InAir() then + if LandingMethod == AI_A2A_DISPATCHER.Landing.AtEngineShutdown and not DefenderUnit:InAir() then local DefenderSize = Defender:GetSize() if DefenderSize == 1 then self:RemoveDefenderFromSquadron( Squadron, Defender ) @@ -127132,7 +139599,7 @@ do -- AI_A2A_DISPATCHER -- A2ADispatcher:SetDisengageRadius( 50000 ) -- -- -- Set 100km as the Disengage Radius. - -- A2ADispatcher:SetDisngageRadius() -- 300000 is the default value. + -- A2ADispatcher:SetDisengageRadius() -- 300000 is the default value. -- function AI_A2A_DISPATCHER:SetDisengageRadius( DisengageRadius ) @@ -127141,7 +139608,6 @@ do -- AI_A2A_DISPATCHER return self end - --- Define the radius to check if a target can be engaged by an ground controlled intercept. -- When targets are detected that are still really far off, you don't want the AI_A2A_DISPATCHER to launch intercepts just yet. -- You want it to wait until a certain Gci range is reached, which is the **distance of the closest airbase to target** @@ -127176,12 +139642,10 @@ do -- AI_A2A_DISPATCHER return self end - - --- Define a border area to simulate a **cold war** scenario. -- A **cold war** is one where CAP aircraft patrol their territory but will not attack enemy aircraft or launch GCI aircraft unless enemy aircraft enter their territory. In other words the EWR may detect an enemy aircraft but will only send aircraft to attack it if it crosses the border. -- A **hot war** is one where CAP aircraft will intercept any detected enemy aircraft and GCI aircraft will launch against detected enemy aircraft without regard for territory. In other words if the ground radar can detect the enemy aircraft then it will send CAP and GCI aircraft to attack it. - -- If it's a cold war then the **borders of red and blue territory** need to be defined using a @{zone} object derived from @{Core.Zone#ZONE_BASE}. This method needs to be used for this. + -- If it's a cold war then the **borders of red and blue territory** need to be defined using a @{Core.Zone} object derived from @{Core.Zone#ZONE_BASE}. This method needs to be used for this. -- If a hot war is chosen then **no borders** actually need to be defined using the helicopter units other than it makes it easier sometimes for the mission maker to envisage where the red and blue territories roughly are. In a hot war the borders are effectively defined by the ground based radar coverage of a coalition. Set the noborders parameter to 1 -- @param #AI_A2A_DISPATCHER self -- @param Core.Zone#ZONE_BASE BorderZone An object derived from ZONE_BASE, or a list of objects derived from ZONE_BASE. @@ -127235,18 +139699,17 @@ do -- AI_A2A_DISPATCHER return self end - - --- Set the default damage treshold when defenders will RTB. - -- The default damage treshold is by default set to 40%, which means that when the airplane is 40% damaged, it will go RTB. + --- Set the default damage threshold when defenders will RTB. + -- The default damage threshold is by default set to 40%, which means that when the airplane is 40% damaged, it will go RTB. -- @param #AI_A2A_DISPATCHER self - -- @param #number DamageThreshold A decimal number between 0 and 1, that expresses the %-tage of the damage treshold before going RTB. + -- @param #number DamageThreshold A decimal number between 0 and 1, that expresses the % of the damage threshold before going RTB. -- @return #AI_A2A_DISPATCHER self -- @usage -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) -- - -- -- Now Setup the default damage treshold. + -- -- Now Setup the default damage threshold. -- A2ADispatcher:SetDefaultDamageThreshold( 0.90 ) -- Go RTB when the airplane 90% damaged. -- function AI_A2A_DISPATCHER:SetDefaultDamageThreshold( DamageThreshold ) @@ -127256,7 +139719,6 @@ do -- AI_A2A_DISPATCHER return self end - --- Set the default CAP time interval for squadrons, which will be used to determine a random CAP timing. -- The default CAP time interval is between 180 and 600 seconds. -- @param #AI_A2A_DISPATCHER self @@ -127279,7 +139741,6 @@ do -- AI_A2A_DISPATCHER return self end - --- Set the default CAP limit for squadrons, which will be used to determine how many CAP can be airborne at the same time for the squadron. -- The default CAP limit is 1 CAP, which means one CAP group being spawned. -- @param #AI_A2A_DISPATCHER self @@ -127314,7 +139775,6 @@ do -- AI_A2A_DISPATCHER return self end - --- Calculates which AI friendlies are nearby the area -- @param #AI_A2A_DISPATCHER self -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem @@ -127374,7 +139834,7 @@ do -- AI_A2A_DISPATCHER local Message = "Clearing (" .. self.DefenderTasks[Defender].Type .. ") " Message = Message .. Defender:GetName() if Target then - Message = Message .. ( Target and ( " from " .. Target.Index .. " [" .. Target.Set:Count() .. "]" ) ) or "" + Message = Message .. (Target and (" from " .. Target.Index .. " [" .. Target.Set:Count() .. "]")) or "" end self:F( { Target = Message } ) end @@ -127394,24 +139854,23 @@ do -- AI_A2A_DISPATCHER local Message = "Clearing (" .. DefenderTask.Type .. ") " Message = Message .. Defender:GetName() if Target then - Message = Message .. ( Target and ( " from " .. Target.Index .. " [" .. Target.Set:Count() .. "]" ) ) or "" + Message = Message .. ((Target and (" from " .. Target.Index .. " [" .. Target.Set:Count() .. "]")) or "") end self:F( { Target = Message } ) end if Defender and DefenderTask and DefenderTask.Target then DefenderTask.Target = nil end --- if Defender and DefenderTask then --- if DefenderTask.Fsm:Is( "Fuel" ) --- or DefenderTask.Fsm:Is( "LostControl") --- or DefenderTask.Fsm:Is( "Damaged" ) then --- self:ClearDefenderTask( Defender ) --- end --- end + -- if Defender and DefenderTask then + -- if DefenderTask.Fsm:Is( "Fuel" ) + -- or DefenderTask.Fsm:Is( "LostControl") + -- or DefenderTask.Fsm:Is( "Damaged" ) then + -- self:ClearDefenderTask( Defender ) + -- end + -- end return self end - --- Set defender task. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName Name of the squadron. @@ -127422,7 +139881,7 @@ do -- AI_A2A_DISPATCHER -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetDefenderTask( SquadronName, Defender, Type, Fsm, Target ) - self:F( { SquadronName = SquadronName, Defender = Defender:GetName(), Type=Type, Target=Target } ) + self:F( { SquadronName = SquadronName, Defender = Defender:GetName(), Type = Type, Target = Target } ) self.DefenderTasks[Defender] = self.DefenderTasks[Defender] or {} self.DefenderTasks[Defender].Type = Type @@ -127435,7 +139894,6 @@ do -- AI_A2A_DISPATCHER return self end - --- Set defender task target. -- @param #AI_A2A_DISPATCHER self -- @param Wrapper.Group#GROUP Defender The defender group. @@ -127445,7 +139903,7 @@ do -- AI_A2A_DISPATCHER local Message = "(" .. self.DefenderTasks[Defender].Type .. ") " Message = Message .. Defender:GetName() - Message = Message .. ( AttackerDetection and ( " target " .. AttackerDetection.Index .. " [" .. AttackerDetection.Set:Count() .. "]" ) ) or "" + Message = Message .. ((AttackerDetection and (" target " .. AttackerDetection.Index .. " [" .. AttackerDetection.Set:Count() .. "]")) or "") self:F( { AttackerDetection = Message } ) if AttackerDetection then self.DefenderTasks[Defender].Target = AttackerDetection @@ -127453,7 +139911,6 @@ do -- AI_A2A_DISPATCHER return self end - --- This is the main method to define Squadrons programmatically. -- Squadrons: -- @@ -127496,6 +139953,7 @@ do -- AI_A2A_DISPATCHER -- If you have only one prefix name for a squadron, you don't need to use the `{ }`, otherwise you need to use the brackets. -- -- @param #number ResourceCount (optional) A number that specifies how many resources are in stock of the squadron. If not specified, the squadron will have infinite resources available. + -- @return #AI_A2A_DISPATCHER self -- -- @usage -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. @@ -127522,14 +139980,11 @@ do -- AI_A2A_DISPATCHER -- A2ADispatcher:SetSquadron( "104th", "Batumi", "Mig-29" ) -- A2ADispatcher:SetSquadron( "23th", "Batumi", "Su-27" ) -- - -- - -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetSquadron( SquadronName, AirbaseName, TemplatePrefixes, ResourceCount ) - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} - local DefenderSquadron = self.DefenderSquadrons[SquadronName] --#AI_A2A_DISPATCHER.Squadron + local DefenderSquadron = self.DefenderSquadrons[SquadronName] -- #AI_A2A_DISPATCHER.Squadron DefenderSquadron.Name = SquadronName DefenderSquadron.Airbase = AIRBASE:FindByName( AirbaseName ) @@ -127546,7 +140001,7 @@ do -- AI_A2A_DISPATCHER else for TemplateID, SpawnTemplate in pairs( TemplatePrefixes ) do self.DefenderSpawns[SpawnTemplate] = self.DefenderSpawns[SpawnTemplate] or SPAWN:New( SpawnTemplate ) -- :InitCleanUp( 180 ) - DefenderSquadron.Spawn[#DefenderSquadron.Spawn+1] = self.DefenderSpawns[SpawnTemplate] + DefenderSquadron.Spawn[#DefenderSquadron.Spawn + 1] = self.DefenderSpawns[SpawnTemplate] end end DefenderSquadron.ResourceCount = ResourceCount @@ -127555,7 +140010,7 @@ do -- AI_A2A_DISPATCHER self:SetSquadronLanguage( SquadronName, "EN" ) -- Squadrons speak English by default. - self:F( { Squadron = {SquadronName, AirbaseName, TemplatePrefixes, ResourceCount } } ) + self:F( { Squadron = { SquadronName, AirbaseName, TemplatePrefixes, ResourceCount } } ) return self end @@ -127574,8 +140029,7 @@ do -- AI_A2A_DISPATCHER return DefenderSquadron 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 @@ -127583,33 +140037,33 @@ do -- AI_A2A_DISPATCHER -- @return #AI_A2A_DISPATCHER self -- @usage -- - -- -- Set the Squadron visible before startup of dispatcher. - -- A2ADispatcher:SetSquadronVisible( "Mineralnye" ) + -- -- Set the Squadron visible before startup of dispatcher. + -- A2ADispatcher:SetSquadronVisible( "Mineralnye" ) -- function AI_A2A_DISPATCHER:SetSquadronVisible( SquadronName ) self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} - local DefenderSquadron = self:GetSquadron( SquadronName ) --#AI_A2A_DISPATCHER.Squadron + local DefenderSquadron = self:GetSquadron( SquadronName ) -- #AI_A2A_DISPATCHER.Squadron DefenderSquadron.Uncontrolled = true -- For now, grouping is forced to 1 due to other parts of the class which would not work well with grouping>1. - DefenderSquadron.Grouping=1 + DefenderSquadron.Grouping = 1 -- Get free parking for fighter aircraft. - local nfreeparking=DefenderSquadron.Airbase:GetFreeParkingSpotsNumber(AIRBASE.TerminalType.FighterAircraft, true) + local nfreeparking = DefenderSquadron.Airbase:GetFreeParkingSpotsNumber( AIRBASE.TerminalType.FighterAircraft, true ) - -- Take number of free parking spots if no resource count was specifed. - DefenderSquadron.ResourceCount=DefenderSquadron.ResourceCount or nfreeparking + -- Take number of free parking spots if no resource count was specified. + DefenderSquadron.ResourceCount = DefenderSquadron.ResourceCount or nfreeparking -- Check that resource count is not larger than free parking spots. - DefenderSquadron.ResourceCount=math.min(DefenderSquadron.ResourceCount, nfreeparking) + DefenderSquadron.ResourceCount = math.min( DefenderSquadron.ResourceCount, nfreeparking ) -- Set uncontrolled spawning option. - for SpawnTemplate,_DefenderSpawn in pairs( self.DefenderSpawns ) do - local DefenderSpawn=_DefenderSpawn --Core.Spawn#SPAWN - DefenderSpawn:InitUnControlled(true) + for SpawnTemplate, _DefenderSpawn in pairs( self.DefenderSpawns ) do + local DefenderSpawn = _DefenderSpawn -- Core.Spawn#SPAWN + DefenderSpawn:InitUnControlled( true ) end end @@ -127620,14 +140074,14 @@ do -- AI_A2A_DISPATCHER -- @return #boolean true if visible. -- @usage -- - -- -- Set the Squadron visible before startup of dispatcher. - -- local IsVisible = A2ADispatcher:IsSquadronVisible( "Mineralnye" ) + -- -- Set the Squadron visible before startup of dispatcher. + -- local IsVisible = A2ADispatcher:IsSquadronVisible( "Mineralnye" ) -- function AI_A2A_DISPATCHER:IsSquadronVisible( SquadronName ) self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} - local DefenderSquadron = self:GetSquadron( SquadronName ) --#AI_A2A_DISPATCHER.Squadron + local DefenderSquadron = self:GetSquadron( SquadronName ) -- #AI_A2A_DISPATCHER.Squadron if DefenderSquadron then return DefenderSquadron.Uncontrolled == true @@ -127645,7 +140099,7 @@ do -- AI_A2A_DISPATCHER -- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. -- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. -- @param #number EngageAltType The altitude type to engage, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. - -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the CAP will be executed. + -- @param Core.Zone#ZONE_BASE Zone The @{Core.Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the CAP will be executed. -- @param #number PatrolMinSpeed The minimum speed at which the cap can be executed. -- @param #number PatrolMaxSpeed The maximum speed at which the cap can be executed. -- @param #number PatrolFloorAltitude The minimum altitude at which the cap can be executed. @@ -127654,24 +140108,24 @@ do -- AI_A2A_DISPATCHER -- @return #AI_A2A_DISPATCHER -- @usage -- - -- -- CAP Squadron execution. - -- CAPZoneEast = ZONE_POLYGON:New( "CAP Zone East", GROUP:FindByName( "CAP Zone East" ) ) - -- -- Setup a CAP, engaging between 800 and 900 km/h, altitude 30 (above the sea), radio altitude measurement, - -- -- patrolling speed between 500 and 600 km/h, altitude between 4000 and 10000 meters, barometric altitude measurement. - -- A2ADispatcher:SetSquadronCapV2( "Mineralnye", 800, 900, 30, 30, "RADIO", CAPZoneEast, 500, 600, 4000, 10000, "BARO" ) - -- A2ADispatcher:SetSquadronCapInterval( "Mineralnye", 2, 30, 60, 1 ) - -- - -- CAPZoneWest = ZONE_POLYGON:New( "CAP Zone West", GROUP:FindByName( "CAP Zone West" ) ) - -- -- Setup a CAP, engaging between 800 and 1200 km/h, altitude between 4000 and 10000 meters, radio altitude measurement, - -- -- patrolling speed between 600 and 800 km/h, altitude between 4000 and 8000, barometric altitude measurement. - -- A2ADispatcher:SetSquadronCapV2( "Sochi", 800, 1200, 2000, 3000, "RADIO", CAPZoneWest, 600, 800, 4000, 8000, "BARO" ) - -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) - -- - -- CAPZoneMiddle = ZONE:New( "CAP Zone Middle") - -- -- Setup a CAP, engaging between 800 and 1200 km/h, altitude between 5000 and 8000 meters, barometric altitude measurement, - -- -- patrolling speed between 600 and 800 km/h, altitude between 4000 and 8000, radio altitude. - -- A2ADispatcher:SetSquadronCapV2( "Maykop", 800, 1200, 5000, 8000, "BARO", CAPZoneMiddle, 600, 800, 4000, 8000, "RADIO" ) - -- A2ADispatcher:SetSquadronCapInterval( "Maykop", 2, 30, 120, 1 ) + -- -- CAP Squadron execution. + -- CAPZoneEast = ZONE_POLYGON:New( "CAP Zone East", GROUP:FindByName( "CAP Zone East" ) ) + -- -- Setup a CAP, engaging between 800 and 900 km/h, altitude 30 (above the sea), radio altitude measurement, + -- -- patrolling speed between 500 and 600 km/h, altitude between 4000 and 10000 meters, barometric altitude measurement. + -- A2ADispatcher:SetSquadronCapV2( "Mineralnye", 800, 900, 30, 30, "RADIO", CAPZoneEast, 500, 600, 4000, 10000, "BARO" ) + -- A2ADispatcher:SetSquadronCapInterval( "Mineralnye", 2, 30, 60, 1 ) + -- + -- CAPZoneWest = ZONE_POLYGON:New( "CAP Zone West", GROUP:FindByName( "CAP Zone West" ) ) + -- -- Setup a CAP, engaging between 800 and 1200 km/h, altitude between 4000 and 10000 meters, radio altitude measurement, + -- -- patrolling speed between 600 and 800 km/h, altitude between 4000 and 8000, barometric altitude measurement. + -- A2ADispatcher:SetSquadronCapV2( "Sochi", 800, 1200, 2000, 3000, "RADIO", CAPZoneWest, 600, 800, 4000, 8000, "BARO" ) + -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) + -- + -- CAPZoneMiddle = ZONE:New( "CAP Zone Middle") + -- -- Setup a CAP, engaging between 800 and 1200 km/h, altitude between 5000 and 8000 meters, barometric altitude measurement, + -- -- patrolling speed between 600 and 800 km/h, altitude between 4000 and 8000, radio altitude. + -- A2ADispatcher:SetSquadronCapV2( "Maykop", 800, 1200, 5000, 8000, "BARO", CAPZoneMiddle, 600, 800, 4000, 8000, "RADIO" ) + -- A2ADispatcher:SetSquadronCapInterval( "Maykop", 2, 30, 120, 1 ) -- function AI_A2A_DISPATCHER:SetSquadronCap2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType, Zone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType ) @@ -127712,7 +140166,7 @@ do -- AI_A2A_DISPATCHER --- Set a CAP for a Squadron. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The squadron name. - -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the CAP will be executed. + -- @param Core.Zone#ZONE_BASE Zone The @{Core.Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the CAP will be executed. -- @param #number PatrolFloorAltitude The minimum altitude at which the cap can be executed. -- @param #number PatrolCeilingAltitude the maximum altitude at which the cap can be executed. -- @param #number PatrolMinSpeed The minimum speed at which the cap can be executed. @@ -127723,18 +140177,18 @@ do -- AI_A2A_DISPATCHER -- @return #AI_A2A_DISPATCHER -- @usage -- - -- -- CAP Squadron execution. - -- CAPZoneEast = ZONE_POLYGON:New( "CAP Zone East", GROUP:FindByName( "CAP Zone East" ) ) - -- A2ADispatcher:SetSquadronCap( "Mineralnye", CAPZoneEast, 4000, 10000, 500, 600, 800, 900 ) - -- A2ADispatcher:SetSquadronCapInterval( "Mineralnye", 2, 30, 60, 1 ) + -- -- CAP Squadron execution. + -- CAPZoneEast = ZONE_POLYGON:New( "CAP Zone East", GROUP:FindByName( "CAP Zone East" ) ) + -- A2ADispatcher:SetSquadronCap( "Mineralnye", CAPZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- A2ADispatcher:SetSquadronCapInterval( "Mineralnye", 2, 30, 60, 1 ) -- - -- CAPZoneWest = ZONE_POLYGON:New( "CAP Zone West", GROUP:FindByName( "CAP Zone West" ) ) - -- A2ADispatcher:SetSquadronCap( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" ) - -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) + -- CAPZoneWest = ZONE_POLYGON:New( "CAP Zone West", GROUP:FindByName( "CAP Zone West" ) ) + -- A2ADispatcher:SetSquadronCap( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" ) + -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) -- - -- CAPZoneMiddle = ZONE:New( "CAP Zone Middle") - -- A2ADispatcher:SetSquadronCap( "Maykop", CAPZoneMiddle, 4000, 8000, 600, 800, 800, 1200, "RADIO" ) - -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) + -- CAPZoneMiddle = ZONE:New( "CAP Zone Middle") + -- A2ADispatcher:SetSquadronCap( "Maykop", CAPZoneMiddle, 4000, 8000, 600, 800, 800, 1200, "RADIO" ) + -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) -- function AI_A2A_DISPATCHER:SetSquadronCap( SquadronName, Zone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType ) @@ -127751,18 +140205,18 @@ do -- AI_A2A_DISPATCHER -- @return #AI_A2A_DISPATCHER -- @usage -- - -- -- CAP Squadron execution. - -- CAPZoneEast = ZONE_POLYGON:New( "CAP Zone East", GROUP:FindByName( "CAP Zone East" ) ) - -- A2ADispatcher:SetSquadronCap( "Mineralnye", CAPZoneEast, 4000, 10000, 500, 600, 800, 900 ) - -- A2ADispatcher:SetSquadronCapInterval( "Mineralnye", 2, 30, 60, 1 ) + -- -- CAP Squadron execution. + -- CAPZoneEast = ZONE_POLYGON:New( "CAP Zone East", GROUP:FindByName( "CAP Zone East" ) ) + -- A2ADispatcher:SetSquadronCap( "Mineralnye", CAPZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- A2ADispatcher:SetSquadronCapInterval( "Mineralnye", 2, 30, 60, 1 ) -- - -- CAPZoneWest = ZONE_POLYGON:New( "CAP Zone West", GROUP:FindByName( "CAP Zone West" ) ) - -- A2ADispatcher:SetSquadronCap( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" ) - -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) + -- CAPZoneWest = ZONE_POLYGON:New( "CAP Zone West", GROUP:FindByName( "CAP Zone West" ) ) + -- A2ADispatcher:SetSquadronCap( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" ) + -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) -- - -- CAPZoneMiddle = ZONE:New( "CAP Zone Middle") - -- A2ADispatcher:SetSquadronCap( "Maykop", CAPZoneMiddle, 4000, 8000, 600, 800, 800, 1200, "RADIO" ) - -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) + -- CAPZoneMiddle = ZONE:New( "CAP Zone Middle") + -- A2ADispatcher:SetSquadronCap( "Maykop", CAPZoneMiddle, 4000, 8000, 600, 800, 800, 1200, "RADIO" ) + -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) -- function AI_A2A_DISPATCHER:SetSquadronCapInterval( SquadronName, CapLimit, LowInterval, HighInterval, Probability ) @@ -127780,7 +140234,7 @@ do -- AI_A2A_DISPATCHER Cap.Scheduler = Cap.Scheduler or SCHEDULER:New( self ) local Scheduler = Cap.Scheduler -- Core.Scheduler#SCHEDULER local ScheduleID = Cap.ScheduleID - local Variance = ( Cap.HighInterval - Cap.LowInterval ) / 2 + local Variance = (Cap.HighInterval - Cap.LowInterval) / 2 local Repeat = Cap.LowInterval + Variance local Randomization = Variance / Repeat local Start = math.random( 1, Cap.HighInterval ) @@ -127820,7 +140274,7 @@ do -- AI_A2A_DISPATCHER -- @param #string SquadronName The squadron name. -- @return #AI_A2A_DISPATCHER.Squadron DefenderSquadron function AI_A2A_DISPATCHER:CanCAP( SquadronName ) - self:F({SquadronName = SquadronName}) + self:F( { SquadronName = SquadronName } ) self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} self.DefenderSquadrons[SquadronName].Cap = self.DefenderSquadrons[SquadronName].Cap or {} @@ -127829,7 +140283,7 @@ do -- AI_A2A_DISPATCHER if DefenderSquadron.Captured == false then -- We can only spawn new CAP if the base has not been captured. - if ( not DefenderSquadron.ResourceCount ) or ( DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount > 0 ) then -- And, if there are sufficient resources. + if (not DefenderSquadron.ResourceCount) or (DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount > 0) then -- And, if there are sufficient resources. local Cap = DefenderSquadron.Cap if Cap then @@ -127847,7 +140301,6 @@ do -- AI_A2A_DISPATCHER return nil end - --- Set race track pattern as default when any squadron is performing CAP. -- @param #AI_A2A_DISPATCHER self -- @param #number LeglengthMin Min length of the race track leg in meters. Default 10,000 m. @@ -127858,16 +140311,16 @@ do -- AI_A2A_DISPATCHER -- @param #number DurationMax (Optional) Max duration in seconds before switching the orbit position. Default is keep same orbit until RTB or engage. -- @param #table CapCoordinates Table of coordinates of first race track point. Second point is determined by leg length and heading. -- @return #AI_A2A_DISPATCHER self - function AI_A2A_DISPATCHER:SetDefaultCapRacetrack(LeglengthMin, LeglengthMax, HeadingMin, HeadingMax, DurationMin, DurationMax, CapCoordinates) + function AI_A2A_DISPATCHER:SetDefaultCapRacetrack( LeglengthMin, LeglengthMax, HeadingMin, HeadingMax, DurationMin, DurationMax, CapCoordinates ) - self.DefenderDefault.Racetrack=true - self.DefenderDefault.RacetrackLengthMin=LeglengthMin - self.DefenderDefault.RacetrackLengthMax=LeglengthMax - self.DefenderDefault.RacetrackHeadingMin=HeadingMin - self.DefenderDefault.RacetrackHeadingMax=HeadingMax - self.DefenderDefault.RacetrackDurationMin=DurationMin - self.DefenderDefault.RacetrackDurationMax=DurationMax - self.DefenderDefault.RacetrackCoordinates=CapCoordinates + self.DefenderDefault.Racetrack = true + self.DefenderDefault.RacetrackLengthMin = LeglengthMin + self.DefenderDefault.RacetrackLengthMax = LeglengthMax + self.DefenderDefault.RacetrackHeadingMin = HeadingMin + self.DefenderDefault.RacetrackHeadingMax = HeadingMax + self.DefenderDefault.RacetrackDurationMin = DurationMin + self.DefenderDefault.RacetrackDurationMax = DurationMax + self.DefenderDefault.RacetrackCoordinates = CapCoordinates return self end @@ -127883,31 +140336,30 @@ do -- AI_A2A_DISPATCHER -- @param #number DurationMax (Optional) Max duration in seconds before switching the orbit position. Default is keep same orbit until RTB or engage. -- @param #table CapCoordinates Table of coordinates of first race track point. Second point is determined by leg length and heading. -- @return #AI_A2A_DISPATCHER self - function AI_A2A_DISPATCHER:SetSquadronCapRacetrack(SquadronName, LeglengthMin, LeglengthMax, HeadingMin, HeadingMax, DurationMin, DurationMax, CapCoordinates) + function AI_A2A_DISPATCHER:SetSquadronCapRacetrack( SquadronName, LeglengthMin, LeglengthMax, HeadingMin, HeadingMax, DurationMin, DurationMax, CapCoordinates ) local DefenderSquadron = self:GetSquadron( SquadronName ) if DefenderSquadron then - DefenderSquadron.Racetrack=true - DefenderSquadron.RacetrackLengthMin=LeglengthMin - DefenderSquadron.RacetrackLengthMax=LeglengthMax - DefenderSquadron.RacetrackHeadingMin=HeadingMin - DefenderSquadron.RacetrackHeadingMax=HeadingMax - DefenderSquadron.RacetrackDurationMin=DurationMin - DefenderSquadron.RacetrackDurationMax=DurationMax - DefenderSquadron.RacetrackCoordinates=CapCoordinates + DefenderSquadron.Racetrack = true + DefenderSquadron.RacetrackLengthMin = LeglengthMin + DefenderSquadron.RacetrackLengthMax = LeglengthMax + DefenderSquadron.RacetrackHeadingMin = HeadingMin + DefenderSquadron.RacetrackHeadingMax = HeadingMax + DefenderSquadron.RacetrackDurationMin = DurationMin + DefenderSquadron.RacetrackDurationMax = DurationMax + DefenderSquadron.RacetrackCoordinates = CapCoordinates end return self end - --- Check if squadron can do GCI. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The squadron name. -- @return #table DefenderSquadron function AI_A2A_DISPATCHER:CanGCI( SquadronName ) - self:F({SquadronName = SquadronName}) + self:F( { SquadronName = SquadronName } ) self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} self.DefenderSquadrons[SquadronName].Gci = self.DefenderSquadrons[SquadronName].Gci or {} @@ -127916,7 +140368,7 @@ do -- AI_A2A_DISPATCHER if DefenderSquadron.Captured == false then -- We can only spawn new CAP if the base has not been captured. - if ( not DefenderSquadron.ResourceCount ) or ( DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount > 0 ) then -- And, if there are sufficient resources. + if (not DefenderSquadron.ResourceCount) or (DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount > 0) then -- And, if there are sufficient resources. local Gci = DefenderSquadron.Gci if Gci then return DefenderSquadron @@ -127934,14 +140386,14 @@ do -- AI_A2A_DISPATCHER -- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. -- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. -- @param DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO". + -- @return #AI_A2A_DISPATCHER -- @usage -- - -- -- GCI Squadron execution. - -- A2ADispatcher:SetSquadronGci2( "Mozdok", 900, 1200, 5000, 5000, "BARO" ) - -- A2ADispatcher:SetSquadronGci2( "Novo", 900, 2100, 30, 30, "RADIO" ) - -- A2ADispatcher:SetSquadronGci2( "Maykop", 900, 1200, 100, 300, "RADIO" ) + -- -- GCI Squadron execution. + -- A2ADispatcher:SetSquadronGci2( "Mozdok", 900, 1200, 5000, 5000, "BARO" ) + -- A2ADispatcher:SetSquadronGci2( "Novo", 900, 2100, 30, 30, "RADIO" ) + -- A2ADispatcher:SetSquadronGci2( "Maykop", 900, 1200, 100, 300, "RADIO" ) -- - -- @return #AI_A2A_DISPATCHER function AI_A2A_DISPATCHER:SetSquadronGci2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} @@ -127963,14 +140415,14 @@ do -- AI_A2A_DISPATCHER -- @param #string SquadronName The squadron name. -- @param #number EngageMinSpeed The minimum speed [km/h] at which the GCI can be executed. -- @param #number EngageMaxSpeed The maximum speed [km/h] at which the GCI can be executed. + -- @return #AI_A2A_DISPATCHER -- @usage -- - -- -- GCI Squadron execution. - -- A2ADispatcher:SetSquadronGci( "Mozdok", 900, 1200 ) - -- A2ADispatcher:SetSquadronGci( "Novo", 900, 2100 ) - -- A2ADispatcher:SetSquadronGci( "Maykop", 900, 1200 ) + -- -- GCI Squadron execution. + -- A2ADispatcher:SetSquadronGci( "Mozdok", 900, 1200 ) + -- A2ADispatcher:SetSquadronGci( "Novo", 900, 2100 ) + -- A2ADispatcher:SetSquadronGci( "Maykop", 900, 1200 ) -- - -- @return #AI_A2A_DISPATCHER function AI_A2A_DISPATCHER:SetSquadronGci( SquadronName, EngageMinSpeed, EngageMaxSpeed ) self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} @@ -127986,7 +140438,8 @@ do -- AI_A2A_DISPATCHER --- Defines the default amount of extra planes that will take-off as part of the defense system. -- @param #AI_A2A_DISPATCHER self - -- @param #number Overhead The %-tage of Units that dispatching command will allocate to intercept in surplus of detected amount of units. + -- @param #number Overhead The % of Units that dispatching command will allocate to intercept in surplus of detected amount of units. + -- @return #AI_A2A_DISPATCHER -- The default overhead is 1, so equal balance. The @{#AI_A2A_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength, -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance A2A missiles payload, may still be less effective than a F-15C with short missiles... -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes. @@ -128013,7 +140466,6 @@ do -- AI_A2A_DISPATCHER -- -- A2ADispatcher:SetDefaultOverhead( 1.5 ) -- - -- @return #AI_A2A_DISPATCHER function AI_A2A_DISPATCHER:SetDefaultOverhead( Overhead ) self.DefenderDefault.Overhead = Overhead @@ -128021,11 +140473,11 @@ do -- AI_A2A_DISPATCHER return self end - --- Defines the amount of extra planes that will take-off as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. - -- @param #number Overhead The %-tage of Units that dispatching command will allocate to intercept in surplus of detected amount of units. + -- @param #number Overhead The % of Units that dispatching command will allocate to intercept in surplus of detected amount of units. + -- @return #AI_A2A_DISPATCHER self -- The default overhead is 1, so equal balance. The @{#AI_A2A_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength, -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance A2A missiles payload, may still be less effective than a F-15C with short missiles... -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes. @@ -128052,7 +140504,6 @@ do -- AI_A2A_DISPATCHER -- -- A2ADispatcher:SetSquadronOverhead( "SquadronName", 1.5 ) -- - -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetSquadronOverhead( SquadronName, Overhead ) local DefenderSquadron = self:GetSquadron( SquadronName ) @@ -128061,11 +140512,11 @@ do -- AI_A2A_DISPATCHER return self end - --- Sets the default grouping of new airplanes spawned. -- Grouping will trigger how new airplanes will be grouped if more than one airplane is spawned for defense. -- @param #AI_A2A_DISPATCHER self -- @param #number Grouping The level of grouping that will be applied of the CAP or GCI defenders. + -- @return #AI_A2A_DISPATCHER self -- @usage: -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) @@ -128073,8 +140524,6 @@ do -- AI_A2A_DISPATCHER -- -- Set a grouping by default per 2 airplanes. -- A2ADispatcher:SetDefaultGrouping( 2 ) -- - -- - -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetDefaultGrouping( Grouping ) self.DefenderDefault.Grouping = Grouping @@ -128082,12 +140531,12 @@ do -- AI_A2A_DISPATCHER return self end - --- Sets the grouping of new airplanes spawned. -- Grouping will trigger how new airplanes will be grouped if more than one airplane is spawned for defense. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. -- @param #number Grouping The level of grouping that will be applied of the CAP or GCI defenders. + -- @return #AI_A2A_DISPATCHER self -- @usage: -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) @@ -128095,8 +140544,6 @@ do -- AI_A2A_DISPATCHER -- -- Set a grouping per 2 airplanes. -- A2ADispatcher:SetSquadronGrouping( "SquadronName", 2 ) -- - -- - -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetSquadronGrouping( SquadronName, Grouping ) local DefenderSquadron = self:GetSquadron( SquadronName ) @@ -128105,10 +140552,10 @@ do -- AI_A2A_DISPATCHER return self end - --- Defines the default method at which new flights will spawn and take-off as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @param #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. + -- @return #AI_A2A_DISPATCHER self -- @usage: -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) @@ -128125,9 +140572,6 @@ do -- AI_A2A_DISPATCHER -- -- Let new flights by default take-off from the airbase cold. -- A2ADispatcher:SetDefaultTakeoff( AI_A2A_Dispatcher.Takeoff.Cold ) -- - -- - -- @return #AI_A2A_DISPATCHER self - -- function AI_A2A_DISPATCHER:SetDefaultTakeoff( Takeoff ) self.DefenderDefault.Takeoff = Takeoff @@ -128139,6 +140583,7 @@ do -- AI_A2A_DISPATCHER -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. -- @param #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. + -- @return #AI_A2A_DISPATCHER self -- @usage: -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) @@ -128155,9 +140600,6 @@ do -- AI_A2A_DISPATCHER -- -- Let new flights take-off from the airbase cold. -- A2ADispatcher:SetSquadronTakeoff( "SquadronName", AI_A2A_Dispatcher.Takeoff.Cold ) -- - -- - -- @return #AI_A2A_DISPATCHER self - -- function AI_A2A_DISPATCHER:SetSquadronTakeoff( SquadronName, Takeoff ) local DefenderSquadron = self:GetSquadron( SquadronName ) @@ -128166,7 +140608,6 @@ do -- AI_A2A_DISPATCHER return self end - --- Gets the default method at which new flights will spawn and take-off as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @return #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. @@ -128180,7 +140621,7 @@ do -- AI_A2A_DISPATCHER -- ... -- end -- - function AI_A2A_DISPATCHER:GetDefaultTakeoff( ) + function AI_A2A_DISPATCHER:GetDefaultTakeoff() return self.DefenderDefault.Takeoff end @@ -128205,9 +140646,9 @@ do -- AI_A2A_DISPATCHER return DefenderSquadron.Takeoff or self.DefenderDefault.Takeoff end - --- Sets flights to default take-off in the air, as part of the defense system. -- @param #AI_A2A_DISPATCHER self + -- @return #AI_A2A_DISPATCHER self -- @usage: -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) @@ -128215,8 +140656,6 @@ do -- AI_A2A_DISPATCHER -- -- Let new flights by default take-off in the air. -- A2ADispatcher:SetDefaultTakeoffInAir() -- - -- @return #AI_A2A_DISPATCHER self - -- function AI_A2A_DISPATCHER:SetDefaultTakeoffInAir() self:SetDefaultTakeoff( AI_A2A_DISPATCHER.Takeoff.Air ) @@ -128224,11 +140663,18 @@ do -- AI_A2A_DISPATCHER return self end + --- Set flashing player messages on or off + -- @param #AI_A2A_DISPATCHER self + -- @param #boolean onoff Set messages on (true) or off (false) + function AI_A2A_DISPATCHER:SetSendMessages( onoff ) + self.SetSendPlayerMessages = onoff + end --- Sets flights to take-off in the air, as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. -- @param #number TakeoffAltitude (optional) The altitude in meters above the ground. If not given, the default takeoff altitude will be used. + -- @return #AI_A2A_DISPATCHER self -- @usage: -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) @@ -128236,8 +140682,6 @@ do -- AI_A2A_DISPATCHER -- -- Let new flights take-off in the air. -- A2ADispatcher:SetSquadronTakeoffInAir( "SquadronName" ) -- - -- @return #AI_A2A_DISPATCHER self - -- function AI_A2A_DISPATCHER:SetSquadronTakeoffInAir( SquadronName, TakeoffAltitude ) self:SetSquadronTakeoff( SquadronName, AI_A2A_DISPATCHER.Takeoff.Air ) @@ -128249,9 +140693,9 @@ do -- AI_A2A_DISPATCHER return self end - --- Sets flights by default to take-off from the runway, as part of the defense system. -- @param #AI_A2A_DISPATCHER self + -- @return #AI_A2A_DISPATCHER self -- @usage: -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) @@ -128259,8 +140703,6 @@ do -- AI_A2A_DISPATCHER -- -- Let new flights by default take-off from the runway. -- A2ADispatcher:SetDefaultTakeoffFromRunway() -- - -- @return #AI_A2A_DISPATCHER self - -- function AI_A2A_DISPATCHER:SetDefaultTakeoffFromRunway() self:SetDefaultTakeoff( AI_A2A_DISPATCHER.Takeoff.Runway ) @@ -128268,10 +140710,10 @@ do -- AI_A2A_DISPATCHER return self end - --- Sets flights to take-off from the runway, as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. + -- @return #AI_A2A_DISPATCHER self -- @usage: -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) @@ -128279,8 +140721,6 @@ do -- AI_A2A_DISPATCHER -- -- Let new flights take-off from the runway. -- A2ADispatcher:SetSquadronTakeoffFromRunway( "SquadronName" ) -- - -- @return #AI_A2A_DISPATCHER self - -- function AI_A2A_DISPATCHER:SetSquadronTakeoffFromRunway( SquadronName ) self:SetSquadronTakeoff( SquadronName, AI_A2A_DISPATCHER.Takeoff.Runway ) @@ -128288,9 +140728,9 @@ do -- AI_A2A_DISPATCHER return self end - --- Sets flights by default to take-off from the airbase at a hot location, as part of the defense system. -- @param #AI_A2A_DISPATCHER self + -- @return #AI_A2A_DISPATCHER self -- @usage: -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) @@ -128298,8 +140738,6 @@ do -- AI_A2A_DISPATCHER -- -- Let new flights by default take-off at a hot parking spot. -- A2ADispatcher:SetDefaultTakeoffFromParkingHot() -- - -- @return #AI_A2A_DISPATCHER self - -- function AI_A2A_DISPATCHER:SetDefaultTakeoffFromParkingHot() self:SetDefaultTakeoff( AI_A2A_DISPATCHER.Takeoff.Hot ) @@ -128310,6 +140748,7 @@ do -- AI_A2A_DISPATCHER --- Sets flights to take-off from the airbase at a hot location, as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. + -- @return #AI_A2A_DISPATCHER self -- @usage: -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) @@ -128317,8 +140756,6 @@ do -- AI_A2A_DISPATCHER -- -- Let new flights take-off in the air. -- A2ADispatcher:SetSquadronTakeoffFromParkingHot( "SquadronName" ) -- - -- @return #AI_A2A_DISPATCHER self - -- function AI_A2A_DISPATCHER:SetSquadronTakeoffFromParkingHot( SquadronName ) self:SetSquadronTakeoff( SquadronName, AI_A2A_DISPATCHER.Takeoff.Hot ) @@ -128326,9 +140763,9 @@ do -- AI_A2A_DISPATCHER return self end - --- Sets flights to by default take-off from the airbase at a cold location, as part of the defense system. -- @param #AI_A2A_DISPATCHER self + -- @return #AI_A2A_DISPATCHER self -- @usage: -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) @@ -128336,8 +140773,6 @@ do -- AI_A2A_DISPATCHER -- -- Let new flights take-off from a cold parking spot. -- A2ADispatcher:SetDefaultTakeoffFromParkingCold() -- - -- @return #AI_A2A_DISPATCHER self - -- function AI_A2A_DISPATCHER:SetDefaultTakeoffFromParkingCold() self:SetDefaultTakeoff( AI_A2A_DISPATCHER.Takeoff.Cold ) @@ -128345,10 +140780,10 @@ do -- AI_A2A_DISPATCHER return self end - --- Sets flights to take-off from the airbase at a cold location, as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. + -- @return #AI_A2A_DISPATCHER self -- @usage: -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) @@ -128356,8 +140791,6 @@ do -- AI_A2A_DISPATCHER -- -- Let new flights take-off from a cold parking spot. -- A2ADispatcher:SetSquadronTakeoffFromParkingCold( "SquadronName" ) -- - -- @return #AI_A2A_DISPATCHER self - -- function AI_A2A_DISPATCHER:SetSquadronTakeoffFromParkingCold( SquadronName ) self:SetSquadronTakeoff( SquadronName, AI_A2A_DISPATCHER.Takeoff.Cold ) @@ -128365,10 +140798,10 @@ do -- AI_A2A_DISPATCHER return self end - --- Defines the default altitude where airplanes will spawn in the air and take-off as part of the defense system, when the take-off in the air method has been selected. -- @param #AI_A2A_DISPATCHER self -- @param #number TakeoffAltitude The altitude in meters above the ground. + -- @return #AI_A2A_DISPATCHER self -- @usage: -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) @@ -128376,8 +140809,6 @@ do -- AI_A2A_DISPATCHER -- -- Set the default takeoff altitude when taking off in the air. -- A2ADispatcher:SetDefaultTakeoffInAirAltitude( 2000 ) -- This makes planes start at 2000 meters above the ground. -- - -- @return #AI_A2A_DISPATCHER self - -- function AI_A2A_DISPATCHER:SetDefaultTakeoffInAirAltitude( TakeoffAltitude ) self.DefenderDefault.TakeoffAltitude = TakeoffAltitude @@ -128389,6 +140820,7 @@ do -- AI_A2A_DISPATCHER -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. -- @param #number TakeoffAltitude The altitude in meters above the ground. + -- @return #AI_A2A_DISPATCHER self -- @usage: -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) @@ -128396,8 +140828,6 @@ do -- AI_A2A_DISPATCHER -- -- Set the default takeoff altitude when taking off in the air. -- A2ADispatcher:SetSquadronTakeoffInAirAltitude( "SquadronName", 2000 ) -- This makes planes start at 2000 meters above the ground. -- - -- @return #AI_A2A_DISPATCHER self - -- function AI_A2A_DISPATCHER:SetSquadronTakeoffInAirAltitude( SquadronName, TakeoffAltitude ) local DefenderSquadron = self:GetSquadron( SquadronName ) @@ -128406,10 +140836,10 @@ do -- AI_A2A_DISPATCHER return self end - --- Defines the default method at which flights will land and despawn as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @param #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown + -- @return #AI_A2A_DISPATCHER self -- @usage: -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) @@ -128423,7 +140853,6 @@ do -- AI_A2A_DISPATCHER -- -- Let new flights by default despawn after landing and parking, and after engine shutdown. -- A2ADispatcher:SetDefaultLanding( AI_A2A_Dispatcher.Landing.AtEngineShutdown ) -- - -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetDefaultLanding( Landing ) self.DefenderDefault.Landing = Landing @@ -128431,11 +140860,11 @@ do -- AI_A2A_DISPATCHER return self end - --- Defines the method at which flights will land and despawn as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. -- @param #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown + -- @return #AI_A2A_DISPATCHER self -- @usage: -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) @@ -128449,7 +140878,6 @@ do -- AI_A2A_DISPATCHER -- -- Let new flights despawn after landing and parking, and after engine shutdown. -- A2ADispatcher:SetSquadronLanding( "SquadronName", AI_A2A_Dispatcher.Landing.AtEngineShutdown ) -- - -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetSquadronLanding( SquadronName, Landing ) local DefenderSquadron = self:GetSquadron( SquadronName ) @@ -128458,7 +140886,6 @@ do -- AI_A2A_DISPATCHER return self end - --- Gets the default method at which flights will land and despawn as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @return #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown @@ -128477,7 +140904,6 @@ do -- AI_A2A_DISPATCHER return self.DefenderDefault.Landing end - --- Gets the method at which flights will land and despawn as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. @@ -128498,9 +140924,9 @@ do -- AI_A2A_DISPATCHER return DefenderSquadron.Landing or self.DefenderDefault.Landing end - --- Sets flights by default to land and despawn near the airbase in the air, as part of the defense system. -- @param #AI_A2A_DISPATCHER self + -- @return #AI_A2A_DISPATCHER self -- @usage: -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) @@ -128508,7 +140934,6 @@ do -- AI_A2A_DISPATCHER -- -- Let flights by default to land near the airbase and despawn. -- A2ADispatcher:SetDefaultLandingNearAirbase() -- - -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetDefaultLandingNearAirbase() self:SetDefaultLanding( AI_A2A_DISPATCHER.Landing.NearAirbase ) @@ -128516,10 +140941,10 @@ do -- AI_A2A_DISPATCHER return self end - --- Sets flights to land and despawn near the airbase in the air, as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. + -- @return #AI_A2A_DISPATCHER self -- @usage: -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) @@ -128527,7 +140952,6 @@ do -- AI_A2A_DISPATCHER -- -- Let flights to land near the airbase and despawn. -- A2ADispatcher:SetSquadronLandingNearAirbase( "SquadronName" ) -- - -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetSquadronLandingNearAirbase( SquadronName ) self:SetSquadronLanding( SquadronName, AI_A2A_DISPATCHER.Landing.NearAirbase ) @@ -128535,9 +140959,9 @@ do -- AI_A2A_DISPATCHER return self end - --- Sets flights by default to land and despawn at the runway, as part of the defense system. -- @param #AI_A2A_DISPATCHER self + -- @return #AI_A2A_DISPATCHER self -- @usage: -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) @@ -128545,7 +140969,6 @@ do -- AI_A2A_DISPATCHER -- -- Let flights by default land at the runway and despawn. -- A2ADispatcher:SetDefaultLandingAtRunway() -- - -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetDefaultLandingAtRunway() self:SetDefaultLanding( AI_A2A_DISPATCHER.Landing.AtRunway ) @@ -128553,10 +140976,10 @@ do -- AI_A2A_DISPATCHER return self end - --- Sets flights to land and despawn at the runway, as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. + -- @return #AI_A2A_DISPATCHER self -- @usage: -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) @@ -128564,7 +140987,6 @@ do -- AI_A2A_DISPATCHER -- -- Let flights land at the runway and despawn. -- A2ADispatcher:SetSquadronLandingAtRunway( "SquadronName" ) -- - -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetSquadronLandingAtRunway( SquadronName ) self:SetSquadronLanding( SquadronName, AI_A2A_DISPATCHER.Landing.AtRunway ) @@ -128572,9 +140994,9 @@ do -- AI_A2A_DISPATCHER return self end - --- Sets flights by default to land and despawn at engine shutdown, as part of the defense system. -- @param #AI_A2A_DISPATCHER self + -- @return #AI_A2A_DISPATCHER self -- @usage: -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) @@ -128582,7 +141004,6 @@ do -- AI_A2A_DISPATCHER -- -- Let flights by default land and despawn at engine shutdown. -- A2ADispatcher:SetDefaultLandingAtEngineShutdown() -- - -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetDefaultLandingAtEngineShutdown() self:SetDefaultLanding( AI_A2A_DISPATCHER.Landing.AtEngineShutdown ) @@ -128590,10 +141011,10 @@ do -- AI_A2A_DISPATCHER return self end - --- Sets flights to land and despawn at engine shutdown, as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. + -- @return #AI_A2A_DISPATCHER self -- @usage: -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) @@ -128601,7 +141022,6 @@ do -- AI_A2A_DISPATCHER -- -- Let flights land and despawn at engine shutdown. -- A2ADispatcher:SetSquadronLandingAtEngineShutdown( "SquadronName" ) -- - -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetSquadronLandingAtEngineShutdown( SquadronName ) self:SetSquadronLanding( SquadronName, AI_A2A_DISPATCHER.Landing.AtEngineShutdown ) @@ -128609,17 +141029,17 @@ do -- AI_A2A_DISPATCHER return self end - --- Set the default fuel treshold when defenders will RTB or Refuel in the air. - -- The fuel treshold is by default set to 15%, which means that an airplane will stay in the air until 15% of its fuel has been consumed. + --- Set the default fuel threshold when defenders will RTB or Refuel in the air. + -- The fuel threshold is by default set to 15%, which means that an airplane will stay in the air until 15% of its fuel has been consumed. -- @param #AI_A2A_DISPATCHER self - -- @param #number FuelThreshold A decimal number between 0 and 1, that expresses the %-tage of the treshold of fuel remaining in the tank when the plane will go RTB or Refuel. + -- @param #number FuelThreshold A decimal number between 0 and 1, that expresses the % of the threshold of fuel remaining in the tank when the plane will go RTB or Refuel. -- @return #AI_A2A_DISPATCHER self -- @usage -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) -- - -- -- Now Setup the default fuel treshold. + -- -- Now Setup the default fuel threshold. -- A2ADispatcher:SetDefaultFuelThreshold( 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. -- function AI_A2A_DISPATCHER:SetDefaultFuelThreshold( FuelThreshold ) @@ -128629,19 +141049,18 @@ do -- AI_A2A_DISPATCHER return self end - - --- Set the fuel treshold for the squadron when defenders will RTB or Refuel in the air. - -- The fuel treshold is by default set to 15%, which means that an airplane will stay in the air until 15% of its fuel has been consumed. + --- Set the fuel threshold for the squadron when defenders will RTB or Refuel in the air. + -- The fuel threshold is by default set to 15%, which means that an airplane will stay in the air until 15% of its fuel has been consumed. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. - -- @param #number FuelThreshold A decimal number between 0 and 1, that expresses the %-tage of the treshold of fuel remaining in the tank when the plane will go RTB or Refuel. + -- @param #number FuelThreshold A decimal number between 0 and 1, that expresses the % of the threshold of fuel remaining in the tank when the plane will go RTB or Refuel. -- @return #AI_A2A_DISPATCHER self -- @usage -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) -- - -- -- Now Setup the default fuel treshold. + -- -- Now Setup the default fuel threshold. -- A2ADispatcher:SetSquadronFuelThreshold( "SquadronName", 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. -- function AI_A2A_DISPATCHER:SetSquadronFuelThreshold( SquadronName, FuelThreshold ) @@ -128661,11 +141080,12 @@ do -- AI_A2A_DISPATCHER -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) -- - -- -- Now Setup the default fuel treshold. + -- -- Now Setup the default fuel threshold. -- A2ADispatcher:SetDefaultFuelThreshold( 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. -- -- -- Now Setup the default tanker. -- A2ADispatcher:SetDefaultTanker( "Tanker" ) -- The group name of the tanker is "Tanker" in the Mission Editor. + -- function AI_A2A_DISPATCHER:SetDefaultTanker( TankerName ) self.DefenderDefault.TankerName = TankerName @@ -128673,7 +141093,6 @@ do -- AI_A2A_DISPATCHER return self end - --- Set the squadron tanker where defenders will Refuel in the air. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. @@ -128684,11 +141103,12 @@ do -- AI_A2A_DISPATCHER -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) -- - -- -- Now Setup the squadron fuel treshold. + -- -- Now Setup the squadron fuel threshold. -- A2ADispatcher:SetSquadronFuelThreshold( "SquadronName", 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. -- -- -- Now Setup the squadron tanker. -- A2ADispatcher:SetSquadronTanker( "SquadronName", "Tanker" ) -- The group name of the tanker is "Tanker" in the Mission Editor. + -- function AI_A2A_DISPATCHER:SetSquadronTanker( SquadronName, TankerName ) local DefenderSquadron = self:GetSquadron( SquadronName ) @@ -128697,7 +141117,6 @@ do -- AI_A2A_DISPATCHER return self end - --- Set the squadron language. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. @@ -128725,7 +141144,6 @@ do -- AI_A2A_DISPATCHER return self end - --- Set the frequency of communication and the mode of communication for voice overs. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. @@ -128760,7 +141178,7 @@ do -- AI_A2A_DISPATCHER function AI_A2A_DISPATCHER:AddDefenderToSquadron( Squadron, Defender, Size ) self.Defenders = self.Defenders or {} local DefenderName = Defender:GetName() - self.Defenders[ DefenderName ] = Squadron + self.Defenders[DefenderName] = Squadron if Squadron.ResourceCount then Squadron.ResourceCount = Squadron.ResourceCount - Size end @@ -128777,7 +141195,7 @@ do -- AI_A2A_DISPATCHER if Squadron.ResourceCount then Squadron.ResourceCount = Squadron.ResourceCount + Defender:GetSize() end - self.Defenders[ DefenderName ] = nil + self.Defenders[DefenderName] = nil self:F( { DefenderName = DefenderName, SquadronResourceCount = Squadron.ResourceCount } ) end @@ -128790,13 +141208,12 @@ do -- AI_A2A_DISPATCHER if Defender ~= nil then local DefenderName = Defender:GetName() self:F( { DefenderName = DefenderName } ) - return self.Defenders[ DefenderName ] + return self.Defenders[DefenderName] else - return nil + return nil end end - --- Creates an SWEEP task when there are targets for it. -- @param #AI_A2A_DISPATCHER self -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem @@ -128807,7 +141224,6 @@ do -- AI_A2A_DISPATCHER local DetectedSet = DetectedItem.Set local DetectedZone = DetectedItem.Zone - if DetectedItem.IsDetected == false then -- Here we're doing something advanced... We're copying the DetectedSet. @@ -128837,9 +141253,9 @@ do -- AI_A2A_DISPATCHER if AIGroup and AIGroup:IsAlive() then -- Check if the CAP is patrolling or engaging. If not, this is not a valid CAP, even if it is alive! -- The CAP could be damaged, lost control, or out of fuel! - --env.info("FF fsm state "..tostring(DefenderTask.Fsm:GetState())) + -- env.info("FF fsm state "..tostring(DefenderTask.Fsm:GetState())) if DefenderTask.Fsm:Is( "Patrolling" ) or DefenderTask.Fsm:Is( "Engaging" ) or DefenderTask.Fsm:Is( "Refuelling" ) or DefenderTask.Fsm:Is( "Started" ) then - --env.info("FF capcount "..CapCount) + -- env.info("FF capcount "..CapCount) CapCount = CapCount + 1 end end @@ -128851,24 +141267,23 @@ do -- AI_A2A_DISPATCHER return CapCount end - --- Count number of engaging defender groups. -- @param #AI_A2A_DISPATCHER self -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detection object. -- @return #number Number of defender groups engaging. function AI_A2A_DISPATCHER:CountDefendersEngaged( AttackerDetection ) - -- First, count the active AIGroups Units, targetting the DetectedSet + -- First, count the active AIGroups Units, targeting the DetectedSet local DefenderCount = 0 local DetectedSet = AttackerDetection.Set - --DetectedSet:Flush() + -- DetectedSet:Flush() local DefenderTasks = self:GetDefenderTasks() for DefenderGroup, DefenderTask in pairs( DefenderTasks ) do local Defender = DefenderGroup -- Wrapper.Group#GROUP - local DefenderTaskTarget = DefenderTask.Target --Functional.Detection#DETECTION_BASE.DetectedItem + local DefenderTaskTarget = DefenderTask.Target -- Functional.Detection#DETECTION_BASE.DetectedItem local DefenderSquadronName = DefenderTask.SquadronName if DefenderTaskTarget and DefenderTaskTarget.Index == AttackerDetection.Index then @@ -128907,7 +141322,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. @@ -128935,7 +141363,6 @@ do -- AI_A2A_DISPATCHER return Friendlies end - --- Activate resource. -- @param #AI_A2A_DISPATCHER self -- @param #AI_A2A_DISPATCHER.Squadron DefenderSquadron The defender squadron. @@ -128950,36 +141377,36 @@ do -- AI_A2A_DISPATCHER local DefenderGrouping = DefenderSquadron.Grouping or self.DefenderDefault.Grouping - DefenderGrouping = ( DefenderGrouping < DefendersNeeded ) and DefenderGrouping or DefendersNeeded + DefenderGrouping = (DefenderGrouping < DefendersNeeded) and DefenderGrouping or DefendersNeeded - --env.info(string.format("FF resource activate: Squadron=%s grouping=%d needed=%d visible=%s", SquadronName, DefenderGrouping, DefendersNeeded, tostring(self:IsSquadronVisible( SquadronName )))) + -- env.info(string.format("FF resource activate: Squadron=%s grouping=%d needed=%d visible=%s", SquadronName, DefenderGrouping, DefendersNeeded, tostring(self:IsSquadronVisible( SquadronName )))) if self:IsSquadronVisible( SquadronName ) then - local n=#self.uncontrolled[SquadronName] + local n = #self.uncontrolled[SquadronName] - if n>0 then + if n > 0 then -- Random number 1,...n - local id=math.random(n) + local id = math.random( n ) -- Pick a random defender group. - local Defender=self.uncontrolled[SquadronName][id].group --Wrapper.Group#GROUP + local Defender = self.uncontrolled[SquadronName][id].group -- Wrapper.Group#GROUP -- Start uncontrolled group. Defender:StartUncontrolled() -- Get grouping. - DefenderGrouping=self.uncontrolled[SquadronName][id].grouping + DefenderGrouping = self.uncontrolled[SquadronName][id].grouping -- Add defender to squadron. self:AddDefenderToSquadron( DefenderSquadron, Defender, DefenderGrouping ) -- Remove defender from uncontrolled table. - table.remove(self.uncontrolled[SquadronName], id) + table.remove( self.uncontrolled[SquadronName], id ) return Defender, DefenderGrouping else - return nil,0 + return nil, 0 end -- Here we CAP the new planes. @@ -129037,7 +141464,7 @@ do -- AI_A2A_DISPATCHER --- Squadron not visible --- ---------------------------- - local Spawn = DefenderSquadron.Spawn[ math.random( 1, #DefenderSquadron.Spawn ) ] -- Core.Spawn#SPAWN + local Spawn = DefenderSquadron.Spawn[math.random( 1, #DefenderSquadron.Spawn )] -- Core.Spawn#SPAWN if DefenderGrouping then Spawn:InitGrouping( DefenderGrouping ) @@ -129065,7 +141492,7 @@ do -- AI_A2A_DISPATCHER -- @param #string SquadronName Name of the squadron. function AI_A2A_DISPATCHER:onafterCAP( From, Event, To, SquadronName ) - self:F({SquadronName = SquadronName}) + self:F( { SquadronName = SquadronName } ) self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} self.DefenderSquadrons[SquadronName].Cap = self.DefenderSquadrons[SquadronName].Cap or {} @@ -129089,13 +141516,13 @@ do -- AI_A2A_DISPATCHER AI_A2A_Fsm:SetDisengageRadius( self.DisengageRadius ) AI_A2A_Fsm:SetTanker( DefenderSquadron.TankerName or self.DefenderDefault.TankerName ) if DefenderSquadron.Racetrack or self.DefenderDefault.Racetrack then - AI_A2A_Fsm:SetRaceTrackPattern(DefenderSquadron.RacetrackLengthMin or self.DefenderDefault.RacetrackLengthMin, - DefenderSquadron.RacetrackLengthMax or self.DefenderDefault.RacetrackLengthMax, - DefenderSquadron.RacetrackHeadingMin or self.DefenderDefault.RacetrackHeadingMin, - DefenderSquadron.RacetrackHeadingMax or self.DefenderDefault.RacetrackHeadingMax, - DefenderSquadron.RacetrackDurationMin or self.DefenderDefault.RacetrackDurationMin, - DefenderSquadron.RacetrackDurationMax or self.DefenderDefault.RacetrackDurationMax, - DefenderSquadron.RacetrackCoordinates or self.DefenderDefault.RacetrackCoordinates) + AI_A2A_Fsm:SetRaceTrackPattern( DefenderSquadron.RacetrackLengthMin or self.DefenderDefault.RacetrackLengthMin, + DefenderSquadron.RacetrackLengthMax or self.DefenderDefault.RacetrackLengthMax, + DefenderSquadron.RacetrackHeadingMin or self.DefenderDefault.RacetrackHeadingMin, + DefenderSquadron.RacetrackHeadingMax or self.DefenderDefault.RacetrackHeadingMax, + DefenderSquadron.RacetrackDurationMin or self.DefenderDefault.RacetrackDurationMin, + DefenderSquadron.RacetrackDurationMax or self.DefenderDefault.RacetrackDurationMax, + DefenderSquadron.RacetrackCoordinates or self.DefenderDefault.RacetrackCoordinates ) end AI_A2A_Fsm:Start() @@ -129104,15 +141531,17 @@ do -- AI_A2A_DISPATCHER function AI_A2A_Fsm:onafterTakeoff( DefenderGroup, From, Event, To ) -- Issue GetCallsign() returns nil, see https://github.com/FlightControl-Master/MOOSE/issues/1228 if DefenderGroup and DefenderGroup:IsAlive() then - self:F({"CAP Takeoff", DefenderGroup:GetName()}) - --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) + self:F( { "CAP Takeoff", DefenderGroup:GetName() } ) + -- self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = AI_A2A_Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) if Squadron then - Dispatcher:MessageToPlayers( Squadron, DefenderName .. " Wheels up.", DefenderGroup ) + if self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. " Wheels up.", DefenderGroup ) + end AI_A2A_Fsm:__Patrol( 2 ) -- Start Patrolling end end @@ -129120,14 +141549,14 @@ do -- AI_A2A_DISPATCHER function AI_A2A_Fsm:onafterPatrolRoute( DefenderGroup, From, Event, To ) if DefenderGroup and DefenderGroup:IsAlive() then - self:F({"CAP PatrolRoute", DefenderGroup:GetName()}) - self:GetParent(self).onafterPatrolRoute( self, DefenderGroup, From, Event, To ) + self:F( { "CAP PatrolRoute", DefenderGroup:GetName() } ) + self:GetParent( self ).onafterPatrolRoute( self, DefenderGroup, From, Event, To ) local DefenderName = DefenderGroup:GetCallsign() - local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER + local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - if Squadron then - Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", patrolling.", DefenderGroup ) + if Squadron and self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", patrolling.", DefenderGroup ) end Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) @@ -129136,14 +141565,14 @@ do -- AI_A2A_DISPATCHER function AI_A2A_Fsm:onafterRTB( DefenderGroup, From, Event, To ) if DefenderGroup and DefenderGroup:IsAlive() then - self:F({"CAP RTB", DefenderGroup:GetName()}) + self:F( { "CAP RTB", DefenderGroup:GetName() } ) - self:GetParent(self).onafterRTB( self, DefenderGroup, From, Event, To ) + self:GetParent( self ).onafterRTB( self, DefenderGroup, From, Event, To ) local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - if Squadron then + if Squadron and self.SetSendPlayerMessages then Dispatcher:MessageToPlayers( Squadron, DefenderName .. " returning to base.", DefenderGroup ) end Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) @@ -129153,8 +141582,8 @@ do -- AI_A2A_DISPATCHER --- @param #AI_A2A_DISPATCHER self function AI_A2A_Fsm:onafterHome( Defender, From, Event, To, Action ) if Defender and Defender:IsAlive() then - self:F({"CAP Home", Defender:GetName()}) - self:GetParent(self).onafterHome( self, Defender, From, Event, To ) + self:F( { "CAP Home", Defender:GetName() } ) + self:GetParent( self ).onafterHome( self, Defender, From, Event, To ) local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) @@ -129177,7 +141606,6 @@ do -- AI_A2A_DISPATCHER end - --- On after "ENGAGE" event. -- @param #AI_A2A_DISPATCHER self -- @param #string From From state. @@ -129186,7 +141614,7 @@ do -- AI_A2A_DISPATCHER -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. -- @param #table Defenders Defenders table. function AI_A2A_DISPATCHER:onafterENGAGE( From, Event, To, AttackerDetection, Defenders ) - self:F("ENGAGING Detection ID="..tostring(AttackerDetection.ID)) + self:F( "ENGAGING Detection ID=" .. tostring( AttackerDetection.ID ) ) if Defenders then @@ -129213,7 +141641,7 @@ do -- AI_A2A_DISPATCHER -- @param #table DefenderFriendlies Friendly defenders. function AI_A2A_DISPATCHER:onafterGCI( From, Event, To, AttackerDetection, DefendersMissing, DefenderFriendlies ) - self:F("GCI Detection ID="..tostring(AttackerDetection.ID)) + self:F( "GCI Detection ID=" .. tostring( AttackerDetection.ID ) ) self:F( { From, Event, To, AttackerDetection.Index, DefendersMissing, DefenderFriendlies } ) @@ -129242,7 +141670,7 @@ do -- AI_A2A_DISPATCHER local BreakLoop = false - while( DefenderCount > 0 and not BreakLoop ) do + while (DefenderCount > 0 and not BreakLoop) do self:F( { DefenderSquadrons = self.DefenderSquadrons } ) @@ -129288,7 +141716,7 @@ do -- AI_A2A_DISPATCHER local DefenderGrouping = DefenderSquadron.Grouping or self.DefenderDefault.Grouping local DefendersNeeded = math.ceil( DefenderCount * DefenderOverhead ) - self:F( { Overhead = DefenderOverhead, SquadronOverhead = DefenderSquadron.Overhead , DefaultOverhead = self.DefenderDefault.Overhead } ) + self:F( { Overhead = DefenderOverhead, SquadronOverhead = DefenderSquadron.Overhead, DefaultOverhead = self.DefenderDefault.Overhead } ) self:F( { Grouping = DefenderGrouping, SquadronGrouping = DefenderSquadron.Grouping, DefaultGrouping = self.DefenderDefault.Grouping } ) self:F( { DefendersCount = DefenderCount, DefendersNeeded = DefendersNeeded } ) @@ -129299,7 +141727,7 @@ do -- AI_A2A_DISPATCHER BreakLoop = true end - while ( DefendersNeeded > 0 ) do + while (DefendersNeeded > 0) do local DefenderGCI, DefenderGrouping = self:ResourceActivate( DefenderSquadron, DefendersNeeded ) @@ -129317,13 +141745,11 @@ do -- AI_A2A_DISPATCHER Fsm:SetDisengageRadius( self.DisengageRadius ) Fsm:Start() - self:SetDefenderTask( ClosestDefenderSquadronName, DefenderGCI, "GCI", Fsm, AttackerDetection ) - function Fsm:onafterTakeoff( DefenderGroup, From, Event, To ) - self:F({"GCI Birth", DefenderGroup:GetName()}) - --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) + self:F( { "GCI Birth", DefenderGroup:GetName() } ) + -- self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER @@ -129331,18 +141757,18 @@ do -- AI_A2A_DISPATCHER local DefenderTarget = Dispatcher:GetDefenderTaskTarget( DefenderGroup ) if DefenderTarget then - if Squadron.Language == "EN" then + if Squadron.Language == "EN" and self.SetSendPlayerMessages then Dispatcher:MessageToPlayers( Squadron, DefenderName .. " wheels up.", DefenderGroup ) - elseif Squadron.Language == "RU" then - Dispatcher:MessageToPlayers( Squadron, DefenderName .. " колеса вверх.", DefenderGroup ) + elseif Squadron.Language == "RU" and self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. " колёса вверх.", DefenderGroup ) end - --Fsm:__Engage( 2, DefenderTarget.Set ) -- Engage on the TargetSetUnit + -- Fsm:__Engage( 2, DefenderTarget.Set ) -- Engage on the TargetSetUnit Fsm:EngageRoute( DefenderTarget.Set ) -- Engage on the TargetSetUnit end end function Fsm:onafterEngageRoute( DefenderGroup, From, Event, To, AttackSetUnit ) - self:F({"GCI Route", DefenderGroup:GetName()}) + self:F( { "GCI Route", DefenderGroup:GetName() } ) local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER @@ -129352,11 +141778,11 @@ do -- AI_A2A_DISPATCHER local FirstUnit = AttackSetUnit:GetFirst() local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE - if Squadron.Language == "EN" then + if Squadron.Language == "EN" and self.SetSendPlayerMessages then Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", intercepting bogeys at " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup ) - elseif Squadron.Language == "RU" then - Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", перехват самолетов в " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup ) - elseif Squadron.Language == "DE" then + elseif Squadron.Language == "RU" and self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", перехватывая боги в " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup ) + elseif Squadron.Language == "DE" and self.SetSendPlayerMessages then Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", Eindringlinge abfangen bei" .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup ) end end @@ -129364,7 +141790,7 @@ do -- AI_A2A_DISPATCHER end function Fsm:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit ) - self:F({"GCI Engage", DefenderGroup:GetName()}) + self:F( { "GCI Engage", DefenderGroup:GetName() } ) local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER @@ -129374,28 +141800,28 @@ do -- AI_A2A_DISPATCHER local FirstUnit = AttackSetUnit:GetFirst() local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE - if Squadron.Language == "EN" then + if Squadron.Language == "EN" and self.SetSendPlayerMessages then Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", engaging bogeys at " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup ) - elseif Squadron.Language == "RU" then - Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", захватывающие самолеты в " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup ) + elseif Squadron.Language == "RU" and self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", задействуя боги в " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup ) end end self:GetParent( Fsm ).onafterEngage( self, DefenderGroup, From, Event, To, AttackSetUnit ) end function Fsm:onafterRTB( DefenderGroup, From, Event, To ) - self:F({"GCI RTB", DefenderGroup:GetName()}) - self:GetParent(self).onafterRTB( self, DefenderGroup, From, Event, To ) + self:F( { "GCI RTB", DefenderGroup:GetName() } ) + self:GetParent( self ).onafterRTB( self, DefenderGroup, From, Event, To ) local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) if Squadron then - if Squadron.Language == "EN" then + if Squadron.Language == "EN" and self.SetSendPlayerMessages then Dispatcher:MessageToPlayers( Squadron, DefenderName .. " returning to base.", DefenderGroup ) - elseif Squadron.Language == "RU" then - Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", возвращаясь на базу.", DefenderGroup ) + elseif Squadron.Language == "RU" and self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", возвращение на базу.", DefenderGroup ) end end Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) @@ -129403,8 +141829,8 @@ do -- AI_A2A_DISPATCHER --- @param #AI_A2A_DISPATCHER self function Fsm:onafterLostControl( Defender, From, Event, To ) - self:F({"GCI LostControl", Defender:GetName()}) - self:GetParent(self).onafterHome( self, Defender, From, Event, To ) + self:F( { "GCI LostControl", Defender:GetName() } ) + self:GetParent( self ).onafterHome( self, Defender, From, Event, To ) local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) @@ -129416,17 +141842,17 @@ do -- AI_A2A_DISPATCHER --- @param #AI_A2A_DISPATCHER self function Fsm:onafterHome( DefenderGroup, From, Event, To, Action ) - self:F({"GCI Home", DefenderGroup:GetName()}) - self:GetParent(self).onafterHome( self, DefenderGroup, From, Event, To ) + self:F( { "GCI Home", DefenderGroup:GetName() } ) + self:GetParent( self ).onafterHome( self, DefenderGroup, From, Event, To ) local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - if Squadron.Language == "EN" then + if Squadron.Language == "EN" and self.SetSendPlayerMessages then Dispatcher:MessageToPlayers( Squadron, DefenderName .. " landing at base.", DefenderGroup ) - elseif Squadron.Language == "RU" then - Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", захватывающие самолеты в посадка на базу.", DefenderGroup ) + elseif Squadron.Language == "RU" and self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", посадка на базу.", DefenderGroup ) end if Action and Action == "Destroy" then @@ -129441,8 +141867,8 @@ do -- AI_A2A_DISPATCHER end end - end -- if DefenderGCI then - end -- while ( DefendersNeeded > 0 ) do + end -- if DefenderGCI then + end -- while ( DefendersNeeded > 0 ) do end else -- No more resources, try something else. @@ -129458,8 +141884,6 @@ do -- AI_A2A_DISPATCHER end -- if AttackerUnit end - - --- Creates an ENGAGE task when there are human friendlies airborne near the targets. -- @param #AI_A2A_DISPATCHER self -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem The detected item. @@ -129467,7 +141891,7 @@ do -- AI_A2A_DISPATCHER function AI_A2A_DISPATCHER:EvaluateENGAGE( DetectedItem ) self:F( { DetectedItem.ItemID } ) - -- First, count the active AIGroups Units, targetting the DetectedSet + -- First, count the active AIGroups Units, targeting the DetectedSet local DefenderCount = self:CountDefendersEngaged( DetectedItem ) local DefenderGroups = self:CountDefendersToBeEngaged( DetectedItem, DefenderCount ) @@ -129496,7 +141920,7 @@ do -- AI_A2A_DISPATCHER local AttackerSet = DetectedItem.Set local AttackerCount = AttackerSet:Count() - -- First, count the active AIGroups Units, targetting the DetectedSet + -- First, count the active AIGroups Units, targeting the DetectedSet local DefenderCount = self:CountDefendersEngaged( DetectedItem ) local DefendersMissing = AttackerCount - DefenderCount self:F( { AttackerCount = AttackerCount, DefenderCount = DefenderCount, DefendersMissing = DefendersMissing } ) @@ -129511,12 +141935,11 @@ do -- AI_A2A_DISPATCHER return nil, nil end - --- Assigns A2G AI Tasks in relation to the detected items. - -- @param #AI_A2G_DISPATCHER self + -- @param #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:Order( DetectedItem ) - local detection=self.Detection -- Functional.Detection#DETECTION_AREAS + local detection = self.Detection -- Functional.Detection#DETECTION_AREAS local ShortestDistance = 999999999 @@ -129545,7 +141968,6 @@ do -- AI_A2A_DISPATCHER return ShortestDistance end - --- Shows the tactical display. -- @param #AI_A2A_DISPATCHER self -- @param Functional.Detection#DETECTION_BASE Detection The detection created by the @{Functional.Detection#DETECTION_BASE} derived object. @@ -129562,8 +141984,10 @@ do -- AI_A2A_DISPATCHER local DefenderGroupCount = 0 -- Now that all obsolete tasks are removed, loop through the detected targets. - --for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do - for DetectedItemID, DetectedItem in UTILS.spairs( Detection:GetDetectedItems(), function( t, a, b ) return self:Order(t[a]) < self:Order(t[b]) end ) do + -- for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do + for DetectedItemID, DetectedItem in UTILS.spairs( Detection:GetDetectedItems(), function( t, a, b ) + return self:Order( t[a] ) < self:Order( t[b] ) + end ) do local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT @@ -129578,30 +142002,30 @@ do -- AI_A2A_DISPATCHER local DetectedItemChanged = DetectedItem.Changed -- Show tactical situation - Report:Add( string.format( "\n- Target %s (%s): (#%d) %s" , DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Set:GetObjectNames() ) ) + Report:Add( string.format( "\n- Target %s (%s): (#%d) %s", DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Set:GetObjectNames() ) ) for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do local Defender = Defender -- Wrapper.Group#GROUP - if DefenderTask.Target and DefenderTask.Target.Index == DetectedItem.Index then - if Defender and Defender:IsAlive() then - DefenderGroupCount = DefenderGroupCount + 1 - local Fuel = Defender:GetFuelMin() * 100 - local Damage = Defender:GetLife() / Defender:GetLife0() * 100 - Report:Add( string.format( " - %s*%d/%d (%s - %s): (#%d) F: %3d, D:%3d - %s", - Defender:GetName(), - Defender:GetSize(), - Defender:GetInitialSize(), - DefenderTask.Type, - DefenderTask.Fsm:GetState(), - Defender:GetSize(), - Fuel, - Damage, - Defender:HasTask() == true and "Executing" or "Idle" ) ) - end - end + if DefenderTask.Target and DefenderTask.Target.Index == DetectedItem.Index then + if Defender and Defender:IsAlive() then + DefenderGroupCount = DefenderGroupCount + 1 + local Fuel = Defender:GetFuelMin() * 100 + local Damage = Defender:GetLife() / Defender:GetLife0() * 100 + Report:Add( string.format( " - %s*%d/%d (%s - %s): (#%d) F: %3d, D:%3d - %s", + Defender:GetName(), + Defender:GetSize(), + Defender:GetInitialSize(), + DefenderTask.Type, + DefenderTask.Fsm:GetState(), + Defender:GetSize(), + Fuel, + Damage, + Defender:HasTask() == true and "Executing" or "Idle" ) ) + end + end end end - Report:Add( "\n- No Targets:") + Report:Add( "\n- No Targets:" ) local TaskCount = 0 for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do TaskCount = TaskCount + 1 @@ -129646,7 +142070,6 @@ do -- AI_A2A_DISPATCHER local TaskReport = REPORT:New() - for AIGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do local AIGroup = AIGroup -- Wrapper.Group#GROUP if not AIGroup:IsAlive() then @@ -129680,8 +142103,10 @@ do -- AI_A2A_DISPATCHER -- Now that all obsolete tasks are removed, loop through the detected targets. -- Closest detected targets to be considered first! - --for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do - for DetectedItemID, DetectedItem in UTILS.spairs( Detection:GetDetectedItems(), function( t, a, b ) return self:Order(t[a]) < self:Order(t[b]) end ) do + -- for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do + for DetectedItemID, DetectedItem in UTILS.spairs( Detection:GetDetectedItems(), function( t, a, b ) + return self:Order( t[a] ) < self:Order( t[b] ) + end ) do local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT @@ -129740,7 +142165,7 @@ do for PlayerUnitName, PlayerUnitData in pairs( PlayersNearBy ) do local PlayerUnit = PlayerUnitData -- Wrapper.Unit#UNIT local PlayerName = PlayerUnit:GetPlayerName() - --self:F( { PlayerName = PlayerName, PlayerUnit = PlayerUnit } ) + -- self:F( { PlayerName = PlayerName, PlayerUnit = PlayerUnit } ) if PlayerUnit:IsAirPlane() and PlayerName ~= nil then local FriendlyUnitThreatLevel = PlayerUnit:GetThreatLevel() PlayersCount = PlayersCount + 1 @@ -129753,19 +142178,18 @@ do end - --self:F( { PlayersCount = PlayersCount } ) + -- self:F( { PlayersCount = PlayersCount } ) local PlayerTypesReport = REPORT:New() if PlayersCount > 0 then for PlayerName, PlayerType in pairs( PlayerTypes ) do - PlayerTypesReport:Add( string.format('"%s" in %s', PlayerName, PlayerType ) ) + PlayerTypesReport:Add( string.format( '"%s" in %s', PlayerName, PlayerType ) ) end else PlayerTypesReport:Add( "-" ) end - return PlayersCount, PlayerTypesReport end @@ -129789,7 +142213,7 @@ do local FriendlyUnitThreatLevel = FriendlyUnit:GetThreatLevel() FriendliesCount = FriendliesCount + 1 local FriendlyType = FriendlyUnit:GetTypeName() - FriendlyTypes[FriendlyType] = FriendlyTypes[FriendlyType] and ( FriendlyTypes[FriendlyType] + 1 ) or 1 + FriendlyTypes[FriendlyType] = FriendlyTypes[FriendlyType] and (FriendlyTypes[FriendlyType] + 1) or 1 if DetectedTreatLevel < FriendlyUnitThreatLevel + 2 then end end @@ -129797,19 +142221,18 @@ do end - --self:F( { FriendliesCount = FriendliesCount } ) + -- self:F( { FriendliesCount = FriendliesCount } ) local FriendlyTypesReport = REPORT:New() if FriendliesCount > 0 then for FriendlyType, FriendlyTypeCount in pairs( FriendlyTypes ) do - FriendlyTypesReport:Add( string.format("%d of %s", FriendlyTypeCount, FriendlyType ) ) + FriendlyTypesReport:Add( string.format( "%d of %s", FriendlyTypeCount, FriendlyType ) ) end else FriendlyTypesReport:Add( "-" ) end - return FriendliesCount, FriendlyTypesReport end @@ -129819,6 +142242,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 @@ -129834,11 +142281,7 @@ do -- -- # Demo Missions -- - -- ### [AI\_A2A\_GCICAP for Caucasus](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-200%20-%20AI_A2A%20-%20GCICAP%20Demonstration) - -- ### [AI\_A2A\_GCICAP for NTTR](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-210%20-%20NTTR%20AI_A2A_GCICAP%20Demonstration) - -- ### [AI\_A2A\_GCICAP for Normandy](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-220%20-%20NORMANDY%20AI_A2A_GCICAP%20Demonstration) - -- - -- ### [AI\_A2A\_GCICAP for beta testers](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AID%20-%20AI%20Dispatching) + -- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AID%20-%20AI%20Dispatching/AID-A2A%20-%20AI%20A2A%20Dispatching) -- -- === -- @@ -129916,13 +142359,13 @@ do -- -- ![Mission Editor Action](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_GCICAP-ME_4.JPG) -- - -- **All airplane or helicopter groups that are starting with any of the choosen Template Prefixes will result in a squadron created at the airbase.** + -- **All airplane or helicopter groups that are starting with any of the chosen Template Prefixes will result in a squadron created at the airbase.** -- -- ### 1.4) Place floating helicopters to create the CAP zones defined by its route points. -- -- ![Mission Editor Action](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_GCICAP-ME_5.JPG) -- - -- **All airplane or helicopter groups that are starting with any of the choosen Template Prefixes will result in a squadron created at the airbase.** + -- **All airplane or helicopter groups that are starting with any of the chosen Template Prefixes will result in a squadron created at the airbase.** -- -- The helicopter indicates the start of the CAP zone. -- The route points define the form of the CAP zone polygon. @@ -129935,7 +142378,7 @@ do -- -- ### 2.1) Planes are taking off in the air from the airbases. -- - -- This prevents airbases to get cluttered with airplanes taking off, it also reduces the risk of human players colliding with taxiiing airplanes, + -- This prevents airbases to get cluttered with airplanes taking off, it also reduces the risk of human players colliding with taxiing airplanes, -- resulting in the airbase to halt operations. -- -- You can change the way how planes take off by using the inherited methods from AI\_A2A\_DISPATCHER: @@ -129959,7 +142402,7 @@ do -- -- ### 2.2) Planes return near the airbase or will land if damaged. -- - -- When damaged airplanes return to the airbase, they will be routed and will dissapear in the air when they are near the airbase. + -- When damaged airplanes return to the airbase, they will be routed and will disappear in the air when they are near the airbase. -- There are exceptions to this rule, airplanes that aren't "listening" anymore due to damage or out of fuel, will return to the airbase and land. -- -- You can change the way how planes land by using the inherited methods from AI\_A2A\_DISPATCHER: @@ -129969,7 +142412,7 @@ do -- * @{#AI_A2A_DISPATCHER.SetSquadronLandingAtRunway}() will despawn the returning aircraft directly after landing at the runway. -- * @{#AI_A2A_DISPATCHER.SetSquadronLandingAtEngineShutdown}() will despawn the returning aircraft when the aircraft has returned to its parking spot and has turned off its engines. -- - -- You can use these methods to minimize the airbase coodination overhead and to increase the airbase efficiency. + -- You can use these methods to minimize the airbase coordination overhead and to increase the airbase efficiency. -- When there are lots of aircraft returning for landing, at the same airbase, the takeoff process will be halted, which can cause a complete failure of the -- A2A defense system, as no new CAP or GCI planes can takeoff. -- Note that the method @{#AI_A2A_DISPATCHER.SetSquadronLandingNearAirbase}() will only work for returning aircraft, not for damaged or out of fuel aircraft. @@ -129992,7 +142435,7 @@ do -- * The minimum and maximum engage speed -- * The type of altitude measurement -- - -- These define how the squadron will perform the CAP while partrolling. Different terrain types requires different types of CAP. + -- These define how the squadron will perform the CAP while patrolling. Different terrain types requires different types of CAP. -- -- The @{#AI_A2A_DISPATCHER.SetSquadronCapInterval}() method specifies **how much** and **when** CAP flights will takeoff. -- @@ -130018,7 +142461,7 @@ do -- Essentially this controls how many flights of GCI aircraft can be active at any time. -- Note allowing large numbers of active GCI flights can adversely impact mission performance on low or medium specification hosts/servers. -- GCI needs to be setup at strategic airbases. Too far will mean that the aircraft need to fly a long way to reach the intruders, - -- too short will mean that the intruders may have alraedy passed the ideal interception point! + -- too short will mean that the intruders may have already passed the ideal interception point! -- -- For example, the following setup will create a GCI for squadron "Sochi": -- @@ -130057,8 +142500,7 @@ do -- These late activated Groups start with the name `SQUADRON CCCP`. Each Group object contains only one Unit, and defines the weapon payload, skin and skill level. -- * `"CAP CCCP"`: CAP Zones are defined using floating, late activated Helicopter Group objects, where the route points define the route of the polygon of the CAP Zone. -- These Helicopter Group objects start with the name `CAP CCCP`, and will be the locations wherein CAP will be performed. - -- * `2` Defines how many CAP airplanes are patrolling in each CAP zone defined simulateneously. - -- + -- * `2` Defines how many CAP airplanes are patrolling in each CAP zone defined simultaneously. -- -- ### 4.2) A more advanced setup: -- @@ -130075,7 +142517,7 @@ do -- * `{ "104th CAP" }`: An array of the names of the CAP zones are defined using floating, late activated helicopter group objects, -- where the route points define the route of the polygon of the CAP Zone. -- These Helicopter Group objects start with the name `104th CAP`, and will be the locations wherein CAP will be performed. - -- * `4` Defines how many CAP airplanes are patrolling in each CAP zone defined simulateneously. + -- * `4` Defines how many CAP airplanes are patrolling in each CAP zone defined simultaneously. -- -- @field #AI_A2A_GCICAP AI_A2A_GCICAP = { @@ -130083,7 +142525,6 @@ do Detection = nil, } - --- AI_A2A_GCICAP constructor. -- @param #AI_A2A_GCICAP self -- @param #string EWRPrefixes A list of prefixes that of groups that setup the Early Warning Radar network. @@ -130163,7 +142604,7 @@ do -- -- The CAP Zone prefix is nil. No CAP is created. -- -- The CAP Limit is nil. -- -- The Grouping Radius is nil. The default range of 6km radius will be grouped as a group of targets. - -- -- The Engage Radius is set nil. The default Engage Radius will be used to consider a defenser being assigned to a task. + -- -- The Engage Radius is set nil. The default Engage Radius will be used to consider a defender being assigned to a task. -- -- The GCI Radius is nil. Any target detected within the default GCI Radius will be considered for GCI engagement. -- -- The amount of resources for each squadron is set to 30. Thus about 30 resources are allocated to each squadron created. -- @@ -130175,7 +142616,7 @@ do EWRSetGroup:FilterPrefixes( EWRPrefixes ) EWRSetGroup:FilterStart() - local Detection = DETECTION_AREAS:New( EWRSetGroup, GroupingRadius or 30000 ) + local Detection = DETECTION_AREAS:New( EWRSetGroup, GroupingRadius or 30000 ) local self = BASE:Inherit( self, AI_A2A_DISPATCHER:New( Detection ) ) -- #AI_A2A_GCICAP @@ -130196,14 +142637,11 @@ do end end - self.Templates = SET_GROUP - :New() - :FilterPrefixes( TemplatePrefixes ) - :FilterOnce() + self.Templates = SET_GROUP:New():FilterPrefixes( TemplatePrefixes ):FilterOnce() -- Setup squadrons - self:I( { Airbases = AirbaseNames } ) + self:I( { Airbases = AirbaseNames } ) self:I( "Defining Templates for Airbases ..." ) for AirbaseID, AirbaseName in pairs( AirbaseNames ) do @@ -130281,7 +142719,7 @@ do self:HandleEvent( EVENTS.Crash, self.OnEventCrashOrDead ) self:HandleEvent( EVENTS.Dead, self.OnEventCrashOrDead ) - --self:HandleEvent( EVENTS.RemoveUnit, self.OnEventCrashOrDead ) + -- self:HandleEvent( EVENTS.RemoveUnit, self.OnEventCrashOrDead ) self:HandleEvent( EVENTS.Land ) self:HandleEvent( EVENTS.EngineShutdown ) @@ -130378,7 +142816,7 @@ do -- -- The CAP Zone prefix is nil. No CAP is created. -- -- The CAP Limit is nil. -- -- The Grouping Radius is nil. The default range of 6km radius will be grouped as a group of targets. - -- -- The Engage Radius is set nil. The default Engage Radius will be used to consider a defenser being assigned to a task. + -- -- The Engage Radius is set nil. The default Engage Radius will be used to consider a defender being assigned to a task. -- -- The GCI Radius is nil. Any target detected within the default GCI Radius will be considered for GCI engagement. -- -- The amount of resources for each squadron is set to 30. Thus about 30 resources are allocated to each squadron created. -- @@ -130395,11 +142833,11 @@ do return self end - + end ---- **AI** -- Models the process of air to ground BAI engagement for airplanes and helicopters. +--- **AI** - Models the process of air to ground BAI engagement for airplanes and helicopters. -- --- This is a class used in the @{AI_A2G_Dispatcher}. +-- This is a class used in the @{AI.AI_A2G_Dispatcher}. -- -- === -- @@ -130410,11 +142848,8 @@ end -- @module AI.AI_A2G_BAI -- @image AI_Air_To_Ground_Engage.JPG - - --- @type AI_A2G_BAI --- @extends AI.AI_A2A_Engage#AI_A2A_Engage - +-- @extends AI.AI_A2A_Engage#AI_A2A_Engage -- TODO: Documentation. This class does not exist, unable to determine what it extends. --- Implements the core functions to intercept intruders. Use the Engage trigger to intercept intruders. -- @@ -130425,8 +142860,6 @@ AI_A2G_BAI = { ClassName = "AI_A2G_BAI", } - - --- Creates a new AI_A2G_BAI object -- @param #AI_A2G_BAI self -- @param Wrapper.Group#GROUP AIGroup @@ -130435,7 +142868,7 @@ AI_A2G_BAI = { -- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. -- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. -- @param DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO". --- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Core.Zone} where the patrol needs to be executed. -- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. -- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. -- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. @@ -130452,7 +142885,6 @@ function AI_A2G_BAI:New2( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAl return self end - --- Creates a new AI_A2G_BAI object -- @param #AI_A2G_BAI self -- @param Wrapper.Group#GROUP AIGroup @@ -130460,7 +142892,7 @@ end -- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. -- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. -- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. --- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Core.Zone} where the patrol needs to be executed. -- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. -- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. -- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. @@ -130475,7 +142907,7 @@ end --- Evaluate the attack and create an AttackUnitTask list. -- @param #AI_A2G_BAI self -- @param Core.Set#SET_UNIT AttackSetUnit The set of units to attack. --- @param Wrappper.Group#GROUP DefenderGroup The group of defenders. +-- @param Wrapper.Group#GROUP DefenderGroup The group of defenders. -- @param #number EngageAltitude The altitude to engage the targets. -- @return #AI_A2G_BAI self function AI_A2G_BAI:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageAltitude ) @@ -130491,14 +142923,12 @@ function AI_A2G_BAI:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageA end end end - + return AttackUnitTasks end - - ---- **AI** -- Models the process of air to ground engagement for airplanes and helicopters. +--- **AI** - Models the process of air to ground engagement for airplanes and helicopters. -- --- This is a class used in the @{AI_A2G_Dispatcher}. +-- This is a class used in the @{AI.AI_A2G_Dispatcher}. -- -- === -- @@ -130509,11 +142939,8 @@ end -- @module AI.AI_A2G_CAS -- @image AI_Air_To_Ground_Engage.JPG - - --- @type AI_A2G_CAS --- @extends AI.AI_A2G_Patrol#AI_AIR_PATROL - +-- @extends AI.AI_A2G_Patrol#AI_AIR_PATROL TODO: Documentation. This class does not exist, unable to determine what it extends. --- Implements the core functions to intercept intruders. Use the Engage trigger to intercept intruders. -- @@ -130524,8 +142951,6 @@ AI_A2G_CAS = { ClassName = "AI_A2G_CAS", } - - --- Creates a new AI_A2G_CAS object -- @param #AI_A2G_CAS self -- @param Wrapper.Group#GROUP AIGroup @@ -130534,7 +142959,7 @@ AI_A2G_CAS = { -- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. -- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. -- @param DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO". --- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Core.Zone} where the patrol needs to be executed. -- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. -- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. -- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. @@ -130551,7 +142976,6 @@ function AI_A2G_CAS:New2( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAl return self end - --- Creates a new AI_A2G_CAS object -- @param #AI_A2G_CAS self -- @param Wrapper.Group#GROUP AIGroup @@ -130559,7 +142983,7 @@ end -- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. -- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. -- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. --- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Core.Zone} where the patrol needs to be executed. -- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. -- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. -- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. @@ -130574,7 +142998,7 @@ end --- Evaluate the attack and create an AttackUnitTask list. -- @param #AI_A2G_CAS self -- @param Core.Set#SET_UNIT AttackSetUnit The set of units to attack. --- @param Wrappper.Group#GROUP DefenderGroup The group of defenders. +-- @param Wrapper.Group#GROUP DefenderGroup The group of defenders. -- @param #number EngageAltitude The altitude to engage the targets. -- @return #AI_A2G_CAS self function AI_A2G_CAS:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageAltitude ) @@ -130590,15 +143014,12 @@ function AI_A2G_CAS:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageA end end end - + return AttackUnitTasks end - - - ---- **AI** -- Models the process of air to ground SEAD engagement for airplanes and helicopters. +--- **AI** - Models the process of air to ground SEAD engagement for airplanes and helicopters. -- --- This is a class used in the @{AI_A2G_Dispatcher}. +-- This is a class used in the @{AI.AI_A2G_Dispatcher}. -- -- === -- @@ -130640,8 +143061,8 @@ end -- -- ![Process](..\Presentations\AI_GCI\Dia10.JPG) -- --- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. --- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- Until a fuel or damage threshold has been reached by the AI, or when the AI is commanded to RTB. +-- When the fuel threshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. -- -- ![Process](..\Presentations\AI_GCI\Dia13.JPG) -- @@ -130663,9 +143084,9 @@ end -- -- ![Zone](..\Presentations\AI_GCI\Dia12.JPG) -- --- An optional @{Zone} can be set, +-- An optional @{Core.Zone} can be set, -- that will define when the AI will engage with the detected airborne enemy targets. --- Use the method @{AI.AI_Cap#AI_A2G_SEAD.SetEngageZone}() to define that Zone. +-- Use the method @{AI.AI_CAP#AI_CAP_ZONE.SetEngageZone}() to define that Zone. -- TODO: Documentation. Check that this is actually correct. The originally referenced class does not exist. -- -- === -- @@ -130674,8 +143095,6 @@ AI_A2G_SEAD = { ClassName = "AI_A2G_SEAD", } - - --- Creates a new AI_A2G_SEAD object -- @param #AI_A2G_SEAD self -- @param Wrapper.Group#GROUP AIGroup @@ -130684,7 +143103,7 @@ AI_A2G_SEAD = { -- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. -- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. -- @param DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO". --- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Core.Zone} where the patrol needs to be executed. -- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. -- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. -- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. @@ -130709,7 +143128,7 @@ end -- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. -- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. -- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. --- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Core.Zone} where the patrol needs to be executed. -- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. -- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. -- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. @@ -130725,7 +143144,7 @@ end --- Evaluate the attack and create an AttackUnitTask list. -- @param #AI_A2G_SEAD self -- @param Core.Set#SET_UNIT AttackSetUnit The set of units to attack. --- @param Wrappper.Group#GROUP DefenderGroup The group of defenders. +-- @param Wrapper.Group#GROUP DefenderGroup The group of defenders. -- @param #number EngageAltitude The altitude to engage the targets. -- @return #AI_A2G_SEAD self function AI_A2G_SEAD:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageAltitude ) @@ -130748,7 +143167,7 @@ function AI_A2G_SEAD:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, Engage return AttackUnitTasks end ---- **AI** - Create an automated A2G defense system based on a detection network of reconnaissance vehicles and air units, coordinating SEAD, BAI and CAP operations. +--- **AI** - Create an automated A2G defense system based on a detection network of reconnaissance vehicles and air units, coordinating SEAD, BAI and CAS operations. -- -- === -- @@ -130757,14 +143176,14 @@ end -- * Setup quickly an A2G defense system for a coalition. -- * Setup multiple defense zones to defend specific coordinates in your battlefield. -- * Setup (SEAD) Suppression of Air Defense squadrons, to gain control in the air of enemy grounds. --- * Setup (CAS) Controlled Air Support squadrons, to attack closeby enemy ground units near friendly installations. -- * Setup (BAI) Battleground Air Interdiction squadrons to attack remote enemy ground units and targets. +-- * Setup (CAS) Controlled Air Support squadrons, to attack close by enemy ground units near friendly installations. -- * Define and use a detection network controlled by recce. --- * Define A2G defense squadrons at airbases, farps and carriers. +-- * Define A2G defense squadrons at airbases, FARPs and carriers. -- * Enable airbases for A2G defenses. -- * Add different planes and helicopter templates to squadrons. -- * Assign squadrons to execute a specific engagement type depending on threat level of the detected ground enemy unit composition. --- * Add multiple squadrons to different airbases, farps or carriers. +-- * Add multiple squadrons to different airbases, FARPs or carriers. -- * Define different ranges to engage upon. -- * Establish an automatic in air refuel process for planes using refuel tankers. -- * Setup default settings for all squadrons and A2G defenses. @@ -130780,7 +143199,7 @@ end -- -- ## YouTube Channel: -- --- [DCS WORLD - MOOSE - A2G GCICAP - Build an automatic A2G Defense System](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0S4KMNUUJpaUs6zZHjLKNx) +-- [DCS WORLD - MOOSE - A2G DISPATCHER - Build an automatic A2G Defense System - Introduction](https://www.youtube.com/watch?v=zwSxWRAGVH8) -- -- === -- @@ -130790,10 +143209,10 @@ end -- -- AI_A2G_DISPATCHER is the main A2G defense class that models the A2G defense system. -- --- Before you start using the AI_A2G_DISPATCHER, ask youself the following questions. +-- Before you start using the AI_A2G_DISPATCHER, ask yourself the following questions: -- -- --- ## 1. Which coalition am I modeling an A2G defense system for? blue or red? +-- ## 1. Which coalition am I modeling an A2G defense system for? Blue or red? -- -- One AI_A2G_DISPATCHER object can create a defense system for **one coalition**, which is blue or red. -- If you want to create a **mutual defense system**, for both blue and red, then you need to create **two** AI_A2G_DISPATCHER **objects**, @@ -130802,7 +143221,7 @@ end -- -- ## 2. Which type of detection will I setup? Grouping based per AREA, per TYPE or per UNIT? (Later others will follow). -- --- The MOOSE framework leverages the @{Functional.Detection} classes to perform the reconnaissance, detecting enemy units +-- The MOOSE framework leverages the @{Functional.Detection} classes to perform the reconnaissance, detecting enemy units -- and reporting them to the head quarters. -- Several types of @{Functional.Detection} classes exist, and the most common characteristics of these classes is that they: -- @@ -130823,10 +143242,10 @@ end -- the recce will be very effective at detecting approaching enemy targets. Therefore, always use the terrain very carefully! -- -- Airborne recce (AFAC) are also very effective. The are capable of patrolling at a functional detection altitude, --- having an overview of the whole battlefield. However, airborne recce can be vulnerable to air to ground attacks, +-- having an overview of the whole battlefield. However, airborne recce can be vulnerable to air to ground attacks, -- so you need air superiority to make them effective. -- Airborne recce will also have varying ground detection technology, which plays a big role in the effectiveness of the reconnaissance. --- Certain helicopter or plane types have ground searching radars or advanced ground scanning technology, and are very effective +-- Certain helicopter or plane types have ground searching radars or advanced ground scanning technology, and are very effective -- compared to air units having only visual detection capabilities. -- For example, for the red coalition, the Mi-28N and the Su-34; and for the blue side, the reaper, are such effective airborne recce units. -- @@ -130838,7 +143257,7 @@ end -- -- ## 4. How do the defenses decide **when and where to engage** on approaching enemy units? -- --- The A2G dispacher needs you to setup (various) defense coordinates, which are strategic positions in the battle field to be defended. +-- The A2G dispatcher needs you to setup (various) defense coordinates, which are strategic positions in the battle field to be defended. -- Any ground based enemy approaching within the proximity of such a defense point, may trigger for a defensive action by friendly air units. -- -- There are 2 important parameters that play a role in the defensive decision making: defensiveness and reactivity. @@ -130858,7 +143277,7 @@ end -- ## 5. Are defense coordinates and defense reactivity the only parameters? -- -- No, depending on the target type, and the threat level of the target, the probability of defense will be higher. --- In other words, when a SAM-10 radar emitter is detected, its probabilty for defense will be much higher than when a BMP-1 vehicle is +-- In other words, when a SAM-10 radar emitter is detected, its probability for defense will be much higher than when a BMP-1 vehicle is -- detected, even when both enemies are at the same distance from a defense coordinate. -- This will ensure optimal defenses, SEAD tasks will be launched much more quicker against engaging radar emitters, to ensure air superiority. -- Approaching main battle tanks will be engaged much faster, than a group of approaching trucks. @@ -130867,25 +143286,26 @@ end -- ## 6. Which Squadrons will I create and which name will I give each Squadron? -- -- The A2G defense system works with **Squadrons**. Each Squadron must be given a unique name, that forms the **key** to the squadron. --- Several options and activities can be set per Squadron. A free format name can be given, but always ensure that the name is meaningfull +-- Several options and activities can be set per Squadron. A free format name can be given, but always ensure that the name is meaningful -- for your mission, and remember that squadron names are used for communication to the players of your mission. -- --- There are mainly 3 types of defenses: **SEAD**, **CAS** and **BAI**. +-- There are mainly 3 types of defenses: **SEAD**, **BAI**, and **CAS**. -- --- Suppression of Air Defenses (SEAD) are effective agains radar emitters. Close Air Support (CAS) is launched when the enemy is close near friendly units. +-- Suppression of Air Defenses (SEAD) are effective against radar emitters. -- Battleground Air Interdiction (BAI) tasks are launched when there are no friendlies around. +-- Close Air Support (CAS) is launched when the enemy is close near friendly units. -- -- Depending on the defense type, different payloads will be needed. See further points on squadron definition. -- -- --- ## 7. Where will the Squadrons be located? On Airbases? On Carrier Ships? On Farps? +-- ## 7. Where will the Squadrons be located? On Airbases? On Carriers? On FARPs? -- --- Squadrons are placed at the **home base** on an **airfield**, **carrier** or **farp**. +-- Squadrons are placed at the **home base** on an **airfield**, **carrier** or **FARP**. -- Carefully plan where each Squadron will be located as part of the defense system required for mission effective defenses. -- If the home base of the squadron is too far from assumed enemy positions, then the defenses will be too late. -- The home bases must be **behind** enemy lines, you want to prevent your home bases to be engaged by enemies! -- Depending on the units applied for defenses, the home base can be further or closer to the enemies. --- Any airbase, farp or carrier can act as the launching platform for A2G defenses. +-- Any airbase, FARP, or carrier can act as the launching platform for A2G defenses. -- Carefully plan which airbases will take part in the coalition. Color each airbase **in the color of the coalition**, using the mission editor, -- or your air units will not return for landing at the airbase! -- @@ -130896,7 +143316,7 @@ end -- These are late activated groups with one airplane or helicopter that start with a specific name, called the **template prefix**. -- The A2G defense system will select from the given templates a random template to spawn a new plane (group). -- --- A squadron will perform specific task types (SEAD, CAS or BAI). So, squadrons will require specific templates for the +-- A squadron will perform specific task types (SEAD, BAI or CAS). So, squadrons will require specific templates for the -- task types it will perform. A squadron executing SEAD defenses, will require a payload with long range anti-radar seeking missiles. -- -- @@ -130907,10 +143327,10 @@ end -- The A2G defense system will select from the given templates a random template to spawn a new plane (group). -- -- --- ## 10. How to squadrons engage in a defensive action? +-- ## 10. How do squadrons engage in a defensive action? -- -- There are two ways how squadrons engage and execute your A2G defenses. --- Squadrons can start the defense directly from the airbase, farp or carrier. When a squadron launches a defensive group, that group +-- Squadrons can start the defense directly from the airbase, FARP or carrier. When a squadron launches a defensive group, that group -- will start directly from the airbase. The other way is to launch early on in the mission a patrolling mechanism. -- Squadrons will launch air units to patrol in specific zone(s), so that when ground enemy targets are detected, that the airborne -- A2G defenses can come immediately into action. @@ -130924,32 +143344,32 @@ end -- * polygon zones -- * moving zones -- --- Depending on the type of zone selected, a different @{Zone} object needs to be created from a ZONE_ class. +-- Depending on the type of zone selected, a different @{Core.Zone} object needs to be created from a ZONE_ class. -- -- -- ## 12. Are moving defense coordinates possible? -- -- Yes, different COORDINATE types are possible to be used. --- The COORDINATE_UNIT will help you to specify a defense coodinate that is attached to a moving unit. +-- The COORDINATE_UNIT will help you to specify a defense coordinate that is attached to a moving unit. -- -- --- ## 13. How much defense coordinates do I need to create? +-- ## 13. How many defense coordinates do I need to create? -- -- It depends, but the idea is to define only the necessary defense points that drive your mission. --- If you define too much defense points, the performance of your mission may decrease. Per defense point defined, --- all the possible enemies are evaluated. Note that each defense coordinate has a reach depending on the size of the defense radius. --- The default defense radius is about 60km, and depending on the defense reactivity, defenses will be launched when the enemy is at --- close or greater distance from the defense coordinate. +-- If you define too many defense coordinates, the performance of your mission may decrease. For each defined defense coordinate, +-- all the possible enemies are evaluated. Note that each defense coordinate has a reach depending on the size of the associated defense radius. +-- The default defense radius is about 60km. Depending on the defense reactivity, defenses will be launched when the enemy is at a +-- closer distance from the defense coordinate than the defense radius. -- -- -- ## 14. For each Squadron doing patrols, what are the time intervals and patrol amounts to be performed? -- -- For each patrol: -- --- * **How many** patrol you want to have airborne at the same time? +-- * **How many** patrols you want to have airborne at the same time? -- * **How frequent** you want the defense mechanism to check whether to start a new patrol? -- --- other considerations: +-- Other considerations: -- -- * **How far** is the patrol area from the engagement "hot zone". You want to ensure that the enemy is reached on time! -- * **How safe** is the patrol area taking into account air superiority. Is it well defended, are there nearby A2A bases? @@ -130964,9 +143384,8 @@ end -- * From a parking spot with running engines -- * From a parking spot with cold engines -- --- **The default takeoff method is staight in the air.** --- This takeoff method is the most useful if you want to avoid airplane clutter at airbases! --- But it is the least realistic one! +-- **The default takeoff method is straight in the air.** +-- This takeoff method is the most useful if you want to avoid airplane clutter at airbases, but it is the least realistic one. -- -- -- ## 16. For each Squadron, which landing method will I use? @@ -130977,20 +143396,19 @@ end -- * Despawn after landing on the runway -- * Despawn after engine shutdown after landing -- --- **The default landing method is despawn when near the airbase when returning.** --- This landing method is the most useful if you want to avoid airplane clutter at airbases! --- But it is the least realistic one! +-- **The default landing method is to despawn when near the airbase when returning.** +-- This landing method is the most useful if you want to avoid aircraft clutter at airbases, but it is the least realistic one. -- -- -- ## 19. For each Squadron, which **defense overhead** will I use? -- -- For each Squadron, depending on the helicopter or airplane type (modern, old) and payload, which overhead is required to provide any defense? -- --- In other words, if **X** enemy ground units are detected, how many **Y** defense helicpters or airplanes need to engage (per squadron)? --- The **Y** is dependent on the type of airplane (era), payload, fuel levels, skills etc. +-- In other words, if **X** enemy ground units are detected, how many **Y** defense helicopters or airplanes need to engage (per squadron)? +-- The **Y** is dependent on the type of aircraft (era), payload, fuel levels, skills etc. -- But the most important factor is the payload, which is the amount of A2G weapons the defense can carry to attack the enemy ground units. --- For example, a Ka-50 can carry 16 vikrs, that means, that it potentially can destroy at least 8 ground units without a reload of ammunication. --- That means, that one defender can destroy more enemy ground units. +-- For example, a Ka-50 can carry 16 Vikhrs, this means that it potentially can destroy at least 8 ground units without a reload of ammunition. +-- That means, that one defender can destroy more enemy ground units. -- Thus, the overhead is a **factor** that will calculate dynamically how many **Y** defenses will be required based on **X** attackers detected. -- -- **The default overhead is 1. A smaller value than 1, like 0.25 will decrease the overhead to a 1 / 4 ratio, meaning, @@ -131013,14 +143431,13 @@ end -- @image AI_Air_To_Ground_Dispatching.JPG - do -- AI_A2G_DISPATCHER --- AI_A2G_DISPATCHER class. -- @type AI_A2G_DISPATCHER -- @extends Tasking.DetectionManager#DETECTION_MANAGER - --- Create an automated A2G defense system based on a detection network of reconnaissance vehicles and air units, coordinating SEAD, BAI and CAP operations. + --- Create an automated A2G defense system based on a detection network of reconnaissance vehicles and air units, coordinating SEAD, BAI and CAS operations. -- -- === -- @@ -131029,7 +143446,7 @@ do -- AI_A2G_DISPATCHER -- Multiple defense coordinates can be setup. Defense coordinates can be strategic or tactical positions or references to strategic units or scenery. -- The A2G dispatcher will evaluate every x seconds the tactical situation around each defense coordinate. When a defense coordinate -- is under threat, it will communicate through the command center that defensive actions need to be taken and will launch groups of air units for defense. - -- The level of threat to the defense coordinate varyies upon the strength and types of the enemy units, the distance to the defense point, and the defensiveness parameters. + -- The level of threat to the defense coordinate varies upon the strength and types of the enemy units, the distance to the defense point, and the defensiveness parameters. -- Defensive actions are taken through probability, but the closer and the more threat the enemy poses to the defense coordinate, the faster it will be attacked by friendly A2G units. -- -- Please study carefully the underlying explanations how to setup and use this module, as it has many features. @@ -131040,6 +143457,7 @@ do -- AI_A2G_DISPATCHER -- -- # USAGE GUIDE -- + -- -- ## 1. AI\_A2G\_DISPATCHER constructor: -- -- ![Banner Image](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_DISPATCHER-ME_1.JPG) @@ -131047,6 +143465,7 @@ do -- AI_A2G_DISPATCHER -- -- The @{#AI_A2G_DISPATCHER.New}() method creates a new AI_A2G_DISPATCHER instance. -- + -- -- ### 1.1. Define the **reconnaissance network**: -- -- As part of the AI_A2G_DISPATCHER :New() constructor, a reconnaissance network must be given as the first parameter. @@ -131078,37 +143497,38 @@ do -- AI_A2G_DISPATCHER -- By spawning in dynamically additional recce, you can ensure that there is sufficient reconnaissance coverage so the defense mechanism is continuously -- alerted of new enemy ground targets. -- - -- The following example defens a new reconnaissance network using a @{Functional.Detection#DETECTION_AREAS} object. + -- The following is an example defense of a new reconnaissance network using a @{Functional.Detection#DETECTION_AREAS} object. -- -- -- Define a SET_GROUP object that builds a collection of groups that define the recce network. -- -- Here we build the network with all the groups that have a name starting with CCCP Recce. - -- DetectionSetGroup = SET_GROUP:New() -- Defene a set of group objects, caled DetectionSetGroup. - -- + -- DetectionSetGroup = SET_GROUP:New() -- Define a set of group objects, called DetectionSetGroup. + -- -- DetectionSetGroup:FilterPrefixes( { "CCCP Recce" } ) -- The DetectionSetGroup will search for groups that start with the name "CCCP Recce". - -- - -- -- This command will start the dynamic filtering, so when groups spawn in or are destroyed, + -- + -- -- This command will start the dynamic filtering, so when groups spawn in or are destroyed, -- -- which have a group name starting with "CCCP Recce", then these will be automatically added or removed from the set. - -- DetectionSetGroup:FilterStart() - -- + -- DetectionSetGroup:FilterStart() + -- -- -- This command defines the reconnaissance network. -- -- It will group any detected ground enemy targets within a radius of 1km. -- -- It uses the DetectionSetGroup, which defines the set of reconnaissance groups to detect for enemy ground targets. -- Detection = DETECTION_AREAS:New( DetectionSetGroup, 1000 ) -- - -- -- Setup the A2A dispatcher, and initialize it. + -- -- Setup the A2G dispatcher, and initialize it. -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) - -- - -- + -- + -- -- The above example creates a SET_GROUP instance, and stores this in the variable (object) **DetectionSetGroup**. -- **DetectionSetGroup** is then being configured to filter all active groups with a group name starting with `"CCCP Recce"` to be included in the set. - -- **DetectionSetGroup** is then calling `FilterStart()`, which is starting the dynamic filtering or inclusion of these groups. + -- **DetectionSetGroup** is then calling `FilterStart()`, which is starting the dynamic filtering or inclusion of these groups. -- Note that any destroy or new spawn of a group having a name, starting with the above prefix, will be removed or added to the set. -- - -- Then a new detection object is created from the class `DETECTION_AREAS`. A grouping radius of 1000 meters (1km) is choosen. + -- Then a new detection object is created from the class `DETECTION_AREAS`. A grouping radius of 1000 meters (1km) is chosen. -- - -- The `Detection` object is then passed to the @{#AI_A2G_DISPATCHER.New}() method to indicate the reconnaissance network + -- The `Detection` object is then passed to the @{#AI_A2G_DISPATCHER.New}() method to indicate the reconnaissance network -- configuration and setup the A2G defense detection mechanism. -- + -- -- ### 1.2. Setup the A2G dispatcher for both a red and blue coalition. -- -- Following the above described procedure, you'll need to create for each coalition an separate detection network, and a separate A2G dispatcher. @@ -131124,19 +143544,20 @@ do -- AI_A2G_DISPATCHER -- DetectionBlue = DETECTION_AREAS:New( DetectionSetGroupBlue, 1000 ) -- A2GDispatcherBlue = AI_A2G_DISPATCHER:New( DetectionBlue ) -- - -- -- Note: Also the SET_GROUP objects should be created for each coalition separately, containing each red and blue recce respectively! -- + -- -- ### 1.3. Define the enemy ground target **grouping radius**, in case you use DETECTION_AREAS: -- - -- The target grouping radius is a property of the DETECTION_AREAS class, that was passed to the AI_A2G_DISPATCHER:New() method, + -- The target grouping radius is a property of the DETECTION_AREAS class, that was passed to the AI_A2G_DISPATCHER:New() method -- but can be changed. The grouping radius should not be too small, but also depends on the types of ground forces and the way you want your mission to evolve. -- A large radius will mean large groups of enemy ground targets, while making smaller groups will result in a more fragmented defense system. -- Typically I suggest a grouping radius of 1km. This is the right balance to create efficient defenses. -- - -- Note that detected targets are constantly re-grouped, that is, when certain detected enemy ground units are moving further than the group radius, - -- then these units will become a separate area being detected. This may result in additional defenses being started by the dispatcher! - -- So don't make this value too small! Again, I advise about 1km or 1000 meters. + -- Note that detected targets are constantly re-grouped, that is, when certain detected enemy ground units are moving further than the group radius + -- then these units will become a separate area being detected. This may result in additional defenses being started by the dispatcher, + -- so don't make this value too small! Again, about 1km, or 1000 meters, is recommended. + -- -- -- ## 2. Setup (a) **Defense Coordinate(s)**. -- @@ -131156,7 +143577,7 @@ do -- AI_A2G_DISPATCHER -- -- Later, a COORDINATE_UNIT will be added to the framework, which can be used to assign "moving" coordinates to an A2G dispatcher. -- - -- **REMEMBER!** + -- **REMEMBER!** -- -- - **Defense coordinates are the center of the A2G dispatcher defense system!** -- - **You can define more defense coordinates to defend a larger area.** @@ -131179,15 +143600,16 @@ do -- AI_A2G_DISPATCHER -- This defines an A2G dispatcher which will engage on enemy ground targets within 30km radius around the defense coordinate. -- Note that the defense radius **applies to all defense coordinates** defined within the A2G dispatcher. -- + -- -- ### 2.2. The **Defense Reactivity**. -- - -- There are three levels that can be configured to tweak the defense reactivity. As explained above, the threat to a defense coordinate is + -- There are three levels that can be configured to tweak the defense reactivity. As explained above, the threat to a defense coordinate is -- also determined by the distance of the enemy ground target to the defense coordinate. -- If you want to have a **low** defense reactivity, that is, the probability that an A2G defense will engage to the enemy ground target, then - -- use the @{#AI_A2G_DISPATCHER.SetDefenseReactivityLow}() method. For medium and high reactivity, use the methods + -- use the @{#AI_A2G_DISPATCHER.SetDefenseReactivityLow}() method. For medium and high reactivity, use the methods -- @{#AI_A2G_DISPATCHER.SetDefenseReactivityMedium}() and @{#AI_A2G_DISPATCHER.SetDefenseReactivityHigh}() respectively. -- - -- Note that the reactivity of defenses is always in relation to the Defense Radius! the shorter the distance, + -- Note that the reactivity of defenses is always in relation to the Defense Radius! the shorter the distance, -- the less reactive the defenses will be in terms of distance to enemy ground targets! -- -- For example: @@ -131196,11 +143618,12 @@ do -- AI_A2G_DISPATCHER -- -- This defines an A2G dispatcher with high defense reactivity. -- + -- -- ## 3. **Squadrons**. -- -- The A2G dispatcher works with **Squadrons**, that need to be defined using the different methods available. -- - -- Use the method @{#AI_A2G_DISPATCHER.SetSquadron}() to **setup a new squadron** active at an airfield, farp or carrier, + -- Use the method @{#AI_A2G_DISPATCHER.SetSquadron}() to **setup a new squadron** active at an airfield, FARP or carrier, -- while defining which helicopter or plane **templates** are being used by the squadron and how many **resources** are available. -- -- **Multiple squadrons** can be defined within one A2G dispatcher, each having specific defense tasks and defense parameter settings! @@ -131219,11 +143642,11 @@ do -- AI_A2G_DISPATCHER -- * Control how new helicopters or aircraft are taking off from the airfield, farp or carrier (in the air, cold, hot, at the runway). -- * Control how returning helicopters or aircraft are landing at the airfield, farp or carrier (in the air near the airbase, after landing, after engine shutdown). -- * Control the **grouping** of new helicopters or aircraft spawned at the airfield, farp or carrier. If there is more than one helicopter or aircraft to be spawned, these may be grouped. - -- * Control the **overhead** or defensive strength of the squadron. Depending on the types of helicopters, planes, amount of resources and payload (weapon configuration) chosen, + -- * Control the **overhead** or defensive strength of the squadron. Depending on the types of helicopters, planes, amount of resources and payload (weapon configuration) chosen, -- the mission designer can choose to increase or reduce the amount of planes spawned. -- -- The method @{#AI_A2G_DISPATCHER.SetSquadron}() defines for you a new squadron. - -- The provided parameters are the squadron name, airbase name and a list of template prefixe, and a number that indicates the amount of resources. + -- The provided parameters are the squadron name, airbase name and a list of template prefixes, and a number that indicates the amount of resources. -- -- For example, this defines 3 new squadrons: -- @@ -131239,16 +143662,17 @@ do -- AI_A2G_DISPATCHER -- Squadrons can be commanded to execute 3 types of tasks, as explained above: -- -- - SEAD: Suppression of Air Defenses, which are ground targets that have medium or long range radar emitters. + -- - BAI : Battlefield Air Interdiction, which are targets further away from the front-line. -- - CAS : Close Air Support, when there are enemy ground targets close to friendly units. - -- - BAI : Battlefield Air Interdiction, which are targets further away from the frond-line. -- -- You need to configure each squadron which task types you want it to perform. Read on ... -- + -- -- ### 3.2. Squadrons enemy ground target **engagement types**. -- - -- There are two ways how targets can be engaged: directly **on call** from the airfield, farp or carrier, or through a **patrol**. + -- There are two ways how targets can be engaged: directly **on call** from the airfield, FARP or carrier, or through a **patrol**. -- - -- Patrols are extremely handy, as these will airborne your helicopters or airplanes in advance. They will patrol in defined zones outlined, + -- Patrols are extremely handy, as these will get your helicopters or airplanes airborne in advance. They will patrol in defined zones outlined, -- and will engage with the targets once commanded. If the patrol zone is close enough to the enemy ground targets, then the time required -- to engage is heavily minimized! -- @@ -131256,13 +143680,14 @@ do -- AI_A2G_DISPATCHER -- -- The mission designer needs to carefully balance the need for patrols or the need for engagement on call from the airfields. -- + -- -- ### 3.3. Squadron **on call** engagement. -- -- So to make squadrons engage targets from the airfields, use the following methods: -- -- - For SEAD, use the @{#AI_A2G_DISPATCHER.SetSquadronSead}() method. - -- - For CAS, use the @{#AI_A2G_DISPATCHER.SetSquadronCas}() method. -- - For BAI, use the @{#AI_A2G_DISPATCHER.SetSquadronBai}() method. + -- - For CAS, use the @{#AI_A2G_DISPATCHER.SetSquadronCas}() method. -- -- Note that for the tasks, specific helicopter or airplane templates are required to be used, which you can configure using your mission editor. -- Especially the payload (weapons configuration) is important to get right. @@ -131272,11 +143697,12 @@ do -- AI_A2G_DISPATCHER -- A2GDispatcher:SetSquadron( "Maykop SEAD", AIRBASE.Caucasus.Maykop_Khanskaya, { "CCCP KA-50 SEAD" }, 10 ) -- A2GDispatcher:SetSquadronSead( "Maykop SEAD", 120, 250 ) -- + -- A2GDispatcher:SetSquadron( "Maykop BAI", "BAI", { "CCCP KA-50 BAI" }, 10 ) + -- A2GDispatcher:SetSquadronBai( "Maykop BAI", 120, 250 ) + -- -- A2GDispatcher:SetSquadron( "Maykop CAS", "CAS", { "CCCP KA-50 CAS" }, 10 ) -- A2GDispatcher:SetSquadronCas( "Maykop CAS", 120, 250 ) -- - -- A2GDispatcher:SetSquadron( "Maykop BAI", "BAI", { "CCCP KA-50 BAI" }, 10 ) - -- A2GDispatcher:SetSquadronBai( "Maykop BAI", 120, 250 ) -- -- ### 3.4. Squadron **on patrol engagement**. -- @@ -131286,14 +143712,14 @@ do -- AI_A2G_DISPATCHER -- So to make squadrons engage targets from a patrol zone, use the following methods: -- -- - For SEAD, use the @{#AI_A2G_DISPATCHER.SetSquadronSeadPatrol}() method. - -- - For CAS, use the @{#AI_A2G_DISPATCHER.SetSquadronCasPatrol}() method. -- - For BAI, use the @{#AI_A2G_DISPATCHER.SetSquadronBaiPatrol}() method. + -- - For CAS, use the @{#AI_A2G_DISPATCHER.SetSquadronCasPatrol}() method. -- -- Because a patrol requires more parameters, the following methods must be used to fine-tune the patrols for each squadron. -- -- - For SEAD, use the @{#AI_A2G_DISPATCHER.SetSquadronSeadPatrolInterval}() method. - -- - For CAS, use the @{#AI_A2G_DISPATCHER.SetSquadronCasPatrolInterval}() method. -- - For BAI, use the @{#AI_A2G_DISPATCHER.SetSquadronBaiPatrolInterval}() method. + -- - For CAS, use the @{#AI_A2G_DISPATCHER.SetSquadronCasPatrolInterval}() method. -- -- Here an example to setup patrols of various task types: -- @@ -131301,16 +143727,16 @@ do -- AI_A2G_DISPATCHER -- A2GDispatcher:SetSquadronSeadPatrol( "Maykop SEAD", PatrolZone, 300, 500, 50, 80, 250, 300 ) -- A2GDispatcher:SetSquadronPatrolInterval( "Maykop SEAD", 2, 30, 60, 1, "SEAD" ) -- - -- A2GDispatcher:SetSquadron( "Maykop CAS", "CAS", { "CCCP KA-50 CAS" }, 10 ) - -- A2GDispatcher:SetSquadronCasPatrol( "Maykop CAS", PatrolZone, 600, 700, 50, 80, 250, 300 ) - -- A2GDispatcher:SetSquadronPatrolInterval( "Maykop CAS", 2, 30, 60, 1, "CAS" ) - -- -- A2GDispatcher:SetSquadron( "Maykop BAI", "BAI", { "CCCP KA-50 BAI" }, 10 ) -- A2GDispatcher:SetSquadronBaiPatrol( "Maykop BAI", PatrolZone, 800, 900, 50, 80, 250, 300 ) -- A2GDispatcher:SetSquadronPatrolInterval( "Maykop BAI", 2, 30, 60, 1, "BAI" ) -- + -- A2GDispatcher:SetSquadron( "Maykop CAS", "CAS", { "CCCP KA-50 CAS" }, 10 ) + -- A2GDispatcher:SetSquadronCasPatrol( "Maykop CAS", PatrolZone, 600, 700, 50, 80, 250, 300 ) + -- A2GDispatcher:SetSquadronPatrolInterval( "Maykop CAS", 2, 30, 60, 1, "CAS" ) + -- -- - -- ### 3.5. Set squadron take-off methods + -- ### 3.5. Set squadron takeoff methods -- -- Use the various SetSquadronTakeoff... methods to control how squadrons are taking-off from the home airfield, FARP or ship. -- @@ -131329,24 +143755,24 @@ do -- AI_A2G_DISPATCHER -- * aircraft may get into a "dead-lock" situation, where two aircraft are blocking each other. -- * aircraft may collide at the airbase. -- * aircraft may be awaiting the landing of a plane currently in the air, but never lands ... - -- + -- -- Currently within the DCS engine, the airfield traffic coordination is erroneous and contains a lot of bugs. - -- If you experience while testing problems with aircraft take-off or landing, please use one of the above methods as a solution to workaround these issues! + -- If you experience while testing problems with aircraft takeoff or landing, please use one of the above methods as a solution to workaround these issues! -- -- This example sets the default takeoff method to be from the runway. -- And for a couple of squadrons overrides this default method. -- - -- -- Setup the Takeoff methods - -- - -- -- The default takeoff - -- A2ADispatcher:SetDefaultTakeOffFromRunway() - -- - -- -- The individual takeoff per squadron - -- A2ADispatcher:SetSquadronTakeoff( "Mineralnye", AI_A2G_DISPATCHER.Takeoff.Air ) - -- A2ADispatcher:SetSquadronTakeoffInAir( "Sochi" ) - -- A2ADispatcher:SetSquadronTakeoffFromRunway( "Mozdok" ) - -- A2ADispatcher:SetSquadronTakeoffFromParkingCold( "Maykop" ) - -- A2ADispatcher:SetSquadronTakeoffFromParkingHot( "Novo" ) + -- -- Setup the takeoff methods + -- + -- -- Set the default takeoff method + -- A2GDispatcher:SetDefaultTakeoffFromRunway() + -- + -- -- Set the individual squadrons takeoff method + -- A2GDispatcher:SetSquadronTakeoff( "Mineralnye", AI_A2G_DISPATCHER.Takeoff.Air ) + -- A2GDispatcher:SetSquadronTakeoffInAir( "Sochi" ) + -- A2GDispatcher:SetSquadronTakeoffFromRunway( "Mozdok" ) + -- A2GDispatcher:SetSquadronTakeoffFromParkingCold( "Maykop" ) + -- A2GDispatcher:SetSquadronTakeoffFromParkingHot( "Novo" ) -- -- -- ### 3.5.1. Set Squadron takeoff altitude when spawning new aircraft in the air. @@ -131358,6 +143784,7 @@ do -- AI_A2G_DISPATCHER -- As part of the method @{#AI_A2G_DISPATCHER.SetSquadronTakeoffInAir}() a parameter can be specified to set the takeoff altitude. -- If this parameter is not specified, then the default altitude will be used for the squadron. -- + -- -- ### 3.5.2. Set Squadron takeoff interval. -- -- The different types of available airfields have different amounts of available launching platforms: @@ -131365,13 +143792,13 @@ do -- AI_A2G_DISPATCHER -- - Airbases typically have a lot of platforms. -- - FARPs have 4 platforms. -- - Ships have 2 to 4 platforms. - -- + -- -- Depending on the demand of requested takeoffs by the A2G dispatcher, an airfield can become overloaded. Too many aircraft need to be taken -- off at the same time, which will result in clutter as described above. In order to better control this behaviour, a takeoff scheduler is implemented, -- which can be used to control how many aircraft are ordered for takeoff between specific time intervals. - -- The takeff intervals can be specified per squadron, which make sense, as each squadron have a "home" airfield. + -- The takeoff intervals can be specified per squadron, which make sense, as each squadron have a "home" airfield. -- - -- For this purpose, the method @{#AI_A2G_DISPATCHER.SetSquadronTakeOffInterval}() can be used to specify the takeoff intervals of + -- For this purpose, the method @{#AI_A2G_DISPATCHER.SetSquadronTakeoffInterval}() can be used to specify the takeoff intervals of -- aircraft groups per squadron to avoid cluttering of aircraft at airbases. -- This is especially useful for FARPs and ships. Each takeoff dispatch is queued by the dispatcher and when the interval time -- has been reached, a new group will be spawned or activated for takeoff. @@ -131384,8 +143811,8 @@ do -- AI_A2G_DISPATCHER -- -- Imagine a squadron launched from a FARP, with a grouping of 4. -- -- Aircraft will cold start from the FARP, and thus, a maximum of 4 aircraft can be launched at the same time. -- -- Additionally, depending on the group composition of the aircraft, defending units will be ordered for takeoff together. - -- -- It takes about 3 to 4 minutes to takeoff helicopters from FARPs in cold start. - -- A2ADispatcher:SetSquadronTakeOffInterval( "Mineralnye", 60 * 4 ) + -- -- It takes about 3 to 4 minutes for helicopters to takeoff from FARPs in cold start. + -- A2GDispatcher:SetSquadronTakeoffInterval( "Mineralnye", 60 * 4 ) -- -- -- ### 3.6. Set squadron landing methods @@ -131397,9 +143824,9 @@ do -- AI_A2G_DISPATCHER -- * @{#AI_A2G_DISPATCHER.SetSquadronLandingAtRunway}() will despawn the returning aircraft directly after landing at the runway. -- * @{#AI_A2G_DISPATCHER.SetSquadronLandingAtEngineShutdown}() will despawn the returning aircraft when the aircraft has returned to its parking spot and has turned off its engines. -- - -- You can use these methods to minimize the airbase coodination overhead and to increase the airbase efficiency. + -- You can use these methods to minimize the airbase coordination overhead and to increase the airbase efficiency. -- When there are lots of aircraft returning for landing, at the same airbase, the takeoff process will be halted, which can cause a complete failure of the - -- A2A defense system, as no new CAP or GCI planes can takeoff. + -- A2G defense system, as no new SEAD, BAI or CAS planes can takeoff. -- Note that the method @{#AI_A2G_DISPATCHER.SetSquadronLandingNearAirbase}() will only work for returning aircraft, not for damaged or out of fuel aircraft. -- Damaged or out-of-fuel aircraft are returning to the nearest friendly airbase and will land, and are out of control from ground control. -- @@ -131409,14 +143836,14 @@ do -- AI_A2G_DISPATCHER -- -- Setup the Landing methods -- -- -- The default landing method - -- A2ADispatcher:SetDefaultLandingAtRunway() + -- A2GDispatcher:SetDefaultLandingAtRunway() -- -- -- The individual landing per squadron - -- A2ADispatcher:SetSquadronLandingAtRunway( "Mineralnye" ) - -- A2ADispatcher:SetSquadronLandingNearAirbase( "Sochi" ) - -- A2ADispatcher:SetSquadronLandingAtEngineShutdown( "Mozdok" ) - -- A2ADispatcher:SetSquadronLandingNearAirbase( "Maykop" ) - -- A2ADispatcher:SetSquadronLanding( "Novo", AI_A2G_DISPATCHER.Landing.AtRunway ) + -- A2GDispatcher:SetSquadronLandingAtRunway( "Mineralnye" ) + -- A2GDispatcher:SetSquadronLandingNearAirbase( "Sochi" ) + -- A2GDispatcher:SetSquadronLandingAtEngineShutdown( "Mozdok" ) + -- A2GDispatcher:SetSquadronLandingNearAirbase( "Maykop" ) + -- A2GDispatcher:SetSquadronLanding( "Novo", AI_A2G_DISPATCHER.Landing.AtRunway ) -- -- -- ### 3.7. Set squadron **grouping**. @@ -131425,7 +143852,7 @@ do -- AI_A2G_DISPATCHER -- -- ![Banner Image](..\Presentations\AI_A2G_DISPATCHER\Dia12.JPG) -- - -- In the case of **on call** engagement, the @{#AI_A2G_DISPATCHER.SetSquadronGrouping}() method has additional behaviour. + -- In the case of **on call** engagement, the @{#AI_A2G_DISPATCHER.SetSquadronGrouping}() method has additional behaviour. -- When there aren't enough patrol flights airborne, a on call will be initiated for the remaining -- targets to be engaged. Depending on the grouping parameter, the spawned flights for on call aircraft are grouped into this setting. -- For example with a group setting of 2, if 3 targets are detected and cannot be engaged by the available patrols or any airborne flight, @@ -131447,13 +143874,13 @@ do -- AI_A2G_DISPATCHER -- -- For example, a A-10C with full long-distance A2G missiles payload, may still be less effective than a Su-23 with short range A2G missiles... -- So in this case, one may want to use the @{#AI_A2G_DISPATCHER.SetOverhead}() method to allocate more defending planes as the amount of detected attacking ground units. - -- The overhead must be given as a decimal value with 1 as the neutral value, which means that overhead values: + -- The overhead must be given as a decimal value with 1 as the neutral value, which means that overhead values: -- -- * Higher than 1.0, for example 1.5, will increase the defense unit amounts. For 4 attacking ground units detected, 6 aircraft will be spawned. -- * Lower than 1, for example 0.75, will decrease the defense unit amounts. For 4 attacking ground units detected, only 3 aircraft will be spawned. -- - -- The amount of defending units is calculated by multiplying the amount of detected attacking ground units as part of the detected group - -- multiplied by the overhead parameter, and rounded up to the smallest integer. + -- The amount of defending units is calculated by multiplying the amount of detected attacking ground units as part of the detected group + -- multiplied by the overhead parameter, and rounded up to the smallest integer. -- -- Typically, for A2G defenses, values small than 1 will be used. Here are some good values for a couple of aircraft to support CAS operations: -- @@ -131462,7 +143889,7 @@ do -- AI_A2G_DISPATCHER -- - A-10A: 0.25 -- - SU-25T: 0.10 -- - -- So generically, the amount of missiles that an aircraft can take will determine its attacking effectiveness. The longer the range of the missiles, + -- So generically, the amount of missiles that an aircraft can take will determine its attacking effectiveness. The longer the range of the missiles, -- the less risk that the defender may be destroyed by the enemy, thus, the less aircraft needs to be activated in a defense. -- -- The **overhead value is set for a Squadron**, and can be **dynamically adjusted** during mission execution, so to adjust the defense overhead when the tactical situation changes. @@ -131474,13 +143901,13 @@ do -- AI_A2G_DISPATCHER -- -- Use the method @{#AI_A2G_DISPATCHER.SetSquadronEngageLimit}() to limit the amount of aircraft that will engage with the enemy, per squadron. -- - -- ## 4. Set the **fuel treshold**. + -- ## 4. Set the **fuel threshold**. -- - -- When aircraft get **out of fuel** to a certain %-tage, which is by default **15% (0.15)**, there are two possible actions that can be taken: + -- When an aircraft gets **out of fuel** with only a certain % of fuel left, which is **15% (0.15)** by default, there are two possible actions that can be taken: -- - The aircraft will go RTB, and will be replaced with a new aircraft if possible. -- - The aircraft will refuel at a tanker, if a tanker has been specified for the squadron. -- - -- Use the method @{#AI_A2G_DISPATCHER.SetSquadronFuelThreshold}() to set the **squadron fuel treshold** of the aircraft for all squadrons. + -- Use the method @{#AI_A2G_DISPATCHER.SetSquadronFuelThreshold}() to set the **squadron fuel threshold** of the aircraft for all squadrons. -- -- ## 6. Other configuration options -- @@ -131492,7 +143919,7 @@ do -- AI_A2G_DISPATCHER -- -- ## 10. Default settings. -- - -- Default settings configure the standard behaviour of the squadrons. + -- Default settings configure the standard behaviour of the squadrons. -- This section a good overview of the different parameters that setup the behaviour of **ALL** the squadrons by default. -- Note that default behaviour can be tweaked, and thus, this will change the behaviour of all the squadrons. -- Unless there is a specific behaviour set for a specific squadron, the default configured behaviour will be followed. @@ -131511,7 +143938,7 @@ do -- AI_A2G_DISPATCHER -- -- ## 10.2. Default landing behaviour. -- - -- The default landing behaviour is set to **near the airbase**, which means that returning airplanes will be despawned directly in the air by default. + -- The default landing behaviour is set to **near the airbase**, which means that returning aircraft will be despawned directly in the air by default. -- -- The default landing method can be set for ALL squadrons that don't have an individual landing method configured. -- @@ -131530,23 +143957,23 @@ do -- AI_A2G_DISPATCHER -- -- ## 10.4. Default **grouping**. -- - -- The default grouping is set to **one airplane**. That essentially means that there won't be any grouping applied by default. + -- The default grouping is set to **one aircraft**. That essentially means that there won't be any grouping applied by default. -- -- The default grouping value can be set for ALL squadrons that don't have an individual grouping value configured. -- - -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultGrouping}() to set the **default grouping** of spawned airplanes for all squadrons. + -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultGrouping}() to set the **default grouping** of spawned aircraft for all squadrons. -- - -- ## 10.5. Default RTB fuel treshold. + -- ## 10.5. Default RTB fuel threshold. -- - -- When an airplane gets **out of fuel** to a certain %-tage, which is **15% (0.15)**, it will go RTB, and will be replaced with a new airplane when applicable. + -- When an aircraft gets **out of fuel** with only a certain % of fuel left, which is **15% (0.15)** by default, it will go RTB, and will be replaced with a new aircraft when applicable. -- - -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultFuelThreshold}() to set the **default fuel treshold** of spawned airplanes for all squadrons. + -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultFuelThreshold}() to set the **default fuel threshold** of spawned aircraft for all squadrons. -- - -- ## 10.6. Default RTB damage treshold. + -- ## 10.6. Default RTB damage threshold. -- - -- When an airplane is **damaged** to a certain %-tage, which is **40% (0.40)**, it will go RTB, and will be replaced with a new airplane when applicable. + -- When an aircraft is **damaged** to a certain %, which is **40% (0.40)** by default, it will go RTB, and will be replaced with a new aircraft when applicable. -- - -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultDamageThreshold}() to set the **default damage treshold** of spawned airplanes for all squadrons. + -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultDamageThreshold}() to set the **default damage threshold** of spawned aircraft for all squadrons. -- -- ## 10.7. Default settings for **patrol**. -- @@ -131572,14 +143999,14 @@ do -- AI_A2G_DISPATCHER -- Note that you can still change the patrol limit and patrol time intervals for each patrol individually using -- the @{#AI_A2G_DISPATCHER.SetSquadronPatrolTimeInterval}() method. -- - -- ## 10.7.3. Default tanker for refuelling when executing CAP. + -- ## 10.7.3. Default tanker for refuelling when executing SEAD, BAI and CAS operations. -- - -- Instead of sending CAP to RTB when out of fuel, you can let CAP refuel in mid air using a tanker. - -- This greatly increases the efficiency of your CAP operations. + -- Instead of sending SEAD, BAI and CAS aircraft to RTB when out of fuel, you can let SEAD, BAI and CAS aircraft refuel in mid air using a tanker. + -- This greatly increases the efficiency of your SEAD, BAI and CAS operations. -- -- In the mission editor, setup a group with task Refuelling. A tanker unit of the correct coalition will be automatically selected. -- Then, use the method @{#AI_A2G_DISPATCHER.SetDefaultTanker}() to set the tanker for the dispatcher. - -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultFuelThreshold}() to set the %-tage left in the defender airplane tanks when a refuel action is needed. + -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultFuelThreshold}() to set the % left in the defender aircraft tanks when a refuel action is needed. -- -- When the tanker specified is alive and in the air, the tanker will be used for refuelling. -- @@ -131587,15 +144014,9 @@ do -- AI_A2G_DISPATCHER -- -- ![Banner Image](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_DISPATCHER-ME_11.JPG) -- - -- -- Define the CAP - -- A2ADispatcher:SetSquadron( "Sochi", AIRBASE.Caucasus.Sochi_Adler, { "SQ CCCP SU-34" }, 20 ) - -- A2ADispatcher:SetSquadronCap( "Sochi", ZONE:New( "PatrolZone" ), 4000, 8000, 600, 800, 1000, 1300 ) - -- A2ADispatcher:SetSquadronCapInterval("Sochi", 2, 30, 600, 1 ) - -- A2ADispatcher:SetSquadronGci( "Sochi", 900, 1200 ) - -- - -- -- Set the default tanker for refuelling to "Tanker", when the default fuel treshold has reached 90% fuel left. - -- A2ADispatcher:SetDefaultFuelThreshold( 0.9 ) - -- A2ADispatcher:SetDefaultTanker( "Tanker" ) + -- -- Set the default tanker for refuelling to "Tanker", when the default fuel threshold has reached 90% fuel left. + -- A2GDispatcher:SetDefaultFuelThreshold( 0.9 ) + -- A2GDispatcher:SetDefaultTanker( "Tanker" ) -- -- ## 10.8. Default settings for GCI. -- @@ -131628,10 +144049,10 @@ do -- AI_A2G_DISPATCHER -- ## 11. Airbase capture: -- -- Different squadrons can be located at one airbase. - -- If the airbase gets captured, that is, when there is an enemy unit near the airbase, and there aren't anymore friendlies at the airbase, the airbase will change coalition ownership. - -- As a result, the GCI and CAP will stop! - -- However, the squadron will still stay alive. Any airplane that is airborne will continue its operations until all airborne airplanes - -- of the squadron will be destroyed. This to keep consistency of air operations not to confuse the players. + -- If the airbase gets captured, that is when there is an enemy unit near the airbase and there are no friendlies at the airbase, the airbase will change coalition ownership. + -- As a result, further SEAD, BAI, and CAS operations from that airbase will stop. + -- However, the squadron will still stay alive. Any aircraft that is airborne will continue its operations until all airborne aircraft + -- of the squadron are destroyed. This is to keep consistency of air operations and avoid confusing players. -- -- -- @@ -131650,9 +144071,8 @@ do -- AI_A2G_DISPATCHER -- @field Core.Spawn#SPAWN Spawn The spawning object. -- @field #number ResourceCount The number of resources available. -- @field #list<#string> TemplatePrefixes The list of template prefixes. - -- @field #boolean Captured true if the squadron is captured. - -- @field #number Overhead The overhead for the squadron. - + -- @field #boolean Captured true if the squadron is captured. + -- @field #number Overhead The overhead for the squadron. --- List of defense coordinates. -- @type AI_A2G_DISPATCHER.DefenseCoordinates @@ -131661,14 +144081,14 @@ do -- AI_A2G_DISPATCHER --- @field #AI_A2G_DISPATCHER.DefenseCoordinates DefenseCoordinates AI_A2G_DISPATCHER.DefenseCoordinates = {} - --- Enumerator for spawns at airbases + --- Enumerator for spawns at airbases. -- @type AI_A2G_DISPATCHER.Takeoff -- @extends Wrapper.Group#GROUP.Takeoff --- @field #AI_A2G_DISPATCHER.Takeoff Takeoff AI_A2G_DISPATCHER.Takeoff = GROUP.Takeoff - --- Defnes Landing location. + --- Defines Landing location. -- @field #AI_A2G_DISPATCHER.Landing AI_A2G_DISPATCHER.Landing = { NearAirbase = 1, @@ -131676,7 +144096,7 @@ do -- AI_A2G_DISPATCHER AtEngineShutdown = 3, } - --- A defense queue item description + --- A defense queue item description. -- @type AI_A2G_DISPATCHER.DefenseQueueItem -- @field Squadron -- @field #AI_A2G_DISPATCHER.Squadron DefenderSquadron The squadron in the queue. @@ -131688,7 +144108,7 @@ do -- AI_A2G_DISPATCHER -- @field #string SquadronName The name of the squadron. --- Queue of planned defenses to be launched. - -- This queue exists because defenses must be launched on FARPS, or in the air, or on an airbase, or on carriers. + -- This queue exists because defenses must be launched from FARPs, in the air, from airbases, or from carriers. -- And some of these platforms have very limited amount of "launching" platforms. -- Therefore, this queue concept is introduced that queues each defender request. -- Depending on the location of the launching site, the queued defenders will be launched at varying time intervals. @@ -131699,8 +144119,7 @@ do -- AI_A2G_DISPATCHER --- @field #AI_A2G_DISPATCHER.DefenseQueue DefenseQueue AI_A2G_DISPATCHER.DefenseQueue = {} - - --- Defense approach types + --- Defense approach types. -- @type #AI_A2G_DISPATCHER.DefenseApproach AI_A2G_DISPATCHER.DefenseApproach = { Random = 1, @@ -131708,9 +144127,9 @@ do -- AI_A2G_DISPATCHER } --- AI_A2G_DISPATCHER constructor. - -- This is defining the A2G DISPATCHER for one coaliton. + -- This is defining the A2G DISPATCHER for one coalition. -- The Dispatcher works with a @{Functional.Detection#DETECTION_BASE} object that is taking of the detection of targets using the EWR units. - -- The Detection object is polymorphic, depending on the type of detection object choosen, the detection will work differently. + -- The Detection object is polymorphic, depending on the type of detection object chosen, the detection will work differently. -- @param #AI_A2G_DISPATCHER self -- @param Functional.Detection#DETECTION_BASE Detection The DETECTION object that will detects targets using the the Early Warning Radar network. -- @return #AI_A2G_DISPATCHER self @@ -131749,7 +144168,9 @@ do -- AI_A2G_DISPATCHER -- self.Detection:InitDetectRadar( false ) -- self.Detection:InitDetectVisual( true ) -- self.Detection:SetRefreshTimeInterval( 30 ) - + + self.SetSendPlayerMessages = false --flash messages to players + self:SetDefenseRadius() self:SetDefenseLimit( nil ) self:SetDefenseApproach( AI_A2G_DISPATCHER.DefenseApproach.Random ) @@ -131757,11 +144178,11 @@ do -- AI_A2G_DISPATCHER self:SetDisengageRadius( 300000 ) -- The default Disengage Radius is 300 km. self:SetDefaultTakeoff( AI_A2G_DISPATCHER.Takeoff.Air ) - self:SetDefaultTakeoffInAirAltitude( 500 ) -- Default takeoff is 500 meters above the ground. + self:SetDefaultTakeoffInAirAltitude( 500 ) -- Default takeoff is 500 meters above ground level (AGL). self:SetDefaultLanding( AI_A2G_DISPATCHER.Landing.NearAirbase ) self:SetDefaultOverhead( 1 ) self:SetDefaultGrouping( 1 ) - self:SetDefaultFuelThreshold( 0.15, 0 ) -- 15% of fuel remaining in the tank will trigger the airplane to return to base or refuel. + self:SetDefaultFuelThreshold( 0.15, 0 ) -- 15% of fuel remaining in the tank will trigger the aircraft to return to base or refuel. self:SetDefaultDamageThreshold( 0.4 ) -- When 40% of damage, go RTB. self:SetDefaultPatrolTimeInterval( 180, 600 ) -- Between 180 and 600 seconds. self:SetDefaultPatrolLimit( 1 ) -- Maximum one Patrol per squadron. @@ -131905,6 +144326,7 @@ do -- AI_A2G_DISPATCHER end + --- Locks the DefenseItem from being defended. -- @param #AI_A2G_DISPATCHER self -- @param #string DetectedItemIndex The index of the detected item. @@ -131978,7 +144400,7 @@ do -- AI_A2G_DISPATCHER self:I( "Captured " .. AirbaseName ) - -- Now search for all squadrons located at the airbase, and sanatize them. + -- Now search for all squadrons located at the airbase, and sanitize them. for SquadronName, Squadron in pairs( self.DefenderSquadrons ) do if Squadron.AirbaseName == AirbaseName then Squadron.ResourceCount = -999 -- The base has been captured, and the resources are eliminated. No more spawning. @@ -131988,12 +144410,14 @@ do -- AI_A2G_DISPATCHER end end + --- @param #AI_A2G_DISPATCHER self -- @param Core.Event#EVENTDATA EventData function AI_A2G_DISPATCHER:OnEventCrashOrDead( EventData ) self.Detection:ForgetDetectedUnit( EventData.IniUnitName ) end + --- @param #AI_A2G_DISPATCHER self -- @param Core.Event#EVENTDATA EventData function AI_A2G_DISPATCHER:OnEventLand( EventData ) @@ -132022,6 +144446,7 @@ do -- AI_A2G_DISPATCHER end end + --- @param #AI_A2G_DISPATCHER self -- @param Core.Event#EVENTDATA EventData function AI_A2G_DISPATCHER:OnEventEngineShutdown( EventData ) @@ -132043,6 +144468,7 @@ do -- AI_A2G_DISPATCHER end end + do -- Manage the defensive behaviour --- @param #AI_A2G_DISPATCHER self @@ -132052,16 +144478,19 @@ do -- AI_A2G_DISPATCHER self.DefenseCoordinates[DefenseCoordinateName] = DefenseCoordinate end + --- @param #AI_A2G_DISPATCHER self function AI_A2G_DISPATCHER:SetDefenseReactivityLow() self.DefenseReactivity = 0.05 end + --- @param #AI_A2G_DISPATCHER self function AI_A2G_DISPATCHER:SetDefenseReactivityMedium() self.DefenseReactivity = 0.15 end + --- @param #AI_A2G_DISPATCHER self function AI_A2G_DISPATCHER:SetDefenseReactivityHigh() self.DefenseReactivity = 0.5 @@ -132080,7 +144509,7 @@ do -- AI_A2G_DISPATCHER -- A2GDispatcher:SetDisengageRadius( 50000 ) -- -- -- Set 100km as the Disengage Radius. - -- A2GDispatcher:SetDisngageRadius() -- 300000 is the default value. + -- A2GDispatcher:SetDisengageRadius() -- 300000 is the default value. -- function AI_A2G_DISPATCHER:SetDisengageRadius( DisengageRadius ) @@ -132090,7 +144519,7 @@ do -- AI_A2G_DISPATCHER end - --- Define the defense radius to check if a target can be engaged by a squadron group for SEAD, CAS or BAI for defense. + --- Define the defense radius to check if a target can be engaged by a squadron group for SEAD, BAI, or CAS for defense. -- When targets are detected that are still really far off, you don't want the AI_A2G_DISPATCHER to launch defenders, as they might need to travel too far. -- You want it to wait until a certain defend radius is reached, which is calculated as: -- 1. the **distance of the closest airbase to target**, being smaller than the **Defend Radius**. @@ -132126,11 +144555,10 @@ do -- AI_A2G_DISPATCHER end - --- Define a border area to simulate a **cold war** scenario. -- A **cold war** is one where Patrol aircraft patrol their territory but will not attack enemy aircraft or launch GCI aircraft unless enemy aircraft enter their territory. In other words the EWR may detect an enemy aircraft but will only send aircraft to attack it if it crosses the border. -- A **hot war** is one where Patrol aircraft will intercept any detected enemy aircraft and GCI aircraft will launch against detected enemy aircraft without regard for territory. In other words if the ground radar can detect the enemy aircraft then it will send Patrol and GCI aircraft to attack it. - -- If it's a cold war then the **borders of red and blue territory** need to be defined using a @{zone} object derived from @{Core.Zone#ZONE_BASE}. This method needs to be used for this. + -- If it's a cold war then the **borders of red and blue territory** need to be defined using a @{Core.Zone} object derived from @{Core.Zone#ZONE_BASE}. This method needs to be used for this. -- If a hot war is chosen then **no borders** actually need to be defined using the helicopter units other than it makes it easier sometimes for the mission maker to envisage where the red and blue territories roughly are. In a hot war the borders are effectively defined by the ground based radar coverage of a coalition. Set the noborders parameter to 1 -- @param #AI_A2G_DISPATCHER self -- @param Core.Zone#ZONE_BASE BorderZone An object derived from ZONE_BASE, or a list of objects derived from ZONE_BASE. @@ -132151,7 +144579,6 @@ do -- AI_A2G_DISPATCHER -- local BorderZone2 = ZONE_POLYGON( "CCCP Border2", GROUP:FindByName( "CCCP Border2" ) ) -- The GROUP object is a late activate helicopter unit. -- A2GDispatcher:SetBorderZone( { BorderZone1, BorderZone2 } ) -- - -- function AI_A2G_DISPATCHER:SetBorderZone( BorderZone ) self.Detection:SetAcceptZones( BorderZone ) @@ -132159,6 +144586,7 @@ do -- AI_A2G_DISPATCHER return self end + --- Display a tactical report every 30 seconds about which aircraft are: -- * Patrolling -- * Engaging @@ -132185,18 +144613,18 @@ do -- AI_A2G_DISPATCHER end - --- Set the default damage treshold when defenders will RTB. - -- The default damage treshold is by default set to 40%, which means that when the airplane is 40% damaged, it will go RTB. + --- Set the default damage threshold when defenders will RTB. + -- The default damage threshold is by default set to 40%, which means that when the aircraft is 40% damaged, it will go RTB. -- @param #AI_A2G_DISPATCHER self - -- @param #number DamageThreshold A decimal number between 0 and 1, that expresses the %-tage of the damage treshold before going RTB. + -- @param #number DamageThreshold A decimal number between 0 and 1, that expresses the % of damage when the aircraft will go RTB. -- @return #AI_A2G_DISPATCHER -- @usage -- -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) -- - -- -- Now Setup the default damage treshold. - -- A2GDispatcher:SetDefaultDamageThreshold( 0.90 ) -- Go RTB when the airplane 90% damaged. + -- -- Now Setup the default damage threshold. + -- A2GDispatcher:SetDefaultDamageThreshold( 0.90 ) -- Go RTB when the aircraft is 90% damaged. -- function AI_A2G_DISPATCHER:SetDefaultDamageThreshold( DamageThreshold ) @@ -132310,36 +144738,42 @@ do -- AI_A2G_DISPATCHER return DefenderFriendliesNearBy end + --- -- @param #AI_A2G_DISPATCHER self function AI_A2G_DISPATCHER:GetDefenderTasks() return self.DefenderTasks or {} end + --- -- @param #AI_A2G_DISPATCHER self function AI_A2G_DISPATCHER:GetDefenderTask( Defender ) return self.DefenderTasks[Defender] end + --- -- @param #AI_A2G_DISPATCHER self function AI_A2G_DISPATCHER:GetDefenderTaskFsm( Defender ) return self:GetDefenderTask( Defender ).Fsm end + --- -- @param #AI_A2G_DISPATCHER self function AI_A2G_DISPATCHER:GetDefenderTaskTarget( Defender ) return self:GetDefenderTask( Defender ).Target end + --- -- @param #AI_A2G_DISPATCHER self function AI_A2G_DISPATCHER:GetDefenderTaskSquadronName( Defender ) return self:GetDefenderTask( Defender ).SquadronName end + --- -- @param #AI_A2G_DISPATCHER self function AI_A2G_DISPATCHER:ClearDefenderTask( Defender ) @@ -132356,6 +144790,7 @@ do -- AI_A2G_DISPATCHER return self end + --- -- @param #AI_A2G_DISPATCHER self function AI_A2G_DISPATCHER:ClearDefenderTaskTarget( Defender ) @@ -132525,6 +144960,7 @@ do -- AI_A2G_DISPATCHER return self end + --- Get an item from the Squadron table. -- @param #AI_A2G_DISPATCHER self -- @return #table @@ -132568,6 +145004,7 @@ do -- AI_A2G_DISPATCHER -- -- end + --- Check if the Squadron is visible before startup of the dispatcher. -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. @@ -132591,6 +145028,7 @@ do -- AI_A2G_DISPATCHER end + --- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. -- @param #number TakeoffInterval Only Takeoff new units each specified interval in seconds in 10 seconds steps. @@ -132612,7 +145050,6 @@ do -- AI_A2G_DISPATCHER end - --- Set the squadron patrol parameters for a specific task type. -- Mission designers should not use this method, instead use the below methods. This method is used by the below methods. @@ -132623,7 +145060,7 @@ do -- AI_A2G_DISPATCHER -- -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. - -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Note that a Patrol is a group, so can consist out of 1 to 4 airplanes. The default is 1 Patrol group. + -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Note that each Patrol is a group, and can consist of 1 to 4 aircraft. The default is 1 Patrol group. -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new Patrol will be spawned. The default is 180 seconds. -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new Patrol will be spawned. The default is 600 seconds. -- @param #number Probability Is not in use, you can skip this parameter. @@ -132666,13 +145103,12 @@ do -- AI_A2G_DISPATCHER end - --- Set the squadron Patrol parameters for SEAD tasks. -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. - -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Note that a Patrol is a group, so can consist out of 1 to 4 airplanes. The default is 1 Patrol group. - -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new Patrol will be spawned. The default is 180 seconds. - -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new Patrol will be spawned. The default is 600 seconds. + -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Each Patrol group can consist of 1 to 4 aircraft. The default is 1 Patrol group. + -- @param #number LowInterval (optional) The minimum time in seconds between new Patrols being spawned. The default is 180 seconds. + -- @param #number HighInterval (optional) The maximum ttime in seconds between new Patrols being spawned. The default is 600 seconds. -- @param #number Probability Is not in use, you can skip this parameter. -- @return #AI_A2G_DISPATCHER -- @usage @@ -132692,9 +145128,9 @@ do -- AI_A2G_DISPATCHER --- Set the squadron Patrol parameters for CAS tasks. -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. - -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Note that a Patrol is a group, so can consist out of 1 to 4 airplanes. The default is 1 Patrol group. - -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new Patrol will be spawned. The default is 180 seconds. - -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new Patrol will be spawned. The default is 600 seconds. + -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Each Patrol group can consist of 1 to 4 aircraft. The default is 1 Patrol group. + -- @param #number LowInterval (optional) The minimum time in seconds between new Patrols being spawned. The default is 180 seconds. + -- @param #number HighInterval (optional) The maximum time in seconds between new Patrols being spawned. The default is 600 seconds. -- @param #number Probability Is not in use, you can skip this parameter. -- @return #AI_A2G_DISPATCHER -- @usage @@ -132714,9 +145150,9 @@ do -- AI_A2G_DISPATCHER --- Set the squadron Patrol parameters for BAI tasks. -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. - -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Note that a Patrol is a group, so can consist out of 1 to 4 airplanes. The default is 1 Patrol group. - -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new Patrol will be spawned. The default is 180 seconds. - -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new Patrol will be spawned. The default is 600 seconds. + -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Each Patrol group can consist of 1 to 4 aircraft. The default is 1 Patrol group. + -- @param #number LowInterval (optional) The minimum time in seconds between new Patrols being spawned. The default is 180 seconds. + -- @param #number HighInterval (optional) The maximum time in seconds between new Patrols being spawned. The default is 600 seconds. -- @param #number Probability Is not in use, you can skip this parameter. -- @return #AI_A2G_DISPATCHER -- @usage @@ -132752,6 +145188,7 @@ do -- AI_A2G_DISPATCHER end end + --- -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. @@ -132804,6 +145241,7 @@ do -- AI_A2G_DISPATCHER return nil end + --- Set the squadron engage limit for a specific task type. -- Mission designers should not use this method, instead use the below methods. This method is used by the below methods. -- @@ -132836,8 +145274,6 @@ do -- AI_A2G_DISPATCHER end - - --- Set a squadron to engage for suppression of air defenses, when a defense point is under attack. -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. @@ -132874,6 +145310,7 @@ do -- AI_A2G_DISPATCHER return self end + --- Set a squadron to engage for suppression of air defenses, when a defense point is under attack. -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. @@ -132894,6 +145331,7 @@ do -- AI_A2G_DISPATCHER return self:SetSquadronSead2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, "RADIO" ) end + --- Set the squadron SEAD engage limit. -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. @@ -132910,15 +145348,13 @@ do -- AI_A2G_DISPATCHER self:SetSquadronEngageLimit( SquadronName, EngageLimit, "SEAD" ) end - - --- Set a Sead patrol for a Squadron. -- The Sead patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded. -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. - -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. + -- @param Core.Zone#ZONE_BASE Zone The @{Core.Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. -- @param #number PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed at which the cap can be executed. -- @param #number PatrolMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the cap can be executed. -- @param #number PatrolFloorAltitude (optional, default = 1000m ) The minimum altitude at which the cap can be executed. @@ -132967,7 +145403,7 @@ do -- AI_A2G_DISPATCHER -- The Sead patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded. -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. - -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. + -- @param Core.Zone#ZONE_BASE Zone The @{Core.Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. -- @param #number FloorAltitude (optional, default = 1000m ) The minimum altitude at which the cap can be executed. -- @param #number CeilingAltitude (optional, default = 1500m ) The maximum altitude at which the cap can be executed. -- @param #number PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed at which the cap can be executed. @@ -133025,6 +145461,7 @@ do -- AI_A2G_DISPATCHER return self end + --- Set a squadron to engage for close air support, when a defense point is under attack. -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. @@ -133068,7 +145505,7 @@ do -- AI_A2G_DISPATCHER -- The Cas patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded. -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. - -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. + -- @param Core.Zone#ZONE_BASE Zone The @{Core.Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. -- @param #number PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed at which the cap can be executed. -- @param #number PatrolMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the cap can be executed. -- @param #number PatrolFloorAltitude (optional, default = 1000m ) The minimum altitude at which the cap can be executed. @@ -133117,7 +145554,7 @@ do -- AI_A2G_DISPATCHER -- The Cas patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded. -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. - -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. + -- @param Core.Zone#ZONE_BASE Zone The @{Core.Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. -- @param #number FloorAltitude (optional, default = 1000m ) The minimum altitude at which the cap can be executed. -- @param #number CeilingAltitude (optional, default = 1500m ) The maximum altitude at which the cap can be executed. -- @param #number PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed at which the cap can be executed. @@ -133138,6 +145575,7 @@ do -- AI_A2G_DISPATCHER end + --- Set a squadron to engage for a battlefield area interdiction, when a defense point is under attack. -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. @@ -133174,6 +145612,7 @@ do -- AI_A2G_DISPATCHER return self end + --- Set a squadron to engage for a battlefield area interdiction, when a defense point is under attack. -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. @@ -133217,7 +145656,7 @@ do -- AI_A2G_DISPATCHER -- The Bai patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded. -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. - -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. + -- @param Core.Zone#ZONE_BASE Zone The @{Core.Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. -- @param #number PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed at which the cap can be executed. -- @param #number PatrolMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the cap can be executed. -- @param #number PatrolFloorAltitude (optional, default = 1000m ) The minimum altitude at which the cap can be executed. @@ -133261,11 +145700,12 @@ do -- AI_A2G_DISPATCHER self:I( { BAI = { Zone:GetName(), PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) end + --- Set a Bai patrol for a Squadron. -- The Bai patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded. -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. - -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. + -- @param Core.Zone#ZONE_BASE Zone The @{Core.Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. -- @param #number FloorAltitude (optional, default = 1000m ) The minimum altitude at which the cap can be executed. -- @param #number CeilingAltitude (optional, default = 1500m ) The maximum altitude at which the cap can be executed. -- @param #number PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed at which the cap can be executed. @@ -133287,9 +145727,9 @@ do -- AI_A2G_DISPATCHER end - --- Defines the default amount of extra planes that will take-off as part of the defense system. + --- Defines the default amount of extra planes that will takeoff as part of the defense system. -- @param #AI_A2G_DISPATCHER self - -- @param #number Overhead The %-tage of Units that dispatching command will allocate to intercept in surplus of detected amount of units. + -- @param #number Overhead The % of Units that dispatching command will allocate to intercept in surplus of detected amount of units. -- The default overhead is 1, so equal balance. The @{#AI_A2G_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength, -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance A2G missiles payload, may still be less effective than a F-15C with short missiles... -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes. @@ -133325,10 +145765,10 @@ do -- AI_A2G_DISPATCHER end - --- Defines the amount of extra planes that will take-off as part of the defense system. + --- Defines the amount of extra planes that will takeoff as part of the defense system. -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The name of the squadron. - -- @param #number Overhead The %-tage of Units that dispatching command will allocate to intercept in surplus of detected amount of units. + -- @param #number Overhead The % of Units that dispatching command will allocate to intercept in surplus of detected amount of units. -- The default overhead is 1, so equal balance. The @{#AI_A2G_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength, -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance A2G missiles payload, may still be less effective than a F-15C with short missiles... -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes. @@ -133368,7 +145808,7 @@ do -- AI_A2G_DISPATCHER --- Gets the overhead of planes as part of the defense system, in comparison with the attackers. -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The name of the squadron. - -- @return #number The %-tage of Units that dispatching command will allocate to intercept in surplus of detected amount of units. + -- @return #number The % of Units that dispatching command will allocate to intercept in surplus of detected amount of units. -- The default overhead is 1, so equal balance. The @{#AI_A2G_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength, -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance A2G missiles payload, may still be less effective than a F-15C with short missiles... -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes. @@ -133403,15 +145843,15 @@ do -- AI_A2G_DISPATCHER end - --- Sets the default grouping of new airplanes spawned. - -- Grouping will trigger how new airplanes will be grouped if more than one airplane is spawned for defense. + --- Sets the default grouping of new aircraft spawned. + -- Grouping will trigger how new aircraft will be grouped if more than one aircraft is spawned for defense. -- @param #AI_A2G_DISPATCHER self - -- @param #number Grouping The level of grouping that will be applied of the Patrol or GCI defenders. + -- @param #number Grouping The level of grouping that will be applied for the Patrol. -- @usage: -- -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) -- - -- -- Set a grouping by default per 2 airplanes. + -- -- Set a grouping by default per 2 aircraft. -- A2GDispatcher:SetDefaultGrouping( 2 ) -- -- @@ -133424,16 +145864,16 @@ do -- AI_A2G_DISPATCHER end - --- Sets the grouping of new airplanes spawned. - -- Grouping will trigger how new airplanes will be grouped if more than one airplane is spawned for defense. + --- Sets the Squadron grouping of new aircraft spawned. + -- Grouping will trigger how new aircraft will be grouped if more than one aircraft is spawned for defense. -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The name of the squadron. - -- @param #number Grouping The level of grouping that will be applied of the Patrol or GCI defenders. + -- @param #number Grouping The level of grouping that will be applied for a Patrol from the Squadron. -- @usage: -- -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) -- - -- -- Set a grouping per 2 airplanes. + -- -- Set a Squadron specific grouping per 2 aircraft. -- A2GDispatcher:SetSquadronGrouping( "SquadronName", 2 ) -- -- @@ -133471,23 +145911,23 @@ do -- AI_A2G_DISPATCHER end - --- Defines the default method at which new flights will spawn and take-off as part of the defense system. + --- Defines the default method at which new flights will spawn and takeoff as part of the defense system. -- @param #AI_A2G_DISPATCHER self -- @param #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. -- @usage: -- -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) -- - -- -- Let new flights by default take-off in the air. + -- -- Let new flights by default takeoff in the air. -- A2GDispatcher:SetDefaultTakeoff( AI_A2G_Dispatcher.Takeoff.Air ) -- - -- -- Let new flights by default take-off from the runway. + -- -- Let new flights by default takeoff from the runway. -- A2GDispatcher:SetDefaultTakeoff( AI_A2G_Dispatcher.Takeoff.Runway ) -- - -- -- Let new flights by default take-off from the airbase hot. + -- -- Let new flights by default takeoff from the airbase hot. -- A2GDispatcher:SetDefaultTakeoff( AI_A2G_Dispatcher.Takeoff.Hot ) -- - -- -- Let new flights by default take-off from the airbase cold. + -- -- Let new flights by default takeoff from the airbase cold. -- A2GDispatcher:SetDefaultTakeoff( AI_A2G_Dispatcher.Takeoff.Cold ) -- -- @@ -133500,7 +145940,7 @@ do -- AI_A2G_DISPATCHER return self end - --- Defines the method at which new flights will spawn and take-off as part of the defense system. + --- Defines the method at which new flights will spawn and takeoff as part of the defense system. -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The name of the squadron. -- @param #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. @@ -133508,16 +145948,16 @@ do -- AI_A2G_DISPATCHER -- -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) -- - -- -- Let new flights take-off in the air. + -- -- Let new flights takeoff in the air. -- A2GDispatcher:SetSquadronTakeoff( "SquadronName", AI_A2G_Dispatcher.Takeoff.Air ) -- - -- -- Let new flights take-off from the runway. + -- -- Let new flights takeoff from the runway. -- A2GDispatcher:SetSquadronTakeoff( "SquadronName", AI_A2G_Dispatcher.Takeoff.Runway ) -- - -- -- Let new flights take-off from the airbase hot. + -- -- Let new flights takeoff from the airbase hot. -- A2GDispatcher:SetSquadronTakeoff( "SquadronName", AI_A2G_Dispatcher.Takeoff.Hot ) -- - -- -- Let new flights take-off from the airbase cold. + -- -- Let new flights takeoff from the airbase cold. -- A2GDispatcher:SetSquadronTakeoff( "SquadronName", AI_A2G_Dispatcher.Takeoff.Cold ) -- -- @@ -133532,16 +145972,16 @@ do -- AI_A2G_DISPATCHER end - --- Gets the default method at which new flights will spawn and take-off as part of the defense system. + --- Gets the default method at which new flights will spawn and takeoff as part of the defense system. -- @param #AI_A2G_DISPATCHER self -- @return #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. -- @usage: -- -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) -- - -- -- Let new flights by default take-off in the air. + -- -- Let new flights by default takeoff in the air. -- local TakeoffMethod = A2GDispatcher:GetDefaultTakeoff() - -- if TakeOffMethod == , AI_A2G_Dispatcher.Takeoff.InAir then + -- if TakeoffMethod == , AI_A2G_Dispatcher.Takeoff.InAir then -- ... -- end -- @@ -133550,7 +145990,7 @@ do -- AI_A2G_DISPATCHER return self.DefenderDefault.Takeoff end - --- Gets the method at which new flights will spawn and take-off as part of the defense system. + --- Gets the method at which new flights will spawn and takeoff as part of the defense system. -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The name of the squadron. -- @return #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. @@ -133558,9 +145998,9 @@ do -- AI_A2G_DISPATCHER -- -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) -- - -- -- Let new flights take-off in the air. + -- -- Let new flights takeoff in the air. -- local TakeoffMethod = A2GDispatcher:GetSquadronTakeoff( "SquadronName" ) - -- if TakeOffMethod == , AI_A2G_Dispatcher.Takeoff.InAir then + -- if TakeoffMethod == , AI_A2G_Dispatcher.Takeoff.InAir then -- ... -- end -- @@ -133571,13 +146011,13 @@ do -- AI_A2G_DISPATCHER end - --- Sets flights to default take-off in the air, as part of the defense system. + --- Sets flights to default takeoff in the air, as part of the defense system. -- @param #AI_A2G_DISPATCHER self -- @usage: -- -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) -- - -- -- Let new flights by default take-off in the air. + -- -- Let new flights by default takeoff in the air. -- A2GDispatcher:SetDefaultTakeoffInAir() -- -- @return #AI_A2G_DISPATCHER @@ -133590,7 +146030,7 @@ do -- AI_A2G_DISPATCHER end - --- Sets flights to take-off in the air, as part of the defense system. + --- Sets flights to takeoff in the air, as part of the defense system. -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The name of the squadron. -- @param #number TakeoffAltitude (optional) The altitude in meters above the ground. If not given, the default takeoff altitude will be used. @@ -133598,7 +146038,7 @@ do -- AI_A2G_DISPATCHER -- -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) -- - -- -- Let new flights take-off in the air. + -- -- Let new flights takeoff in the air. -- A2GDispatcher:SetSquadronTakeoffInAir( "SquadronName" ) -- -- @return #AI_A2G_DISPATCHER @@ -133615,13 +146055,13 @@ do -- AI_A2G_DISPATCHER end - --- Sets flights by default to take-off from the runway, as part of the defense system. + --- Sets flights by default to takeoff from the runway, as part of the defense system. -- @param #AI_A2G_DISPATCHER self -- @usage: -- -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) -- - -- -- Let new flights by default take-off from the runway. + -- -- Let new flights by default takeoff from the runway. -- A2GDispatcher:SetDefaultTakeoffFromRunway() -- -- @return #AI_A2G_DISPATCHER @@ -133634,14 +146074,14 @@ do -- AI_A2G_DISPATCHER end - --- Sets flights to take-off from the runway, as part of the defense system. + --- Sets flights to takeoff from the runway, as part of the defense system. -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The name of the squadron. -- @usage: -- -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) -- - -- -- Let new flights take-off from the runway. + -- -- Let new flights takeoff from the runway. -- A2GDispatcher:SetSquadronTakeoffFromRunway( "SquadronName" ) -- -- @return #AI_A2G_DISPATCHER @@ -133654,13 +146094,13 @@ do -- AI_A2G_DISPATCHER end - --- Sets flights by default to take-off from the airbase at a hot location, as part of the defense system. + --- Sets flights by default to takeoff from the airbase at a hot location, as part of the defense system. -- @param #AI_A2G_DISPATCHER self -- @usage: -- -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) -- - -- -- Let new flights by default take-off at a hot parking spot. + -- -- Let new flights by default takeoff at a hot parking spot. -- A2GDispatcher:SetDefaultTakeoffFromParkingHot() -- -- @return #AI_A2G_DISPATCHER @@ -133672,14 +146112,14 @@ do -- AI_A2G_DISPATCHER return self end - --- Sets flights to take-off from the airbase at a hot location, as part of the defense system. + --- Sets flights to takeoff from the airbase at a hot location, as part of the defense system. -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The name of the squadron. -- @usage: -- -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) -- - -- -- Let new flights take-off in the air. + -- -- Let new flights takeoff in the air. -- A2GDispatcher:SetSquadronTakeoffFromParkingHot( "SquadronName" ) -- -- @return #AI_A2G_DISPATCHER @@ -133692,13 +146132,13 @@ do -- AI_A2G_DISPATCHER end - --- Sets flights to by default take-off from the airbase at a cold location, as part of the defense system. + --- Sets flights to by default takeoff from the airbase at a cold location, as part of the defense system. -- @param #AI_A2G_DISPATCHER self -- @usage: -- -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) -- - -- -- Let new flights take-off from a cold parking spot. + -- -- Let new flights takeoff from a cold parking spot. -- A2GDispatcher:SetDefaultTakeoffFromParkingCold() -- -- @return #AI_A2G_DISPATCHER @@ -133711,14 +146151,14 @@ do -- AI_A2G_DISPATCHER end - --- Sets flights to take-off from the airbase at a cold location, as part of the defense system. + --- Sets flights to takeoff from the airbase at a cold location, as part of the defense system. -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The name of the squadron. -- @usage: -- -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) -- - -- -- Let new flights take-off from a cold parking spot. + -- -- Let new flights takeoff from a cold parking spot. -- A2GDispatcher:SetSquadronTakeoffFromParkingCold( "SquadronName" ) -- -- @return #AI_A2G_DISPATCHER @@ -133731,9 +146171,9 @@ do -- AI_A2G_DISPATCHER end - --- Defines the default altitude where airplanes will spawn in the air and take-off as part of the defense system, when the take-off in the air method has been selected. + --- Defines the default altitude where aircraft will spawn in the air and takeoff as part of the defense system, when the takeoff in the air method has been selected. -- @param #AI_A2G_DISPATCHER self - -- @param #number TakeoffAltitude The altitude in meters above the ground. + -- @param #number TakeoffAltitude The altitude in meters above ground level (AGL). -- @usage: -- -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) @@ -133750,16 +146190,16 @@ do -- AI_A2G_DISPATCHER return self end - --- Defines the default altitude where airplanes will spawn in the air and take-off as part of the defense system, when the take-off in the air method has been selected. + --- Defines the default altitude where aircraft will spawn in the air and takeoff as part of the defense system, when the takeoff in the air method has been selected. -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The name of the squadron. - -- @param #number TakeoffAltitude The altitude in meters above the ground. + -- @param #number TakeoffAltitude The altitude in meters above ground level (AGL). -- @usage: -- -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) -- -- -- Set the default takeoff altitude when taking off in the air. - -- A2GDispatcher:SetSquadronTakeoffInAirAltitude( "SquadronName", 2000 ) -- This makes planes start at 2000 meters above the ground. + -- A2GDispatcher:SetSquadronTakeoffInAirAltitude( "SquadronName", 2000 ) -- This makes aircraft start at 2000 meters above ground level (AGL). -- -- @return #AI_A2G_DISPATCHER -- @@ -133832,7 +146272,7 @@ do -- AI_A2G_DISPATCHER -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) -- -- -- Let new flights by default despawn near the airbase when returning. - -- local LandingMethod = A2GDispatcher:GetDefaultLanding( AI_A2G_Dispatcher.Landing.NearAirbase ) + -- local LandingMethod = A2GDispatcher:GetDefaultLanding() -- if LandingMethod == AI_A2G_Dispatcher.Landing.NearAirbase then -- ... -- end @@ -133974,17 +146414,17 @@ do -- AI_A2G_DISPATCHER return self end - --- Set the default fuel treshold when defenders will RTB or Refuel in the air. - -- The fuel treshold is by default set to 15%, which means that an airplane will stay in the air until 15% of its fuel has been consumed. + --- Set the default fuel threshold when defenders will RTB or Refuel in the air. + -- The fuel threshold is by default set to 15%, which means that an aircraft will stay in the air until 15% of its fuel is remaining. -- @param #AI_A2G_DISPATCHER self - -- @param #number FuelThreshold A decimal number between 0 and 1, that expresses the %-tage of the treshold of fuel remaining in the tank when the plane will go RTB or Refuel. + -- @param #number FuelThreshold A decimal number between 0 and 1, that expresses the % of the threshold of fuel remaining in the tank when the plane will go RTB or Refuel. -- @return #AI_A2G_DISPATCHER -- @usage -- -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) -- - -- -- Now Setup the default fuel treshold. + -- -- Now Setup the default fuel threshold. -- A2GDispatcher:SetDefaultFuelThreshold( 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. -- function AI_A2G_DISPATCHER:SetDefaultFuelThreshold( FuelThreshold ) @@ -133995,18 +146435,18 @@ do -- AI_A2G_DISPATCHER end - --- Set the fuel treshold for the squadron when defenders will RTB or Refuel in the air. - -- The fuel treshold is by default set to 15%, which means that an airplane will stay in the air until 15% of its fuel has been consumed. + --- Set the fuel threshold for the squadron when defenders will RTB or Refuel in the air. + -- The fuel threshold is by default set to 15%, which means that an aircraft will stay in the air until 15% of its fuel is remaining. -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The name of the squadron. - -- @param #number FuelThreshold A decimal number between 0 and 1, that expresses the %-tage of the treshold of fuel remaining in the tank when the plane will go RTB or Refuel. + -- @param #number FuelThreshold A decimal number between 0 and 1, that expresses the % of the threshold of fuel remaining in the tank when the plane will go RTB or Refuel. -- @return #AI_A2G_DISPATCHER -- @usage -- -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) -- - -- -- Now Setup the default fuel treshold. + -- -- Now Setup the default fuel threshold. -- A2GDispatcher:SetSquadronRefuelThreshold( "SquadronName", 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. -- function AI_A2G_DISPATCHER:SetSquadronFuelThreshold( SquadronName, FuelThreshold ) @@ -134026,7 +146466,7 @@ do -- AI_A2G_DISPATCHER -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) -- - -- -- Now Setup the default fuel treshold. + -- -- Now Setup the default fuel threshold. -- A2GDispatcher:SetDefaultFuelThreshold( 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. -- -- -- Now Setup the default tanker. @@ -134049,7 +146489,7 @@ do -- AI_A2G_DISPATCHER -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) -- - -- -- Now Setup the squadron fuel treshold. + -- -- Now Setup the squadron fuel threshold. -- A2GDispatcher:SetSquadronRefuelThreshold( "SquadronName", 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. -- -- -- Now Setup the squadron tanker. @@ -134152,7 +146592,7 @@ do -- AI_A2G_DISPATCHER -- @param #AI_A2G_DISPATCHER self function AI_A2G_DISPATCHER:CountDefendersEngaged( AttackerDetection, AttackerCount ) - -- First, count the active AIGroups Units, targetting the DetectedSet + -- First, count the active AIGroups Units, targeting the DetectedSet local DefendersEngaged = 0 local DefendersTotal = 0 @@ -134449,7 +146889,9 @@ do -- AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) if Squadron then - Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", wheels up.", DefenderGroup ) + if self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", wheels up.", DefenderGroup ) + end AI_A2G_Fsm:Patrol() -- Engage on the TargetSetUnit end end @@ -134461,7 +146903,7 @@ do -- AI_A2G_DISPATCHER local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - if Squadron then + if Squadron and self.SetSendPlayerMessages then Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", patrolling.", DefenderGroup ) end @@ -134480,8 +146922,9 @@ do -- AI_A2G_DISPATCHER if Squadron and AttackSetUnit:Count() > 0 then local FirstUnit = AttackSetUnit:GetFirst() local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE - - Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", moving on to ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup ) + if self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", moving on to ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup ) + end end end @@ -134495,8 +146938,9 @@ do -- AI_A2G_DISPATCHER local FirstUnit = AttackSetUnit:GetFirst() if FirstUnit then local Coordinate = FirstUnit:GetCoordinate() - - Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", engaging ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup ) + if self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", engaging ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup ) + end end end @@ -134507,8 +146951,9 @@ do -- AI_A2G_DISPATCHER local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", returning to base.", DefenderGroup ) - + if self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", returning to base.", DefenderGroup ) + end Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) end @@ -134520,7 +146965,9 @@ do -- AI_A2G_DISPATCHER local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", lost control." ) + if self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", lost control." ) + end if DefenderGroup:IsAboveRunway() then Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) DefenderGroup:Destroy() @@ -134535,8 +146982,9 @@ do -- AI_A2G_DISPATCHER local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", landing at base.", DefenderGroup ) - + if self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", landing at base.", DefenderGroup ) + end if Action and Action == "Destroy" then Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) DefenderGroup:Destroy() @@ -134592,7 +147040,9 @@ do -- AI_A2G_DISPATCHER self:F( { DefenderTarget = DefenderTarget } ) if DefenderTarget then - Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", wheels up.", DefenderGroup ) + if self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", wheels up.", DefenderGroup ) + end AI_A2G_Fsm:EngageRoute( DefenderTarget.Set ) -- Engage on the TargetSetUnit end end @@ -134607,8 +147057,9 @@ do -- AI_A2G_DISPATCHER if Squadron then local FirstUnit = AttackSetUnit:GetFirst() local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE - - Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", on route to ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup ) + if self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", on route to ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup ) + end end self:GetParent(self).onafterEngageRoute( self, DefenderGroup, From, Event, To, AttackSetUnit ) end @@ -134623,8 +147074,9 @@ do -- AI_A2G_DISPATCHER local FirstUnit = AttackSetUnit:GetFirst() if FirstUnit then local Coordinate = FirstUnit:GetCoordinate() - - Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", engaging ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup ) + if self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", engaging ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup ) + end end end @@ -134634,8 +147086,9 @@ do -- AI_A2G_DISPATCHER local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", returning to base.", DefenderGroup ) - + if self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", returning to base.", DefenderGroup ) + end self:GetParent(self).onafterRTB( self, DefenderGroup, From, Event, To ) Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) @@ -134649,8 +147102,9 @@ do -- AI_A2G_DISPATCHER local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - --Dispatcher:MessageToPlayers( Squadron, "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " lost control." ) - + if self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " lost control." ) + end if DefenderGroup:IsAboveRunway() then Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) DefenderGroup:Destroy() @@ -134665,8 +147119,9 @@ do -- AI_A2G_DISPATCHER local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", landing at base.", DefenderGroup ) - + if self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", landing at base.", DefenderGroup ) + end if Action and Action == "Destroy" then Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) DefenderGroup:Destroy() @@ -134706,7 +147161,7 @@ do -- AI_A2G_DISPATCHER local EvaluateDistance = AttackCoordinate:Get2DDistance( DefenseCoordinate ) -- Now check if this coordinate is not in a danger zone, meaning, that the attack line is not crossing other coordinates. - -- (y1 – y2)x + (x2 – x1)y + (x1y2 – x2y1) = 0 + -- (y1 - y2)x + (x2 - x1)y + (x1y2 - x2y1) = 0 local c1 = DefenseCoordinate local c2 = AttackCoordinate @@ -134767,7 +147222,7 @@ do -- AI_A2G_DISPATCHER for DefenderID, DefenderGroup in pairs( DefenderFriendlies or {} ) do -- Here we check if the defenders have a defense line to the attackers. - -- If the attackers are behind enemy lines or too close to an other defense line; then don´t engage. + -- If the attackers are behind enemy lines or too close to an other defense line; then don't engage. local DefenseCoordinate = DefenderGroup:GetCoordinate() local HasDefenseLine = self:HasDefenseLine( DefenseCoordinate, DetectedItem ) @@ -135012,6 +147467,7 @@ do -- AI_A2G_DISPATCHER return ShortestDistance end + --- Assigns A2G AI Tasks in relation to the detected items. -- @param #AI_A2G_DISPATCHER self function AI_A2G_DISPATCHER:Order( DetectedItem ) @@ -135032,6 +147488,7 @@ do -- AI_A2G_DISPATCHER return ShortestDistance end + --- Shows the tactical display. -- @param #AI_A2G_DISPATCHER self function AI_A2G_DISPATCHER:ShowTacticalDisplay( Detection ) @@ -135070,7 +147527,7 @@ do -- AI_A2G_DISPATCHER -- Show tactical situation local ThreatLevel = DetectedItem.Set:CalculateThreatLevelA2G() - Report:Add( string.format( " - %1s%s ( %04s ): ( #%02d - %-4s ) %s" , ( DetectedItem.IsDetected == true ) and "!" or " ", DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Type or " --- ", string.rep( "■", ThreatLevel ) ) ) + Report:Add( string.format( " - %1s%s ( %04s ): ( #%02d - %-4s ) %s" , ( DetectedItem.IsDetected == true ) and "!" or " ", DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Type or " --- ", string.rep( "■", ThreatLevel ) ) ) for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do local Defender = Defender -- Wrapper.Group#GROUP if DefenderTask.Target and DefenderTask.Target.Index == DetectedItem.Index then @@ -135133,6 +147590,7 @@ do -- AI_A2G_DISPATCHER end + --- Assigns A2G AI Tasks in relation to the detected items. -- @param #AI_A2G_DISPATCHER self -- @param Functional.Detection#DETECTION_BASE Detection The detection created by the @{Functional.Detection#DETECTION_BASE} derived object. @@ -135287,7 +147745,7 @@ do -- AI_A2G_DISPATCHER if self.TacticalDisplay then -- Show tactical situation local ThreatLevel = DetectedItem.Set:CalculateThreatLevelA2G() - Report:Add( string.format( " - %1s%s ( %4s ): ( #%d - %4s ) %s" , ( DetectedItem.IsDetected == true ) and "!" or " ", DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Type or " --- ", string.rep( "■", ThreatLevel ) ) ) + Report:Add( string.format( " - %1s%s ( %4s ): ( #%d - %4s ) %s" , ( DetectedItem.IsDetected == true ) and "!" or " ", DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Type or " --- ", string.rep( "■", ThreatLevel ) ) ) for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do local Defender = Defender -- Wrapper.Group#GROUP if DefenderTask.Target and DefenderTask.Target.Index == DetectedItem.Index then @@ -135371,7 +147829,7 @@ do local PlayersCount = 0 if PlayersNearBy then - local DetectedTreatLevel = DetectedSet:CalculateThreatLevelA2G() + local DetectedThreatLevel = DetectedSet:CalculateThreatLevelA2G() for PlayerUnitName, PlayerUnitData in pairs( PlayersNearBy ) do local PlayerUnit = PlayerUnitData -- Wrapper.Unit#UNIT local PlayerName = PlayerUnit:GetPlayerName() @@ -135381,7 +147839,7 @@ do PlayersCount = PlayersCount + 1 local PlayerType = PlayerUnit:GetTypeName() PlayerTypes[PlayerName] = PlayerType - if DetectedTreatLevel < FriendlyUnitThreatLevel + 2 then + if DetectedThreatLevel < FriendlyUnitThreatLevel + 2 then end end end @@ -135417,7 +147875,7 @@ do local FriendliesCount = 0 if FriendlyUnitsNearBy then - local DetectedTreatLevel = DetectedSet:CalculateThreatLevelA2G() + local DetectedThreatLevel = DetectedSet:CalculateThreatLevelA2G() for FriendlyUnitName, FriendlyUnitData in pairs( FriendlyUnitsNearBy ) do local FriendlyUnit = FriendlyUnitData -- Wrapper.Unit#UNIT if FriendlyUnit:IsAirPlane() then @@ -135425,7 +147883,7 @@ do FriendliesCount = FriendliesCount + 1 local FriendlyType = FriendlyUnit:GetTypeName() FriendlyTypes[FriendlyType] = FriendlyTypes[FriendlyType] and ( FriendlyTypes[FriendlyType] + 1 ) or 1 - if DetectedTreatLevel < FriendlyUnitThreatLevel + 2 then + if DetectedThreatLevel < FriendlyUnitThreatLevel + 2 then end end end @@ -135456,16 +147914,44 @@ do local PatrolTaskType = PatrolTaskTypes[math.random(1,3)] self:Patrol( SquadronName, PatrolTaskType ) end - + + --- Set flashing player messages on or off + -- @param #AI_A2G_DISPATCHER self + -- @param #boolean onoff Set messages on (true) or off (false) + function AI_A2G_DISPATCHER:SetSendMessages( onoff ) + self.SetSendPlayerMessages = onoff + 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:** -- -- * Patrol AI airplanes within a given zone. -- * Trigger detected events when enemy airplanes are detected. --- * Manage a fuel treshold to RTB on time. +-- * Manage a fuel threshold to RTB on time. -- -- === -- @@ -135477,7 +147963,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) -- -- === -- @@ -135499,7 +147985,7 @@ end --- AI_PATROL_ZONE class -- @type AI_PATROL_ZONE -- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Wrapper.Controllable} patrolling. --- @field Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @field Core.Zone#ZONE_BASE PatrolZone The @{Core.Zone} where the patrol needs to be executed. -- @field DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. -- @field DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. -- @field DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h. @@ -135507,7 +147993,7 @@ end -- @field Core.Spawn#SPAWN CoordTest -- @extends Core.Fsm#FSM_CONTROLLABLE ---- Implements the core functions to patrol a @{Zone} by an AI @{Wrapper.Controllable} or @{Wrapper.Group}. +--- Implements the core functions to patrol a @{Core.Zone} by an AI @{Wrapper.Controllable} or @{Wrapper.Group}. -- -- ![Process](..\Presentations\AI_PATROL\Dia3.JPG) -- @@ -135533,8 +148019,8 @@ end -- -- ![Process](..\Presentations\AI_PATROL\Dia10.JPG) -- --- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. --- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- Until a fuel or damage threshold has been reached by the AI, or when the AI is commanded to RTB. +-- When the fuel threshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. -- -- ![Process](..\Presentations\AI_PATROL\Dia11.JPG) -- @@ -135562,7 +148048,7 @@ end -- * **RTB** ( Group ): Route the AI to the home base. -- * **Detect** ( Group ): The AI is detecting targets. -- * **Detected** ( Group ): The AI has detected new targets. --- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. +-- * **Status** ( Group ): The AI is checking status (fuel and damage). When the thresholds have been reached, the AI will RTB. -- -- ## 3. Set or Get the AI controllable -- @@ -135594,17 +148080,17 @@ end -- ## 6. Manage the "out of fuel" in the AI_PATROL_ZONE -- -- 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 AI is targetted to the AI_PATROL_ZONE. +-- 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 AI is targeted to the AI_PATROL_ZONE. -- Once the time is finished, the old AI will return to the base. --- Use the method @{#AI_PATROL_ZONE.ManageFuel}() to have this proces in place. +-- Use the method @{#AI_PATROL_ZONE.ManageFuel}() to have this process in place. -- -- ## 7. Manage "damage" behaviour of the AI in the AI_PATROL_ZONE -- -- When the AI is damaged, it is required that a new AIControllable is started. However, damage cannon be foreseen early on. --- Therefore, when the damage treshold is reached, the AI will return immediately to the home base (RTB). --- Use the method @{#AI_PATROL_ZONE.ManageDamage}() to have this proces in place. +-- Therefore, when the damage threshold is reached, the AI will return immediately to the home base (RTB). +-- Use the method @{#AI_PATROL_ZONE.ManageDamage}() to have this process in place. -- -- === -- @@ -135615,7 +148101,7 @@ AI_PATROL_ZONE = { --- Creates a new AI_PATROL_ZONE object -- @param #AI_PATROL_ZONE self --- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Core.Zone} where the patrol needs to be executed. -- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. -- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. -- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h. @@ -135631,27 +148117,27 @@ function AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltit -- Inherits from BASE local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- #AI_PATROL_ZONE - - + + self.PatrolZone = PatrolZone self.PatrolFloorAltitude = PatrolFloorAltitude self.PatrolCeilingAltitude = PatrolCeilingAltitude self.PatrolMinSpeed = PatrolMinSpeed self.PatrolMaxSpeed = PatrolMaxSpeed - + -- defafult PatrolAltType to "BARO" if not specified self.PatrolAltType = PatrolAltType or "BARO" - + self:SetRefreshTimeInterval( 30 ) - + self.CheckStatus = true - + self:ManageFuel( .2, 60 ) self:ManageDamage( 1 ) - + self.DetectedUnits = {} -- This table contains the targets detected during patrol. - + self:SetStartState( "None" ) self:AddTransition( "*", "Stop", "Stopped" ) @@ -135689,7 +148175,7 @@ function AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltit -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. - + --- Synchronous Event Trigger for Event Stop. -- @function [parent=#AI_PATROL_ZONE] Stop -- @param #AI_PATROL_ZONE self @@ -135717,7 +148203,7 @@ function AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltit -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. - + --- Synchronous Event Trigger for Event Start. -- @function [parent=#AI_PATROL_ZONE] Start -- @param #AI_PATROL_ZONE self @@ -135790,7 +148276,7 @@ function AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltit -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. - + --- Synchronous Event Trigger for Event Status. -- @function [parent=#AI_PATROL_ZONE] Status -- @param #AI_PATROL_ZONE self @@ -135874,7 +148360,7 @@ function AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltit -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. - + --- Synchronous Event Trigger for Event RTB. -- @function [parent=#AI_PATROL_ZONE] RTB -- @param #AI_PATROL_ZONE self @@ -135902,11 +148388,11 @@ function AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltit -- @param #string To The To State string. self:AddTransition( "*", "Reset", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. - + self:AddTransition( "*", "Eject", "*" ) self:AddTransition( "*", "Crash", "Crashed" ) self:AddTransition( "*", "PilotDead", "*" ) - + return self end @@ -135920,7 +148406,7 @@ end -- @return #AI_PATROL_ZONE self function AI_PATROL_ZONE:SetSpeed( PatrolMinSpeed, PatrolMaxSpeed ) self:F2( { PatrolMinSpeed, PatrolMaxSpeed } ) - + self.PatrolMinSpeed = PatrolMinSpeed self.PatrolMaxSpeed = PatrolMaxSpeed end @@ -135934,7 +148420,7 @@ end -- @return #AI_PATROL_ZONE self function AI_PATROL_ZONE:SetAltitude( PatrolFloorAltitude, PatrolCeilingAltitude ) self:F2( { PatrolFloorAltitude, PatrolCeilingAltitude } ) - + self.PatrolFloorAltitude = PatrolFloorAltitude self.PatrolCeilingAltitude = PatrolCeilingAltitude end @@ -136042,56 +148528,56 @@ function AI_PATROL_ZONE:ClearDetectedUnits() 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_PATROL_ZONE. +-- 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 targeted to the AI_PATROL_ZONE. -- Once the time is finished, the old AI will return to the base. -- @param #AI_PATROL_ZONE self --- @param #number PatrolFuelThresholdPercentage The treshold in percentage (between 0 and 1) when the AIControllable is considered to get out of fuel. +-- @param #number PatrolFuelThresholdPercentage The threshold in percentage (between 0 and 1) when the AIControllable is considered to get out of fuel. -- @param #number PatrolOutOfFuelOrbitTime The amount of seconds the out of fuel AIControllable will orbit before returning to the base. -- @return #AI_PATROL_ZONE self function AI_PATROL_ZONE:ManageFuel( PatrolFuelThresholdPercentage, PatrolOutOfFuelOrbitTime ) self.PatrolFuelThresholdPercentage = PatrolFuelThresholdPercentage self.PatrolOutOfFuelOrbitTime = PatrolOutOfFuelOrbitTime - + 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_PATROL_ZONE 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_PATROL_ZONE self function AI_PATROL_ZONE:ManageDamage( PatrolDamageThreshold ) self.PatrolManageDamage = true self.PatrolDamageThreshold = PatrolDamageThreshold - + return self end ---- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. +--- Defines a new patrol route using the @{#AI_PATROL_ZONE} parameters and settings. -- @param #AI_PATROL_ZONE self --- @return #AI_PATROL_ZONE self -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. +-- @return #AI_PATROL_ZONE self function AI_PATROL_ZONE:onafterStart( Controllable, From, Event, To ) self:F2() self:__Route( 1 ) -- Route to the patrol point. The asynchronous trigger is important, because a spawned group and units takes at least one second to come live. self:__Status( 60 ) -- Check status status every 30 seconds. self:SetDetectionActivated() - + self:HandleEvent( EVENTS.PilotDead, self.OnPilotDead ) self:HandleEvent( EVENTS.Crash, self.OnCrash ) self:HandleEvent( EVENTS.Ejection, self.OnEjection ) - + Controllable:OptionROEHoldFire() Controllable:OptionROTVertical() @@ -136128,12 +148614,12 @@ function AI_PATROL_ZONE:onafterDetect( Controllable, From, Event, To ) if TargetObject and TargetObject:isExist() and TargetObject.id_ < 50000000 then local TargetUnit = UNIT:Find( TargetObject ) - + -- Check that target is alive due to issue https://github.com/FlightControl-Master/MOOSE/issues/1234 if TargetUnit and TargetUnit:IsAlive() then - + local TargetUnitName = TargetUnit:GetName() - + if self.DetectionZone then if TargetUnit:IsInZone( self.DetectionZone ) then self:T( {"Detected ", TargetUnit } ) @@ -136148,13 +148634,13 @@ function AI_PATROL_ZONE:onafterDetect( Controllable, From, Event, To ) end Detected = true end - + end end end self:__Detect( -self.DetectInterval ) - + if Detected == true then self:__Detected( 1.5 ) end @@ -136162,7 +148648,7 @@ function AI_PATROL_ZONE:onafterDetect( Controllable, From, Event, To ) end --- @param Wrapper.Controllable#CONTROLLABLE AIControllable --- This statis method is called from the route path within the last task at the last waaypoint of the Controllable. +-- This static method is called from the route path within the last task at the last waypoint of the Controllable. -- Note that this method is required, as triggers the next route when patrolling for the Controllable. function AI_PATROL_ZONE:_NewPatrolRoute( AIControllable ) @@ -136171,7 +148657,7 @@ function AI_PATROL_ZONE:_NewPatrolRoute( AIControllable ) end ---- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. +--- Defines a new patrol route using the @{#AI_PATROL_ZONE} parameters and settings. -- @param #AI_PATROL_ZONE self -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. -- @param #string From The From State string. @@ -136186,11 +148672,11 @@ 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. - + local PatrolRoute = {} -- Calculate the current route point of the controllable as the start point of the route. @@ -136204,8 +148690,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( @@ -136219,8 +148706,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( @@ -136234,7 +148722,7 @@ function AI_PATROL_ZONE:onafterRoute( Controllable, From, Event, To ) end - --- Define a random point in the @{Zone}. The AI will fly to that point within the zone. + --- Define a random point in the @{Core.Zone}. The AI will fly to that point within the zone. --- Find a random 2D point in PatrolZone. local ToTargetVec2 = self.PatrolZone:GetRandomVec2() @@ -136331,9 +148819,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( @@ -136390,19 +148879,18 @@ function AI_PATROL_ZONE:OnPilotDead( EventData ) self:__PilotDead( 1, EventData ) end end ---- **AI** -- Perform Combat Air Patrolling (CAP) for airplanes. +--- **AI** - Perform Combat Air Patrolling (CAP) for airplanes. -- -- **Features:** -- -- * Patrol AI airplanes within a given zone. -- * Trigger detected events when enemy airplanes are detected. --- * Manage a fuel treshold to RTB on time. +-- * Manage a fuel threshold to RTB on time. -- * Engage the enemy when detected. --- -- -- === -- --- ### [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) -- -- === -- @@ -136411,27 +148899,25 @@ end -- === -- -- ### Author: **FlightControl** --- ### Contributions: +-- ### Contributions: -- -- * **[Quax](https://forums.eagle.ru/member.php?u=90530)**: Concept, Advice & Testing. -- * **[Pikey](https://forums.eagle.ru/member.php?u=62835)**: Concept, Advice & Testing. -- * **[Gunterlund](http://forums.eagle.ru:8080/member.php?u=75036)**: Test case revision. -- * **[Whisper](http://forums.eagle.ru/member.php?u=3829): Testing. --- * **[Delta99](https://forums.eagle.ru/member.php?u=125166): Testing. +-- * **[Delta99](https://forums.eagle.ru/member.php?u=125166): Testing. -- -- === -- --- @module AI.AI_Cap +-- @module AI.AI_CAP -- @image AI_Combat_Air_Patrol.JPG - --- @type AI_CAP_ZONE -- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Wrapper.Controllable} patrolling. --- @field Core.Zone#ZONE_BASE TargetZone The @{Zone} where the patrol needs to be executed. +-- @field Core.Zone#ZONE_BASE TargetZone The @{Core.Zone} where the patrol needs to be executed. -- @extends AI.AI_Patrol#AI_PATROL_ZONE - ---- Implements the core functions to patrol a @{Zone} by an AI @{Wrapper.Controllable} or @{Wrapper.Group} +--- Implements the core functions to patrol a @{Core.Zone} by an AI @{Wrapper.Controllable} or @{Wrapper.Group} -- and automatically engage any airborne enemies that are within a certain range or within a certain zone. -- -- ![Process](..\Presentations\AI_CAP\Dia3.JPG) @@ -136457,8 +148943,8 @@ end -- -- ![Process](..\Presentations\AI_CAP\Dia10.JPG) -- --- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. --- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- Until a fuel or damage threshold has been reached by the AI, or when the AI is commanded to RTB. +-- When the fuel threshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. -- -- ![Process](..\Presentations\AI_CAP\Dia13.JPG) -- @@ -136488,7 +148974,7 @@ end -- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. -- * **@{#AI_CAP_ZONE.Destroy}**: The AI has destroyed a bogey @{Wrapper.Unit}. -- * **@{#AI_CAP_ZONE.Destroyed}**: The AI has destroyed all bogeys @{Wrapper.Unit}s assigned in the CAS task. --- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. +-- * **Status** ( Group ): The AI is checking status (fuel and damage). When the thresholds have been reached, the AI will RTB. -- -- ## 3. Set the Range of Engagement -- @@ -136498,15 +148984,15 @@ end -- that will define when the AI will engage with the detected airborne enemy targets. -- The range can be beyond or smaller than the range of the Patrol Zone. -- The range is applied at the position of the AI. --- Use the method @{AI.AI_CAP#AI_CAP_ZONE.SetEngageRange}() to define that range. +-- Use the method @{#AI_CAP_ZONE.SetEngageRange}() to define that range. -- -- ## 4. Set the Zone of Engagement -- -- ![Zone](..\Presentations\AI_CAP\Dia12.JPG) -- --- An optional @{Zone} can be set, +-- An optional @{Core.Zone} can be set, -- that will define when the AI will engage with the detected airborne enemy targets. --- Use the method @{AI.AI_Cap#AI_CAP_ZONE.SetEngageZone}() to define that Zone. +-- Use the method @{#AI_CAP_ZONE.SetEngageZone}() to define that Zone. -- -- === -- @@ -136515,11 +149001,9 @@ AI_CAP_ZONE = { ClassName = "AI_CAP_ZONE", } - - --- Creates a new AI_CAP_ZONE object -- @param #AI_CAP_ZONE self --- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Core.Zone} where the patrol needs to be executed. -- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. -- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. -- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h. @@ -136533,7 +149017,7 @@ function AI_CAP_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude self.Accomplished = false self.Engaging = false - + self:AddTransition( { "Patrolling", "Engaging" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE. --- OnBefore Transition Handler for Event Engage. @@ -136544,7 +149028,7 @@ function AI_CAP_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude -- @param #string Event The Event string. -- @param #string To The To State string. -- @return #boolean Return false to cancel Transition. - + --- OnAfter Transition Handler for Event Engage. -- @function [parent=#AI_CAP_ZONE] OnAfterEngage -- @param #AI_CAP_ZONE self @@ -136552,11 +149036,11 @@ function AI_CAP_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. - + --- Synchronous Event Trigger for Event Engage. -- @function [parent=#AI_CAP_ZONE] Engage -- @param #AI_CAP_ZONE self - + --- Asynchronous Event Trigger for Event Engage. -- @function [parent=#AI_CAP_ZONE] __Engage -- @param #AI_CAP_ZONE self @@ -136580,7 +149064,7 @@ function AI_CAP_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude -- @param #string To The To State string. self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE. - + --- OnBefore Transition Handler for Event Fired. -- @function [parent=#AI_CAP_ZONE] OnBeforeFired -- @param #AI_CAP_ZONE self @@ -136589,7 +149073,7 @@ function AI_CAP_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude -- @param #string Event The Event string. -- @param #string To The To State string. -- @return #boolean Return false to cancel Transition. - + --- OnAfter Transition Handler for Event Fired. -- @function [parent=#AI_CAP_ZONE] OnAfterFired -- @param #AI_CAP_ZONE self @@ -136597,11 +149081,11 @@ function AI_CAP_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. - + --- Synchronous Event Trigger for Event Fired. -- @function [parent=#AI_CAP_ZONE] Fired -- @param #AI_CAP_ZONE self - + --- Asynchronous Event Trigger for Event Fired. -- @function [parent=#AI_CAP_ZONE] __Fired -- @param #AI_CAP_ZONE self @@ -136617,7 +149101,7 @@ function AI_CAP_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude -- @param #string Event The Event string. -- @param #string To The To State string. -- @return #boolean Return false to cancel Transition. - + --- OnAfter Transition Handler for Event Destroy. -- @function [parent=#AI_CAP_ZONE] OnAfterDestroy -- @param #AI_CAP_ZONE self @@ -136625,17 +149109,16 @@ function AI_CAP_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. - + --- Synchronous Event Trigger for Event Destroy. -- @function [parent=#AI_CAP_ZONE] Destroy -- @param #AI_CAP_ZONE self - + --- Asynchronous Event Trigger for Event Destroy. -- @function [parent=#AI_CAP_ZONE] __Destroy -- @param #AI_CAP_ZONE self -- @param #number Delay The delay in seconds. - self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE. --- OnBefore Transition Handler for Event Abort. @@ -136646,7 +149129,7 @@ function AI_CAP_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude -- @param #string Event The Event string. -- @param #string To The To State string. -- @return #boolean Return false to cancel Transition. - + --- OnAfter Transition Handler for Event Abort. -- @function [parent=#AI_CAP_ZONE] OnAfterAbort -- @param #AI_CAP_ZONE self @@ -136654,11 +149137,11 @@ function AI_CAP_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. - + --- Synchronous Event Trigger for Event Abort. -- @function [parent=#AI_CAP_ZONE] Abort -- @param #AI_CAP_ZONE self - + --- Asynchronous Event Trigger for Event Abort. -- @function [parent=#AI_CAP_ZONE] __Abort -- @param #AI_CAP_ZONE self @@ -136674,7 +149157,7 @@ function AI_CAP_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude -- @param #string Event The Event string. -- @param #string To The To State string. -- @return #boolean Return false to cancel Transition. - + --- OnAfter Transition Handler for Event Accomplish. -- @function [parent=#AI_CAP_ZONE] OnAfterAccomplish -- @param #AI_CAP_ZONE self @@ -136682,11 +149165,11 @@ function AI_CAP_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. - + --- Synchronous Event Trigger for Event Accomplish. -- @function [parent=#AI_CAP_ZONE] Accomplish -- @param #AI_CAP_ZONE self - + --- Asynchronous Event Trigger for Event Accomplish. -- @function [parent=#AI_CAP_ZONE] __Accomplish -- @param #AI_CAP_ZONE self @@ -136695,7 +149178,6 @@ function AI_CAP_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude return self end - --- Set the Engage Zone which defines where the AI will engage bogies. -- @param #AI_CAP_ZONE self -- @param Core.Zone#ZONE EngageZone The zone where the AI is performing CAP. @@ -136703,7 +149185,7 @@ end function AI_CAP_ZONE:SetEngageZone( EngageZone ) self:F2() - if EngageZone then + if EngageZone then self.EngageZone = EngageZone else self.EngageZone = nil @@ -136738,7 +149220,6 @@ function AI_CAP_ZONE:onafterStart( Controllable, From, Event, To ) end - --- @param AI.AI_CAP#AI_CAP_ZONE -- @param Wrapper.Group#GROUP EngageGroup function AI_CAP_ZONE.EngageRoute( EngageGroup, Fsm ) @@ -136750,8 +149231,6 @@ function AI_CAP_ZONE.EngageRoute( EngageGroup, Fsm ) end end - - --- @param #AI_CAP_ZONE self -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. -- @param #string From The From State string. @@ -136772,11 +149251,11 @@ end function AI_CAP_ZONE:onafterDetected( Controllable, From, Event, To ) if From ~= "Engaging" then - + local Engage = false - + for DetectedUnit, Detected in pairs( self.DetectedUnits ) do - + local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT self:T( DetectedUnit ) if DetectedUnit:IsAlive() and DetectedUnit:IsAir() then @@ -136784,7 +149263,7 @@ function AI_CAP_ZONE:onafterDetected( Controllable, From, Event, To ) break end end - + if Engage == true then self:F( 'Detected -> Engaging' ) self:__Engage( 1 ) @@ -136792,7 +149271,6 @@ function AI_CAP_ZONE:onafterDetected( Controllable, From, Event, To ) end end - --- @param #AI_CAP_ZONE self -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. -- @param #string From The From State string. @@ -136803,9 +149281,6 @@ function AI_CAP_ZONE:onafterAbort( Controllable, From, Event, To ) self:__Route( 1 ) end - - - --- @param #AI_CAP_ZONE self -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. -- @param #string From The From State string. @@ -136819,22 +149294,23 @@ 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( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToEngageZoneSpeed, - true + local CurrentRoutePoint = CurrentPointVec3:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToEngageZoneSpeed, + true ) - + EngageRoute[#EngageRoute+1] = CurrentRoutePoint - --- Find a random 2D point in PatrolZone. local ToTargetVec2 = self.PatrolZone:GetRandomVec2() self:T2( ToTargetVec2 ) @@ -136843,17 +149319,17 @@ function AI_CAP_ZONE:onafterEngage( Controllable, From, Event, To ) local ToTargetAltitude = math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) self:T2( { self.PatrolMinSpeed, self.PatrolMaxSpeed, ToTargetSpeed } ) - + --- Obtain a 3D @{Point} from the 2D point + altitude. local ToTargetPointVec3 = POINT_VEC3:New( ToTargetVec2.x, ToTargetAltitude, ToTargetVec2.y ) - + --- Create a route point of type air. - local ToPatrolRoutePoint = ToTargetPointVec3:WaypointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true + local ToPatrolRoutePoint = ToTargetPointVec3:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true ) EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint @@ -136872,7 +149348,7 @@ function AI_CAP_ZONE:onafterEngage( Controllable, From, Event, To ) self:F( {"Within Zone and Engaging ", DetectedUnit } ) AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit ) end - else + else if self.EngageRange then if DetectedUnit:GetPointVec3():Get2DDistance(Controllable:GetPointVec3() ) <= self.EngageRange then self:F( {"Within Range and Engaging", DetectedUnit } ) @@ -136896,12 +149372,12 @@ function AI_CAP_ZONE:onafterEngage( Controllable, From, Event, To ) AttackTasks[#AttackTasks+1] = Controllable:TaskFunction( "AI_CAP_ZONE.EngageRoute", self ) EngageRoute[1].task = Controllable:TaskCombo( AttackTasks ) - + self:SetDetectionDeactivated() end - + Controllable:Route( EngageRoute, 0.5 ) - + end end @@ -136939,7 +149415,7 @@ function AI_CAP_ZONE:OnEventDead( EventData ) end end end ---- **AI** -- Perform Close Air Support (CAS) near friendlies. +--- **AI** - Perform Close Air Support (CAS) near friendlies. -- -- **Features:** -- @@ -136952,7 +149428,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) -- -- === -- @@ -136969,16 +149445,16 @@ end -- -- === -- --- @module AI.AI_Cas +-- @module AI.AI_CAS -- @image AI_Close_Air_Support.JPG --- AI_CAS_ZONE class -- @type AI_CAS_ZONE -- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Wrapper.Controllable} patrolling. --- @field Core.Zone#ZONE_BASE TargetZone The @{Zone} where the patrol needs to be executed. +-- @field Core.Zone#ZONE_BASE TargetZone The @{Core.Zone} where the patrol needs to be executed. -- @extends AI.AI_Patrol#AI_PATROL_ZONE ---- Implements the core functions to provide Close Air Support in an Engage @{Zone} by an AIR @{Wrapper.Controllable} or @{Wrapper.Group}. +--- Implements the core functions to provide Close Air Support in an Engage @{Core.Zone} by an AIR @{Wrapper.Controllable} or @{Wrapper.Group}. -- The AI_CAS_ZONE runs a process. It holds an AI in a Patrol Zone and when the AI is commanded to engage, it will fly to an Engage Zone. -- -- ![HoldAndEngage](..\Presentations\AI_CAS\Dia3.JPG) @@ -136990,7 +149466,7 @@ end -- Upon started, The AI will **Route** itself towards the random 3D point within a patrol zone, -- using a random speed within the given altitude and speed limits. -- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. --- This cycle will continue until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. +-- This cycle will continue until a fuel or damage threshold has been reached by the AI, or when the AI is commanded to RTB. -- -- ![Route Event](..\Presentations\AI_CAS\Dia5.JPG) -- @@ -137028,7 +149504,7 @@ end -- It will keep patrolling there, until it is notified to RTB or move to another CAS Zone. -- It can be notified to go RTB through the **RTB** event. -- --- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- When the fuel threshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. -- -- ![Engage Event](..\Presentations\AI_CAS\Dia12.JPG) -- @@ -137058,7 +149534,7 @@ end -- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. -- * **@{#AI_CAS_ZONE.Destroy}**: The AI has destroyed a target @{Wrapper.Unit}. -- * **@{#AI_CAS_ZONE.Destroyed}**: The AI has destroyed all target @{Wrapper.Unit}s assigned in the CAS task. --- * **Status**: The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. +-- * **Status**: The AI is checking status (fuel and damage). When the thresholds have been reached, the AI will RTB. -- -- === -- @@ -137071,7 +149547,7 @@ AI_CAS_ZONE = { --- Creates a new AI_CAS_ZONE object -- @param #AI_CAS_ZONE self --- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Core.Zone} where the patrol needs to be executed. -- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. -- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. -- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h. @@ -137400,8 +149876,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( @@ -137437,7 +149913,7 @@ function AI_CAS_ZONE:onafterEngage( Controllable, From, Event, To, AttackTasks[#AttackTasks+1] = Controllable:TaskFunction( "AI_CAS_ZONE.EngageRoute", self ) EngageRoute[#EngageRoute].task = Controllable:TaskCombo( AttackTasks ) - --- Define a random point in the @{Zone}. The AI will fly to that point within the zone. + --- Define a random point in the @{Core.Zone}. The AI will fly to that point within the zone. --- Find a random 2D point in EngageZone. local ToTargetVec2 = self.EngageZone:GetRandomVec2() @@ -137461,7 +149937,7 @@ function AI_CAS_ZONE:onafterEngage( Controllable, From, Event, To, self:SetRefreshTimeInterval( 2 ) self:SetDetectionActivated() - self:__Target( -2 ) -- Start Targetting + self:__Target( -2 ) -- Start targeting end end @@ -137504,7 +149980,7 @@ function AI_CAS_ZONE:OnEventDead( EventData ) end ---- **AI** -- Peform Battlefield Area Interdiction (BAI) within an engagement zone. +--- **AI** - Peform Battlefield Area Interdiction (BAI) within an engagement zone. -- -- **Features:** -- @@ -137532,17 +150008,17 @@ end -- -- === -- --- @module AI.AI_Bai +-- @module AI.AI_BAI -- @image AI_Battlefield_Air_Interdiction.JPG --- AI_BAI_ZONE class -- @type AI_BAI_ZONE -- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Wrapper.Controllable} patrolling. --- @field Core.Zone#ZONE_BASE TargetZone The @{Zone} where the patrol needs to be executed. +-- @field Core.Zone#ZONE_BASE TargetZone The @{Core.Zone} where the patrol needs to be executed. -- @extends AI.AI_Patrol#AI_PATROL_ZONE ---- Implements the core functions to provide BattleGround Air Interdiction in an Engage @{Zone} by an AIR @{Wrapper.Controllable} or @{Wrapper.Group}. +--- Implements the core functions to provide BattleGround Air Interdiction in an Engage @{Core.Zone} by an AIR @{Wrapper.Controllable} or @{Wrapper.Group}. -- -- The AI_BAI_ZONE runs a process. It holds an AI in a Patrol Zone and when the AI is commanded to engage, it will fly to an Engage Zone. -- @@ -137555,7 +150031,7 @@ end -- Upon started, The AI will **Route** itself towards the random 3D point within a patrol zone, -- using a random speed within the given altitude and speed limits. -- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. --- This cycle will continue until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. +-- This cycle will continue until a fuel or damage threshold has been reached by the AI, or when the AI is commanded to RTB. -- -- ![Route Event](..\Presentations\AI_BAI\Dia5.JPG) -- @@ -137593,7 +150069,7 @@ end -- It will keep patrolling there, until it is notified to RTB or move to another BOMB Zone. -- It can be notified to go RTB through the **RTB** event. -- --- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- When the fuel threshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. -- -- ![Engage Event](..\Presentations\AI_BAI\Dia12.JPG) -- @@ -137623,7 +150099,7 @@ end -- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. -- * **@{#AI_BAI_ZONE.Destroy}**: The AI has destroyed a target @{Wrapper.Unit}. -- * **@{#AI_BAI_ZONE.Destroyed}**: The AI has destroyed all target @{Wrapper.Unit}s assigned in the BOMB task. --- * **Status**: The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. +-- * **Status**: The AI is checking status (fuel and damage). When the thresholds have been reached, the AI will RTB. -- -- ## 3. Modify the Engage Zone behaviour to pinpoint a **map object** or **scenery object** -- @@ -137648,7 +150124,7 @@ AI_BAI_ZONE = { --- Creates a new AI_BAI_ZONE object -- @param #AI_BAI_ZONE self --- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Core.Zone} where the patrol needs to be executed. -- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. -- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. -- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h. @@ -138021,8 +150497,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( @@ -138072,7 +150548,7 @@ function AI_BAI_ZONE:onafterEngage( Controllable, From, Event, To, EngageRoute[#EngageRoute].task = Controllable:TaskCombo( AttackTasks ) - --- Define a random point in the @{Zone}. The AI will fly to that point within the zone. + --- Define a random point in the @{Core.Zone}. The AI will fly to that point within the zone. --- Find a random 2D point in EngageZone. local ToTargetVec2 = self.EngageZone:GetRandomVec2() @@ -138108,7 +150584,7 @@ function AI_BAI_ZONE:onafterEngage( Controllable, From, Event, To, self:SetRefreshTimeInterval( 2 ) self:SetDetectionActivated() - self:__Target( -2 ) -- Start Targetting + self:__Target( -2 ) -- Start targeting end end @@ -138151,7 +150627,7 @@ function AI_BAI_ZONE:OnEventDead( EventData ) end ---- **AI** -- Build large airborne formations of aircraft. +--- **AI** - Build large airborne formations of aircraft. -- -- **Features:** -- @@ -138194,7 +150670,7 @@ end --- Build large formations, make AI follow a @{Wrapper.Client#CLIENT} (player) leader or a @{Wrapper.Unit#UNIT} (AI) leader. -- --- AI_FORMATION makes AI @{GROUP}s fly in formation of various compositions. +-- AI_FORMATION makes AI @{Wrapper.Group#GROUP}s fly in formation of various compositions. -- The AI_FORMATION class models formations in a different manner than the internal DCS formation logic!!! -- The purpose of the class is to: -- @@ -139293,8 +151769,8 @@ end -- @param DCS#Vec3 CV2 Vec3 function AI_FORMATION:FollowMe(FollowGroup, ClientUnit, CT1, CV1, CT2, CV2) - if FollowGroup:GetState( FollowGroup, "Mode" ) == self.__Enum.Mode.Formation then - + if FollowGroup:GetState( FollowGroup, "Mode" ) == self.__Enum.Mode.Formation and not self:Is("Stopped") then + self:T({Mode=FollowGroup:GetState( FollowGroup, "Mode" )}) FollowGroup:OptionROTEvadeFire() @@ -139434,7 +151910,7 @@ function AI_FORMATION:FollowMe(FollowGroup, ClientUnit, CT1, CV1, CT2, CV2) end end end ---- **Functional** -- Taking the lead of AI escorting your flight or of other AI. +--- **AI** - Taking the lead of AI escorting your flight or of other AI. -- -- === -- @@ -141616,7 +154092,7 @@ function AI_ESCORT:_FlightReportTargetsScheduler() end ---- **Functional** -- Taking the lead of AI escorting your flight or of other AI, upon request using the menu. +--- **AI** - Taking the lead of AI escorting your flight or of other AI, upon request using the menu. -- -- === -- @@ -141761,7 +154237,7 @@ end -- -- === -- --- @module AI.AI_Escort +-- @module AI.AI_Escort_Request -- @image Escorting.JPG @@ -142130,7 +154606,7 @@ end -- -- === -- --- @module AI.AI_ESCORT_DISPATCHER_REQUEST +-- @module AI.AI_Escort_Dispatcher_Request -- @image MOOSE.JPG @@ -144610,7 +157086,7 @@ function AI_CARGO_AIRPLANE:onafterHome(Airplane, From, Event, To, Coordinate, Sp end end ---- **AI** -- (R2.5.1) - Models the intelligent transportation of infantry and other cargo. +--- **AI** - Models the intelligent transportation of infantry and other cargo. -- -- === -- @@ -144619,7 +157095,7 @@ end -- === -- -- @module AI.AI_Cargo_Ship --- @image AI_Cargo_Dispatching_For_Ship.JPG +-- @image AI_Cargo_Dispatcher.JPG --- @type AI_CARGO_SHIP -- @extends AI.AI_Cargo#AI_CARGO @@ -144658,12 +157134,12 @@ end -- -- ## Cargo deployment. -- --- Using the @{AI_CARGO_SHIP.Deploy}() method, you are able to direct the Ship towards a Deploy zone to unboard/unload the cargo at the +-- Using the @{#AI_CARGO_SHIP.Deploy}() method, you are able to direct the Ship towards a Deploy zone to unboard/unload the cargo at the -- specified coordinate. The Ship will follow the Shipping Lane to ensure consistent cargo transportation within the simulation environment. -- -- ## Cargo pickup. -- --- Using the @{AI_CARGO_SHIP.Pickup}() method, you are able to direct the Ship towards a Pickup zone to board/load the cargo at the specified +-- Using the @{#AI_CARGO_SHIP.Pickup}() method, you are able to direct the Ship towards a Pickup zone to board/load the cargo at the specified -- coordinate. The Ship will follow the Shipping Lane to ensure consistent cargo transportation within the simulation environment. -- -- @@ -146495,7 +158971,7 @@ function AI_CARGO_DISPATCHER_APC:SetDeployOffRoad(Offroad, Formation) self.deployFormation=Formation or ENUMS.Formation.Vehicle.OffRoad return self -end--- **AI** -- (2.4) - Models the intelligent transportation of infantry and other cargo using Helicopters. +end--- **AI** - Models the intelligent transportation of infantry and other cargo using Helicopters. -- -- ## Features: -- @@ -146671,8 +159147,8 @@ function AI_CARGO_DISPATCHER_HELICOPTER:New( HelicopterSet, CargoSet, PickupZone self:SetPickupSpeed( 350, 150 ) self:SetDeploySpeed( 350, 150 ) - self:SetPickupRadius( 0, 0 ) - self:SetDeployRadius( 0, 0 ) + self:SetPickupRadius( 40, 12 ) + self:SetDeployRadius( 40, 12 ) self:SetPickupHeight( 500, 200 ) self:SetDeployHeight( 500, 200 ) @@ -146683,10 +159159,13 @@ end function AI_CARGO_DISPATCHER_HELICOPTER:AICargo( Helicopter, CargoSet ) - return AI_CARGO_HELICOPTER:New( Helicopter, CargoSet ) + local dispatcher = AI_CARGO_HELICOPTER:New( Helicopter, CargoSet ) + dispatcher:SetLandingSpeedAndHeight(27, 6) + return dispatcher + end ---- **AI** -- (R2.4) - Models the intelligent transportation of infantry and other cargo using Planes. +--- **AI** - Models the intelligent transportation of infantry and other cargo using Planes. -- -- ## Features: -- @@ -146850,7 +159329,7 @@ function AI_CARGO_DISPATCHER_AIRPLANE:AICargo( Airplane, CargoSet ) return AI_CARGO_AIRPLANE:New( Airplane, CargoSet ) end ---- **AI** -- (2.5.1) - Models the intelligent transportation of infantry and other cargo using Ships +--- **AI** - Models the intelligent transportation of infantry and other cargo using Ships. -- -- ## Features: -- @@ -146873,7 +159352,7 @@ end -- === -- -- @module AI.AI_Cargo_Dispatcher_Ship --- @image AI_Cargo_Dispatching_For_Ship.JPG +-- @image AI_Cargo_Dispatcher.JPG --- @type AI_CARGO_DISPATCHER_SHIP -- @extends AI.AI_Cargo_Dispatcher#AI_CARGO_DISPATCHER @@ -146889,14 +159368,14 @@ end -- -- This will be particularly helpful in order to determine how to **Tailor the different cargo handling events**. -- --- The AI_CARGO_DISPATCHER_SHIP class uses the @{Cargo.Cargo} capabilities within the MOOSE framwork. +-- The AI_CARGO_DISPATCHER_SHIP class uses the @{Cargo.Cargo} capabilities within the MOOSE framework. -- Also ensure that you fully understand how to declare and setup Cargo objects within the MOOSE framework before using this class. -- CARGO derived objects must generally be declared within the mission to make the AI_CARGO_DISPATCHER_SHIP object recognize the cargo. -- -- -- # 1) AI_CARGO_DISPATCHER_SHIP constructor. -- --- * @{AI_CARGO_DISPATCHER_SHIP.New}(): Creates a new AI_CARGO_DISPATCHER_SHIP object. +-- * @{#AI_CARGO_DISPATCHER_SHIP.New}(): Creates a new AI_CARGO_DISPATCHER_SHIP object. -- -- --- -- @@ -147121,7 +159600,7 @@ end--- (SP) (MP) (FSM) Accept or reject process for player (task) assignments. -- -- === -- --- @module Actions.Assign +-- @module Actions.Act_Assign -- @image MOOSE.JPG @@ -147393,7 +159872,7 @@ end -- ACT_ASSIGN_MENU_ACCEPT -- -- # 1) @{#ACT_ROUTE_ZONE} class, extends @{Core.Fsm.Route#ACT_ROUTE} -- --- The ACT_ROUTE_ZONE class implements the core functions to route an AIR @{Wrapper.Controllable} player @{Wrapper.Unit} to a @{Zone}. +-- The ACT_ROUTE_ZONE class implements the core functions to route an AIR @{Wrapper.Controllable} player @{Wrapper.Unit} to a @{Core.Zone}. -- The player receives on perioding times messages with the coordinates of the route to follow. -- Upon arrival at the zone, a confirmation of arrival is sent, and the process will be ended. -- @@ -147403,7 +159882,7 @@ end -- ACT_ASSIGN_MENU_ACCEPT -- -- === -- --- @module Actions.Route +-- @module Actions.Act_Route -- @image MOOSE.JPG @@ -147807,13 +160286,13 @@ do -- ACT_ROUTE_ZONE end end -- ACT_ROUTE_ZONE ---- **Actions** - ACT_ACCOUNT_ classes **account for** (detect, count & report) various DCS events occuring on @{Wrapper.Unit}s. +--- **Actions** - ACT_ACCOUNT_ classes **account for** (detect, count & report) various DCS events occurring on UNITs. -- -- ![Banner Image](..\Presentations\ACT_ACCOUNT\Dia1.JPG) -- -- === -- --- @module Actions.Account +-- @module Actions.Act_Account -- @image MOOSE.JPG do -- ACT_ACCOUNT @@ -147829,7 +160308,7 @@ do -- ACT_ACCOUNT -- -- ### ACT_ACCOUNT States -- - -- * **Asigned**: The player is assigned. + -- * **Assigned**: The player is assigned. -- * **Waiting**: Waiting for an event. -- * **Report**: Reporting. -- * **Account**: Account for an event. @@ -147913,7 +160392,6 @@ do -- ACT_ACCOUNT self:__Wait( 1 ) end - --- StateMachine callback function -- @param #ACT_ACCOUNT self -- @param Wrapper.Unit#UNIT ProcessUnit @@ -147950,7 +160428,7 @@ do -- ACT_ACCOUNT_DEADS --- # @{#ACT_ACCOUNT_DEADS} FSM class, extends @{Core.Fsm.Account#ACT_ACCOUNT} -- -- The ACT_ACCOUNT_DEADS class accounts (detects, counts and reports) successful kills of DCS units. - -- The process is given a @{Set} of units that will be tracked upon successful destruction. + -- The process is given a @{Core.Set} of units that will be tracked upon successful destruction. -- The process will end after each target has been successfully destroyed. -- Each successful dead will trigger an Account state transition that can be scored, modified or administered. -- @@ -147966,7 +160444,6 @@ do -- ACT_ACCOUNT_DEADS ClassName = "ACT_ACCOUNT_DEADS", } - --- Creates a new DESTROY process. -- @param #ACT_ACCOUNT_DEADS self -- @param Core.Set#SET_UNIT TargetSetUnit @@ -148004,7 +160481,6 @@ do -- ACT_ACCOUNT_DEADS self:GetCommandCenter():MessageTypeToGroup( MessageText, ProcessUnit:GetGroup(), MESSAGE.Type.Information ) end - --- StateMachine callback function -- @param #ACT_ACCOUNT_DEADS self -- @param Wrapper.Unit#UNIT ProcessUnit @@ -148079,7 +160555,6 @@ do -- ACT_ACCOUNT_DEADS end end - --- DCS Events --- @param #ACT_ACCOUNT_DEADS self @@ -148168,7 +160643,7 @@ end -- ACT_ACCOUNT DEADS -- -- # 1) @{#ACT_ASSIST_SMOKE_TARGETS_ZONE} class, extends @{Core.Fsm.Route#ACT_ASSIST} -- --- The ACT_ASSIST_SMOKE_TARGETS_ZONE class implements the core functions to smoke targets in a @{Zone}. +-- The ACT_ASSIST_SMOKE_TARGETS_ZONE class implements the core functions to smoke targets in a @{Core.Zone}. -- The targets are smoked within a certain range around each target, simulating a realistic smoking behaviour. -- At random intervals, a new target is smoked. -- @@ -148178,7 +160653,7 @@ end -- ACT_ACCOUNT DEADS -- -- === -- --- @module Actions.Assist +-- @module Actions.Act_Assist -- @image MOOSE.JPG @@ -148372,7 +160847,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 @@ -148390,7 +160865,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 @@ -148407,7 +160882,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 ) @@ -148423,7 +160898,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 ) @@ -148439,7 +160914,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 ) @@ -148455,9 +160930,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 @@ -148468,6 +160943,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. -- @@ -148654,13 +161151,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 @@ -148877,34 +161378,34 @@ do -- Text-To-Speech end end--- **Sound** - Radio transmissions. --- +-- -- === --- +-- -- ## Features: --- +-- -- * Provide radio functionality to broadcast radio transmissions. --- +-- -- What are radio communications in DCS? --- +-- -- * Radio transmissions consist of **sound files** that are broadcasted on a specific **frequency** (e.g. 115MHz) and **modulation** (e.g. AM), --- * They can be **subtitled** for a specific **duration**, the **power** in Watts of the transmiter's antenna can be set, and the transmission can be **looped**. --- +-- * They can be **subtitled** for a specific **duration**, the **power** in Watts of the transmitter's antenna can be set, and the transmission can be **looped**. +-- -- How to supply DCS my own Sound Files? --- +-- -- * Your sound files need to be encoded in **.ogg** or .wav, -- * Your sound files should be **as tiny as possible**. It is suggested you encode in .ogg with low bitrate and sampling settings, --- * They need to be added in .\l10n\DEFAULT\ in you .miz file (wich can be decompressed like a .zip file), +-- * They need to be added in .\l10n\DEFAULT\ in you .miz file (which can be decompressed like a .zip file), -- * For simplicity sake, you can **let DCS' Mission Editor add the file** itself, by creating a new Trigger with the action "Sound to Country", and choosing your sound file and a country you don't use in your mission. --- +-- -- Due to weird DCS quirks, **radio communications behave differently** if sent by a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP} or by any other @{Wrapper.Positionable#POSITIONABLE} --- +-- -- * If the transmitter is a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}, DCS will set the power of the transmission automatically, -- * If the transmitter is any other @{Wrapper.Positionable#POSITIONABLE}, the transmisison can't be subtitled or looped. --- +-- -- Note that obviously, the **frequency** and the **modulation** of the transmission are important only if the players are piloting an **Advanced System Modelling** enabled aircraft, -- like the A10C or the Mirage 2000C. They will **hear the transmission** if they are tuned on the **right frequency and modulation** (and if they are close enough - more on that below). -- If an FC3 aircraft is used, it will **hear every communication, whatever the frequency and the modulation** is set to. The same is true for TACAN beacons. If your aircraft isn't compatible, --- you won't hear/be able to use the TACAN beacon informations. +-- you won't hear/be able to use the TACAN beacon information. -- -- === -- @@ -148915,41 +161416,41 @@ end--- **Sound** - Radio transmissions. --- *It's not true I had nothing on, I had the radio on.* -- Marilyn Monroe --- +-- -- # RADIO usage --- +-- -- There are 3 steps to a successful radio transmission. --- +-- -- * First, you need to **"add a @{#RADIO} object** to your @{Wrapper.Positionable#POSITIONABLE}. This is done using the @{Wrapper.Positionable#POSITIONABLE.GetRadio}() function, -- * Then, you will **set the relevant parameters** to the transmission (see below), -- * When done, you can actually **broadcast the transmission** (i.e. play the sound) with the @{RADIO.Broadcast}() function. --- +-- -- Methods to set relevant parameters for both a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP} or any other @{Wrapper.Positionable#POSITIONABLE} --- +-- -- * @{#RADIO.SetFileName}() : Sets the file name of your sound file (e.g. "Noise.ogg"), -- * @{#RADIO.SetFrequency}() : Sets the frequency of your transmission. -- * @{#RADIO.SetModulation}() : Sets the modulation of your transmission. -- * @{#RADIO.SetLoop}() : Choose if you want the transmission to be looped. If you need your transmission to be looped, you might need a @{#BEACON} instead... --- +-- -- Additional Methods to set relevant parameters if the transmitter is a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP} --- +-- -- * @{#RADIO.SetSubtitle}() : Set both the subtitle and its duration, -- * @{#RADIO.NewUnitTransmission}() : Shortcut to set all the relevant parameters in one method call --- +-- -- Additional Methods to set relevant parameters if the transmitter is any other @{Wrapper.Positionable#POSITIONABLE} --- +-- -- * @{#RADIO.SetPower}() : Sets the power of the antenna in Watts -- * @{#RADIO.NewGenericTransmission}() : Shortcut to set all the relevant parameters in one method call --- +-- -- What is this power thing? --- +-- -- * If your transmission is sent by a @{Wrapper.Positionable#POSITIONABLE} other than a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}, you can set the power of the antenna, -- * Otherwise, DCS sets it automatically, depending on what's available on your Unit, -- * If the player gets **too far** from the transmitter, or if the antenna is **too weak**, the transmission will **fade** and **become noisyer**, -- * This an automated DCS calculation you have no say on, -- * For reference, a standard VOR station has a 100 W antenna, a standard AA TACAN has a 120 W antenna, and civilian ATC's antenna usually range between 300 and 500 W, --- * Note that if the transmission has a subtitle, it will be readable, regardless of the quality of the transmission. --- +-- * Note that if the transmission has a subtitle, it will be readable, regardless of the quality of the transmission. +-- -- @type RADIO -- @field Wrapper.Controllable#CONTROLLABLE Positionable The @{#CONTROLLABLE} that will transmit the radio calls. -- @field #string FileName Name of the sound file played. @@ -148976,19 +161477,19 @@ RADIO = { --- Create a new RADIO Object. This doesn't broadcast a transmission, though, use @{#RADIO.Broadcast} to actually broadcast. -- If you want to create a RADIO, you probably should use @{Wrapper.Positionable#POSITIONABLE.GetRadio}() instead. -- @param #RADIO self --- @param Wrapper.Positionable#POSITIONABLE Positionable The @{Positionable} that will receive radio capabilities. +-- @param Wrapper.Positionable#POSITIONABLE Positionable The @{Wrapper.Positionable#POSITIONABLE} that will receive radio capabilities. -- @return #RADIO The RADIO object or #nil if Positionable is invalid. function RADIO:New(Positionable) -- Inherit base - local self = BASE:Inherit( self, BASE:New() ) -- Core.Radio#RADIO + local self = BASE:Inherit( self, BASE:New() ) -- Sound.Radio#RADIO self:F(Positionable) - + if Positionable:GetPointVec2() then -- It's stupid, but the only way I found to make sure positionable is valid self.Positionable = Positionable return self end - + self:E({error="The passed positionable is invalid, no RADIO created!", positionable=Positionable}) return nil end @@ -149015,19 +161516,19 @@ end -- @return #RADIO self function RADIO:SetFileName(FileName) self:F2(FileName) - + if type(FileName) == "string" then - + if FileName:find(".ogg") or FileName:find(".wav") then if not FileName:find("l10n/DEFAULT/") then FileName = "l10n/DEFAULT/" .. FileName end - + self.FileName = FileName return self end end - + self:E({"File name invalid. Maybe something wrong with the extension?", FileName}) return self end @@ -149035,39 +161536,39 @@ end --- Set the frequency for the radio transmission. -- If the transmitting positionable is a unit or group, this also set the command "SetFrequency" with the defined frequency and modulation. -- @param #RADIO self --- @param #number Frequency Frequency in MHz. Ranges allowed for radio transmissions in DCS : 30-87.995 / 108-173.995 / 225-399.975MHz. +-- @param #number Frequency Frequency in MHz. -- @return #RADIO self function RADIO:SetFrequency(Frequency) self:F2(Frequency) - + if type(Frequency) == "number" then - + -- If frequency is in range - if (Frequency >= 30 and Frequency <= 87.995) or (Frequency >= 108 and Frequency <= 173.995) or (Frequency >= 225 and Frequency <= 399.975) then - + --if (Frequency >= 30 and Frequency <= 87.995) or (Frequency >= 108 and Frequency <= 173.995) or (Frequency >= 225 and Frequency <= 399.975) then + -- Convert frequency from MHz to Hz self.Frequency = Frequency * 1000000 - + -- If the RADIO is attached to a UNIT or a GROUP, we need to send the DCS Command "SetFrequency" to change the UNIT or GROUP frequency if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then - + local commandSetFrequency={ id = "SetFrequency", params = { frequency = self.Frequency, modulation = self.Modulation, } - } - + } + self:T2(commandSetFrequency) self.Positionable:SetCommand(commandSetFrequency) end - + return self - end + --end end - - self:E({"Frequency is outside of DCS Frequency ranges (30-80, 108-152, 225-400). Frequency unchanged.", Frequency}) + + self:E({"Frequency is not a number. Frequency unchanged.", Frequency}) return self end @@ -149093,13 +161594,13 @@ end -- @return #RADIO self function RADIO:SetPower(Power) self:F2(Power) - + if type(Power) == "number" then self.Power = math.floor(math.abs(Power)) --TODO Find what is the maximum power allowed by DCS and limit power to that else self:E({"Power is invalid. Power unchanged.", self.Power}) end - + return self end @@ -149127,7 +161628,7 @@ end -- -- create the broadcaster and attaches it a RADIO -- local MyUnit = UNIT:FindByName("MyUnit") -- local MyUnitRadio = MyUnit:GetRadio() --- +-- -- -- add a subtitle for the next transmission, which will be up for 10s -- MyUnitRadio:SetSubtitle("My Subtitle, 10) function RADIO:SetSubtitle(Subtitle, SubtitleDuration) @@ -149142,14 +161643,14 @@ function RADIO:SetSubtitle(Subtitle, SubtitleDuration) self.SubtitleDuration = SubtitleDuration else self.SubtitleDuration = 0 - self:E({"SubtitleDuration is invalid. SubtitleDuration reset.", self.SubtitleDuration}) + self:E({"SubtitleDuration is invalid. SubtitleDuration reset.", self.SubtitleDuration}) end return self end --- Create a new transmission, that is to say, populate the RADIO with relevant data -- In this function the data is especially relevant if the broadcaster is anything but a UNIT or a GROUP, --- but it will work with a UNIT or a GROUP anyway. +-- but it will work with a UNIT or a GROUP anyway. -- Only the #RADIO and the Filename are mandatory -- @param #RADIO self -- @param #string FileName Name of the sound file that will be transmitted. @@ -149159,20 +161660,20 @@ end -- @return #RADIO self function RADIO:NewGenericTransmission(FileName, Frequency, Modulation, Power, Loop) self:F({FileName, Frequency, Modulation, Power}) - + self:SetFileName(FileName) if Frequency then self:SetFrequency(Frequency) end if Modulation then self:SetModulation(Modulation) end if Power then self:SetPower(Power) end if Loop then self:SetLoop(Loop) end - + return self end --- Create a new transmission, that is to say, populate the RADIO with relevant data -- In this function the data is especially relevant if the broadcaster is a UNIT or a GROUP, --- but it will work for any @{Wrapper.Positionable#POSITIONABLE}. +-- but it will work for any @{Wrapper.Positionable#POSITIONABLE}. -- Only the RADIO and the Filename are mandatory. -- @param #RADIO self -- @param #string FileName Name of sound file. @@ -149194,20 +161695,20 @@ function RADIO:NewUnitTransmission(FileName, Subtitle, SubtitleDuration, Frequen end -- Set frequency. - if Frequency then + if Frequency then self:SetFrequency(Frequency) end - + -- Set subtitle. if Subtitle then self:SetSubtitle(Subtitle, SubtitleDuration or 0) end - + -- Set Looping. - if Loop then + if Loop then self:SetLoop(Loop) end - + return self end @@ -149224,7 +161725,7 @@ end -- @return #RADIO self function RADIO:Broadcast(viatrigger) self:F({viatrigger=viatrigger}) - + -- If the POSITIONABLE is actually a UNIT or a GROUP, use the more complicated DCS command system. if (self.Positionable.ClassName=="UNIT" or self.Positionable.ClassName=="GROUP") and (not viatrigger) then self:T("Broadcasting from a UNIT or a GROUP") @@ -149237,7 +161738,7 @@ function RADIO:Broadcast(viatrigger) subtitle = self.Subtitle, loop = self.Loop, }} - + self:T3(commandTransmitMessage) self.Positionable:SetCommand(commandTransmitMessage) else @@ -149246,23 +161747,23 @@ function RADIO:Broadcast(viatrigger) self:T("Broadcasting from a POSITIONABLE") trigger.action.radioTransmission(self.FileName, self.Positionable:GetPositionVec3(), self.Modulation, self.Loop, self.Frequency, self.Power, tostring(self.ID)) end - + return self end --- Stops a transmission --- This function is especially usefull to stop the broadcast of looped transmissions +-- This function is especially useful to stop the broadcast of looped transmissions -- @param #RADIO self -- @return #RADIO self function RADIO:StopBroadcast() self:F() - -- If the POSITIONABLE is a UNIT or a GROUP, stop the transmission with the DCS "StopTransmission" command + -- If the POSITIONABLE is a UNIT or a GROUP, stop the transmission with the DCS "StopTransmission" command if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then - + local commandStopTransmission={id="StopTransmission", params={}} - + self.Positionable:SetCommand(commandStopTransmission) else -- Else, we use the appropriate singleton funciton @@ -149903,11 +162404,11 @@ function RADIOQUEUE:_GetRadioSenderCoord() return nil end --- **Core** - Makes the radio talk. --- +-- -- === --- +-- -- ## Features: --- +-- -- * Send text strings using a vocabulary that is converted in spoken language. -- * Possiblity to implement multiple language. -- @@ -149919,10 +162420,10 @@ end -- @image Core_Radio.JPG --- Makes the radio speak. --- +-- -- # RADIOSPEECH usage --- --- +-- +-- -- @type RADIOSPEECH -- @extends Core.RadioQueue#RADIOQUEUE RADIOSPEECH = { @@ -149963,24 +162464,24 @@ RADIOSPEECH.Vocabulary.EN = { ["70"] = { "70", 0.48 }, ["80"] = { "80", 0.26 }, ["90"] = { "90", 0.36 }, - ["100"] = { "100", 0.55 }, - ["200"] = { "200", 0.55 }, - ["300"] = { "300", 0.61 }, - ["400"] = { "400", 0.60 }, - ["500"] = { "500", 0.61 }, - ["600"] = { "600", 0.65 }, - ["700"] = { "700", 0.70 }, - ["800"] = { "800", 0.54 }, - ["900"] = { "900", 0.60 }, - ["1000"] = { "1000", 0.60 }, - ["2000"] = { "2000", 0.61 }, - ["3000"] = { "3000", 0.64 }, - ["4000"] = { "4000", 0.62 }, - ["5000"] = { "5000", 0.69 }, - ["6000"] = { "6000", 0.69 }, - ["7000"] = { "7000", 0.75 }, - ["8000"] = { "8000", 0.59 }, - ["9000"] = { "9000", 0.65 }, + ["100"] = { "100", 0.55 }, + ["200"] = { "200", 0.55 }, + ["300"] = { "300", 0.61 }, + ["400"] = { "400", 0.60 }, + ["500"] = { "500", 0.61 }, + ["600"] = { "600", 0.65 }, + ["700"] = { "700", 0.70 }, + ["800"] = { "800", 0.54 }, + ["900"] = { "900", 0.60 }, + ["1000"] = { "1000", 0.60 }, + ["2000"] = { "2000", 0.61 }, + ["3000"] = { "3000", 0.64 }, + ["4000"] = { "4000", 0.62 }, + ["5000"] = { "5000", 0.69 }, + ["6000"] = { "6000", 0.69 }, + ["7000"] = { "7000", 0.75 }, + ["8000"] = { "8000", 0.59 }, + ["9000"] = { "9000", 0.65 }, ["chevy"] = { "chevy", 0.35 }, ["colt"] = { "colt", 0.35 }, @@ -149998,10 +162499,10 @@ RADIOSPEECH.Vocabulary.EN = { ["meters"] = { "meters", 0.41 }, ["mi"] = { "miles", 0.45 }, ["feet"] = { "feet", 0.29 }, - + ["br"] = { "br", 1.1 }, ["bra"] = { "bra", 0.3 }, - + ["returning to base"] = { "returning_to_base", 0.85 }, ["on route to ground target"] = { "on_route_to_ground_target", 1.05 }, @@ -150047,24 +162548,24 @@ RADIOSPEECH.Vocabulary.RU = { ["70"] = { "70", 0.68 }, ["80"] = { "80", 0.84 }, ["90"] = { "90", 0.71 }, - ["100"] = { "100", 0.35 }, - ["200"] = { "200", 0.59 }, - ["300"] = { "300", 0.53 }, - ["400"] = { "400", 0.70 }, - ["500"] = { "500", 0.50 }, - ["600"] = { "600", 0.58 }, - ["700"] = { "700", 0.64 }, - ["800"] = { "800", 0.77 }, - ["900"] = { "900", 0.75 }, - ["1000"] = { "1000", 0.87 }, - ["2000"] = { "2000", 0.83 }, - ["3000"] = { "3000", 0.84 }, - ["4000"] = { "4000", 1.00 }, - ["5000"] = { "5000", 0.77 }, - ["6000"] = { "6000", 0.90 }, - ["7000"] = { "7000", 0.77 }, - ["8000"] = { "8000", 0.92 }, - ["9000"] = { "9000", 0.87 }, + ["100"] = { "100", 0.35 }, + ["200"] = { "200", 0.59 }, + ["300"] = { "300", 0.53 }, + ["400"] = { "400", 0.70 }, + ["500"] = { "500", 0.50 }, + ["600"] = { "600", 0.58 }, + ["700"] = { "700", 0.64 }, + ["800"] = { "800", 0.77 }, + ["900"] = { "900", 0.75 }, + ["1000"] = { "1000", 0.87 }, + ["2000"] = { "2000", 0.83 }, + ["3000"] = { "3000", 0.84 }, + ["4000"] = { "4000", 1.00 }, + ["5000"] = { "5000", 0.77 }, + ["6000"] = { "6000", 0.90 }, + ["7000"] = { "7000", 0.77 }, + ["8000"] = { "8000", 0.92 }, + ["9000"] = { "9000", 0.87 }, ["градусы"] = { "degrees", 0.5 }, ["километры"] = { "kilometers", 0.65 }, @@ -150074,10 +162575,11 @@ RADIOSPEECH.Vocabulary.RU = { ["метров"] = { "meters", 0.41 }, ["m"] = { "meters", 0.41 }, ["ноги"] = { "feet", 0.37 }, - + ["br"] = { "br", 1.1 }, ["bra"] = { "bra", 0.3 }, - + + ["возвращение на базу"] = { "returning_to_base", 1.40 }, ["на пути к наземной цели"] = { "on_route_to_ground_target", 1.45 }, ["перехват боги"] = { "intercepting_bogeys", 1.22 }, @@ -150103,11 +162605,11 @@ function RADIOSPEECH:New(frequency, modulation) -- Inherit base local self = BASE:Inherit( self, RADIOQUEUE:New( frequency, modulation ) ) -- #RADIOSPEECH - + self.Language = "EN" - + self:BuildTree() - + return self end @@ -150165,7 +162667,7 @@ end function RADIOSPEECH:BuildTree() self.Speech = {} - + for Language, Sentences in pairs( self.Vocabulary ) do self:I( { Language = Language, Sentences = Sentences }) self.Speech[Language] = {} @@ -150174,7 +162676,7 @@ function RADIOSPEECH:BuildTree() self:AddSentenceToSpeech( Sentence, self.Speech[Language], Sentence, Data ) end end - + self:I( { Speech = self.Speech } ) return self @@ -150193,7 +162695,7 @@ function RADIOSPEECH:SpeakWords( Sentence, Speech, Language ) local Word, RemainderSentence = Sentence:match( "^[., ]*([^ .,]+)(.*)" ) self:I( { Word = Word, Speech = Speech[Word], RemainderSentence = RemainderSentence } ) - + if Word then if Word ~= "" and tonumber(Word) == nil then @@ -150205,7 +162707,7 @@ function RADIOSPEECH:SpeakWords( Sentence, Speech, Language ) if Speech[Word].Next == nil then self:I( { Sentence = Speech[Word].Sentence, Data = Speech[Word].Data } ) self:NewTransmission( Speech[Word].Data[1] .. ".wav", Speech[Word].Data[2], Language .. "/" ) - else + else if RemainderSentence and RemainderSentence ~= "" then return self:SpeakWords( RemainderSentence, Speech[Word].Next, Language ) end @@ -150213,11 +162715,11 @@ function RADIOSPEECH:SpeakWords( Sentence, Speech, Language ) end return RemainderSentence end - return OriginalSentence + return OriginalSentence else return "" - end - + end + end --- Speak a sentence. @@ -150236,7 +162738,7 @@ function RADIOSPEECH:SpeakDigits( Sentence, Speech, Langauge ) if Digits then if Digits ~= "" and tonumber( Digits ) ~= nil then - + -- Construct numbers local Number = tonumber( Digits ) local Multiple = nil @@ -150260,7 +162762,7 @@ function RADIOSPEECH:SpeakDigits( Sentence, Speech, Langauge ) end return RemainderSentence end - return OriginalSentence + return OriginalSentence else return "" end @@ -150277,26 +162779,26 @@ function RADIOSPEECH:Speak( Sentence, Language ) self:I( { Sentence, Language } ) local Language = Language or "EN" - + self:I( { Language = Language } ) - + -- If there is no node for Speech, then we start at the first nodes of the language. local Speech = self.Speech[Language] - + self:I( { Speech = Speech, Language = Language } ) - + self:NewTransmission( "_In.wav", 0.52, Language .. "/" ) - + repeat Sentence = self:SpeakWords( Sentence, Speech, Language ) - + self:I( { Sentence = Sentence } ) Sentence = self:SpeakDigits( Sentence, Speech, Language ) self:I( { Sentence = Sentence } ) - + -- Sentence = self:SpeakSymbols( Sentence, Speech ) -- -- self:I( { Sentence = Sentence } ) @@ -150306,7 +162808,7 @@ function RADIOSPEECH:Speak( Sentence, Language ) self:NewTransmission( "_Out.wav", 0.28, Language .. "/" ) end ---- **Sound** - Simple Radio Standalone (SRS) Integration. +--- **Sound** - Simple Radio Standalone (SRS) Integration and Text-to-Speech. -- -- === -- @@ -150334,7 +162836,7 @@ end -- === -- -- ### Author: **funkyfranky** --- @module Sound.MSRS +-- @module Sound.SRS -- @image Sound_MSRS.png --- MSRS class. @@ -150353,14 +162855,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). @@ -150396,15 +162897,42 @@ 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 +-- +-- +-- **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")`. +-- Use a specific 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 +-- For voices there are enumerators in this class to help you out on voice names: +-- +-- MSRS.Voices.Microsoft -- e.g. MSRS.Voices.Microsoft.Hedda - the Microsoft enumerator contains all voices known to work with SRS +-- MSRS.Voices.Google -- e.g. MSRS.Voices.Google.Standard.en_AU_Standard_A or MSRS.Voices.Google.Wavenet.de_DE_Wavenet_C - The Google enumerator contains voices for EN, DE, IT, FR and ES. -- -- ## 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", @@ -150420,17 +162948,119 @@ MSRS = { volume = 1, speed = 1, coordinate = nil, + Label = "ROBOT", } --- MSRS class version. -- @field #string version -MSRS.version="0.0.3" +MSRS.version="0.1.1" + +--- Voices +-- @type Voices +MSRS.Voices = { + Microsoft = { + ["Hedda"] = "Microsoft Hedda Desktop", -- de-DE + ["Hazel"] = "Microsoft Hazel Desktop", -- en-GB + ["David"] = "Microsoft David Desktop", -- en-US + ["Zira"] = "Microsoft Zira Desktop", -- en-US + ["Hortense"] = "Microsoft Hortense Desktop", --fr-FR + }, + Google = { + Standard = { + ["en_AU_Standard_A"] = 'en-AU-Standard-A', -- [1] FEMALE + ["en_AU_Standard_B"] = 'en-AU-Standard-B', -- [2] MALE + ["en_AU_Standard_C"] = 'en-AU-Standard-C', -- [3] FEMALE + ["en_AU_Standard_D"] = 'en-AU-Standard-D', -- [4] MALE + ["en_IN_Standard_A"] = 'en-IN-Standard-A', -- [5] FEMALE + ["en_IN_Standard_B"] = 'en-IN-Standard-B', -- [6] MALE + ["en_IN_Standard_C"] = 'en-IN-Standard-C', -- [7] MALE + ["en_IN_Standard_D"] = 'en-IN-Standard-D', -- [8] FEMALE + ["en_GB_Standard_A"] = 'en-GB-Standard-A', -- [9] FEMALE + ["en_GB_Standard_B"] = 'en-GB-Standard-B', -- [10] MALE + ["en_GB_Standard_C"] = 'en-GB-Standard-C', -- [11] FEMALE + ["en_GB_Standard_D"] = 'en-GB-Standard-D', -- [12] MALE + ["en_GB_Standard_F"] = 'en-GB-Standard-F', -- [13] FEMALE + ["en_US_Standard_A"] = 'en-US-Standard-A', -- [14] MALE + ["en_US_Standard_B"] = 'en-US-Standard-B', -- [15] MALE + ["en_US_Standard_C"] = 'en-US-Standard-C', -- [16] FEMALE + ["en_US_Standard_D"] = 'en-US-Standard-D', -- [17] MALE + ["en_US_Standard_E"] = 'en-US-Standard-E', -- [18] FEMALE + ["en_US_Standard_F"] = 'en-US-Standard-F', -- [19] FEMALE + ["en_US_Standard_G"] = 'en-US-Standard-G', -- [20] FEMALE + ["en_US_Standard_H"] = 'en-US-Standard-H', -- [21] FEMALE + ["en_US_Standard_I"] = 'en-US-Standard-I', -- [22] MALE + ["en_US_Standard_J"] = 'en-US-Standard-J', -- [23] MALE + ["fr_FR_Standard_A"] = "fr-FR-Standard-A", -- Female + ["fr_FR_Standard_B"] = "fr-FR-Standard-B", -- Male + ["fr_FR_Standard_C"] = "fr-FR-Standard-C", -- Female + ["fr_FR_Standard_D"] = "fr-FR-Standard-D", -- Male + ["fr_FR_Standard_E"] = "fr-FR-Standard-E", -- Female + ["de_DE_Standard_A"] = "de-DE-Standard-A", -- Female + ["de_DE_Standard_B"] = "de-DE-Standard-B", -- Male + ["de_DE_Standard_C"] = "de-DE-Standard-C", -- Female + ["de_DE_Standard_D"] = "de-DE-Standard-D", -- Male + ["de_DE_Standard_E"] = "de-DE-Standard-E", -- Male + ["de_DE_Standard_F"] = "de-DE-Standard-F", -- Female + ["es_ES_Standard_A"] = "es-ES-Standard-A", -- Female + ["es_ES_Standard_B"] = "es-ES-Standard-B", -- Male + ["es_ES_Standard_C"] = "es-ES-Standard-C", -- Female + ["es_ES_Standard_D"] = "es-ES-Standard-D", -- Female + ["it_IT_Standard_A"] = "it-IT-Standard-A", -- Female + ["it_IT_Standard_B"] = "it-IT-Standard-B", -- Female + ["it_IT_Standard_C"] = "it-IT-Standard-C", -- Male + ["it_IT_Standard_D"] = "it-IT-Standard-D", -- Male + }, + Wavenet = { + ["en_AU_Wavenet_A"] = 'en-AU-Wavenet-A', -- [1] FEMALE + ["en_AU_Wavenet_B"] = 'en-AU-Wavenet-B', -- [2] MALE + ["en_AU_Wavenet_C"] = 'en-AU-Wavenet-C', -- [3] FEMALE + ["en_AU_Wavenet_D"] = 'en-AU-Wavenet-D', -- [4] MALE + ["en_IN_Wavenet_A"] = 'en-IN-Wavenet-A', -- [5] FEMALE + ["en_IN_Wavenet_B"] = 'en-IN-Wavenet-B', -- [6] MALE + ["en_IN_Wavenet_C"] = 'en-IN-Wavenet-C', -- [7] MALE + ["en_IN_Wavenet_D"] = 'en-IN-Wavenet-D', -- [8] FEMALE + ["en_GB_Wavenet_A"] = 'en-GB-Wavenet-A', -- [9] FEMALE + ["en_GB_Wavenet_B"] = 'en-GB-Wavenet-B', -- [10] MALE + ["en_GB_Wavenet_C"] = 'en-GB-Wavenet-C', -- [11] FEMALE + ["en_GB_Wavenet_D"] = 'en-GB-Wavenet-D', -- [12] MALE + ["en_GB_Wavenet_F"] = 'en-GB-Wavenet-F', -- [13] FEMALE + ["en_US_Wavenet_A"] = 'en-US-Wavenet-A', -- [14] MALE + ["en_US_Wavenet_B"] = 'en-US-Wavenet-B', -- [15] MALE + ["en_US_Wavenet_C"] = 'en-US-Wavenet-C', -- [16] FEMALE + ["en_US_Wavenet_D"] = 'en-US-Wavenet-D', -- [17] MALE + ["en_US_Wavenet_E"] = 'en-US-Wavenet-E', -- [18] FEMALE + ["en_US_Wavenet_F"] = 'en-US-Wavenet-F', -- [19] FEMALE + ["en_US_Wavenet_G"] = 'en-US-Wavenet-G', -- [20] FEMALE + ["en_US_Wavenet_H"] = 'en-US-Wavenet-H', -- [21] FEMALE + ["en_US_Wavenet_I"] = 'en-US-Wavenet-I', -- [22] MALE + ["en_US_Wavenet_J"] = 'en-US-Wavenet-J', -- [23] MALE + ["fr_FR_Wavenet_A"] = "fr-FR-Wavenet-A", -- Female + ["fr_FR_Wavenet_B"] = "fr-FR-Wavenet-B", -- Male + ["fr_FR_Wavenet_C"] = "fr-FR-Wavenet-C", -- Female + ["fr_FR_Wavenet_D"] = "fr-FR-Wavenet-D", -- Male + ["fr_FR_Wavenet_E"] = "fr-FR-Wavenet-E", -- Female + ["de_DE_Wavenet_A"] = "de-DE-Wavenet-A", -- Female + ["de_DE_Wavenet_B"] = "de-DE-Wavenet-B", -- Male + ["de_DE_Wavenet_C"] = "de-DE-Wavenet-C", -- Female + ["de_DE_Wavenet_D"] = "de-DE-Wavenet-D", -- Male + ["de_DE_Wavenet_E"] = "de-DE-Wavenet-E", -- Male + ["de_DE_Wavenet_F"] = "de-DE-Wavenet-F", -- Female + ["es_ES_Wavenet_B"] = "es-ES-Wavenet-B", -- Male + ["es_ES_Wavenet_C"] = "es-ES-Wavenet-C", -- Female + ["es_ES_Wavenet_D"] = "es-ES-Wavenet-D", -- Female + ["it_IT_Wavenet_A"] = "it-IT-Wavenet-A", -- Female + ["it_IT_Wavenet_B"] = "it-IT-Wavenet-B", -- Female + ["it_IT_Wavenet_C"] = "it-IT-Wavenet-C", -- Male + ["it_IT_Wavenet_D"] = "it-IT-Wavenet-D", -- Male + } , + }, + } ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Add functions to add/remove freqs and modulations. +-- TODO: Add functions to remove freqs and modulations. -- DONE: Add coordinate. -- DONE: Add google. @@ -150443,8 +163073,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 @@ -150459,6 +163090,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 @@ -150501,12 +163139,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. @@ -150522,6 +163195,7 @@ end -- @return #MSRS self function MSRS:SetCoalition(Coalition) self.coalition=Coalition or 0 + return self end --- Get coalition. @@ -150548,6 +163222,24 @@ function MSRS:SetFrequencies(Frequencies) return self end +--- Add frequencies. +-- @param #MSRS self +-- @param #table Frequencies Frequencies in MHz. Can also be given as a #number if only one frequency should be used. +-- @return #MSRS self +function MSRS:AddFrequencies(Frequencies) + + -- Ensure table. + if type(Frequencies)~="table" then + Frequencies={Frequencies} + end + + for _,_freq in pairs(Frequencies) do + table.insert(self.frequencies,_freq) + end + + return self +end + --- Get frequencies. -- @param #MSRS self -- @param #table Frequencies in MHz. @@ -150572,6 +163264,24 @@ function MSRS:SetModulations(Modulations) return self end +--- Add modulations. +-- @param #MSRS self +-- @param #table Modulations Modulations. Can also be given as a #number if only one modulation should be used. +-- @return #MSRS self +function MSRS:AddModulations(Modulations) + + -- Ensure table. + if type(Modulations)~="table" then + Modulations={Modulations} + end + + for _,_mod in pairs(Modulations) do + table.insert(self.modulations,_mod) + end + + return self +end + --- Get modulations. -- @param #MSRS self -- @param #table Modulations. @@ -150690,21 +163400,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 @@ -150730,16 +163429,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 @@ -150766,37 +163455,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 + end + + return self +end - -- Debug output. - self:I(string.format("MSRS Text command=%s", command)) +--- 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 - -- Execute SRS command. - local x=os.execute(command) + -- 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 @@ -150933,8 +163633,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" @@ -150947,21 +163648,14 @@ 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") modus=modus:gsub("1", "FM") - -- This did not work well. Stopped if the transmission was a bit longer with no apparent error. - --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 /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 @@ -150970,7 +163664,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 @@ -150986,7 +163680,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. @@ -150995,10 +163689,382 @@ function MSRS:_GetCommand(freqs, modus, coal, gender, voice, culture, volume, sp return command 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. +-- @field #boolean TransmitOnlyWithPlayers If true, only transmit if there are alive Players. +-- @field Core.Set#SET_CLIENT PlayerSet PlayerSet created when TransmitOnlyWithPlayers == true + +--- 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 + +--- Switch to only transmit if there are players on the server. +-- @param #MSRSQUEUE self +-- @param #boolean Switch If true, only send SRS if there are alive Players. +-- @return #MSRSQUEUE self +function MSRSQUEUE:SetTransmitOnlyWithPlayers(Switch) + self.TransmitOnlyWithPlayers = Switch + if Switch == false or Switch==nil then + if self.PlayerSet then + self.PlayerSet:FilterStop() + end + self.PlayerSet = nil + else + self.PlayerSet = SET_CLIENT:New():FilterStart() + 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) + + if self.TransmitOnlyWithPlayers then + if self.PlayerSet and self.PlayerSet:CountAlive() == 0 then + return self + end + end + + -- 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. +--- **Tasking** - A command center governs multiple missions, and takes care of the reporting and communications. -- -- **Features:** -- @@ -151216,7 +164282,7 @@ function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) local MenuReporting = MENU_GROUP:New( EventGroup, "Missions Reports", CommandCenterMenu ) local MenuMissionsSummary = MENU_GROUP_COMMAND:New( EventGroup, "Missions Status Report", MenuReporting, self.ReportSummary, self, EventGroup ) local MenuMissionsDetails = MENU_GROUP_COMMAND:New( EventGroup, "Missions Players Report", MenuReporting, self.ReportMissionsPlayers, self, EventGroup ) - self:ReportSummary( EventGroup ) + --self:ReportSummary( EventGroup ) local PlayerUnit = EventData.IniUnit for MissionID, Mission in pairs( self:GetMissions() ) do local Mission = Mission -- Tasking.Mission#MISSION @@ -151512,7 +164578,7 @@ function COMMANDCENTER:AssignTask( TaskGroup ) if Task then - self:I( "Assigning task " .. Task:GetName() .. " using auto assign method " .. self.AutoAssignMethod .. " to " .. TaskGroup:GetName() .. " with task priority " .. AssignPriority ) + self:T( "Assigning task " .. Task:GetName() .. " using auto assign method " .. self.AutoAssignMethod .. " to " .. TaskGroup:GetName() .. " with task priority " .. AssignPriority ) if not self.AutoAcceptTasks == true then Task:SetAutoAssignMethod( ACT_ASSIGN_MENU_ACCEPT:New( Task.TaskBriefing ) ) @@ -151560,9 +164626,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 @@ -151811,7 +164879,7 @@ function COMMANDCENTER:SetMessageDuration(seconds) self.MessageDuration = 10 or seconds end ---- **Tasking** -- A mission models a goal to be achieved through the execution and completion of tasks by human players. +--- **Tasking** - A mission models a goal to be achieved through the execution and completion of tasks by human players. -- -- **Features:** -- @@ -151899,7 +164967,7 @@ end -- - @{#MISSION.GetTasks}(): Retrieves a list of the tasks controlled by the mission. -- - @{#MISSION.GetTask}(): Retrieves a specific task controlled by the mission. -- - @{#MISSION.GetTasksRemaining}(): Retrieve a list of the tasks that aren't finished or failed, and are governed by the mission. --- - @{#MISSION.GetGroupTasks}(): Retrieve a list of the tasks that can be asigned to a @{Wrapper.Group}. +-- - @{#MISSION.GetGroupTasks}(): Retrieve a list of the tasks that can be assigned to a @{Wrapper.Group}. -- - @{#MISSION.GetTaskTypes}(): Retrieve a list of the different task types governed by the mission. -- -- ### 3.3. Get the command center. @@ -151944,8 +165012,8 @@ MISSION = { -- @param Tasking.CommandCenter#COMMANDCENTER CommandCenter -- @param #string MissionName Name of the mission. This name will be used to reference the status of each mission by the players. -- @param #string MissionPriority String indicating the "priority" of the Mission. e.g. "Primary", "Secondary". It is free format and up to the Mission designer to choose. There are no rules behind this field. --- @param #string MissionBriefing String indicating the mission briefing to be shown when a player joins a @{CLIENT}. --- @param DCS#coaliton.side MissionCoalition Side of the coalition, i.e. and enumerator @{#DCS.coalition.side} corresponding to RED, BLUE or NEUTRAL. +-- @param #string MissionBriefing String indicating the mission briefing to be shown when a player joins a @{Wrapper.Client#CLIENT}. +-- @param DCS#coalition.side MissionCoalition Side of the coalition, i.e. and enumerator @{#DCS.coalition.side} corresponding to RED, BLUE or NEUTRAL. -- @return #MISSION self function MISSION:New( CommandCenter, MissionName, MissionPriority, MissionBriefing, MissionCoalition ) @@ -152226,7 +165294,7 @@ end -- @param Wrapper.Group#GROUP PlayerGroup The GROUP of the player joining the Mission. -- @return #boolean true if Unit is part of a Task in the Mission. function MISSION:JoinUnit( PlayerUnit, PlayerGroup ) - self:I( { Mission = self:GetName(), PlayerUnit = PlayerUnit, PlayerGroup = PlayerGroup } ) + self:T( { Mission = self:GetName(), PlayerUnit = PlayerUnit, PlayerGroup = PlayerGroup } ) local PlayerUnitAdded = false @@ -152356,7 +165424,7 @@ end do -- Group Assignment - --- Returns if the @{Mission} is assigned to the Group. + --- Returns if the @{Tasking.Mission} is assigned to the Group. -- @param #MISSION self -- @param Wrapper.Group#GROUP MissionGroup -- @return #boolean @@ -152374,7 +165442,7 @@ do -- Group Assignment end - --- Set @{Wrapper.Group} assigned to the @{Mission}. + --- Set @{Wrapper.Group} assigned to the @{Tasking.Mission}. -- @param #MISSION self -- @param Wrapper.Group#GROUP MissionGroup -- @return #MISSION @@ -152384,12 +165452,12 @@ do -- Group Assignment local MissionGroupName = MissionGroup:GetName() self.AssignedGroups[MissionGroupName] = MissionGroup - self:I( string.format( "Mission %s is assigned to %s", MissionName, MissionGroupName ) ) + self:T( string.format( "Mission %s is assigned to %s", MissionName, MissionGroupName ) ) return self end - --- Clear the @{Wrapper.Group} assignment from the @{Mission}. + --- Clear the @{Wrapper.Group} assignment from the @{Tasking.Mission}. -- @param #MISSION self -- @param Wrapper.Group#GROUP MissionGroup -- @return #MISSION @@ -152480,7 +165548,7 @@ end --- Get the TASK identified by the TaskNumber from the Mission. This function is useful in GoalFunctions. --- @param #string TaskName The Name of the @{Task} within the @{Mission}. +-- @param #string TaskName The Name of the @{Tasking.Task} within the @{Tasking.Mission}. -- @return Tasking.Task#TASK The Task -- @return #nil Returns nil if no task was found. function MISSION:GetTask( TaskName ) @@ -152490,9 +165558,9 @@ function MISSION:GetTask( TaskName ) end ---- Return the next @{Task} ID to be completed within the @{Mission}. +--- Return the next @{Tasking.Task} ID to be completed within the @{Tasking.Mission}. -- @param #MISSION self --- @param Tasking.Task#TASK Task is the @{Task} object. +-- @param Tasking.Task#TASK Task is the @{Tasking.Task} object. -- @return Tasking.Task#TASK The task added. function MISSION:GetNextTaskID( Task ) @@ -152502,16 +165570,16 @@ function MISSION:GetNextTaskID( Task ) end ---- Register a @{Task} to be completed within the @{Mission}. --- Note that there can be multiple @{Task}s registered to be completed. +--- Register a @{Tasking.Task} to be completed within the @{Tasking.Mission}. +-- Note that there can be multiple @{Tasking.Task}s registered to be completed. -- Each Task can be set a certain Goals. The Mission will not be completed until all Goals are reached. -- @param #MISSION self --- @param Tasking.Task#TASK Task is the @{Task} object. +-- @param Tasking.Task#TASK Task is the @{Tasking.Task} object. -- @return Tasking.Task#TASK The task added. function MISSION:AddTask( Task ) local TaskName = Task:GetTaskName() - self:I( { "==> Adding TASK ", MissionName = self:GetName(), TaskName = TaskName } ) + self:T( { "==> Adding TASK ", MissionName = self:GetName(), TaskName = TaskName } ) self.Tasks[TaskName] = Task @@ -152521,16 +165589,16 @@ function MISSION:AddTask( Task ) end ---- Removes a @{Task} to be completed within the @{Mission}. --- Note that there can be multiple @{Task}s registered to be completed. +--- Removes a @{Tasking.Task} to be completed within the @{Tasking.Mission}. +-- Note that there can be multiple @{Tasking.Task}s registered to be completed. -- Each Task can be set a certain Goals. The Mission will not be completed until all Goals are reached. -- @param #MISSION self --- @param Tasking.Task#TASK Task is the @{Task} object. +-- @param Tasking.Task#TASK Task is the @{Tasking.Task} object. -- @return #nil The cleaned Task reference. function MISSION:RemoveTask( Task ) local TaskName = Task:GetTaskName() - self:I( { "<== Removing TASK ", MissionName = self:GetName(), TaskName = TaskName } ) + self:T( { "<== Removing TASK ", MissionName = self:GetName(), TaskName = TaskName } ) self:F( TaskName ) self.Tasks[TaskName] = self.Tasks[TaskName] or { n = 0 } @@ -152546,35 +165614,35 @@ function MISSION:RemoveTask( Task ) return nil end ---- Is the @{Mission} **COMPLETED**. +--- Is the @{Tasking.Mission} **COMPLETED**. -- @param #MISSION self -- @return #boolean function MISSION:IsCOMPLETED() return self:Is( "COMPLETED" ) end ---- Is the @{Mission} **IDLE**. +--- Is the @{Tasking.Mission} **IDLE**. -- @param #MISSION self -- @return #boolean function MISSION:IsIDLE() return self:Is( "IDLE" ) end ---- Is the @{Mission} **ENGAGED**. +--- Is the @{Tasking.Mission} **ENGAGED**. -- @param #MISSION self -- @return #boolean function MISSION:IsENGAGED() return self:Is( "ENGAGED" ) end ---- Is the @{Mission} **FAILED**. +--- Is the @{Tasking.Mission} **FAILED**. -- @param #MISSION self -- @return #boolean function MISSION:IsFAILED() return self:Is( "FAILED" ) end ---- Is the @{Mission} **HOLD**. +--- Is the @{Tasking.Mission} **HOLD**. -- @param #MISSION self -- @return #boolean function MISSION:IsHOLD() @@ -152918,7 +165986,7 @@ function MISSION:ReportDetails( ReportGroup ) end --- Get all the TASKs from the Mission. This function is useful in GoalFunctions. --- @return {TASK,...} Structure of TASKS with the @{TASK} number as the key. +-- @return {TASK,...} Structure of TASKS with the @{Tasking.Task#TASK} number as the key. -- @usage -- -- Get Tasks from the Mission. -- Tasks = Mission:GetTasks() @@ -153016,7 +166084,7 @@ end ---- **Tasking** -- A task object governs the main engine to administer human taskings. +--- **Tasking** - A task object governs the main engine to administer human taskings. -- -- **Features:** -- @@ -153056,7 +166124,7 @@ end -- -- A mission can be in a specific state during the simulation run. For more information about these states, please check the @{Tasking.Mission} section. -- --- To achieve the mission goal, a mission administers @{Tasking.Task}s that are set to achieve the mission goal by the human players. +-- To achieve the mission goal, a mission administers @{#TASK}s that are set to achieve the mission goal by the human players. -- Each of these tasks can be **dynamically created** using a task dispatcher, or **coded** by the mission designer. -- Each mission has a separate **Mission Menu**, that focuses on the administration of these tasks. -- @@ -153100,7 +166168,7 @@ end -- -- ![Mission](../Tasking/Report_Statistics_Progress.JPG) -- --- A statistic report on the progress of the mission. Each task achievement will increase the %-tage to 100% as a goal to complete the task. +-- A statistic report on the progress of the mission. Each task achievement will increase the % to 100% as a goal to complete the task. -- -- ## 1.3) Join a Task. -- @@ -153161,7 +166229,7 @@ end -- -- ![Command Center](../Tasking/Menu_CommandCenter.JPG) -- --- When we take back the command center menu, you see two addtional **Assign Task** menu items. +-- When we take back the command center menu, you see two additional **Assign Task** menu items. -- The menu **Assign Task On** will automatically allocate a task to the player. -- After the selection of this menu, the menu will change into **Assign Task Off**, -- and will need to be selected again by the player to switch of the automatic task assignment. @@ -153208,7 +166276,7 @@ end -- -- The state completion is by default set to **Success**, if the goals of the task have been reached, but can be overruled by a goal method. -- --- Depending on the tactical situation, a task can be **Cancelled** by the mission governer. +-- Depending on the tactical situation, a task can be **Cancelled** by the mission governor. -- It is actually the mission designer who has the flexibility to decide at which conditions a task would be set to **Success**, **Failed** or **Cancelled**. -- This decision all depends on the task goals, and the phase/evolution of the task conditions that would accomplish the goals. -- @@ -153217,16 +166285,16 @@ end -- However, it could very well be also acceptable that the task would be flagged as **Success**. -- -- The tasking mechanism governs beside the progress also a scoring mechanism, and in case of goal completion without any active pilot involved --- in the execution of the task, could result in a **Success** task completion status, but no score would be awared, as there were no players involved. +-- in the execution of the task, could result in a **Success** task completion status, but no score would be awarded, as there were no players involved. -- -- These different completion states are important for the mission designer to reflect scoring to a player. -- A success could mean a positive score to be given, while a failure could mean a negative score or penalties to be awarded. -- -- === -- --- ### Author: **FlightControl** +-- ### Author(s): **FlightControl** -- --- ### Contributions: +-- ### Contribution(s): -- -- === -- @@ -153280,8 +166348,8 @@ end -- -- ## 1.3) Cargo Tasks -- --- - @{Tasking.Task_Cargo#TASK_CARGO_TRANSPORT} - Models the transportation of cargo to deployment zones. --- - @{Tasking.Task_Cargo#TASK_CARGO_CSAR} - Models the rescue of downed friendly pilots from behind enemy lines. +-- - @{Tasking.Task_CARGO#TASK_CARGO_TRANSPORT} - Models the transportation of cargo to deployment zones. +-- - @{Tasking.Task_CARGO#TASK_CARGO_CSAR} - Models the rescue of downed friendly pilots from behind enemy lines. -- -- -- # 2) Task status events. @@ -153311,7 +166379,7 @@ end -- -- function Task:OnAfterGoal() -- if condition == true then --- self:Success() -- This will flag the task to Succcess when the condition is true. +-- self:Success() -- This will flag the task to Success when the condition is true. -- else -- if condition2 == true and condition3 == true then -- self:Fail() -- This will flag the task to Failed, when condition2 and condition3 would be true. @@ -153750,7 +166818,7 @@ end do -- Group Assignment - --- Returns if the @{Task} is assigned to the Group. + --- Returns if the @{#TASK} is assigned to the Group. -- @param #TASK self -- @param Wrapper.Group#GROUP TaskGroup -- @return #boolean @@ -153768,7 +166836,7 @@ do -- Group Assignment end - --- Set @{Wrapper.Group} assigned to the @{Task}. + --- Set @{Wrapper.Group} assigned to the @{#TASK}. -- @param #TASK self -- @param Wrapper.Group#GROUP TaskGroup -- @return #TASK @@ -153798,7 +166866,7 @@ do -- Group Assignment return self end - --- Clear the @{Wrapper.Group} assignment from the @{Task}. + --- Clear the @{Wrapper.Group} assignment from the @{#TASK}. -- @param #TASK self -- @param Wrapper.Group#GROUP TaskGroup -- @return #TASK @@ -153842,7 +166910,7 @@ do -- Group Assignment end - --- Assign the @{Task} to a @{Wrapper.Group}. + --- Assign the @{#TASK} to a @{Wrapper.Group}. -- @param #TASK self -- @param Wrapper.Group#GROUP TaskGroup -- @return #TASK @@ -153879,7 +166947,7 @@ do -- Group Assignment return self end - --- UnAssign the @{Task} from a @{Wrapper.Group}. + --- UnAssign the @{#TASK} from a @{Wrapper.Group}. -- @param #TASK self -- @param Wrapper.Group#GROUP TaskGroup function TASK:UnAssignFromGroup( TaskGroup ) @@ -153917,7 +166985,7 @@ function TASK:HasGroup( FindGroup ) end ---- Assign the @{Task} to an alive @{Wrapper.Unit}. +--- Assign the @{#TASK} to an alive @{Wrapper.Unit}. -- @param #TASK self -- @param Wrapper.Unit#UNIT TaskUnit -- @return #TASK self @@ -153936,7 +167004,7 @@ function TASK:AssignToUnit( TaskUnit ) return self end ---- UnAssign the @{Task} from an alive @{Wrapper.Unit}. +--- UnAssign the @{#TASK} from an alive @{Wrapper.Unit}. -- @param #TASK self -- @param Wrapper.Unit#UNIT TaskUnit -- @return #TASK self @@ -153950,7 +167018,7 @@ function TASK:UnAssignFromUnit( TaskUnit ) return self end ---- Sets the TimeOut for the @{Task}. If @{Task} stayed planned for longer than TimeOut, it gets into Cancelled status. +--- Sets the TimeOut for the @{#TASK}. If @{#TASK} stayed planned for longer than TimeOut, it gets into Cancelled status. -- @param #TASK self -- @param #integer Timer in seconds -- @return #TASK self @@ -153961,7 +167029,7 @@ function TASK:SetTimeOut ( Timer ) return self end ---- Send a message of the @{Task} to the assigned @{Wrapper.Group}s. +--- Send a message of the @{#TASK} to the assigned @{Wrapper.Group}s. -- @param #TASK self function TASK:MessageToGroups( Message ) self:F( { Message = Message } ) @@ -153978,7 +167046,7 @@ function TASK:MessageToGroups( Message ) end ---- Send the briefng message of the @{Task} to the assigned @{Wrapper.Group}s. +--- Send the briefing message of the @{#TASK} to the assigned @{Wrapper.Group}s. -- @param #TASK self function TASK:SendBriefingToAssignedGroups() self:F2() @@ -153993,7 +167061,7 @@ function TASK:SendBriefingToAssignedGroups() end ---- UnAssign the @{Task} from the @{Wrapper.Group}s. +--- UnAssign the @{#TASK} from the @{Wrapper.Group}s. -- @param #TASK self function TASK:UnAssignFromGroups() self:F2() @@ -154009,7 +167077,7 @@ end ---- Returns if the @{Task} has still alive and assigned Units. +--- Returns if the @{#TASK} has still alive and assigned Units. -- @param #TASK self -- @return #boolean function TASK:HasAliveUnits() @@ -154034,7 +167102,7 @@ function TASK:HasAliveUnits() return false end ---- Set the menu options of the @{Task} to all the groups in the SetGroup. +--- Set the menu options of the @{#TASK} to all the groups in the SetGroup. -- @param #TASK self -- @param #number MenuTime -- @return #TASK @@ -154075,7 +167143,7 @@ function TASK:SetMenuForGroup( TaskGroup, MenuTime ) end ---- Set the planned menu option of the @{Task}. +--- Set the planned menu option of the @{#TASK}. -- @param #TASK self -- @param Wrapper.Group#GROUP TaskGroup -- @param #string MenuText The menu text. @@ -154110,7 +167178,7 @@ function TASK:SetPlannedMenuForGroup( TaskGroup, MenuTime ) return self end ---- Set the assigned menu options of the @{Task}. +--- Set the assigned menu options of the @{#TASK}. -- @param #TASK self -- @param Wrapper.Group#GROUP TaskGroup -- @param #number MenuTime @@ -154145,7 +167213,7 @@ function TASK:SetAssignedMenuForGroup( TaskGroup, MenuTime ) return self end ---- Remove the menu options of the @{Task} to all the groups in the SetGroup. +--- Remove the menu options of the @{#TASK} to all the groups in the SetGroup. -- @param #TASK self -- @param #number MenuTime -- @return #TASK @@ -154163,7 +167231,7 @@ function TASK:RemoveMenu( MenuTime ) end ---- Remove the menu option of the @{Task} for a @{Wrapper.Group}. +--- Remove the menu option of the @{#TASK} for a @{Wrapper.Group}. -- @param #TASK self -- @param Wrapper.Group#GROUP TaskGroup -- @param #number MenuTime @@ -154194,7 +167262,7 @@ function TASK:RefreshMenus( TaskGroup, MenuTime ) end ---- Remove the assigned menu option of the @{Task} for a @{Wrapper.Group}. +--- Remove the assigned menu option of the @{#TASK} for a @{Wrapper.Group}. -- @param #TASK self -- @param Wrapper.Group#GROUP TaskGroup -- @param #number MenuTime @@ -154293,14 +167361,14 @@ end ---- Returns the @{Task} name. +--- Returns the @{#TASK} name. -- @param #TASK self -- @return #string TaskName function TASK:GetTaskName() return self.TaskName end ---- Returns the @{Task} briefing. +--- Returns the @{#TASK} briefing. -- @param #TASK self -- @return #string Task briefing. function TASK:GetTaskBriefing() @@ -154310,7 +167378,7 @@ end ---- Get the default or currently assigned @{Process} template with key ProcessName. +--- Get the default or currently assigned @{Core.Fsm#FSM_PROCESS} template with key ProcessName. -- @param #TASK self -- @param #string ProcessName -- @return Core.Fsm#FSM_PROCESS @@ -154323,8 +167391,8 @@ end --- TODO: Obscolete? ---- Fail processes from @{Task} with key @{Wrapper.Unit} +-- TODO: Obsolete? +--- Fail processes from @{#TASK} with key @{Wrapper.Unit}. -- @param #TASK self -- @param #string TaskUnitName -- @return #TASK self @@ -154336,7 +167404,7 @@ function TASK:FailProcesses( TaskUnitName ) end end ---- Add a FiniteStateMachine to @{Task} with key Task@{Wrapper.Unit} +--- Add a FiniteStateMachine to @{#TASK} with key @{Wrapper.Unit}. -- @param #TASK self -- @param Wrapper.Unit#UNIT TaskUnit -- @param Core.Fsm#FSM_PROCESS Fsm @@ -154349,7 +167417,7 @@ function TASK:SetStateMachine( TaskUnit, Fsm ) return Fsm end ---- Gets the FiniteStateMachine of @{Task} with key Task@{Wrapper.Unit} +--- Gets the FiniteStateMachine of @{#TASK} with key @{Wrapper.Unit}. -- @param #TASK self -- @param Wrapper.Unit#UNIT TaskUnit -- @return Core.Fsm#FSM_PROCESS @@ -154359,7 +167427,7 @@ function TASK:GetStateMachine( TaskUnit ) return self.Fsm[TaskUnit] end ---- Remove FiniteStateMachines from @{Task} with key Task@{Wrapper.Unit} +--- Remove FiniteStateMachines from @{#TASK} with key @{Wrapper.Unit}. -- @param #TASK self -- @param Wrapper.Unit#UNIT TaskUnit -- @return #TASK self @@ -154383,7 +167451,7 @@ function TASK:RemoveStateMachine( TaskUnit ) end ---- Checks if there is a FiniteStateMachine assigned to Task@{Wrapper.Unit} for @{Task} +--- Checks if there is a FiniteStateMachine assigned to @{Wrapper.Unit} for @{#TASK}. -- @param #TASK self -- @param Wrapper.Unit#UNIT TaskUnit -- @return #TASK self @@ -154456,117 +167524,117 @@ function TASK:GetID() end ---- Sets a @{Task} to status **Success**. +--- Sets a @{#TASK} to status **Success**. -- @param #TASK self function TASK:StateSuccess() self:SetState( self, "State", "Success" ) return self end ---- Is the @{Task} status **Success**. +--- Is the @{#TASK} status **Success**. -- @param #TASK self function TASK:IsStateSuccess() return self:Is( "Success" ) end ---- Sets a @{Task} to status **Failed**. +--- Sets a @{#TASK} to status **Failed**. -- @param #TASK self function TASK:StateFailed() self:SetState( self, "State", "Failed" ) return self end ---- Is the @{Task} status **Failed**. +--- Is the @{#TASK} status **Failed**. -- @param #TASK self function TASK:IsStateFailed() return self:Is( "Failed" ) end ---- Sets a @{Task} to status **Planned**. +--- Sets a @{#TASK} to status **Planned**. -- @param #TASK self function TASK:StatePlanned() self:SetState( self, "State", "Planned" ) return self end ---- Is the @{Task} status **Planned**. +--- Is the @{#TASK} status **Planned**. -- @param #TASK self function TASK:IsStatePlanned() return self:Is( "Planned" ) end ---- Sets a @{Task} to status **Aborted**. +--- Sets a @{#TASK} to status **Aborted**. -- @param #TASK self function TASK:StateAborted() self:SetState( self, "State", "Aborted" ) return self end ---- Is the @{Task} status **Aborted**. +--- Is the @{#TASK} status **Aborted**. -- @param #TASK self function TASK:IsStateAborted() return self:Is( "Aborted" ) end ---- Sets a @{Task} to status **Cancelled**. +--- Sets a @{#TASK} to status **Cancelled**. -- @param #TASK self function TASK:StateCancelled() self:SetState( self, "State", "Cancelled" ) return self end ---- Is the @{Task} status **Cancelled**. +--- Is the @{#TASK} status **Cancelled**. -- @param #TASK self function TASK:IsStateCancelled() return self:Is( "Cancelled" ) end ---- Sets a @{Task} to status **Assigned**. +--- Sets a @{#TASK} to status **Assigned**. -- @param #TASK self function TASK:StateAssigned() self:SetState( self, "State", "Assigned" ) return self end ---- Is the @{Task} status **Assigned**. +--- Is the @{#TASK} status **Assigned**. -- @param #TASK self function TASK:IsStateAssigned() return self:Is( "Assigned" ) end ---- Sets a @{Task} to status **Hold**. +--- Sets a @{#TASK} to status **Hold**. -- @param #TASK self function TASK:StateHold() self:SetState( self, "State", "Hold" ) return self end ---- Is the @{Task} status **Hold**. +--- Is the @{#TASK} status **Hold**. -- @param #TASK self function TASK:IsStateHold() return self:Is( "Hold" ) end ---- Sets a @{Task} to status **Replanned**. +--- Sets a @{#TASK} to status **Replanned**. -- @param #TASK self function TASK:StateReplanned() self:SetState( self, "State", "Replanned" ) return self end ---- Is the @{Task} status **Replanned**. +--- Is the @{#TASK} status **Replanned**. -- @param #TASK self function TASK:IsStateReplanned() return self:Is( "Replanned" ) end ---- Gets the @{Task} status. +--- Gets the @{#TASK} status. -- @param #TASK self function TASK:GetStateString() return self:GetState( self, "State" ) end ---- Sets a @{Task} briefing. +--- Sets a @{#TASK} briefing. -- @param #TASK self -- @param #string TaskBriefing -- @return #TASK self @@ -154576,7 +167644,7 @@ function TASK:SetBriefing( TaskBriefing ) return self end ---- Gets the @{Task} briefing. +--- Gets the @{#TASK} briefing. -- @param #TASK self -- @return #string The briefing text. function TASK:GetBriefing() @@ -155068,7 +168136,7 @@ do -- Task Control Menu end end ---- **Tasking** -- Controls the information of a Task. +--- **Tasking** - Controls the information of a Task. -- -- === -- @@ -155438,7 +168506,7 @@ function TASKINFO:Report( Report, Detail, ReportGroup, Task ) Report:AddIndent( LineReport:Text( ", " ) ) end ---- This module contains the TASK_MANAGER class and derived classes. +--- **Tasking** - This module contains the TASK_MANAGER class and derived classes. -- -- === -- @@ -155473,7 +168541,7 @@ end -- @image MOOSE.JPG do -- TASK_MANAGER - + --- TASK_MANAGER class. -- @type TASK_MANAGER -- @field Core.Set#SET_GROUP SetGroup The set of group objects containing players for which tasks are managed. @@ -155482,21 +168550,21 @@ do -- TASK_MANAGER ClassName = "TASK_MANAGER", SetGroup = nil, } - + --- TASK\_MANAGER constructor. -- @param #TASK_MANAGER self -- @param Core.Set#SET_GROUP SetGroup The set of group objects containing players for which tasks are managed. -- @return #TASK_MANAGER self function TASK_MANAGER:New( SetGroup ) - + -- Inherits from BASE local self = BASE:Inherit( self, FSM:New() ) -- #TASK_MANAGER - + self.SetGroup = SetGroup - + self:SetStartState( "Stopped" ) self:AddTransition( "Stopped", "StartTasks", "Started" ) - + --- StartTasks Handler OnBefore for TASK_MANAGER -- @function [parent=#TASK_MANAGER] OnBeforeStartTasks -- @param #TASK_MANAGER self @@ -155504,27 +168572,25 @@ do -- TASK_MANAGER -- @param #string Event -- @param #string To -- @return #boolean - + --- StartTasks Handler OnAfter for TASK_MANAGER -- @function [parent=#TASK_MANAGER] OnAfterStartTasks -- @param #TASK_MANAGER self -- @param #string From -- @param #string Event -- @param #string To - + --- StartTasks Trigger for TASK_MANAGER -- @function [parent=#TASK_MANAGER] StartTasks -- @param #TASK_MANAGER self - + --- StartTasks Asynchronous Trigger for TASK_MANAGER -- @function [parent=#TASK_MANAGER] __StartTasks -- @param #TASK_MANAGER self -- @param #number Delay - - - + self:AddTransition( "Started", "StopTasks", "Stopped" ) - + --- StopTasks Handler OnBefore for TASK_MANAGER -- @function [parent=#TASK_MANAGER] OnBeforeStopTasks -- @param #TASK_MANAGER self @@ -155532,28 +168598,27 @@ do -- TASK_MANAGER -- @param #string Event -- @param #string To -- @return #boolean - + --- StopTasks Handler OnAfter for TASK_MANAGER -- @function [parent=#TASK_MANAGER] OnAfterStopTasks -- @param #TASK_MANAGER self -- @param #string From -- @param #string Event -- @param #string To - + --- StopTasks Trigger for TASK_MANAGER -- @function [parent=#TASK_MANAGER] StopTasks -- @param #TASK_MANAGER self - + --- StopTasks Asynchronous Trigger for TASK_MANAGER -- @function [parent=#TASK_MANAGER] __StopTasks -- @param #TASK_MANAGER self -- @param #number Delay - self:AddTransition( "Started", "Manage", "Started" ) self:AddTransition( "Started", "Success", "Started" ) - + --- Success Handler OnAfter for TASK_MANAGER -- @function [parent=#TASK_MANAGER] OnAfterSuccess -- @param #TASK_MANAGER self @@ -155561,10 +168626,9 @@ do -- TASK_MANAGER -- @param #string Event -- @param #string To -- @param Tasking.Task#TASK Task - - + self:AddTransition( "Started", "Failed", "Started" ) - + --- Failed Handler OnAfter for TASK_MANAGER -- @function [parent=#TASK_MANAGER] OnAfterFailed -- @param #TASK_MANAGER self @@ -155572,10 +168636,9 @@ do -- TASK_MANAGER -- @param #string Event -- @param #string To -- @param Tasking.Task#TASK Task - - + self:AddTransition( "Started", "Aborted", "Started" ) - + --- Aborted Handler OnAfter for TASK_MANAGER -- @function [parent=#TASK_MANAGER] OnAfterAborted -- @param #TASK_MANAGER self @@ -155583,9 +168646,9 @@ do -- TASK_MANAGER -- @param #string Event -- @param #string To -- @param Tasking.Task#TASK Task - + self:AddTransition( "Started", "Cancelled", "Started" ) - + --- Cancelled Handler OnAfter for TASK_MANAGER -- @function [parent=#TASK_MANAGER] OnAfterCancelled -- @param #TASK_MANAGER self @@ -155595,37 +168658,37 @@ do -- TASK_MANAGER -- @param Tasking.Task#TASK Task self:SetRefreshTimeInterval( 30 ) - + return self end - + function TASK_MANAGER:onafterStartTasks( From, Event, To ) self:Manage() end - + function TASK_MANAGER:onafterManage( From, Event, To ) self:__Manage( -self._RefreshTimeInterval ) self:ManageTasks() end - + --- Set the refresh time interval in seconds when a new task management action needs to be done. -- @param #TASK_MANAGER self -- @param #number RefreshTimeInterval The refresh time interval in seconds when a new task management action needs to be done. -- @return #TASK_MANAGER self function TASK_MANAGER:SetRefreshTimeInterval( RefreshTimeInterval ) self:F2() - + self._RefreshTimeInterval = RefreshTimeInterval end - - + + --- Manages the tasks for the @{Core.Set#SET_GROUP}. -- @param #TASK_MANAGER self -- @return #TASK_MANAGER self function TASK_MANAGER:ManageTasks() - + end end @@ -155974,7 +169037,7 @@ do -- DETECTION_REPORTING return self end - --- Creates a string of the detected items in a @{Detection}. + --- Creates a string of the detected items in a @{Functional.Detection} object. -- @param #DETECTION_MANAGER self -- @param Core.Set#SET_UNIT DetectedSet The detected Set created by the @{Functional.Detection#DETECTION_BASE} object. -- @return #DETECTION_MANAGER self @@ -156027,27 +169090,28 @@ do -- DETECTION_REPORTING end ---- **Tasking** -- Dynamically allocates A2G tasks to human players, based on detected ground targets through reconnaissance. --- +--- **Tasking** - Dynamically allocates A2G tasks to human players, based on detected ground targets through reconnaissance. +-- -- **Features:** --- +-- -- * Dynamically assign tasks to human players based on detected targets. -- * Dynamically change the tasks as the tactical situation evolves during the mission. -- * Dynamically assign (CAS) Close Air Support tasks for human players. -- * Dynamically assign (BAI) Battlefield Air Interdiction tasks for human players. --- * Dynamically assign (SEAD) Supression of Enemy Air Defense tasks for human players to eliminate G2A missile threats. +-- * Dynamically assign (SEAD) Suppression of Enemy Air Defense tasks for human players to eliminate G2A missile threats. -- * Define and use an EWR (Early Warning Radar) network. -- * Define different ranges to engage upon intruders. -- * Keep task achievements. --- * Score task achievements.-- +-- * Score task achievements. +-- -- === --- +-- -- ### Author: **FlightControl** --- --- ### Contributions: --- +-- +-- ### Contributions: +-- -- === --- +-- -- @module Tasking.Task_A2G_Dispatcher -- @image Task_A2G_Dispatcher.JPG @@ -156060,152 +169124,152 @@ do -- TASK_A2G_DISPATCHER -- @field Tasking.Mission#MISSION Mission -- @extends Tasking.DetectionManager#DETECTION_MANAGER - --- Orchestrates dynamic **A2G Task Dispatching** based on the detection results of a linked @{Detection} object. - -- + --- Orchestrates dynamic **A2G Task Dispatching** based on the detection results of a linked @{Functional.Detection} object. + -- -- It uses the Tasking System within the MOOSE framework, which is a multi-player Tasking Orchestration system. -- It provides a truly dynamic battle environment for pilots and ground commanders to engage upon, -- in a true co-operation environment wherein **Multiple Teams** will collaborate in Missions to **achieve a common Mission Goal**. - -- - -- The A2G dispatcher will dispatch the A2G Tasks to a defined @{Set} of @{Wrapper.Group}s that will be manned by **Players**. - -- We call this the **AttackSet** of the A2G dispatcher. So, the Players are seated in the @{Client}s of the @{Wrapper.Group} @{Set}. - -- + -- + -- The A2G dispatcher will dispatch the A2G Tasks to a defined @{Core.Set} of @{Wrapper.Group}s that will be manned by **Players**. + -- We call this the **AttackSet** of the A2G dispatcher. So, the Players are seated in the @{Wrapper.Client}s of the @{Wrapper.Group} @{Core.Set}. + -- -- Depending on the actions of the enemy, preventive tasks are dispatched to the players to orchestrate the engagement in a true co-operation. - -- The detection object will group the detected targets by its grouping method, and integrates a @{Set} of @{Wrapper.Group}s that are Recce vehicles or air units. + -- The detection object will group the detected targets by its grouping method, and integrates a @{Core.Set} of @{Wrapper.Group}s that are Recce vehicles or air units. -- We call this the **RecceSet** of the A2G dispatcher. - -- + -- -- Depending on the current detected tactical situation, different task types will be dispatched to the Players seated in the AttackSet.. -- There are currently 3 **Task Types** implemented in the TASK\_A2G\_DISPATCHER: - -- + -- -- - **SEAD Task**: Dispatched when there are ground based Radar Emitters detected within an area. -- - **CAS Task**: Dispatched when there are no ground based Radar Emitters within the area, but there are friendly ground Units within 6 km from the enemy. -- - **BAI Task**: Dispatched when there are no ground based Radar Emitters within the area, and there aren't friendly ground Units within 6 km from the enemy. -- -- # 0. Tactical Situations - -- + -- -- This chapters provides some insights in the tactical situations when certain Task Types are created. -- The Task Types are depending on the enemy positions that were detected, and the current location of friendly units. - -- + -- -- ![](..\Presentations\TASK_A2G_DISPATCHER\Dia3.JPG) - -- + -- -- In the demonstration mission [TAD-A2G-000 - AREAS - Detection test], -- the tactical situation is a demonstration how the A2G detection works. -- This example will be taken further in the explanation in the following chapters. - -- + -- -- ![](..\Presentations\TASK_A2G_DISPATCHER\Dia4.JPG) - -- + -- -- The red coalition are the players, the blue coalition is the enemy. - -- + -- -- Red reconnaissance vehicles and airborne units are detecting the targets. -- We call this the RecceSet as explained above, which is a Set of Groups that -- have a group name starting with `Recce` (configured in the mission script). - -- + -- -- Red attack units are responsible for executing the mission for the command center. -- We call this the AttackSet, which is a Set of Groups with a group name starting with `Attack` (configured in the mission script). -- These units are setup in this demonstration mission to be ground vehicles and airplanes. -- For demonstration purposes, the attack airplane is stationed on the ground to explain -- the messages and the menus properly. -- Further test missions demonstrate the A2G task dispatcher from within air. - -- + -- -- Depending upon the detection results, the A2G dispatcher will create different tasks. - -- + -- -- # 0.1. SEAD Task - -- + -- -- A SEAD Task is dispatched when there are ground based Radar Emitters detected within an area. - -- + -- -- ![](..\Presentations\TASK_A2G_DISPATCHER\Dia9.JPG) - -- + -- -- - Once all Radar Emitting Units have been destroyed, the Task will convert into a BAI or CAS task! -- - A CAS and BAI task may be converted into a SEAD task, once a radar has been detected within the area! - -- + -- -- # 0.2. CAS Task - -- + -- -- A CAS Task is dispatched when there are no ground based Radar Emitters within the area, but there are friendly ground Units within 6 km from the enemy. - -- + -- -- ![](..\Presentations\TASK_A2G_DISPATCHER\Dia10.JPG) - -- + -- -- - After the detection of the CAS task, if the friendly Units are destroyed, the CAS task will convert into a BAI task! -- - Only ground Units are taken into account. Airborne units are ships are not considered friendlies that require Close Air Support. - -- + -- -- # 0.3. BAI Task - -- + -- -- A BAI Task is dispatched when there are no ground based Radar Emitters within the area, and there aren't friendly ground Units within 6 km from the enemy. - -- + -- -- ![](..\Presentations\TASK_A2G_DISPATCHER\Dia11.JPG) -- -- - A BAI task may be converted into a CAS task if friendly Ground Units approach within 6 km range! -- -- # 1. Player Experience - -- - -- The A2G dispatcher is residing under a @{CommandCenter}, which is orchestrating a @{Mission}. + -- + -- The A2G dispatcher is residing under a @{Tasking.CommandCenter}, which is orchestrating a @{Tasking.Mission}. -- As a result, you'll find for DCS World missions that implement the A2G dispatcher a **Command Center Menu** and under this one or more **Mission Menus**. - -- + -- -- For example, if there are 2 Command Centers (CC). -- Each CC is controlling a couple of Missions, the Radio Menu Structure could look like this: - -- + -- -- Radio MENU Structure (F10. Other) - -- + -- -- F1. Command Center [Gori] -- F1. Mission "Alpha (Primary)" -- F2. Mission "Beta (Secondary)" -- F3. Mission "Gamma (Tactical)" -- F1. Command Center [Lima] -- F1. Mission "Overlord (High)" - -- - -- Command Center [Gori] is controlling Mission "Alpha", "Beta", "Gamma". Alpha is the Primary mission, Beta the Secondary and there is a Tacical mission Gamma. + -- + -- Command Center [Gori] is controlling Mission "Alpha", "Beta", "Gamma". Alpha is the Primary mission, Beta the Secondary and there is a Tactical mission Gamma. -- Command Center [Lima] is controlling Missions "Overlord", which needs to be executed with High priority. -- -- ## 1.1. Mission Menu (Under the Command Center Menu) - -- + -- -- The Mission Menu controls the information of the mission, including the: - -- + -- -- - **Mission Briefing**: A briefing of the Mission in text, which will be shown as a message. -- - **Mark Task Locations**: A summary of each Task will be shown on the map as a marker. -- - **Create Task Reports**: A menu to create various reports of the current tasks dispatched by the A2G dispatcher. -- - **Create Mission Reports**: A menu to create various reports on the current mission. - -- + -- -- For CC [Lima], Mission "Overlord", the menu structure could look like this: - -- + -- -- Radio MENU Structure (F10. Other) - -- + -- -- F1. Command Center [Lima] -- F1. Mission "Overlord" -- F1. Mission Briefing -- F2. Mark Task Locations on Map -- F3. Task Reports -- F4. Mission Reports - -- + -- -- ![](..\Presentations\TASK_A2G_DISPATCHER\Dia5.JPG) - -- + -- -- ### 1.1.1. Mission Briefing Menu - -- + -- -- The Mission Briefing Menu will show in text a summary description of the overall mission objectives and expectations. -- Note that the Mission Briefing is not the briefing of a specific task, but rather provides an overall strategy and tactical situation, - -- and explains the mission goals. - -- - -- + -- and explains the mission goals. + -- + -- -- ### 1.1.2. Mark Task Locations Menu - -- + -- -- The Mark Task Locations Menu will mark the location indications of the Tasks on the map, if this intelligence is known by the Command Center. -- For A2G tasks this information will always be know, but it can be that for other tasks a location intelligence will be less relevant. -- Note that each Planned task and each Engaged task will be marked. Completed, Failed and Cancelled tasks are not marked. -- Depending on the task type, a summary information is shown to bring to the player the relevant information for situational awareness. - -- + -- -- ### 1.1.3. Task Reports Menu - -- + -- -- The Task Reports Menu is a sub menu, that allows to create various reports: - -- + -- -- - **Tasks Summary**: This report will list all the Tasks that are or were active within the mission, indicating its status. -- - **Planned Tasks**: This report will list all the Tasks that are in status Planned, which are Tasks not assigned to any player, and are ready to be executed. -- - **Assigned Tasks**: This report will list all the Tasks that are in status Assigned, which are Tasks assigned to (a) player(s) and are currently executed. -- - **Successful Tasks**: This report will list all the Tasks that are in status Success, which are Tasks executed by (a) player(s) and are completed successfully. -- - **Failed Tasks**: This report will list all the Tasks that are in status Success, which are Tasks executed by (a) player(s) and that have failed. - -- + -- -- The information shown of the tasks will vary according the underlying task type, but are self explanatory. -- -- For CC [Gori], Mission "Alpha", the Task Reports menu structure could look like this: - -- + -- -- Radio MENU Structure (F10. Other) - -- + -- -- F1. Command Center [Gori] -- F1. Mission "Alpha" -- F1. Mission Briefing @@ -156217,21 +169281,21 @@ do -- TASK_A2G_DISPATCHER -- F4. Successful Tasks -- F5. Failed Tasks -- F4. Mission Reports - -- + -- -- Note that these reports provide an "overview" of the tasks. Detailed information of the task can be retrieved using the Detailed Report on the Task Menu. -- (See later). - -- + -- -- ### 1.1.4. Mission Reports Menu - -- + -- -- The Mission Reports Menu is a sub menu, that provides options to retrieve further information on the current Mission: - -- - -- - **Report Mission Progress**: Shows the progress of the current Mission. Each Task has a %-tage of completion. + -- + -- - **Report Mission Progress**: Shows the progress of the current Mission. Each Task has a % of completion. -- - **Report Players per Task**: Show which players are engaged on which Task within the Mission. - -- + -- -- For CC |Gori|, Mission "Alpha", the Mission Reports menu structure could look like this: - -- + -- -- Radio MENU Structure (F10. Other) - -- + -- -- F1. Command Center [Gori] -- F1. Mission "Alpha" -- F1. Mission Briefing @@ -156240,25 +169304,25 @@ do -- TASK_A2G_DISPATCHER -- F4. Mission Reports -- F1. Report Mission Progress -- F2. Report Players per Task - -- - -- + -- + -- -- ## 1.2. Task Management Menus - -- + -- -- Very important to remember is: **Multiple Players can be assigned to the same Task, but from the player perspective, the Player can only be assigned to one Task per Mission at the same time!** -- Consider this like the two major modes in which a player can be in. He can be free of tasks or he can be assigned to a Task. -- Depending on whether a Task has been Planned or Assigned to a Player (Group), -- **the Mission Menu will contain extra Menus to control specific Tasks.** - -- + -- -- #### 1.2.1. Join a Planned Task - -- + -- -- If the Player has not yet been assigned to a Task within the Mission, the Mission Menu will contain additionally a: - -- + -- -- - Join Planned Task Menu: This menu structure allows the player to join a planned task (a Task with status Planned). - -- + -- -- For CC |Gori|, Mission "Alpha", the menu structure could look like this: - -- + -- -- Radio MENU Structure (F10. Other) - -- + -- -- F1. Command Center [Gori] -- F1. Mission "Alpha" -- F1. Mission Briefing @@ -156266,23 +169330,23 @@ do -- TASK_A2G_DISPATCHER -- F3. Task Reports -- F4. Mission Reports -- F5. Join Planned Task - -- + -- -- **The F5. Join Planned Task allows the player to join a Planned Task and take an engagement in the running Mission.** - -- - -- #### 1.2.2. Manage an Assigned Task - -- + -- + -- #### 1.2.2. Manage an Assigned Task + -- -- If the Player has been assigned to one Task within the Mission, the Mission Menu will contain an extra: - -- + -- -- - Assigned Task __TaskName__ Menu: This menu structure allows the player to take actions on the currently engaged task. - -- + -- -- In this example, the Group currently seated by the player is not assigned yet to a Task. -- The Player has the option to assign itself to a Planned Task using menu option F5 under the Mission Menu "Alpha". - -- + -- -- This would be an example menu structure, -- for CC |Gori|, Mission "Alpha", when a player would have joined Task CAS.001: - -- + -- -- Radio MENU Structure (F10. Other) - -- + -- -- F1. Command Center [Gori] -- F1. Mission "Alpha" -- F1. Mission Briefing @@ -156290,26 +169354,25 @@ do -- TASK_A2G_DISPATCHER -- F3. Task Reports -- F4. Mission Reports -- F5. Assigned Task CAS.001 - -- + -- -- **The F5. Assigned Task __TaskName__ allows the player to control the current Assigned Task and take further actions.** - -- - -- + -- -- ## 1.3. Join Planned Task Menu - -- + -- -- The Join Planned Task Menu contains the different Planned A2G Tasks **in a structured Menu Hierarchy**. - -- The Menu Hierarchy is structuring the Tasks per **Task Type**, and then by **Task Name (ID)**. - -- - -- For example, for CC [Gori], Mission "Alpha", + -- The Menu Hierarchy is structuring the Tasks per **Task Type**, and then by **Task Name (ID)**. + -- + -- For example, for CC [Gori], Mission "Alpha", -- if a Mission "ALpha" contains 5 Planned Tasks, which would be: - -- - -- - 2 CAS Tasks + -- + -- - 2 CAS Tasks -- - 1 BAI Task -- - 2 SEAD Tasks - -- + -- -- the Join Planned Task Menu Hierarchy could look like this: - -- + -- -- Radio MENU Structure (F10. Other) - -- + -- -- F1. Command Center [Gori] -- F1. Mission "Alpha" -- F1. Mission Briefing @@ -156325,26 +169388,26 @@ do -- TASK_A2G_DISPATCHER -- F1. SEAD.003 -- F2. SEAD.004 -- F3. SEAD.005 - -- + -- -- An example from within a running simulation: - -- + -- -- ![](..\Presentations\TASK_A2G_DISPATCHER\Dia6.JPG) - -- + -- -- Each Task Type Menu would have a list of the Task Menus underneath. -- Each Task Menu (eg. `CAS.001`) has a **detailed Task Menu structure to control the specific task**! -- -- ### 1.3.1. Planned Task Menu -- -- Each Planned Task Menu will allow for the following actions: - -- + -- -- - Report Task Details: Provides a detailed report on the Planned Task. -- - Mark Task Location on Map: Mark the approximate location of the Task on the Map, if relevant. -- - Join Task: Join the Task. This is THE menu option to let a Player join the Task, and to engage within the Mission. - -- - -- The Join Planned Task Menu could look like this for for CC |Gori|, Mission "Alpha": - -- + -- + -- The Join Planned Task Menu could look like this for for CC |Gori|, Mission "Alpha": + -- -- Radio MENU Structure (F10. Other) - -- + -- -- F1. Command Center |Gori| -- F1. Mission "Alpha" -- F1. Mission Briefing @@ -156357,22 +169420,22 @@ do -- TASK_A2G_DISPATCHER -- F1. Report Task Details -- F2. Mark Task Location on Map -- F3. Join Task - -- + -- -- **The Join Task is THE menu option to let a Player join the Task, and to engage within the Mission.** - -- - -- + -- + -- -- ## 1.4. Assigned Task Menu - -- + -- -- The Assigned Task Menu allows to control the **current assigned task** within the Mission. - -- + -- -- Depending on the Type of Task, the following menu options will be available: - -- + -- -- - **Report Task Details**: Provides a detailed report on the Planned Task. -- - **Mark Task Location on Map**: Mark the approximate location of the Task on the Map, if relevant. -- - **Abort Task: Abort the current assigned Task:** This menu option lets the player abort the Task. - -- + -- -- For example, for CC |Gori|, Mission "Alpha", the Assigned Menu could be: - -- + -- -- F1. Command Center |Gori| -- F1. Mission "Alpha" -- F1. Mission Briefing @@ -156383,90 +169446,89 @@ do -- TASK_A2G_DISPATCHER -- F1. Report Task Details -- F2. Mark Task Location on Map -- F3. Abort Task - -- + -- -- Task abortion will result in the Task to be Cancelled, and the Task **may** be **Replanned**. - -- However, this will depend on the setup of each Mission. - -- + -- However, this will depend on the setup of each Mission. + -- -- ## 1.5. Messages - -- + -- -- During game play, different messages are displayed. -- These messages provide an update of the achievements made, and the state wherein the task is. - -- + -- -- The various reports can be used also to retrieve the current status of the mission and its tasks. - -- + -- -- ![](..\Presentations\TASK_A2G_DISPATCHER\Dia7.JPG) - -- - -- The @{Settings} menu provides additional options to control the timing of the messages. + -- + -- The @{Core.Settings} menu provides additional options to control the timing of the messages. -- There are: - -- + -- -- - Status messages, which are quick status updates. The settings menu allows to switch off these messages. -- - Information messages, which are shown a bit longer, as they contain important information. -- - Summary reports, which are quick reports showing a high level summary. -- - Overview reports, which are providing the essential information. It provides an overview of a greater thing, and may take a bit of time to read. -- - Detailed reports, which provide with very detailed information. It takes a bit longer to read those reports, so the display of those could be a bit longer. - -- + -- -- # 2. TASK\_A2G\_DISPATCHER constructor - -- + -- -- The @{#TASK_A2G_DISPATCHER.New}() method creates a new TASK\_A2G\_DISPATCHER instance. -- -- # 3. Usage -- -- To use the TASK\_A2G\_DISPATCHER class, you need: - -- - -- - A @{CommandCenter} object. The master communication channel. - -- - A @{Mission} object. Each task belongs to a Mission. - -- - A @{Detection} object. There are several detection grouping methods to choose from. - -- - A @{Task_A2G_Dispatcher} object. The master A2G task dispatcher. - -- - A @{Set} of @{Wrapper.Group} objects that will detect the emeny, the RecceSet. This is attached to the @{Detection} object. - -- - A @{Set} ob @{Wrapper.Group} objects that will attack the enemy, the AttackSet. This is attached to the @{Task_A2G_Dispatcher} object. - -- - -- Below an example mission declaration that is defines a Task A2G Dispatcher object. -- - -- -- Declare the Command Center + -- - A @{Tasking.CommandCenter} object. The master communication channel. + -- - A @{Tasking.Mission} object. Each task belongs to a Mission. + -- - A @{Functional.Detection} object. There are several detection grouping methods to choose from. + -- - A @{Tasking.Task_A2G_Dispatcher} object. The master A2G task dispatcher. + -- - A @{Core.Set} of @{Wrapper.Group} objects that will detect the enemy, the RecceSet. This is attached to the @{Functional.Detection} object. + -- - A @{Core.Set} of @{Wrapper.Group} objects that will attack the enemy, the AttackSet. This is attached to the @{Tasking.Task_A2G_Dispatcher} object. + -- + -- Below an example mission declaration that is defines a Task A2G Dispatcher object. + -- + -- -- Declare the Command Center -- local HQ = GROUP -- :FindByName( "HQ", "Bravo HQ" ) -- -- local CommandCenter = COMMANDCENTER -- :New( HQ, "Lima" ) - -- + -- -- -- Declare the Mission for the Command Center. -- local Mission = MISSION -- :New( CommandCenter, "Overlord", "High", "Attack Detect Mission Briefing", coalition.side.RED ) - -- + -- -- -- Define the RecceSet that will detect the enemy. -- local RecceSet = SET_GROUP -- :New() -- :FilterPrefixes( "FAC" ) -- :FilterCoalitions("red") -- :FilterStart() - -- + -- -- -- Setup the detection. We use DETECTION_AREAS to detect and group the enemies within areas of 3 km radius. -- local DetectionAreas = DETECTION_AREAS -- :New( RecceSet, 3000 ) -- The RecceSet will detect the enemies. - -- + -- -- -- Setup the AttackSet, which is a SET_GROUP. - -- -- The SET_GROUP is a dynamic collection of GROUP objects. + -- -- The SET_GROUP is a dynamic collection of GROUP objects. -- local AttackSet = SET_GROUP -- :New() -- Create the SET_GROUP object. -- :FilterCoalitions( "red" ) -- Only incorporate the RED coalitions. -- :FilterPrefixes( "Attack" ) -- Only incorporate groups that start with the name Attack. -- :FilterStart() -- Enable the dynamic filtering. From this moment the AttackSet will contain all groups that are red and start with the name Attack. - -- + -- -- -- Now we have everything to setup the main A2G TaskDispatcher. -- TaskDispatcher = TASK_A2G_DISPATCHER - -- :New( Mission, AttackSet, DetectionAreas ) -- We assign the TaskDispatcher under Mission. The AttackSet will engage the enemy and will recieve the dispatched Tasks. The DetectionAreas will report any detected enemies to the TaskDispatcher. - -- - -- + -- :New( Mission, AttackSet, DetectionAreas ) -- We assign the TaskDispatcher under Mission. The AttackSet will engage the enemy and will receive the dispatched Tasks. The DetectionAreas will report any detected enemies to the TaskDispatcher. + -- + -- -- -- @field #TASK_A2G_DISPATCHER TASK_A2G_DISPATCHER = { ClassName = "TASK_A2G_DISPATCHER", Mission = nil, Detection = nil, - Tasks = {}, + Tasks = {} } - - + --- TASK_A2G_DISPATCHER constructor. -- @param #TASK_A2G_DISPATCHER self -- @param Tasking.Mission#MISSION Mission The mission for which the task dispatching is done. @@ -156474,18 +169536,18 @@ do -- TASK_A2G_DISPATCHER -- @param Functional.Detection#DETECTION_BASE Detection The detection results that are used to dynamically assign new tasks to human players. -- @return #TASK_A2G_DISPATCHER self function TASK_A2G_DISPATCHER:New( Mission, SetGroup, Detection ) - + -- Inherits from DETECTION_MANAGER local self = BASE:Inherit( self, DETECTION_MANAGER:New( SetGroup, Detection ) ) -- #TASK_A2G_DISPATCHER - + self.Detection = Detection self.Mission = Mission - self.FlashNewTask = true --set to false to suppress flash messages - + self.FlashNewTask = true -- set to false to suppress flash messages + self.Detection:FilterCategories( { Unit.Category.GROUND_UNIT } ) - + self:AddTransition( "Started", "Assign", "Started" ) - + --- OnAfter Transition Handler for Event Assign. -- @function [parent=#TASK_A2G_DISPATCHER] OnAfterAssign -- @param #TASK_A2G_DISPATCHER self @@ -156495,19 +169557,19 @@ do -- TASK_A2G_DISPATCHER -- @param Tasking.Task_A2G#TASK_A2G Task -- @param Wrapper.Unit#UNIT TaskUnit -- @param #string PlayerName - + self:__Start( 5 ) - + return self end - - --- Set flashing player messages on or off + + --- Set flashing player messages on or off -- @param #TASK_A2G_DISPATCHER self -- @param #boolean onoff Set messages on (true) or off (false) function TASK_A2G_DISPATCHER:SetSendMessages( onoff ) - self.FlashNewTask = onoff + self.FlashNewTask = onoff end - + --- Creates a SEAD task when there are targets for it. -- @param #TASK_A2G_DISPATCHER self -- @param Functional.Detection#DETECTION_AREAS.DetectedItem DetectedItem @@ -156515,7 +169577,7 @@ do -- TASK_A2G_DISPATCHER -- @return #nil If there are no targets to be set. function TASK_A2G_DISPATCHER:EvaluateSEAD( DetectedItem ) self:F( { DetectedItem.ItemID } ) - + local DetectedSet = DetectedItem.Set local DetectedZone = DetectedItem.Zone @@ -156529,10 +169591,10 @@ do -- TASK_A2G_DISPATCHER TargetSetUnit:SetDatabase( DetectedSet ) TargetSetUnit:FilterHasSEAD() TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection. - + return TargetSetUnit end - + return nil end @@ -156543,11 +169605,10 @@ do -- TASK_A2G_DISPATCHER -- @return #nil If there are no targets to be set. function TASK_A2G_DISPATCHER:EvaluateCAS( DetectedItem ) self:F( { DetectedItem.ItemID } ) - + local DetectedSet = DetectedItem.Set local DetectedZone = DetectedItem.Zone - -- Determine if the set has ground units. -- There should be ground unit friendlies nearby. Airborne units are valid friendlies types. -- And there shouldn't be any radar. @@ -156561,13 +169622,13 @@ do -- TASK_A2G_DISPATCHER local TargetSetUnit = SET_UNIT:New() TargetSetUnit:SetDatabase( DetectedSet ) TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection. - + return TargetSetUnit end - + return nil end - + --- Creates a BAI task when there are targets for it. -- @param #TASK_A2G_DISPATCHER self -- @param Functional.Detection#DETECTION_AREAS.DetectedItem DetectedItem @@ -156575,11 +169636,10 @@ do -- TASK_A2G_DISPATCHER -- @return #nil If there are no targets to be set. function TASK_A2G_DISPATCHER:EvaluateBAI( DetectedItem, FriendlyCoalition ) self:F( { DetectedItem.ItemID } ) - + local DetectedSet = DetectedItem.Set local DetectedZone = DetectedItem.Zone - -- Determine if the set has ground units. -- There shouldn't be any ground unit friendlies nearby. -- And there shouldn't be any radar. @@ -156593,19 +169653,18 @@ do -- TASK_A2G_DISPATCHER local TargetSetUnit = SET_UNIT:New() TargetSetUnit:SetDatabase( DetectedSet ) TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection. - + return TargetSetUnit end - + return nil end - - + function TASK_A2G_DISPATCHER:RemoveTask( TaskIndex ) self.Mission:RemoveTask( self.Tasks[TaskIndex] ) self.Tasks[TaskIndex] = nil end - + --- Evaluates the removal of the Task from the Mission. -- Can only occur when the DetectedItem is Changed AND the state of the Task is "Planned". -- @param #TASK_A2G_DISPATCHER self @@ -156615,17 +169674,16 @@ do -- TASK_A2G_DISPATCHER -- @param #boolean DetectedItemChange -- @return Tasking.Task#TASK function TASK_A2G_DISPATCHER:EvaluateRemoveTask( Mission, Task, TaskIndex, DetectedItemChanged ) - + if Task then - if ( Task:IsStatePlanned() and DetectedItemChanged == true ) or Task:IsStateCancelled() then - --self:F( "Removing Tasking: " .. Task:GetTaskName() ) + if (Task:IsStatePlanned() and DetectedItemChanged == true) or Task:IsStateCancelled() then + -- self:F( "Removing Tasking: " .. Task:GetTaskName() ) self:RemoveTask( TaskIndex ) end end - + return Task end - --- Assigns tasks in relation to the detected items to the @{Core.Set#SET_GROUP}. -- @param #TASK_A2G_DISPATCHER self @@ -156633,15 +169691,15 @@ do -- TASK_A2G_DISPATCHER -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop. function TASK_A2G_DISPATCHER:ProcessDetected( Detection ) self:F() - + local AreaMsg = {} local TaskMsg = {} local ChangeMsg = {} - + local Mission = self.Mission - + if Mission:IsIDLE() or Mission:IsENGAGED() then - + local TaskReport = REPORT:New() -- Checking the task queue for the dispatcher, and removing any obsolete task! @@ -156663,21 +169721,21 @@ do -- TASK_A2G_DISPATCHER --- First we need to the detected targets. for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do - + local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT local DetectedZone = DetectedItem.Zone - --self:F( { "Targets in DetectedItem", DetectedItem.ItemID, DetectedSet:Count(), tostring( DetectedItem ) } ) - --DetectedSet:Flush( self ) - + -- self:F( { "Targets in DetectedItem", DetectedItem.ItemID, DetectedSet:Count(), tostring( DetectedItem ) } ) + -- DetectedSet:Flush( self ) + local DetectedItemID = DetectedItem.ID local TaskIndex = DetectedItem.Index local DetectedItemChanged = DetectedItem.Changed - + self:F( { DetectedItemChanged = DetectedItemChanged, DetectedItemID = DetectedItemID, TaskIndex = TaskIndex } ) - + local Task = self.Tasks[TaskIndex] -- Tasking.Task_A2G#TASK_A2G - + if Task then -- If there is a Task and the task was assigned, then we check if the task was changed ... If it was, we need to reevaluate the targets. if Task:IsStateAssigned() then @@ -156689,7 +169747,7 @@ do -- TASK_A2G_DISPATCHER Task:SetTargetSetUnit( TargetSetUnit ) Task:SetDetection( Detection, DetectedItem ) Task:UpdateTaskInfo( DetectedItem ) - TargetsReport:Add( Detection:GetChangeText( DetectedItem ) ) + TargetsReport:Add( Detection:GetChangeText( DetectedItem ) ) else Task:Cancel() end @@ -156720,18 +169778,18 @@ do -- TASK_A2G_DISPATCHER end end end - + -- Now we send to each group the changes, if any. for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do - local TargetsText = TargetsReport:Text(", ") - if ( Mission:IsGroupAssigned(TaskGroup) ) and TargetsText ~= "" and self.FlashNewTask then + local TargetsText = TargetsReport:Text( ", " ) + if (Mission:IsGroupAssigned( TaskGroup )) and TargetsText ~= "" and self.FlashNewTask then Mission:GetCommandCenter():MessageToGroup( string.format( "Task %s has change of targets:\n %s", Task:GetName(), TargetsText ), TaskGroup ) end end end end end - + if Task then if Task:IsStatePlanned() then if DetectedItemChanged == true then -- The detection has changed, thus a new TargetSet is to be evaluated and set @@ -156782,7 +169840,7 @@ do -- TASK_A2G_DISPATCHER local TargetSetUnit = self:EvaluateSEAD( DetectedItem ) -- Returns a SetUnit if there are targets to be SEADed... if TargetSetUnit then Task = TASK_A2G_SEAD:New( Mission, self.SetGroup, string.format( "SEAD.%03d", DetectedItemID ), TargetSetUnit ) - DetectedItem.DesignateMenuName = string.format( "SEAD.%03d", DetectedItemID ) --inject a name for DESIGNATE, if using same DETECTION object + DetectedItem.DesignateMenuName = string.format( "SEAD.%03d", DetectedItemID ) -- inject a name for DESIGNATE, if using same DETECTION object Task:SetDetection( Detection, DetectedItem ) end @@ -156791,7 +169849,7 @@ do -- TASK_A2G_DISPATCHER local TargetSetUnit = self:EvaluateCAS( DetectedItem ) -- Returns a SetUnit if there are targets to be CASed... if TargetSetUnit then Task = TASK_A2G_CAS:New( Mission, self.SetGroup, string.format( "CAS.%03d", DetectedItemID ), TargetSetUnit ) - DetectedItem.DesignateMenuName = string.format( "CAS.%03d", DetectedItemID ) --inject a name for DESIGNATE, if using same DETECTION object + DetectedItem.DesignateMenuName = string.format( "CAS.%03d", DetectedItemID ) -- inject a name for DESIGNATE, if using same DETECTION object Task:SetDetection( Detection, DetectedItem ) end @@ -156800,19 +169858,19 @@ do -- TASK_A2G_DISPATCHER local TargetSetUnit = self:EvaluateBAI( DetectedItem, self.Mission:GetCommandCenter():GetPositionable():GetCoalition() ) -- Returns a SetUnit if there are targets to be BAIed... if TargetSetUnit then Task = TASK_A2G_BAI:New( Mission, self.SetGroup, string.format( "BAI.%03d", DetectedItemID ), TargetSetUnit ) - DetectedItem.DesignateMenuName = string.format( "BAI.%03d", DetectedItemID ) --inject a name for DESIGNATE, if using same DETECTION object + DetectedItem.DesignateMenuName = string.format( "BAI.%03d", DetectedItemID ) -- inject a name for DESIGNATE, if using same DETECTION object Task:SetDetection( Detection, DetectedItem ) end end end - + if Task then self.Tasks[TaskIndex] = Task Task:SetTargetZone( DetectedZone ) Task:SetDispatcher( self ) Task:UpdateTaskInfo( DetectedItem ) Mission:AddTask( Task ) - + function Task.OnEnterSuccess( Task, From, Event, To ) self:Success( Task ) end @@ -156820,7 +169878,7 @@ do -- TASK_A2G_DISPATCHER function Task.OnEnterCancelled( Task, From, Event, To ) self:Cancelled( Task ) end - + function Task.OnEnterFailed( Task, From, Event, To ) self:Failed( Task ) end @@ -156828,45 +169886,43 @@ do -- TASK_A2G_DISPATCHER function Task.OnEnterAborted( Task, From, Event, To ) self:Aborted( Task ) end - - + TaskReport:Add( Task:GetName() ) else - self:F("This should not happen") + self:F( "This should not happen" ) end end - -- OK, so the tasking has been done, now delete the changes reported for the area. Detection:AcceptChanges( DetectedItem ) end - + -- TODO set menus using the HQ coordinator Mission:GetCommandCenter():SetMenu() - - local TaskText = TaskReport:Text(", ") + + local TaskText = TaskReport:Text( ", " ) for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do - if ( not Mission:IsGroupAssigned(TaskGroup) ) and TaskText ~= "" and self.FlashNewTask then + if (not Mission:IsGroupAssigned( TaskGroup )) and TaskText ~= "" and self.FlashNewTask then Mission:GetCommandCenter():MessageToGroup( string.format( "%s has tasks %s. Subscribe to a task using the radio menu.", Mission:GetShortText(), TaskText ), TaskGroup ) end end - + end - + return true end end --- **Tasking** - The TASK_A2G models tasks for players in Air to Ground engagements. --- +-- -- === --- +-- -- ### Author: **FlightControl** --- --- ### Contributions: --- +-- +-- ### Contributions: +-- -- === --- +-- -- @module Tasking.Task_A2G -- @image MOOSE.JPG @@ -156877,29 +169933,29 @@ do -- TASK_A2G -- @field Core.Set#SET_UNIT TargetSetUnit -- @extends Tasking.Task#TASK - --- The TASK_A2G class defines Air To Ground tasks for a @{Set} of Target Units, + --- The TASK_A2G class defines Air To Ground tasks for a @{Core.Set} of Target Units, -- based on the tasking capabilities defined in @{Tasking.Task#TASK}. -- The TASK_A2G is implemented using a @{Core.Fsm#FSM_TASK}, and has the following statuses: - -- + -- -- * **None**: Start of the process -- * **Planned**: The A2G task is planned. -- * **Assigned**: The A2G task is assigned to a @{Wrapper.Group#GROUP}. -- * **Success**: The A2G task is successfully completed. -- * **Failed**: The A2G task has failed. This will happen if the player exists the task early, without communicating a possible cancellation to HQ. - -- + -- -- ## 1) Set the scoring of achievements in an A2G attack. - -- + -- -- Scoring or penalties can be given in the following circumstances: - -- + -- -- * @{#TASK_A2G.SetScoreOnDestroy}(): Set a score when a target in scope of the A2G attack, has been destroyed. -- * @{#TASK_A2G.SetScoreOnSuccess}(): Set a score when all the targets in scope of the A2G attack, have been destroyed. -- * @{#TASK_A2G.SetPenaltyOnFailed}(): Set a penalty when the A2G attack has failed. - -- + -- -- @field #TASK_A2G TASK_A2G = { - ClassName = "TASK_A2G", + ClassName = "TASK_A2G" } - + --- Instantiates a new TASK_A2G. -- @param #TASK_A2G self -- @param Tasking.Mission#MISSION Mission @@ -156913,53 +169969,51 @@ do -- TASK_A2G function TASK_A2G:New( Mission, SetGroup, TaskName, TargetSetUnit, TaskType, TaskBriefing ) local self = BASE:Inherit( self, TASK:New( Mission, SetGroup, TaskName, TaskType, TaskBriefing ) ) -- Tasking.Task#TASK_A2G self:F() - + self.TargetSetUnit = TargetSetUnit self.TaskType = TaskType - + local Fsm = self:GetUnitProcess() - + Fsm:AddTransition( "Assigned", "RouteToRendezVous", "RoutingToRendezVous" ) - Fsm:AddProcess ( "RoutingToRendezVous", "RouteToRendezVousPoint", ACT_ROUTE_POINT:New(), { Arrived = "ArriveAtRendezVous" } ) - Fsm:AddProcess ( "RoutingToRendezVous", "RouteToRendezVousZone", ACT_ROUTE_ZONE:New(), { Arrived = "ArriveAtRendezVous" } ) - + Fsm:AddProcess( "RoutingToRendezVous", "RouteToRendezVousPoint", ACT_ROUTE_POINT:New(), { Arrived = "ArriveAtRendezVous" } ) + Fsm:AddProcess( "RoutingToRendezVous", "RouteToRendezVousZone", ACT_ROUTE_ZONE:New(), { Arrived = "ArriveAtRendezVous" } ) + Fsm:AddTransition( { "Arrived", "RoutingToRendezVous" }, "ArriveAtRendezVous", "ArrivedAtRendezVous" ) - + Fsm:AddTransition( { "ArrivedAtRendezVous", "HoldingAtRendezVous" }, "Engage", "Engaging" ) Fsm:AddTransition( { "ArrivedAtRendezVous", "HoldingAtRendezVous" }, "HoldAtRendezVous", "HoldingAtRendezVous" ) - - Fsm:AddProcess ( "Engaging", "Account", ACT_ACCOUNT_DEADS:New(), {} ) + + Fsm:AddProcess( "Engaging", "Account", ACT_ACCOUNT_DEADS:New(), {} ) Fsm:AddTransition( "Engaging", "RouteToTarget", "Engaging" ) Fsm:AddProcess( "Engaging", "RouteToTargetZone", ACT_ROUTE_ZONE:New(), {} ) Fsm:AddProcess( "Engaging", "RouteToTargetPoint", ACT_ROUTE_POINT:New(), {} ) Fsm:AddTransition( "Engaging", "RouteToTargets", "Engaging" ) - - --Fsm:AddTransition( "Accounted", "DestroyedAll", "Accounted" ) - --Fsm:AddTransition( "Accounted", "Success", "Success" ) + + -- Fsm:AddTransition( "Accounted", "DestroyedAll", "Accounted" ) + -- Fsm:AddTransition( "Accounted", "Success", "Success" ) Fsm:AddTransition( "Rejected", "Reject", "Aborted" ) Fsm:AddTransition( "Failed", "Fail", "Failed" ) - - - --- Test + --- Test -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit -- @param Tasking.Task_A2G#TASK_A2G Task function Fsm:onafterAssigned( TaskUnit, Task ) self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) -- Determine the first Unit from the self.RendezVousSetUnit - + self:RouteToRendezVous() end - - --- Test + + --- Test -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit -- @param Tasking.Task_A2G#TASK_A2G Task function Fsm:onafterRouteToRendezVous( TaskUnit, Task ) self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) -- Determine the first Unit from the self.RendezVousSetUnit - + if Task:GetRendezVousZone( TaskUnit ) then self:__RouteToRendezVousZone( 0.1 ) else @@ -156971,36 +170025,36 @@ do -- TASK_A2G end end - --- Test + --- Test -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit -- @param Tasking.Task#TASK_A2G Task function Fsm:OnAfterArriveAtRendezVous( TaskUnit, Task ) self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) -- Determine the first Unit from the self.TargetSetUnit - - self:__Engage( 0.1 ) + + self:__Engage( 0.1 ) end - - --- Test + + --- Test -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit -- @param Tasking.Task#TASK_A2G Task function Fsm:onafterEngage( TaskUnit, Task ) self:F( { self } ) self:__Account( 0.1 ) - self:__RouteToTarget(0.1 ) + self:__RouteToTarget( 0.1 ) self:__RouteToTargets( -10 ) end - - --- Test + + --- Test -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit -- @param Tasking.Task_A2G#TASK_A2G Task function Fsm:onafterRouteToTarget( TaskUnit, Task ) self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) -- Determine the first Unit from the self.TargetSetUnit - + if Task:GetTargetZone( TaskUnit ) then self:__RouteToTargetZone( 0.1 ) else @@ -157013,8 +170067,8 @@ do -- TASK_A2G self:__RouteToTargetPoint( 0.1 ) end end - - --- Test + + --- Test -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit -- @param Tasking.Task_A2G#TASK_A2G Task @@ -157026,20 +170080,18 @@ do -- TASK_A2G end self:__RouteToTargets( -10 ) end - + return self - + end --- @param #TASK_A2G self -- @param Core.Set#SET_UNIT TargetSetUnit The set of targets. function TASK_A2G:SetTargetSetUnit( TargetSetUnit ) - + self.TargetSetUnit = TargetSetUnit end - - --- @param #TASK_A2G self function TASK_A2G:GetPlannedMenuText() return self:GetStateString() .. " - " .. self:GetTaskName() .. " ( " .. self.TargetSetUnit:GetUnitTypesText() .. " )" @@ -157049,34 +170101,32 @@ do -- TASK_A2G -- @param Core.Point#COORDINATE RendezVousCoordinate The Coordinate object referencing to the 2D point where the RendezVous point is located on the map. -- @param #number RendezVousRange The RendezVousRange that defines when the player is considered to have arrived at the RendezVous point. -- @param Wrapper.Unit#UNIT TaskUnit - function TASK_A2G:SetRendezVousCoordinate( RendezVousCoordinate, RendezVousRange, TaskUnit ) - + function TASK_A2G:SetRendezVousCoordinate( RendezVousCoordinate, RendezVousRange, TaskUnit ) + local ProcessUnit = self:GetUnitProcess( TaskUnit ) - + local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT ActRouteRendezVous:SetCoordinate( RendezVousCoordinate ) ActRouteRendezVous:SetRange( RendezVousRange ) end - + --- @param #TASK_A2G self -- @param Wrapper.Unit#UNIT TaskUnit -- @return Core.Point#COORDINATE The Coordinate object referencing to the 2D point where the RendezVous point is located on the map. -- @return #number The RendezVousRange that defines when the player is considered to have arrived at the RendezVous point. function TASK_A2G:GetRendezVousCoordinate( TaskUnit ) - + local ProcessUnit = self:GetUnitProcess( TaskUnit ) local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT return ActRouteRendezVous:GetCoordinate(), ActRouteRendezVous:GetRange() end - - - + --- @param #TASK_A2G self -- @param Core.Zone#ZONE_BASE RendezVousZone The Zone object where the RendezVous is located on the map. -- @param Wrapper.Unit#UNIT TaskUnit function TASK_A2G:SetRendezVousZone( RendezVousZone, TaskUnit ) - + local ProcessUnit = self:GetUnitProcess( TaskUnit ) local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE @@ -157093,18 +170143,17 @@ do -- TASK_A2G local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE return ActRouteRendezVous:GetZone() end - + --- @param #TASK_A2G self -- @param Core.Point#COORDINATE TargetCoordinate The Coordinate object where the Target is located on the map. -- @param Wrapper.Unit#UNIT TaskUnit function TASK_A2G:SetTargetCoordinate( TargetCoordinate, TaskUnit ) - + local ProcessUnit = self:GetUnitProcess( TaskUnit ) local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT ActRouteTarget:SetCoordinate( TargetCoordinate ) end - --- @param #TASK_A2G self -- @param Wrapper.Unit#UNIT TaskUnit @@ -157117,18 +170166,16 @@ do -- TASK_A2G return ActRouteTarget:GetCoordinate() end - --- @param #TASK_A2G self -- @param Core.Zone#ZONE_BASE TargetZone The Zone object where the Target is located on the map. -- @param Wrapper.Unit#UNIT TaskUnit function TASK_A2G:SetTargetZone( TargetZone, TaskUnit ) - + local ProcessUnit = self:GetUnitProcess( TaskUnit ) local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE ActRouteTarget:SetZone( TargetZone ) end - --- @param #TASK_A2G self -- @param Wrapper.Unit#UNIT TaskUnit @@ -157142,47 +170189,46 @@ do -- TASK_A2G end function TASK_A2G:SetGoalTotal() - + self.GoalTotal = self.TargetSetUnit:Count() end function TASK_A2G:GetGoalTotal() - + return self.GoalTotal end - + --- Return the relative distance to the target vicinity from the player, in order to sort the targets in the reports per distance from the threats. -- @param #TASK_A2G self - function TASK_A2G:ReportOrder( ReportGroup ) - self:UpdateTaskInfo( self.DetectedItem ) - + function TASK_A2G:ReportOrder( ReportGroup ) + self:UpdateTaskInfo( self.DetectedItem ) + local Coordinate = self.TaskInfo:GetData( "Coordinate" ) local Distance = ReportGroup:GetCoordinate():Get2DDistance( Coordinate ) - + return Distance end - - + --- This method checks every 10 seconds if the goal has been reached of the task. -- @param #TASK_A2G self function TASK_A2G:onafterGoal( TaskUnit, From, Event, To ) local TargetSetUnit = self.TargetSetUnit -- Core.Set#SET_UNIT - + if TargetSetUnit:Count() == 0 then self:Success() end - + self:__Goal( -10 ) end --- @param #TASK_A2G self function TASK_A2G:UpdateTaskInfo( DetectedItem ) - + if self:IsStatePlanned() or self:IsStateAssigned() then - local TargetCoordinate = DetectedItem and self.Detection:GetDetectedItemCoordinate( DetectedItem ) or self.TargetSetUnit:GetFirst():GetCoordinate() + local TargetCoordinate = DetectedItem and self.Detection:GetDetectedItemCoordinate( DetectedItem ) or self.TargetSetUnit:GetFirst():GetCoordinate() self.TaskInfo:AddTaskName( 0, "MSOD" ) self.TaskInfo:AddCoordinate( TargetCoordinate, 1, "SOD" ) - + local ThreatLevel, ThreatText if DetectedItem then ThreatLevel, ThreatText = self.Detection:GetDetectedItemThreatLevel( DetectedItem ) @@ -157190,7 +170236,7 @@ do -- TASK_A2G ThreatLevel, ThreatText = self.TargetSetUnit:CalculateThreatLevelA2G() end self.TaskInfo:AddThreat( ThreatText, ThreatLevel, 10, "MOD", true ) - + if self.Detection then local DetectedItemsCount = self.TargetSetUnit:Count() local ReportTypes = REPORT:New() @@ -157203,33 +170249,33 @@ do -- TASK_A2G end end self.TaskInfo:AddTargetCount( DetectedItemsCount, 11, "O", true ) - self.TaskInfo:AddTargets( DetectedItemsCount, ReportTypes:Text( ", " ), 20, "D", true ) + self.TaskInfo:AddTargets( DetectedItemsCount, ReportTypes:Text( ", " ), 20, "D", true ) else local DetectedItemsCount = self.TargetSetUnit:Count() local DetectedItemsTypes = self.TargetSetUnit:GetTypeNames() self.TaskInfo:AddTargetCount( DetectedItemsCount, 11, "O", true ) - self.TaskInfo:AddTargets( DetectedItemsCount, DetectedItemsTypes, 20, "D", true ) + self.TaskInfo:AddTargets( DetectedItemsCount, DetectedItemsTypes, 20, "D", true ) end self.TaskInfo:AddQFEAtCoordinate( TargetCoordinate, 30, "MOD" ) self.TaskInfo:AddTemperatureAtCoordinate( TargetCoordinate, 31, "MD" ) self.TaskInfo:AddWindAtCoordinate( TargetCoordinate, 32, "MD" ) end - + end - + --- This function is called from the @{Tasking.CommandCenter#COMMANDCENTER} to determine the method of automatic task selection. -- @param #TASK_A2G self -- @param #number AutoAssignMethod The method to be applied to the task. -- @param Tasking.CommandCenter#COMMANDCENTER CommandCenter The command center. -- @param Wrapper.Group#GROUP TaskGroup The player group. function TASK_A2G:GetAutoAssignPriority( AutoAssignMethod, CommandCenter, TaskGroup ) - - if AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Random then + + if AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Random then return math.random( 1, 9 ) elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Distance then local Coordinate = self.TaskInfo:GetData( "Coordinate" ) local Distance = Coordinate:Get2DDistance( CommandCenter:GetPositionable():GetCoordinate() ) - self:F({Distance=Distance}) + self:F( { Distance = Distance } ) return math.floor( Distance ) elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Priority then return 1 @@ -157238,8 +170284,7 @@ do -- TASK_A2G return 0 end -end - +end do -- TASK_A2G_SEAD @@ -157256,9 +170301,9 @@ do -- TASK_A2G_SEAD -- -- @field #TASK_A2G_SEAD TASK_A2G_SEAD = { - ClassName = "TASK_A2G_SEAD", + ClassName = "TASK_A2G_SEAD" } - + --- Instantiates a new TASK_A2G_SEAD. -- @param #TASK_A2G_SEAD self -- @param Tasking.Mission#MISSION Mission @@ -157267,19 +170312,16 @@ do -- TASK_A2G_SEAD -- @param Core.Set#SET_UNIT TargetSetUnit -- @param #string TaskBriefing The briefing of the task. -- @return #TASK_A2G_SEAD self - function TASK_A2G_SEAD:New( Mission, SetGroup, TaskName, TargetSetUnit, TaskBriefing) + function TASK_A2G_SEAD:New( Mission, SetGroup, TaskName, TargetSetUnit, TaskBriefing ) local self = BASE:Inherit( self, TASK_A2G:New( Mission, SetGroup, TaskName, TargetSetUnit, "SEAD", TaskBriefing ) ) -- #TASK_A2G_SEAD self:F() - + Mission:AddTask( self ) - - self:SetBriefing( - TaskBriefing or - "Execute a Suppression of Enemy Air Defenses." - ) + + self:SetBriefing( TaskBriefing or "Execute a Suppression of Enemy Air Defenses." ) return self - end + end --- Set a score when a target in scope of the A2G attack, has been destroyed . -- @param #TASK_A2G_SEAD self @@ -157293,7 +170335,7 @@ do -- TASK_A2G_SEAD local ProcessUnit = self:GetUnitProcess( TaskUnit ) ProcessUnit:AddScoreProcess( "Engaging", "Account", "AccountForPlayer", "Player " .. PlayerName .. " has SEADed a target.", Score ) - + return self end @@ -157309,7 +170351,7 @@ do -- TASK_A2G_SEAD local ProcessUnit = self:GetUnitProcess( TaskUnit ) ProcessUnit:AddScore( "Success", "All radar emitting targets have been successfully SEADed!", Score ) - + return self end @@ -157325,11 +170367,10 @@ do -- TASK_A2G_SEAD local ProcessUnit = self:GetUnitProcess( TaskUnit ) ProcessUnit:AddScore( "Failed", "The SEADing has failed!", Penalty ) - + return self end - end do -- TASK_A2G_BAI @@ -157347,10 +170388,8 @@ do -- TASK_A2G_BAI -- based on detected enemy ground targets. -- -- @field #TASK_A2G_BAI - TASK_A2G_BAI = { - ClassName = "TASK_A2G_BAI", - } - + TASK_A2G_BAI = { ClassName = "TASK_A2G_BAI" } + --- Instantiates a new TASK_A2G_BAI. -- @param #TASK_A2G_BAI self -- @param Tasking.Mission#MISSION Mission @@ -157362,14 +170401,11 @@ do -- TASK_A2G_BAI function TASK_A2G_BAI:New( Mission, SetGroup, TaskName, TargetSetUnit, TaskBriefing ) local self = BASE:Inherit( self, TASK_A2G:New( Mission, SetGroup, TaskName, TargetSetUnit, "BAI", TaskBriefing ) ) -- #TASK_A2G_BAI self:F() - + Mission:AddTask( self ) - - self:SetBriefing( - TaskBriefing or - "Execute a Battlefield Air Interdiction of a group of enemy targets." - ) - + + self:SetBriefing( TaskBriefing or "Execute a Battlefield Air Interdiction of a group of enemy targets." ) + return self end @@ -157385,7 +170421,7 @@ do -- TASK_A2G_BAI local ProcessUnit = self:GetUnitProcess( TaskUnit ) ProcessUnit:AddScoreProcess( "Engaging", "Account", "AccountForPlayer", "Player " .. PlayerName .. " has destroyed a target in Battlefield Air Interdiction (BAI).", Score ) - + return self end @@ -157401,7 +170437,7 @@ do -- TASK_A2G_BAI local ProcessUnit = self:GetUnitProcess( TaskUnit ) ProcessUnit:AddScore( "Success", "All targets have been successfully destroyed! The Battlefield Air Interdiction (BAI) is a success!", Score ) - + return self end @@ -157417,15 +170453,12 @@ do -- TASK_A2G_BAI local ProcessUnit = self:GetUnitProcess( TaskUnit ) ProcessUnit:AddScore( "Failed", "The Battlefield Air Interdiction (BAI) has failed!", Penalty ) - + return self end end - - - do -- TASK_A2G_CAS --- The TASK_A2G_CAS class @@ -157440,10 +170473,8 @@ do -- TASK_A2G_CAS -- based on detected enemy ground targets. -- -- @field #TASK_A2G_CAS - TASK_A2G_CAS = { - ClassName = "TASK_A2G_CAS", - } - + TASK_A2G_CAS = { ClassName = "TASK_A2G_CAS" } + --- Instantiates a new TASK_A2G_CAS. -- @param #TASK_A2G_CAS self -- @param Tasking.Mission#MISSION Mission @@ -157455,19 +170486,13 @@ do -- TASK_A2G_CAS function TASK_A2G_CAS:New( Mission, SetGroup, TaskName, TargetSetUnit, TaskBriefing ) local self = BASE:Inherit( self, TASK_A2G:New( Mission, SetGroup, TaskName, TargetSetUnit, "CAS", TaskBriefing ) ) -- #TASK_A2G_CAS self:F() - + Mission:AddTask( self ) - - self:SetBriefing( - TaskBriefing or - "Execute a Close Air Support for a group of enemy targets. " .. - "Beware of friendlies at the vicinity! " - ) - + self:SetBriefing( TaskBriefing or ( "Execute a Close Air Support for a group of enemy targets. " .. "Beware of friendlies at the vicinity! " ) ) + return self - end - + end --- Set a score when a target in scope of the A2G attack, has been destroyed . -- @param #TASK_A2G_CAS self @@ -157481,7 +170506,7 @@ do -- TASK_A2G_CAS local ProcessUnit = self:GetUnitProcess( TaskUnit ) ProcessUnit:AddScoreProcess( "Engaging", "Account", "AccountForPlayer", "Player " .. PlayerName .. " has destroyed a target in Close Air Support (CAS).", Score ) - + return self end @@ -157497,7 +170522,7 @@ do -- TASK_A2G_CAS local ProcessUnit = self:GetUnitProcess( TaskUnit ) ProcessUnit:AddScore( "Success", "All targets have been successfully destroyed! The Close Air Support (CAS) was a success!", Score ) - + return self end @@ -157513,16 +170538,15 @@ do -- TASK_A2G_CAS local ProcessUnit = self:GetUnitProcess( TaskUnit ) ProcessUnit:AddScore( "Failed", "The Close Air Support (CAS) has failed!", Penalty ) - + return self end - end --- **Tasking** - Dynamically allocates A2A tasks to human players, based on detected airborne targets through an EWR network. --- +-- -- **Features:** --- +-- -- * Dynamically assign tasks to human players based on detected targets. -- * Dynamically change the tasks as the tactical situation evolves during the mission. -- * Dynamically assign (CAP) Control Air Patrols tasks for human players to perform CAP. @@ -157532,15 +170556,15 @@ end -- * Define different ranges to engage upon intruders. -- * Keep task achievements. -- * Score task achievements. --- +-- -- === --- +-- -- ### Author: **FlightControl** --- --- ### Contributions: --- +-- +-- ### Contributions: +-- -- === --- +-- -- @module Tasking.Task_A2A_Dispatcher -- @image Task_A2A_Dispatcher.JPG @@ -157550,73 +170574,73 @@ do -- TASK_A2A_DISPATCHER -- @type TASK_A2A_DISPATCHER -- @extends Tasking.DetectionManager#DETECTION_MANAGER - --- Orchestrates the dynamic dispatching of tasks upon groups of detected units determined a @{Set} of EWR installation groups. - -- + --- Orchestrates the dynamic dispatching of tasks upon groups of detected units determined a @{Core.Set} of EWR installation groups. + -- -- ![Banner Image](..\Presentations\TASK_A2A_DISPATCHER\Dia3.JPG) - -- - -- The EWR will detect units, will group them, and will dispatch @{Task}s to groups. Depending on the type of target detected, different tasks will be dispatched. + -- + -- The EWR will detect units, will group them, and will dispatch @{Tasking.Task}s to groups. Depending on the type of target detected, different tasks will be dispatched. -- Find a summary below describing for which situation a task type is created: - -- + -- -- ![Banner Image](..\Presentations\TASK_A2A_DISPATCHER\Dia9.JPG) - -- + -- -- * **INTERCEPT Task**: Is created when the target is known, is detected and within a danger zone, and there is no friendly airborne in range. -- * **SWEEP Task**: Is created when the target is unknown, was detected and the last position is only known, and within a danger zone, and there is no friendly airborne in range. -- * **ENGAGE Task**: Is created when the target is known, is detected and within a danger zone, and there is a friendly airborne in range, that will receive this task. - -- + -- -- ## 1. TASK\_A2A\_DISPATCHER constructor: - -- + -- -- The @{#TASK_A2A_DISPATCHER.New}() method creates a new TASK\_A2A\_DISPATCHER instance. - -- + -- -- ### 1.1. Define or set the **Mission**: - -- + -- -- Tasking is executed to accomplish missions. Therefore, a MISSION object needs to be given as the first parameter. - -- + -- -- local HQ = GROUP:FindByName( "HQ", "Bravo" ) -- local CommandCenter = COMMANDCENTER:New( HQ, "Lima" ) -- local Mission = MISSION:New( CommandCenter, "A2A Mission", "High", "Watch the air enemy units being detected.", coalition.side.RED ) - -- + -- -- Missions are governed by COMMANDCENTERS, so, ensure you have a COMMANDCENTER object installed and setup within your mission. -- Create the MISSION object, and hook it under the command center. - -- + -- -- ### 1.2. Build a set of the groups seated by human players: - -- + -- -- ![Banner Image](..\Presentations\TASK_A2A_DISPATCHER\Dia6.JPG) - -- + -- -- A set or collection of the groups wherein human players can be seated, these can be clients or units that can be joined as a slot or jumping into. - -- + -- -- local AttackGroups = SET_GROUP:New():FilterCoalitions( "red" ):FilterPrefixes( "Defender" ):FilterStart() - -- + -- -- The set is built using the SET_GROUP class. Apply any filter criteria to identify the correct groups for your mission. -- Only these slots or units will be able to execute the mission and will receive tasks for this mission, once available. - -- + -- -- ### 1.3. Define the **EWR network**: - -- + -- -- As part of the TASK\_A2A\_DISPATCHER constructor, an EWR network must be given as the third parameter. -- An EWR network, or, Early Warning Radar network, is used to early detect potential airborne targets and to understand the position of patrolling targets of the enemy. - -- + -- -- ![Banner Image](..\Presentations\TASK_A2A_DISPATCHER\Dia5.JPG) - -- + -- -- Typically EWR networks are setup using 55G6 EWR, 1L13 EWR, Hawk sr and Patriot str ground based radar units. -- These radars have different ranges and 55G6 EWR and 1L13 EWR radars are Eastern Bloc units (eg Russia, Ukraine, Georgia) while the Hawk and Patriot radars are Western (eg US). -- Additionally, ANY other radar capable unit can be part of the EWR network! Also AWACS airborne units, planes, helicopters can help to detect targets, as long as they have radar. -- The position of these units is very important as they need to provide enough coverage -- to pick up enemy aircraft as they approach so that CAP and GCI flights can be tasked to intercept them. - -- + -- -- ![Banner Image](..\Presentations\TASK_A2A_DISPATCHER\Dia7.JPG) - -- - -- Additionally in a hot war situation where the border is no longer respected the placement of radars has a big effect on how fast the war escalates. - -- For example if they are a long way forward and can detect enemy planes on the ground and taking off - -- they will start to vector CAP and GCI flights to attack them straight away which will immediately draw a response from the other coalition. - -- Having the radars further back will mean a slower escalation because fewer targets will be detected and - -- therefore less CAP and GCI flights will spawn and this will tend to make just the border area active rather than a melee over the whole map. - -- It all depends on what the desired effect is. - -- + -- + -- Additionally in a hot war situation where the border is no longer respected the placement of radars has a big effect on how fast the war escalates. + -- For example if they are a long way forward and can detect enemy planes on the ground and taking off + -- they will start to vector CAP and GCI flights to attack them straight away which will immediately draw a response from the other coalition. + -- Having the radars further back will mean a slower escalation because fewer targets will be detected and + -- therefore less CAP and GCI flights will spawn and this will tend to make just the border area active rather than a melee over the whole map. + -- It all depends on what the desired effect is. + -- -- EWR networks are **dynamically constructed**, that is, they form part of the @{Functional.Detection#DETECTION_BASE} object that is given as the input parameter of the TASK\_A2A\_DISPATCHER class. - -- By defining in a **smart way the names or name prefixes of the groups** with EWR capable units, these groups will be **automatically added or deleted** from the EWR network, + -- By defining in a **smart way the names or name prefixes of the groups** with EWR capable units, these groups will be **automatically added or deleted** from the EWR network, -- increasing or decreasing the radar coverage of the Early Warning System. - -- + -- -- See the following example to setup an EWR network containing EWR stations and AWACS. - -- + -- -- local EWRSet = SET_GROUP:New():FilterPrefixes( "EWR" ):FilterCoalitions("red"):FilterStart() -- -- local EWRDetection = DETECTION_AREAS:New( EWRSet, 6000 ) @@ -157625,50 +170649,50 @@ do -- TASK_A2A_DISPATCHER -- -- -- Setup the A2A dispatcher, and initialize it. -- A2ADispatcher = TASK_A2A_DISPATCHER:New( Mission, AttackGroups, EWRDetection ) - -- + -- -- The above example creates a SET_GROUP instance, and stores this in the variable (object) **EWRSet**. -- **EWRSet** is then being configured to filter all active groups with a group name starting with **EWR** to be included in the Set. -- **EWRSet** is then being ordered to start the dynamic filtering. Note that any destroy or new spawn of a group with the above names will be removed or added to the Set. - -- Then a new **EWRDetection** object is created from the class DETECTION_AREAS. A grouping radius of 6000 is choosen, which is 6km. + -- Then a new **EWRDetection** object is created from the class DETECTION_AREAS. A grouping radius of 6000 is chosen, which is 6 km. -- The **EWRDetection** object is then passed to the @{#TASK_A2A_DISPATCHER.New}() method to indicate the EWR network configuration and setup the A2A tasking and detection mechanism. - -- + -- -- ### 2. Define the detected **target grouping radius**: - -- + -- -- ![Banner Image](..\Presentations\TASK_A2A_DISPATCHER\Dia8.JPG) - -- + -- -- The target grouping radius is a property of the Detection object, that was passed to the AI\_A2A\_DISPATCHER object, but can be changed. -- The grouping radius should not be too small, but also depends on the types of planes and the era of the simulation. - -- Fast planes like in the 80s, need a larger radius than WWII planes. + -- Fast planes like in the 80s, need a larger radius than WWII planes. -- Typically I suggest to use 30000 for new generation planes and 10000 for older era aircraft. - -- + -- -- Note that detected targets are constantly re-grouped, that is, when certain detected aircraft are moving further than the group radius, then these aircraft will become a separate -- group being detected. This may result in additional GCI being started by the dispatcher! So don't make this value too small! - -- + -- -- ## 3. Set the **Engage radius**: - -- + -- -- Define the radius to engage any target by airborne friendlies, which are executing cap or returning from an intercept mission. - -- + -- -- ![Banner Image](..\Presentations\TASK_A2A_DISPATCHER\Dia11.JPG) - -- - -- So, if there is a target area detected and reported, - -- then any friendlies that are airborne near this target area, + -- + -- So, if there is a target area detected and reported, + -- then any friendlies that are airborne near this target area, -- will be commanded to (re-)engage that target when available (if no other tasks were commanded). - -- For example, if 100000 is given as a value, then any friendly that is airborne within 100km from the detected target, + -- For example, if 100000 is given as a value, then any friendly that is airborne within 100km from the detected target, -- will be considered to receive the command to engage that target area. -- You need to evaluate the value of this parameter carefully. -- If too small, more intercept missions may be triggered upon detected target areas. -- If too large, any airborne cap may not be able to reach the detected target area in time, because it is too far. - -- + -- -- ## 4. Set **Scoring** and **Messages**: - -- - -- The TASK\_A2A\_DISPATCHER is a state machine. It triggers the event Assign when a new player joins a @{Task} dispatched by the TASK\_A2A\_DISPATCHER. + -- + -- The TASK\_A2A\_DISPATCHER is a state machine. It triggers the event Assign when a new player joins a @{Tasking.Task} dispatched by the TASK\_A2A\_DISPATCHER. -- An _event handler_ can be defined to catch the **Assign** event, and add **additional processing** to set _scoring_ and to _define messages_, -- when the player reaches certain achievements in the task. - -- + -- -- The prototype to handle the **Assign** event needs to be developed as follows: - -- + -- -- TaskDispatcher = TASK_A2A_DISPATCHER:New( ... ) - -- + -- -- --- @param #TaskDispatcher self -- -- @param #string From Contains the name of the state from where the Event was triggered. -- -- @param #string Event Contains the name of the event that was triggered. In this case Assign. @@ -157681,22 +170705,22 @@ do -- TASK_A2A_DISPATCHER -- Task:SetScoreOnSuccess( PlayerName, 200, TaskUnit ) -- Task:SetScoreOnFail( PlayerName, -100, TaskUnit ) -- end - -- + -- -- The **OnAfterAssign** method (function) is added to the TaskDispatcher object. -- This method will be called when a new player joins a unit in the set of groups in scope of the dispatcher. -- So, this method will be called only **ONCE** when a player joins a unit in scope of the task. - -- + -- -- The TASK class implements various methods to additional **set scoring** for player achievements: - -- + -- -- * @{Tasking.Task#TASK.SetScoreOnProgress}() will add additional scores when a player achieves **Progress** while executing the task. -- Examples of **task progress** can be destroying units, arriving at zones etc. - -- - -- * @{Tasking.Task#TASK.SetScoreOnSuccess}() will add additional scores when the task goes into **Success** state. + -- + -- * @{Tasking.Task#TASK.SetScoreOnSuccess}() will add additional scores when the task goes into **Success** state. -- This means the **task has been successfully completed**. - -- - -- * @{Tasking.Task#TASK.SetScoreOnSuccess}() will add additional (negative) scores when the task goes into **Failed** state. + -- + -- * @{Tasking.Task#TASK.SetScoreOnSuccess}() will add additional (negative) scores when the task goes into **Failed** state. -- This means the **task has not been successfully completed**, and the scores must be given with a negative value! - -- + -- -- @field #TASK_A2A_DISPATCHER TASK_A2A_DISPATCHER = { ClassName = "TASK_A2A_DISPATCHER", @@ -157705,8 +170729,7 @@ do -- TASK_A2A_DISPATCHER Tasks = {}, SweepZones = {}, } - - + --- TASK_A2A_DISPATCHER constructor. -- @param #TASK_A2A_DISPATCHER self -- @param Tasking.Mission#MISSION Mission The mission for which the task dispatching is done. @@ -157714,22 +170737,21 @@ do -- TASK_A2A_DISPATCHER -- @param Functional.Detection#DETECTION_BASE Detection The detection results that are used to dynamically assign new tasks to human players. -- @return #TASK_A2A_DISPATCHER self function TASK_A2A_DISPATCHER:New( Mission, SetGroup, Detection ) - + -- Inherits from DETECTION_MANAGER local self = BASE:Inherit( self, DETECTION_MANAGER:New( SetGroup, Detection ) ) -- #TASK_A2A_DISPATCHER - + self.Detection = Detection self.Mission = Mission self.FlashNewTask = false - + -- TODO: Check detection through radar. self.Detection:FilterCategories( Unit.Category.AIRPLANE, Unit.Category.HELICOPTER ) self.Detection:InitDetectRadar( true ) self.Detection:SetRefreshTimeInterval( 30 ) - + self:AddTransition( "Started", "Assign", "Started" ) - --- OnAfter Transition Handler for Event Assign. -- @function [parent=#TASK_A2A_DISPATCHER] OnAfterAssign -- @param #TASK_A2A_DISPATCHER self @@ -157741,14 +170763,13 @@ do -- TASK_A2A_DISPATCHER -- @param #string PlayerName self:__Start( 5 ) - + return self end - --- Define the radius to when an ENGAGE task will be generated for any nearby by airborne friendlies, which are executing cap or returning from an intercept mission. - -- So, if there is a target area detected and reported, - -- then any friendlies that are airborne near this target area, + -- So, if there is a target area detected and reported, + -- then any friendlies that are airborne near this target area, -- will be commanded to (re-)engage that target when available (if no other tasks were commanded). -- An ENGAGE task will be created for those pilots. -- For example, if 100000 is given as a value, then any friendly that is airborne within 100km from the detected target, @@ -157760,27 +170781,27 @@ do -- TASK_A2A_DISPATCHER -- @param #number EngageRadius (Optional, Default = 100000) The radius to report friendlies near the target. -- @return #TASK_A2A_DISPATCHER -- @usage - -- + -- -- -- Set 50km as the radius to engage any target by airborne friendlies. -- TaskA2ADispatcher:SetEngageRadius( 50000 ) - -- + -- -- -- Set 100km as the radius to engage any target by airborne friendlies. -- TaskA2ADispatcher:SetEngageRadius() -- 100000 is the default value. - -- + -- function TASK_A2A_DISPATCHER:SetEngageRadius( EngageRadius ) self.Detection:SetFriendliesRange( EngageRadius or 100000 ) - + return self end - + --- Set flashing player messages on or off -- @param #TASK_A2A_DISPATCHER self -- @param #boolean onoff Set messages on (true) or off (false) function TASK_A2A_DISPATCHER:SetSendMessages( onoff ) - self.FlashNewTask = onoff + self.FlashNewTask = onoff end - + --- Creates an INTERCEPT task when there are targets for it. -- @param #TASK_A2A_DISPATCHER self -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem @@ -157788,26 +170809,25 @@ do -- TASK_A2A_DISPATCHER -- @return #nil If there are no targets to be set. function TASK_A2A_DISPATCHER:EvaluateINTERCEPT( DetectedItem ) self:F( { DetectedItem.ItemID } ) - + local DetectedSet = DetectedItem.Set local DetectedZone = DetectedItem.Zone -- Check if there is at least one UNIT in the DetectedSet is visible. - + if DetectedItem.IsDetected == true then -- Here we're doing something advanced... We're copying the DetectedSet. local TargetSetUnit = SET_UNIT:New() TargetSetUnit:SetDatabase( DetectedSet ) TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection. - + return TargetSetUnit end - + return nil end - --- Creates an SWEEP task when there are targets for it. -- @param #TASK_A2A_DISPATCHER self -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem @@ -157815,10 +170835,9 @@ do -- TASK_A2A_DISPATCHER -- @return #nil If there are no targets to be set. function TASK_A2A_DISPATCHER:EvaluateSWEEP( DetectedItem ) self:F( { DetectedItem.ItemID } ) - - local DetectedSet = DetectedItem.Set - local DetectedZone = DetectedItem.Zone + local DetectedSet = DetectedItem.Set + local DetectedZone = DetectedItem.Zone -- TODO: This seems unused, remove? if DetectedItem.IsDetected == false then @@ -157826,14 +170845,13 @@ do -- TASK_A2A_DISPATCHER local TargetSetUnit = SET_UNIT:New() TargetSetUnit:SetDatabase( DetectedSet ) TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection. - + return TargetSetUnit end - + return nil end - --- Creates an ENGAGE task when there are human friendlies airborne near the targets. -- @param #TASK_A2A_DISPATCHER self -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem @@ -157841,13 +170859,12 @@ do -- TASK_A2A_DISPATCHER -- @return #nil If there are no targets to be set. function TASK_A2A_DISPATCHER:EvaluateENGAGE( DetectedItem ) self:F( { DetectedItem.ItemID } ) - + local DetectedSet = DetectedItem.Set - local DetectedZone = DetectedItem.Zone + local DetectedZone = DetectedItem.Zone -- TODO: This seems unused, remove? local PlayersCount, PlayersReport = self:GetPlayerFriendliesNearBy( DetectedItem ) - -- Only allow ENGAGE when there are Players near the zone, and when the Area has detected items since the last run in a 60 seconds time zone. if PlayersCount > 0 and DetectedItem.IsDetected == true then @@ -157855,16 +170872,13 @@ do -- TASK_A2A_DISPATCHER local TargetSetUnit = SET_UNIT:New() TargetSetUnit:SetDatabase( DetectedSet ) TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection. - + return TargetSetUnit end - + return nil end - - - --- Evaluates the removal of the Task from the Mission. -- Can only occur when the DetectedItem is Changed AND the state of the Task is "Planned". -- @param #TASK_A2A_DISPATCHER self @@ -157875,24 +170889,24 @@ do -- TASK_A2A_DISPATCHER -- @param #boolean DetectedItemChange -- @return Tasking.Task#TASK function TASK_A2A_DISPATCHER:EvaluateRemoveTask( Mission, Task, Detection, DetectedItem, DetectedItemIndex, DetectedItemChanged ) - + if Task then if Task:IsStatePlanned() then local TaskName = Task:GetName() local TaskType = TaskName:match( "(%u+)%.%d+" ) - + self:T2( { TaskType = TaskType } ) - + local Remove = false - + local IsPlayers = Detection:IsPlayersNearBy( DetectedItem ) if TaskType == "ENGAGE" then if IsPlayers == false then Remove = true end end - + if TaskType == "INTERCEPT" then if IsPlayers == true then Remove = true @@ -157901,7 +170915,7 @@ do -- TASK_A2A_DISPATCHER Remove = true end end - + if TaskType == "SWEEP" then if DetectedItem.IsDetected == true then Remove = true @@ -157909,18 +170923,18 @@ do -- TASK_A2A_DISPATCHER end local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT - --DetectedSet:Flush( self ) - --self:F( { DetectedSetCount = DetectedSet:Count() } ) + -- DetectedSet:Flush( self ) + -- self:F( { DetectedSetCount = DetectedSet:Count() } ) if DetectedSet:Count() == 0 then Remove = true end - + if DetectedItemChanged == true or Remove then Task = self:RemoveTask( DetectedItemIndex ) end end end - + return Task end @@ -157929,10 +170943,10 @@ do -- TASK_A2A_DISPATCHER -- @param DetectedItem -- @return #number, Core.CommandCenter#REPORT function TASK_A2A_DISPATCHER:GetFriendliesNearBy( DetectedItem ) - + local DetectedSet = DetectedItem.Set local FriendlyUnitsNearBy = self.Detection:GetFriendliesNearBy( DetectedItem, Unit.Category.AIRPLANE ) - + local FriendlyTypes = {} local FriendliesCount = 0 @@ -157944,27 +170958,26 @@ do -- TASK_A2A_DISPATCHER local FriendlyUnitThreatLevel = FriendlyUnit:GetThreatLevel() FriendliesCount = FriendliesCount + 1 local FriendlyType = FriendlyUnit:GetTypeName() - FriendlyTypes[FriendlyType] = FriendlyTypes[FriendlyType] and ( FriendlyTypes[FriendlyType] + 1 ) or 1 + FriendlyTypes[FriendlyType] = FriendlyTypes[FriendlyType] and (FriendlyTypes[FriendlyType] + 1) or 1 if DetectedTreatLevel < FriendlyUnitThreatLevel + 2 then end end end - + end - --self:F( { FriendliesCount = FriendliesCount } ) - + -- self:F( { FriendliesCount = FriendliesCount } ) + local FriendlyTypesReport = REPORT:New() - + if FriendliesCount > 0 then for FriendlyType, FriendlyTypeCount in pairs( FriendlyTypes ) do - FriendlyTypesReport:Add( string.format("%d of %s", FriendlyTypeCount, FriendlyType ) ) + FriendlyTypesReport:Add( string.format( "%d of %s", FriendlyTypeCount, FriendlyType ) ) end else FriendlyTypesReport:Add( "-" ) end - - + return FriendliesCount, FriendlyTypesReport end @@ -157973,10 +170986,10 @@ do -- TASK_A2A_DISPATCHER -- @param DetectedItem -- @return #number, Core.CommandCenter#REPORT function TASK_A2A_DISPATCHER:GetPlayerFriendliesNearBy( DetectedItem ) - + local DetectedSet = DetectedItem.Set local PlayersNearBy = self.Detection:GetPlayersNearBy( DetectedItem ) - + local PlayerTypes = {} local PlayersCount = 0 @@ -157985,7 +170998,7 @@ do -- TASK_A2A_DISPATCHER for PlayerUnitName, PlayerUnitData in pairs( PlayersNearBy ) do local PlayerUnit = PlayerUnitData -- Wrapper.Unit#UNIT local PlayerName = PlayerUnit:GetPlayerName() - --self:F( { PlayerName = PlayerName, PlayerUnit = PlayerUnit } ) + -- self:F( { PlayerName = PlayerName, PlayerUnit = PlayerUnit } ) if PlayerUnit:IsAirPlane() and PlayerName ~= nil then local FriendlyUnitThreatLevel = PlayerUnit:GetThreatLevel() PlayersCount = PlayersCount + 1 @@ -157995,20 +171008,19 @@ do -- TASK_A2A_DISPATCHER end end end - + end local PlayerTypesReport = REPORT:New() - + if PlayersCount > 0 then for PlayerName, PlayerType in pairs( PlayerTypes ) do - PlayerTypesReport:Add( string.format('"%s" in %s', PlayerName, PlayerType ) ) + PlayerTypesReport:Add( string.format( '"%s" in %s', PlayerName, PlayerType ) ) end else PlayerTypesReport:Add( "-" ) end - - + return PlayersCount, PlayerTypesReport end @@ -158017,24 +171029,23 @@ do -- TASK_A2A_DISPATCHER self.Tasks[TaskIndex] = nil end - --- Assigns tasks in relation to the detected items to the @{Core.Set#SET_GROUP}. -- @param #TASK_A2A_DISPATCHER self -- @param Functional.Detection#DETECTION_BASE Detection The detection created by the @{Functional.Detection#DETECTION_BASE} derived object. -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop. function TASK_A2A_DISPATCHER:ProcessDetected( Detection ) self:F() - + local AreaMsg = {} local TaskMsg = {} local ChangeMsg = {} - + local Mission = self.Mission - + if Mission:IsIDLE() or Mission:IsENGAGED() then - + local TaskReport = REPORT:New() - + -- Checking the task queue for the dispatcher, and removing any obsolete task! for TaskIndex, TaskData in pairs( self.Tasks ) do local Task = TaskData -- Tasking.Task#TASK @@ -158052,18 +171063,18 @@ do -- TASK_A2A_DISPATCHER -- Now that all obsolete tasks are removed, loop through the detected targets. for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do - + local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT local DetectedCount = DetectedSet:Count() local DetectedZone = DetectedItem.Zone - --self:F( { "Targets in DetectedItem", DetectedItem.ItemID, DetectedSet:Count(), tostring( DetectedItem ) } ) - --DetectedSet:Flush( self ) - + -- self:F( { "Targets in DetectedItem", DetectedItem.ItemID, DetectedSet:Count(), tostring( DetectedItem ) } ) + -- DetectedSet:Flush( self ) + local DetectedID = DetectedItem.ID local TaskIndex = DetectedItem.Index local DetectedItemChanged = DetectedItem.Changed - + local Task = self.Tasks[TaskIndex] Task = self:EvaluateRemoveTask( Mission, Task, Detection, DetectedItem, TaskIndex, DetectedItemChanged ) -- Task will be removed if it is planned and changed. @@ -158086,7 +171097,7 @@ do -- TASK_A2A_DISPATCHER Task = TASK_A2A_SWEEP:New( Mission, self.SetGroup, string.format( "SWEEP.%03d", DetectedID ), TargetSetUnit ) Task:SetDetection( Detection, DetectedItem ) Task:UpdateTaskInfo( DetectedItem ) - end + end end end @@ -158103,7 +171114,7 @@ do -- TASK_A2A_DISPATCHER function Task.OnEnterCancelled( Task, From, Event, To ) self:Cancelled( Task ) end - + function Task.OnEnterFailed( Task, From, Event, To ) self:Failed( Task ) end @@ -158111,52 +171122,52 @@ do -- TASK_A2A_DISPATCHER function Task.OnEnterAborted( Task, From, Event, To ) self:Aborted( Task ) end - + TaskReport:Add( Task:GetName() ) else - self:F("This should not happen") + self:F( "This should not happen" ) end end if Task then local FriendliesCount, FriendliesReport = self:GetFriendliesNearBy( DetectedItem, Unit.Category.AIRPLANE ) - Task.TaskInfo:AddText( "Friendlies", string.format( "%d ( %s )", FriendliesCount, FriendliesReport:Text( "," ) ), 40, "MOD" ) + Task.TaskInfo:AddText( "Friendlies", string.format( "%d ( %s )", FriendliesCount, FriendliesReport:Text( "," ) ), 40, "MOD" ) local PlayersCount, PlayersReport = self:GetPlayerFriendliesNearBy( DetectedItem ) - Task.TaskInfo:AddText( "Players", string.format( "%d ( %s )", PlayersCount, PlayersReport:Text( "," ) ), 40, "MOD" ) + Task.TaskInfo:AddText( "Players", string.format( "%d ( %s )", PlayersCount, PlayersReport:Text( "," ) ), 40, "MOD" ) end - + -- OK, so the tasking has been done, now delete the changes reported for the area. Detection:AcceptChanges( DetectedItem ) end - + -- TODO set menus using the HQ coordinator Mission:GetCommandCenter():SetMenu() - local TaskText = TaskReport:Text(", ") - + local TaskText = TaskReport:Text( ", " ) + for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do - if ( not Mission:IsGroupAssigned(TaskGroup) ) and TaskText ~= "" and (self.FlashNewTask) then + if (not Mission:IsGroupAssigned( TaskGroup )) and TaskText ~= "" and (self.FlashNewTask) then Mission:GetCommandCenter():MessageToGroup( string.format( "%s has tasks %s. Subscribe to a task using the radio menu.", Mission:GetShortText(), TaskText ), TaskGroup ) end end - + end - + return true end end --- **Tasking** - The TASK_A2A models tasks for players in Air to Air engagements. --- +-- -- === --- +-- -- ### Author: **FlightControl** --- --- ### Contributions: --- +-- +-- ### Contributions: +-- -- === --- +-- -- @module Tasking.Task_A2A -- @image MOOSE.JPG @@ -158167,7 +171178,7 @@ do -- TASK_A2A -- @field Core.Set#SET_UNIT TargetSetUnit -- @extends Tasking.Task#TASK - --- Defines Air To Air tasks for a @{Set} of Target Units, + --- Defines Air To Air tasks for a @{Core.Set} of Target Units, -- based on the tasking capabilities defined in @{Tasking.Task#TASK}. -- The TASK_A2A is implemented using a @{Core.Fsm#FSM_TASK}, and has the following statuses: -- @@ -158184,12 +171195,12 @@ do -- TASK_A2A -- * @{#TASK_A2A.SetScoreOnDestroy}(): Set a score when a target in scope of the A2A attack, has been destroyed. -- * @{#TASK_A2A.SetScoreOnSuccess}(): Set a score when all the targets in scope of the A2A attack, have been destroyed. -- * @{#TASK_A2A.SetPenaltyOnFailed}(): Set a penalty when the A2A attack has failed. - -- + -- -- @field #TASK_A2A TASK_A2A = { - ClassName = "TASK_A2A", + ClassName = "TASK_A2A" } - + --- Instantiates a new TASK_A2A. -- @param #TASK_A2A self -- @param Tasking.Mission#MISSION Mission @@ -158203,51 +171214,49 @@ do -- TASK_A2A function TASK_A2A:New( Mission, SetAttack, TaskName, TargetSetUnit, TaskType, TaskBriefing ) local self = BASE:Inherit( self, TASK:New( Mission, SetAttack, TaskName, TaskType, TaskBriefing ) ) -- Tasking.Task#TASK_A2A self:F() - + self.TargetSetUnit = TargetSetUnit self.TaskType = TaskType local Fsm = self:GetUnitProcess() - Fsm:AddTransition( "Assigned", "RouteToRendezVous", "RoutingToRendezVous" ) - Fsm:AddProcess ( "RoutingToRendezVous", "RouteToRendezVousPoint", ACT_ROUTE_POINT:New(), { Arrived = "ArriveAtRendezVous" } ) - Fsm:AddProcess ( "RoutingToRendezVous", "RouteToRendezVousZone", ACT_ROUTE_ZONE:New(), { Arrived = "ArriveAtRendezVous" } ) - + Fsm:AddProcess( "RoutingToRendezVous", "RouteToRendezVousPoint", ACT_ROUTE_POINT:New(), { Arrived = "ArriveAtRendezVous" } ) + Fsm:AddProcess( "RoutingToRendezVous", "RouteToRendezVousZone", ACT_ROUTE_ZONE:New(), { Arrived = "ArriveAtRendezVous" } ) + Fsm:AddTransition( { "Arrived", "RoutingToRendezVous" }, "ArriveAtRendezVous", "ArrivedAtRendezVous" ) - + Fsm:AddTransition( { "ArrivedAtRendezVous", "HoldingAtRendezVous" }, "Engage", "Engaging" ) Fsm:AddTransition( { "ArrivedAtRendezVous", "HoldingAtRendezVous" }, "HoldAtRendezVous", "HoldingAtRendezVous" ) - - Fsm:AddProcess ( "Engaging", "Account", ACT_ACCOUNT_DEADS:New(), {} ) + + Fsm:AddProcess( "Engaging", "Account", ACT_ACCOUNT_DEADS:New(), {} ) Fsm:AddTransition( "Engaging", "RouteToTarget", "Engaging" ) Fsm:AddProcess( "Engaging", "RouteToTargetZone", ACT_ROUTE_ZONE:New(), {} ) Fsm:AddProcess( "Engaging", "RouteToTargetPoint", ACT_ROUTE_POINT:New(), {} ) Fsm:AddTransition( "Engaging", "RouteToTargets", "Engaging" ) - --- Fsm:AddTransition( "Accounted", "DestroyedAll", "Accounted" ) --- Fsm:AddTransition( "Accounted", "Success", "Success" ) + + -- Fsm:AddTransition( "Accounted", "DestroyedAll", "Accounted" ) + -- Fsm:AddTransition( "Accounted", "Success", "Success" ) Fsm:AddTransition( "Rejected", "Reject", "Aborted" ) Fsm:AddTransition( "Failed", "Fail", "Failed" ) - - ---- @param #FSM_PROCESS self + -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit -- @param #TASK_CARGO Task function Fsm:OnLeaveAssigned( TaskUnit, Task ) self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - + self:SelectAction() end - - --- Test + + --- Test -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit -- @param Tasking.Task_A2A#TASK_A2A Task function Fsm:onafterRouteToRendezVous( TaskUnit, Task ) self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) -- Determine the first Unit from the self.RendezVousSetUnit - + if Task:GetRendezVousZone( TaskUnit ) then self:__RouteToRendezVousZone( 0.1 ) else @@ -158259,36 +171268,36 @@ do -- TASK_A2A end end - --- Test + --- Test -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit -- @param Tasking.Task#TASK_A2A Task function Fsm:OnAfterArriveAtRendezVous( TaskUnit, Task ) self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) -- Determine the first Unit from the self.TargetSetUnit - - self:__Engage( 0.1 ) + + self:__Engage( 0.1 ) end - - --- Test + + --- Test -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit -- @param Tasking.Task#TASK_A2A Task function Fsm:onafterEngage( TaskUnit, Task ) self:F( { self } ) self:__Account( 0.1 ) - self:__RouteToTarget(0.1 ) + self:__RouteToTarget( 0.1 ) self:__RouteToTargets( -10 ) end - - --- Test + + --- Test -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit -- @param Tasking.Task_A2A#TASK_A2A Task function Fsm:onafterRouteToTarget( TaskUnit, Task ) self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) -- Determine the first Unit from the self.TargetSetUnit - + if Task:GetTargetZone( TaskUnit ) then self:__RouteToTargetZone( 0.1 ) else @@ -158301,8 +171310,8 @@ do -- TASK_A2A self:__RouteToTargetPoint( 0.1 ) end end - - --- Test + + --- Test -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit -- @param Tasking.Task_A2A#TASK_A2A Task @@ -158314,20 +171323,18 @@ do -- TASK_A2A end self:__RouteToTargets( -10 ) end - + return self - + end - + --- @param #TASK_A2A self -- @param Core.Set#SET_UNIT TargetSetUnit The set of targets. function TASK_A2A:SetTargetSetUnit( TargetSetUnit ) - + self.TargetSetUnit = TargetSetUnit end - - --- @param #TASK_A2A self function TASK_A2A:GetPlannedMenuText() return self:GetStateString() .. " - " .. self:GetTaskName() .. " ( " .. self.TargetSetUnit:GetUnitTypesText() .. " )" @@ -158337,34 +171344,32 @@ do -- TASK_A2A -- @param Core.Point#COORDINATE RendezVousCoordinate The Coordinate object referencing to the 2D point where the RendezVous point is located on the map. -- @param #number RendezVousRange The RendezVousRange that defines when the player is considered to have arrived at the RendezVous point. -- @param Wrapper.Unit#UNIT TaskUnit - function TASK_A2A:SetRendezVousCoordinate( RendezVousCoordinate, RendezVousRange, TaskUnit ) - + function TASK_A2A:SetRendezVousCoordinate( RendezVousCoordinate, RendezVousRange, TaskUnit ) + local ProcessUnit = self:GetUnitProcess( TaskUnit ) - + local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT ActRouteRendezVous:SetCoordinate( RendezVousCoordinate ) ActRouteRendezVous:SetRange( RendezVousRange ) end - + --- @param #TASK_A2A self -- @param Wrapper.Unit#UNIT TaskUnit -- @return Core.Point#COORDINATE The Coordinate object referencing to the 2D point where the RendezVous point is located on the map. -- @return #number The RendezVousRange that defines when the player is considered to have arrived at the RendezVous point. function TASK_A2A:GetRendezVousCoordinate( TaskUnit ) - + local ProcessUnit = self:GetUnitProcess( TaskUnit ) local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT return ActRouteRendezVous:GetCoordinate(), ActRouteRendezVous:GetRange() end - - - + --- @param #TASK_A2A self -- @param Core.Zone#ZONE_BASE RendezVousZone The Zone object where the RendezVous is located on the map. -- @param Wrapper.Unit#UNIT TaskUnit function TASK_A2A:SetRendezVousZone( RendezVousZone, TaskUnit ) - + local ProcessUnit = self:GetUnitProcess( TaskUnit ) local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE @@ -158381,18 +171386,17 @@ do -- TASK_A2A local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE return ActRouteRendezVous:GetZone() end - + --- @param #TASK_A2A self -- @param Core.Point#COORDINATE TargetCoordinate The Coordinate object where the Target is located on the map. -- @param Wrapper.Unit#UNIT TaskUnit function TASK_A2A:SetTargetCoordinate( TargetCoordinate, TaskUnit ) - + local ProcessUnit = self:GetUnitProcess( TaskUnit ) local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT ActRouteTarget:SetCoordinate( TargetCoordinate ) end - --- @param #TASK_A2A self -- @param Wrapper.Unit#UNIT TaskUnit @@ -158405,18 +171409,16 @@ do -- TASK_A2A return ActRouteTarget:GetCoordinate() end - --- @param #TASK_A2A self -- @param Core.Zone#ZONE_BASE TargetZone The Zone object where the Target is located on the map. -- @param Wrapper.Unit#UNIT TaskUnit function TASK_A2A:SetTargetZone( TargetZone, Altitude, Heading, TaskUnit ) - + local ProcessUnit = self:GetUnitProcess( TaskUnit ) local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE ActRouteTarget:SetZone( TargetZone, Altitude, Heading ) end - --- @param #TASK_A2A self -- @param Wrapper.Unit#UNIT TaskUnit @@ -158430,45 +171432,43 @@ do -- TASK_A2A end function TASK_A2A:SetGoalTotal() - + self.GoalTotal = self.TargetSetUnit:Count() end function TASK_A2A:GetGoalTotal() - + return self.GoalTotal end --- Return the relative distance to the target vicinity from the player, in order to sort the targets in the reports per distance from the threats. -- @param #TASK_A2A self function TASK_A2A:ReportOrder( ReportGroup ) - self:UpdateTaskInfo( self.DetectedItem ) - + self:UpdateTaskInfo( self.DetectedItem ) + local Coordinate = self.TaskInfo:GetData( "Coordinate" ) local Distance = ReportGroup:GetCoordinate():Get2DDistance( Coordinate ) - + return Distance end - - + --- This method checks every 10 seconds if the goal has been reached of the task. -- @param #TASK_A2A self function TASK_A2A:onafterGoal( TaskUnit, From, Event, To ) local TargetSetUnit = self.TargetSetUnit -- Core.Set#SET_UNIT - + if TargetSetUnit:Count() == 0 then self:Success() end - + self:__Goal( -10 ) end - --- @param #TASK_A2A self function TASK_A2A:UpdateTaskInfo( DetectedItem ) if self:IsStatePlanned() or self:IsStateAssigned() then - local TargetCoordinate = DetectedItem and self.Detection:GetDetectedItemCoordinate( DetectedItem ) or self.TargetSetUnit:GetFirst():GetCoordinate() + local TargetCoordinate = DetectedItem and self.Detection:GetDetectedItemCoordinate( DetectedItem ) or self.TargetSetUnit:GetFirst():GetCoordinate() self.TaskInfo:AddTaskName( 0, "MSOD" ) self.TaskInfo:AddCoordinate( TargetCoordinate, 1, "SOD" ) @@ -158492,12 +171492,12 @@ do -- TASK_A2A end end self.TaskInfo:AddTargetCount( DetectedItemsCount, 11, "O", true ) - self.TaskInfo:AddTargets( DetectedItemsCount, ReportTypes:Text( ", " ), 20, "D", true ) + self.TaskInfo:AddTargets( DetectedItemsCount, ReportTypes:Text( ", " ), 20, "D", true ) else local DetectedItemsCount = self.TargetSetUnit:Count() local DetectedItemsTypes = self.TargetSetUnit:GetTypeNames() self.TaskInfo:AddTargetCount( DetectedItemsCount, 11, "O", true ) - self.TaskInfo:AddTargets( DetectedItemsCount, DetectedItemsTypes, 20, "D", true ) + self.TaskInfo:AddTargets( DetectedItemsCount, DetectedItemsTypes, 20, "D", true ) end end end @@ -158508,8 +171508,8 @@ do -- TASK_A2A -- @param Tasking.CommandCenter#COMMANDCENTER CommandCenter The command center. -- @param Wrapper.Group#GROUP TaskGroup The player group. function TASK_A2A:GetAutoAssignPriority( AutoAssignMethod, CommandCenter, TaskGroup ) - - if AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Random then + + if AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Random then return math.random( 1, 9 ) elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Distance then local Coordinate = self.TaskInfo:GetData( "Coordinate" ) @@ -158522,8 +171522,7 @@ do -- TASK_A2A return 0 end -end - +end do -- TASK_A2A_INTERCEPT @@ -158533,44 +171532,39 @@ do -- TASK_A2A_INTERCEPT -- @extends Tasking.Task#TASK --- Defines an intercept task for a human player to be executed. - -- When enemy planes need to be intercepted by human players, use this task type to urgen the players to get out there! - -- - -- The TASK_A2A_INTERCEPT is used by the @{Tasking.Task_A2A_Dispatcher#TASK_A2A_DISPATCHER} to automatically create intercept tasks + -- When enemy planes need to be intercepted by human players, use this task type to urge the players to get out there! + -- + -- The TASK_A2A_INTERCEPT is used by the @{Tasking.Task_A2A_Dispatcher#TASK_A2A_DISPATCHER} to automatically create intercept tasks -- based on detected airborne enemy targets intruding friendly airspace. - -- + -- -- The task is defined for a @{Tasking.Mission#MISSION}, where a friendly @{Core.Set#SET_GROUP} consisting of GROUPs with one human players each, is intercepting the targets. -- The task is given a name and a briefing, that is used in the menu structure and in the reporting. - -- + -- -- @field #TASK_A2A_INTERCEPT TASK_A2A_INTERCEPT = { - ClassName = "TASK_A2A_INTERCEPT", + ClassName = "TASK_A2A_INTERCEPT" } - - --- Instantiates a new TASK_A2A_INTERCEPT. -- @param #TASK_A2A_INTERCEPT self -- @param Tasking.Mission#MISSION Mission -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. -- @param #string TaskName The name of the Task. - -- @param Core.Set#SET_UNIT TargetSetUnit + -- @param Core.Set#SET_UNIT TargetSetUnit -- @param #string TaskBriefing The briefing of the task. -- @return #TASK_A2A_INTERCEPT function TASK_A2A_INTERCEPT:New( Mission, SetGroup, TaskName, TargetSetUnit, TaskBriefing ) local self = BASE:Inherit( self, TASK_A2A:New( Mission, SetGroup, TaskName, TargetSetUnit, "INTERCEPT", TaskBriefing ) ) -- #TASK_A2A_INTERCEPT self:F() - + Mission:AddTask( self ) - - self:SetBriefing( - TaskBriefing or - "Intercept incoming intruders.\n" - ) + + self:SetBriefing( TaskBriefing or "Intercept incoming intruders.\n" ) return self end - - --- Set a score when a target in scope of the A2A attack, has been destroyed . + + --- Set a score when a target in scope of the A2A attack, has been destroyed. -- @param #TASK_A2A_INTERCEPT self -- @param #string PlayerName The name of the player. -- @param #number Score The score in points to be granted when task process has been achieved. @@ -158582,7 +171576,7 @@ do -- TASK_A2A_INTERCEPT local ProcessUnit = self:GetUnitProcess( TaskUnit ) ProcessUnit:AddScoreProcess( "Engaging", "Account", "AccountForPlayer", "Player " .. PlayerName .. " has intercepted a target.", Score ) - + return self end @@ -158598,7 +171592,7 @@ do -- TASK_A2A_INTERCEPT local ProcessUnit = self:GetUnitProcess( TaskUnit ) ProcessUnit:AddScore( "Success", "All targets have been successfully intercepted!", Score ) - + return self end @@ -158614,14 +171608,12 @@ do -- TASK_A2A_INTERCEPT local ProcessUnit = self:GetUnitProcess( TaskUnit ) ProcessUnit:AddScore( "Failed", "The intercept has failed!", Penalty ) - + return self end - end - do -- TASK_A2A_SWEEP --- The TASK_A2A_SWEEP class @@ -158633,20 +171625,18 @@ do -- TASK_A2A_SWEEP -- A sweep task needs to be given when targets were detected but somehow the detection was lost. -- Most likely, these enemy planes are hidden in the mountains or are flying under radar. -- These enemy planes need to be sweeped by human players, and use this task type to urge the players to get out there and find those enemy fighters. - -- - -- The TASK_A2A_SWEEP is used by the @{Tasking.Task_A2A_Dispatcher#TASK_A2A_DISPATCHER} to automatically create sweep tasks + -- + -- The TASK_A2A_SWEEP is used by the @{Tasking.Task_A2A_Dispatcher#TASK_A2A_DISPATCHER} to automatically create sweep tasks -- based on detected airborne enemy targets intruding friendly airspace, for which the detection has been lost for more than 60 seconds. - -- + -- -- The task is defined for a @{Tasking.Mission#MISSION}, where a friendly @{Core.Set#SET_GROUP} consisting of GROUPs with one human players each, is sweeping the targets. -- The task is given a name and a briefing, that is used in the menu structure and in the reporting. - -- + -- -- @field #TASK_A2A_SWEEP TASK_A2A_SWEEP = { - ClassName = "TASK_A2A_SWEEP", + ClassName = "TASK_A2A_SWEEP" } - - --- Instantiates a new TASK_A2A_SWEEP. -- @param #TASK_A2A_SWEEP self -- @param Tasking.Mission#MISSION Mission @@ -158658,29 +171648,26 @@ do -- TASK_A2A_SWEEP function TASK_A2A_SWEEP:New( Mission, SetGroup, TaskName, TargetSetUnit, TaskBriefing ) local self = BASE:Inherit( self, TASK_A2A:New( Mission, SetGroup, TaskName, TargetSetUnit, "SWEEP", TaskBriefing ) ) -- #TASK_A2A_SWEEP self:F() - + Mission:AddTask( self ) - - self:SetBriefing( - TaskBriefing or - "Perform a fighter sweep. Incoming intruders were detected and could be hiding at the location.\n" - ) + + self:SetBriefing( TaskBriefing or "Perform a fighter sweep. Incoming intruders were detected and could be hiding at the location.\n" ) return self - end + end --- @param #TASK_A2A_SWEEP self function TASK_A2A_SWEEP:onafterGoal( TaskUnit, From, Event, To ) local TargetSetUnit = self.TargetSetUnit -- Core.Set#SET_UNIT - + if TargetSetUnit:Count() == 0 then self:Success() end - + self:__Goal( -10 ) end - --- Set a score when a target in scope of the A2A attack, has been destroyed . + --- Set a score when a target in scope of the A2A attack, has been destroyed. -- @param #TASK_A2A_SWEEP self -- @param #string PlayerName The name of the player. -- @param #number Score The score in points to be granted when task process has been achieved. @@ -158692,7 +171679,7 @@ do -- TASK_A2A_SWEEP local ProcessUnit = self:GetUnitProcess( TaskUnit ) ProcessUnit:AddScoreProcess( "Engaging", "Account", "AccountForPlayer", "Player " .. PlayerName .. " has sweeped a target.", Score ) - + return self end @@ -158708,7 +171695,7 @@ do -- TASK_A2A_SWEEP local ProcessUnit = self:GetUnitProcess( TaskUnit ) ProcessUnit:AddScore( "Success", "All targets have been successfully sweeped!", Score ) - + return self end @@ -158724,13 +171711,12 @@ do -- TASK_A2A_SWEEP local ProcessUnit = self:GetUnitProcess( TaskUnit ) ProcessUnit:AddScore( "Failed", "The sweep has failed!", Penalty ) - + return self end end - do -- TASK_A2A_ENGAGE --- The TASK_A2A_ENGAGE class @@ -158740,42 +171726,37 @@ do -- TASK_A2A_ENGAGE --- Defines an engage task for a human player to be executed. -- When enemy planes are close to human players, use this task type is used urge the players to get out there! - -- - -- The TASK_A2A_ENGAGE is used by the @{Tasking.Task_A2A_Dispatcher#TASK_A2A_DISPATCHER} to automatically create engage tasks + -- + -- The TASK_A2A_ENGAGE is used by the @{Tasking.Task_A2A_Dispatcher#TASK_A2A_DISPATCHER} to automatically create engage tasks -- based on detected airborne enemy targets intruding friendly airspace. - -- + -- -- The task is defined for a @{Tasking.Mission#MISSION}, where a friendly @{Core.Set#SET_GROUP} consisting of GROUPs with one human players each, is engaging the targets. -- The task is given a name and a briefing, that is used in the menu structure and in the reporting. - -- + -- -- @field #TASK_A2A_ENGAGE TASK_A2A_ENGAGE = { - ClassName = "TASK_A2A_ENGAGE", + ClassName = "TASK_A2A_ENGAGE" } - - --- Instantiates a new TASK_A2A_ENGAGE. -- @param #TASK_A2A_ENGAGE self -- @param Tasking.Mission#MISSION Mission -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. -- @param #string TaskName The name of the Task. - -- @param Core.Set#SET_UNIT TargetSetUnit + -- @param Core.Set#SET_UNIT TargetSetUnit -- @param #string TaskBriefing The briefing of the task. -- @return #TASK_A2A_ENGAGE self function TASK_A2A_ENGAGE:New( Mission, SetGroup, TaskName, TargetSetUnit, TaskBriefing ) local self = BASE:Inherit( self, TASK_A2A:New( Mission, SetGroup, TaskName, TargetSetUnit, "ENGAGE", TaskBriefing ) ) -- #TASK_A2A_ENGAGE self:F() - + Mission:AddTask( self ) - - self:SetBriefing( - TaskBriefing or - "Bogeys are nearby! Players close by are ordered to ENGAGE the intruders!\n" - ) + + self:SetBriefing( TaskBriefing or "Bogeys are nearby! Players close by are ordered to ENGAGE the intruders!\n" ) return self - end - + end + --- Set a score when a target in scope of the A2A attack, has been destroyed . -- @param #TASK_A2A_ENGAGE self -- @param #string PlayerName The name of the player. @@ -158788,7 +171769,7 @@ do -- TASK_A2A_ENGAGE local ProcessUnit = self:GetUnitProcess( TaskUnit ) ProcessUnit:AddScoreProcess( "Engaging", "Account", "AccountForPlayer", "Player " .. PlayerName .. " has engaged and destroyed a target.", Score ) - + return self end @@ -158804,7 +171785,7 @@ do -- TASK_A2A_ENGAGE local ProcessUnit = self:GetUnitProcess( TaskUnit ) ProcessUnit:AddScore( "Success", "All targets have been successfully engaged!", Score ) - + return self end @@ -158820,20 +171801,20 @@ do -- TASK_A2A_ENGAGE local ProcessUnit = self:GetUnitProcess( TaskUnit ) ProcessUnit:AddScore( "Failed", "The target engagement has failed!", Penalty ) - + return self end end ---- **Tasking** -- Base class to model tasks for players to transport cargo. +--- **Tasking** - Base class to model tasks for players to transport cargo. -- -- ## Features: -- -- * TASK_CARGO is the **base class** for: -- -- * @{Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT} --- * @{Tasking.Task_Cargo_CSAR#TASK_CARGO_CSAR} +-- * @{Tasking.Task_CARGO_CSAR#TASK_CARGO_CSAR} -- -- -- === @@ -158862,7 +171843,7 @@ end -- The following TASK_CARGO_ classes are important, as they implement the CONCRETE tasks: -- -- * @{Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT}: Defines a task for a human player to transport a set of cargo between various zones. --- * @{Tasking.Task_Cargo_CSAR#TASK_CARGO_CSAR}: Defines a task for a human player to Search and Rescue wounded pilots. +-- * @{Tasking.Task_CARGO_CSAR#TASK_CARGO_CSAR}: Defines a task for a human player to Search and Rescue wounded pilots. -- -- However! The menu system and basic usage of the TASK_CARGO classes is explained in the @{#TASK_CARGO} class description. -- So please browse further below to understand how to use it from a player perspective! @@ -159215,9 +172196,9 @@ end -- -- Please consult the documentation how to implement the derived classes of SET_CARGO in: -- --- - @{Tasking.Task_Cargo#TASK_CARGO}: Documents the main methods how to handle the cargo tasking from a mission designer perspective. --- - @{Tasking.Task_Cargo#TASK_CARGO_TRANSPORT}: Documents the specific methods how to handle the cargo transportation tasking from a mission designer perspective. --- - @{Tasking.Task_Cargo#TASK_CARGO_CSAR}: Documents the specific methods how to handle the cargo CSAR tasking from a mission designer perspective. +-- - @{Tasking.Task_CARGO#TASK_CARGO}: Documents the main methods how to handle the cargo tasking from a mission designer perspective. +-- - @{Tasking.Task_CARGO#TASK_CARGO_TRANSPORT}: Documents the specific methods how to handle the cargo transportation tasking from a mission designer perspective. +-- - @{Tasking.Task_CARGO#TASK_CARGO_CSAR}: Documents the specific methods how to handle the cargo CSAR tasking from a mission designer perspective. -- -- -- === @@ -159228,7 +172209,7 @@ end -- -- === -- --- @module Tasking.Task_Cargo +-- @module Tasking.Task_CARGO -- @image MOOSE.JPG do -- TASK_CARGO @@ -159266,8 +172247,8 @@ do -- TASK_CARGO -- -- ### 2.1.1) Cargo Tasks -- - -- - @{Tasking.Task_Cargo#TASK_CARGO_TRANSPORT} - Models the transportation of cargo to deployment zones. - -- - @{Tasking.Task_Cargo#TASK_CARGO_CSAR} - Models the rescue of downed friendly pilots from behind enemy lines. + -- - @{Tasking.Task_CARGO#TASK_CARGO_TRANSPORT} - Models the transportation of cargo to deployment zones. + -- - @{Tasking.Task_CARGO#TASK_CARGO_CSAR} - Models the rescue of downed friendly pilots from behind enemy lines. -- -- ## 2.2) Handle TASK_CARGO Events ... -- @@ -160232,11 +173213,11 @@ do -- TASK_CARGO end ---- **Tasking** -- Models tasks for players to transport cargo. +--- **Tasking** - Models tasks for players to transport cargo. -- -- **Specific features:** -- --- * Creates a task to transport @{Cargo.Cargo} to and between deployment zones. +-- * Creates a task to transport #Cargo.Cargo to and between deployment zones. -- * Derived from the TASK_CARGO class, which is derived from the TASK class. -- * Orchestrate the task flow, so go from Planned to Assigned to Success, Failed or Cancelled. -- * Co-operation tasking, so a player joins a group of players executing the same task. @@ -160278,7 +173259,7 @@ end -- -- === -- --- Please read through the @{Tasking.Task_Cargo} process to understand the mechanisms of tasking and cargo tasking and handling. +-- Please read through the #Tasking.Task_Cargo process to understand the mechanisms of tasking and cargo tasking and handling. -- -- Enjoy! -- FC @@ -160291,7 +173272,7 @@ end do -- TASK_CARGO_TRANSPORT - --- @type TASK_CARGO_TRANSPORT + -- @type TASK_CARGO_TRANSPORT -- @extends Tasking.Task_CARGO#TASK_CARGO --- Orchestrates the task for players to transport cargo to or between deployment zones. @@ -160310,7 +173291,7 @@ do -- TASK_CARGO_TRANSPORT -- -- ## 1.1) Create a command center. -- - -- First you need to create a command center using the @{Tasking.CommandCenter#COMMANDCENTER.New}() constructor. + -- First you need to create a command center using the Tasking.CommandCenter#COMMANDCENTER.New constructor. -- -- local CommandCenter = COMMANDCENTER -- :New( HQ, "Lima" ) -- Create the CommandCenter. @@ -160319,7 +173300,7 @@ do -- TASK_CARGO_TRANSPORT -- -- Tasks work in a mission, which groups these tasks to achieve a joint mission goal. -- A command center can govern multiple missions. - -- Create a new mission, using the @{Tasking.Mission#MISSION.New}() constructor. + -- Create a new mission, using the Tasking.Mission#MISSION.New constructor. -- -- -- Declare the Mission for the Command Center. -- local Mission = MISSION @@ -160333,7 +173314,7 @@ do -- TASK_CARGO_TRANSPORT -- ## 1.3) Create the transport cargo task. -- -- So, now that we have a command center and a mission, we now create the transport task. - -- We create the transport task using the @{#TASK_CARGO_TRANSPORT.New}() constructor. + -- We create the transport task using the #TASK_CARGO_TRANSPORT.New constructor. -- -- Because a transport task will not generate the cargo itself, you'll need to create it first. -- The cargo in this case will be the downed pilot! @@ -160352,7 +173333,7 @@ do -- TASK_CARGO_TRANSPORT -- -- The cargoset "CargoSet" will embed all defined cargo of type "Pilots" (prefix) into its set. -- local CargoGroup = CARGO_GROUP:New( PilotGroup, "Cargo", "Engineer Team 1", 500 ) -- - -- What is also needed, is to have a set of @{Core.Group}s defined that contains the clients of the players. + -- What is also needed, is to have a set of Core.Groups defined that contains the clients of the players. -- -- -- Allocate the Transport, which are the helicopter to retrieve the pilot, that can be manned by players. -- local GroupSet = SET_GROUP:New():FilterPrefixes( "Transport" ):FilterStart() @@ -160373,48 +173354,48 @@ do -- TASK_CARGO_TRANSPORT -- By doing this, cargo transport tasking will become a dynamic experience. -- -- - -- # 2) Create a task using the @{Tasking.Task_Cargo_Dispatcher} module. + -- # 2) Create a task using the Tasking.Task_Cargo_Dispatcher module. -- - -- Actually, it is better to **GENERATE** these tasks using the @{Tasking.Task_Cargo_Dispatcher} module. - -- Using the dispatcher module, transport tasks can be created much more easy. + -- Actually, it is better to **GENERATE** these tasks using the Tasking.Task_Cargo_Dispatcher module. + -- Using the dispatcher module, transport tasks can be created easier. -- -- Find below an example how to use the TASK_CARGO_DISPATCHER class: -- -- - -- -- Find the HQ group. - -- HQ = GROUP:FindByName( "HQ", "Bravo" ) + -- -- Find the HQ group. + -- HQ = GROUP:FindByName( "HQ", "Bravo" ) -- - -- -- Create the command center with the name "Lima". - -- CommandCenter = COMMANDCENTER - -- :New( HQ, "Lima" ) + -- -- Create the command center with the name "Lima". + -- CommandCenter = COMMANDCENTER + -- :New( HQ, "Lima" ) -- - -- -- Create the mission, for the command center, with the name "Operation Cargo Fun", a "Tactical" mission, with the mission briefing "Transport Cargo", for the BLUE coalition. - -- Mission = MISSION - -- :New( CommandCenter, "Operation Cargo Fun", "Tactical", "Transport Cargo", coalition.side.BLUE ) + -- -- Create the mission, for the command center, with the name "Operation Cargo Fun", a "Tactical" mission, with the mission briefing "Transport Cargo", for the BLUE coalition. + -- Mission = MISSION + -- :New( CommandCenter, "Operation Cargo Fun", "Tactical", "Transport Cargo", coalition.side.BLUE ) -- - -- -- Create the SET of GROUPs containing clients (players) that will transport the cargo. - -- -- These are have a name that start with "Transport" and are of the "blue" coalition. - -- TransportGroups = SET_GROUP:New():FilterCoalitions( "blue" ):FilterPrefixes( "Transport" ):FilterStart() + -- -- Create the SET of GROUPs containing clients (players) that will transport the cargo. + -- -- These are have a name that start with "Transport" and are of the "blue" coalition. + -- TransportGroups = SET_GROUP:New():FilterCoalitions( "blue" ):FilterPrefixes( "Transport" ):FilterStart() -- -- - -- -- Here we create the TASK_CARGO_DISPATCHER object! This is where we assign the dispatcher to generate tasks in the Mission for the TransportGroups. - -- TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, TransportGroups ) + -- -- Here we create the TASK_CARGO_DISPATCHER object! This is where we assign the dispatcher to generate tasks in the Mission for the TransportGroups. + -- TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, TransportGroups ) -- -- - -- -- Here we declare the SET of CARGOs called "Workmaterials". - -- local CargoSetWorkmaterials = SET_CARGO:New():FilterTypes( "Workmaterials" ):FilterStart() + -- -- Here we declare the SET of CARGOs called "Workmaterials". + -- local CargoSetWorkmaterials = SET_CARGO:New():FilterTypes( "Workmaterials" ):FilterStart() -- - -- -- Here we declare (add) CARGO_GROUP objects of various types, that are filtered and added in the CargoSetworkmaterials cargo set. - -- -- These cargo objects have the type "Workmaterials" which is exactly the type of cargo the CargoSetworkmaterials is filtering on. - -- local EngineerCargoGroup = CARGO_GROUP:New( GROUP:FindByName( "Engineers" ), "Workmaterials", "Engineers", 250 ) - -- local ConcreteCargo = CARGO_SLINGLOAD:New( STATIC:FindByName( "Concrete" ), "Workmaterials", "Concrete", 150, 50 ) - -- local CrateCargo = CARGO_CRATE:New( STATIC:FindByName( "Crate" ), "Workmaterials", "Crate", 150, 50 ) - -- local EnginesCargo = CARGO_CRATE:New( STATIC:FindByName( "Engines" ), "Workmaterials", "Engines", 150, 50 ) - -- local MetalCargo = CARGO_CRATE:New( STATIC:FindByName( "Metal" ), "Workmaterials", "Metal", 150, 50 ) + -- -- Here we declare (add) CARGO_GROUP objects of various types, that are filtered and added in the CargoSetworkmaterials cargo set. + -- -- These cargo objects have the type "Workmaterials" which is exactly the type of cargo the CargoSetworkmaterials is filtering on. + -- local EngineerCargoGroup = CARGO_GROUP:New( GROUP:FindByName( "Engineers" ), "Workmaterials", "Engineers", 250 ) + -- local ConcreteCargo = CARGO_SLINGLOAD:New( STATIC:FindByName( "Concrete" ), "Workmaterials", "Concrete", 150, 50 ) + -- local CrateCargo = CARGO_CRATE:New( STATIC:FindByName( "Crate" ), "Workmaterials", "Crate", 150, 50 ) + -- local EnginesCargo = CARGO_CRATE:New( STATIC:FindByName( "Engines" ), "Workmaterials", "Engines", 150, 50 ) + -- local MetalCargo = CARGO_CRATE:New( STATIC:FindByName( "Metal" ), "Workmaterials", "Metal", 150, 50 ) -- - -- -- And here we create a new WorkplaceTask, using the :AddTransportTask method of the TaskDispatcher. - -- local WorkplaceTask = TaskDispatcher:AddTransportTask( "Build a Workplace", CargoSetWorkmaterials, "Transport the workers, engineers and the equipment near the Workplace." ) - -- TaskDispatcher:SetTransportDeployZone( WorkplaceTask, ZONE:New( "Workplace" ) ) + -- -- And here we create a new WorkplaceTask, using the :AddTransportTask method of the TaskDispatcher. + -- local WorkplaceTask = TaskDispatcher:AddTransportTask( "Build a Workplace", CargoSetWorkmaterials, "Transport the workers, engineers and the equipment near the Workplace." ) + -- TaskDispatcher:SetTransportDeployZone( WorkplaceTask, ZONE:New( "Workplace" ) ) -- -- # 3) Handle cargo task events. -- @@ -160423,7 +173404,7 @@ do -- TASK_CARGO_TRANSPORT -- In order to properly capture the events and avoid mistakes using the documentation, it is advised that you execute the following actions: -- -- * **Copy / Paste** the code section into your script. - -- * **Change** the CLASS literal to the task object name you have in your script. + -- * **Change** the "myclass" literal to the task object name you have in your script. -- * Within the function, you can now **write your own code**! -- * **IntelliSense** will recognize the type of the variables provided by the function. Note: the From, Event and To variables can be safely ignored, -- but you need to declare them as they are automatically provided by the event handling system of MOOSE. @@ -160444,14 +173425,13 @@ do -- TASK_CARGO_TRANSPORT -- If you want to code your own event handler, use this code fragment to tailor the event when a player carrier has picked up a cargo object in the CarrierGroup. -- You can use this event handler to post messages to players, or provide status updates etc. -- - -- --- CargoPickedUp event handler OnAfter for CLASS. - -- -- @param #CLASS self + -- --- CargoPickedUp event handler OnAfter for "myclass". -- -- @param #string From A string that contains the "*from state name*" when the event was triggered. -- -- @param #string Event A string that contains the "*event name*" when the event was triggered. -- -- @param #string To A string that contains the "*to state name*" when the event was triggered. -- -- @param Wrapper.Unit#UNIT TaskUnit The unit (client) of the player that has picked up the cargo. -- -- @param Cargo.Cargo#CARGO Cargo The cargo object that has been picked up. Note that this can be a CARGO_GROUP, CARGO_CRATE or CARGO_SLINGLOAD object! - -- function CLASS:OnAfterCargoPickedUp( From, Event, To, TaskUnit, Cargo ) + -- function myclass:OnAfterCargoPickedUp( From, Event, To, TaskUnit, Cargo ) -- -- -- Write here your own code. -- @@ -160474,15 +173454,14 @@ do -- TASK_CARGO_TRANSPORT -- You can use this event handler to post messages to players, or provide status updates etc. -- -- - -- --- CargoDeployed event handler OnAfter for CLASS. - -- -- @param #CLASS self + -- --- CargoDeployed event handler OnAfter foR "myclass". -- -- @param #string From A string that contains the "*from state name*" when the event was triggered. -- -- @param #string Event A string that contains the "*event name*" when the event was triggered. -- -- @param #string To A string that contains the "*to state name*" when the event was triggered. -- -- @param Wrapper.Unit#UNIT TaskUnit The unit (client) of the player that has deployed the cargo. -- -- @param Cargo.Cargo#CARGO Cargo The cargo object that has been deployed. Note that this can be a CARGO_GROUP, CARGO_CRATE or CARGO_SLINGLOAD object! -- -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. - -- function CLASS:OnAfterCargoDeployed( From, Event, To, TaskUnit, Cargo, DeployZone ) + -- function myclass:OnAfterCargoDeployed( From, Event, To, TaskUnit, Cargo, DeployZone ) -- -- -- Write here your own code. -- @@ -160592,7 +173571,7 @@ do -- TASK_CARGO_TRANSPORT end ---- **Tasking** -- Orchestrates the task for players to execute CSAR for downed pilots. +--- **Tasking** - Orchestrates the task for players to execute CSAR for downed pilots. -- -- **Specific features:** -- @@ -160638,7 +173617,7 @@ end -- -- === -- --- Please read through the @{Tasking.Task_Cargo} process to understand the mechanisms of tasking and cargo tasking and handling. +-- Please read through the @{Tasking.Task_CARGO} process to understand the mechanisms of tasking and cargo tasking and handling. -- -- The cargo will be a downed pilot, which is located somwhere on the battlefield. Use the menus system and facilities to -- join the CSAR task, and retrieve the pilot from behind enemy lines. The menu system is generic, there is nothing @@ -161670,6 +174649,7 @@ do -- TASK_CARGO_DISPATCHER -- If no TaskPrefix is given, then "Transport" will be used as the prefix. -- @param Core.SetCargo#SET_CARGO SetCargo The SetCargo to be transported. -- @param #string Briefing The briefing of the task transport to be shown to the player. + -- @param #boolean Silent If true don't send a message that a new task is available. -- @return Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT -- @usage -- @@ -161692,10 +174672,12 @@ do -- TASK_CARGO_DISPATCHER -- -- Here we set a TransportDeployZone. We use the WorkplaceTask as the reference, and provide a ZONE object. -- TaskDispatcher:SetTransportDeployZone( WorkplaceTask, ZONE:New( "Workplace" ) ) -- - function TASK_CARGO_DISPATCHER:AddTransportTask( TaskPrefix, SetCargo, Briefing ) + function TASK_CARGO_DISPATCHER:AddTransportTask( TaskPrefix, SetCargo, Briefing, Silent ) self.TransportCount = self.TransportCount + 1 + local verbose = Silent or false + local TaskName = string.format( ( TaskPrefix or "Transport" ) .. ".%03d", self.TransportCount ) self.Transport[TaskName] = {} @@ -161704,7 +174686,7 @@ do -- TASK_CARGO_DISPATCHER self.Transport[TaskName].Task = nil self.Transport[TaskName].TaskPrefix = TaskPrefix - self:ManageTasks() + self:ManageTasks(verbose) return self.Transport[TaskName] and self.Transport[TaskName].Task end @@ -161772,10 +174754,11 @@ do -- TASK_CARGO_DISPATCHER --- Assigns tasks to the @{Core.Set#SET_GROUP}. -- @param #TASK_CARGO_DISPATCHER self + -- @param #boolean Silent Announce new task (nil/false) or not (true). -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop. - function TASK_CARGO_DISPATCHER:ManageTasks() + function TASK_CARGO_DISPATCHER:ManageTasks(Silent) self:F() - + local verbose = Silent and true local AreaMsg = {} local TaskMsg = {} local ChangeMsg = {} @@ -161884,7 +174867,7 @@ do -- TASK_CARGO_DISPATCHER local TaskText = TaskReport:Text(", ") for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do - if ( not Mission:IsGroupAssigned(TaskGroup) ) and TaskText ~= "" then + if ( not Mission:IsGroupAssigned(TaskGroup) ) and TaskText ~= "" and not verbose then Mission:GetCommandCenter():MessageToGroup( string.format( "%s has tasks %s. Subscribe to a task using the radio menu.", Mission:GetShortText(), TaskText ), TaskGroup ) end end @@ -161905,14 +174888,14 @@ end -- -- === -- --- @module Tasking.TaskZoneCapture +-- @module Tasking.Task_Capture_Zone -- @image MOOSE.JPG do -- TASK_ZONE_GOAL --- The TASK_ZONE_GOAL class -- @type TASK_ZONE_GOAL - -- @field Core.ZoneGoal#ZONE_GOAL ZoneGoal + -- @field Functional.ZoneGoal#ZONE_GOAL ZoneGoal -- @extends Tasking.Task#TASK --- # TASK_ZONE_GOAL class, extends @{Tasking.Task#TASK} @@ -161944,7 +174927,7 @@ do -- TASK_ZONE_GOAL -- @param Tasking.Mission#MISSION Mission -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. -- @param #string TaskName The name of the Task. - -- @param Core.ZoneGoalCoalition#ZONE_GOAL_COALITION ZoneGoal + -- @param Functional.ZoneGoalCoalition#ZONE_GOAL_COALITION ZoneGoal -- @return #TASK_ZONE_GOAL self function TASK_ZONE_GOAL:New( Mission, SetGroup, TaskName, ZoneGoal, TaskType, TaskBriefing ) local self = BASE:Inherit( self, TASK:New( Mission, SetGroup, TaskName, TaskType, TaskBriefing ) ) -- #TASK_ZONE_GOAL @@ -162012,10 +174995,10 @@ do -- TASK_ZONE_GOAL end --- @param #TASK_ZONE_GOAL self - -- @param Core.ZoneGoal#ZONE_GOAL ZoneGoal The ZoneGoal Engine. + -- @param Functional.ZoneGoal#ZONE_GOAL ZoneGoal The ZoneGoal Engine. function TASK_ZONE_GOAL:SetProtect( ZoneGoal ) - self.ZoneGoal = ZoneGoal -- Core.ZoneGoal#ZONE_GOAL + self.ZoneGoal = ZoneGoal -- Functional.ZoneGoal#ZONE_GOAL end @@ -162066,10 +175049,10 @@ do -- TASK_CAPTURE_ZONE --- The TASK_CAPTURE_ZONE class -- @type TASK_CAPTURE_ZONE - -- @field Core.ZoneGoalCoalition#ZONE_GOAL_COALITION ZoneGoal + -- @field Functional.ZoneGoalCoalition#ZONE_GOAL_COALITION ZoneGoal -- @extends #TASK_ZONE_GOAL - --- # TASK_CAPTURE_ZONE class, extends @{Tasking.TaskZoneGoal#TASK_ZONE_GOAL} + --- # TASK_CAPTURE_ZONE class, extends @{Tasking.Task_Capture_Zone#TASK_ZONE_GOAL} -- -- The TASK_CAPTURE_ZONE class defines an Suppression or Extermination of Air Defenses task for a human player to be executed. -- These tasks are important to be executed as they will help to achieve air superiority at the vicinity. @@ -162088,7 +175071,7 @@ do -- TASK_CAPTURE_ZONE -- @param Tasking.Mission#MISSION Mission -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. -- @param #string TaskName The name of the Task. - -- @param Core.ZoneGoalCoalition#ZONE_GOAL_COALITION ZoneGoalCoalition + -- @param Functional.ZoneGoalCoalition#ZONE_GOAL_COALITION ZoneGoalCoalition -- @param #string TaskBriefing The briefing of the task. -- @return #TASK_CAPTURE_ZONE self function TASK_CAPTURE_ZONE:New( Mission, SetGroup, TaskName, ZoneGoalCoalition, TaskBriefing) @@ -162280,7 +175263,7 @@ end -- -- === -- --- @module Tasking.Task_Zone_Capture_Dispatcher +-- @module Tasking.Task_Capture_Dispatcher -- @image MOOSE.JPG do -- TASK_CAPTURE_DISPATCHER @@ -162641,30 +175624,43 @@ _DATABASE:_RegisterCargos() --- Register zones. _DATABASE:_RegisterZones() +_DATABASE:_RegisterAirbases() --- Check if os etc is available. BASE:I("Checking de-sanitization of os, io and lfs:") -local __na=false +local __na = false if os then BASE:I("- os available") else BASE:I("- os NOT available! Some functions may not work.") - __na=true + __na = true end if io then BASE:I("- io available") else BASE:I("- io NOT available! Some functions may not work.") - __na=true + __na = true end if lfs then BASE:I("- lfs available") else BASE:I("- lfs NOT available! Some functions may not work.") - __na=true + __na = true end if __na then BASE:I("Check /Scripts/MissionScripting.lua and comment out the lines with sanitizeModule(''). Use at your own risk!)") end +BASE.ServerName = "Unknown" +if lfs and loadfile then + local serverfile = lfs.writedir() .. 'Config/serverSettings.lua' + if UTILS.FileExists(serverfile) then + loadfile(serverfile)() + if cfg and cfg.name then + BASE.ServerName = cfg.name + end + end + BASE.ServerName = BASE.ServerName or "Unknown" + BASE:I("Server Name: " .. tostring(BASE.ServerName)) +end BASE:TraceOnOff( false ) env.info( '*** MOOSE INCLUDE END *** ' ) diff --git a/OVERLOAD-Caucasus.miz b/OVERLOAD-Caucasus.miz index 89f7894..14c265f 100644 Binary files a/OVERLOAD-Caucasus.miz and b/OVERLOAD-Caucasus.miz differ diff --git a/overload-mission.lua b/overload-mission.lua deleted file mode 100644 index bd0f473..0000000 --- a/overload-mission.lua +++ /dev/null @@ -1,55 +0,0 @@ -SPAWN:New('Su33 Group 1') - :InitKeepUnitNames(false) - :InitLimit(2, 500) - :InitDelayOff() - :InitSkill("Random") - :InitRepeatOnLanding() - :SpawnScheduled(600, .3) - -SPAWN:New('Su27 Group 1') - :InitKeepUnitNames(false) - :InitLimit(3, 500) - :InitDelayOff() - :InitSkill("Random") - :InitRepeatOnLanding() - :SpawnScheduled(600, .3) - -SPAWN:New('Su27 Group 2') - :InitKeepUnitNames(false) - :InitLimit(4, 500) - :InitDelayOff() - :InitSkill("Random") - :InitRepeatOnLanding() - :SpawnScheduled(600, .3) - -SPAWN:New('MiG29 Group 1') - :InitKeepUnitNames(false) - :InitLimit(3, 500) - :InitDelayOff() - :InitSkill("Random") - :InitRepeatOnLanding() - :SpawnScheduled(600, .3) - -SPAWN:New('Su27 Group 2') - :InitKeepUnitNames(false) - :InitLimit(4, 500) - :InitDelayOff() - :InitSkill("Random") - :InitRepeatOnLanding() - :SpawnScheduled(600, .3) - -SPAWN:New('Su27 Group 3') - :InitKeepUnitNames(false) - :InitLimit(4, 500) - :InitDelayOff() - :InitSkill("Random") - :InitRepeatOnLanding() - :SpawnScheduled(600, .3) - -SPAWN:New('MiG29 Group 2') - :InitKeepUnitNames(false) - :InitLimit(4, 500) - :InitDelayOff() - :InitSkill("Random") - :InitRepeatOnLanding() - :SpawnScheduled(600, .3) \ No newline at end of file diff --git a/primary_script.lua b/primary_script.lua new file mode 100644 index 0000000..ec1b96b --- /dev/null +++ b/primary_script.lua @@ -0,0 +1,46 @@ +local PlaneTable = { + "Opp-MiG29A", + "Opp-MiG29G", + "Opp-Su30MKA", + "Opp-Su27", + "Opp-Su33", + "Opp-MiG21", + "Opp-F16C", + "Opp-F5E3", + "Opp-F14A", + "Opp-F14B", + "Opp-F15C", + "Opp-F15EX", + "Opp-F86", + "Opp-FA18", + "Opp-JF17", + "Opp-M2000C", + "Opp-MiG15", + "Opp-MiG19", + "Opp-MiG29S", + "Opp-MiG31", + "Opp-F1CE", + "Opp-F1EE", + "Opp-Su57", + "Opp-J11A" +} + +local ZoneTable = { + ZONE:New("Zone-1"), + ZONE:New("Zone-2"), + ZONE:New("Zone-3"), + ZONE:New("Zone-4"), + ZONE:New("Zone-5"), + ZONE:New("Zone-6"), + ZONE:New("Zone-7") +} + +SPAWN:New("Opp-Plane") + :InitKeepUnitNames(false) + :InitLimit(10, 200) + :InitRandomizeRoute(0, 1, 25, 10000) + :InitRandomizeTemplate(PlaneTable) + :InitRandomizeZones(ZoneTable) + :InitSkill("Random") + :InitRepeatOnLanding() + :SpawnScheduled(30, 0.5) \ No newline at end of file