diff --git a/game/operation/operation.py b/game/operation/operation.py index 43883625..69c152ba 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -148,23 +148,23 @@ class Operation: logging.debug( f"Skipping already loaded {script} for {plugin_mnemonic}" ) + else: + self.plugin_scripts.append(script_mnemonic) - self.plugin_scripts.append(script_mnemonic) + plugin_path = Path("./resources/plugins", plugin_mnemonic) - plugin_path = Path("./resources/plugins", plugin_mnemonic) + script_path = Path(plugin_path, script) + if not script_path.exists(): + logging.error( + f"Cannot find {script_path} for plugin {plugin_mnemonic}" + ) + return - script_path = Path(plugin_path, script) - if not script_path.exists(): - logging.error( - f"Cannot find {script_path} for plugin {plugin_mnemonic}" - ) - return - - trigger = TriggerStart(comment=f"Load {script_mnemonic}") - filename = script_path.resolve() - fileref = self.current_mission.map_resource.add_resource_file(filename) - trigger.add_action(DoScriptFile(fileref)) - self.current_mission.triggerrules.triggers.append(trigger) + trigger = TriggerStart(comment=f"Load {script_mnemonic}") + filename = script_path.resolve() + fileref = self.current_mission.map_resource.add_resource_file(filename) + trigger.add_action(DoScriptFile(fileref)) + self.current_mission.triggerrules.triggers.append(trigger) def generate(self): radio_registry = RadioRegistry() diff --git a/gen/sam/genericsam_group_generator.py b/gen/sam/genericsam_group_generator.py new file mode 100644 index 00000000..00d5ed6e --- /dev/null +++ b/gen/sam/genericsam_group_generator.py @@ -0,0 +1,19 @@ +import random + +from dcs.vehicles import AirDefence +from game import db +from gen.sam.group_generator import GroupGenerator + + +class GenericSamGroupGenerator(GroupGenerator): + """ + This is the base for all SAM group generators + """ + + @property + def groupNamePrefix(self) -> str: + # prefix the SAM site for use with the Skynet IADS plugin + if self.faction == self.game.player_name: # this is the player faction + return "BLUE SAM " + else: + return "RED SAM " diff --git a/gen/sam/group_generator.py b/gen/sam/group_generator.py index e6620211..9f150ef4 100644 --- a/gen/sam/group_generator.py +++ b/gen/sam/group_generator.py @@ -8,16 +8,20 @@ from dcs.unit import Vehicle class GroupGenerator(): - def __init__(self, game, ground_object): + def __init__(self, game, ground_object, faction = None): # faction is not mandatory because some subclasses do not use it self.game = game self.go = ground_object self.position = ground_object.position self.heading = random.randint(0, 359) - self.vg = unitgroup.VehicleGroup(self.game.next_group_id(), self.go.group_identifier) - + self.faction = faction + self.vg = unitgroup.VehicleGroup(self.game.next_group_id(), self.groupNamePrefix + self.go.group_identifier) wp = self.vg.add_waypoint(self.position, PointAction.OffRoad, 0) wp.ETA_locked = True + @property + def groupNamePrefix(self) -> str: + return "" + def generate(self): raise NotImplementedError diff --git a/gen/sam/sam_group_generator.py b/gen/sam/sam_group_generator.py index 25472ea1..7a127830 100644 --- a/gen/sam/sam_group_generator.py +++ b/gen/sam/sam_group_generator.py @@ -106,7 +106,6 @@ def get_faction_possible_sams_generator(faction: str) -> List[Type[GroupGenerato """ return [SAM_MAP[s] for s in db.FACTIONS[faction].sams if s in SAM_MAP.keys()] - def generate_anti_air_group(game, parent_cp, ground_object, faction:str): """ This generate a SAM group @@ -118,7 +117,7 @@ def generate_anti_air_group(game, parent_cp, ground_object, faction:str): possible_sams_generators = get_faction_possible_sams_generator(faction) if len(possible_sams_generators) > 0: sam_generator_class = random.choice(possible_sams_generators) - generator = sam_generator_class(game, ground_object) + generator = sam_generator_class(game, ground_object, faction) generator.generate() return generator.get_generated_group() return None @@ -139,5 +138,3 @@ def generate_shorad_group(game, parent_cp, ground_object, faction_name: str): - - diff --git a/gen/sam/sam_hawk.py b/gen/sam/sam_hawk.py index 89c11bc0..da8c700a 100644 --- a/gen/sam/sam_hawk.py +++ b/gen/sam/sam_hawk.py @@ -2,10 +2,10 @@ import random from dcs.vehicles import AirDefence -from gen.sam.group_generator import GroupGenerator +from gen.sam.genericsam_group_generator import GenericSamGroupGenerator -class HawkGenerator(GroupGenerator): +class HawkGenerator(GenericSamGroupGenerator): """ This generate an HAWK group """ @@ -14,8 +14,8 @@ class HawkGenerator(GroupGenerator): price = 115 def generate(self): - self.add_unit(AirDefence.SAM_Hawk_PCP, "PCP", self.position.x, self.position.y, self.heading) self.add_unit(AirDefence.SAM_Hawk_SR_AN_MPQ_50, "SR", self.position.x + 20, self.position.y, self.heading) + self.add_unit(AirDefence.SAM_Hawk_PCP, "PCP", self.position.x, self.position.y, self.heading) self.add_unit(AirDefence.SAM_Hawk_TR_AN_MPQ_46, "TR", self.position.x + 40, self.position.y, self.heading) # Triple A for close range defense diff --git a/gen/sam/sam_hq7.py b/gen/sam/sam_hq7.py index f8a531ea..adba14b5 100644 --- a/gen/sam/sam_hq7.py +++ b/gen/sam/sam_hq7.py @@ -2,10 +2,10 @@ import random from dcs.vehicles import AirDefence -from gen.sam.group_generator import GroupGenerator +from gen.sam.genericsam_group_generator import GenericSamGroupGenerator -class HQ7Generator(GroupGenerator): +class HQ7Generator(GenericSamGroupGenerator): """ This generate an HQ7 group """ diff --git a/gen/sam/sam_patriot.py b/gen/sam/sam_patriot.py index b55dbaea..782655e2 100644 --- a/gen/sam/sam_patriot.py +++ b/gen/sam/sam_patriot.py @@ -2,10 +2,10 @@ import random from dcs.vehicles import AirDefence -from gen.sam.group_generator import GroupGenerator +from gen.sam.genericsam_group_generator import GenericSamGroupGenerator -class PatriotGenerator(GroupGenerator): +class PatriotGenerator(GenericSamGroupGenerator): """ This generate a Patriot group """ @@ -15,11 +15,11 @@ class PatriotGenerator(GroupGenerator): def generate(self): # Command Post + self.add_unit(AirDefence.SAM_Patriot_STR_AN_MPQ_53, "ICC", self.position.x + 30, self.position.y + 30, self.heading) self.add_unit(AirDefence.SAM_Patriot_AMG_AN_MRC_137, "MRC", self.position.x, self.position.y, self.heading) self.add_unit(AirDefence.SAM_Patriot_ECS_AN_MSQ_104, "MSQ", self.position.x + 30, self.position.y, self.heading) self.add_unit(AirDefence.SAM_Patriot_ICC, "ICC", self.position.x + 60, self.position.y, self.heading) self.add_unit(AirDefence.SAM_Patriot_EPP_III, "EPP", self.position.x, self.position.y + 30, self.heading) - self.add_unit(AirDefence.SAM_Patriot_STR_AN_MPQ_53, "ICC", self.position.x + 30, self.position.y + 30, self.heading) num_launchers = random.randint(3, 4) positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=360) @@ -30,4 +30,4 @@ class PatriotGenerator(GroupGenerator): num_launchers = random.randint(3, 4) positions = self.get_circular_position(num_launchers, launcher_distance=300, coverage=360) for i, position in enumerate(positions): - self.add_unit(AirDefence.AAA_Vulcan_M163, "SPAAA#" + str(i), position[0], position[1], position[2]) + self.add_unit(AirDefence.AAA_Vulcan_M163, "SPAAA#" + str(i), position[0], position[1], position[2]) \ No newline at end of file diff --git a/gen/sam/sam_rapier.py b/gen/sam/sam_rapier.py index 99b7b205..981a098e 100644 --- a/gen/sam/sam_rapier.py +++ b/gen/sam/sam_rapier.py @@ -2,10 +2,10 @@ import random from dcs.vehicles import AirDefence -from gen.sam.group_generator import GroupGenerator +from gen.sam.genericsam_group_generator import GenericSamGroupGenerator -class RapierGenerator(GroupGenerator): +class RapierGenerator(GenericSamGroupGenerator): """ This generate a Rapier Group """ diff --git a/gen/sam/sam_roland.py b/gen/sam/sam_roland.py index 9e31d5fe..1f970517 100644 --- a/gen/sam/sam_roland.py +++ b/gen/sam/sam_roland.py @@ -1,9 +1,9 @@ from dcs.vehicles import AirDefence, Unarmed -from gen.sam.group_generator import GroupGenerator +from gen.sam.genericsam_group_generator import GenericSamGroupGenerator -class RolandGenerator(GroupGenerator): +class RolandGenerator(GenericSamGroupGenerator): """ This generate a Roland group """ @@ -12,7 +12,7 @@ class RolandGenerator(GroupGenerator): price = 40 def generate(self): - self.add_unit(AirDefence.SAM_Roland_ADS, "ADS", self.position.x, self.position.y, self.heading) self.add_unit(AirDefence.SAM_Roland_EWR, "EWR", self.position.x + 40, self.position.y, self.heading) + self.add_unit(AirDefence.SAM_Roland_ADS, "ADS", self.position.x, self.position.y, self.heading) self.add_unit(Unarmed.Transport_M818, "TRUCK", self.position.x + 80, self.position.y, self.heading) diff --git a/gen/sam/sam_sa10.py b/gen/sam/sam_sa10.py index ae2102f0..6ea5809f 100644 --- a/gen/sam/sam_sa10.py +++ b/gen/sam/sam_sa10.py @@ -2,10 +2,10 @@ import random from dcs.vehicles import AirDefence -from gen.sam.group_generator import GroupGenerator +from gen.sam.genericsam_group_generator import GenericSamGroupGenerator -class SA10Generator(GroupGenerator): +class SA10Generator(GenericSamGroupGenerator): """ This generate a SA-10 group """ @@ -14,15 +14,15 @@ class SA10Generator(GroupGenerator): price = 450 def generate(self): - # Command Post - self.add_unit(AirDefence.SAM_SA_10_S_300PS_CP_54K6, "CP", self.position.x, self.position.y, self.heading) - # Search Radar self.add_unit(AirDefence.SAM_SA_10_S_300PS_SR_5N66M, "SR1", self.position.x, self.position.y + 40, self.heading) # Search radar for missiles (optionnal) self.add_unit(AirDefence.SAM_SA_10_S_300PS_SR_64H6E, "SR2", self.position.x - 40, self.position.y, self.heading) + # Command Post + self.add_unit(AirDefence.SAM_SA_10_S_300PS_CP_54K6, "CP", self.position.x, self.position.y, self.heading) + # 2 Tracking radars self.add_unit(AirDefence.SAM_SA_10_S_300PS_TR_30N6, "TR1", self.position.x - 40, self.position.y - 40, self.heading) diff --git a/gen/sam/sam_sa11.py b/gen/sam/sam_sa11.py index 3af6c242..e7634b92 100644 --- a/gen/sam/sam_sa11.py +++ b/gen/sam/sam_sa11.py @@ -2,10 +2,10 @@ import random from dcs.vehicles import AirDefence -from gen.sam.group_generator import GroupGenerator +from gen.sam.genericsam_group_generator import GenericSamGroupGenerator -class SA11Generator(GroupGenerator): +class SA11Generator(GenericSamGroupGenerator): """ This generate a SA-11 group """ @@ -14,8 +14,8 @@ class SA11Generator(GroupGenerator): price = 180 def generate(self): - self.add_unit(AirDefence.SAM_SA_11_Buk_CC_9S470M1, "CC", self.position.x, self.position.y, self.heading) self.add_unit(AirDefence.SAM_SA_11_Buk_SR_9S18M1, "SR", self.position.x+20, self.position.y, self.heading) + self.add_unit(AirDefence.SAM_SA_11_Buk_CC_9S470M1, "CC", self.position.x, self.position.y, self.heading) num_launchers = random.randint(2, 4) positions = self.get_circular_position(num_launchers, launcher_distance=140, coverage=180) diff --git a/gen/sam/sam_sa2.py b/gen/sam/sam_sa2.py index c108c1e8..ff77265f 100644 --- a/gen/sam/sam_sa2.py +++ b/gen/sam/sam_sa2.py @@ -2,10 +2,10 @@ import random from dcs.vehicles import AirDefence -from gen.sam.group_generator import GroupGenerator +from gen.sam.genericsam_group_generator import GenericSamGroupGenerator -class SA2Generator(GroupGenerator): +class SA2Generator(GenericSamGroupGenerator): """ This generate a SA-2 group """ diff --git a/gen/sam/sam_sa3.py b/gen/sam/sam_sa3.py index 455bab19..e57f184c 100644 --- a/gen/sam/sam_sa3.py +++ b/gen/sam/sam_sa3.py @@ -2,10 +2,10 @@ import random from dcs.vehicles import AirDefence -from gen.sam.group_generator import GroupGenerator +from gen.sam.genericsam_group_generator import GenericSamGroupGenerator -class SA3Generator(GroupGenerator): +class SA3Generator(GenericSamGroupGenerator): """ This generate a SA-3 group """ diff --git a/gen/sam/sam_sa6.py b/gen/sam/sam_sa6.py index 7ec2afca..1028ed76 100644 --- a/gen/sam/sam_sa6.py +++ b/gen/sam/sam_sa6.py @@ -2,10 +2,10 @@ import random from dcs.vehicles import AirDefence -from gen.sam.group_generator import GroupGenerator +from gen.sam.genericsam_group_generator import GenericSamGroupGenerator -class SA6Generator(GroupGenerator): +class SA6Generator(GenericSamGroupGenerator): """ This generate a SA-6 group """ diff --git a/resources/plugins/plugins.json b/resources/plugins/plugins.json index 5b809d07..6dcac3ae 100644 --- a/resources/plugins/plugins.json +++ b/resources/plugins/plugins.json @@ -1,4 +1,5 @@ -[ +[ + "base", "jtacautolase", - "base" + "skynetiads" ] diff --git a/resources/plugins/skynetiads/plugin.json b/resources/plugins/skynetiads/plugin.json new file mode 100644 index 00000000..7fe1e176 --- /dev/null +++ b/resources/plugins/skynetiads/plugin.json @@ -0,0 +1,59 @@ +{ + "mnemonic": "skynetiads", + "nameInUI": "Skynet IADS", + "defaultValue": false, + "specificOptions": [ + { + "nameInUI": "create IADS for RED coalition", + "mnemonic": "createRedIADS", + "defaultValue": true + }, + { + "nameInUI": "create IADS for BLUE coalition", + "mnemonic": "createBlueIADS", + "defaultValue": true + }, + { + "nameInUI": "Long-range SAM act as EWR for RED coalition", + "mnemonic": "actAsEwrRED", + "defaultValue": true + }, + { + "nameInUI": "Long-range SAM act as EWR for BLUE coalition", + "mnemonic": "actAsEwrBLUE", + "defaultValue": true + }, + { + "nameInUI": "Include RED IADS in radio menu", + "mnemonic": "includeRedInRadio", + "defaultValue": true + }, + { + "nameInUI": "Include BLUE IADS in radio menu", + "mnemonic": "includeBlueInRadio", + "defaultValue": true + }, + { + "nameInUI": "Generate debug information for RED IADS", + "mnemonic": "debugRED", + "defaultValue": false + }, + { + "nameInUI": "Generate debug information for BLUE IADS", + "mnemonic": "debugBLUE", + "defaultValue": false + } + ], + "scriptsWorkOrders": [ + { + "file": "skynet-iads-compiled.lua", + "mnemonic": "skynetiads-script" + } + ], + "configurationWorkOrders": [ + { + "file": "skynetiads-config.lua", + "mnemonic": "skynetiads-config" + } + ] +} \ No newline at end of file diff --git a/resources/plugins/skynetiads/skynet-iads-compiled.lua b/resources/plugins/skynetiads/skynet-iads-compiled.lua new file mode 100644 index 00000000..1187e93a --- /dev/null +++ b/resources/plugins/skynetiads/skynet-iads-compiled.lua @@ -0,0 +1,2963 @@ +env.info("--- SKYNET VERSION: 1.1.3 | BUILD TIME: 30.09.2020 1816Z ---") +do +--this file contains the required units per sam type +samTypesDB = { + ['S-300'] = { + ['type'] = 'complex', + ['searchRadar'] = { + ['S-300PS 40B6MD sr'] = { + }, + ['S-300PS 64H6E sr'] = { + }, + }, + ['trackingRadar'] = { + ['S-300PS 40B6M tr'] = { + }, + }, + ['launchers'] = { + ['S-300PS 5P85D ln'] = { + }, + ['S-300PS 5P85C ln'] = { + }, + }, + ['misc'] = { + ['S-300PS 54K6 cp'] = { + ['required'] = true, + }, + }, + ['name'] = { + ['NATO'] = 'SA-10 Grumble', + }, + ['harm_detection_chance'] = 90 + }, + ['Buk'] = { + ['type'] = 'complex', + ['searchRadar'] = { + ['SA-11 Buk SR 9S18M1'] = { + }, + }, + ['launchers'] = { + ['SA-11 Buk LN 9A310M1'] = { + }, + }, + ['misc'] = { + ['SA-11 Buk CC 9S470M1'] = { + ['required'] = true, + }, + }, + ['name'] = { + ['NATO'] = 'SA-11 Gadfly', + }, + ['harm_detection_chance'] = 70 + }, + ['s-125'] = { + ['type'] = 'complex', + ['searchRadar'] = { + ['p-19 s-125 sr'] = { + }, + }, + ['trackingRadar'] = { + ['snr s-125 tr'] = { + }, + }, + ['launchers'] = { + ['5p73 s-125 ln'] = { + }, + }, + ['name'] = { + ['NATO'] = 'SA-3 Goa', + }, + ['harm_detection_chance'] = 40 + }, + ['s-75'] = { + ['type'] = 'complex', + ['searchRadar'] = { + ['p-19 s-125 sr'] = { + }, + }, + ['trackingRadar'] = { + ['SNR_75V'] = { + }, + }, + ['launchers'] = { + ['S_75M_Volhov'] = { + }, + }, + ['name'] = { + ['NATO'] = 'SA-2 Guideline', + }, + ['harm_detection_chance'] = 30 + }, + ['Kub'] = { + ['type'] = 'complex', + ['searchRadar'] = { + ['Kub 1S91 str'] = { + }, + }, + ['launchers'] = { + ['Kub 2P25 ln'] = { + }, + }, + ['name'] = { + ['NATO'] = 'SA-6 Gainful', + }, + ['harm_detection_chance'] = 40 + }, + ['Patriot'] = { + ['type'] = 'complex', + ['searchRadar'] = { + ['Patriot str'] = { + }, + }, + + ['launchers'] = { + ['Patriot ln'] = { + }, + }, + ['misc'] = { + ['Patriot cp'] = { + ['required'] = false, + }, + ['Patriot EPP'] = { + ['required'] = false, + }, + ['Patriot ECS'] = { + ['required'] = true, + }, + ['Patriot AMG'] = { + ['required'] = false, + }, + }, + + + ['name'] = { + ['NATO'] = 'Patriot', + }, + ['harm_detection_chance'] = 90 + }, + ['Hawk'] = { + ['type'] = 'complex', + ['searchRadar'] = { + ['Hawk sr'] = { + }, + }, + ['trackingRadar'] = { + ['Hawk tr'] = { + }, + }, + ['launchers'] = { + ['Hawk ln'] = { + }, + }, + + ['name'] = { + ['NATO'] = 'Hawk', + }, + ['harm_detection_chance'] = 40 + + }, + ['Roland ADS'] = { + ['type'] = 'single', + ['searchRadar'] = { + ['Roland ADS'] = { + }, + }, + ['launchers'] = { + ['Roland ADS'] = { + }, + }, + + ['name'] = { + ['NATO'] = 'Roland ADS', + }, + ['harm_detection_chance'] = 60 + }, + ['2S6 Tunguska'] = { + ['type'] = 'single', + ['searchRadar'] = { + ['2S6 Tunguska'] = { + }, + }, + ['launchers'] = { + ['2S6 Tunguska'] = { + }, + }, + ['name'] = { + ['NATO'] = 'SA-19 Grison', + }, + }, + ['Osa'] = { + ['type'] = 'single', + ['searchRadar'] = { + ['Osa 9A33 ln'] = { + }, + }, + ['launchers'] = { + ['Osa 9A33 ln'] = { + + }, + }, + ['name'] = { + ['NATO'] = 'SA-8 Gecko', + }, + ['harm_detection_chance'] = 20 + }, + ['Strela-10M3'] = { + ['type'] = 'single', + ['searchRadar'] = { + ['Strela-10M3'] = { + ['trackingRadar'] = true, + }, + }, + ['launchers'] = { + ['Strela-10M3'] = { + }, + }, + ['name'] = { + ['NATO'] = 'SA-13 Gopher', + }, + }, + ['Strela-1 9P31'] = { + ['type'] = 'single', + ['searchRadar'] = { + ['Strela-1 9P31'] = { + }, + }, + ['launchers'] = { + ['Strela-1 9P31'] = { + }, + }, + ['name'] = { + ['NATO'] = 'SA-9 Gaskin', + }, + ['harm_detection_chance'] = 20 + }, + ['Tor'] = { + ['type'] = 'single', + ['searchRadar'] = { + ['Tor 9A331'] = { + }, + }, + ['launchers'] = { + ['Tor 9A331'] = { + }, + }, + ['name'] = { + ['NATO'] = 'SA-15 Gauntlet', + }, + }, + ['Gepard'] = { + ['type'] = 'single', + ['searchRadar'] = { + ['Gepard'] = { + }, + }, + ['launchers'] = { + ['Gepard'] = { + }, + }, + ['name'] = { + ['NATO'] = 'Gepard', + }, + ['harm_detection_chance'] = 10 + }, + ['Rapier'] = { + ['searchRadar'] = { + ['rapier_fsa_blindfire_radar'] = { + }, + }, + ['launchers'] = { + ['rapier_fsa_launcher'] = { + ['trackingRadar'] = true, + }, + }, + ['misc'] = { + ['rapier_fsa_optical_tracker_unit'] = { + ['required'] = true, + }, + }, + ['name'] = { + ['NATO'] = 'Rapier', + }, + ['harm_detection_chance'] = 10 + }, + ['ZSU-23-4 Shilka'] = { + ['type'] = 'single', + ['searchRadar'] = { + ['ZSU-23-4 Shilka'] = { + }, + }, + ['launchers'] = { + ['ZSU-23-4 Shilka'] = { + }, + }, + ['name'] = { + ['NATO'] = 'Zues', + }, + ['harm_detection_chance'] = 10 + }, + ['HQ-7'] = { + ['searchRadar'] = { + ['HQ-7_STR_SP'] = { + }, + }, + ['launchers'] = { + ['HQ-7_LN_SP'] = { + }, + }, + ['name'] = { + ['NATO'] = 'CSA-4', + }, + ['harm_detection_chance'] = 30 + }, +--- Start of EW radars: + ['1L13 EWR'] = { + ['type'] = 'ewr', + ['searchRadar'] = { + ['1L13 EWR'] = { + }, + }, + ['name'] = { + ['NATO'] = '1L13 EWR', + }, + ['harm_detection_chance'] = 60 + }, + ['55G6 EWR'] = { + ['type'] = 'ewr', + ['searchRadar'] = { + ['55G6 EWR'] = { + }, + }, + ['name'] = { + ['NATO'] = '55G6 EWR', + }, + ['harm_detection_chance'] = 60 + }, + ['Dog Ear'] = { + ['type'] = 'ewr', + ['searchRadar'] = { + ['Dog Ear radar'] = { + }, + }, + ['name'] = { + ['NATO'] = 'Dog Ear', + }, + ['harm_detection_chance'] = 20 + }, + ['Roland Radar'] = { + ['type'] = 'ewr', + ['searchRadar'] = { + ['Roland Radar'] = { + }, + }, + ['name'] = { + ['NATO'] = 'Roland EWR', + }, + ['harm_detection_chance'] = 60 + }, + ['p-19 s-125 sr'] = { + ['searchRadar'] = { + ['p-19 s-125 sr'] = { + }, + }, + ['name'] = { + ['NATO'] = 'Flat Face', + }, + ['harm_detection_chance'] = 40 + }, + ['Patriot str'] = { + ['searchRadar'] = { + ['Patriot str'] = { + }, + }, + ['name'] = { + ['NATO'] = 'Patriot str', + }, + ['harm_detection_chance'] = 80 + }, + ['EW S-300'] = { + ['searchRadar'] = { + ['S-300PS 40B6MD sr'] = { + }, + ['S-300PS 64H6E sr'] = { + }, + }, + ['name'] = { + ['NATO'] = 'Big Bird', + }, + ['harm_detection_chance'] = 90 + }, + ['SA-11 Buk SR 9S18M1'] = { + ['searchRadar'] = { + ['SA-11 Buk SR 9S18M1'] = { + }, + }, + ['name'] = { + ['NATO'] = 'Snow Drift', + }, + ['harm_detection_chance'] = 70 + }, + ['Kub 1S91 str'] = { + ['searchRadar'] = { + ['Kub 1S91 str'] = { + }, + }, + ['name'] = { + ['NATO'] = 'Straight Flush', + }, + ['harm_detection_chance'] = 40 + }, + ['Hawk str'] = { + ['searchRadar'] = { + ['Hawk sr'] = { + }, + }, + ['name'] = { + ['NATO'] = 'Hawk str', + }, + ['harm_detection_chance'] = 40 + }, +} +end +do + +SkynetIADS = {} +SkynetIADS.__index = SkynetIADS + +SkynetIADS.database = samTypesDB + +function SkynetIADS:create(name) + local iads = {} + setmetatable(iads, SkynetIADS) + iads.radioMenu = nil + iads.earlyWarningRadars = {} + iads.samSites = {} + iads.commandCenters = {} + iads.ewRadarScanMistTaskID = nil + iads.samSetupMistTaskID = nil + iads.coalition = nil + iads.contacts = {} + iads.maxTargetAge = 32 + iads.name = name + if iads.name == nil then + iads.name = "" + end + iads.contactUpdateInterval = 5 + iads.samSetupTime = 60 + iads.destroyedUnitResponsibleForUpdateAutonomousStateOfSAMSite = nil + iads.debugOutput = {} + iads.debugOutput.IADSStatus = false + iads.debugOutput.samWentDark = false + iads.debugOutput.contacts = false + iads.debugOutput.radarWentLive = false + iads.debugOutput.ewRadarNoConnection = false + iads.debugOutput.samNoConnection = false + iads.debugOutput.jammerProbability = false + iads.debugOutput.addedEWRadar = false + iads.debugOutput.hasNoPower = false + iads.debugOutput.addedSAMSite = false + iads.debugOutput.warnings = true + iads.debugOutput.harmDefence = false + iads.debugOutput.samSiteStatusEnvOutput = false + iads.debugOutput.earlyWarningRadarStatusEnvOutput = false + return iads +end + +function SkynetIADS:setUpdateInterval(interval) + self.contactUpdateInterval = interval +end + +function SkynetIADS:setCoalition(item) + if item then + local coalitionID = item:getCoalition() + if self.coalitionID == nil then + self.coalitionID = coalitionID + end + if self.coalitionID ~= coalitionID then + self:printOutput("element: "..item:getName().." has a different coalition than the IADS", true) + end + end +end + +function SkynetIADS:addJammer(jammer) + table.insert(self.jammers, jammer) +end + +function SkynetIADS:getCoalition() + return self.coalitionID +end + +function SkynetIADS:getDestroyedEarlyWarningRadars() + local destroyedSites = {} + for i = 1, #self.earlyWarningRadars do + local ewSite = self.earlyWarningRadars[i] + if ewSite:isDestroyed() then + table.insert(destroyedSites, ewSite) + end + end + return destroyedSites +end + +function SkynetIADS:getUsableAbstractRadarElemtentsOfTable(abstractRadarTable) + local usable = {} + for i = 1, #abstractRadarTable do + local abstractRadarElement = abstractRadarTable[i] + if abstractRadarElement:hasActiveConnectionNode() and abstractRadarElement:hasWorkingPowerSource() and abstractRadarElement:isDestroyed() == false then + table.insert(usable, abstractRadarElement) + end + end + return usable +end + +function SkynetIADS:getUsableEarlyWarningRadars() + return self:getUsableAbstractRadarElemtentsOfTable(self.earlyWarningRadars) +end + +function SkynetIADS:createTableDelegator(units) + local sites = SkynetIADSTableDelegator:create() + for i = 1, #units do + local site = units[i] + table.insert(sites, site) + end + return sites +end + +function SkynetIADS:addEarlyWarningRadarsByPrefix(prefix) + self:deactivateEarlyWarningRadars() + self.earlyWarningRadars = {} + for unitName, unit in pairs(mist.DBs.unitsByName) do + local pos = self:findSubString(unitName, prefix) + --somehow the MIST unit db contains StaticObject, we check to see we only add Units + local unit = Unit.getByName(unitName) + if pos and pos == 1 and unit then + self:addEarlyWarningRadar(unitName) + end + end + return self:createTableDelegator(self.earlyWarningRadars) +end + +function SkynetIADS:addEarlyWarningRadar(earlyWarningRadarUnitName) + local earlyWarningRadarUnit = Unit.getByName(earlyWarningRadarUnitName) + if earlyWarningRadarUnit == nil then + self:printOutput("you have added an EW Radar that does not exist, check name of Unit in Setup and Mission editor: "..earlyWarningRadarUnitName, true) + return + end + self:setCoalition(earlyWarningRadarUnit) + local ewRadar = nil + local category = earlyWarningRadarUnit:getDesc().category + if category == Unit.Category.AIRPLANE or category == Unit.Category.SHIP then + ewRadar = SkynetIADSAWACSRadar:create(earlyWarningRadarUnit, self) + else + ewRadar = SkynetIADSEWRadar:create(earlyWarningRadarUnit, self) + end + ewRadar:setupElements() + ewRadar:setCachedTargetsMaxAge(self:getCachedTargetsMaxAge()) + -- for performance improvement, if iads is not scanning no update coverage update needs to be done, will be executed once when iads activates + if self.ewRadarScanMistTaskID ~= nil then + self:updateIADSCoverage() + end + ewRadar:goLive() + table.insert(self.earlyWarningRadars, ewRadar) + if self:getDebugSettings().addedEWRadar then + self:printOutput(ewRadar:getDescription().." added to IADS") + end + return ewRadar +end + +function SkynetIADS:getCachedTargetsMaxAge() + return self.contactUpdateInterval +end + +function SkynetIADS:getEarlyWarningRadars() + return self:createTableDelegator(self.earlyWarningRadars) +end + +function SkynetIADS:getEarlyWarningRadarByUnitName(unitName) + for i = 1, #self.earlyWarningRadars do + local ewRadar = self.earlyWarningRadars[i] + if ewRadar:getDCSName() == unitName then + return ewRadar + end + end +end + +function SkynetIADS:findSubString(haystack, needle) + return string.find(haystack, needle, 1, true) +end + +function SkynetIADS:addSAMSitesByPrefix(prefix) + self:deativateSAMSites() + self.samSites = {} + for groupName, groupData in pairs(mist.DBs.groupsByName) do + local pos = self:findSubString(groupName, prefix) + if pos and pos == 1 then + --mist returns groups, units and, StaticObjects + local dcsObject = Group.getByName(groupName) + if dcsObject then + self:addSAMSite(groupName) + end + end + end + return self:createTableDelegator(self.samSites) +end + +function SkynetIADS:getSAMSitesByPrefix(prefix) + local returnSams = {} + for i = 1, #self.samSites do + local samSite = self.samSites[i] + local groupName = samSite:getDCSName() + local pos = self:findSubString(groupName, prefix) + if pos and pos == 1 then + table.insert(returnSams, samSite) + end + end + return self:createTableDelegator(returnSams) +end + +function SkynetIADS:addSAMSite(samSiteName) + local samSiteDCS = Group.getByName(samSiteName) + if samSiteDCS == nil then + self:printOutput("you have added an SAM Site that does not exist, check name of Group in Setup and Mission editor: "..tostring(samSiteName), true) + return + end + self:setCoalition(samSiteDCS) + local samSite = SkynetIADSSamSite:create(samSiteDCS, self) + samSite:setupElements() + -- for performance improvement, if iads is not scanning no update coverage update needs to be done, will be executed once when iads activates + if self.ewRadarScanMistTaskID ~= nil then + self:updateIADSCoverage() + end + samSite:setCachedTargetsMaxAge(self:getCachedTargetsMaxAge()) + samSite:goLive() + if samSite:getNatoName() == "UNKNOWN" then + self:printOutput("you have added an SAM Site that Skynet IADS can not handle: "..samSite:getDCSName(), true) + samSite:cleanUp() + else + samSite:goDark() + table.insert(self.samSites, samSite) + if self:getDebugSettings().addedSAMSite then + self:printOutput(samSite:getDescription().." added to IADS") + end + return samSite + end +end + +function SkynetIADS:getUsableSAMSites() + return self:getUsableAbstractRadarElemtentsOfTable(self.samSites) +end + +function SkynetIADS:getDestroyedSAMSites() + local destroyedSites = {} + for i = 1, #self.samSites do + local samSite = self.samSites[i] + if samSite:isDestroyed() then + table.insert(destroyedSites, samSite) + end + end + return destroyedSites +end + +function SkynetIADS:getSAMSites() + return self:createTableDelegator(self.samSites) +end + +function SkynetIADS:getActiveSAMSites() + local activeSAMSites = {} + for i = 1, #self.samSites do + if self.samSites[i]:isActive() then + table.insert(activeSAMSites, self.samSites[i]) + end + end + return activeSAMSites +end + +function SkynetIADS:getSAMSiteByGroupName(groupName) + for i = 1, #self.samSites do + local samSite = self.samSites[i] + if samSite:getDCSName() == groupName then + return samSite + end + end +end + +function SkynetIADS:getSAMSitesByNatoName(natoName) + local selectedSAMSites = SkynetIADSTableDelegator:create() + for i = 1, #self.samSites do + local samSite = self.samSites[i] + if samSite:getNatoName() == natoName then + table.insert(selectedSAMSites, samSite) + end + end + return selectedSAMSites +end + +function SkynetIADS:addCommandCenter(commandCenter) + self:setCoalition(commandCenter) + local comCenter = SkynetIADSCommandCenter:create(commandCenter, self) + table.insert(self.commandCenters, comCenter) + return comCenter +end + +function SkynetIADS:isCommandCenterUsable() + local hasWorkingCommandCenter = (#self.commandCenters == 0) + for i = 1, #self.commandCenters do + local comCenter = self.commandCenters[i] + if comCenter:isDestroyed() == false and comCenter:hasWorkingPowerSource() then + hasWorkingCommandCenter = true + break + else + hasWorkingCommandCenter = false + end + end + return hasWorkingCommandCenter +end + +function SkynetIADS:getCommandCenters() + return self.commandCenters +end + +function SkynetIADS:setSAMSitesToAutonomousMode() + for i= 1, #self.samSites do + samSite = self.samSites[i] + samSite:goAutonomous() + end +end + +function SkynetIADS.evaluateContacts(self) + if self:isCommandCenterUsable() == false then + if self:getDebugSettings().noWorkingCommmandCenter then + self:printOutput("No Working Command Center") + end + self:setSAMSitesToAutonomousMode() + return + end + + local ewRadars = self:getUsableEarlyWarningRadars() + local samSites = self:getUsableSAMSites() + + -- rewrote this part of the code to keep loops to a minimum + + --will add SAM Sites acting as EW Rardars to the ewRadars array: + for i = 1, #samSites do + local samSite = samSites[i] + --We inform SAM sites that a target update is about to happen. If they have no targets in range after the cycle they go dark + samSite:targetCycleUpdateStart() + if samSite:getActAsEW() then + table.insert(ewRadars, samSite) + end + --if the sam site is not in ew mode and active we grab the detected targets right here + if samSite:isActive() and samSite:getActAsEW() == false then + local contacts = samSite:getDetectedTargets() + for j = 1, #contacts do + local contact = contacts[j] + self:mergeContact(contact) + end + end + end + + local samSitesToTrigger = {} + + for i = 1, #ewRadars do + local ewRadar = ewRadars[i] + --call go live in case ewRadar had to shut down (HARM attack) + ewRadar:goLive() + -- if an awacs has traveled more than a predeterminded distance we update the autonomous state of the sams + if getmetatable(ewRadar) == SkynetIADSAWACSRadar and ewRadar:isUpdateOfAutonomousStateOfSAMSitesRequired() then + self:updateAutonomousStatesOfSAMSites() + end + local ewContacts = ewRadar:getDetectedTargets() + if #ewContacts > 0 then + local samSitesUnderCoverage = ewRadar:getSAMSitesInCoveredArea() + for j = 1, #samSitesUnderCoverage do + local samSiteUnterCoverage = samSitesUnderCoverage[j] + -- only if a SAM site is not active we add it to the hash of SAM sites to be iterated later on + if samSiteUnterCoverage:isActive() == false then + --we add them to a hash to make sure each SAM site is in the collection only once, reducing the number of loops we conduct later on + samSitesToTrigger[samSiteUnterCoverage:getDCSName()] = samSiteUnterCoverage + end + end + for j = 1, #ewContacts do + local contact = ewContacts[j] + self:mergeContact(contact) + end + end + end + + self:cleanAgedTargets() + + for samName, samToTrigger in pairs(samSitesToTrigger) do + for j = 1, #self.contacts do + local contact = self.contacts[j] + -- the DCS Radar only returns enemy aircraft, if that should change a coalition check will be required + -- currently every type of object in the air is handed of to the SAM site, including missiles + local description = contact:getDesc() + local category = description.category + if category and category ~= Unit.Category.GROUND_UNIT and category ~= Unit.Category.SHIP and category ~= Unit.Category.STRUCTURE then + samToTrigger:informOfContact(contact) + end + end + end + + for i = 1, #samSites do + local samSite = samSites[i] + samSite:targetCycleUpdateEnd() + end + + self:printSystemStatus() +end + +function SkynetIADS:cleanAgedTargets() + local contactsToKeep = {} + for i = 1, #self.contacts do + local contact = self.contacts[i] + if contact:getAge() < self.maxTargetAge then + table.insert(contactsToKeep, contact) + end + end + self.contacts = contactsToKeep +end + +function SkynetIADS:buildSAMSitesInCoveredArea() + local samSites = self:getUsableSAMSites() + for i = 1, #samSites do + local samSite = samSites[i] + samSite:updateSAMSitesInCoveredArea() + end + + local ewRadars = self:getUsableEarlyWarningRadars() + for i = 1, #ewRadars do + local ewRadar = ewRadars[i] + ewRadar:updateSAMSitesInCoveredArea() + end +end + +function SkynetIADS:updateIADSCoverage() + self:buildSAMSitesInCoveredArea() + self:enforceRebuildAutonomousStateOfSAMSites() + --update moose connector with radar group names Skynet is able to use + self:getMooseConnector():update() +end + +function SkynetIADS:updateAutonomousStatesOfSAMSites(deadUnit) + --deat unit is to prevent multiple calls via the event handling of SkynetIADSAbstractElement when a units power source or connection node is destroyed + if deadUnit == nil or self.destroyedUnitResponsibleForUpdateAutonomousStateOfSAMSite ~= deadUnit then + self:updateIADSCoverage() + self.destroyedUnitResponsibleForUpdateAutonomousStateOfSAMSite = deadUnit + end +end + +function SkynetIADS:enforceRebuildAutonomousStateOfSAMSites() + local ewRadars = self:getUsableEarlyWarningRadars() + local samSites = self:getUsableSAMSites() + + for i = 1, #samSites do + local samSite = samSites[i] + if samSite:getActAsEW() then + table.insert(ewRadars, samSite) + end + end + + for i = 1, #samSites do + local samSite = samSites[i] + local inRange = false + for j = 1, #ewRadars do + if samSite:isInRadarDetectionRangeOf(ewRadars[j]) then + inRange = true + end + end + if inRange == false then + samSite:goAutonomous() + else + samSite:resetAutonomousState() + end + end +end + +function SkynetIADS:mergeContact(contact) + local existingContact = false + for i = 1, #self.contacts do + local iadsContact = self.contacts[i] + if iadsContact:getName() == contact:getName() then + iadsContact:refresh() + existingContact = true + end + end + if existingContact == false then + table.insert(self.contacts, contact) + end +end + +function SkynetIADS:getContacts() + return self.contacts +end + +function SkynetIADS:printOutput(output, typeWarning) + if typeWarning == true and self.debugOutput.warnings or typeWarning == nil then + if typeWarning == true then + output = "WARNING: "..output + end + trigger.action.outText(output, 4) + end +end + +function SkynetIADS:getDebugSettings() + return self.debugOutput +end + +-- will start going through the Early Warning Radars and SAM sites to check what targets they have detected +function SkynetIADS.activate(self) + mist.removeFunction(self.ewRadarScanMistTaskID) + mist.removeFunction(self.samSetupMistTaskID) + self.ewRadarScanMistTaskID = mist.scheduleFunction(SkynetIADS.evaluateContacts, {self}, 1, self.contactUpdateInterval) + self:updateIADSCoverage() +end + +function SkynetIADS:setupSAMSitesAndThenActivate(setupTime) + if setupTime then + self.samSetupTime = setupTime + end + local samSites = self:getSAMSites() + for i = 1, #samSites do + local sam = samSites[i] + sam:goLive() + --stop harm scan, because this function will shut down point defences + sam:stopScanningForHARMs() + --point defences will go dark after sam:goLive() call on the SAM they are protecting, so we load them and call a separate goLive call here, some SAMs will therefore receive 2 goLive calls + -- this should not have a negative impact on performance + local pointDefences = sam:getPointDefences() + for j = 1, #pointDefences do + pointDefence = pointDefences[j] + pointDefence:goLive() + end + end + self.samSetupMistTaskID = mist.scheduleFunction(SkynetIADS.postSetupSAMSites, {self}, timer.getTime() + self.samSetupTime) +end + +function SkynetIADS.postSetupSAMSites(self) + local samSites = self:getSAMSites() + for i = 1, #samSites do + local sam = samSites[i] + --turn on the scan again otherwise SAMs that fired a missile while in setup will not turn off anymore + sam:scanForHarms() + end + self:activate() +end + +function SkynetIADS:deactivate() + mist.removeFunction(self.ewRadarScanMistTaskID) + self:deativateSAMSites() + self:deactivateEarlyWarningRadars() + self:deactivateCommandCenters() +end + +function SkynetIADS:deactivateCommandCenters() + for i = 1, #self.commandCenters do + local comCenter = self.commandCenters[i] + comCenter:cleanUp() + end +end + +function SkynetIADS:deativateSAMSites() + for i = 1, #self.samSites do + local samSite = self.samSites[i] + samSite:cleanUp() + end +end + +function SkynetIADS:deactivateEarlyWarningRadars() + for i = 1, #self.earlyWarningRadars do + local ewRadar = self.earlyWarningRadars[i] + ewRadar:cleanUp() + end +end + +function SkynetIADS:addRadioMenu() + self.radioMenu = missionCommands.addSubMenu('SKYNET IADS '..self:getCoalitionString()) + local displayIADSStatus = missionCommands.addCommand('show IADS Status', self.radioMenu, SkynetIADS.updateDisplay, {self = self, value = true, option = 'IADSStatus'}) + local displayIADSStatus = missionCommands.addCommand('hide IADS Status', self.radioMenu, SkynetIADS.updateDisplay, {self = self, value = false, option = 'IADSStatus'}) + local displayIADSStatus = missionCommands.addCommand('show contacts', self.radioMenu, SkynetIADS.updateDisplay, {self = self, value = true, option = 'contacts'}) + local displayIADSStatus = missionCommands.addCommand('hide contacts', self.radioMenu, SkynetIADS.updateDisplay, {self = self, value = false, option = 'contacts'}) +end + +function SkynetIADS:removeRadioMenu() + missionCommands.removeItem(self.radioMenu) +end + +function SkynetIADS.updateDisplay(params) + local option = params.option + local self = params.self + local value = params.value + if option == 'IADSStatus' then + self:getDebugSettings()[option] = value + elseif option == 'contacts' then + self:getDebugSettings()[option] = value + end +end + +function SkynetIADS:getCoalitionString() + local coalitionStr = "RED" + if self.coalitionID == coalition.side.BLUE then + coalitionStr = "BLUE" + elseif self.coalitionID == coalition.side.NEUTRAL then + coalitionStr = "NEUTRAL" + end + + if self.name then + coalitionStr = coalitionStr.." "..self.name + end + + return coalitionStr +end + +function SkynetIADS:getMooseConnector() + if self.mooseConnector == nil then + self.mooseConnector = SkynetMooseA2ADispatcherConnector:create(self) + end + return self.mooseConnector +end + +function SkynetIADS:addMooseSetGroup(mooseSetGroup) + self:getMooseConnector():addMooseSetGroup(mooseSetGroup) +end + +function SkynetIADS:printDetailedEarlyWarningRadarStatus() + local ewRadars = self:getEarlyWarningRadars() + env.info("------------------------------------------ EW RADAR STATUS: "..self:getCoalitionString().." -------------------------------") + for i = 1, #ewRadars do + local ewRadar = ewRadars[i] + local numConnectionNodes = #ewRadar:getConnectionNodes() + local numPowerSources = #ewRadar:getPowerSources() + local isActive = ewRadar:isActive() + local connectionNodes = ewRadar:getConnectionNodes() + local firstRadar = nil + local radars = ewRadar:getRadars() + + --get the first existing radar to prevent issues in calculating the distance later on: + for i = 1, #radars do + if radars[i]:isExist() then + firstRadar = radars[i] + break + end + + end + local numDamagedConnectionNodes = 0 + + + for j = 1, #connectionNodes do + local connectionNode = connectionNodes[j] + if connectionNode:isExist() == false then + numDamagedConnectionNodes = numDamagedConnectionNodes + 1 + end + end + local intactConnectionNodes = numConnectionNodes - numDamagedConnectionNodes + + local powerSources = ewRadar:getPowerSources() + local numDamagedPowerSources = 0 + for j = 1, #powerSources do + local powerSource = powerSources[j] + if powerSource:isExist() == false then + numDamagedPowerSources = numDamagedPowerSources + 1 + end + end + local intactPowerSources = numPowerSources - numDamagedPowerSources + + local detectedTargets = ewRadar:getDetectedTargets() + local samSitesInCoveredArea = ewRadar:getSAMSitesInCoveredArea() + + local unitName = "DESTROYED" + + if ewRadar:getDCSRepresentation():isExist() then + unitName = ewRadar:getDCSName() + end + + env.info("UNIT: "..unitName.." | TYPE: "..ewRadar:getNatoName()) + env.info("ACTIVE: "..tostring(isActive).."| DETECTED TARGETS: "..#detectedTargets.." | DEFENDING HARM: "..tostring(ewRadar:isDefendingHARM())) + if numConnectionNodes > 0 then + env.info("CONNECTION NODES: "..numConnectionNodes.." | DAMAGED: "..numDamagedConnectionNodes.." | INTACT: "..intactConnectionNodes) + else + env.info("NO CONNECTION NODES SET") + end + if numPowerSources > 0 then + env.info("POWER SOURCES : "..numPowerSources.." | DAMAGED:"..numDamagedPowerSources.." | INTACT: "..intactPowerSources) + else + env.info("NO POWER SOURCES SET") + end + + env.info("SAM SITES IN COVERED AREA: "..#samSitesInCoveredArea) + for j = 1, #samSitesInCoveredArea do + local samSiteCovered = samSitesInCoveredArea[j] + env.info(samSiteCovered:getDCSName()) + end + + for j = 1, #detectedTargets do + local contact = detectedTargets[j] + if firstRadar ~= nil and firstRadar:isExist() then + local distance = mist.utils.round(mist.utils.metersToNM(ewRadar:getDistanceInMetersToContact(firstRadar:getDCSRepresentation(), contact:getPosition().p)), 2) + env.info("CONTACT: "..contact:getName().." | TYPE: "..contact:getTypeName().." | DISTANCE NM: "..distance) + end + end + + env.info("---------------------------------------------------") + + end + +end + +function SkynetIADS:printDetailedSAMSiteStatus() + local samSites = self:getSAMSites() + + env.info("------------------------------------------ SAM STATUS: "..self:getCoalitionString().." -------------------------------") + for i = 1, #samSites do + local samSite = samSites[i] + local numConnectionNodes = #samSite:getConnectionNodes() + local numPowerSources = #samSite:getPowerSources() + local isAutonomous = samSite:getAutonomousState() + local isActive = samSite:isActive() + + local connectionNodes = samSite:getConnectionNodes() + local firstRadar = samSite:getRadars()[1] + local numDamagedConnectionNodes = 0 + for j = 1, #connectionNodes do + local connectionNode = connectionNodes[j] + if connectionNode:isExist() == false then + numDamagedConnectionNodes = numDamagedConnectionNodes + 1 + end + end + local intactConnectionNodes = numConnectionNodes - numDamagedConnectionNodes + + local powerSources = samSite:getPowerSources() + local numDamagedPowerSources = 0 + for j = 1, #powerSources do + local powerSource = powerSources[j] + if powerSource:isExist() == false then + numDamagedPowerSources = numDamagedPowerSources + 1 + end + end + local intactPowerSources = numPowerSources - numDamagedPowerSources + + local detectedTargets = samSite:getDetectedTargets() + + local samSitesInCoveredArea = samSite:getSAMSitesInCoveredArea() + + env.info("GROUP: "..samSite:getDCSName().." | TYPE: "..samSite:getNatoName()) + env.info("ACTIVE: "..tostring(isActive).." | AUTONOMOUS: "..tostring(isAutonomous).." | IS ACTING AS EW: "..tostring(samSite:getActAsEW()).." | DETECTED TARGETS: "..#detectedTargets.." | DEFENDING HARM: "..tostring(samSite:isDefendingHARM()).." | MISSILES IN FLIGHT:"..tostring(samSite:getNumberOfMissilesInFlight())) + + if numConnectionNodes > 0 then + env.info("CONNECTION NODES: "..numConnectionNodes.." | DAMAGED: "..numDamagedConnectionNodes.." | INTACT: "..intactConnectionNodes) + else + env.info("NO CONNECTION NODES SET") + end + if numPowerSources > 0 then + env.info("POWER SOURCES : "..numPowerSources.." | DAMAGED:"..numDamagedPowerSources.." | INTACT: "..intactPowerSources) + else + env.info("NO POWER SOURCES SET") + end + + env.info("SAM SITES IN COVERED AREA: "..#samSitesInCoveredArea) + for j = 1, #samSitesInCoveredArea do + local samSiteCovered = samSitesInCoveredArea[j] + env.info(samSiteCovered:getDCSName()) + end + + for j = 1, #detectedTargets do + local contact = detectedTargets[j] + if firstRadar ~= nil and firstRadar:isExist() then + local distance = mist.utils.round(mist.utils.metersToNM(samSite:getDistanceInMetersToContact(firstRadar:getDCSRepresentation(), contact:getPosition().p)), 2) + env.info("CONTACT: "..contact:getName().." | TYPE: "..contact:getTypeName().." | DISTANCE NM: "..distance) + end + end + + env.info("---------------------------------------------------") + end +end + +function SkynetIADS:printSystemStatus() + + if self:getDebugSettings().IADSStatus or self:getDebugSettings().contacts then + local coalitionStr = self:getCoalitionString() + self:printOutput("---- IADS: "..coalitionStr.." ------") + end + + if self:getDebugSettings().IADSStatus then + + local numComCenters = #self.commandCenters + local numIntactComCenters = 0 + local numDestroyedComCenters = 0 + local numComCentersNoPower = 0 + local numComCentersServingIADS = 0 + for i = 1, #self.commandCenters do + local commandCenter = self.commandCenters[i] + if commandCenter:hasWorkingPowerSource() == false then + numComCentersNoPower = numComCentersNoPower + 1 + end + if commandCenter:isDestroyed() == false then + numIntactComCenters = numIntactComCenters + 1 + end + if commandCenter:isDestroyed() == false and commandCenter:hasWorkingPowerSource() then + numComCentersServingIADS = numComCentersServingIADS + 1 + end + end + + numDestroyedComCenters = numComCenters - numIntactComCenters + + + self:printOutput("COMMAND CENTERS: Serving IADS: "..numComCentersServingIADS.." | Total: "..numComCenters.." | Intact: "..numIntactComCenters.." | Destroyed: "..numDestroyedComCenters.." | NoPower: "..numComCentersNoPower) + + local ewNoPower = 0 + local ewTotal = #self:getEarlyWarningRadars() + local ewNoConnectionNode = 0 + local ewActive = 0 + local ewRadarsInactive = 0 + + for i = 1, #self.earlyWarningRadars do + local ewRadar = self.earlyWarningRadars[i] + if ewRadar:hasWorkingPowerSource() == false then + ewNoPower = ewNoPower + 1 + end + if ewRadar:hasActiveConnectionNode() == false then + ewNoConnectionNode = ewNoConnectionNode + 1 + end + if ewRadar:isActive() then + ewActive = ewActive + 1 + end + end + + ewRadarsInactive = ewTotal - ewActive + local numEWRadarsDestroyed = #self:getDestroyedEarlyWarningRadars() + self:printOutput("EW: "..ewTotal.." | Act: "..ewActive.." | Inact: "..ewRadarsInactive.." | Destroyed: "..numEWRadarsDestroyed.." | NoPowr: "..ewNoPower.." | NoCon: "..ewNoConnectionNode) + + local samSitesInactive = 0 + local samSitesActive = 0 + local samSitesTotal = #self:getSAMSites() + local samSitesNoPower = 0 + local samSitesNoConnectionNode = 0 + local samSitesOutOfAmmo = 0 + local samSiteAutonomous = 0 + local samSiteRadarDestroyed = 0 + for i = 1, #self.samSites do + local samSite = self.samSites[i] + if samSite:hasWorkingPowerSource() == false then + samSitesNoPower = samSitesNoPower + 1 + end + if samSite:hasActiveConnectionNode() == false then + samSitesNoConnectionNode = samSitesNoConnectionNode + 1 + end + if samSite:isActive() then + samSitesActive = samSitesActive + 1 + end + if samSite:hasRemainingAmmo() == false then + samSitesOutOfAmmo = samSitesOutOfAmmo + 1 + end + if samSite:getAutonomousState() == true then + samSiteAutonomous = samSiteAutonomous + 1 + end + if samSite:hasWorkingRadar() == false then + samSiteRadarDestroyed = samSiteRadarDestroyed + 1 + end + end + + samSitesInactive = samSitesTotal - samSitesActive + self:printOutput("SAM: "..samSitesTotal.." | Act: "..samSitesActive.." | Inact: "..samSitesInactive.." | Autonm: "..samSiteAutonomous.." | Raddest: "..samSiteRadarDestroyed.." | NoPowr: "..samSitesNoPower.." | NoCon: "..samSitesNoConnectionNode.." | NoAmmo: "..samSitesOutOfAmmo) + end + if self:getDebugSettings().contacts then + for i = 1, #self.contacts do + local contact = self.contacts[i] + self:printOutput("CONTACT: "..contact:getName().." | TYPE: "..contact:getTypeName().." | GS: "..tostring(contact:getGroundSpeedInKnots()).." | LAST SEEN: "..contact:getAge()) + end + end + + if self:getDebugSettings().earlyWarningRadarStatusEnvOutput then + self:printDetailedEarlyWarningRadarStatus() + end + + if self:getDebugSettings().samSiteStatusEnvOutput then + self:printDetailedSAMSiteStatus() + end +end + +end +do + +SkynetMooseA2ADispatcherConnector = {} + +function SkynetMooseA2ADispatcherConnector:create(iads) + local instance = {} + setmetatable(instance, self) + self.__index = self + instance.iadsCollection = {} + instance.mooseGroups = {} + instance.ewRadarGroupNames = {} + instance.samSiteGroupNames = {} + table.insert(instance.iadsCollection, iads) + return instance +end + +function SkynetMooseA2ADispatcherConnector:addIADS(iads) + table.insert(self.iadsCollection, iads) +end + +function SkynetMooseA2ADispatcherConnector:addMooseSetGroup(mooseSetGroup) + table.insert(self.mooseGroups, mooseSetGroup) + self:update() +end + +function SkynetMooseA2ADispatcherConnector:getEarlyWarningRadarGroupNames() + self.ewRadarGroupNames = {} + for i = 1, #self.iadsCollection do + local ewRadars = self.iadsCollection[i]:getUsableEarlyWarningRadars() + for j = 1, #ewRadars do + local ewRadar = ewRadars[j] + table.insert(self.ewRadarGroupNames, ewRadar:getDCSRepresentation():getGroup():getName()) + end + end + return self.ewRadarGroupNames +end + +function SkynetMooseA2ADispatcherConnector:getSAMSiteGroupNames() + self.samSiteGroupNames = {} + for i = 1, #self.iadsCollection do + local samSites = self.iadsCollection[i]:getUsableSAMSites() + for j = 1, #samSites do + local samSite = samSites[j] + table.insert(self.samSiteGroupNames, samSite:getDCSName()) + end + end + return self.samSiteGroupNames +end + +function SkynetMooseA2ADispatcherConnector:update() + + --mooseGroup elements are type of: + --https://flightcontrol-master.github.io/MOOSE_DOCS_DEVELOP/Documentation/Core.Set.html##(SET_GROUP) + + --remove previously set group names: + for i = 1, #self.mooseGroups do + local mooseGroup = self.mooseGroups[i] + mooseGroup:RemoveGroupsByName(self.ewRadarGroupNames) + mooseGroup:RemoveGroupsByName(self.samSiteGroupNames) + end + + --add group names of IADS radars that are currently usable by the IADS: + for i = 1, #self.mooseGroups do + local mooseGroup = self.mooseGroups[i] + mooseGroup:AddGroupsByName(self:getEarlyWarningRadarGroupNames()) + mooseGroup:AddGroupsByName(self:getSAMSiteGroupNames()) + end +end + +end +do + + +SkynetIADSTableDelegator = {} + +function SkynetIADSTableDelegator:create() + local instance = {} + local forwarder = {} + forwarder.__index = function(tbl, name) + tbl[name] = function(self, ...) + for i = 1, #self do + self[i][name](self[i], ...) + end + return self + end + return tbl[name] + end + setmetatable(instance, forwarder) + instance.__index = forwarder + return instance +end + +end +do + +SkynetIADSAbstractDCSObjectWrapper = {} + +function SkynetIADSAbstractDCSObjectWrapper:create(dcsObject) + local instance = {} + setmetatable(instance, self) + self.__index = self + instance.dcsObject = dcsObject + if dcsObject and dcsObject:isExist() and getmetatable(dcsObject) == Unit then + --we store inital life here, because getLife0() returs a value that is lower that getLife() when no damage has happened... + instance.initialLife = dcsObject:getLife() + end + return instance +end + +function SkynetIADSAbstractDCSObjectWrapper:getName() + return self.dcsObject:getName() +end + +function SkynetIADSAbstractDCSObjectWrapper:getTypeName() + return self.dcsObject:getTypeName() +end + +function SkynetIADSAbstractDCSObjectWrapper:getPosition() + return self.dcsObject:getPosition() +end + +function SkynetIADSAbstractDCSObjectWrapper:isExist() + if self.dcsObject then + return self.dcsObject:isExist() + else + return false + end +end + +function SkynetIADSAbstractDCSObjectWrapper:getLifePercentage() + if self.dcsObject and self.dcsObject:isExist() then + return self.dcsObject:getLife() / self.initialLife * 100 + else + return 0 + end + +end + +function SkynetIADSAbstractDCSObjectWrapper:getDCSRepresentation() + return self.dcsObject +end + +end +do + +SkynetIADSAbstractElement = {} + +function SkynetIADSAbstractElement:create(dcsRepresentation, iads) + local instance = {} + setmetatable(instance, self) + self.__index = self + instance.connectionNodes = {} + instance.powerSources = {} + instance.iads = iads + instance.natoName = "UNKNOWN" + instance.dcsName = "" + instance:setDCSRepresentation(dcsRepresentation) + world.addEventHandler(instance) + return instance +end + +function SkynetIADSAbstractElement:removeEventHandlers() + world.removeEventHandler(self) +end + +function SkynetIADSAbstractElement:cleanUp() + self:removeEventHandlers() +end + +function SkynetIADSAbstractElement:isDestroyed() + return self:getDCSRepresentation():isExist() == false +end + +function SkynetIADSAbstractElement:addPowerSource(powerSource) + table.insert(self.powerSources, powerSource) + return self +end + +function SkynetIADSAbstractElement:getPowerSources() + return self.powerSources +end + +function SkynetIADSAbstractElement:addConnectionNode(connectionNode) + table.insert(self.connectionNodes, connectionNode) + self.iads:updateAutonomousStatesOfSAMSites() + return self +end + +function SkynetIADSAbstractElement:getConnectionNodes() + return self.connectionNodes +end + +function SkynetIADSAbstractElement:hasActiveConnectionNode() + local connectionNode = self:genericCheckOneObjectIsAlive(self.connectionNodes) + if connectionNode == false and self.iads:getDebugSettings().samNoConnection then + self.iads:printOutput(self:getDescription().." no connection to Command Center") + end + return connectionNode +end + +function SkynetIADSAbstractElement:hasWorkingPowerSource() + local power = self:genericCheckOneObjectIsAlive(self.powerSources) + if power == false and self.iads:getDebugSettings().hasNoPower then + self.iads:printOutput(self:getDescription().." has no power") + end + return power +end + +function SkynetIADSAbstractElement:getDCSName() + return self.dcsName +end + +-- generic function to theck if power plants, command centers, connection nodes are still alive +function SkynetIADSAbstractElement:genericCheckOneObjectIsAlive(objects) + local isAlive = (#objects == 0) + for i = 1, #objects do + local object = objects[i] + --if we find one object that is not fully destroyed we assume the IADS is still working + if object:isExist() then + isAlive = true + break + end + end + return isAlive +end + +function SkynetIADSAbstractElement:setDCSRepresentation(representation) + self.dcsRepresentation = representation + if self.dcsRepresentation then + self.dcsName = self:getDCSRepresentation():getName() + end +end + +function SkynetIADSAbstractElement:getDCSRepresentation() + return self.dcsRepresentation +end + +function SkynetIADSAbstractElement:getNatoName() + return self.natoName +end + +function SkynetIADSAbstractElement:getDescription() + return "IADS ELEMENT: "..self:getDCSName().." | Type : "..tostring(self:getNatoName()) +end + +function SkynetIADSAbstractElement:onEvent(event) + --if a unit is destroyed we check to see if its a power plant powering the unit or a connection node + if event.id == world.event.S_EVENT_DEAD then + if self:hasWorkingPowerSource() == false or self:isDestroyed() then + self:goDark() + self.iads:updateAutonomousStatesOfSAMSites(event.initiator) + end + if self:hasActiveConnectionNode() == false then + self:goAutonomous() + self.iads:updateAutonomousStatesOfSAMSites(event.initiator) + end + end + if event.id == world.event.S_EVENT_SHOT then + self:weaponFired(event) + end +end + +--placeholder method, can be implemented by subclasses +function SkynetIADSAbstractElement:weaponFired(event) + +end + +--placeholder method, can be implemented by subclasses +function SkynetIADSAbstractElement:goDark() + +end + +--placeholder method, can be implemented by subclasses +function SkynetIADSAbstractElement:goAutonomous() + +end + +-- helper code for class inheritance +function inheritsFrom( baseClass ) + + local new_class = {} + local class_mt = { __index = new_class } + + function new_class:create() + local newinst = {} + setmetatable( newinst, class_mt ) + return newinst + end + + if nil ~= baseClass then + setmetatable( new_class, { __index = baseClass } ) + end + + -- Implementation of additional OO properties starts here -- + + -- Return the class object of the instance + function new_class:class() + return new_class + end + + -- Return the super class object of the instance + function new_class:superClass() + return baseClass + end + + -- Return true if the caller is an instance of theClass + function new_class:isa( theClass ) + local b_isa = false + + local cur_class = new_class + + while ( nil ~= cur_class ) and ( false == b_isa ) do + if cur_class == theClass then + b_isa = true + else + cur_class = cur_class:superClass() + end + end + + return b_isa + end + + return new_class +end + +end +do + +SkynetIADSAbstractRadarElement = {} +SkynetIADSAbstractRadarElement = inheritsFrom(SkynetIADSAbstractElement) + +SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DCS_AI = 1 +SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK = 2 + +SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE = 1 +SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE = 2 + +function SkynetIADSAbstractRadarElement:create(dcsElementWithRadar, iads) + local instance = self:superClass():create(dcsElementWithRadar, iads) + setmetatable(instance, self) + self.__index = self + instance.aiState = false + instance.harmScanID = nil + instance.harmSilenceID = nil + instance.lastJammerUpdate = 0 + instance.objectsIdentifiedAsHarms = {} + instance.objectsIdentifiedAsHarmsMaxTargetAge = 60 + instance.launchers = {} + instance.trackingRadars = {} + instance.searchRadars = {} + instance.samSitesInCoveredArea = {} + instance.missilesInFlight = {} + instance.pointDefences = {} + instance.ingnoreHARMSWhilePointDefencesHaveAmmo = false + instance.autonomousBehaviour = SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DCS_AI + instance.goLiveRange = SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE + instance.isAutonomous = false + instance.harmDetectionChance = 0 + instance.minHarmShutdownTime = 0 + instance.maxHarmShutDownTime = 0 + instance.minHarmPresetShutdownTime = 30 + instance.maxHarmPresetShutdownTime = 180 + instance.firingRangePercent = 100 + instance.actAsEW = false + instance.cachedTargets = {} + instance.cachedTargetsMaxAge = 1 + instance.cachedTargetsCurrentAge = 0 + instance.goLiveTime = 0 + -- 5 seconds seems to be a good value for the sam site to find the target with its organic radar + instance.noCacheActiveForSecondsAfterGoLive = 5 + return instance +end + +--TODO: this method could be updated to only return Radar weapons fired, this way a SAM firing an IR weapon could go dark faster in the goDark() method +function SkynetIADSAbstractRadarElement:weaponFired(event) + if event.id == world.event.S_EVENT_SHOT then + local weapon = event.weapon + local launcherFired = event.initiator + for i = 1, #self.launchers do + local launcher = self.launchers[i] + if launcher:getDCSRepresentation() == launcherFired then + table.insert(self.missilesInFlight, weapon) + end + end + end +end + +function SkynetIADSAbstractRadarElement:setCachedTargetsMaxAge(maxAge) + self.cachedTargetsMaxAge = maxAge +end + +function SkynetIADSAbstractRadarElement:cleanUp() + for i = 1, #self.pointDefences do + local pointDefence = self.pointDefences[i] + pointDefence:cleanUp() + end + mist.removeFunction(self.harmScanID) + mist.removeFunction(self.harmSilenceID) + --call method from super class + self:removeEventHandlers() +end + +function SkynetIADSAbstractRadarElement:addPointDefence(pointDefence) + table.insert(self.pointDefences, pointDefence) + return self +end + +function SkynetIADSAbstractRadarElement:getPointDefences() + return self.pointDefences +end + + +function SkynetIADSAbstractRadarElement:updateSAMSitesInCoveredArea() + local samSites = self.iads:getUsableSAMSites() + self.samSitesInCoveredArea = {} + for i = 1, #samSites do + local samSite = samSites[i] + if samSite:isInRadarDetectionRangeOf(self) and samSite ~= self then + table.insert(self.samSitesInCoveredArea, samSite) + end + end + return self.samSitesInCoveredArea +end + +function SkynetIADSAbstractRadarElement:getSAMSitesInCoveredArea() + return self.samSitesInCoveredArea +end + +function SkynetIADSAbstractRadarElement:pointDefencesHaveRemainingAmmo(minNumberOfMissiles) + local remainingMissiles = 0 + for i = 1, #self.pointDefences do + local pointDefence = self.pointDefences[i] + remainingMissiles = remainingMissiles + pointDefence:getRemainingNumberOfMissiles() + end + local returnValue = false + if ( remainingMissiles > 0 and remainingMissiles >= minNumberOfMissiles ) then + returnValue = true + end + return returnValue +end + +function SkynetIADSAbstractElement:pointDefencesHaveEnoughLaunchers(minNumberOfLaunchers) + local numOfLaunchers = 0 + for i = 1, #self.pointDefences do + local pointDefence = self.pointDefences[i] + numOfLaunchers = numOfLaunchers + #pointDefence:getLaunchers() + end + local returnValue = false + if ( numOfLaunchers > 0 and numOfLaunchers >= minNumberOfLaunchers ) then + returnValue = true + end + return returnValue +end + +function SkynetIADSAbstractElement:setIgnoreHARMSWhilePointDefencesHaveAmmo(state) + if state == true or state == false then + self.ingnoreHARMSWhilePointDefencesHaveAmmo = state + end + return self +end + +function SkynetIADSAbstractRadarElement:hasMissilesInFlight() + return #self.missilesInFlight > 0 +end + +function SkynetIADSAbstractRadarElement:getNumberOfMissilesInFlight() + return #self.missilesInFlight +end + +-- DCS does not send an event, when a missile is destroyed, so this method needs to be polled so that the missiles in flight are current, polling is done in the HARM Search call: evaluateIfTargetsContainHARMs +function SkynetIADSAbstractRadarElement:updateMissilesInFlight() + local missilesInFlight = {} + for i = 1, #self.missilesInFlight do + local missile = self.missilesInFlight[i] + if missile:isExist() then + table.insert(missilesInFlight, missile) + end + end + self.missilesInFlight = missilesInFlight + self:goDarkIfOutOfAmmo() +end + +function SkynetIADSAbstractRadarElement:goDarkIfOutOfAmmo() + if self:hasRemainingAmmo() == false and self:getActAsEW() == false then + self:goDark() + end +end + +function SkynetIADSAbstractRadarElement:getActAsEW() + return self.actAsEW +end + +function SkynetIADSAbstractRadarElement:setActAsEW(ewState) + if ewState == true or ewState == false then + self.actAsEW = ewState + end + if self.actAsEW == true then + self:goLive() + else + self:goDark() + end + return self +end + +function SkynetIADSAbstractRadarElement:getUnitsToAnalyse() + local units = {} + table.insert(units, self:getDCSRepresentation()) + if getmetatable(self:getDCSRepresentation()) == Group then + units = self:getDCSRepresentation():getUnits() + end + return units +end + +function SkynetIADSAbstractRadarElement:getRemainingNumberOfMissiles() + local remainingNumberOfMissiles = 0 + for i = 1, #self.launchers do + local launcher = self.launchers[i] + remainingNumberOfMissiles = remainingNumberOfMissiles + launcher:getRemainingNumberOfMissiles() + end + return remainingNumberOfMissiles +end + +function SkynetIADSAbstractRadarElement:getInitialNumberOfMissiles() + local initalNumberOfMissiles = 0 + for i = 1, #self.launchers do + local launcher = self.launchers[i] + initalNumberOfMissiles = launcher:getInitialNumberOfMissiles() + initalNumberOfMissiles + end + return initalNumberOfMissiles +end + +function SkynetIADSAbstractRadarElement:getRemainingNumberOfShells() + local remainingNumberOfShells = 0 + for i = 1, #self.launchers do + local launcher = self.launchers[i] + remainingNumberOfShells = remainingNumberOfShells + launcher:getRemainingNumberOfShells() + end + return remainingNumberOfShells +end + +function SkynetIADSAbstractRadarElement:getInitialNumberOfShells() + local initialNumberOfShells = 0 + for i = 1, #self.launchers do + local launcher = self.launchers[i] + initialNumberOfShells = initialNumberOfShells + launcher:getInitialNumberOfShells() + end + return initialNumberOfShells +end + +function SkynetIADSAbstractRadarElement:hasRemainingAmmo() + --the launcher check is due to ew radars they have no launcher and no ammo and therefore are never out of ammo + return ( #self.launchers == 0 ) or ((self:getRemainingNumberOfMissiles() > 0 ) or ( self:getRemainingNumberOfShells() > 0 ) ) +end + +function SkynetIADSAbstractRadarElement:getHARMDetectionChance() + return self.harmDetectionChance +end + +function SkynetIADSAbstractRadarElement:setHARMDetectionChance(chance) + self.harmDetectionChance = chance + return self +end + +function SkynetIADSAbstractRadarElement:setupElements() + local numUnits = #self:getUnitsToAnalyse() + for typeName, dataType in pairs(SkynetIADS.database) do + local hasSearchRadar = false + local hasTrackingRadar = false + local hasLauncher = false + self.searchRadars = {} + self.trackingRadars = {} + self.launchers = {} + for entry, unitData in pairs(dataType) do + if entry == 'searchRadar' then + self:analyseAndAddUnit(SkynetIADSSAMSearchRadar, self.searchRadars, unitData) + hasSearchRadar = true + end + if entry == 'launchers' then + self:analyseAndAddUnit(SkynetIADSSAMLauncher, self.launchers, unitData) + hasLauncher = true + end + if entry == 'trackingRadar' then + self:analyseAndAddUnit(SkynetIADSSAMTrackingRadar, self.trackingRadars, unitData) + hasTrackingRadar = true + end + end + + local numElementsCreated = #self.searchRadars + #self.trackingRadars + #self.launchers + --this check ensures a unit or group has all required elements for the specific sam or ew type: + if (hasLauncher and hasSearchRadar and hasTrackingRadar and #self.launchers > 0 and #self.searchRadars > 0 and #self.trackingRadars > 0 ) + or (hasSearchRadar and hasLauncher and #self.searchRadars > 0 and #self.launchers > 0) + or (hasSearchRadar and hasLauncher == false and hasTrackingRadar == false and #self.searchRadars > 0 and numUnits == 1) then + local harmDetection = dataType['harm_detection_chance'] + if harmDetection then + self.harmDetectionChance = harmDetection + end + local natoName = dataType['name']['NATO'] + --we shorten the SA-XX names and don't return their code names eg goa, gainful.. + local pos = natoName:find(" ") + local prefix = natoName:sub(1, 2) + if string.lower(prefix) == 'sa' and pos ~= nil then + self.natoName = natoName:sub(1, (pos-1)) + else + self.natoName = natoName + end + break + end + end +end + +function SkynetIADSAbstractRadarElement:analyseAndAddUnit(class, tableToAdd, unitData) + local units = self:getUnitsToAnalyse() + for i = 1, #units do + local unit = units[i] + local unitTypeName = unit:getTypeName() + for unitName, unitPerformanceData in pairs(unitData) do + if unitName == unitTypeName then + samElement = class:create(unit) + samElement:setupRangeData() + table.insert(tableToAdd, samElement) + end + end + end +end + +function SkynetIADSAbstractRadarElement:getController() + local dcsRepresentation = self:getDCSRepresentation() + if dcsRepresentation:isExist() then + return dcsRepresentation:getController() + else + return nil + end +end + +function SkynetIADSAbstractRadarElement:getLaunchers() + return self.launchers +end + +function SkynetIADSAbstractRadarElement:getSearchRadars() + return self.searchRadars +end + +function SkynetIADSAbstractRadarElement:getTrackingRadars() + return self.trackingRadars +end + +function SkynetIADSAbstractRadarElement:getRadars() + local radarUnits = {} + for i = 1, #self.searchRadars do + table.insert(radarUnits, self.searchRadars[i]) + end + for i = 1, #self.trackingRadars do + table.insert(radarUnits, self.trackingRadars[i]) + end + return radarUnits +end + +function SkynetIADSAbstractRadarElement:setGoLiveRangeInPercent(percent) + if percent ~= nil then + self.firingRangePercent = percent + for i = 1, #self.launchers do + local launcher = self.launchers[i] + launcher:setFiringRangePercent(self.firingRangePercent) + end + for i = 1, #self.searchRadars do + local radar = self.searchRadars[i] + radar:setFiringRangePercent(self.firingRangePercent) + end + end + return self +end + +function SkynetIADSAbstractRadarElement:getGoLiveRangeInPercent() + return self.firingRangePercent +end + +function SkynetIADSAbstractRadarElement:setEngagementZone(engagementZone) + if engagementZone == SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE then + self.goLiveRange = engagementZone + elseif engagementZone == SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE then + self.goLiveRange = engagementZone + end + return self +end + +function SkynetIADSAbstractRadarElement:getEngagementZone() + return self.goLiveRange +end + +function SkynetIADSAbstractRadarElement:goLive() + if ( self.aiState == false and self:hasWorkingPowerSource() and self.harmSilenceID == nil) + and ( (self.isAutonomous == false) or (self.isAutonomous == true and self.autonomousBehaviour == SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DCS_AI ) ) + and (self:hasRemainingAmmo() == true ) + then + if self:isDestroyed() == false then + local cont = self:getController() + cont:setOnOff(true) + cont:setOption(AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.RED) + cont:setOption(AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_FREE) + self.goLiveTime = timer.getTime() + end + self.aiState = true + self:pointDefencesStopActingAsEW() + if self.iads:getDebugSettings().radarWentLive then + self.iads:printOutput(self:getDescription().." going live") + end + self:scanForHarms() + end +end + +function SkynetIADSAbstractRadarElement:pointDefencesStopActingAsEW() + for i = 1, #self.pointDefences do + local pointDefence = self.pointDefences[i] + pointDefence:setActAsEW(false) + end +end + + +function SkynetIADSAbstractRadarElement:noDamageToRadars() + local radars = self:getRadars() + for i = 1, #radars do + local radar = radars[i] + if radar:getLifePercentage() < 100 then + return false + end + end + return true +end + +function SkynetIADSAbstractRadarElement:goDark() + if ( self.aiState == true ) + and (self.harmSilenceID ~= nil or ( self.harmSilenceID == nil and #self:getDetectedTargets() == 0 and self:hasMissilesInFlight() == false) or ( self.harmSilenceID == nil and #self:getDetectedTargets() > 0 and self:hasMissilesInFlight() == false and self:hasRemainingAmmo() == false ) ) + and ( self.isAutonomous == false or ( self.isAutonomous == true and self.autonomousBehaviour == SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK ) ) + then + if self:isDestroyed() == false then + local controller = self:getController() + -- if the SAM site still has ammo we turn off the controller, this prevents rearming, however this way the SAM site is frozen in a red state, on the next actication it will be up and running much faster, therefore it will instantaneously engage targets + -- also this is a better way to get the HARM to miss the target, if not set to false the HARM often sticks to the target + if self:hasRemainingAmmo() then + controller:setOnOff(false) + --if the SAM is out of ammo we set the state to green, and ROE to weapon hold, this way it will shut down its radar and it can be rearmed + else + controller:setOption(AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.GREEN) + controller:setOption(AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD) + end + end + -- point defence will only go live if the Radar Emitting site it is protecting goes dark and this is due to a it defending against a HARM + if (self.harmSilenceID ~= nil) then + self:pointDefencesGoLive() + end + self.aiState = false + self:stopScanningForHARMs() + if self.iads:getDebugSettings().samWentDark then + self.iads:printOutput(self:getDescription().." going dark") + end + end +end + +function SkynetIADSAbstractRadarElement:pointDefencesGoLive() + for i = 1, #self.pointDefences do + local pointDefence = self.pointDefences[i] + pointDefence:setActAsEW(true) + end +end + +function SkynetIADSAbstractRadarElement:isActive() + return self.aiState +end + +function SkynetIADSAbstractRadarElement:isTargetInRange(target) + + local isSearchRadarInRange = false + local isTrackingRadarInRange = false + local isLauncherInRange = false + + local isSearchRadarInRange = ( #self.searchRadars == 0 ) + for i = 1, #self.searchRadars do + local searchRadar = self.searchRadars[i] + if searchRadar:isInRange(target) then + isSearchRadarInRange = true + break + end + end + + if self.goLiveRange == SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE then + + isLauncherInRange = ( #self.launchers == 0 ) + for i = 1, #self.launchers do + local launcher = self.launchers[i] + if launcher:isInRange(target) then + isLauncherInRange = true + break + end + end + + isTrackingRadarInRange = ( #self.trackingRadars == 0 ) + for i = 1, #self.trackingRadars do + local trackingRadar = self.trackingRadars[i] + if trackingRadar:isInRange(target) then + isTrackingRadarInRange = true + break + end + end + else + isLauncherInRange = true + isTrackingRadarInRange = true + end + return (isSearchRadarInRange and isTrackingRadarInRange and isLauncherInRange ) +end + +function SkynetIADSAbstractRadarElement:isInRadarDetectionRangeOf(abstractRadarElement) + local radars = self:getRadars() + local abstractRadarElementRadars = abstractRadarElement:getRadars() + for i = 1, #radars do + local radar = radars[i] + for j = 1, #abstractRadarElementRadars do + local abstractRadarElementRadar = abstractRadarElementRadars[j] + if abstractRadarElementRadar:isExist() and radar:isExist() then + local distance = self:getDistanceToUnit(radar:getDCSRepresentation():getPosition().p, abstractRadarElementRadar:getDCSRepresentation():getPosition().p) + if abstractRadarElementRadar:getMaxRangeFindingTarget() >= distance then + return true + end + end + end + end + return false +end + +function SkynetIADSAbstractRadarElement:getDistanceToUnit(unitPosA, unitPosB) + return mist.utils.round(mist.utils.get2DDist(unitPosA, unitPosB, 0)) +end + +function SkynetIADSAbstractRadarElement:setAutonomousBehaviour(mode) + if mode ~= nil then + self.autonomousBehaviour = mode + end + return self +end + +function SkynetIADSAbstractRadarElement:getAutonomousBehaviour() + return self.autonomousBehaviour +end + +function SkynetIADSAbstractRadarElement:resetAutonomousState() + if self.isAutonomous == true then + self.isAutonomous = false + self:goDark() + end +end + +function SkynetIADSAbstractRadarElement:goAutonomous() + if self.isAutonomous == false then + self.isAutonomous = true + self:goDark() + self:goLive() + end +end + +function SkynetIADSAbstractRadarElement:getAutonomousState() + return self.isAutonomous +end + +function SkynetIADSAbstractRadarElement:hasWorkingRadar() + local radars = self:getRadars() + for i = 1, #radars do + local radar = radars[i] + if radar:isRadarWorking() == true then + return true + end + end + return false +end + +function SkynetIADSAbstractRadarElement:jam(successProbability) + if self:isDestroyed() == false then + local controller = self:getController() + local probability = math.random(1, 100) + if self.iads:getDebugSettings().jammerProbability then + self.iads:printOutput("JAMMER: "..self:getDescription()..": Probability: "..successProbability) + end + if successProbability > probability then + controller:setOption(AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD) + if self.iads:getDebugSettings().jammerProbability then + self.iads:printOutput("JAMMER: "..self:getDescription()..": jammed, setting to weapon hold") + end + else + controller:setOption(AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_FREE) + if self.iads:getDebugSettings().jammerProbability then + self.iads:printOutput("Jammer: "..self:getDescription()..": jammed, setting to weapon free") + end + end + self.lastJammerUpdate = timer:getTime() + end +end + +function SkynetIADSAbstractRadarElement:scanForHarms() + self:stopScanningForHARMs() + self.harmScanID = mist.scheduleFunction(SkynetIADSAbstractRadarElement.evaluateIfTargetsContainHARMs, {self}, 1, 2) +end + +function SkynetIADSAbstractElement:isScanningForHARMs() + return self.harmScanID ~= nil +end + +function SkynetIADSAbstractElement:isDefendingHARM() + return self.harmSilenceID ~= nil +end + +function SkynetIADSAbstractRadarElement:stopScanningForHARMs() + mist.removeFunction(self.harmScanID) + self.harmScanID = nil +end + +function SkynetIADSAbstractRadarElement:goSilentToEvadeHARM(timeToImpact) + self:finishHarmDefence(self) + self.objectsIdentifiedAsHarms = {} + local harmTime = self:getHarmShutDownTime() + if self.iads:getDebugSettings().harmDefence then + self.iads:printOutput("HARM DEFENCE: "..self:getDCSName().." shutting down | FOR: "..harmTime.." seconds | TTI: "..timeToImpact) + end + self.harmSilenceID = mist.scheduleFunction(SkynetIADSAbstractRadarElement.finishHarmDefence, {self}, timer.getTime() + harmTime, 1) + self:goDark() +end + +function SkynetIADSAbstractRadarElement:getHarmShutDownTime() + local shutDownTime = math.random(self.minHarmShutdownTime, self.maxHarmShutDownTime) + return shutDownTime +end + +function SkynetIADSAbstractRadarElement.finishHarmDefence(self) + mist.removeFunction(self.harmSilenceID) + self.harmSilenceID = nil +end + +function SkynetIADSAbstractRadarElement:getDetectedTargets() + if ( timer.getTime() - self.cachedTargetsCurrentAge > self.cachedTargetsMaxAge ) or ( timer.getTime() - self.goLiveTime < self.noCacheActiveForSecondsAfterGoLive ) then + self.cachedTargets = {} + self.cachedTargetsCurrentAge = timer.getTime() + if self:hasWorkingPowerSource() and self:isDestroyed() == false then + local targets = self:getController():getDetectedTargets(Controller.Detection.RADAR) + for i = 1, #targets do + local target = targets[i] + -- there are cases when a destroyed object is still visible as a target to the radar, don't add it, will cause errors everywhere the dcs object is accessed + if target.object then + local iadsTarget = SkynetIADSContact:create(target) + iadsTarget:refresh() + if self:isTargetInRange(iadsTarget) then + table.insert(self.cachedTargets, iadsTarget) + end + end + end + end + end + return self.cachedTargets +end + +function SkynetIADSAbstractRadarElement:getSecondsToImpact(distanceNM, speedKT) + local tti = 0 + if speedKT > 0 then + tti = mist.utils.round((distanceNM / speedKT) * 3600, 0) + if tti < 0 then + tti = 0 + end + end + return tti +end + +function SkynetIADSAbstractRadarElement:getDistanceInMetersToContact(radarUnit, point) + return mist.utils.round(mist.utils.get3DDist(radarUnit:getPosition().p, point), 0) +end + +function SkynetIADSAbstractRadarElement:calculateMinimalShutdownTimeInSeconds(timeToImpact) + return timeToImpact + self.minHarmPresetShutdownTime +end + +function SkynetIADSAbstractRadarElement:calculateMaximalShutdownTimeInSeconds(minShutdownTime) + return minShutdownTime + mist.random(1, self.maxHarmPresetShutdownTime) +end + +function SkynetIADSAbstractRadarElement:calculateImpactPoint(target, distanceInMeters) + -- distance needs to be incremented by a certain value for ip calculation to work, check why presumably due to rounding errors in the previous distance calculation + return land.getIP(target:getPosition().p, target:getPosition().x, distanceInMeters + 50) +end + +function SkynetIADSAbstractRadarElement:shallReactToHARM() + return self.harmDetectionChance >= math.random(1, 100) +end + +-- will only check for missiles, if DCS ads AAA than can engage HARMs then this code must be updated: +function SkynetIADSAbstractRadarElement:shallIgnoreHARMShutdown() + local numOfHarms = self:getNumberOfObjectsItentifiedAsHARMS() + return ( self:pointDefencesHaveRemainingAmmo(numOfHarms) and self:pointDefencesHaveEnoughLaunchers(numOfHarms) and self.ingnoreHARMSWhilePointDefencesHaveAmmo == true) +end + + +function SkynetIADSAbstractRadarElement:getNumberOfObjectsItentifiedAsHARMS() + local numFound = 0 + for unitName, unit in pairs(self.objectsIdentifiedAsHarms) do + numFound = numFound + 1 + end + return numFound +end + +function SkynetIADSAbstractRadarElement:cleanUpOldObjectsIdentifiedAsHARMS() + local validObjects = {} + for unitName, unit in pairs(self.objectsIdentifiedAsHarms) do + local harm = unit['target'] + if harm:getAge() <= self.objectsIdentifiedAsHarmsMaxTargetAge then + validObjects[harm:getName()] = {} + validObjects[harm:getName()]['target'] = harm + validObjects[harm:getName()]['count'] = unit['count'] + end + end + self.objectsIdentifiedAsHarms = validObjects + + --stop point defences acting as ew (always on), will occur of activated via shallIgnoreHARMShutdown() in evaluateIfTargetsContainHARMs + if self:getNumberOfObjectsItentifiedAsHARMS() == 0 then + self:pointDefencesStopActingAsEW() + end +end + + +function SkynetIADSAbstractRadarElement.evaluateIfTargetsContainHARMs(self) + + --if an emitter dies the SAM site being jammed will revert back to normal operation: + if self.lastJammerUpdate > 0 and ( timer:getTime() - self.lastJammerUpdate ) > 10 then + self:jam(0) + self.lastJammerUpdate = 0 + end + + --we use the regular interval of this method to update to other states: + self:updateMissilesInFlight() + self:cleanUpOldObjectsIdentifiedAsHARMS() + + + local targets = self:getDetectedTargets() + for i = 1, #targets do + local target = targets[i] + local radars = self:getRadars() + for j = 1, #radars do + local radar = radars[j] + if radar:isExist() == true then + local distance = self:getDistanceInMetersToContact(radar, target:getPosition().p) + local impactPoint = self:calculateImpactPoint(target, distance) + if impactPoint then + local harmImpactPointDistanceToSAM = self:getDistanceInMetersToContact(radar, impactPoint) + if harmImpactPointDistanceToSAM <= 100 then + if self.objectsIdentifiedAsHarms[target:getName()] then + self.objectsIdentifiedAsHarms[target:getName()]['count'] = self.objectsIdentifiedAsHarms[target:getName()]['count'] + 1 + else + self.objectsIdentifiedAsHarms[target:getName()] = {} + self.objectsIdentifiedAsHarms[target:getName()]['target'] = target + self.objectsIdentifiedAsHarms[target:getName()]['count'] = 1 + end + local savedTarget = self.objectsIdentifiedAsHarms[target:getName()]['target'] + savedTarget:refresh() + local numDetections = self.objectsIdentifiedAsHarms[target:getName()]['count'] + local speed = savedTarget:getGroundSpeedInKnots() + local timeToImpact = self:getSecondsToImpact(mist.utils.metersToNM(distance), speed) + local shallReactToHarm = self:shallReactToHARM() + + -- if self:getNumberOfObjectsItentifiedAsHARMS() > 0 then + -- env.info("detect as HARM: "..self:getDCSName().." "..self:getNumberOfObjectsItentifiedAsHARMS()) + -- end + + -- we use 2 detection cycles so a random object in the air pointing at the SAM site for a spilt second will not trigger a shutdown. shallReactToHarm adds some salt otherwise the SAM will always shut down 100% of the time. + if numDetections == 2 and shallReactToHarm then + if self:shallIgnoreHARMShutdown() == false then + self.minHarmShutdownTime = self:calculateMinimalShutdownTimeInSeconds(timeToImpact) + self.maxHarmShutDownTime = self:calculateMaximalShutdownTimeInSeconds(self.minHarmShutdownTime) + self:goSilentToEvadeHARM(timeToImpact) + else + self:pointDefencesGoLive() + end + end + if numDetections == 2 and shallReactToHarm == false then + if self.iads:getDebugSettings().harmDefence then + self.iads:printOutput("HARM DEFENCE: "..self:getDCSName().." will not react") + end + end + end + end + end + end + end +end + +end +do +--this class is currently used for AWACS and Ships, at a latter date a separate class for ships could be created, currently not needed +SkynetIADSAWACSRadar = {} +SkynetIADSAWACSRadar = inheritsFrom(SkynetIADSAbstractRadarElement) + +function SkynetIADSAWACSRadar:create(radarUnit, iads) + local instance = self:superClass():create(radarUnit, iads) + setmetatable(instance, self) + self.__index = self + instance.lastUpdatePosition = nil + return instance +end + +function SkynetIADSAWACSRadar:setupElements() + local unit = self:getDCSRepresentation() + local radar = SkynetIADSSAMSearchRadar:create(unit) + radar:setupRangeData() + table.insert(self.searchRadars, radar) +end + +function SkynetIADSAWACSRadar:getNatoName() + return self:getDCSRepresentation():getTypeName() +end + +-- AWACs will not scan for HARMS +function SkynetIADSAWACSRadar:scanForHarms() + +end + +function SkynetIADSAWACSRadar:getMaxAllowedMovementForAutonomousUpdateInNM() + local radarRange = mist.utils.metersToNM(self.searchRadars[1]:getMaxRangeFindingTarget()) + return mist.utils.round(radarRange / 10) +end + +function SkynetIADSAWACSRadar:isUpdateOfAutonomousStateOfSAMSitesRequired() + return self:getDistanceTraveledSinceLastUpdate() > self:getMaxAllowedMovementForAutonomousUpdateInNM() +end + +function SkynetIADSAWACSRadar:getDistanceTraveledSinceLastUpdate() + local currentPosition = nil + if self.lastUpdatePosition == nil and self:getDCSRepresentation():isExist() then + self.lastUpdatePosition = self:getDCSRepresentation():getPosition().p + end + if self:getDCSRepresentation():isExist() then + currentPosition = self:getDCSRepresentation():getPosition().p + end + return mist.utils.round(mist.utils.metersToNM(self:getDistanceToUnit(self.lastUpdatePosition, currentPosition))) +end + +end + +do +SkynetIADSCommandCenter = {} +SkynetIADSCommandCenter = inheritsFrom(SkynetIADSAbstractElement) + +function SkynetIADSCommandCenter:create(commandCenter, iads) + local instance = self:superClass():create(commandCenter, iads) + setmetatable(instance, self) + self.__index = self + instance.natoName = "Command Center" + return instance +end + +end +do + +SkynetIADSContact = {} +SkynetIADSContact = inheritsFrom(SkynetIADSAbstractDCSObjectWrapper) + +function SkynetIADSContact:create(dcsRadarTarget) + local instance = self:superClass():create(dcsRadarTarget.object) + setmetatable(instance, self) + self.__index = self + instance.firstContactTime = timer.getAbsTime() + instance.lastTimeSeen = 0 + instance.dcsRadarTarget = dcsRadarTarget + instance.name = instance.dcsObject:getName() + instance.typeName = instance.dcsObject:getTypeName() + instance.position = instance.dcsObject:getPosition() + instance.numOfTimesRefreshed = 0 + instance.speed = 0 + return instance +end + +function SkynetIADSContact:getName() + return self.name +end + +function SkynetIADSContact:getTypeName() + return self.typeName +end + +function SkynetIADSContact:isTypeKnown() + return self.dcsRadarTarget.type +end + +function SkynetIADSContact:isDistanceKnown() + return self.dcsRadarTarget.distance +end + +function SkynetIADSContact:getPosition() + return self.position +end + +function SkynetIADSContact:getGroundSpeedInKnots(decimals) + if decimals == nil then + decimals = 2 + end + return mist.utils.round(self.speed, decimals) +end + +function SkynetIADSContact:getDesc() + if self.dcsObject:isExist() then + return self.dcsObject:getDesc() + else + return {} + end +end + +function SkynetIADSContact:getNumberOfTimesHitByRadar() + return self.numOfTimesRefreshed +end + +function SkynetIADSContact:refresh() + self.numOfTimesRefreshed = self.numOfTimesRefreshed + 1 + if self.dcsObject and self.dcsObject:isExist() then + local distance = mist.utils.metersToNM(mist.utils.get2DDist(self.position.p, self.dcsObject:getPosition().p)) + local timeDelta = (timer.getAbsTime() - self.lastTimeSeen) + if timeDelta > 0 then + local hours = timeDelta / 3600 + self.speed = (distance / hours) + end + self.position = self.dcsObject:getPosition() + end + self.lastTimeSeen = timer.getAbsTime() +end + +function SkynetIADSContact:getAge() + return mist.utils.round(timer.getAbsTime() - self.lastTimeSeen) +end + +end + +do + +SkynetIADSEWRadar = {} +SkynetIADSEWRadar = inheritsFrom(SkynetIADSAbstractRadarElement) + +function SkynetIADSEWRadar:create(radarUnit, iads) + local instance = self:superClass():create(radarUnit, iads) + setmetatable(instance, self) + self.__index = self + instance.autonomousBehaviour = SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK + return instance +end + +end +do + +SkynetIADSJammer = {} +SkynetIADSJammer.__index = SkynetIADSJammer + +function SkynetIADSJammer:create(emitter, iads) + local jammer = {} + setmetatable(jammer, SkynetIADSJammer) + jammer.radioMenu = nil + jammer.emitter = emitter + jammer.jammerTaskID = nil + jammer.iads = {iads} + jammer.maximumEffectiveDistanceNM = 200 + --jammer probability settings are stored here, visualisation, see: https://docs.google.com/spreadsheets/d/16rnaU49ZpOczPEsdGJ6nfD0SLPxYLEYKmmo4i2Vfoe0/edit#gid=0 + jammer.jammerTable = { + ['SA-2'] = { + ['function'] = function(distanceNauticalMiles) return ( 1.4 ^ distanceNauticalMiles ) + 90 end, + ['canjam'] = true, + }, + ['SA-3'] = { + ['function'] = function(distanceNauticalMiles) return ( 1.4 ^ distanceNauticalMiles ) + 80 end, + ['canjam'] = true, + }, + ['SA-6'] = { + ['function'] = function(distanceNauticalMiles) return ( 1.4 ^ distanceNauticalMiles ) + 23 end, + ['canjam'] = true, + }, + ['SA-8'] = { + ['function'] = function(distanceNauticalMiles) return ( 1.35 ^ distanceNauticalMiles ) + 30 end, + ['canjam'] = true, + }, + ['SA-10'] = { + ['function'] = function(distanceNauticalMiles) return ( 1.07 ^ (distanceNauticalMiles / 1.13) ) + 5 end, + ['canjam'] = true, + }, + ['SA-11'] = { + ['function'] = function(distanceNauticalMiles) return ( 1.25 ^ distanceNauticalMiles ) + 15 end, + ['canjam'] = true, + }, + ['SA-15'] = { + ['function'] = function(distanceNauticalMiles) return ( 1.15 ^ distanceNauticalMiles ) + 5 end, + ['canjam'] = true, + }, + } + return jammer +end + +function SkynetIADSJammer:masterArmOn() + self:masterArmSafe() + self.jammerTaskID = mist.scheduleFunction(SkynetIADSJammer.runCycle, {self}, 1, 10) +end + +function SkynetIADSJammer:addFunction(natoName, jammerFunction) + self.jammerTable[natoName] = { + ['function'] = jammerFunction, + ['canjam'] = true + } +end + +function SkynetIADSJammer:setMaximumEffectiveDistance(distance) + self.maximumEffectiveDistanceNM = distance +end + +function SkynetIADSJammer:disableFor(natoName) + self.jammerTable[natoName]['canjam'] = false +end + +function SkynetIADSJammer:isKnownRadarEmitter(natoName) + local isActive = false + for unitName, unit in pairs(self.jammerTable) do + if unitName == natoName and unit['canjam'] == true then + isActive = true + end + end + return isActive +end + +function SkynetIADSJammer:addIADS(iads) + table.insert(self.iads, iads) +end + +function SkynetIADSJammer:getSuccessProbability(distanceNauticalMiles, natoName) + local probability = 0 + local jammerSettings = self.jammerTable[natoName] + if jammerSettings ~= nil then + probability = jammerSettings['function'](distanceNauticalMiles) + end + return probability +end + +function SkynetIADSJammer:getDistanceNMToRadarUnit(radarUnit) + return mist.utils.metersToNM(mist.utils.get3DDist(self.emitter:getPosition().p, radarUnit:getPosition().p)) +end + +function SkynetIADSJammer.runCycle(self) + + if self.emitter:isExist() == false then + self:masterArmSafe() + return + end + + for i = 1, #self.iads do + local iads = self.iads[i] + local samSites = iads:getActiveSAMSites() + for j = 1, #samSites do + local samSite = samSites[j] + local radars = samSite:getRadars() + local hasLOS = false + local distance = 0 + local natoName = samSite:getNatoName() + for l = 1, #radars do + local radar = radars[l] + distance = self:getDistanceNMToRadarUnit(radar) + -- I try to emulate the system as it would work in real life, so a jammer can only jam a SAM site if has line of sight to at least one radar in the group + if self:isKnownRadarEmitter(natoName) and self:hasLineOfSightToRadar(radar) and distance <= self.maximumEffectiveDistanceNM then + if iads:getDebugSettings().jammerProbability then + iads:printOutput("JAMMER: Distance: "..distance) + end + samSite:jam(self:getSuccessProbability(distance, natoName)) + end + end + end + end +end + +function SkynetIADSJammer:hasLineOfSightToRadar(radar) + local radarPos = radar:getPosition().p + --lift the radar 30 meters off the ground, some 3d models are dug in to the ground, creating issues in calculating LOS + radarPos.y = radarPos.y + 30 + return land.isVisible(radarPos, self.emitter:getPosition().p) +end + +function SkynetIADSJammer:masterArmSafe() + mist.removeFunction(self.jammerTaskID) +end + +--TODO: Remove Menu when emitter dies: +function SkynetIADSJammer:addRadioMenu() + self.radioMenu = missionCommands.addSubMenu('Jammer: '..self.emitter:getName()) + missionCommands.addCommand('Master Arm On', self.radioMenu, SkynetIADSJammer.updateMasterArm, {self = self, option = 'masterArmOn'}) + missionCommands.addCommand('Master Arm Safe', self.radioMenu, SkynetIADSJammer.updateMasterArm, {self = self, option = 'masterArmSafe'}) +end + +function SkynetIADSJammer.updateMasterArm(params) + local option = params.option + local self = params.self + if option == 'masterArmOn' then + self:masterArmOn() + elseif option == 'masterArmSafe' then + self:masterArmSafe() + end +end + +function SkynetIADSJammer:removeRadioMenu() + missionCommands.removeItem(self.radioMenu) +end + +end +do + +SkynetIADSSAMSearchRadar = {} +SkynetIADSSAMSearchRadar = inheritsFrom(SkynetIADSAbstractDCSObjectWrapper) + +function SkynetIADSSAMSearchRadar:create(unit) + local instance = self:superClass():create(unit) + setmetatable(instance, self) + self.__index = self + instance.firingRangePercent = 100 + instance.maximumRange = 0 + instance.initialNumberOfMissiles = 0 + instance.remainingNumberOfMissiles = 0 + instance.initialNumberOfShells = 0 + instance.remainingNumberOfShells = 0 + instance.triedSensors = 0 + return instance +end + +--override in subclasses to match different datastructure of getSensors() +function SkynetIADSSAMSearchRadar:setupRangeData() + if self:isExist() then + local data = self:getDCSRepresentation():getSensors() + if data == nil then + --this is to prevent infinite calls between launcher and search radar + self.triedSensors = self.triedSensors + 1 + --the SA-13 does not have any sensor data, but is has launcher data, so we use the stuff from the launcher for the radar range. + SkynetIADSSAMLauncher.setupRangeData(self) + return + end + for i = 1, #data do + local subEntries = data[i] + for j = 1, #subEntries do + local sensorInformation = subEntries[j] + -- some sam sites have IR and passive EWR detection, we are just interested in the radar data + -- investigate if upperHemisphere and headOn is ok, I guess it will work for most detection cases + if sensorInformation.type == Unit.SensorType.RADAR then + local upperHemisphere = sensorInformation['detectionDistanceAir']['upperHemisphere']['headOn'] + local lowerHemisphere = sensorInformation['detectionDistanceAir']['lowerHemisphere']['headOn'] + self.maximumRange = upperHemisphere + if lowerHemisphere > upperHemisphere then + self.maximumRange = lowerHemisphere + end + end + end + end + end +end + +function SkynetIADSSAMSearchRadar:getMaxRangeFindingTarget() + return self.maximumRange +end + +function SkynetIADSSAMSearchRadar:isRadarWorking() + -- the ammo check is for the SA-13 which does not return any sensor data: + return (self:isExist() == true and ( self:getDCSRepresentation():getSensors() ~= nil or self:getDCSRepresentation():getAmmo() ~= nil ) ) +end + +function SkynetIADSSAMSearchRadar:setFiringRangePercent(percent) + self.firingRangePercent = percent +end + +function SkynetIADSSAMSearchRadar:getDistance(target) + return mist.utils.get2DDist(target:getPosition().p, self.dcsObject:getPosition().p) +end + +function SkynetIADSSAMSearchRadar:getHeight(target) + local radarElevation = self:getDCSRepresentation():getPosition().p.y + local targetElevation = target:getPosition().p.y + return math.abs(targetElevation - radarElevation) +end + +function SkynetIADSSAMSearchRadar:isInHorizontalRange(target) + return (self:getMaxRangeFindingTarget() / 100 * self.firingRangePercent) >= self:getDistance(target) +end + +function SkynetIADSSAMSearchRadar:isInRange(target) + if self:isExist() == false then + return false + end + return self:isInHorizontalRange(target) +end + +end + +do + +SkynetIADSSamSite = {} +SkynetIADSSamSite = inheritsFrom(SkynetIADSAbstractRadarElement) + +function SkynetIADSSamSite:create(samGroup, iads) + local sam = self:superClass():create(samGroup, iads) + setmetatable(sam, self) + self.__index = self + sam.targetsInRange = false + return sam +end + +function SkynetIADSSamSite:isDestroyed() + local isDestroyed = true + for i = 1, #self.launchers do + local launcher = self.launchers[i] + if launcher:isExist() == true then + isDestroyed = false + end + end + local radars = self:getRadars() + for i = 1, #radars do + local radar = radars[i] + if radar:isExist() == true then + isDestroyed = false + end + end + return isDestroyed +end + +function SkynetIADSSamSite:targetCycleUpdateStart() + self.targetsInRange = false +end + +function SkynetIADSSamSite:targetCycleUpdateEnd() + if self.targetsInRange == false and self.actAsEW == false then + self:goDark() + end +end + +function SkynetIADSSamSite:informOfContact(contact) + -- we make sure isTargetInRange (expensive call) is only triggered if no previous calls to this method resulted in targets in range + if self.targetsInRange == false and self:isTargetInRange(contact) then + self:goLive() + self.targetsInRange = true + end +end + +end +do + +SkynetIADSSAMTrackingRadar = {} +SkynetIADSSAMTrackingRadar = inheritsFrom(SkynetIADSSAMSearchRadar) + +function SkynetIADSSAMTrackingRadar:create(unit) + local instance = self:superClass():create(unit) + setmetatable(instance, self) + self.__index = self + return instance +end + +end +do + +SkynetIADSSAMLauncher = {} +SkynetIADSSAMLauncher = inheritsFrom(SkynetIADSSAMSearchRadar) + +function SkynetIADSSAMLauncher:create(unit) + local instance = self:superClass():create(unit) + setmetatable(instance, self) + self.__index = self + instance.maximumFiringAltitude = 0 + return instance +end + +function SkynetIADSSAMLauncher:setupRangeData() + self.remainingNumberOfMissiles = 0 + self.remainingNumberOfShells = 0 + if self:isExist() then + local data = self:getDCSRepresentation():getAmmo() + local initialNumberOfMissiles = 0 + local initialNumberOfShells = 0 + --data becomes nil, when all missiles are fired + if data then + for i = 1, #data do + local ammo = data[i] + --we ignore checks on radar guidance types, since we are not interested in how exactly the missile is guided by the SAM site. + if ammo.desc.category == Weapon.Category.MISSILE then + --TODO: see what the difference is between Max and Min values, SA-3 has higher Min value than Max?, most likely it has to do with the box parameters supplied by launcher + --to simplyfy we just use the larger value, sam sites need a few seconds of tracking time to fire, by that time contact has most likely closed in on the SAM site. + local altMin = ammo.desc.rangeMaxAltMin + local altMax = ammo.desc.rangeMaxAltMax + self.maximumRange = altMin + if altMin < altMax then + self.maximumRange = altMax + end + self.maximumFiringAltitude = ammo.desc.altMax + self.remainingNumberOfMissiles = self.remainingNumberOfMissiles + ammo.count + initialNumberOfMissiles = self.remainingNumberOfMissiles + end + if ammo.desc.category == Weapon.Category.SHELL then + self.remainingNumberOfShells = self.remainingNumberOfShells + ammo.count + initialNumberOfShells = self.remainingNumberOfShells + end + --if no distance was detected we run the code for the search radar. This happens when all in one units are passed like the shilka + if self.maximumRange == 0 then + --this is to prevent infinite calls between launcher and search radar + if self.triedSensors <= 2 then + SkynetIADSSAMSearchRadar.setupRangeData(self) + end + end + end + -- conditions here are because setupRangeData() is called multiple times in the code to update ammo status, we set initial values only the first time the method is called + if self.initialNumberOfMissiles == 0 then + self.initialNumberOfMissiles = initialNumberOfMissiles + end + if self.initialNumberOfShells == 0 then + self.initialNumberOfShells = initialNumberOfShells + end + end + end +end + +function SkynetIADSSAMLauncher:getInitialNumberOfShells() + return self.initialNumberOfShells +end + +function SkynetIADSSAMLauncher:getRemainingNumberOfShells() + self:setupRangeData() + return self.remainingNumberOfShells +end + +function SkynetIADSSAMLauncher:getInitialNumberOfMissiles() + return self.initialNumberOfMissiles +end + +function SkynetIADSSAMLauncher:getRemainingNumberOfMissiles() + self:setupRangeData() + return self.remainingNumberOfMissiles +end + +function SkynetIADSSAMLauncher:getRange() + return self.maximumRange +end + +function SkynetIADSSAMLauncher:getMaximumFiringAltitude() + return self.maximumFiringAltitude +end + +function SkynetIADSSAMLauncher:isWithinFiringHeight(target) + -- if no max firing height is set (radar quided AAA) then we use the vertical range, bit of a hack but probably ok for AAA + if self:getMaximumFiringAltitude() > 0 then + return self:getMaximumFiringAltitude() >= self:getHeight(target) + else + return self:getRange() >= self:getHeight(target) + end +end + +function SkynetIADSSAMLauncher:isInRange(target) + if self:isExist() == false then + return false + end + return self:isWithinFiringHeight(target) and self:isInHorizontalRange(target) +end + +end + +--[[ +SA-2 Launcher: + { + count=1, + desc={ + Nmax=17, + RCS=0.39669999480247, + _origin="", + altMax=25000, + altMin=100, + box={ + max={x=4.7303376197815, y=0.84564626216888, z=0.84564626216888}, + min={x=-5.8387970924377, y=-0.84564626216888, z=-0.84564626216888} + }, + category=1, + displayName="SA2V755", + fuseDist=20, + guidance=4, + life=2, + missileCategory=2, + rangeMaxAltMax=30000, + rangeMaxAltMin=40000, + rangeMin=7000, + typeName="SA2V755", + warhead={caliber=500, explosiveMass=196, mass=196, type=1} + } + } +} +--]] diff --git a/resources/plugins/skynetiads/skynetiads-config.lua b/resources/plugins/skynetiads/skynetiads-config.lua new file mode 100644 index 00000000..ea6cb4a5 --- /dev/null +++ b/resources/plugins/skynetiads/skynetiads-config.lua @@ -0,0 +1,130 @@ +------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Mission configuration file for the Skynet-IADS framework +-- see https://github.com/walder/Skynet-IADS +-- +-- This configuration is tailored for a mission generated by DCS Liberation +-- see https://github.com/Khopa/dcs_liberation +------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- Skynet-IADS plugin - configuration +env.info("DCSLiberation|Skynet-IADS plugin - configuration") + +if dcsLiberation and SkynetIADS then + + -- specific options + local createRedIADS = false + local createBlueIADS = false + local actAsEwrRED = false + local actAsEwrBLUE = false + local includeRedInRadio = false + local includeBlueInRadio = false + local debugRED = false + local debugBLUE = false + + -- retrieve specific options values + if dcsLiberation.plugins then + if dcsLiberation.plugins.skynetiads then + createRedIADS = dcsLiberation.plugins.skynetiads.createRedIADS + createBlueIADS = dcsLiberation.plugins.skynetiads.createBlueIADS + actAsEwrRED = dcsLiberation.plugins.skynetiads.actAsEwrRED + actAsEwrBLUE = dcsLiberation.plugins.skynetiads.actAsEwrBLUE + includeRedInRadio = dcsLiberation.plugins.skynetiads.includeRedInRadio + includeBlueInRadio = dcsLiberation.plugins.skynetiads.includeBlueInRadio + debugRED = dcsLiberation.plugins.skynetiads.debugRED + debugBLUE = dcsLiberation.plugins.skynetiads.debugBLUE + end + end + + env.info(string.format("DCSLiberation|Skynet-IADS plugin - createRedIADS=%s",tostring(createRedIADS))) + env.info(string.format("DCSLiberation|Skynet-IADS plugin - createBlueIADS=%s",tostring(createBlueIADS))) + env.info(string.format("DCSLiberation|Skynet-IADS plugin - actAsEwrRED=%s",tostring(actAsEwrRED))) + env.info(string.format("DCSLiberation|Skynet-IADS plugin - actAsEwrBLUE=%s",tostring(actAsEwrBLUE))) + env.info(string.format("DCSLiberation|Skynet-IADS plugin - includeRedInRadio=%s",tostring(includeRedInRadio))) + env.info(string.format("DCSLiberation|Skynet-IADS plugin - includeBlueInRadio=%s",tostring(includeBlueInRadio))) + env.info(string.format("DCSLiberation|Skynet-IADS plugin - debugRED=%s",tostring(debugRED))) + env.info(string.format("DCSLiberation|Skynet-IADS plugin - debugBLUE=%s",tostring(debugBLUE))) + + -- actual configuration code + + local function initializeIADS(iads, coalition, actAsEwr, inRadio, debug) + + local coalitionPrefix = "BLUE" + if coalition == 1 then + coalitionPrefix = "RED" + end + + if debug then + env.info("adding debug information") + local iadsDebug = iads:getDebugSettings() + iadsDebug.IADSStatus = true + iadsDebug.samWentDark = true + iadsDebug.contacts = true + iadsDebug.radarWentLive = true + iadsDebug.noWorkingCommmandCenter = false + iadsDebug.ewRadarNoConnection = false + iadsDebug.samNoConnection = false + iadsDebug.jammerProbability = true + iadsDebug.addedEWRadar = false + iadsDebug.hasNoPower = false + iadsDebug.harmDefence = true + iadsDebug.samSiteStatusEnvOutput = true + iadsDebug.earlyWarningRadarStatusEnvOutput = true + end + + --add EW units to the IADS: + iads:addEarlyWarningRadarsByPrefix(coalitionPrefix .. " EW") + + --add SAM groups to the IADS: + iads:addSAMSitesByPrefix(coalitionPrefix .. " SAM") + + -- specific configurations, for each SAM type + if actAsEwr then + iads:getSAMSitesByNatoName('SA-10'):setActAsEW(true) + iads:getSAMSitesByNatoName('SA-6'):setActAsEW(true) + iads:getSAMSitesByNatoName('Patriot'):setActAsEW(true) + end + + -- add the AWACS + if dcsLiberation.AWACs then + for _, data in pairs(dcsLiberation.AWACs) do + env.info(string.format("DCSLiberation|Skynet-IADS plugin - processing AWACS %s", data.dcsGroupName)) + local group = Group.getByName(data.dcsGroupName) + if group then + if group:getCoalition() == coalition then + local unit = group:getUnit(1) + if unit then + local unitName = unit:getName() + env.info(string.format("DCSLiberation|Skynet-IADS plugin - adding AWACS %s", unitName)) + iads:addEarlyWarningRadar(unitName) + end + end + end + end + end + + if inRadio then + --activate the radio menu to toggle IADS Status output + env.info("DCSLiberation|Skynet-IADS plugin - adding in radio menu") + iads:addRadioMenu() + end + + --activate the IADS + iads:setupSAMSitesAndThenActivate() + end + + ------------------------------------------------------------------------------------------------------------------------------------------------------------ + -- create the IADS networks + ------------------------------------------------------------------------------------------------------------------------------------------------------------- + if createRedIADS then + env.info("DCSLiberation|Skynet-IADS plugin - creating red IADS") + redIADS = SkynetIADS:create("IADS") + initializeIADS(redIADS, 1, actAsEwrRED, includeRedInRadio, debugRED) -- RED + end + + if createBlueIADS then + env.info("DCSLiberation|Skynet-IADS plugin - creating blue IADS") + blueIADS = SkynetIADS:create("IADS") + initializeIADS(blueIADS, 2, actAsEwrBLUE, includeBlueInRadio, debugBLUE) -- BLUE + end + +end \ No newline at end of file