--[[ GROUND UNITS AI ENHANCEMENT -- AI-EN This script is possible thanks to the existence of many other scripts that I had the opportunity to use as inspiration, or partial copy, or modification due to being publicly available. I basically learnt coding starting with mist, CTLD, SLmod, dismount script, and else. All credits goes to these authors, and I am very grateful. Therefore this script is obviously public and you should feel free to use it totally, partially or anything else for any DCS gaming purpose. What is it? This script is done with the purpose of rehearsing the standard DCS ground groups and units AI, implementing additional behaviours using the SSE, aiming to get some more dynamic and realistic response of those units to the environment and users actions. The script currently is focused on ground units, except SAM and medium-to-long range air defences which are not being commanded. This works mostly by changing 2 common behaviours: - the fact the the units basically do not react to any hostile fire moving elsewhere: the script provide movement decision - the fact that artillery units fire only when they directly achieve a target, which is mostly unlikely: the script provide targets on arty groups via allied units informations It also act on another important thing on ground war: dismountable soldiers' teams. By rehearsing an idea of MBot dismount script, AIEN add soldier teams to any IFV, APC and trucks unit in the scenery. That means that a ground unit normally "unarmed" like a M-939 might in fact transport RPG, rifle and even manpads units that will dismount from it when necessary to respond to specific menace. In the end, if you need a specific group or more to do NOT be altered by the script, you simply have to add the exclusion tag (AIEN_xcl_tag string variable) in their group name: they will be completely ignored. Usage informations: - The script does not require any other script to run; - Load the script in a "do script file" trigger after at least 3 seconds from mission start; - The script does not need set-up or additional coding besides the user customization variables setup below; - It won't be properly compatible with any other script that try to change the AI behaviours for ground movers. It should be ok with IADS scripts; - It alter the ground groups behaviour and any change will effectively "broke" any other actions given by DCS or CA user when kicking in (and that's just normal and ok). - The script do not use any naming convention (except for the exclusion tag), it will define available AI behaviour change using DCS available parameters such as unit attributes, skills, etc. - you will be able to customize each of its "features", by enabling and disabling each option you want to use, here below Purpose and limitations: The script does not want to provide a complete "automated ground war" thing: this is already in development by both ED and also by DSMC script by me. In fact, this script is a part of the DSMC script, but still usable externally as standalone. Suggestion, ideas: -- Please refer to the script GitHub project, you will find the project details to suggest modification, contribute with coding and else. --]]-- --## GLOBAL GENERAL AIEN CONTENT TABLE AIEN = {} --## USER CUSTOMIZATION VARIABLES ## AIEN.config = {} AIEN.config.dontInitialize = true -- if true, AIEN will not initialize; instead, you'll have to run it from your own code - it's useful when you want to override some functions/parameters before the initialization takes place -- coalition affected by the script AIEN.config.blueAI = true -- true/false. If true, the AI enhancement will be applied to the blue coalition ground groups, else, no script effect will take place AIEN.config.redAI = true -- true/false. If true, the AI enhancement will be applied to the red coalition ground groups, else, no script effect will take place -- Action sets allowed. AIEN.config.firemissions = true -- true/false. If true, each artillery in the coalition will fire automatically at available targets provided by other ground units and drones AIEN.config.reactions = true -- true/false. If true, when a mover group gets an hit, it will react accordingly to its skills and to its situational awareness, not staying there taking hits without doing nothing AIEN.config.suppression = true -- true/false. If true, once a group take fire from arty or air and it's not armoured, it will be suppressed for 15-45 seconds and won't return fire. Require reactions to be set as 'true' AIEN.config.dismount = true -- true/false. //BEWARE: CAN AFFECT PERFORMANCES ON LOW END SYSTEMS // Thanks to MBot's original script, if true AI ground units with infantry transport capabilities (mainly APC/IFV/Trucks) will dismount soldiers with rifle, rpg and sometimes mandpads when appropriate -- User advanced customization AIEN.config.AIEN_xcl_tag = "XCL" -- string, global, case sensitive. Can be dynamically changed by other script or triggers, since it's a global variable. used as a text format without spaces or special characters. only letters and numbers allowed. Any ground group with this 'tag' in its group name won't get AI enhancement behaviour, regardless of its coalition AIEN.config.AIEN_zoneFilter = "" -- string, global, case sensitive. Can be dynamically changed by other script or triggers, since it's a global variable. used as a text format without spaces or special characters. only letters and numbers allowed, i.e. "AIEN" will fit. If left nil, or void string like "", won't be used. Only groups inside the named trigger zone will be affected by AIEN script behaviors of reaction, dismount and suppression, and vice versa. If no trigger zone with the specific name is in the mission, then all the groups will use AIEN features. AIEN.config.message_feed = true -- true/false. If true, each relevant AI action starting will also create a trigger message feedback for its coalition AIEN.config.mark_on_f10_map = true -- true/false. If true, when an artillery fire mission is ongoing, a markpoint will appear on the map of the allied coalition to show the expected impact point AIEN.config.skill_action_const = false -- true/false. If true, AI available reactions types will be limited by the group average skill. If not, almost 2/3 of all available actions will be always be available regardless of the group skills -- User bug report: prior to report a bug, please try reproducing it with this variable set to "true" AIEN.config.AIEN_debugProcessDetail = true --## LOCAL HIGH LEVEL VARIABLES ## -- changing the variable below is for fine customization, but it's not recommended cause it can change the code behaviour. -- If you do so, please revert to original value and retry before reporting bugs. -- movement variables AIEN.config.outRoadSpeed = 8 -- do *3.6 for km/h, cause DCS thinks in m/s AIEN.config.inRoadSpeed = 15 -- do *3.6 for km/h, cause DCS thinks in m/s AIEN.config.infantrySpeed = 2 -- do *3.6 for km/h, cause DCS thinks in m/s AIEN.config.repositionDistance = 500 -- meters, radius to a specific destination point that will be randomized between 90% and 110% of this value. Used when a group is moved upon another group position: the other group position will be the destination. AIEN.config.rndFleeDistance = 2000 -- meters, reposition distance given to a group when a destination is not defined. The direction also will be totally random. Used, i.e., for "panic" reaction -- dismounted troops variables AIEN.config.droppedReposition = 80 -- if no enemy is identified, this is the distance where dismount group will reposition themselves AIEN.config.remountTime = 600 -- time after which dismounted troops will try to go back to their original vehicle for remount, if commanded AIEN.config.infantryExtractDist = 200 -- max distance from vehicle to troops to allow a group extraction AIEN.config.infantrySearchDist = 2000 -- max distance from vehicle to troops to allow a dismount group to run toward the enemies -- informative calls variables AIEN.config.outAmmoLowLevel = 0.5 -- factor on total amount -- reactions and tasking variables AIEN.config.intelDbTimeout = 1200 -- seconds. Used to cancel intelDb entries for units (not static!), when the time of the contact gathering is more than this value AIEN.config.artyFireLastContactThereshold = 300 -- seconds, max amount of time since last contact to consider an arty target ok AIEN.config.taskTimeout = 480 -- seconds after which a tasked group is removed from the database AIEN.config.targetedTimeout = 240 -- seconds after which a targeted variable in inteldb is removed from database AIEN.config.disperseActionTime = 120 -- seconds AIEN.config.counterBatteryRadarRange = 50000 -- m, capable distance for a radar to perform counter battery calculations AIEN.config.counterBatteryPlanDelay = 240 -- s, will be also randomized on +-35%. Used to define the delay of the planned counter battery fire if available AIEN.config.smoke_source_num = 5 -- number, between 4 and 9. Generated smokes for each unit when smoke reaction is called in. Any number below 4 or above 9 will be converted in the nearest threshold -- SA evaluation variables AIEN.config.proxyBuildingDistance = 4000 -- m, if buildings are within this distance value, they are considered "close" AIEN.config.proxyUnitsDistance = 5000 -- m, if units are within this distance value, they are considered "close" AIEN.config.supportDistance = 8000 -- m, maximum distance for evaluating support or cover movements when under attack AIEN.config.withrawDist = 15000 -- m, maximum distance for withdraw manoeuvre nearby a friendly support unit --#################################################################################################### --###### DO NOT CHANGE CODE BELOW HERE ############################################################### --#################################################################################################### --###### CONFIG AND VARIABLES ######################################################################## --## MAIN VARIABLES -- Mark id addition local markIdStart = 12345000000 -- DSMC version of the script check: if already there due to DSMC version, the script won't be loaded. if AIEN.performPhaseCycle then env.info(("AIEN already there in another way, stopping")) return end --## LOCAL GENERAL INFORMATIONS VARIABLES (mostly used for debug log and info) local ModuleName = "AIEN" local MainVersion = "1" local SubVersion = "0" local Build = "0149" local Date = "2025.03.24" --## NOT USED (YET) / TO BE REMOVED local resumeRouteTimer = 300 -- seconds local supportDist = 20000 -- m of distance max between objective point and group local resupplyMaxDist = 200000 -- m of distance for the maximum allowed resupply route. local soldierWeight = 110 -- weight of a soldiers in kg local f10menuUpdateFreq = 10 -- F10 menù refresh rate local baseArtyRange = 15000 -- m when AIEN.tblThreatsRange is not available - QUIIII local artilleryFrequencyFire = 10*60 -- time between two salvo fires local proxyVegetationDistance = 2000 -- m, if trees or vegetation are whitin this distance value, they are considered "close" -- // CURRENTLY NOT POSSIBLE IN DCS local infantryDismountRange = 1000 -- distance from enemy that trigger troops dismount --## LOCAL LOW LEVEL VARIABLES --Debug local AIEN_io = _G.io -- check if io is available in mission environment, if so debug will also produce files. NOT-RECOMMENDED to have an unsanitized mission env. local AIEN_lfs = _G.lfs -- check if lfs is available in mission environment, if so debug will also produce files. NOT-RECOMMENDED to have an unsanitized mission env. -- FSM and system local PHASE = "Initialization" -- used by FSM, don't change, it won't affect anything local phase_index = nil local phase_keys = {} local phaseCycleTimer = 0.2 -- seconds, used by FSM. Define how much time pass between a loop entry calculation and another. You might want to reduce it further or if you feel DCS being "slow" you can raise up to 1.0 second. local forceRoadUse = nil -- this variable is automatically handled. It's used for every planned movement, being dependant by obstacle numbers and also by weather local rndMinRT_xper = 2 -- seconds counted as minimum basic reaction time after an event (beware, reaction time also depends on group averaged skill) local rndMacRT_xper = 4 -- seconds counted as maximum basic reaction time after an event (beware, reaction time also depends on group averaged skill) local stupidIndex = 1 -- used to avoid infinite loops --AI processing timers local underAttack = {} -- used when a group has been attacked, it keeps "tactical" tasking off for 10 mins leaving room for "reaction" decision making --Dynamic and_or linked to other code if not DSMC_baseGcounter then DSMC_baseGcounter = 20000000 end if not DSMC_baseUcounter then DSMC_baseUcounter = 19000000 end local STTS_radioAM = DSMC_STTS_rAM local STTS_radioFM = DSMC_STTS_rFM local forceRoadUse = nil if env.mission.weather.clouds.iprecptns > 0 then forceRoadUse = true end --## LOCAL DYNAMIC TABLES (DBs) local groundgroupsDb = {} -- used for general purpose on groups command local droneunitDb = {} -- used mostly for artillery control local intelDb = {} -- used for any enemy assessment evaluation. The getSA function is used for populating the db local mountedDb = {} -- used for assign infantry teams to each capable vehicle or trucks local infcarrierDb = {} -- used for store infantry carriers informations (i.e. available space) --## LOCAL STATIC TABLES -- used for prioritizing arty target by class local classPriority = { ["MLRS"] = 10, ["ARTY"] = 9.5, ["MBT"] = 9, ["ATGM"] = 8, ["SAM"] = 9.6, ["SHORAD"] = 7.5, ["IFV"] = 7, ["APC"] = 4, ["RECCE"] = 3.5, ["AAA"] = 3, ["MISSILE"] = 4.5, ["MANPADS"] = 6, ["LOGI"] = 2, ["INF"] = 1, ["UNKN"] = 0.5, ["ARBN"] = 0, ["SHIP"] = 2, } -- used to identify if a group is suitable for supporting others in ground battle local supportGroundClasses = { ["MLRS"] = 2.1, ["ARTY"] = 2.3, ["MBT"] = 10, ["ATGM"] = 9, ["SAM"] = 3, ["SHORAD"] = 5, ["IFV"] = 7, ["APC"] = 5.5, ["RECCE"] = 3.5, ["AAA"] = 2, ["MISSILE"] = 1, ["MANPADS"] = 0.8, ["LOGI"] = 1.2, ["INF"] = 0.5, ["UNKN"] = 0.9, --["ARBN"] = 0, --["SHIP"] = 0, } -- used to identify if a group is suitable for supporting others in ground battle local supportCounterAirClasses = { ["MLRS"] = 0.5, ["ARTY"] = 1, ["MBT"] = 4, ["ATGM"] = 5, ["SAM"] = 10, ["SHORAD"] = 9, ["IFV"] = 4.5, ["APC"] = 3, ["RECCE"] = 2.5, ["AAA"] = 7, ["MISSILE"] = 0.2, ["MANPADS"] = 8, ["LOGI"] = 1.2, ["INF"] = 0.1, ["UNKN"] = 2, --["ARBN"] = 0, --["SHIP"] = 0, } -- used in multiple part of the scripts for defining reactions, speed, and smartness of the groups local skills = { ["Average"] = {aim_delay = 170, skillVal = 4}, ["High"] = {aim_delay = 130, skillVal = 9}, ["Good"] = {aim_delay = 150, skillVal = 6}, ["Excellent"] = {aim_delay = 110, skillVal = 12}, ["Random"] = {aim_delay = 140, skillVal = 8}, -- skill val is NOT used in this case, it's replaced by a randomness. } -- used for define infantry teams carrier capacity by attribute. BEWARE: the table key are not "classes", are actual DCS attributes enum. Don't add "casual" things, stick to them. You can find a list here: https://wiki.hoggitworld.com/view/DCS_enum_attributes local dismountCarriers = { ["Trucks"] = 16, -- this should be handled as a 3 teams of 4 each ["APC"] = 8, -- this should be handled as a 2 teams of 4 each ["IFV"] = 5, -- this should be handled as a 1 teams of 4 each } -- used for define infantry teams composition. In the table, p is the probability from 1 to 100, c is the composition local dismountTeamsWest = { ["rifle"] = {id = "rifle", p = 55, c = { [1] = "Soldier M4", [2] = "Soldier M4", [3] = "Soldier M4", [4] = "Soldier M249", }}, ["mixed"] = {id = "mixed", p = 20, c = { [1] = "Soldier M4", [2] = "Paratrooper RPG-16", [3] = "Soldier M4", [4] = "Soldier M249", }},-- mixed is actually 3 rifle and 1 rpg ["RPGs"] = {id = "RPGs", p = 9, c = { [1] = "Paratrooper RPG-16", [2] = "Paratrooper RPG-16", [3] = "Soldier M4", [4] = "Soldier M4", }}, ["manpads"] = {id = "manpads", p = 3, c = { [1] = "Stinger manpad", [2] = "Stinger manpad", [3] = "Soldier M4", }}, } local dismountTeamsEast = { ["rifle"] = {id = "rifle", p = 55, c = { [1] = "Infantry AK ver3", [2] = "Infantry AK ver2", [3] = "Infantry AK ver2", [4] = "Infantry AK ver3", }}, ["mixed"] = {id = "mixed", p = 20, c = { [1] = "Infantry AK ver2", [2] = "Paratrooper RPG-16", [3] = "Infantry AK ver3", [4] = "Infantry AK ver3", }},-- mixed is actually 3 rifle and 1 rpg ["RPGs"] = {id = "RPGs", p = 9, c = { [1] = "Paratrooper RPG-16", [2] = "Paratrooper RPG-16", [3] = "Infantry AK ver2", [4] = "Infantry AK ver3", }}, ["manpads"] = {id = "manpads", p = 3, c = { [1] = "SA-18 Igla manpad", [2] = "SA-18 Igla manpad", [3] = "Infantry AK ver3", }}, } if env.mission and env.mission.date and env.mission.date.Year then local y = tonumber(env.mission.date.Year) if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN mission date: " .. tostring(y))) end if y < 1980 then dismountTeamsWest["manpads"] = nil if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN removed stinger")) end elseif y < 1970 then dismountTeamsEast["manpads"] = nil dismountTeamsWest["manpads"] = nil if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN removed all manpads")) end end end --## LINKED TABLES (or local if not available) local tblThreatsRange = nil -- this is a foundamental table cause it holds the firing range of any units, but mostly artillery one! Since the required data aren't available in the mission env, it can be either ported by DSMC (if used) or manually prompted. For the latter, obviously, it require to be manually updated. if EMBD then -- just for compatibility enhancement tblThreatsRange = EMBD.tblThreatsRange if tblThreatsRange then for tId, tData in pairs(tblThreatsRange) do tData.attr = nil end end end if DGWS then -- just for compatibility enhancement tblThreatsRange = DSMC_tblThreatsRange for tId, tData in pairs(tblThreatsRange) do tData.attr = nil end end if not tblThreatsRange then tblThreatsRange = { ["S-60_Type59_Artillery"] = { ["detection"] = 5000, ["threat"] = 6000, }, -- end of ["S-60_Type59_Artillery"] ["flak30"] = { ["irsignature"] = 0.01, ["detection"] = 0, ["threat"] = 2500, }, -- end of ["flak30"] ["Daimler_AC"] = { ["irsignature"] = 0.07, ["detection"] = 0, ["threat"] = 2000, }, -- end of ["Daimler_AC"] ["MTLB"] = { ["irsignature"] = 0.09, ["detection"] = 0, ["threat"] = 1000, }, -- end of ["MTLB"] ["L-39ZA"] = { ["irsignature"] = 0.2, }, -- end of ["L-39ZA"] ["Czech hedgehogs 2"] = { }, -- end of ["Czech hedgehogs 2"] ["Horch_901_typ_40_kfz_21"] = { ["irsignature"] = 0.065, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Horch_901_typ_40_kfz_21"] ["uh1h_cargo"] = { }, -- end of ["uh1h_cargo"] ["pipes_small_cargo"] = { }, -- end of ["pipes_small_cargo"] ["Gepard"] = { ["irsignature"] = 0.1, ["detection"] = 15000, ["threat"] = 4000, }, -- end of ["Gepard"] ["M 818"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 0, }, -- end of ["M 818"] ["Soldier RPG"] = { ["irsignature"] = 0.004, ["detection"] = 0, ["threat"] = 500, }, -- end of ["Soldier RPG"] ["Container_watchtower_lights"] = { }, -- end of ["Container_watchtower_lights"] ["FarpHide_Med"] = { }, -- end of ["FarpHide_Med"] ["CV_59_MD3"] = { }, -- end of ["CV_59_MD3"] ["CastleClass_01"] = { ["detection"] = 25000, ["threat"] = 3000, }, -- end of ["CastleClass_01"] ["Merkava_Mk4"] = { ["irsignature"] = 0.12, ["detection"] = 0, ["threat"] = 3500, }, -- end of ["Merkava_Mk4"] ["Patriot cp"] = { ["irsignature"] = 0.05, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Patriot cp"] ["5p73 s-125 ln"] = { ["irsignature"] = 0.02, ["detection"] = 0, ["threat"] = 18000, }, -- end of ["5p73 s-125 ln"] ["house2arm"] = { ["irsignature"] = 0.007, ["detection"] = 0, ["threat"] = 800, }, -- end of ["house2arm"] ["F-15E"] = { ["irsignature"] = 0.91, }, -- end of ["F-15E"] ["M45_Quadmount"] = { ["irsignature"] = 0.01, ["detection"] = 0, ["threat"] = 1500, }, -- end of ["M45_Quadmount"] ["Churchill_VII"] = { ["irsignature"] = 0.105, ["detection"] = 0, ["threat"] = 3000, }, -- end of ["Churchill_VII"] ["B-52H"] = { ["irsignature"] = 4, }, -- end of ["B-52H"] ["AS32-p25"] = { }, -- end of ["AS32-p25"] ["SturmPzIV"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 4500, }, -- end of ["SturmPzIV"] ["T155_Firtina"] = { ["irsignature"] = 0.11, ["detection"] = 0, ["threat"] = 41000, }, -- end of ["T155_Firtina"] ["oiltank_cargo"] = { }, -- end of ["oiltank_cargo"] ["Carrier LSO Personell 2"] = { }, -- end of ["Carrier LSO Personell 2"] ["Freya_Shelter_Brick"] = { }, -- end of ["Freya_Shelter_Brick"] ["Building04_PBR"] = { }, -- end of ["Building04_PBR"] ["A-20G"] = { ["irsignature"] = 0.1, }, -- end of ["A-20G"] ["r11_volvo"] = { }, -- end of ["r11_volvo"] ["Container_40ft"] = { }, -- end of ["Container_40ft"] ["Predator GCS"] = { ["irsignature"] = 0.05, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Predator GCS"] ["hy_launcher"] = { ["irsignature"] = 0.01, ["detection"] = 100000, ["threat"] = 100000, }, -- end of ["hy_launcher"] ["Bf-109K-4"] = { ["irsignature"] = 0.1, }, -- end of ["Bf-109K-4"] ["Seawise_Giant"] = { ["irsignature"] = 0.4, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Seawise_Giant"] ["LST_Mk2"] = { ["irsignature"] = 0.3, ["detection"] = 0, ["threat"] = 4000, }, -- end of ["LST_Mk2"] ["m1_vla"] = { }, -- end of ["m1_vla"] ["Mirage-F1BQ"] = { ["irsignature"] = 0.8, }, -- end of ["Mirage-F1BQ"] ["ZU-23 Closed Insurgent"] = { ["irsignature"] = 0.006, ["detection"] = 5000, ["threat"] = 2500, }, -- end of ["ZU-23 Closed Insurgent"] ["HQ-7_LN_P"] = { ["irsignature"] = 0.08, ["detection"] = 8000, ["threat"] = 12000, }, -- end of ["HQ-7_LN_P"] ["snr s-125 tr"] = { ["irsignature"] = 0.06, ["detection"] = 100000, ["threat"] = 0, }, -- end of ["snr s-125 tr"] ["Siegfried Line"] = { }, -- end of ["Siegfried Line"] ["SAU Msta"] = { ["irsignature"] = 0.1, ["detection"] = 0, ["threat"] = 23500, }, -- end of ["SAU Msta"] ["Tent02"] = { }, -- end of ["Tent02"] ["outpost_road"] = { ["irsignature"] = 0.007, ["detection"] = 0, ["threat"] = 800, }, -- end of ["outpost_road"] ["Vulcan"] = { ["irsignature"] = 0.09, ["detection"] = 5000, ["threat"] = 2000, }, -- end of ["Vulcan"] ["Dragonteeth 2"] = { }, -- end of ["Dragonteeth 2"] ["leopard-2A4_trs"] = { ["irsignature"] = 0.12, ["detection"] = 0, ["threat"] = 3500, }, -- end of ["leopard-2A4_trs"] ["Sd_Kfz_7"] = { ["irsignature"] = 0.075, ["detection"] = 0, }, -- end of ["Sd_Kfz_7"] ["container_40ft"] = { }, -- end of ["container_40ft"] ["TYPE-59"] = { ["irsignature"] = 0.1, ["detection"] = 0, ["threat"] = 2500, }, -- end of ["TYPE-59"] ["SpGH_Dana"] = { ["irsignature"] = 0.09, ["detection"] = 0, ["threat"] = 18700, }, -- end of ["SpGH_Dana"] ["Ski Ramp"] = { }, -- end of ["Ski Ramp"] ["BMP-2"] = { ["irsignature"] = 0.09, ["detection"] = 0, ["threat"] = 3000, }, -- end of ["BMP-2"] ["santafe"] = { }, -- end of ["santafe"] ["Allies_Director"] = { ["irsignature"] = 0.03, ["detection"] = 30000, ["threat"] = 0, }, -- end of ["Allies_Director"] ["F-5E"] = { ["irsignature"] = 0.4, }, -- end of ["F-5E"] ["VAB_Mephisto"] = { ["irsignature"] = 0.085, ["detection"] = 0, ["threat"] = 3800, }, -- end of ["VAB_Mephisto"] ["Cargo06"] = { }, -- end of ["Cargo06"] ["Stug_IV"] = { ["irsignature"] = 0.1, ["detection"] = 0, ["threat"] = 3000, }, -- end of ["Stug_IV"] ["F-4E"] = { ["irsignature"] = 1, }, -- end of ["F-4E"] ["M-109"] = { ["irsignature"] = 0.11, ["detection"] = 0, ["threat"] = 22000, }, -- end of ["M-109"] ["LAZ Bus"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 0, }, -- end of ["LAZ Bus"] ["UH-60A"] = { ["irsignature"] = 0.22, }, -- end of ["UH-60A"] ["Beer Bomb"] = { }, -- end of ["Beer Bomb"] ["ATZ-10"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 0, }, -- end of ["ATZ-10"] ["P20_01"] = { }, -- end of ["P20_01"] ["Elefant_SdKfz_184"] = { ["irsignature"] = 0.11, ["detection"] = 0, ["threat"] = 6000, }, -- end of ["Elefant_SdKfz_184"] ["M978 HEMTT Tanker"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 0, }, -- end of ["M978 HEMTT Tanker"] ["Mirage-F1CE"] = { ["irsignature"] = 0.8, }, -- end of ["Mirage-F1CE"] ["Marder"] = { ["irsignature"] = 0.085, ["detection"] = 0, ["threat"] = 1500, }, -- end of ["Marder"] ["HarborTug"] = { ["detection"] = 0, ["threat"] = 0, }, -- end of ["HarborTug"] ["CVN_72"] = { ["irsignature"] = 0.4, ["detection"] = 50000, ["threat"] = 25000, }, -- end of ["CVN_72"] ["Sandbag_11"] = { }, -- end of ["Sandbag_11"] ["BRDM-2"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 1600, }, -- end of ["BRDM-2"] ["ATZ-5"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 0, }, -- end of ["ATZ-5"] ["Tent01"] = { }, -- end of ["Tent01"] ["An-26B"] = { ["irsignature"] = 0.5, }, -- end of ["An-26B"] ["LiAZ Bus"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 0, }, -- end of ["LiAZ Bus"] ["soldier_wwii_us"] = { ["irsignature"] = 0.004, ["detection"] = 0, ["threat"] = 500, }, -- end of ["soldier_wwii_us"] ["trunks_small_cargo"] = { }, -- end of ["trunks_small_cargo"] ["Tiger_II_H"] = { ["irsignature"] = 0.105, ["detection"] = 0, ["threat"] = 6000, }, -- end of ["Tiger_II_H"] ["KC130"] = { ["irsignature"] = 1, }, -- end of ["KC130"] ["LAV-25"] = { ["irsignature"] = 0.085, ["detection"] = 0, ["threat"] = 2500, }, -- end of ["LAV-25"] ["SK_C_28_naval_gun"] = { ["irsignature"] = 0.01, ["detection"] = 0, ["threat"] = 20000, }, -- end of ["SK_C_28_naval_gun"] ["Stug_III"] = { ["irsignature"] = 0.1, ["detection"] = 0, ["threat"] = 3000, }, -- end of ["Stug_III"] ["BoomBarrier_open"] = { }, -- end of ["BoomBarrier_open"] ["tetrapod_cargo"] = { }, -- end of ["tetrapod_cargo"] ["German_covered_wagon_G10"] = { ["irsignature"] = 0.01, ["detection"] = 0, ["threat"] = 0, }, -- end of ["German_covered_wagon_G10"] ["TugHarlan_drivable"] = { ["detection"] = 0, ["threat"] = 0, }, -- end of ["TugHarlan_drivable"] ["UAZ-469"] = { ["irsignature"] = 0.07, ["detection"] = 0, ["threat"] = 0, }, -- end of ["UAZ-469"] ["CV_59_NS60"] = { }, -- end of ["CV_59_NS60"] ["FPS-117 Dome"] = { ["irsignature"] = 0.07, ["detection"] = 400000, ["threat"] = 0, }, -- end of ["FPS-117 Dome"] ["Building03_PBR"] = { }, -- end of ["Building03_PBR"] ["Mirage-F1DDA"] = { ["irsignature"] = 0.8, }, -- end of ["Mirage-F1DDA"] ["CH-53E"] = { ["irsignature"] = 1, }, -- end of ["CH-53E"] ["Mirage-F1CJ"] = { ["irsignature"] = 0.8, }, -- end of ["Mirage-F1CJ"] ["FW-190A8"] = { ["irsignature"] = 0.1, }, -- end of ["FW-190A8"] ["FuMG-401"] = { ["irsignature"] = 0.05, ["detection"] = 160000, ["threat"] = 0, }, -- end of ["FuMG-401"] ["Ural-4320T"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Ural-4320T"] ["HandyWind"] = { ["irsignature"] = 0.35, ["detection"] = 0, ["threat"] = 0, }, -- end of ["HandyWind"] ["Ka-50"] = { ["irsignature"] = 0.3, }, -- end of ["Ka-50"] ["Tiger_I"] = { ["irsignature"] = 0.105, ["detection"] = 0, ["threat"] = 3000, }, -- end of ["Tiger_I"] ["FireExtinguisher03"] = { }, -- end of ["FireExtinguisher03"] ["Shelter01"] = { }, -- end of ["Shelter01"] ["Kub 1S91 str"] = { ["irsignature"] = 0.085, ["detection"] = 70000, ["threat"] = 0, }, -- end of ["Kub 1S91 str"] ["ALBATROS"] = { ["irsignature"] = 0.35, ["detection"] = 30000, ["threat"] = 16000, }, -- end of ["ALBATROS"] ["M1126 Stryker ICV"] = { ["irsignature"] = 0.085, ["detection"] = 0, ["threat"] = 1200, }, -- end of ["M1126 Stryker ICV"] ["E-3A"] = { ["irsignature"] = 4, }, -- end of ["E-3A"] ["Soldier AK"] = { ["irsignature"] = 0.004, ["detection"] = 0, ["threat"] = 500, }, -- end of ["Soldier AK"] ["SA-18 Igla-S manpad"] = { ["irsignature"] = 0.004, ["detection"] = 5000, ["threat"] = 5200, }, -- end of ["SA-18 Igla-S manpad"] ["Kub 2P25 ln"] = { ["irsignature"] = 0.085, ["detection"] = 0, ["threat"] = 25000, }, -- end of ["Kub 2P25 ln"] ["Cobra"] = { ["irsignature"] = 0.07, ["detection"] = 0, ["threat"] = 1200, }, -- end of ["Cobra"] ["MLRS FDDM"] = { ["irsignature"] = 0.075, ["detection"] = 0, ["threat"] = 1200, }, -- end of ["MLRS FDDM"] ["Mi-8MT"] = { ["irsignature"] = 0.6, }, -- end of ["Mi-8MT"] ["bofors40"] = { ["irsignature"] = 0.01, ["detection"] = 0, ["threat"] = 4000, }, -- end of ["bofors40"] ["ZU-23 Insurgent"] = { ["irsignature"] = 0.006, ["detection"] = 5000, ["threat"] = 2500, }, -- end of ["ZU-23 Insurgent"] ["Ural-4320-31"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Ural-4320-31"] ["Hawk tr"] = { ["irsignature"] = 0.06, ["detection"] = 90000, ["threat"] = 0, }, -- end of ["Hawk tr"] ["TACAN_beacon"] = { ["irsignature"] = 0.005, ["detection"] = 0, ["threat"] = 0, }, -- end of ["TACAN_beacon"] ["SNR_75V"] = { ["irsignature"] = 0.05, ["detection"] = 100000, ["threat"] = 0, }, -- end of ["SNR_75V"] ["Building08_PBR"] = { }, -- end of ["Building08_PBR"] ["Stinger comm"] = { ["irsignature"] = 0.004, ["detection"] = 5000, ["threat"] = 0, }, -- end of ["Stinger comm"] ["IL-76MD"] = { ["irsignature"] = 4, }, -- end of ["IL-76MD"] ["Su-34"] = { ["irsignature"] = 1, }, -- end of ["Su-34"] ["soldier_mauser98"] = { ["irsignature"] = 0.004, ["detection"] = 0, ["threat"] = 500, }, -- end of ["soldier_mauser98"] ["AS32-36A"] = { }, -- end of ["AS32-36A"] ["S-300PS 40B6MD sr_19J6"] = { ["irsignature"] = 0.08, ["detection"] = 150000, ["threat"] = 0, }, -- end of ["S-300PS 40B6MD sr_19J6"] ["MJ-1_02"] = { }, -- end of ["MJ-1_02"] ["M1045 HMMWV TOW"] = { ["irsignature"] = 0.75, ["detection"] = 0, ["threat"] = 3800, }, -- end of ["M1045 HMMWV TOW"] ["Mirage-F1B"] = { ["irsignature"] = 0.8, }, -- end of ["Mirage-F1B"] ["LHD_LHA"] = { }, -- end of ["LHD_LHA"] ["HQ-7_LN_SP"] = { ["irsignature"] = 0.08, ["detection"] = 8000, ["threat"] = 12000, }, -- end of ["HQ-7_LN_SP"] ["SAU Akatsia"] = { ["irsignature"] = 0.095, ["detection"] = 0, ["threat"] = 17000, }, -- end of ["SAU Akatsia"] ["NASAMS_Radar_MPQ64F1"] = { ["irsignature"] = 0.06, ["detection"] = 50000, ["threat"] = 0, }, -- end of ["NASAMS_Radar_MPQ64F1"] ["Haystack 4"] = { }, -- end of ["Haystack 4"] ["HESCO_wallperimeter_5"] = { }, -- end of ["HESCO_wallperimeter_5"] ["F/A-18A"] = { ["irsignature"] = 0.73, }, -- end of ["F/A-18A"] ["Revetment_x8"] = { }, -- end of ["Revetment_x8"] ["Tetrarch"] = { ["irsignature"] = 0.085, ["detection"] = 0, ["threat"] = 2000, }, -- end of ["Tetrarch"] ["MiG-23MLD"] = { ["irsignature"] = 0.69, }, -- end of ["MiG-23MLD"] ["M32-10C_04"] = { }, -- end of ["M32-10C_04"] ["Jagdpanther_G1"] = { ["irsignature"] = 0.1, ["detection"] = 0, ["threat"] = 5000, }, -- end of ["Jagdpanther_G1"] ["Container_10ft"] = { }, -- end of ["Container_10ft"] ["AV8BNA"] = { ["irsignature"] = 0.7, }, -- end of ["AV8BNA"] ["Infantry AK Ins"] = { ["irsignature"] = 0.004, ["detection"] = 0, ["threat"] = 500, }, -- end of ["Infantry AK Ins"] ["Coach cargo"] = { ["irsignature"] = 0, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Coach cargo"] ["M-2000C"] = { ["irsignature"] = 0.8, }, -- end of ["M-2000C"] ["Carrier LSO Personell 5"] = { }, -- end of ["Carrier LSO Personell 5"] ["ZIL-131 KUNG"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 0, }, -- end of ["ZIL-131 KUNG"] ["Container_20ft"] = { }, -- end of ["Container_20ft"] ["ZiL-131 APA-80"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 0, }, -- end of ["ZiL-131 APA-80"] ["BMP-3"] = { ["irsignature"] = 0.095, ["detection"] = 0, ["threat"] = 4000, }, -- end of ["BMP-3"] ["P-47D-30bl1"] = { ["irsignature"] = 0.1, }, -- end of ["P-47D-30bl1"] ["CVN_75"] = { ["irsignature"] = 0.4, ["detection"] = 50000, ["threat"] = 25000, }, -- end of ["CVN_75"] ["UH-1H"] = { ["irsignature"] = 0.2, }, -- end of ["UH-1H"] ["Hummer"] = { ["irsignature"] = 0.075, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Hummer"] ["Dragonteeth 1"] = { }, -- end of ["Dragonteeth 1"] ["AH-64D_BLK_II"] = { ["irsignature"] = 0.2, }, -- end of ["AH-64D_BLK_II"] ["leopard-2A4"] = { ["irsignature"] = 0.12, ["detection"] = 0, ["threat"] = 3500, }, -- end of ["leopard-2A4"] ["RQ-1A Predator"] = { ["irsignature"] = 0.01, }, -- end of ["RQ-1A Predator"] ["USS_Arleigh_Burke_IIa"] = { ["irsignature"] = 0.4, ["detection"] = 150000, ["threat"] = 100000, }, -- end of ["USS_Arleigh_Burke_IIa"] ["Carrier LSO Personell 1"] = { }, -- end of ["Carrier LSO Personell 1"] ["SA342L"] = { ["irsignature"] = 0.2, }, -- end of ["SA342L"] ["Sandbag_02"] = { }, -- end of ["Sandbag_02"] ["Ladder"] = { }, -- end of ["Ladder"] ["f_bar_cargo"] = { }, -- end of ["f_bar_cargo"] ["Tornado GR4"] = { ["irsignature"] = 0.6, }, -- end of ["Tornado GR4"] ["Hawk pcp"] = { ["irsignature"] = 0.05, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Hawk pcp"] ["S-300PS 40B6M tr"] = { ["irsignature"] = 0.08, ["detection"] = 160000, ["threat"] = 0, }, -- end of ["S-300PS 40B6M tr"] ["ZU-23 Emplacement Closed"] = { ["irsignature"] = 0.006, ["detection"] = 5000, ["threat"] = 2500, }, -- end of ["ZU-23 Emplacement Closed"] ["ZIL-135"] = { ["irsignature"] = 0.1, ["detection"] = 0, ["threat"] = 0, }, -- end of ["ZIL-135"] ["OH58D"] = { ["irsignature"] = 0.07, }, -- end of ["OH58D"] ["Bunker"] = { ["irsignature"] = 0.005, ["detection"] = 0, ["threat"] = 800, }, -- end of ["Bunker"] ["Container_office"] = { }, -- end of ["Container_office"] ["GAZ-3308"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 0, }, -- end of ["GAZ-3308"] ["flak41"] = { ["irsignature"] = 0.01, ["detection"] = 0, ["threat"] = 5000, }, -- end of ["flak41"] ["SA-11 Buk LN 9A310M1"] = { ["irsignature"] = 0.095, ["detection"] = 50000, ["threat"] = 35000, }, -- end of ["SA-11 Buk LN 9A310M1"] ["tt_B8M1"] = { ["irsignature"] = 0.08, ["detection"] = 5000, ["threat"] = 5000, }, -- end of ["tt_B8M1"] ["SON_9"] = { ["irsignature"] = 0.05, ["detection"] = 55000, ["threat"] = 0, }, -- end of ["SON_9"] ["ZSU-23-4 Shilka"] = { ["irsignature"] = 0.08, ["detection"] = 5000, ["threat"] = 2500, }, -- end of ["ZSU-23-4 Shilka"] ["Freya_Shelter_Concrete"] = { }, -- end of ["Freya_Shelter_Concrete"] ["Chieftain_mk3"] = { ["irsignature"] = 0.1, ["detection"] = 0, ["threat"] = 3500, }, -- end of ["Chieftain_mk3"] ["BTR-80"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 1600, }, -- end of ["BTR-80"] ["CV_1143_5"] = { ["irsignature"] = 0.45, ["detection"] = 25000, ["threat"] = 12000, }, -- end of ["CV_1143_5"] ["r11_volvo_drivable"] = { ["detection"] = 0, ["threat"] = 0, }, -- end of ["r11_volvo_drivable"] ["AAV7"] = { ["irsignature"] = 0.09, ["detection"] = 0, ["threat"] = 1200, }, -- end of ["AAV7"] ["Ka-27"] = { ["irsignature"] = 0.5, }, -- end of ["Ka-27"] ["KC135MPRS"] = { ["irsignature"] = 4, }, -- end of ["KC135MPRS"] ["FPS-117"] = { ["irsignature"] = 0.07, ["detection"] = 463000, ["threat"] = 0, }, -- end of ["FPS-117"] ["HEMTT_C-RAM_Phalanx"] = { ["irsignature"] = 0.1, ["detection"] = 10000, ["threat"] = 2000, }, -- end of ["HEMTT_C-RAM_Phalanx"] ["Patriot ln"] = { ["irsignature"] = 0.05, ["detection"] = 0, ["threat"] = 100000, }, -- end of ["Patriot ln"] ["H-6J"] = { ["irsignature"] = 2.5, }, -- end of ["H-6J"] ["E-2C"] = { ["irsignature"] = 0.5, }, -- end of ["E-2C"] ["offshore WindTurbine"] = { }, -- end of ["offshore WindTurbine"] ["NASAMS_LN_B"] = { ["irsignature"] = 0.05, ["detection"] = 0, ["threat"] = 15000, }, -- end of ["NASAMS_LN_B"] ["ZU-23 Emplacement"] = { ["irsignature"] = 0.006, ["detection"] = 5000, ["threat"] = 2500, }, -- end of ["ZU-23 Emplacement"] ["tt_ZU-23"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 2500, }, -- end of ["tt_ZU-23"] ["Ural-4320 APA-5D"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Ural-4320 APA-5D"] ["S-300PS 5P85D ln"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 120000, }, -- end of ["S-300PS 5P85D ln"] ["Coach a passenger"] = { ["irsignature"] = 0.05, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Coach a passenger"] ["Dry-cargo ship-1"] = { ["irsignature"] = 0.2, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Dry-cargo ship-1"] ["Mirage-F1C"] = { ["irsignature"] = 0.8, }, -- end of ["Mirage-F1C"] ["Tu-22M3"] = { ["irsignature"] = 4, }, -- end of ["Tu-22M3"] ["Boxcartrinity"] = { ["irsignature"] = 0, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Boxcartrinity"] ["M8_Greyhound"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 2000, }, -- end of ["M8_Greyhound"] ["us carrier tech"] = { }, -- end of ["us carrier tech"] ["Mirage-F1BE"] = { ["irsignature"] = 0.8, }, -- end of ["Mirage-F1BE"] ["M2A1_halftrack"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 1200, }, -- end of ["M2A1_halftrack"] ["DRG_Class_86"] = { ["irsignature"] = 0.1, ["detection"] = 0, ["threat"] = 0, }, -- end of ["DRG_Class_86"] ["MB-339APAN"] = { ["irsignature"] = 0.2, }, -- end of ["MB-339APAN"] ["S-300PS 5H63C 30H6_tr"] = { ["irsignature"] = 0.05, ["detection"] = 120000, ["threat"] = 0, }, -- end of ["S-300PS 5H63C 30H6_tr"] ["p-19 s-125 sr"] = { ["irsignature"] = 0.08, ["detection"] = 160000, ["threat"] = 0, }, -- end of ["p-19 s-125 sr"] ["Carrier LSO Personell 4"] = { }, -- end of ["Carrier LSO Personell 4"] ["Infantry AK"] = { ["irsignature"] = 0.004, ["detection"] = 0, ["threat"] = 500, }, -- end of ["Infantry AK"] ["APFC fuel"] = { }, -- end of ["APFC fuel"] ["T-80UD"] = { ["irsignature"] = 0.1, ["detection"] = 0, ["threat"] = 5000, }, -- end of ["T-80UD"] ["Type_054A"] = { ["detection"] = 160000, ["threat"] = 45000, }, -- end of ["Type_054A"] ["Tu-160"] = { ["irsignature"] = 4, }, -- end of ["Tu-160"] ["Dragonteeth 5"] = { }, -- end of ["Dragonteeth 5"] ["Type_052B"] = { ["detection"] = 100000, ["threat"] = 30000, }, -- end of ["Type_052B"] ["A-10C_2"] = { ["irsignature"] = 0.53, }, -- end of ["A-10C_2"] ["CH-47D"] = { ["irsignature"] = 0.72, }, -- end of ["CH-47D"] ["AH-64A"] = { ["irsignature"] = 0.2, }, -- end of ["AH-64A"] ["Challenger2"] = { ["irsignature"] = 0.11, ["detection"] = 0, ["threat"] = 3500, }, -- end of ["Challenger2"] ["QF_37_AA"] = { ["irsignature"] = 0.01, ["detection"] = 0, ["threat"] = 9000, }, -- end of ["QF_37_AA"] ["WingLoong-I"] = { ["irsignature"] = 0.02, }, -- end of ["WingLoong-I"] ["Patriot EPP"] = { ["irsignature"] = 0.05, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Patriot EPP"] ["S-3B Tanker"] = { ["irsignature"] = 0.53, }, -- end of ["S-3B Tanker"] ["Su-25T"] = { ["irsignature"] = 0.7, }, -- end of ["Su-25T"] ["HESCO_watchtower_2"] = { }, -- end of ["HESCO_watchtower_2"] ["ELNYA"] = { ["irsignature"] = 0.3, ["detection"] = 0, ["threat"] = 0, }, -- end of ["ELNYA"] ["Mirage-F1C-200"] = { ["irsignature"] = 0.8, }, -- end of ["Mirage-F1C-200"] ["Ural-375 PBU"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Ural-375 PBU"] ["Smerch_HE"] = { ["irsignature"] = 0.09, ["detection"] = 0, ["threat"] = 70000, }, -- end of ["Smerch_HE"] ["M6 Linebacker"] = { ["irsignature"] = 0.095, ["detection"] = 8000, ["threat"] = 4500, }, -- end of ["M6 Linebacker"] ["Mirage-F1CT"] = { ["irsignature"] = 0.8, }, -- end of ["Mirage-F1CT"] ["Leopard-2A5"] = { ["irsignature"] = 0.12, ["detection"] = 0, ["threat"] = 3500, }, -- end of ["Leopard-2A5"] ["P-51D"] = { ["irsignature"] = 0.1, }, -- end of ["P-51D"] ["Mirage-F1EH"] = { ["irsignature"] = 0.8, }, -- end of ["Mirage-F1EH"] ["WindTurbine"] = { }, -- end of ["WindTurbine"] ["F-16C bl.52d"] = { ["irsignature"] = 0.6, }, -- end of ["F-16C bl.52d"] ["PT_76"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 2000, }, -- end of ["PT_76"] ["ZWEZDNY"] = { ["irsignature"] = 0.3, ["detection"] = 0, ["threat"] = 0, }, -- end of ["ZWEZDNY"] ["MLRS"] = { ["irsignature"] = 0.09, ["detection"] = 0, ["threat"] = 32000, }, -- end of ["MLRS"] ["Mirage-F1CG"] = { ["irsignature"] = 0.8, }, -- end of ["Mirage-F1CG"] ["Infantry AK ver2"] = { ["irsignature"] = 0.004, ["detection"] = 0, ["threat"] = 500, }, -- end of ["Infantry AK ver2"] ["J-11A"] = { ["irsignature"] = 1, }, -- end of ["J-11A"] ["SpitfireLFMkIX"] = { ["irsignature"] = 0.1, }, -- end of ["SpitfireLFMkIX"] ["Dog Ear radar"] = { ["irsignature"] = 0.08, ["detection"] = 35000, ["threat"] = 0, }, -- end of ["Dog Ear radar"] ["Revetment_x4"] = { }, -- end of ["Revetment_x4"] ["HESCO_post_1"] = { }, -- end of ["HESCO_post_1"] ["P-51D-30-NA"] = { ["irsignature"] = 0.1, }, -- end of ["P-51D-30-NA"] ["M1043 HMMWV Armament"] = { ["irsignature"] = 0.075, ["detection"] = 0, ["threat"] = 1200, }, -- end of ["M1043 HMMWV Armament"] ["S-300PS 40B6MD sr"] = { ["irsignature"] = 0.08, ["detection"] = 60000, ["threat"] = 0, }, -- end of ["S-300PS 40B6MD sr"] ["Mirage-F1EE"] = { ["irsignature"] = 0.8, }, -- end of ["Mirage-F1EE"] ["Barrier A"] = { }, -- end of ["Barrier A"] ["Tu-95MS"] = { ["irsignature"] = 1.1, }, -- end of ["Tu-95MS"] ["HL_B8M1"] = { ["irsignature"] = 0.08, ["detection"] = 5000, ["threat"] = 5000, }, -- end of ["HL_B8M1"] ["KAMAZ Truck"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 0, }, -- end of ["KAMAZ Truck"] ["Toolbox02"] = { }, -- end of ["Toolbox02"] ["Mirage-F1CZ"] = { ["irsignature"] = 0.8, }, -- end of ["Mirage-F1CZ"] ["Carrier LSO Personell"] = { }, -- end of ["Carrier LSO Personell"] ["CV_59_Large_Forklift"] = { }, -- end of ["CV_59_Large_Forklift"] ["Ju-88A4"] = { ["irsignature"] = 0.1, }, -- end of ["Ju-88A4"] ["Silkworm_SR"] = { ["irsignature"] = 0.05, ["detection"] = 200000, ["threat"] = 0, }, -- end of ["Silkworm_SR"] ["NEUSTRASH"] = { ["irsignature"] = 0.35, ["detection"] = 27000, ["threat"] = 12000, }, -- end of ["NEUSTRASH"] ["SKP-11"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 0, }, -- end of ["SKP-11"] ["FarpHide_Dmed"] = { }, -- end of ["FarpHide_Dmed"] ["JagdPz_IV"] = { ["irsignature"] = 0.1, ["detection"] = 0, ["threat"] = 3000, }, -- end of ["JagdPz_IV"] ["USS_Samuel_Chase"] = { ["irsignature"] = 0.4, ["detection"] = 0, ["threat"] = 7000, }, -- end of ["USS_Samuel_Chase"] ["BMP-1"] = { ["irsignature"] = 0.085, ["detection"] = 0, ["threat"] = 3000, }, -- end of ["BMP-1"] ["T-55"] = { ["irsignature"] = 0.1, ["detection"] = 0, ["threat"] = 2500, }, -- end of ["T-55"] ["Su-17M4"] = { ["irsignature"] = 0.69, }, -- end of ["Su-17M4"] ["rapier_fsa_launcher"] = { ["irsignature"] = 0.03, ["detection"] = 30000, ["threat"] = 6800, }, -- end of ["rapier_fsa_launcher"] ["F-4E-45MC"] = { ["irsignature"] = 0.8, }, -- end of ["F-4E-45MC"] ["Log ramps 3"] = { }, -- end of ["Log ramps 3"] ["HESCO_wallperimeter_3"] = { }, -- end of ["HESCO_wallperimeter_3"] ["L118_Unit"] = { ["detection"] = 500, ["threat"] = 17200, }, -- end of ["L118_Unit"] ["DR_50Ton_Flat_Wagon"] = { ["irsignature"] = 0, ["detection"] = 0, ["threat"] = 0, }, -- end of ["DR_50Ton_Flat_Wagon"] ["Su-30"] = { ["irsignature"] = 1, }, -- end of ["Su-30"] ["KILO"] = { ["irsignature"] = 0.2, ["detection"] = 0, ["threat"] = 0, }, -- end of ["KILO"] ["Leopard1A3"] = { ["irsignature"] = 0.1, ["detection"] = 0, ["threat"] = 2500, }, -- end of ["Leopard1A3"] ["M2A1-105"] = { ["irsignature"] = 0.04, ["detection"] = 0, ["threat"] = 11500, }, -- end of ["M2A1-105"] ["Tu-142"] = { ["irsignature"] = 1.1, }, -- end of ["Tu-142"] ["Twall_x6"] = { }, -- end of ["Twall_x6"] ["Gas platform"] = { }, -- end of ["Gas platform"] ["B600_drivable"] = { ["detection"] = 0, ["threat"] = 0, }, -- end of ["B600_drivable"] ["T-72B"] = { ["irsignature"] = 0.1, ["detection"] = 0, ["threat"] = 4000, }, -- end of ["T-72B"] ["NF-2_LightOff02"] = { }, -- end of ["NF-2_LightOff02"] ["AJS37"] = { ["irsignature"] = 0.62, }, -- end of ["AJS37"] ["55G6 EWR"] = { ["irsignature"] = 0.07, ["detection"] = 400000, ["threat"] = 0, }, -- end of ["55G6 EWR"] ["MOLNIYA"] = { ["irsignature"] = 0.35, ["detection"] = 21000, ["threat"] = 2000, }, -- end of ["MOLNIYA"] ["IL-78M"] = { ["irsignature"] = 4, }, -- end of ["IL-78M"] ["MiG-15bis"] = { ["irsignature"] = 0.26, }, -- end of ["MiG-15bis"] ["Log ramps 2"] = { }, -- end of ["Log ramps 2"] ["MJ-1_01"] = { }, -- end of ["MJ-1_01"] ["Haystack 1"] = { }, -- end of ["Haystack 1"] ["ZSU_57_2"] = { ["irsignature"] = 0.1, ["detection"] = 5000, ["threat"] = 7000, }, -- end of ["ZSU_57_2"] ["Invisible FARP"] = { }, -- end of ["Invisible FARP"] ["Uragan_BM-27"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 35800, }, -- end of ["Uragan_BM-27"] ["FarpHide_Dsmall"] = { }, -- end of ["FarpHide_Dsmall"] ["MosquitoFBMkVI"] = { ["irsignature"] = 0.1, }, -- end of ["MosquitoFBMkVI"] ["houseA_arm"] = { ["irsignature"] = 0.007, ["detection"] = 0, ["threat"] = 800, }, -- end of ["houseA_arm"] ["F-14A-135-GR"] = { ["irsignature"] = 0.9, }, -- end of ["F-14A-135-GR"] ["SAU Gvozdika"] = { ["irsignature"] = 0.09, ["detection"] = 0, ["threat"] = 15000, }, -- end of ["SAU Gvozdika"] ["Type_071"] = { ["detection"] = 300000, ["threat"] = 150000, }, -- end of ["Type_071"] ["Sandbag_13"] = { }, -- end of ["Sandbag_13"] ["Mirage-F1BD"] = { ["irsignature"] = 0.8, }, -- end of ["Mirage-F1BD"] ["Tankcartrinity"] = { ["irsignature"] = 0, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Tankcartrinity"] ["2S6 Tunguska"] = { ["irsignature"] = 0.1, ["detection"] = 18000, ["threat"] = 8000, }, -- end of ["2S6 Tunguska"] ["Hawk"] = { ["irsignature"] = 0.62, }, -- end of ["Hawk"] ["Dry-cargo ship-2"] = { ["irsignature"] = 0.3, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Dry-cargo ship-2"] ["FuSe-65"] = { ["irsignature"] = 0.05, ["detection"] = 60000, ["threat"] = 0, }, -- end of ["FuSe-65"] ["tacr2a"] = { ["detection"] = 0, ["threat"] = 0, }, -- end of ["tacr2a"] ["S_75M_Volhov"] = { ["irsignature"] = 0.03, ["detection"] = 0, ["threat"] = 43000, }, -- end of ["S_75M_Volhov"] ["Pz_V_Panther_G"] = { ["irsignature"] = 0.1, ["detection"] = 0, ["threat"] = 3000, }, -- end of ["Pz_V_Panther_G"] ["Camouflage03"] = { }, -- end of ["Camouflage03"] ["FW-190D9"] = { ["irsignature"] = 0.1, }, -- end of ["FW-190D9"] ["Sandbag_16"] = { }, -- end of ["Sandbag_16"] ["Su-27"] = { ["irsignature"] = 1, }, -- end of ["Su-27"] ["Leopard-2"] = { ["irsignature"] = 0.12, ["detection"] = 0, ["threat"] = 3500, }, -- end of ["Leopard-2"] ["generator_5i57"] = { ["irsignature"] = 0.07, ["detection"] = 0, ["threat"] = 0, }, -- end of ["generator_5i57"] ["Sd_Kfz_2"] = { ["irsignature"] = 0.065, ["detection"] = 0, }, -- end of ["Sd_Kfz_2"] ["Strela-10M3"] = { ["irsignature"] = 0.085, ["detection"] = 8000, ["threat"] = 5000, }, -- end of ["Strela-10M3"] ["HESCO_wallperimeter_1"] = { }, -- end of ["HESCO_wallperimeter_1"] ["S-3B"] = { ["irsignature"] = 0.53, }, -- end of ["S-3B"] ["Camouflage06"] = { }, -- end of ["Camouflage06"] ["TZ-22_KrAZ"] = { ["irsignature"] = 0.09, ["detection"] = 0, ["threat"] = 0, }, -- end of ["TZ-22_KrAZ"] ["BTR-82A"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 2000, }, -- end of ["BTR-82A"] ["Paratrooper RPG-16"] = { ["irsignature"] = 0.004, ["detection"] = 0, ["threat"] = 500, }, -- end of ["Paratrooper RPG-16"] ["Willys_MB"] = { ["irsignature"] = 0.07, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Willys_MB"] ["AH-1W"] = { ["irsignature"] = 0.2, }, -- end of ["AH-1W"] ["Sd_Kfz_251"] = { ["irsignature"] = 0.075, ["detection"] = 0, ["threat"] = 1100, }, -- end of ["Sd_Kfz_251"] ["ElevatedPlatform_down"] = { }, -- end of ["ElevatedPlatform_down"] ["Container_watchtower"] = { }, -- end of ["Container_watchtower"] ["Carrier Airboss"] = { }, -- end of ["Carrier Airboss"] ["LeFH_18-40-105"] = { ["irsignature"] = 0.04, ["detection"] = 0, ["threat"] = 10500, }, -- end of ["LeFH_18-40-105"] ["ammo_cargo"] = { }, -- end of ["ammo_cargo"] ["Tower Crane"] = { }, -- end of ["Tower Crane"] ["Oil rig"] = { }, -- end of ["Oil rig"] ["Tent05"] = { }, -- end of ["Tent05"] ["Trolley bus"] = { ["irsignature"] = 0.06, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Trolley bus"] ["Su-25"] = { ["irsignature"] = 0.7, }, -- end of ["Su-25"] ["FARP_SINGLE_01"] = { }, -- end of ["FARP_SINGLE_01"] ["offshore WindTurbine2"] = { }, -- end of ["offshore WindTurbine2"] ["Cargo01"] = { }, -- end of ["Cargo01"] ["tt_KORD"] = { ["irsignature"] = 0.08, ["detection"] = 5000, ["threat"] = 1200, }, -- end of ["tt_KORD"] ["Orca"] = { }, -- end of ["Orca"] ["Tigr_233036"] = { ["irsignature"] = 0.075, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Tigr_233036"] ["MAZ-6303"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 0, }, -- end of ["MAZ-6303"] ["M48 Chaparral"] = { ["irsignature"] = 0.085, ["detection"] = 10000, ["threat"] = 8500, }, -- end of ["M48 Chaparral"] ["1L13 EWR"] = { ["irsignature"] = 0.07, ["detection"] = 300000, ["threat"] = 0, }, -- end of ["1L13 EWR"] ["BDK-775"] = { ["irsignature"] = 0.35, ["detection"] = 25000, ["threat"] = 6000, }, -- end of ["BDK-775"] ["fueltank_cargo"] = { }, -- end of ["fueltank_cargo"] ["Haystack 3"] = { }, -- end of ["Haystack 3"] ["SA-18 Igla manpad"] = { ["irsignature"] = 0.004, ["detection"] = 5000, ["threat"] = 5200, }, -- end of ["SA-18 Igla manpad"] ["SA342M"] = { ["irsignature"] = 0.2, }, -- end of ["SA342M"] ["S-300PS 5P85C ln"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 120000, }, -- end of ["S-300PS 5P85C ln"] ["Ka-50_3"] = { ["irsignature"] = 0.6, }, -- end of ["Ka-50_3"] ["Type_052C"] = { ["detection"] = 260000, ["threat"] = 100000, }, -- end of ["Type_052C"] ["Czech hedgehogs 1"] = { }, -- end of ["Czech hedgehogs 1"] ["trunks_long_cargo"] = { }, -- end of ["trunks_long_cargo"] ["Sandbag_12"] = { }, -- end of ["Sandbag_12"] ["Cargo02"] = { }, -- end of ["Cargo02"] ["Hemmkurvenhindernis"] = { }, -- end of ["Hemmkurvenhindernis"] ["Sandbag_04"] = { }, -- end of ["Sandbag_04"] ["leander-gun-condell"] = { ["detection"] = 150000, ["threat"] = 100000, }, -- end of ["leander-gun-condell"] ["Soldier M4"] = { ["irsignature"] = 0.004, ["detection"] = 0, ["threat"] = 500, }, -- end of ["Soldier M4"] ["FA-18C_hornet"] = { ["irsignature"] = 0.75, }, -- end of ["FA-18C_hornet"] ["AS32-32A"] = { }, -- end of ["AS32-32A"] ["GAZ-3307"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 0, }, -- end of ["GAZ-3307"] ["RD_75"] = { ["irsignature"] = 0.05, ["detection"] = 100000, ["threat"] = 0, }, -- end of ["RD_75"] ["Yak-52"] = { ["irsignature"] = 0.1, }, -- end of ["Yak-52"] ["container_20ft"] = { }, -- end of ["container_20ft"] ["billboard_motorized"] = { }, -- end of ["billboard_motorized"] ["2B11 mortar"] = { ["irsignature"] = 0.005, ["detection"] = 0, ["threat"] = 7000, }, -- end of ["2B11 mortar"] ["FarpHide_small"] = { }, -- end of ["FarpHide_small"] ["Soldier stinger"] = { ["irsignature"] = 0.004, ["detection"] = 5000, ["threat"] = 4500, }, -- end of ["Soldier stinger"] ["Log posts 2"] = { }, -- end of ["Log posts 2"] ["Cow"] = { }, -- end of ["Cow"] ["LARC-V"] = { ["detection"] = 500, ["threat"] = 0, }, -- end of ["LARC-V"] ["P-47D-30"] = { ["irsignature"] = 0.1, }, -- end of ["P-47D-30"] ["SA-18 Igla-S comm"] = { ["irsignature"] = 0.004, ["detection"] = 5000, ["threat"] = 0, }, -- end of ["SA-18 Igla-S comm"] ["RPC_5N62V"] = { ["detection"] = 400000, ["threat"] = 0, }, -- end of ["RPC_5N62V"] ["Dragonteeth 4"] = { }, -- end of ["Dragonteeth 4"] ["P-47D-40"] = { ["irsignature"] = 0.1, }, -- end of ["P-47D-40"] ["HL_ZU-23"] = { ["irsignature"] = 0.08, ["detection"] = 5000, ["threat"] = 2500, }, -- end of ["HL_ZU-23"] ["I-16"] = { ["irsignature"] = 0.1, }, -- end of ["I-16"] ["BMD-1"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 3000, }, -- end of ["BMD-1"] ["Mirage-F1CH"] = { ["irsignature"] = 0.8, }, -- end of ["Mirage-F1CH"] ["Camouflage07"] = { }, -- end of ["Camouflage07"] ["S_75_ZIL"] = { ["irsignature"] = 0.1, ["detection"] = 0, ["threat"] = 0, }, -- end of ["S_75_ZIL"] ["F-14B"] = { ["irsignature"] = 0.9, }, -- end of ["F-14B"] ["M30_CC"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 1200, }, -- end of ["M30_CC"] ["Belgian gate"] = { }, -- end of ["Belgian gate"] ["C-130"] = { ["irsignature"] = 1, }, -- end of ["C-130"] ["Carrier Seaman"] = { }, -- end of ["Carrier Seaman"] ["Mirage-F1JA"] = { ["irsignature"] = 0.8, }, -- end of ["Mirage-F1JA"] ["GAZ-66"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 0, }, -- end of ["GAZ-66"] ["Locomotive"] = { ["irsignature"] = 0.15, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Locomotive"] ["F-14A"] = { ["irsignature"] = 0.97, }, -- end of ["F-14A"] ["HL_DSHK"] = { ["irsignature"] = 0.08, ["detection"] = 5000, ["threat"] = 1200, }, -- end of ["HL_DSHK"] ["CV_59_H60"] = { }, -- end of ["CV_59_H60"] ["L-39C"] = { ["irsignature"] = 0.2, }, -- end of ["L-39C"] ["S-300PS 64H6E sr"] = { ["irsignature"] = 0.08, ["detection"] = 160000, ["threat"] = 0, }, -- end of ["S-300PS 64H6E sr"] ["CVN_73"] = { ["irsignature"] = 0.4, ["detection"] = 50000, ["threat"] = 25000, }, -- end of ["CVN_73"] ["HQ-7_STR_SP"] = { ["irsignature"] = 0.08, ["detection"] = 30000, ["threat"] = 0, }, -- end of ["HQ-7_STR_SP"] ["Mi-24P"] = { ["irsignature"] = 0.5, }, -- end of ["Mi-24P"] ["SOM"] = { ["irsignature"] = 0.2, ["detection"] = 0, ["threat"] = 0, }, -- end of ["SOM"] ["TugHarlan"] = { }, -- end of ["TugHarlan"] ["Yak-40"] = { ["irsignature"] = 0.5, }, -- end of ["Yak-40"] ["Igla manpad INS"] = { ["irsignature"] = 0.004, ["detection"] = 5000, ["threat"] = 5200, }, -- end of ["Igla manpad INS"] ["Sandbag_05"] = { }, -- end of ["Sandbag_05"] ["HESCO_wallperimeter_2"] = { }, -- end of ["HESCO_wallperimeter_2"] ["soldier_wwii_br_01"] = { ["irsignature"] = 0.004, ["detection"] = 0, ["threat"] = 500, }, -- end of ["soldier_wwii_br_01"] ["Hawk sr"] = { ["irsignature"] = 0.06, ["detection"] = 90000, ["threat"] = 0, }, -- end of ["Hawk sr"] ["v1_launcher"] = { ["irsignature"] = 0.01, ["detection"] = 0, ["threat"] = 0, }, -- end of ["v1_launcher"] ["Paratrooper AKS-74"] = { ["irsignature"] = 0.004, ["detection"] = 0, ["threat"] = 500, }, -- end of ["Paratrooper AKS-74"] ["MOSCOW"] = { ["irsignature"] = 0.4, ["detection"] = 160000, ["threat"] = 75000, }, -- end of ["MOSCOW"] ["Carrier LSO Personell 3"] = { }, -- end of ["Carrier LSO Personell 3"] ["Mi-26"] = { ["irsignature"] = 1, }, -- end of ["Mi-26"] ["Pile of Woods"] = { }, -- end of ["Pile of Woods"] ["outpost"] = { ["irsignature"] = 0.007, ["detection"] = 0, ["threat"] = 800, }, -- end of ["outpost"] ["JF-17"] = { ["irsignature"] = 0.6, }, -- end of ["JF-17"] ["KUZNECOW"] = { ["irsignature"] = 0.45, ["detection"] = 25000, ["threat"] = 12000, }, -- end of ["KUZNECOW"] ["Building07_PBR"] = { }, -- end of ["Building07_PBR"] ["Ural-375"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Ural-375"] ["Osa 9A33 ln"] = { ["irsignature"] = 0.08, ["detection"] = 30000, ["threat"] = 10300, }, -- end of ["Osa 9A33 ln"] ["REZKY"] = { ["irsignature"] = 0.35, ["detection"] = 30000, ["threat"] = 16000, }, -- end of ["REZKY"] ["Cargo04"] = { }, -- end of ["Cargo04"] ["Oil Barrel"] = { }, -- end of ["Oil Barrel"] ["Bedford_MWD"] = { ["irsignature"] = 0.07, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Bedford_MWD"] ["Sandbag_10"] = { }, -- end of ["Sandbag_10"] ["OH-58D"] = { ["irsignature"] = 0.2, }, -- end of ["OH-58D"] ["l118"] = { }, -- end of ["l118"] ["pipes_big_cargo"] = { }, -- end of ["pipes_big_cargo"] ["B-17G"] = { ["irsignature"] = 0.1, }, -- end of ["B-17G"] ["Land_Rover_109_S3"] = { ["irsignature"] = 0.075, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Land_Rover_109_S3"] ["M32-10C_02"] = { }, -- end of ["M32-10C_02"] ["C-101CC"] = { ["irsignature"] = 0.2, }, -- end of ["C-101CC"] ["Smerch"] = { ["irsignature"] = 0.09, ["detection"] = 0, ["threat"] = 70000, }, -- end of ["Smerch"] ["SAU 2-C9"] = { ["irsignature"] = 0.09, ["detection"] = 0, ["threat"] = 7000, }, -- end of ["SAU 2-C9"] ["M32-10C_01"] = { }, -- end of ["M32-10C_01"] ["fire_control"] = { ["irsignature"] = 0.01, ["detection"] = 0, ["threat"] = 1100, }, -- end of ["fire_control"] ["M32-10C_03"] = { }, -- end of ["M32-10C_03"] ["container_cargo"] = { }, -- end of ["container_cargo"] ["Type_093"] = { ["irsignature"] = 0.2, ["detection"] = 40000, ["threat"] = 40000, }, -- end of ["Type_093"] ["TICONDEROG"] = { ["irsignature"] = 0.4, ["detection"] = 150000, ["threat"] = 100000, }, -- end of ["TICONDEROG"] ["FireExtinguisher02"] = { }, -- end of ["FireExtinguisher02"] ["FPS-117 ECS"] = { ["irsignature"] = 0.05, ["detection"] = 0, ["threat"] = 0, }, -- end of ["FPS-117 ECS"] ["ara_vdm"] = { ["detection"] = 18000, ["threat"] = 5000, }, -- end of ["ara_vdm"] ["HESCO_watchtower_1"] = { }, -- end of ["HESCO_watchtower_1"] ["AA8"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 0, }, -- end of ["AA8"] ["MCV-80"] = { ["irsignature"] = 0.09, ["detection"] = 0, ["threat"] = 2500, }, -- end of ["MCV-80"] ["Building06_PBR"] = { }, -- end of ["Building06_PBR"] ["Log ramps 1"] = { }, -- end of ["Log ramps 1"] ["KJ-2000"] = { ["irsignature"] = 4, }, -- end of ["KJ-2000"] ["HESCO_generator"] = { }, -- end of ["HESCO_generator"] ["Sandbag_17"] = { }, -- end of ["Sandbag_17"] ["barrels_cargo"] = { }, -- end of ["barrels_cargo"] ["M4_Sherman"] = { ["irsignature"] = 0.1, ["detection"] = 0, ["threat"] = 3000, }, -- end of ["M4_Sherman"] ["ES44AH"] = { ["irsignature"] = 0.15, ["detection"] = 0, ["threat"] = 0, }, -- end of ["ES44AH"] ["345 Excavator"] = { }, -- end of ["345 Excavator"] ["Stanley_LightHouse"] = { }, -- end of ["Stanley_LightHouse"] ["ATZ-60_Maz"] = { ["irsignature"] = 0.1, ["detection"] = 0, ["threat"] = 0, }, -- end of ["ATZ-60_Maz"] ["Electric locomotive"] = { ["irsignature"] = 0.1, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Electric locomotive"] ["FireExtinguisher01"] = { }, -- end of ["FireExtinguisher01"] ["SA342Minigun"] = { ["irsignature"] = 0.2, }, -- end of ["SA342Minigun"] ["Camouflage02"] = { }, -- end of ["Camouflage02"] ["Wespe124"] = { ["irsignature"] = 0.07, ["detection"] = 0, ["threat"] = 10500, }, -- end of ["Wespe124"] ["M12_GMC"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 18300, }, -- end of ["M12_GMC"] ["m117_cargo"] = { }, -- end of ["m117_cargo"] ["VAZ Car"] = { ["irsignature"] = 0.065, ["detection"] = 0, ["threat"] = 0, }, -- end of ["VAZ Car"] ["Toolbox01"] = { }, -- end of ["Toolbox01"] ["ElevatedPlatform_up"] = { }, -- end of ["ElevatedPlatform_up"] ["Predator TrojanSpirit"] = { ["irsignature"] = 0.05, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Predator TrojanSpirit"] ["Mirage-F1EDA"] = { ["irsignature"] = 0.8, }, -- end of ["Mirage-F1EDA"] ["ZIL-4331"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 0, }, -- end of ["ZIL-4331"] ["AH-64D"] = { ["irsignature"] = 0.2, }, -- end of ["AH-64D"] ["Dragonteeth 3"] = { }, -- end of ["Dragonteeth 3"] ["P20_drivable"] = { ["detection"] = 0, ["threat"] = 0, }, -- end of ["P20_drivable"] ["Mirage-F1ED"] = { ["irsignature"] = 0.8, }, -- end of ["Mirage-F1ED"] ["F-117A"] = { ["irsignature"] = 0.15, }, -- end of ["F-117A"] ["Cone01"] = { }, -- end of ["Cone01"] ["M10_GMC"] = { ["irsignature"] = 0.09, ["detection"] = 0, ["threat"] = 6000, }, -- end of ["M10_GMC"] ["Suidae"] = { ["irsignature"] = 0.004, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Suidae"] ["flak36"] = { ["irsignature"] = 0.01, ["detection"] = 0, ["threat"] = 5000, }, -- end of ["flak36"] ["RLS_19J6"] = { ["irsignature"] = 0.08, ["detection"] = 150000, ["threat"] = 0, }, -- end of ["RLS_19J6"] ["Tornado IDS"] = { ["irsignature"] = 0.6, }, -- end of ["Tornado IDS"] ["rapier_fsa_blindfire_radar"] = { ["irsignature"] = 0.03, ["detection"] = 30000, ["threat"] = 0, }, -- end of ["rapier_fsa_blindfire_radar"] ["SH-60B"] = { ["irsignature"] = 0.35, }, -- end of ["SH-60B"] ["ZBD04A"] = { ["irsignature"] = 0.09, ["detection"] = 0, ["threat"] = 4800, }, -- end of ["ZBD04A"] ["KS-19"] = { ["irsignature"] = 0.01, ["detection"] = 0, ["threat"] = 20000, }, -- end of ["KS-19"] ["Strela-1 9P31"] = { ["irsignature"] = 0.08, ["detection"] = 5000, ["threat"] = 4200, }, -- end of ["Strela-1 9P31"] ["us carrier shooter"] = { }, -- end of ["us carrier shooter"] ["M1128 Stryker MGS"] = { ["irsignature"] = 0.085, ["detection"] = 0, ["threat"] = 4000, }, -- end of ["M1128 Stryker MGS"] ["leander-gun-lynch"] = { ["detection"] = 180000, ["threat"] = 140000, }, -- end of ["leander-gun-lynch"] ["MiG-25RBT"] = { ["irsignature"] = 4, }, -- end of ["MiG-25RBT"] ["F-86F Sabre"] = { ["irsignature"] = 0.25, }, -- end of ["F-86F Sabre"] ["C-17A"] = { ["irsignature"] = 3, }, -- end of ["C-17A"] ["CVN_71"] = { ["irsignature"] = 0.4, ["detection"] = 50000, ["threat"] = 25000, }, -- end of ["CVN_71"] ["VINSON"] = { ["irsignature"] = 0.4, ["detection"] = 30000, ["threat"] = 15000, }, -- end of ["VINSON"] ["Tent03"] = { }, -- end of ["Tent03"] ["BoomBarrier_closed"] = { }, -- end of ["BoomBarrier_closed"] ["Sandbag_06"] = { }, -- end of ["Sandbag_06"] ["Mirage-F1EQ"] = { ["irsignature"] = 0.8, }, -- end of ["Mirage-F1EQ"] ["Forrestal"] = { ["detection"] = 50000, ["threat"] = 25000, }, -- end of ["Forrestal"] ["PERRY"] = { ["irsignature"] = 0.4, ["detection"] = 150000, ["threat"] = 100000, }, -- end of ["PERRY"] ["MiG-19P"] = { ["irsignature"] = 0.34, }, -- end of ["MiG-19P"] ["MiG-25PD"] = { ["irsignature"] = 4, }, -- end of ["MiG-25PD"] ["Building05_PBR"] = { }, -- end of ["Building05_PBR"] ["TPZ"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 1000, }, -- end of ["TPZ"] ["C-101EB"] = { ["irsignature"] = 0.2, }, -- end of ["C-101EB"] ["An-30M"] = { ["irsignature"] = 0.5, }, -- end of ["An-30M"] ["S-200_Launcher"] = { ["detection"] = 0, ["threat"] = 255000, }, -- end of ["S-200_Launcher"] ["Mirage-F1M-EE"] = { ["irsignature"] = 0.8, }, -- end of ["Mirage-F1M-EE"] ["Mirage-F1CR"] = { ["irsignature"] = 0.8, }, -- end of ["Mirage-F1CR"] ["Cargo03"] = { }, -- end of ["Cargo03"] ["Barrier D"] = { }, -- end of ["Barrier D"] ["Barrier C"] = { }, -- end of ["Barrier C"] ["Barrier B"] = { }, -- end of ["Barrier B"] ["Fire Control Bunker"] = { }, -- end of ["Fire Control Bunker"] ["rapier_fsa_optical_tracker_unit"] = { ["irsignature"] = 0.03, ["detection"] = 20000, ["threat"] = 0, }, -- end of ["rapier_fsa_optical_tracker_unit"] ["MJ-1_drivable"] = { ["detection"] = 0, ["threat"] = 0, }, -- end of ["MJ-1_drivable"] ["La_Combattante_II"] = { ["irsignature"] = 0.35, ["detection"] = 19000, ["threat"] = 4000, }, -- end of ["La_Combattante_II"] ["KC-135"] = { ["irsignature"] = 4, }, -- end of ["KC-135"] ["MQ-9 Reaper"] = { ["irsignature"] = 0.1, }, -- end of ["MQ-9 Reaper"] ["Camouflage01"] = { }, -- end of ["Camouflage01"] ["LHA_Tarawa"] = { ["detection"] = 150000, ["threat"] = 20000, }, -- end of ["LHA_Tarawa"] ["Scud_B"] = { ["irsignature"] = 0.09, ["detection"] = 0, ["threat"] = 285000, }, -- end of ["Scud_B"] ["NASAMS_LN_C"] = { ["irsignature"] = 0.05, ["detection"] = 0, ["threat"] = 15000, }, -- end of ["NASAMS_LN_C"] ["B600"] = { }, -- end of ["B600"] ["CCKW_353"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 0, }, -- end of ["CCKW_353"] ["SA-18 Igla comm"] = { ["irsignature"] = 0.004, ["detection"] = 5000, ["threat"] = 0, }, -- end of ["SA-18 Igla comm"] ["T-90"] = { ["irsignature"] = 0.1, ["detection"] = 0, ["threat"] = 5000, }, -- end of ["T-90"] ["Patriot ECS"] = { ["irsignature"] = 0.05, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Patriot ECS"] ["A-10A"] = { ["irsignature"] = 0.53, }, -- end of ["A-10A"] ["NF-2_LightOn"] = { }, -- end of ["NF-2_LightOn"] ["Pak40"] = { ["irsignature"] = 0.04, ["detection"] = 0, ["threat"] = 3000, }, -- end of ["Pak40"] ["Coach cargo open"] = { ["irsignature"] = 0, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Coach cargo open"] ["IKARUS Bus"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 0, }, -- end of ["IKARUS Bus"] ["house1arm"] = { ["irsignature"] = 0.007, ["detection"] = 0, ["threat"] = 800, }, -- end of ["house1arm"] ["Hawk ln"] = { ["irsignature"] = 0.05, ["detection"] = 0, ["threat"] = 45000, }, -- end of ["Hawk ln"] ["SA Ski Ramp"] = { }, -- end of ["SA Ski Ramp"] ["Mi-28N"] = { ["irsignature"] = 0.3, }, -- end of ["Mi-28N"] ["Hawk cwar"] = { ["irsignature"] = 0.05, ["detection"] = 70000, ["threat"] = 0, }, -- end of ["Hawk cwar"] ["F-16C bl.50"] = { ["irsignature"] = 0.6, }, -- end of ["F-16C bl.50"] ["SA-11 Buk SR 9S18M1"] = { ["irsignature"] = 0.095, ["detection"] = 100000, ["threat"] = 0, }, -- end of ["SA-11 Buk SR 9S18M1"] ["Sd_Kfz_234_2_Puma"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 2000, }, -- end of ["Sd_Kfz_234_2_Puma"] ["Schnellboot_type_S130"] = { ["irsignature"] = 0.3, ["detection"] = 10000, ["threat"] = 4000, }, -- end of ["Schnellboot_type_S130"] ["AS32-31A"] = { }, -- end of ["AS32-31A"] ["M1097 Avenger"] = { ["irsignature"] = 0.075, ["detection"] = 5200, ["threat"] = 4500, }, -- end of ["M1097 Avenger"] ["flak38"] = { ["irsignature"] = 0.01, ["detection"] = 0, ["threat"] = 2500, }, -- end of ["flak38"] ["WindTurbine_11"] = { }, -- end of ["WindTurbine_11"] ["M4_Tractor"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 1200, }, -- end of ["M4_Tractor"] ["JTAC"] = { ["irsignature"] = 0.004, ["detection"] = 0, ["threat"] = 0, }, -- end of ["JTAC"] ["Mirage-F1M-CE"] = { ["irsignature"] = 0.8, }, -- end of ["Mirage-F1M-CE"] ["M1_37mm"] = { ["irsignature"] = 0.01, ["detection"] = 0, ["threat"] = 5700, }, -- end of ["M1_37mm"] ["KrAZ6322"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 0, }, -- end of ["KrAZ6322"] ["Stinger comm dsr"] = { ["irsignature"] = 0.004, ["detection"] = 5000, ["threat"] = 0, }, -- end of ["Stinger comm dsr"] ["Higgins_boat"] = { ["irsignature"] = 0.08, ["detection"] = 3000, ["threat"] = 1000, }, -- end of ["Higgins_boat"] ["Su-24MR"] = { ["irsignature"] = 1.5, }, -- end of ["Su-24MR"] ["Jerrycan"] = { }, -- end of ["Jerrycan"] ["warning_board_b"] = { }, -- end of ["warning_board_b"] ["ATMZ-5"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 0, }, -- end of ["ATMZ-5"] ["leander-gun-andromeda"] = { ["detection"] = 180000, ["threat"] = 140000, }, -- end of ["leander-gun-andromeda"] ["SA342Mistral"] = { ["irsignature"] = 0.2, }, -- end of ["SA342Mistral"] ["SA-11 Buk CC 9S470M1"] = { ["irsignature"] = 0.095, ["detection"] = 0, ["threat"] = 0, }, -- end of ["SA-11 Buk CC 9S470M1"] ["German_tank_wagon"] = { ["irsignature"] = 0.01, ["detection"] = 0, ["threat"] = 0, }, -- end of ["German_tank_wagon"] ["Soldier M4 GRG"] = { ["irsignature"] = 0.004, ["detection"] = 0, ["threat"] = 500, }, -- end of ["Soldier M4 GRG"] ["Mi-24V"] = { ["irsignature"] = 0.5, }, -- end of ["Mi-24V"] ["MiG-31"] = { ["irsignature"] = 3, }, -- end of ["MiG-31"] ["leander-gun-achilles"] = { ["detection"] = 180000, ["threat"] = 8000, }, -- end of ["leander-gun-achilles"] ["M-113"] = { ["irsignature"] = 0.09, ["detection"] = 0, ["threat"] = 1200, }, -- end of ["M-113"] ["MB-339A"] = { ["irsignature"] = 0.2, }, -- end of ["MB-339A"] ["Soldier M249"] = { ["irsignature"] = 0.004, ["detection"] = 0, ["threat"] = 700, }, -- end of ["Soldier M249"] ["Mirage 2000-5"] = { ["irsignature"] = 0.8, }, -- end of ["Mirage 2000-5"] ["Tent04"] = { }, -- end of ["Tent04"] ["flak18"] = { ["irsignature"] = 0.01, ["detection"] = 0, ["threat"] = 5000, }, -- end of ["flak18"] ["Building02_PBR"] = { }, -- end of ["Building02_PBR"] ["MiG-21Bis"] = { ["irsignature"] = 0.6, }, -- end of ["MiG-21Bis"] ["Roland Radar"] = { ["irsignature"] = 0.085, ["detection"] = 35000, ["threat"] = 0, }, -- end of ["Roland Radar"] ["MiG-29S"] = { ["irsignature"] = 0.77, }, -- end of ["MiG-29S"] ["Ural ATsP-6"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Ural ATsP-6"] ["Su-33"] = { ["irsignature"] = 1, }, -- end of ["Su-33"] ["Coach a tank blue"] = { ["irsignature"] = 0, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Coach a tank blue"] ["Centaur_IV"] = { ["irsignature"] = 0.1, ["detection"] = 0, ["threat"] = 6000, }, -- end of ["Centaur_IV"] ["M4A4_Sherman_FF"] = { ["irsignature"] = 0.1, ["detection"] = 0, ["threat"] = 3000, }, -- end of ["M4A4_Sherman_FF"] ["Christen Eagle II"] = { ["irsignature"] = 0.04, }, -- end of ["Christen Eagle II"] ["Sandbag_03"] = { }, -- end of ["Sandbag_03"] ["BTR_D"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 3000, }, -- end of ["BTR_D"] ["Nodding_Donkey_Pump"] = { }, -- end of ["Nodding_Donkey_Pump"] ["AM32a-60_01"] = { }, -- end of ["AM32a-60_01"] ["AM32a-60_02"] = { }, -- end of ["AM32a-60_02"] ["Kubelwagen_82"] = { ["irsignature"] = 0.065, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Kubelwagen_82"] ["flak37"] = { ["irsignature"] = 0.01, ["detection"] = 0, ["threat"] = 5000, }, -- end of ["flak37"] ["Twall_x1"] = { }, -- end of ["Twall_x1"] ["Sandbag_15"] = { }, -- end of ["Sandbag_15"] ["M-2 Bradley"] = { ["irsignature"] = 0.095, ["detection"] = 0, ["threat"] = 3800, }, -- end of ["M-2 Bradley"] ["M-1 Abrams"] = { ["irsignature"] = 0.15, ["detection"] = 0, ["threat"] = 3500, }, -- end of ["M-1 Abrams"] ["Cromwell_IV"] = { ["irsignature"] = 0.1, ["detection"] = 0, ["threat"] = 3000, }, -- end of ["Cromwell_IV"] ["F-16C_50"] = { ["irsignature"] = 0.6, }, -- end of ["F-16C_50"] ["PIOTR"] = { ["irsignature"] = 0.4, ["detection"] = 250000, ["threat"] = 190000, }, -- end of ["PIOTR"] ["Wellcarnsc"] = { ["irsignature"] = 0, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Wellcarnsc"] ["Building01_PBR"] = { }, -- end of ["Building01_PBR"] ["Cargo05"] = { }, -- end of ["Cargo05"] ["Sandbag_01"] = { }, -- end of ["Sandbag_01"] ["Camouflage04"] = { }, -- end of ["Camouflage04"] ["SH-3W"] = { ["irsignature"] = 0.72, }, -- end of ["SH-3W"] ["F-15ESE"] = { ["irsignature"] = 0.91, }, -- end of ["F-15ESE"] ["SpitfireLFMkIXCW"] = { ["irsignature"] = 0.1, }, -- end of ["SpitfireLFMkIXCW"] ["S-300PS 54K6 cp"] = { ["irsignature"] = 0.05, ["detection"] = 0, ["threat"] = 0, }, -- end of ["S-300PS 54K6 cp"] ["hms_invincible"] = { ["detection"] = 100000, ["threat"] = 74000, }, -- end of ["hms_invincible"] ["Tor 9A331"] = { ["irsignature"] = 0.1, ["detection"] = 25000, ["threat"] = 12000, }, -- end of ["Tor 9A331"] ["Sandbox"] = { ["irsignature"] = 0, ["detection"] = 0, ["threat"] = 800, }, -- end of ["Sandbox"] ["IMPROVED_KILO"] = { ["irsignature"] = 0.2, ["detection"] = 0, ["threat"] = 0, }, -- end of ["IMPROVED_KILO"] ["Flakscheinwerfer_37"] = { ["irsignature"] = 0.3, ["detection"] = 15000, ["threat"] = 15000, }, -- end of ["Flakscheinwerfer_37"] ["T-72B3"] = { ["irsignature"] = 0.105, ["detection"] = 0, ["threat"] = 4000, }, -- end of ["T-72B3"] ["Ural-375 ZU-23"] = { ["irsignature"] = 0.08, ["detection"] = 5000, ["threat"] = 2500, }, -- end of ["Ural-375 ZU-23"] ["KDO_Mod40"] = { ["irsignature"] = 0.03, ["detection"] = 30000, ["threat"] = 0, }, -- end of ["KDO_Mod40"] ["Patriot str"] = { ["irsignature"] = 0.07, ["detection"] = 160000, ["threat"] = 0, }, -- end of ["Patriot str"] ["F-5E-3"] = { ["irsignature"] = 0.4, }, -- end of ["F-5E-3"] ["HEMTT TFFT"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 0, }, -- end of ["HEMTT TFFT"] ["Cone02"] = { }, -- end of ["Cone02"] ["M-60"] = { ["irsignature"] = 0.1, ["detection"] = 0, ["threat"] = 8000, }, -- end of ["M-60"] ["Stennis"] = { ["irsignature"] = 0.4, ["detection"] = 50000, ["threat"] = 25000, }, -- end of ["Stennis"] ["Shelter02"] = { }, -- end of ["Shelter02"] ["Mirage-F1CK"] = { ["irsignature"] = 0.8, }, -- end of ["Mirage-F1CK"] ["F-15C"] = { ["irsignature"] = 0.85, }, -- end of ["F-15C"] ["Su-25TM"] = { ["irsignature"] = 0.7, }, -- end of ["Su-25TM"] ["iso_container_small"] = { }, -- end of ["iso_container_small"] ["Container_generator"] = { }, -- end of ["Container_generator"] ["Maschinensatz_33"] = { ["irsignature"] = 0.07, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Maschinensatz_33"] ["TF-51D"] = { ["irsignature"] = 0.1, }, -- end of ["TF-51D"] ["Leclerc"] = { ["irsignature"] = 0.12, ["detection"] = 0, ["threat"] = 3500, }, -- end of ["Leclerc"] ["Blitz_36-6700A"] = { ["irsignature"] = 0.75, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Blitz_36-6700A"] ["NASAMS_Command_Post"] = { ["irsignature"] = 0.05, ["detection"] = 0, ["threat"] = 0, }, -- end of ["NASAMS_Command_Post"] ["Infantry AK ver3"] = { ["irsignature"] = 0.004, ["detection"] = 0, ["threat"] = 500, }, -- end of ["Infantry AK ver3"] ["MiG-27K"] = { ["irsignature"] = 0.69, }, -- end of ["MiG-27K"] ["Camouflage05"] = { }, -- end of ["Camouflage05"] ["Log posts 3"] = { }, -- end of ["Log posts 3"] ["Coach a platform"] = { ["irsignature"] = 0, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Coach a platform"] ["iso_container"] = { }, -- end of ["iso_container"] ["A-10C"] = { ["irsignature"] = 0.53, }, -- end of ["A-10C"] ["Small_LightHouse"] = { }, -- end of ["Small_LightHouse"] ["speedboat"] = { ["irsignature"] = 0.05, ["detection"] = 5000, ["threat"] = 1000, }, -- end of ["speedboat"] ["Su-24M"] = { ["irsignature"] = 1.5, }, -- end of ["Su-24M"] ["HESCO_wallperimeter_4"] = { }, -- end of ["HESCO_wallperimeter_4"] ["HL_KORD"] = { ["irsignature"] = 0.08, ["detection"] = 5000, ["threat"] = 1200, }, -- end of ["HL_KORD"] ["MiG-29A"] = { ["irsignature"] = 0.77, }, -- end of ["MiG-29A"] ["NF-2_LightOff01"] = { }, -- end of ["NF-2_LightOff01"] ["Ural-375 ZU-23 Insurgent"] = { ["irsignature"] = 0.08, ["detection"] = 5000, ["threat"] = 2500, }, -- end of ["Ural-375 ZU-23 Insurgent"] ["ZTZ96B"] = { ["irsignature"] = 0.12, ["detection"] = 0, ["threat"] = 5000, }, -- end of ["ZTZ96B"] ["Grad-URAL"] = { ["irsignature"] = 0.08, ["detection"] = 0, ["threat"] = 19000, }, -- end of ["Grad-URAL"] ["Haystack 2"] = { }, -- end of ["Haystack 2"] ["HESCO_watchtower_3"] = { }, -- end of ["HESCO_watchtower_3"] ["Coach a tank yellow"] = { ["irsignature"] = 0, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Coach a tank yellow"] ["Uboat_VIIC"] = { ["irsignature"] = 0.25, ["detection"] = 20000, ["threat"] = 4000, }, -- end of ["Uboat_VIIC"] ["Sandbag_09"] = { }, -- end of ["Sandbag_09"] ["B-1B"] = { ["irsignature"] = 3, }, -- end of ["B-1B"] ["Sandbag_07"] = { }, -- end of ["Sandbag_07"] ["PLZ05"] = { ["irsignature"] = 0.1, ["detection"] = 0, ["threat"] = 23500, }, -- end of ["PLZ05"] ["C-47"] = { ["irsignature"] = 0.1, }, -- end of ["C-47"] ["F-16A"] = { ["irsignature"] = 0.6, }, -- end of ["F-16A"] ["leander-gun-ariadne"] = { ["detection"] = 150000, ["threat"] = 100000, }, -- end of ["leander-gun-ariadne"] ["F-16A MLU"] = { ["irsignature"] = 0.6, }, -- end of ["F-16A MLU"] ["M1134 Stryker ATGM"] = { ["irsignature"] = 0.085, ["detection"] = 0, ["threat"] = 3800, }, -- end of ["M1134 Stryker ATGM"] ["FlagPole"] = { }, -- end of ["FlagPole"] ["Roland ADS"] = { ["irsignature"] = 0.085, ["detection"] = 12000, ["threat"] = 8000, }, -- end of ["Roland ADS"] ["MiG-29G"] = { ["irsignature"] = 0.77, }, -- end of ["MiG-29G"] ["Twall_x6_3mts"] = { }, -- end of ["Twall_x6_3mts"] ["F/A-18C"] = { ["irsignature"] = 0.73, }, -- end of ["F/A-18C"] ["warning_board_a"] = { }, -- end of ["warning_board_a"] ["Ship_Tilde_Supply"] = { ["irsignature"] = 0.35, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Ship_Tilde_Supply"] ["Sandbag_08"] = { }, -- end of ["Sandbag_08"] ["Land_Rover_101_FC"] = { ["irsignature"] = 0.075, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Land_Rover_101_FC"] ["Pz_IV_H"] = { ["irsignature"] = 0.1, ["detection"] = 0, ["threat"] = 3000, }, -- end of ["Pz_IV_H"] ["Grad_FDDM"] = { ["irsignature"] = 0.09, ["detection"] = 0, ["threat"] = 1000, }, -- end of ["Grad_FDDM"] ["Concertina wire"] = { }, -- end of ["Concertina wire"] ["Tetrahydra"] = { }, -- end of ["Tetrahydra"] ["Log posts 1"] = { }, -- end of ["Log posts 1"] ["A-50"] = { ["irsignature"] = 4, }, -- end of ["A-50"] ["Patriot AMG"] = { ["irsignature"] = 0.05, ["detection"] = 0, ["threat"] = 0, }, -- end of ["Patriot AMG"] ["tt_DSHK"] = { ["irsignature"] = 0.08, ["detection"] = 5000, ["threat"] = 1200, }, -- end of ["tt_DSHK"] } end --###### UTIL FUNCTIONS ############################################################################ -- all the below functions are basically elements used in other part of the code. Many of them are basically copy or modified copy of other useful code and script, -- the credits list would be quite long but mostly mist, MOOSE, CTLD. When able I kept the original name even if slightly modified. local function escape_string(str) local replacements = { ['%'] = '%%', ['^'] = '%^', ['$'] = '%$', ['('] = '%(', [')'] = '%)', ['%['] = '%[%]', ['{'] = '%{', ['}'] = '%}', ['.'] = '%.', ['*'] = '%*', ['+'] = '%+', ['-'] = '%-', ['?'] = '%?', ['\0'] = '%z' } return (str:gsub(".", replacements)) end local function contains(haystack, needle) -- Effettua l'escape dei caratteri speciali nella stringa 'needle' local function escape_special_characters(str) local replacements = { ['%'] = '%%', ['^'] = '%^', ['$'] = '%$', ['('] = '%(', [')'] = '%)', ['%['] = '%[%]', ['{'] = '%{', ['}'] = '%}', ['.'] = '%.', ['*'] = '%*', ['+'] = '%+', ['-'] = '%-', ['?'] = '%?', ['\0'] = '%z' } return (str:gsub(".", replacements)) end -- Escape della stringa 'needle' local escaped_needle = escape_special_characters(needle) -- Controlla se 'needle' è contenuta in 'haystack' return haystack:find(escaped_needle) ~= nil end local function getDist(point1, point2) local xUnit = point1.x local yUnit = nil local xZone = point2.x local yZone = nil if point1.z then yUnit = point1.z elseif point1.y then yUnit = point1.y end if point2.z then yZone = point2.z elseif point2.y then yZone = point2.y end local xDiff = xUnit - xZone local yDiff = yUnit - yZone return math.sqrt(xDiff * xDiff + yDiff * yDiff) end local function groupTableCheck(group) if group then if type(group) == 'string' then -- assuming name local groupTable = Group.getByName(group) if not groupTable then groupTable = StaticObject.getByName(group) end if groupTable then return groupTable else return nil end elseif type(group) == 'table' then return group else env.info((tostring(ModuleName) .. ", groupTableCheck: wrong variable")) return nil end else env.info((tostring(ModuleName) .. ", groupTableCheck: missing variable")) return nil end end local function unitTableCheck(unit) if unit then if type(unit) == 'string' then -- assuming name local unitTable = Unit.getByName(unit) return unitTable elseif type(unit) == 'table' then return unit else env.info((tostring(ModuleName) .. ", unitTableCheck: wrong variable")) return nil end else env.info((tostring(ModuleName) .. ", unitTableCheck: missing variable")) return nil end end local function vec3Check(vec3) if vec3 then if type(vec3) == 'table' then -- assuming name if vec3.x and vec3.y and vec3.z then return vec3 else env.info((tostring(ModuleName) .. ", vec3Check: wrong vector format")) return nil end else env.info((tostring(ModuleName) .. ", vec3Check: wrong variable")) return nil end else env.info((tostring(ModuleName) .. ", vec3Check: missing variable")) return nil end end local function aie_random(firstNum, secondNum) local lowNum, highNum if not secondNum then highNum = firstNum lowNum = 1 else lowNum = firstNum highNum = secondNum end local total = 1 if math.abs(highNum - lowNum + 1) < 50 then -- if total values is less than 50 total = math.modf(50/math.abs(highNum - lowNum + 1)) -- make x copies required to be above 50 end local choices = {} for i = 1, total do -- iterate required number of times for x = lowNum, highNum do -- iterate between the range choices[#choices +1] = x -- add each entry to a table end end local rtnVal = math.random(#choices) -- will now do a math.random of at least 50 choices for i = 1, 10 do rtnVal = math.random(#choices) -- iterate a few times for giggles end return choices[rtnVal] end local function round(num, idp) local mult = 10^(idp or 0) return math.floor(num * mult + 0.5) / mult end local function buildWP(point, overRideForm, overRideSpeed) if point then local wp = {} wp.x = point.x if point.z then wp.y = point.z else wp.y = point.y end local form, speed if point.speed and not overRideSpeed then wp.speed = point.speed elseif type(overRideSpeed) == 'number' then wp.speed = overRideSpeed else wp.speed = 18/3.6 end if point.form and not overRideForm then form = point.form else form = overRideForm end if not form then wp.action = 'Off Road' else form = string.lower(form) if form == 'off_road' or form == 'off road' then wp.action = 'Off Road' elseif form == 'on_road' or form == 'on road' then wp.action = 'On Road' elseif form == 'rank' or form == 'line_abrest' or form == 'line abrest' or form == 'lineabrest'then wp.action = 'Rank' elseif form == 'cone' then wp.action = 'Cone' elseif form == 'diamond' then wp.action = 'Diamond' elseif form == 'vee' then wp.action = 'Vee' elseif form == 'echelon_left' or form == 'echelon left' or form == 'echelonl' then wp.action = 'EchelonL' elseif form == 'echelon_right' or form == 'echelon right' or form == 'echelonr' then wp.action = 'EchelonR' else wp.action = 'Off Road' -- if nothing matched end end wp.type = 'Turning Point' return wp else return false end end local function getRandPointInCircle(point, radius, innerRadius) local theta = 2*math.pi*math.random() local rad = math.random() + math.random() if rad > 1 then rad = 2 - rad end local radMult if innerRadius and innerRadius <= radius then radMult = (radius - innerRadius)*rad + innerRadius else radMult = radius*rad end if not point.z then --might as well work with vec2/3 point.z = point.y end local rndCoord if radius > 0 then rndCoord = {x = math.cos(theta)*radMult + point.x, y = math.sin(theta)*radMult + point.z} else rndCoord = {x = point.x, y = point.z} end return rndCoord end local function getRandTerrainPointInCircle(var, radius, innerRadius, requestV3) local point = vec3Check(var) if point and radius and innerRadius then for i = 1, 5 do local coordRun = getRandPointInCircle(point, radius, innerRadius) local destlandtype = land.getSurfaceType({coordRun.x, coordRun.z}) if destlandtype == 1 or destlandtype == 4 then if requestV3 == true then local c2 = {x = coordRun.x, y = land.getHeight({x = coordRun.x, y = coordRun.y}), z = coordRun.y} return c2 else return coordRun end end end return nil -- this means that no valid result has found end end local function deepCopy(object) local lookup_table = {} local function _copy(object) if type(object) ~= "table" then return object elseif lookup_table[object] then return lookup_table[object] end local new_table = {} lookup_table[object] = new_table for index, value in pairs(object) do new_table[_copy(index)] = _copy(value) end return setmetatable(new_table, getmetatable(object)) end return _copy(object) end local function findGroupInRange(support_point, attribute, distance, coalition) if support_point then local mindistance = distance local curGroup = nil local volS = { id = world.VolumeType.SPHERE, params = { point = support_point, radius = distance } } local ifFound = function(foundItem, val) local funcCheck = nil if type(attribute) == "string" then funcCheck = function(attribute) if foundItem:hasAttribute(attribute) then return true else return false end end elseif type(attribute) == "table" then funcCheck = function(attribute) local hasVal = false for att, attVal in pairs(attribute) do if foundItem:hasAttribute(attVal) then hasVal = true end end if hasVal == true then return true else return false end end end if funcCheck and funcCheck(attribute) == true then local proceed = true if coalition then local coaCheck = foundItem:getCoalition() if coalition == coaCheck then proceed = true else proceed = false end end if proceed == true then local itemPos = foundItem:getPosition().p if itemPos then local dist = getDist(itemPos, support_point) if dist < mindistance and dist > 1000 then mindistance = dist local foundGroup = foundItem:getGroup() curGroup = foundGroup end end end end end world.searchObjects(Object.Category.UNIT, volS, ifFound) if curGroup and mindistance then return curGroup else return false end end end local function multyTypeMessage(var) local mexType = var[1] local mexText = var[2] local mexDuration = var[3] local mexPos = var[4] local unitId = var[5] local groupId = var[6] local countryId = var[7] local coaId = var[8] local mexAuthor = var[9] local voiceGender = var[10] -- text message if mexType == "text" or mexType == "both" then if mexText and type(mexText) == "string" then local t = mexDuration if not t then t = 30 end if unitId then trigger.action.outTextForGroup(unitId, mexText, t) elseif groupId then trigger.action.outTextForGroup(groupId, mexText, t) elseif countryId then trigger.action.outTextForCoalition(countryId, mexText, t) elseif coaId then trigger.action.outTextForCoalition(coaId, mexText, t) else trigger.action.outText(mexText, t) end else env.info((tostring(ModuleName) .. " multyTypeMessage, mexText is not a valid input")) end end end local function vecmag(vec) return (vec.x^2 + vec.y^2 + vec.z^2)^0.5 end local function vecsub(vec1, vec2) return {x = vec1.x - vec2.x, y = vec1.y - vec2.y, z = vec1.z - vec2.z} end local function vecdp(vec1, vec2) return vec1.x*vec2.x + vec1.y*vec2.y + vec1.z*vec2.z end local function getNorthCorrection(gPoint) local point = deepCopy(gPoint) if not point.z then --Vec2; convert to Vec3 point.z = point.y point.y = 0 end local lat, lon = coord.LOtoLL(point) local north_posit = coord.LLtoLO(lat + 1, lon) return math.atan2(north_posit.z - point.z, north_posit.x - point.x) end local function kmphToMps(kmph) return kmph/3.6 end local function getHeading(unit, rawHeading) local unitpos = unit:getPosition() if unitpos then local Heading = math.atan2(unitpos.x.z, unitpos.x.x) if not rawHeading then Heading = Heading + getNorthCorrection(unitpos.p) end if Heading < 0 then Heading = Heading + 2*math.pi -- put heading in range of 0 to 2*pi end return Heading end end local function makeVec3(vec, y) if not vec.z then if vec.alt and not y then y = vec.alt elseif not y then y = 0 end return {x = vec.x, y = y, z = vec.y} else return {x = vec.x, y = vec.y, z = vec.z} -- it was already Vec3, actually. end end local function avgVec3(tblPos) local avgPoint = {x = 0, y = 0, z = 0} local numpoints = #tblPos if numpoints == 0 then return avgPoint end for _, punto in ipairs(tblPos) do avgPoint.x = avgPoint.x + punto.x avgPoint.y = avgPoint.y + punto.y avgPoint.z = avgPoint.z + punto.z end avgPoint.x = avgPoint.x / numpoints avgPoint.z = avgPoint.z / numpoints if avgPoint.x and avgPoint.z then avgPoint.y = land.getHeight({x = avgPoint.x, y = avgPoint.z}) return avgPoint else return nil end end local function getDir(vec, point) local dir = math.atan2(vec.z, vec.x) if point then dir = dir + getNorthCorrection(point) end if dir < 0 then dir = dir + 2 * math.pi -- put dir in range of 0 to 2*pi end return dir end local function getGroupSpeed(group) local g = groupTableCheck(group) if g then local units = g:getUnits() if units and #units > 0 then local s = 0 local ms = 1000 local u = 0 for u, uData in pairs(units) do u = u + 1 local us = vecmag(uData:getVelocity()) if us and us >= 0 then s = s + us if us < ms then ms = us end end end return s, ms else return nil end else return nil end end local function toDegree(angle) return angle*180/math.pi end local function tostringLL(lat, lon, acc, DMS) local latHemi, lonHemi if lat > 0 then latHemi = 'N' else latHemi = 'S' end if lon > 0 then lonHemi = 'E' else lonHemi = 'W' end lat = math.abs(lat) lon = math.abs(lon) local latDeg = math.floor(lat) local latMin = (lat - latDeg)*60 local lonDeg = math.floor(lon) local lonMin = (lon - lonDeg)*60 if DMS then -- degrees, minutes, and seconds. local oldLatMin = latMin latMin = math.floor(latMin) local latSec = round((oldLatMin - latMin)*60, acc) local oldLonMin = lonMin lonMin = math.floor(lonMin) local lonSec = round((oldLonMin - lonMin)*60, acc) if latSec == 60 then latSec = 0 latMin = latMin + 1 end if lonSec == 60 then lonSec = 0 lonMin = lonMin + 1 end local secFrmtStr -- create the formatting string for the seconds place if acc <= 0 then -- no decimal place. secFrmtStr = '%02d' else local width = 3 + acc -- 01.310 - that's a width of 6, for example. secFrmtStr = '%0' .. width .. '.' .. acc .. 'f' end return string.format('%02d', latDeg) .. ' ' .. string.format('%02d', latMin) .. '\' ' .. string.format(secFrmtStr, latSec) .. '"' .. latHemi .. ' ' .. string.format('%02d', lonDeg) .. ' ' .. string.format('%02d', lonMin) .. '\' ' .. string.format(secFrmtStr, lonSec) .. '"' .. lonHemi else -- degrees, decimal minutes. latMin = round(latMin, acc) lonMin = round(lonMin, acc) if latMin == 60 then latMin = 0 latDeg = latDeg + 1 end if lonMin == 60 then lonMin = 0 lonDeg = lonDeg + 1 end local minFrmtStr -- create the formatting string for the minutes place if acc <= 0 then -- no decimal place. minFrmtStr = '%02d' else local width = 3 + acc -- 01.310 - that's a width of 6, for example. minFrmtStr = '%0' .. width .. '.' .. acc .. 'f' end return string.format('%02d', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\'' .. latHemi .. ' ' .. string.format('%02d', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\'' .. lonHemi end end local function tostringMGRS(MGRS, acc) if acc == 0 then return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph else return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph .. ' ' .. string.format('%0' .. acc .. 'd', round(MGRS.Easting/(10^(5-acc)), 0)) .. ' ' .. string.format('%0' .. acc .. 'd', round(MGRS.Northing/(10^(5-acc)), 0)) end end local function zoneToVec3(zone) local new = {} if type(zone) == 'table' then if zone.point then new.x = zone.point.x new.y = zone.point.y new.z = zone.point.z elseif zone.x and zone.y and zone.z then return zone end return new elseif type(zone) == 'string' then zone = trigger.misc.getZone(zone) if zone then new.x = zone.point.x new.y = zone.point.y new.z = zone.point.z return new end end end local function pointInPolygon(point, poly) -- mist local copy ot f that function point = makeVec3(point) local px = point.x local pz = point.z local cn = 0 local newpoly = deepCopy(poly) local polysize = #newpoly newpoly[#newpoly + 1] = newpoly[1] newpoly[1] = makeVec3(newpoly[1]) for k = 1, polysize do newpoly[k+1] = makeVec3(newpoly[k+1]) if ((newpoly[k].z <= pz) and (newpoly[k+1].z > pz)) or ((newpoly[k].z > pz) and (newpoly[k+1].z <= pz)) then local vt = (pz - newpoly[k].z) / (newpoly[k+1].z - newpoly[k].z) if (px < newpoly[k].x + vt*(newpoly[k+1].x - newpoly[k].x)) then cn = cn + 1 end end end return cn%2 == 1 end local function getPayload(unitName) -- refactor to search by groupId and allow groupId and groupName as inputs local unitTbl = Unit.getByName(unitName) local unitId = unitTbl:getID() local gpTbl = unitTbl:getGroup() local gpId = gpTbl:getID() if gpId and unitId then for coa_name, coa_data in pairs(env.mission.coalition) do if (coa_name == 'red' or coa_name == 'blue') and type(coa_data) == 'table' then if coa_data.country then --there is a country table for cntry_id, cntry_data in pairs(coa_data.country) do for obj_type_name, obj_type_data in pairs(cntry_data) do if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" then -- only these types have points if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! for group_num, group_data in pairs(obj_type_data.group) do if group_data and group_data.groupId == gpId then for unitIndex, unitData in pairs(group_data.units) do --group index if unitData.unitId == unitId then return unitData.payload end end end end end end end end end end end else if AIEN.config.AIEN_debugProcessDetail then env.info(ModuleName .. " getPayload error, no gId or unitId") end return false end return end local function ground_buildWP(point, overRideForm, overRideSpeed) local wp = {} wp.x = point.x if point.z then wp.y = point.z else wp.y = point.y end local form, speed if point.speed and not overRideSpeed then wp.speed = point.speed elseif type(overRideSpeed) == 'number' then wp.speed = overRideSpeed else wp.speed = kmphToMps(20) end if point.form and not overRideForm then form = point.form else form = overRideForm end if not form then wp.action = 'Cone' else form = string.lower(form) if form == 'off_road' or form == 'off road' then wp.action = 'Off Road' elseif form == 'on_road' or form == 'on road' then wp.action = 'On Road' elseif form == 'rank' or form == 'line_abrest' or form == 'line abrest' or form == 'lineabrest'then wp.action = 'Rank' elseif form == 'cone' then wp.action = 'Cone' elseif form == 'diamond' then wp.action = 'Diamond' elseif form == 'vee' then wp.action = 'Vee' elseif form == 'echelon_left' or form == 'echelon left' or form == 'echelonl' then wp.action = 'EchelonL' elseif form == 'echelon_right' or form == 'echelon right' or form == 'echelonr' then wp.action = 'EchelonR' else wp.action = 'Cone' -- if nothing matched end end wp.type = 'Turning Point' return wp end local function dynAdd(ng) local newGroup = deepCopy(ng) local cntry = newGroup.country if newGroup.countryId then cntry = newGroup.countryId end local groupType = newGroup.category local newCountry = '' -- validate data for countryId, countryName in pairs(country.name) do if type(cntry) == 'string' then cntry = cntry:gsub("%s+", "_") if tostring(countryName) == string.upper(cntry) then newCountry = countryName end elseif type(cntry) == 'number' then if countryId == cntry then newCountry = countryName end end end if newCountry == '' then if AIEN.config.AIEN_debugProcessDetail then env.info(ModuleName .. " dynAdd Country not found") end return false end local newCat = '' for catName, catId in pairs(Unit.Category) do if type(groupType) == 'string' then if tostring(catName) == string.upper(groupType) then newCat = catName end elseif type(groupType) == 'number' then if catId == groupType then newCat = catName end end if catName == 'GROUND_UNIT' and (string.upper(groupType) == 'VEHICLE' or string.upper(groupType) == 'GROUND') then newCat = 'GROUND_UNIT' elseif catName == 'AIRPLANE' and string.upper(groupType) == 'PLANE' then newCat = 'AIRPLANE' end end local typeName if newCat == 'GROUND_UNIT' then typeName = ' gnd ' elseif newCat == 'AIRPLANE' then typeName = ' air ' elseif newCat == 'HELICOPTER' then typeName = ' hel ' elseif newCat == 'SHIP' then typeName = ' shp ' elseif newCat == 'BUILDING' then typeName = ' bld ' end if newGroup.groupName or newGroup.name then if newGroup.groupName then newGroup.name = newGroup.groupName elseif newGroup.name then newGroup.name = newGroup.name end end if newGroup.clone or not newGroup.name then newGroup.name = tostring(newCountry .. tostring(typeName) .. string.format("%04d", tostring(stupidIndex))) stupidIndex = stupidIndex + 1 end if not newGroup.hidden then newGroup.hidden = false end if not newGroup.visible then newGroup.visible = false end if (newGroup.start_time and type(newGroup.start_time) ~= 'number') or not newGroup.start_time then if newGroup.startTime then newGroup.start_time = round(newGroup.startTime) else newGroup.start_time = 0 end end for unitIndex, unitData in pairs(newGroup.units) do local originalName = newGroup.units[unitIndex].unitName or newGroup.units[unitIndex].name if newGroup.units[unitIndex].unitName or newGroup.units[unitIndex].name then if newGroup.units[unitIndex].unitName then newGroup.units[unitIndex].name = newGroup.units[unitIndex].unitName elseif newGroup.units[unitIndex].name then newGroup.units[unitIndex].name = newGroup.units[unitIndex].name end end if newGroup.clone or not unitData.name then newGroup.units[unitIndex].name = tostring(newGroup.name .. ' unit' .. unitIndex) end if not unitData.skill then newGroup.units[unitIndex].skill = 'Random' -- provide something here end if newCat == 'AIRPLANE' or newCat == 'HELICOPTER' then if newGroup.units[unitIndex].alt_type and newGroup.units[unitIndex].alt_type ~= 'BARO' or not newGroup.units[unitIndex].alt_type then newGroup.units[unitIndex].alt_type = 'RADIO' end if not unitData.speed then if newCat == 'AIRPLANE' then newGroup.units[unitIndex].speed = 150 elseif newCat == 'HELICOPTER' then newGroup.units[unitIndex].speed = 60 end end if not unitData.payload then newGroup.units[unitIndex].payload = getPayload(originalName) end if not unitData.alt then if newCat == 'AIRPLANE' then newGroup.units[unitIndex].alt = 2000 newGroup.units[unitIndex].alt_type = 'RADIO' newGroup.units[unitIndex].speed = 150 elseif newCat == 'HELICOPTER' then newGroup.units[unitIndex].alt = 500 newGroup.units[unitIndex].alt_type = 'RADIO' newGroup.units[unitIndex].speed = 60 end end elseif newCat == 'GROUND_UNIT' then if nil == unitData.playerCanDrive then unitData.playerCanDrive = true end end end if newGroup.route then if newGroup.route and not newGroup.route.points then if newGroup.route[1] then local copyRoute = deepCopy(newGroup.route) newGroup.route = {} newGroup.route.points = copyRoute end end else -- if aircraft and no route assigned. make a quick and stupid route so AI doesnt RTB immediately --if newCat == 'AIRPLANE' or newCat == 'HELICOPTER' then newGroup.route = {} newGroup.route.points = {} newGroup.route.points[1] = {} --end end newGroup.country = newCountry -- update and verify any self tasks if newGroup.route and newGroup.route.points then --log:warn(newGroup.route.points) for i, pData in pairs(newGroup.route.points) do if pData.task and pData.task.params and pData.task.params.tasks and #pData.task.params.tasks > 0 then for tIndex, tData in pairs(pData.task.params.tasks) do if tData.params and tData.params.action then if tData.params.action.id == "EPLRS" then tData.params.action.params.groupId = newGroup.groupId elseif tData.params.action.id == "ActivateBeacon" or tData.params.action.id == "ActivateICLS" then tData.params.action.params.unitId = newGroup.units[1].unitId end end end end end end -- sanitize table newGroup.groupName = nil newGroup.clone = nil newGroup.category = nil newGroup.country = nil newGroup.tasks = {} for unitIndex, unitData in pairs(newGroup.units) do newGroup.units[unitIndex].unitName = nil end coalition.addGroup(country.id[newCountry], Unit.Category[newCat], newGroup) -- QUIIIII, problema con ID? return newGroup end local function genSmokePoints(pos, dist, n) local points = {} local angle_step = 360 / n for i = 0, n - 1 do local rad = (i * angle_step) * math.pi / 180 local x = pos.x + dist * math.cos(rad) local z = pos.z + dist * math.sin(rad) table.insert(points, {x = x, y = pos.y-50, z = z}) end return points end local function pcallGetCategory(obj) -- done to avoid DCS errors local function effectiveCheck(obj) if obj and obj.isExist and obj.getPosition then if obj:isExist() then if obj:getPosition() then if Object.getCategory(obj) then return Object.getCategory(obj) else if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN pcallGetCategory, missing category")) end return nil end else if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN pcallGetCategory, missing pos")) end return nil end else if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN pcallGetCategory, isExist failed")) end return nil end else if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN pcallGetCategory, missing obj")) end return nil end end local noError, errorOrResult = pcall(effectiveCheck, obj) if noError then return errorOrResult else env.info(string.format("AIEN pcallGetCategory, error returned when calling the function: %s", errorOrResult or "")) end end -- desanitized functions (if available), for logging, table printing and debug purposes -- You should never run DCS desanitized unless specifically knowing the risks. However, if you already do that, for debug purposes AIEN will take advantages of the available io and lfs -- to print out tables of the databases created in it. if AIEN_io and AIEN_lfs then env.info(("AIEN loading desanitized additional function")) function IntegratedbasicSerialize(s) if s == nil then return "\"\"" else if ((type(s) == 'number') or (type(s) == 'boolean') or (type(s) == 'function') or (type(s) == 'table') or (type(s) == 'userdata') ) then return tostring(s) elseif type(s) == 'string' then return string.format('%q', s) end end end function Integratedserialize(name, value, level) -----Based on ED's serialize_simple2 local basicSerialize = function (o) if type(o) == "number" then return tostring(o) elseif type(o) == "boolean" then return tostring(o) else -- assume it is a string return IntegratedbasicSerialize(o) end end local serialize_to_t = function (name, value, level) ----Based on ED's serialize_simple2 local var_str_tbl = {} if level == nil then level = "" end if level ~= "" then level = level.." " end table.insert(var_str_tbl, level .. name .. " = ") if type(value) == "number" or type(value) == "string" or type(value) == "boolean" then table.insert(var_str_tbl, basicSerialize(value) .. ",\n") elseif type(value) == "table" then table.insert(var_str_tbl, "\n"..level.."{\n") for k,v in pairs(value) do -- serialize its fields local key if type(k) == "number" then key = string.format("[%s]", k) else key = string.format("[%q]", k) end table.insert(var_str_tbl, Integratedserialize(key, v, level.." ")) end if level == "" then table.insert(var_str_tbl, level.."} -- end of "..name.."\n") else table.insert(var_str_tbl, level.."}, -- end of "..name.."\n") end else print("Cannot serialize a "..type(value)) end return var_str_tbl end local t_str = serialize_to_t(name, value, level) return table.concat(t_str) end function IntegratedserializeWithCycles(name, value, saved) local basicSerialize = function (o) if type(o) == "number" then return tostring(o) elseif type(o) == "boolean" then return tostring(o) else -- assume it is a string return IntegratedbasicSerialize(o) end end local t_str = {} saved = saved or {} -- initial value if ((type(value) == 'string') or (type(value) == 'number') or (type(value) == 'table') or (type(value) == 'boolean')) then table.insert(t_str, name .. " = ") if type(value) == "number" or type(value) == "string" or type(value) == "boolean" then table.insert(t_str, basicSerialize(value) .. "\n") else if saved[value] then -- value already saved? table.insert(t_str, saved[value] .. "\n") else saved[value] = name -- save name for next time table.insert(t_str, "{}\n") for k,v in pairs(value) do -- save its fields local fieldname = string.format("%s[%s]", name, basicSerialize(k)) table.insert(t_str, IntegratedserializeWithCycles(fieldname, v, saved)) end end end return table.concat(t_str) else return "" end end function tableShow(tbl, loc, indent, tableshow_tbls) tableshow_tbls = tableshow_tbls or {} --create table of tables loc = loc or "" indent = indent or "" if type(tbl) == 'table' then --function only works for tables! tableshow_tbls[tbl] = loc local tbl_str = {} tbl_str[#tbl_str + 1] = indent .. '{\n' for ind,val in pairs(tbl) do -- serialize its fields if type(ind) == "number" then tbl_str[#tbl_str + 1] = indent tbl_str[#tbl_str + 1] = loc .. '[' tbl_str[#tbl_str + 1] = tostring(ind) tbl_str[#tbl_str + 1] = '] = ' else tbl_str[#tbl_str + 1] = indent tbl_str[#tbl_str + 1] = loc .. '[' tbl_str[#tbl_str + 1] = IntegratedbasicSerialize(ind) tbl_str[#tbl_str + 1] = '] = ' end if ((type(val) == 'number') or (type(val) == 'boolean')) then tbl_str[#tbl_str + 1] = tostring(val) tbl_str[#tbl_str + 1] = ',\n' elseif type(val) == 'string' then tbl_str[#tbl_str + 1] = IntegratedbasicSerialize(val) tbl_str[#tbl_str + 1] = ',\n' elseif type(val) == 'nil' then -- won't ever happen, right? tbl_str[#tbl_str + 1] = 'nil,\n' elseif type(val) == 'table' then if tableshow_tbls[val] then tbl_str[#tbl_str + 1] = tostring(val) .. ' already defined: ' .. tableshow_tbls[val] .. ',\n' else tableshow_tbls[val] = loc .. '[' .. IntegratedbasicSerialize(ind) .. ']' tbl_str[#tbl_str + 1] = tostring(val) .. ' ' tbl_str[#tbl_str + 1] = tableShow(val, loc .. '[' .. IntegratedbasicSerialize(ind).. ']', indent .. ' ', tableshow_tbls) tbl_str[#tbl_str + 1] = ',\n' end elseif type(val) == 'function' then if debug and debug.getinfo then local fcnname = tostring(val) local info = debug.getinfo(val, "S") if info.what == "C" then tbl_str[#tbl_str + 1] = string.format('%q', fcnname .. ', C function') .. ',\n' else if (string.sub(info.source, 1, 2) == [[./]]) then tbl_str[#tbl_str + 1] = string.format('%q', fcnname .. ', defined in (' .. info.linedefined .. '-' .. info.lastlinedefined .. ')' .. info.source) ..',\n' else tbl_str[#tbl_str + 1] = string.format('%q', fcnname .. ', defined in (' .. info.linedefined .. '-' .. info.lastlinedefined .. ')') ..',\n' end end else tbl_str[#tbl_str + 1] = 'a function,\n' end else tbl_str[#tbl_str + 1] = 'unable to serialize value type ' .. IntegratedbasicSerialize(type(val)) .. ' at index ' .. tostring(ind) end end tbl_str[#tbl_str + 1] = indent .. '}' return table.concat(tbl_str) end end function dumpTableAIEN(fname, tabledata, varInt) if AIEN_lfs and AIEN_io then local fdir = AIEN_lfs.writedir() .. [[Logs\]] .. fname local f = AIEN_io.open(fdir, 'w') local str = nil if varInt then if varInt == "basic" then str = IntegratedbasicSerialize(fname, tabledata) elseif varInt == "cycles" then str = IntegratedserializeWithCycles(fname, tabledata) elseif varInt == "int" then str = Integratedserialize(fname, tabledata) else str = IntegratedserializeWithCycles(fname, tabledata) end else str = IntegratedserializeWithCycles(fname, tabledata) end if f then f:write(str) f:close() end end end env.info(("AIEN desanitized additional function loaded")) end local function getDist(point1, point2) if point1 and point2 then local xUnit = point1.x local yUnit = point1.z local xZone = point2.x local yZone = point2.z local xDiff = xUnit - xZone local yDiff = yUnit - yZone return math.sqrt(xDiff * xDiff + yDiff * yDiff) else return nil end end local function groupTableCheck(group) if group then if type(group) == 'string' then -- assuming name local groupTable = Group.getByName(group) if not groupTable then groupTable = StaticObject.getByName(group) end if groupTable then return groupTable else return nil end elseif type(group) == 'table' then return group else env.info((tostring(ModuleName) .. ", groupTableCheck: wrong variable")) return nil end else env.info((tostring(ModuleName) .. ", groupTableCheck: missing variable")) return nil end end local function unitTableCheck(unit) if unit then if type(unit) == 'string' then -- assuming name local unitTable = Unit.getByName(unit) return unitTable elseif type(unit) == 'table' then return unit else env.info((tostring(ModuleName) .. ", unitTableCheck: wrong variable")) return nil end else env.info((tostring(ModuleName) .. ", unitTableCheck: missing variable")) return nil end end local function vec3Check(vec3) if vec3 then if type(vec3) == 'table' then -- assuming name if vec3.x and vec3.y and vec3.z then return vec3 else env.info((tostring(ModuleName) .. ", vec3Check: wrong vector format")) return nil end else env.info((tostring(ModuleName) .. ", vec3Check: wrong variable")) return nil end else env.info((tostring(ModuleName) .. ", vec3Check: missing variable")) return nil end end local function round(num, idp) local mult = 10^(idp or 0) return math.floor(num * mult + 0.5) / mult end local function getReactionTime(avg_skill) if avg_skill then -- local multiplier = 10/(avg_skill/10)/4 local min = math.floor(rndMinRT_xper*multiplier) local max = math.floor(rndMacRT_xper*multiplier) return aie_random(min, max) else return aie_random(3, 15) end end local function groupAllowedForAI(group) if group and group:isExist() and group:getUnits() and #group:getUnits() > 0 then if contains(group:getName(), AIEN.config.AIEN_xcl_tag) then return false end end return true end --###### CURRENT MISSION CONDITIONS ################################################################ -- road usage. Easy: if it's raining, off-road is not allowed. if env.mission.weather.clouds.iprecptns > 0 then forceRoadUse = true end --###### GROUP AI QUERY FUNCTIONS ################################################################## -- Below functions has been created to query ground groups for informations about them, most of them used in the key getSA functions that -- try to built the "situational awareness" of a group. When done, each time the FSM cycle goes to that group, it register bunch of info -- so that these would be fastly accessibile during reactions or decision making. --## CAPABILITY CHECKS -- these exist to identify some key characteristics of the group. local function group_hasAttribute(group, attribute) -- group tbl, attribute string (reference on DCS attributes) if group then local units = group:getUnits() if units and #units > 0 then for _, uData in pairs(units) do if uData:hasAttribute(attribute) then return true end end return false else if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN group_hasAttribute, no units retrievable")) end return false end else if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN group_hasAttribute, missing variable")) end return false end end local function group_hasSensors(group, sensor) -- group tbl, attribute string (reference on DCS attributes) if group then local units = group:getUnits() if units and #units > 0 then --[[ Unit.SensorType = { OPTIC = 0, RADAR = 1, IRST = 2, RWR = 3 } Unit.OpticType = { TV = 0, --TV-sensor LLTV = 1, --Low-level TV-sensor IR = 2 --Infra-Red optic sensor } Unit.RadarType = { AS = 0, --air search radar SS = 1 --surface/land search radar } --]]-- local optic, ir, radar, irst for _, uData in pairs(units) do if uData:hasSensors(0) then optic = true end if uData:hasSensors(0, 2) then ir = true end if uData:hasSensors(1) then radar = true end if uData:hasSensors(2) then irst = true end end return optic, ir, radar, irst else if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", hasSensors no units retrievable")) end return false end else if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", hasSensors missing variable")) end return false end end --## INFORMATIVE CHECKS -- these are basic informative "get" functions local function groupStatus(group) if group and group:isExist() == true then local units = group:getUnits() local curLife = 0 local initLife = 0 if units and #units > 0 then for _, uData in pairs(units) do if uData:isExist() then curLife = curLife + uData:getLife() initLife = initLife + uData:getLife0() end end end if curLife == initLife then return false, 1, curLife else local ratio = math.floor(curLife/initLife*10)/10 return true, ratio, curLife end end end local function groupLowAmmo(group) if group and group:isExist() == true then local tblUnits = group:getUnits() local groupSize = group:getSize() local groupOutAmmo = 0 if tblUnits and groupSize then if table.getn(tblUnits) > 0 then for uId, uData in pairs(tblUnits) do local uAmmo = uData:getAmmo() if uAmmo then for aId, aData in pairs(uAmmo) do if aData.count == 0 then groupOutAmmo = groupOutAmmo + 1 end end else groupOutAmmo = groupOutAmmo + 1 end end else env.info(("AIEN.groupLowAmmo, tblUnits is 0")) return false end else env.info(("AIEN.groupLowAmmo, missing tblUnits or groupSize")) return false end local fraction = groupOutAmmo/tonumber(groupSize) if fraction then if fraction > AIEN.config.outAmmoLowLevel then return true else return false end else env.info(("AIEN.groupLowAmmo, error calculating fraction")) return false end end end local function groupHasLosses(group) if group and group:isExist() == true then local curSize = group:getSize() local iniSize = group:getInitialSize() if iniSize == curSize then return false, 1 else local ratio = math.floor(curSize/iniSize*10)/10 return true, ratio end end end local function hasTargets(group, report) if group and group:isExist() == true then local tblUnits = Group.getUnits(group) local coalition = Group.getCoalition(group) if table.getn(tblUnits) > 0 then local hastargets = false local tbltargets = {} for uId, uData in pairs(tblUnits) do local uController = uData:getController() local utblTargets = uController:getDetectedTargets() if utblTargets then if table.getn(utblTargets) > 0 then if report and report == true then return true else for _, utData in pairs(utblTargets) do if utData.object and utData.object:isExist() == true then tbltargets[utData.object.id_] = utData end end end end end end if tbltargets and tbltargets ~= {} then return true, tbltargets end return false else env.info(("AIEN.hasTargets: tblUnits has 0 units")) return false end else env.info(("AIEN.hasTargets: group is nil")) return false end end local function getGroupClass(group) if group and group:isExist() == true then local units = group:getUnits() local coa = group:getCoalition() local max_fire_range = 0 local u_table = {} local str = 0 local cls = "none" local cat = "unknown" if units and coa then local clsCount = {} for uId, unit in pairs(units) do -- first unit define group class if unit:hasAttribute("Ground Units") then if unit:hasAttribute("Tanks") then if clsCount["MBT"] then clsCount["MBT"] = clsCount["MBT"] + 1 else clsCount["MBT"] = 1 end if uId == 1 then cls = "MBT" end elseif unit:hasAttribute("ATGM") then if clsCount["ATGM"] then clsCount["ATGM"] = clsCount["ATGM"] + 1 else clsCount["ATGM"] = 1 end if uId == 1 then cls = "ATGM" end elseif unit:hasAttribute("Indirect fire") and not unit:hasAttribute("SS_missile") then if unit:hasAttribute("MLRS") then if clsCount["MLRS"] then clsCount["MLRS"] = clsCount["MLRS"] + 1 else clsCount["MLRS"] = 1 end if uId == 1 then cls = "MLRS" end elseif unit:hasAttribute("Artillery") then if clsCount["ARTY"] then clsCount["ARTY"] = clsCount["ARTY"] + 1 else clsCount["ARTY"] = 1 end if uId == 1 then cls = "ARTY" end end elseif unit:hasAttribute("SS_missile") then if clsCount["MISSILE"] then clsCount["MISSILE"] = clsCount["MISSILE"] + 1 else clsCount["MISSILE"] = 1 end if uId == 1 then cls = "MISSILE" end elseif unit:hasAttribute("MANPADS") then if clsCount["MANPADS"] then clsCount["MANPADS"] = clsCount["MANPADS"] + 1 else clsCount["MANPADS"] = 1 end if uId == 1 then cls = "MANPADS" end elseif unit:hasAttribute("Air Defence vehicles") then if clsCount["SHORAD"] then clsCount["SHORAD"] = clsCount["SHORAD"] + 1 else clsCount["SHORAD"] = 1 end if uId == 1 then cls = "SHORAD" end elseif unit:hasAttribute("AAA") then if clsCount["AAA"] then clsCount["AAA"] = clsCount["AAA"] + 1 else clsCount["AAA"] = 1 end if uId == 1 then cls = "AAA" end elseif unit:hasAttribute("SAM elements") then if clsCount["SAM"] then clsCount["SAM"] = clsCount["SAM"] + 1 else clsCount["SAM"] = 1 end if uId == 1 then cls = "SAM" end elseif unit:hasAttribute("Armored vehicles") then if unit:hasAttribute("IFV") then if clsCount["IFV"] then clsCount["IFV"] = clsCount["IFV"] + 1 else clsCount["IFV"] = 1 end if uId == 1 then cls = "IFV" end elseif unit:hasAttribute("APC") then if clsCount["APC"] then clsCount["APC"] = clsCount["APC"] + 1 else clsCount["APC"] = 1 end if uId == 1 then cls = "APC" end end elseif unit:hasAttribute("Armed vehicles") then if clsCount["RECCE"] then clsCount["RECCE"] = clsCount["RECCE"] + 1 else clsCount["RECCE"] = 1 end if uId == 1 then cls = "RECCE" end elseif unit:hasAttribute("Unarmed vehicles") and unit:hasAttribute("Trucks") then if clsCount["LOGI"] then clsCount["LOGI"] = clsCount["LOGI"] + 1 else clsCount["LOGI"] = 1 end if uId == 1 then cls = "LOGI" end elseif unit:hasAttribute("Infantry") then if clsCount["INF"] then clsCount["INF"] = clsCount["INF"] + 1 else clsCount["INF"] = 1 end if uId == 1 then cls = "INF" end else if clsCount["UNKN"] then clsCount["UNKN"] = clsCount["UNKN"] + 1 else clsCount["UNKN"] = 1 end if uId == 1 then cls = "UNKN" end end elseif unit:hasAttribute("Air") then cls = "ARBN" elseif unit:hasAttribute("Ships") then cls = "SHIP" end end local mClass = nil local mVal = 2 for class, num in pairs(clsCount) do if num > mVal then -- at least 3 units mClass = class mVal = num end end if mClass then cls = mClass end if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", getGroupClass, group " .. tostring(group:getName()) .. " class: " .. tostring(cls))) end return cls else env.info((tostring(ModuleName) .. ", getGroupClass, missing units")) return false end else env.info((tostring(ModuleName) .. ", getGroupClass, missing group")) return false end end local function getUnitClass(unit) if unit and unit:isExist() == true then local coa = unit:getCoalition() local max_fire_range = 0 local u_table = {} local str = 0 local cls = "none" local cat = "unknown" if coa then if unit:hasAttribute("Air") then cls = "ARBN" elseif unit:hasAttribute("Ships") then cls = "SHIP" elseif unit:hasAttribute("Ground Units") then if unit:hasAttribute("Tanks") then cls = "MBT" elseif unit:hasAttribute("Indirect fire") and not unit:hasAttribute("SS_missile") then if unit:hasAttribute("MLRS") then cls = "MLRS" elseif unit:hasAttribute("Artillery") then cls = "ARTY" end elseif unit:hasAttribute("SS_missile") then cls = "MISSILE" elseif unit:hasAttribute("MANPADS") then cls = "MANPADS" elseif unit:hasAttribute("Air Defence vehicles") then cls = "SHORAD" elseif unit:hasAttribute("AAA") then cls = "AAA" elseif unit:hasAttribute("SAM elements") then cls = "SAM" elseif unit:hasAttribute("ATGM") then cls = "ATGM" elseif unit:hasAttribute("Armored vehicles") then if unit:hasAttribute("IFV") then cls = "IFV" elseif unit:hasAttribute("APC") then cls = "APC" end elseif unit:hasAttribute("Armed vehicles") then cls = "RECCE" elseif unit:hasAttribute("Unarmed vehicles") and unit:hasAttribute("Trucks") then cls = "LOGI" elseif unit:hasAttribute("Infantry") then cls = "INF" else cls = "UNKN" end end if AIEN.config.AIEN_debugProcessDetail == true then -- env.info((tostring(ModuleName) .. ", getUnitClass, unit " .. tostring(unit:getName()) .. " class: " .. tostring(cls))) end return cls else env.info((tostring(ModuleName) .. ", getUnitClass, missing coa")) return false end else env.info((tostring(ModuleName) .. ", getUnitClass, missing unit")) return false end end local function getGroupSkillNum(g) -- important: this try to create an "average skill scoring number" that will be used a lot elsewhere, i.e. for defining reaction time or even the available reactions. AIEN does not handle well the "Random" skill value (cause DCS skill is not available in real time): for best purpose, you should define the skill value of your ground units in the ME. local id = g:getID() --env.info((tostring(ModuleName) .. ", getGroupSkillNum: skLevel " .. tostring(g:getName()) )) for coalitionID,coalition in pairs(env.mission["coalition"]) do for countryID,country in pairs(coalition["country"]) do for attrID,attr in pairs(country) do if (type(attr)=="table") then if attrID == "vehicle" then for groupID,group in pairs(attr["group"]) do if (group) then if group.groupId == id then --env.info((tostring(ModuleName) .. ", getGroupSkillNum: skLevel " .. tostring(g:getName()).. " group found" )) local skLevel = 0 local unitsCount = 0 for unitID, unit in pairs(group["units"]) do local skTbl = skills[unit.skill] if skTbl then local val = skTbl.skillVal if unit.skill == "Random" then val = aie_random(4,12) end skLevel = skLevel + val unitsCount = unitsCount + 1 --env.info((tostring(ModuleName) .. ", getGroupSkillNum: skLevel " .. tostring(skLevel) .. ", unit num " .. tostring(unitsCount) )) end end if skLevel > 0 then local k = math.floor((skLevel/unitsCount)*10)/10 if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", getGroupSkillNum: skLevel " .. tostring(k))) end return k else return 3 end end end end end end end end end --env.info((tostring(ModuleName) .. ", getGroupSkillNum: sklevel not retournable, going random")) return aie_random(2,5) end local function getRanges(group) if group and group:isExist() == true then local units = group:getUnits() local maxDec = 0 local maxThr = 0 for uId, uData in pairs(units) do local t = uData:getTypeName() if t then local tData = tblThreatsRange[t] if tData then if tData.detection and tData.detection > maxDec then maxDec = tData.detection end if tData.threat and tData.threat > maxThr then maxThr = tData.threat end end end end if maxDec == 0 then maxDec = nil end if maxThr == 0 then maxThr = nil end return maxDec, maxThr else if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", getRanges failed, group variable is nil")) end end end local function getLeadPos(group) if group and group:isExist() == true then local units = group:getUnits() local leader = units[1] if leader then if not Unit.isExist(leader) then -- SHOULD be good, but if there is a bug, this code future-proofs it then. local lowestInd = math.huge for ind, unit in pairs(units) do if Unit.isExist(unit) and ind < lowestInd then lowestInd = ind return unit:getPosition().p end end end end if leader and Unit.isExist(leader) then -- maybe a little too paranoid now... return leader:getPosition().p end else if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", getLeadPos failed, group variable is nil")) end end end local function getTroops(group) if group and group:isExist() then if group:getUnits() and #group:getUnits() > 0 then local troopsTbl = {} for id, uData in pairs(group:getUnits()) do local mount = mountedDb[tostring(uData:getID())] if mount and uData then troopsTbl[uData:getID()] = {u = uData, t = mount} end end if troopsTbl and next(troopsTbl) ~= nil then if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.getTroops, returning troopstbl for: " .. tostring(group:getName()) )) end return troopsTbl end end end return nil end local function getDangerClose(vec3, coa, range) if vec3 and type(vec3) == "table" and coa then if vec3.x and vec3.y and vec3.z then if not range or type(range) ~= "number" then if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", getDangerClose: range missing reverted to 500 m ")) end range = 500 end -- check targets local firePoint = vec3 local friendly = nil local _volume = { id = world.VolumeType.SPHERE, params = { point = firePoint, radius = range, }, } local _search = function(_obj) pcall(function() if _obj ~= nil and _obj:isExist() and Object.getCategory(_obj) == 1 and _obj:getCoalition() == coa then friendly = true if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", getDangerClose: found friendly unit")) end return -- is this ok? end end) end world.searchObjects(Object.Category.UNIT, _volume, _search) if friendly == true then return true end return false end end end local function groupInZone(group) local point = getLeadPos(group) local zone = nil if point then if env.mission and env.mission.triggers and env.mission.triggers.zones and #env.mission.triggers.zones > 0 then for zId, zData in pairs(env.mission.triggers.zones) do if zData.name == AIEN.config.AIEN_zoneFilter then zone = zData zone.center = {x = zone.x, y = land.getHeight({x = zone.x, y = zone.y}), z = zone.y} end end end if not zone then return true else if zone.verticies then if pointInPolygon(point, zone.verticies) == true then return true else return false end elseif zone.radius then if getDist(point, zone.center) < zone.radius then return true else return false end end end end end --## AWARENESS CONSTRUCTION FOR FSM USE -- the core of the reaction decision making behaviour: this functions use the upper ones to try to built a virtual situational awareness, and also collect for faster access some key informations. local function getSA(group) -- built a situational awareness check if group and group:isExist() == true then local dbEntry = groundgroupsDb[group:getID()] or droneunitDb[group:getID()] if dbEntry then local sa = {} -- derivable functions sa.enInContact, sa.targets = hasTargets(group) sa.loss = groupHasLosses(group) sa.dmg, sa.life, sa.str = groupStatus(group) -- str must be added sa.low_ammo = groupLowAmmo(group) sa.pos = getLeadPos(group) sa.coa = group:getCoalition() sa.det = dbEntry["detection"] sa.rng = dbEntry["threat"] sa.cls = dbEntry["class"] sa.nearAlly = nil -- table {n = amount, p = nearest position, s = strength as life count} sa.nearEnemy = nil -- table {n = amount, p = nearest position, s = strength as life count} if sa.pos and sa.coa then -- fix potential det and range issue if not sa.rng then if sa.cls == "ARTY" then sa.rng = 15000 elseif sa.cls == "MLRS" then sa.rng = 30000 elseif sa.cls == "ATGM" then sa.rng = 4000 elseif sa.cls == "UAV" then sa.rng = 8000 else sa.rng = 2000 end end if not sa.det then if sa.cls == "ARTY" then sa.det = 2000 elseif sa.cls == "MLRS" then sa.det = 2000 elseif sa.cls == "ATGM" then sa.det = 4000 elseif sa.cls == "UAV" then sa.rng = 40000 else sa.det = 2000 end end -- nearby allies (within proxyUnitsDistance) local an = 0 local as = 0 local near_a = nil local _volume = { id = world.VolumeType.SPHERE, params = { point = sa.pos, radius = AIEN.config.proxyUnitsDistance, }, } local _search = function(_obj) pcall(function() if _obj ~= nil and Object.getCategory(_obj) == 1 and _obj:isExist() then local o_coa = _obj:getCoalition() local o_pos = _obj:getPosition().p local o_str = _obj:getLife() if o_coa and o_pos and o_str then if o_coa ~= 0 then -- skip neutral if o_coa == sa.coa then -- ally local md = AIEN.config.proxyUnitsDistance local d = getDist(sa.pos, o_pos) an = an + 1 as = as + o_str if d < md then md = d near_a = o_pos end end end end end end) end world.searchObjects(Object.Category.UNIT, _volume, _search) if an and near_a and as then sa.nearAlly = {n = an, p = near_a, s = as} end -- update intelDb if sa.targets and sa.targets ~= {} then for _, tgtData in pairs(sa.targets) do local tgt = tgtData.object local check = pcallGetCategory(tgt) if check == 1 then if tgt and tgt:isExist() then local t_id = tgt:getID() local t_pos = tgt:getPosition().p local t_coa = tgt:getCoalition() local t_life = tgt:getLife() local t = math.floor(timer.getTime()) local s = vecmag(tgt:getVelocity()) local knownType = tgt.type local t_type = nil if knownType == true then t_type = tgt:getTypeName() else t_type = "unknown" end local ob_cat = pcallGetCategory(initiator) local t_ucat = nil local t_scat = nil if ob_cat and ob_cat == 1 then t_ucat = tgt:getCategory() elseif ob_cat and ob_cat == 3 then t_scat = tgt:getCategory() end local ob_desc = tgt:getDesc() local t_attr = nil if ob_desc and ob_desc.attributes then t_attr = ob_desc.attributes end local t_cls = getUnitClass(tgt) intelDb[t_id] = {obj = tgt, pos = t_pos, coa = t_coa, life = t_life, record = t, speed = s, type = t_type, ucat = t_ucat, scat = t_scat, attr = t_attr, cls = t_cls, identifier = sa.cls} end end end end -- nearby enemies (use combined intel source DBs) + update intelDb local near_e = nil local es = 0 local en = 0 local dist = nil for iId, iData in pairs(intelDb) do if iData.obj:isExist() then if iData.coa ~= sa.coa and iData.coa ~= 0 and (timer.getTime() - iData.record) < AIEN.config.intelDbTimeout then local d = getDist(sa.pos, iData.pos) if d < AIEN.config.proxyUnitsDistance then en = en + 1 es = es + iData.life if d < AIEN.config.proxyUnitsDistance then near_e = iData.pos dist = d end end end else intelDb[iId] = nil -- removing the non existing object anymore. Do this any available cycle of intelDb end end if en and near_e and es then sa.nearEnemy = {n = en, p = near_e, s = es, d = dist} end return sa else return false end else if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", group not in db")) end return false end else if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", group doesn't exist")) end return false end end --###### GROUP AI COMMAND FUNCTIONS ################################################################ -- Below functions has been created to give command to ground groups, from the most basic to the more advanced. -- These functions are the core of the things that will be done by your units. --## BASIC STATE ACTION -- these are basic command for the group. local function groupGoQuiet(group) if group and group:isExist() == true then local gController = group:getController() gController:setOption(AI.Option.Ground.id.ALARM_STATE, 1) -- green -- Ground or GROUND? gController:setOption(AI.Option.Ground.id.ROE, 3) -- return fire -- Ground or GROUND? if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.groupGoQuiet status quiet")) end end end local function groupGoActive(group) if group and group:isExist() == true then local gController = group:getController() gController:setOption(AI.Option.Ground.id.ALARM_STATE, 2) -- red -- Ground or GROUND? gController:setOption(AI.Option.Ground.id.ROE, 3) -- return fire -- Ground or GROUND? if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.groupGoActive status active and return fire")) end end end local function groupGoShoot(group) if group and group:isExist() == true then local gController = group:getController() gController:setOption(AI.Option.Ground.id.ALARM_STATE, 2) -- red -- Ground or GROUND? gController:setOption(AI.Option.Ground.id.ROE, 2) -- open fire -- Ground or GROUND? if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.groupGoShoot status fire at will")) end end end local function groupAllowDisperse(group) if group and group:isExist() == true then local gController = group:getController() if gController then gController:setOption(AI.Option.Ground.id.DISPERSE_ON_ATTACK, AIEN.config.disperseActionTime) -- Ground or GROUND? if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.groupAllowDisperse will allow dispersal")) end else env.info(("AIEN.groupAllowDisperse, missing controller for: " .. tostring(group:getName()))) end else env.info(("AIEN.groupAllowDisperse, missing group")) end end local function groupPreventDisperse(group) if group and group:isExist() == true then local gController = group:getController() if gController then gController:setOption(AI.Option.Ground.id.DISPERSE_ON_ATTACK, false) -- Ground or GROUND? if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.groupPreventDisperse will prevent dispersal")) end else env.info(("AIEN.groupPreventDisperse, missing controller for: " ..tostring(group:getName()))) end else env.info(("AIEN.groupPreventDisperse, missing group")) end end local function groupSuppress(group) -- quite important: provide random "suppression" effect by enabling the ROE "hold fire" for a limited amount of time that will depend from group skill if group and group:isExist() == true then local c = group:getController() if c then local s = getGroupSkillNum(group) local st = getReactionTime(s)*2 c:setOption(AI.Option.Ground.id.ROE, 4) if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.groupSuppress group has been suppressed " .. tostring(group:getName()) )) end local back = function() c:setOption(AI.Option.Ground.id.ROE, 2) end if back then timer.scheduleFunction(back, {}, timer.getTime() + st) end end end end --## MISSION ACTION -- these are more advanced command for groups local function groupfireAtPoint(var) local group = var[1] -- groupTableCheck(var[1]) if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", groupfireAtPoint group check")) end if group and group:isExist() then if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", groupfireAtPoint group name: " .. tostring(group:getName()))) end local gController = group:getController() local vec3 = vec3Check(var[2]) local qty = var[3] local desc = var[4] local radi = var[5] if gController and vec3 then if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", groupfireAtPoint controller and vec3 identified")) end local expd = true if not var[3] then expd = false qty = nil end if not radi then radi = 50 end local _tgtVec2 = { x = vec3.x , y = vec3.z} local _task = { id = 'FireAtPoint', params = { point = _tgtVec2, radius = 50, expendQty = qty, expendQtyEnabled = expd, alt_type = 1, } } if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", groupfireAtPoint variables set")) end gController:pushTask(_task) if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", groupfireAtPoint fire mission planned")) end -- message feedback if AIEN.config.message_feed == true then local lat, lon = coord.LOtoLL(vec3) local MGRS = coord.LLtoMGRS(coord.LOtoLL(vec3)) if lat and lon then local LL_string = tostringLL(lat, lon, 0, true) local MGRS_string = tostringMGRS(MGRS ,4) local txt = "" txt = txt .. "C2, " .. tostring(group:getName()) .. ", request fire mission, fire for Effect. coordinates:" txt = txt .. "\n" .. tostring(MGRS_string) .. "\n" .. tostring(LL_string) if desc and type(desc) == "string" then txt = txt .. "\n" .. desc end if expd and qty and type(qty) == "number" then txt = txt .. "\n" .. tostring(qty) .. " rounds" end txt = txt .. "\n" .. "Cleared for fire when ready" local vars = {"text", txt, 20, nil, nil, nil, group:getCoalition()} multyTypeMessage(vars) end end -- mark on map for coalition if AIEN.config.mark_on_f10_map == true then local lat, lon = coord.LOtoLL(vec3) local MGRS = coord.LLtoMGRS(coord.LOtoLL(vec3)) if lat and lon then local LL_string = tostringLL(lat, lon, 0, true) local MGRS_string = tostringMGRS(MGRS ,4) local txt = "" txt = txt .. tostring(group:getName()) txt = txt .. "\n" .. "Fire mission, coordinates:" txt = txt .. "\n" .. tostring(MGRS_string) .. "\n" .. tostring(LL_string) markIdStart = markIdStart + 1 trigger.action.markToCoalition(markIdStart, txt, vec3, group:getCoalition(), false, false) end end else env.info((tostring(ModuleName) .. ", groupfireAtPoint, missing controller or for: " .. tostring(group:getName()))) end else env.info((tostring(ModuleName) .. ", groupfireAtPoint, missing group")) end end --## ROUTE AND PATHFINDING local function checkValidTerrainSurface(vec3) if vec3 then if type(vec3) == 'table' then -- assuming name if vec3.x and vec3.y and vec3.z then local l = land.getSurfaceType({x = vec3.x, y = vec3.z}) if l then if l == 1 or l == 4 or l == 5 then return true, l else return false, l end else env.info((tostring(ModuleName) .. ", checkValidDestination: l not identified!")) return false end else env.info((tostring(ModuleName) .. ", checkValidDestination: wrong vector format")) return false end else env.info((tostring(ModuleName) .. ", checkValidDestination: wrong variable")) return false end else env.info((tostring(ModuleName) .. ", checkValidDestination: missing variable")) return false end end local function getMEroute(group) -- basically a copy of getGroupRoute -- refactor to search by groupId and allow groupId and groupName as inputs local gpId = nil if group and group:isExist() == true then gpId = group:getID() end if gpId then for coa_name, coa_data in pairs(env.mission.coalition) do if (coa_name == 'red' or coa_name == 'blue') and type(coa_data) == 'table' then if coa_data.country then --there is a country table for cntry_id, cntry_data in pairs(coa_data.country) do for obj_type_name, obj_type_data in pairs(cntry_data) do if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" then -- only these types have points if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! for group_num, group_data in pairs(obj_type_data.group) do if group_data and group_data.groupId == gpId then -- this is the group we are looking for if group_data.route and group_data.route.points and #group_data.route.points > 0 then local points = {} for point_num, point in pairs(group_data.route.points) do local routeData = {} routeData.name = point.name if not point.point then routeData.x = point.x routeData.y = point.y else routeData.point = point.point --it's possible that the ME could move to the point = Vec2 notation. end routeData.form = point.action routeData.speed = point.speed routeData.alt = point.alt routeData.alt_type = point.alt_type routeData.airdromeId = point.airdromeId routeData.helipadId = point.helipadId routeData.type = point.type routeData.action = point.action routeData.task = point.task points[point_num] = routeData end return points end return end --if group_data and group_data.name and group_data.name == 'groupname' end --for group_num, group_data in pairs(obj_type_data.group) do end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then end --for obj_type_name, obj_type_data in pairs(cntry_data) do end --for cntry_id, cntry_data in pairs(coa_data.country) do end --if coa_data.country then --there is a country table end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then end --for coa_name, coa_data in pairs(mission.coalition) do else return nil end end local function groupRoadOnly(group) if group and group:isExist() == true then local units = group:getUnits() for uId, uData in pairs(units) do if uData:hasAttribute("Trucks") or uData:hasAttribute("Cars") or uData:hasAttribute("Unarmed vehicles") then if AIEN.config.AIEN_debugProcessDetail then env.info(("AIEN.groupRoadOnly found at least one road only unit!")) end return true end end end if AIEN.config.AIEN_debugProcessDetail then env.info(("AIEN.groupRoadOnly no road only unit found, or no group")) end return false end local function goRoute(group, path) if group and path and group:isExist() == true then local misTask = { id = 'Mission', params = { route = { points = deepCopy(path), }, }, } local groupCon = group:getController() if groupCon then groupCon:setTask(misTask) return true end return false end end local function moveToPoint(group, Vec3destination, destRadius, destInnerRadius, reqUseRoad, formation, haltContact, issuedByClient, clientCoa, groupSpeed) -- move the group to a point or, if the point is missing, to a random position at about 2 km env.info((tostring(ModuleName) .. ", moveToPoint Vec3destination x = " .. tostring(Vec3destination.x))) env.info((tostring(ModuleName) .. ", moveToPoint Vec3destination y = " .. tostring(Vec3destination.y))) env.info((tostring(ModuleName) .. ", moveToPoint Vec3destination z = " .. tostring(Vec3destination.z))) if Vec3destination then local vt, vv = checkValidTerrainSurface(Vec3destination) if vt == false then local newX, newZ = land.getClosestPointOnRoads('roads', Vec3destination.x, Vec3destination.z) local newY = land.getHeight({x = newX, y = newZ}) Vec3destination = {x = newX, y = newY, z = newZ} env.info((tostring(ModuleName) .. ", moveToPoint Vec3destination corrected for land, was type " .. tostring(vv))) else env.info((tostring(ModuleName) .. ", moveToPoint Vec3destination is identified as land, type " .. tostring(vv))) end end if group and group:isExist() == true then local unit1 = group:getUnit(1) if unit1 then local curPoint = unit1:getPosition().p local point = Vec3destination --required local dist = getDist(point,curPoint) -- start answer local msg = "" if clientCoa and issuedByClient then msg = msg .. tostring(tostring(group:getName())) end if clientCoa and issuedByClient then local latitude, longitude, elev = coord.LOtoLL(point) local LL_string = tostringLL(latitude, longitude, 0, true) msg = msg .. " move to " .. tostring(LL_string) .. "\n" end -- checking and define road routing requests local useRoads = false if issuedByClient == true then if not reqUseRoad or reqUseRoad == false then if env.mission.weather.clouds.iprecptns > 0 then useRoads= true if clientCoa then msg = msg .. "unable to move on open ground due to weather, will use road" .. "\n" end elseif groupRoadOnly(group) == true then useRoads= true if clientCoa then msg = msg .. "unable to move on open ground due to vehicle spec, will use roads" .. "\n" end elseif dist > 30000 then useRoads = true if clientCoa then msg = msg .. "unable to move in open ground due to distance, will use road instead" .. "\n" end else useRoads = false if clientCoa then msg = msg .. "will move on open ground" .. "\n" end end else useRoads = true if clientCoa then msg = msg .. "using roads " .. "\n" end -- check if possible and the relative answer end else if not reqUseRoad or reqUseRoad == false then if env.mission.weather.clouds.iprecptns > 0 then useRoads= true elseif groupRoadOnly(group) == true then useRoads= true elseif dist > 5000 then useRoads = true else useRoads = false end else useRoads = true end end local rndCoord = nil if point == nil then point = getRandTerrainPointInCircle(group:getPosition().p, AIEN.config.rndFleeDistance*1.3, AIEN.config.rndFleeDistance*0.9) rndCoord = point end if point then local radius = destRadius or 10 local innerRadius = destInnerRadius or 1 -- define formation local form = formation or 'Offroad' if issuedByClient == true and clientCoa and formation then if useRoads == false then msg = msg .. "will deploin in " .. tostring(form) .. " formation " .. "\n" else msg = msg .. "when in open ground, will deploin in " .. tostring(form) .. " formation " .. "\n" end end -- define heading local heading = math.random()*2*math.pi if heading >= 2*math.pi then heading = heading - 2*math.pi end -- define speed local speed = groupSpeed if not speed then if useRoads == false then speed = AIEN.config.outRoadSpeed else speed = AIEN.config.inRoadSpeed end end if issuedByClient == true and clientCoa then msg = msg .. "moving at " .. tostring(math.floor(speed/3.6*10)/10) .. " kmh \n" end local path = {} if not rndCoord then rndCoord = getRandTerrainPointInCircle(point, radius, innerRadius) end if rndCoord then local offset = {} local posStart = getLeadPos(group) if posStart then offset.x = round(math.sin(heading - (math.pi/2)) * 50 + rndCoord.x, 3) offset.z = round(math.cos(heading + (math.pi/2)) * 50 + rndCoord.y, 3) path[#path + 1] = buildWP(posStart, form, speed) if useRoads == true and ((point.x - posStart.x)^2 + (point.z - posStart.z)^2)^0.5 > radius * 1.3 then path[#path + 1] = buildWP({x = posStart.x + 11, z = posStart.z + 11}, 'off_road', AIEN.config.outRoadSpeed) path[#path + 1] = buildWP(posStart, 'on_road', speed) path[#path + 1] = buildWP(offset, 'on_road', speed) else path[#path + 1] = buildWP({x = posStart.x + 25, z = posStart.z + 25}, form, speed) end path[#path + 1] = buildWP(offset, form, speed) path[#path + 1] = buildWP(rndCoord, form, speed) goRoute(group, path) if issuedByClient == true and clientCoa then trigger.action.outTextForCoalition(clientCoa, msg, 30) if AIEN.config.AIEN_debugProcessDetail then env.info(("AIEN.moveToPoint msg " .. tostring(msg))) end end return end else env.info((tostring(ModuleName) .. ", moveToPoint failed, no valid coord available")) end else env.info((tostring(ModuleName) .. ", moveToPoint failed, no valid destination available")) end else env.info((tostring(ModuleName) .. ", moveToPoint failed, unit1 not available")) end end end --###### COUNTER BATTERY FIRE ###################################################################### local function counterBattery(hitPos, tgtPos, coa) -- this function emulates counter battery fire -- this function is not about simulating the counter battery fire process, which involves projectiles radar detection, -- balistic calculations and then defining a shooter position. Instead, for performance purposes, the process is "hinted" using the following method, and start only if the shooter is an "indirect fire" attributes units -- this way: -- * first, since we don't want to do calc, we took the shooter current position when the hit event occours, to use it later if allowed, called tgtPos -- * second, same reason, we pre-check if a free arty is available in range for fire on shooter position. -- * if arty is ok, and given the hit position, we look for the presence of a suitable radar ("SAM SR", "SAM TR", "EWR" since DCS world doesn't have the right kind of unit) within 50km -- * if it's there, since we don't want to do calc much, we simply apply some random math formula that depends on distance as a probabilty of trajectory calc IRL and, also, the accuracy -- * if the random pass, the tgt is the passed for arty fire after a random timing that is counterBatteryPlanDelay+-35%. if hitPos and tgtPos and coa then if type(hitPos) == "table" and type(tgtPos) == "table" then if hitPos.x and hitPos.z and tgtPos.x and tgtPos.z then local arty = nil for _, og in pairs(groundgroupsDb) do if og.coa == coa and og.tasked == false then if og.class == "ARTY" or og.class == "MLRS" then -- or og.class == "MLRS" -- not considering MLRS as they're intended for more area or tactical fire if og.group and og.group:isExist() == true then local d = getDist(og.sa.pos, tgtPos) if d < og.threat*0.9 then og.tasked = true og.taskTime = timer.getTime() og.firePoint = tgtPos if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", counterBattery artillery potentially available")) end arty = og.group break end end end end end if arty then -- check for near radar within 50 km, if there, return closer distance local closestRange = AIEN.config.counterBatteryRadarRange local _volume = { id = world.VolumeType.SPHERE, params = { point = hitPos, radius = AIEN.config.counterBatteryRadarRange, }, } local curPri = 0 local _search = function(_obj) pcall(function() if _obj ~= nil and Object.getCategory(_obj) == 1 and _obj:isExist() and _obj:getCoalition() == coa then if _obj:hasAttribute("SAM SR") or _obj:hasAttribute("SAM TR") or _obj:hasAttribute("EWR") then local d = getDist(_obj:getPoint(), hitPos) if d < closestRange then closestRange = d end end end end) end world.searchObjects(Object.Category.UNIT, _volume, _search) if closestRange < AIEN.config.counterBatteryRadarRange then local f = math.floor((1-(closestRange/AIEN.config.counterBatteryRadarRange)^2)*100) local r = aie_random(1,100) if f > r then local a = math.floor( ((closestRange/AIEN.config.counterBatteryRadarRange)^1.5)*300) local fpos = getRandTerrainPointInCircle(tgtPos, a, 1) if fpos then local t = aie_random(math.floor(AIEN.config.counterBatteryPlanDelay*0.65), math.floor(AIEN.config.counterBatteryPlanDelay*1.35)) if AIEN.config.message_feed == true then local lat, lon = coord.LOtoLL(hitPos) local MGRS = coord.LLtoMGRS(coord.LOtoLL(hitPos)) if lat and lon then local LL_string = tostringLL(lat, lon, 0, true) local MGRS_string = tostringMGRS(MGRS ,4) local txt = "" txt = txt .. "C2, " .. tostring(arty:getName()) .. ", identified enemy artillery fire. coordinates:" txt = txt .. "\n" .. tostring(MGRS_string) .. "\n" .. tostring(LL_string) txt = txt .. "\n" .. "Trying to evaluate enemy position. Please wait" local vars = {"text", txt, 20, nil, nil, nil, coa} multyTypeMessage(vars) end end local func = function() groupfireAtPoint({arty, fpos, 20, "Counter battery fire"}) end timer.scheduleFunction(func, nil, timer.getTime() + t) else if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", counterBattery failed fpos calculation")) end return false end else if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", counterBattery f=" .. tostring(f) .. ", r=" .. tostring(r) .. " failed")) end return false end end else if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", counterBattery artillery not available")) end return false end else if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", counterBattery variable x and z missing")) end return false end else if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", counterBattery variables wrong format")) end return false end else if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", ac_fireMissionOnShooter return false due to missing variable")) end return false end end --###### DISMOUNT FUNCTIONS ######################################################################## -- This part of the code, basically modified from CTLD ones, handle the automatic troop dismount-remount behaviour. -- Those functions are maybe the most "delicate" of the code, so think about them at least three times before touching ;) local function createUnit(_x, _y, _angle, _n, _t) local id = aie_random(1,4) -- had to do this cause random is not defined once in game local sk = nil if id == 1 then sk = "Average" elseif id == 2 then sk = "Good" elseif id == 3 then sk = "High" elseif id == 4 then sk = "Excellent" end local _newUnit = { ["y"] = _y, ["type"] = _t, ["name"] = _n, ["heading"] = _angle, ["playerCanDrive"] = true, ["skill"] = sk, ["x"] = _x, } return _newUnit end local function findNearestEnemy(_side, _point, _searchDistance, _reposition) local repoOffset if not _reposition then repoOffset = 3 else repoOffset = AIEN.config.droppedReposition end local mindistance = _searchDistance local enemyPos = nil local volS = { id = world.VolumeType.SPHERE, params = { point = _point, radius = _searchDistance } } local ifFound = function(foundItem, val) local itemPos = foundItem:getPosition().p local itemCoa = foundItem:getCoalition() if itemPos and itemCoa and itemCoa ~= _side and itemCoa ~= 0 then local dist = getDist(itemPos, _point) if dist < mindistance then mindistance = dist enemyPos = foundItem:getPosition().p end end end world.searchObjects(Object.Category.UNIT, volS, ifFound) if enemyPos ~= nil then local _x = enemyPos.x + aie_random(1, 20) - aie_random(1, 20) local _z = enemyPos.z + aie_random(1, 20) - aie_random(1, 20) local _y = enemyPos.y + aie_random(1, 20) - aie_random(1, 20) return { x = _x, y =_y, z = _z} else local _x = _point.x + aie_random(1, repoOffset) - aie_random(1, repoOffset) local _z = _point.z + aie_random(1, repoOffset) - aie_random(1, repoOffset) local _y = _point.y + aie_random(1, repoOffset) - aie_random(1, repoOffset) return { x = _x, y =_y, z = _z} end end local function getAliveGroup(_group) if _group and _group:isExist() == true and #_group:getUnits() > 0 then return _group end return nil end local function orderInfantryToMoveToPoint(_group, _destination) local _start = getLeadPos(_group) local _path = {} local routing = 'Off Road' local volS = { id = world.VolumeType.SPHERE, params = { point = _start, radius = AIEN.config.infantrySearchDist, } } local _count = 0 local _search = function(_obj) pcall(function() -- filters to avoid objects that are not exactly valuable local _d = _obj:getDesc() if _d then if _d.attributes and _d.attributes.Buildings then -- must be building _count = _count + 1 end end end) return true end world.searchObjects(Object.Category.SCENERY, volS, _search) if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.orderInfantryToMoveToPoint buildings _count = " .. tostring(_count))) end if _count > 5 then routing = 'on_road' if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.orderInfantryToMoveToPoint buildings identified, moving on road")) end end local _dTbl if _destination then local _x = _destination.x + aie_random(1, 5) - aie_random(1, 5) local _z = _destination.z + aie_random(1, 5) - aie_random(1, 5) local _y = _destination.y + aie_random(1, 5) - aie_random(1, 5) _dTbl = { x = _x, y =_y, z = _z} end table.insert(_path, ground_buildWP(_start, routing, AIEN.config.infantrySpeed)) table.insert(_path, ground_buildWP(_dTbl, routing, AIEN.config.infantrySpeed)) if routing == 'on_road' then table.insert(_path, ground_buildWP(_dTbl, 'Off Road', 5)) end local _mission = { id = 'Mission', params = { route = { points =_path }, }, } timer.scheduleFunction(function(_arg) local _grp = getAliveGroup(_arg[1]) if _grp ~= nil then local _controller = _grp:getController(); Controller.setOption(_controller, AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.AUTO) Controller.setOption(_controller, AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.OPEN_FIRE) _controller:setTask(_arg[2]) end end , {_group, _mission}, timer.getTime() + 2) end local function defineTroopsNumber(unit) if unit and unit:isExist() then local num = 0 for dId, dNum in pairs(dismountCarriers) do if unit:hasAttribute(dId) then if num < dNum then num = dNum end end end if num and num > 0 then return num end end return 0 end local function deployTroops(unit, exactPos) local _point = unit:getPoint() local _onboard = mountedDb[unit:getID()] local _coa = unit:getCoalition() local function deploy(team, num) if team then local isMortar = false local _groupName = unit:getName() .. "_dismounted_" .. tostring(num) local _group = { ["visible"] = false, ["hidden"] = false, ["units"] = {}, ["name"] = _groupName, ["task"] = {}, } local _pos = _point for _i, _soldier in ipairs(team) do if contains(_soldier, "mortar") then isMortar = true end local _angle = math.pi * 2 * (_i - 1) / #team local _xOffset = math.cos(_angle) * 15 + num local _yOffset = math.sin(_angle) * 15 + num local _name = _groupName .. "_".. tostring(_i) _group.units[_i] = createUnit(_pos.x + _xOffset, _pos.z + _yOffset, _angle, _name, _soldier) end _group.category = Group.Category.GROUND; _group.country = unit:getCountry(); local _spawnedGroup = Group.getByName(dynAdd(_group).name) if _spawnedGroup then if exactPos then if isMortar == false then orderInfantryToMoveToPoint(_spawnedGroup, exactPos) end else local _enemyPos = findNearestEnemy(_coa, _point, AIEN.config.infantrySearchDist) if _enemyPos and isMortar == false then orderInfantryToMoveToPoint(_spawnedGroup, _enemyPos) end mountedDb[unit:getID()] = nil if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.deployTroops units deployed for unit " .. tostring(unit:getName()))) end end return _spawnedGroup end end end if _onboard then for _g, _team in ipairs(_onboard) do deploy(_team, _g) end infcarrierDb[unit:getID()] = defineTroopsNumber(unit) end end local function extractTroops(unit) if unit and unit:isExist() then if unit:hasAttribute("IFV") or unit:hasAttribute("APC") or unit:hasAttribute("Trucks") then local people = infcarrierDb[unit:getID()] if not people then infcarrierDb[unit:getID()] = defineTroopsNumber(unit) end local uCoa = unit:getCoalition() if people and people > 0 then local foundAnything = true local done = {} local function loadTeam() local mindistance = AIEN.config.infantryExtractDist local volS = { id = world.VolumeType.SPHERE, params = { point = unit:getPoint(), radius = AIEN.config.infantryExtractDist } } local nearestU = nil local ifFound = function(foundItem, val) if foundItem:getCoalition() == uCoa then local fgId = foundItem:getGroup():getID() if not done[fgId] then if foundItem:hasAttribute("Infantry") then local posu = foundItem:getPoint() local dist = getDist(posu, unit:getPoint()) if dist < mindistance then mindistance = dist nearestU = foundItem end end end end end world.searchObjects(Object.Category.UNIT, volS, ifFound) local typ = nil local gtbl = nil if nearestU then local foundg = nearestU:getGroup() if foundg then if not done[foundg:getID()] then local units = foundg:getUnits() if units and #units > 0 then local tot = 0 local inf = 0 for _, u in pairs(units) do if u:hasAttribute("Infantry") then inf = inf + 1 if not typ then typ = {} end typ[#typ+1] = u:getTypeName() end tot = tot + 1 end if inf == tot and inf <= people then people = people - inf gtbl = foundg end end end end end if typ and gtbl then infcarrierDb[unit:getID()] = people local loadedGroups = mountedDb[unit:getID()] or {} loadedGroups[#loadedGroups+1] = typ mountedDb[unit:getID()] = loadedGroups if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.groupExtractTroop unit " .. tostring(unit:getName()) .. ", extracted " .. tostring(gtbl:getName()) .. ", people: " .. tostring(people) )) end done[gtbl:getID()] = true gtbl:destroy() else if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.groupExtractTroop extraction found anything" )) end foundAnything = false end end while people >= 4 and foundAnything == true do if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.groupExtractTroop unit " .. tostring(unit:getName()) .. ", launching loadTeam, people " .. tostring(people) )) end loadTeam() end if AIEN.config.AIEN_debugProcessDetail and AIEN_io and AIEN_lfs then dumpTableAIEN("infcarrierDb.lua", infcarrierDb, "int") dumpTableAIEN("mountedDb.lua", mountedDb, "int") end end end end end local function groupExtractTroop(group) if group and group:isExist() == true and #group:getUnits() > 0 then local units = group:getUnits() for uId, uData in pairs(units) do if mountedDb[uData:getID()] == nil then if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.groupExtractTroop units extracting troops " .. tostring(uData:getName()))) end extractTroops(uData) end end end return nil end local function groupCarryInfantry(group) -- needed? if group and group:isExist() == true and #group:getUnits() > 0 then local units = group:getUnits() local isCarrying = false for uId, uData in pairs(units) do if mountedDb[uData:getID()] then isCarrying = true break end end return isCarrying end end local function mountTeam(unit) local uName = unit:getName() local volS = { id = world.VolumeType.SPHERE, params = { point = unit:getPoint(), radius = AIEN.config.infantrySearchDist } } local commandIssued = {} local groupMoving = 0 local ifFound = function(foundItem, val) if contains(foundItem:getName(), uName) then if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", groupMountTeam, " .. tostring(foundItem:getName()) .. " recognized for " .. tostring(uName))) end local foundg = foundItem:getGroup() if not commandIssued[foundg:getID()] then if foundg and foundg:isExist() == true and #foundg:getUnits() > 0 then orderInfantryToMoveToPoint(foundg, unit:getPoint()) commandIssued[foundg:getID()] = true groupMoving = groupMoving + 1 end end end end world.searchObjects(Object.Category.UNIT, volS, ifFound) if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", groupMountTeam, " .. tostring(groupMoving) .. " groups have been ordered to move nearby " .. tostring(uName))) end if groupMoving > 0 then timer.scheduleFunction(extractTroops, unit, timer.getTime() + 600) end end local function groupMountTeam(group) -- this differs substantially from "Extract": basically it's calling the deployed teams to go back to its original vehicle -- the original vehicle is defined in a very "stupid" way: by searching unit name in the others groups nearby, which should be there -- in the dismounted group name. It's a very "hardcoded" convetion, I know, but still less complicated than many other solutions -- and does not require to track troops in another separated db (which already are too many to me) -- timing of 7 mins (420 s) seems reasonable to me for the regrouping of the dismounted troops, given the 2 km range. if group and group:isExist() == true and #group:getUnits() > 0 then for uId, uData in pairs(group:getUnits()) do mountTeam(uData) end end end local function groupDeployTroop(group, nocomeback, exactPos) if group and group:isExist() == true and group:getUnits() and #group:getUnits() > 0 then local units = group:getUnits() for uId, uData in pairs(units) do local id = uData:getID() if id then if mountedDb[uData:getID()] then deployTroops(uData, exactPos) if not nocomeback then timer.scheduleFunction(groupMountTeam, group, timer.getTime() + AIEN.config.remountTime) end return true end end end end return nil end local function groupExtractDeploy(group) -- needed? if group and group:isExist() == true and #group:getUnits() > 0 then if groupCarryInfantry(group) == true then groupDeployTroop(group) else groupExtractTroop(group) end end end local function groupCheckForManpad(group) if group and group:isExist() then local unitsWithTroops = getTroops(group) if unitsWithTroops and next(unitsWithTroops) ~= nil then if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.groupCheckForManpad, unitsWithTroops available" )) end local manpadTeams = {} for uId, uData in pairs(unitsWithTroops) do for _, teams in pairs(uData.t) do for _, soldier in pairs(teams) do if contains(soldier, "manpad") then if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.groupCheckForManpad, has manpads" )) end manpadTeams[uId] = uData.u end end end end return manpadTeams end end return nil end local function groupDeployManpad(group) -- this won't trigger the deploy of any kind of troops, but only for the manpad team (if there) if group and group:isExist() then local manpadTeams = groupCheckForManpad(group) if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.groupDeployManpad, manpadTeams: " .. tostring(manpadTeams) )) end if manpadTeams and next(manpadTeams) ~= nil then if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.groupDeployManpad, confirmed deployable manpads team" )) end for _, manpads in pairs(manpadTeams) do deployTroops(manpads) timer.scheduleFunction(groupMountTeam, group, timer.getTime() + AIEN.config.remountTime) end end end end -- ## externally access command, by script function AIEN_groupDeploy(gName, noremount) -- this one is global, to provide any user to make a group manually dismount via script or trigger action (do script) if remountVar is true, the dismounted group will go back to its vehicle after about 10 mins. if gName and type(gName) == "string" then local g = Group.getByName(gName) if g then groupDeployTroop(g, noremount) end end end --###### MISSION REACTIONS ######################################################################### --[[ Reactions is probably the most important behaviour change you will notice using this scripts. Reactions are (currently) triggered only by an hit event on one of the group unit. Obvioulsy optimizable, the code structure basically works this way: 1. when the hit event happen, some info are gathered in the event function event_hit that will launch executeActions 2. the function executeActions basically will "try" to execute each of the below actions, in a priority order defined in the event_hit 3. before defining priorities, all these functions are "filtered" by group skills (less skilleg group won't have the most refined solutions) and prioritized by conditions and available informations 4. the first function that return as a "success" is then executed, and the behaviour take place. Side note: suppression, dismount (of all or only manpads) effect are NOT dependand to the scoring model and will take place in parallel. --]]-- local function ac_accelerate(group, ownPos, tgtPos, resume, sa, skill) -- self-explanatory -- doesn't stop a moving group, it simply set its speed as fast as possible. If the group is stationary, it does nothing if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", ac_accelerate launched")) end if group then local s, ms = getGroupSpeed(group) if s and s > 0 then local c = group:getController() if c then c:setSpeed(30, true) -- 30 m/s = 108 km/h return true else if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", ac_accelerate controller not found")) end return false end else if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", ac_accelerate failed to get speed, returning true assuming stationary")) end return true end end return false end local function ac_disperse(group, ownPos, tgtPos, resume, sa, skill) -- basically simply allow for dispersion -- group is the group subject of the action -- pos is, when needed, the reference position for the actions, or own position -- resume is a boolean. If true, after some time the group will resume it's previous condition, else no. -- sa is the SA table passed from the group DB, which hold some useful information for addressing the action if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", ac_disperse launched, move randomly")) end if group then groupAllowDisperse(group) return true else return false end end local function ac_panic(group, ownPos, tgtPos, resume, sa, skill) -- this will make the group to run away randomly.. that mean sometimes even toward the enemy. -- group is the group subject of the action -- pos is, when needed, the reference position for the actions, or own position -- resume is a boolean. If true, after some time the group will resume it's previous condition, else no. -- sa is the SA table passed from the group DB, which hold some useful information for addressing the action if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", ac_panic launched, move randomly")) end if group and group:isExist() and ownPos then if AIEN.config.dismount == true then groupDeployTroop(group, false) end local funcDoAction = function() if group:isExist() then local np = nil while not np do if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", ac_panic creating point...")) end np = getRandTerrainPointInCircle(ownPos, AIEN.config.repositionDistance*10, AIEN.config.repositionDistance*5, true) end moveToPoint(group, np, 50, 5) end end local funcSetParameters = function() if group:isExist() then local c = group:getController() if c then c:setSpeed(30, true) -- 30 m/s = 108 km/h end end end local delay = getReactionTime(skill) timer.scheduleFunction(funcDoAction, nil, timer.getTime() + delay) timer.scheduleFunction(funcSetParameters, nil, timer.getTime() + delay + 5) if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", ac_panic group planned reaction")) end return true end return false end local function ac_dropSmoke(group, ownPos, tgtPos, resume, sa, skill) -- basically spawn smokes around the vehicle and move it for 20-30 meters, trying to hide from enemies -- group is the group subject of the action -- pos is, when needed, the reference position for the actions, or own position -- resume is a boolean. If true, after some time the group will resume it's previous condition, else no. -- sa is the SA table passed from the group DB, which hold some useful information for addressing the action if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", ac_dropSmoke launched")) end if group and group:isExist() and ownPos and sa then local units = group:getUnits() if units then -- check at least 50% units can use smoke local numTot = 0 local numSmk = 0 for iId, iData in pairs(units) do numTot = numTot + 1 if iData:hasAttribute("HeavyArmoredUnits") or iData:hasAttribute("IFV") then numSmk = numSmk +1 end end if numTot > 0 then if numSmk/numTot < 0.5 then if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", ac_dropSmoke dropped cause less than 50% units can do that")) end return false end end end local funcDoAction = function() if group:isExist() then if AIEN.config.smoke_source_num > 9 then AIEN.config.smoke_source_num = 9 elseif AIEN.config.smoke_source_num < 4 then AIEN.config.smoke_source_num = 4 end local units = group:getUnits() local smoked = false if units then -- plan smoke for uId, uData in pairs(units) do if uData:hasAttribute("HeavyArmoredUnits") or uData:hasAttribute("IFV") then local uPos = uData:getPoint() local points = genSmokePoints(uPos, aie_random(15, 30), AIEN.config.smoke_source_num) if points and #points > 0 then if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", ac_dropSmoke points " .. tostring(#points))) end --phase 1 generate smoke for pId, pPos in pairs(points) do local f = function() trigger.action.smoke(pPos, 2) end timer.scheduleFunction(f, nil, timer.getTime() + aie_random(1, 5)) end --phase 2 move in a random point very near (20-30 mt) smoked = true else if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", ac_dropSmoke unable to define smoke points")) end --return false end end end end if smoked == true then moveToPoint(group, ownPos, 5, 14) if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", ac_dropSmoke group planned reaction")) end end end end local delay = getReactionTime(skill) timer.scheduleFunction(funcDoAction, nil, timer.getTime() + delay) return true else if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", ac_dropSmoke missing variables")) end return false end end local function ac_withdraw(group, ownPos, tgtPos, resume, sa, skill) -- this will make the group to run away to the nearest allied ground group -- group is the group subject of the action -- pos is, when needed, the reference position for the actions, or own position -- resume is a boolean. If true, after some time the group will resume it's previous condition, else no. -- sa is the SA table passed from the group DB, which hold some useful information for addressing the action if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", ac_withdraw launched, withdraw")) end if group and ownPos then local bestPos = nil local maxDist = AIEN.config.withrawDist for _, og in pairs(groundgroupsDb) do if og.coa == group:getCoalition() then if og.n ~= group:getName() then if og.group and og.group:isExist() == true then local p = og.sa.pos --local td = og.threat if p then -- and td -- within range local d = getDist(p, ownPos) if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", ac_withdraw d " .. tostring(d))) end if d and d < maxDist and d > 2000 then bestPos = p maxDist = d --break -- just the first one available end end end end end end if bestPos then local funcDoAction = function() if group:isExist() then moveToPoint(group, bestPos, AIEN.config.repositionDistance*1.5, AIEN.config.repositionDistance*0.5, false) end end if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", ac_withdraw group planned reaction")) end local delay = getReactionTime(skill) timer.scheduleFunction(funcDoAction, nil, timer.getTime() + delay) if resume then -- check if there's a route to be followed once action end if group:isExist() then local destination = nil local points = getMEroute(group) if points and #points > 1 then local last = #points local data = points[last] if data and type(data) == "table" then destination = { x = data.x, y = land.getHeight({x = data.x, y = data.y}), z = data.y} end end if not destination then destination = ownPos end local funcresumeRoute = function() if group:isExist() then moveToPoint(group, destination, 200, 10) end end if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", ac_withdraw group planning coming back")) end timer.scheduleFunction(funcresumeRoute, nil, timer.getTime() + aie_random(600, 900)) end end return true else if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", ac_withdraw return false due to missing widraw opportunities")) end return false end else if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", ac_withdraw return false due to missing variable")) end return false end end local function ac_attack(group, ownPos, tgtPos, resume, sa, skill) -- this will make the group to run toward the shooting enemy and open fire -- group is the group subject of the action -- pos is, when needed, the reference position for the actions, or own position -- resume is a boolean. If true, after some time the group will resume it's previous condition, else no. -- sa is the SA table passed from the group DB, which hold some useful information for addressing the action if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", ac_attack launched, move toward enemy")) end if group and tgtPos then local funcDoAction = function() if group:isExist() then local speed = 10 if AIEN.config.dismount == true then local deployed = groupDeployTroop(group, false, tgtPos) if deployed == true then speed = 4 end end moveToPoint(group, tgtPos, 300, 500, false, "cone", nil, nil, nil, speed) end end if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", ac_attack group planned reaction")) end local delay = getReactionTime(skill) timer.scheduleFunction(funcDoAction, nil, timer.getTime() + delay) groupGoShoot(group) if resume then -- check if there's a route to be followed once action end local destination = nil local points = getMEroute(group) if points and #points > 1 then local last = #points local data = points[last] if data and type(data) == "table" then destination = { x = data.x, y = land.getHeight({x = data.x, y = data.y}), z = data.y} end end if not destination then destination = ownPos end local funcresumeRoute = function() if group:isExist() then moveToPoint(group, destination, 200, 10, false) end end if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", ac_attack group planning coming back")) end timer.scheduleFunction(funcresumeRoute, nil, timer.getTime() + aie_random(900, 1200)) end return true else if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", ac_attack return false due to missing variable")) end return false end end local function ac_coverBuildings(group, ownPos, tgtPos, resume, sa, skill) -- this will make the group to run looking cover in a nearby urban area -- group is the group subject of the action -- pos is, when needed, the reference position for the actions, or own position -- resume is a boolean. If true, after some time the group will resume it's previous condition, else no. -- sa is the SA table passed from the group DB, which hold some useful information for addressing the action if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", ac_coverBuildings launched")) end if group and ownPos and sa then -- nearby building (within AIEN.config.proxyBuildingDistance) local bn = 0 local near_b = nil local pN1 = ownPos local pN2 = ownPos local pN3 = ownPos local pN4 = ownPos local pos1, pos2, pos3, pos4 local gCoa = group:getCoalition() if pN1 and pN2 and pN3 and pN4 then pos1 = {x = pN1.x, y = pN1.y, z = pN1.z + AIEN.config.proxyBuildingDistance} pos2 = {x = pN2.x, y = pN2.y, z = pN2.z - AIEN.config.proxyBuildingDistance} pos3 = {x = pN3.x + AIEN.config.proxyBuildingDistance, y = pN3.y, z = pN3.z} pos4 = {x = pN4.x - AIEN.config.proxyBuildingDistance, y = pN4.y, z = pN4.z} local function countBld(p) local _volume = { id = world.VolumeType.SPHERE, params = { point = p, radius = AIEN.config.proxyBuildingDistance, }, } local count = 0 local tblPos = {} local enemies = false local _searchB = function(_obj) pcall(function() if _obj ~= nil then local o_desc = _obj:getDesc() if o_desc then if o_desc.attributes and o_desc.attributes.Buildings then count = count + 1 tblPos[#tblPos+1] = _obj:getPoint() end end end end) end local _searchU = function(_obj) pcall(function() if _obj ~= nil then local o_coa = _obj:getCoalition() if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", ac_coverBuildings o_coa: " .. tostring(o_coa))) end if o_coa then if o_coa ~= gCoa then if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", ac_coverBuildings enemies true! " .. tostring(enemies))) end enemies = true end end end end) end if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", ac_coverBuildings enemies: " .. tostring(enemies))) end world.searchObjects(Object.Category.SCENERY, _volume, _searchB) world.searchObjects(Object.Category.UNIT, _volume, _searchU) if count > 3 and #tblPos > 3 and enemies == false then if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", ac_coverBuildings adding point, count: " .. tostring(count))) end local bestPos = avgVec3(tblPos) return count, bestPos end end if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", ac_coverBuildings starting c1p1")) end local c1, p1 = countBld(pos1) if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", ac_coverBuildings starting c2p2")) end local c2, p2 = countBld(pos2) if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", ac_coverBuildings starting c3p3")) end local c3, p3 = countBld(pos3) if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", ac_coverBuildings starting c4p4")) end local c4, p4 = countBld(pos4) if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", ac_coverBuildings done all P's")) end if p1 or p2 or p3 or p4 then -- at least one should exist local function trovaPuntoPiuVicino(p0, ...) local punti = {...} local puntoPiuVicino = nil local distanzaMinima = nil for _, punto in ipairs(punti) do if punto then local distanza = getDist(p0, punto) if not distanzaMinima or distanza < distanzaMinima then distanzaMinima = distanza puntoPiuVicino = punto end end end return puntoPiuVicino end local dest = trovaPuntoPiuVicino(ownPos, p1, p2, p3, p4) if dest then local funcDoAction = function() if group:isExist() then moveToPoint(group, dest, AIEN.config.repositionDistance, AIEN.config.repositionDistance*0.2) end end if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", ac_coverBuildings group planned reaction")) end local delay = getReactionTime(skill) timer.scheduleFunction(funcDoAction, nil, timer.getTime() + delay) if resume then -- check if there's a route to be followed once action end local destination = nil local points = getMEroute(group) if points and #points > 1 then local last = #points local data = points[last] if data and type(data) == "table" then destination = { x = data.x, y = land.getHeight({x = data.x, y = data.y}), z = data.y} end end if not destination then destination = ownPos end local funcresumeRoute = function() if group:isExist() then if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", ac_coverBuildings group planning coming back to original destination")) end moveToPoint(group, destination, 200, 10) end end timer.scheduleFunction(funcresumeRoute, nil, timer.getTime() + aie_random(420, 900)) end return true else if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", ac_coverBuildings return false due to missing buildings area")) end return false end else if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", ac_coverBuildings didn't found a suitable place")) end return false end else if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", ac_coverBuildings return false due wrong math around the starting point")) end return false end else if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", ac_coverBuildings return false due to missing variable")) end return false end end local function ac_groundSupport(group, ownPos, tgtPos, resume, sa, skill) -- this will make another ground group to come in support -- group is the group subject of the action -- pos is, when needed, the reference position for the actions, or own position -- resume is a boolean. If true, after some time the group will resume it's previous condition, else no. -- sa is the SA table passed from the group DB, which hold some useful information for addressing the action if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", ac_groundSupport launched, move randomly")) end if group and ownPos and sa then local bestPos = nil local bestVal = 0 local bestTd = 1000 local AllyGroup = nil for _, og in pairs(groundgroupsDb) do if og.coa == group:getCoalition() and og.n ~= group:getName() then if og.group and og.group:isExist() == true then if supportGroundClasses[og.class] and supportGroundClasses[og.class] > bestVal then local p = og.sa.pos local td = og.threat if p and td then -- within range local d = getDist(p, ownPos) if d and d < AIEN.config.supportDistance and d > 4000 then bestPos = p bestTd = td/2 bestVal = supportGroundClasses[og.class] AllyGroup = og.group end end end end end end if AllyGroup and bestTd then local funcDoAction = function() if group:isExist() then moveToPoint(AllyGroup, ownPos, bestTd*0.5, bestTd*0.3) end end if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", ac_groundSupport group planned reaction")) end local delay = getReactionTime(skill) timer.scheduleFunction(funcDoAction, nil, timer.getTime() + delay) return true else if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", ac_groundSupport return false due to missing widraw opportunities")) end return false end else if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", ac_groundSupport return false due to missing variable")) end return false end end local function ac_coverADS(group, ownPos, tgtPos, resume, sa, skill) -- this will make the group to run into the effective range of an allied air defence group -- group is the group subject of the action -- pos is, when needed, the reference position for the actions, or own position -- resume is a boolean. If true, after some time the group will resume it's previous condition, else no. -- sa is the SA table passed from the group DB, which hold some useful information for addressing the action if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", ac_coverADS launched")) end if group and ownPos and sa then local bestPos = nil local bestVal = 0 local bestTd = 3000 for _, og in pairs(groundgroupsDb) do if og.coa == group:getCoalition() and og.n ~= group:getName() then if og.group and og.group:isExist() == true then if supportCounterAirClasses[og.class] and supportCounterAirClasses[og.class] > bestVal then local p = og.sa.pos local td = og.threat if p and td then -- within range local d = getDist(p, ownPos) if d and d < AIEN.config.supportDistance*1.5 and d > 2000 then bestPos = p bestTd = td bestVal = supportCounterAirClasses[og.class] end end end end end end if bestPos and bestTd then local funcDoAction = function() if group:isExist() then moveToPoint(group, bestPos, bestTd*0.3, bestTd*0.05) end end if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", ac_coverADS group planned reaction")) end local delay = getReactionTime(skill) timer.scheduleFunction(funcDoAction, nil, timer.getTime() + delay) if resume then -- check if there's a route to be followed once action end local destination = nil local points = getMEroute(group) if points and #points > 1 then local last = #points local data = points[last] if data and type(data) == "table" then destination = { x = data.x, y = land.getHeight({x = data.x, y = data.y}), z = data.y} end end if not destination then destination = ownPos end local funcresumeRoute = function() if group:isExist() then moveToPoint(group, destination, 200, 10) end end if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", ac_coverADS group planning coming back")) end timer.scheduleFunction(funcresumeRoute, nil, timer.getTime() + aie_random(900, 1200)) end return true else if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", ac_coverADS return false due to missing widraw opportunities")) end return false end else if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", ac_coverADS return false due to missing variable")) end return false end end local function ac_fireMissionOnShooter(group, ownPos, tgtPos, resume, sa, skill) -- this will make an allied artillery in range to fire at enemy shooter position -- group is the group subject of the action -- pos is, when needed, the reference position for the actions, or own position -- resume is a boolean. If true, after some time the group will resume it's previous condition, else no. -- sa is the SA table passed from the group DB, which hold some useful information for addressing the action if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", ac_fireMissionOnShooter launched, planning")) end if tgtPos then local arty = nil for _, og in pairs(groundgroupsDb) do if og.coa == group:getCoalition() and og.tasked == false then if og.class == "ARTY" then -- or og.class == "MLRS" -- not considering MLRS as they're intended for more area or tactical fire if og.group and og.group:isExist() == true then local d = getDist(og.sa.pos, tgtPos) if d < og.threat*0.8 then og.tasked = true og.taskTime = timer.getTime() og.firePoint = tgtPos groupfireAtPoint({og.group, tgtPos, 20, "Immediate suppression"}) if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", ac_fireMissionOnShooter return true, planning the fire mission")) end return true end end end end end if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", ac_fireMissionOnShooter return false being unable to plan the fire mission")) end return false else if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", ac_fireMissionOnShooter return false due to missing variable")) end return false end end -- summary tablem holds the "scoring model points" for each condition. This can be seen as a decision matrix for evaluate best reaction available. -- used for fast-filtering actions availability based on group leader skill. -- It basically is an array, where the actions are listed in order of complexity. -- This way, the skill could be converted into a number, and that number will became the maximum index available. -- The higher the skill, the higher the index, the higher the actions that could be evaluated local actionsDb = { [1] = { -- ac_accelerate ["name"] = "ac_accelerate", ["ac_function"] = ac_accelerate, ["message"] = "", ["resume"] = true, ["w_cat"] = { -- weapon category [0] = 0, -- shell [1] = 1, -- missile [2] = 1, -- rocket [3] = 0, -- bomb }, ["s_cat"] = { -- unit category [0] = 1, -- airplane [1] = 0, -- helicopter [2] = 2, -- ground unit [3] = 1, -- ship [4] = 0, -- structure }, ["s_indirect"] = { -- unit category [0] = 2, -- not an indirect fire unit [1] = 0, -- is an indirect fire unit }, ["s_close"] = { -- shooter is within wpn range [0] = 1, -- not so close [1] = 0, -- close }, ["s_fireMis"] = { -- shooter position and speed [0] = 2, -- detailed shooter position not known [1] = 0, -- detailed shooter position known }, ["o_cls"] = { ["MBT"] = 2, ["ATGM"] = 1, ["MLRS"] = 0, ["ARTY"] = 0, ["MISSILE"] = 0, ["MANPADS"] = 0, ["SHORAD"] = 3, ["AAA"] = 3, ["SAM"] = 5, ["IFV"] = 0, ["APC"] = 0, ["RECCE"] = 0, ["LOGI"] = 0, ["INF"] = 0, ["UNKN"] = 0, }, ["s_cls"] = { ["MBT"] = 0.2, ["ATGM"] = 0.4, ["MLRS"] = 2.1, ["ARTY"] = 1.5, ["MISSILE"] = 3.1, ["MANPADS"] = 2, ["SHORAD"] = 2.2, ["AAA"] = 2.8, ["SAM"] = 2.3, ["IFV"] = 0.4, ["APC"] = 0.9, ["RECCE"] = 1.2, ["LOGI"] = 1.9, ["INF"] = 0.8, ["UNKN"] = 1, ["ARBN"] = 1.4, }, }, [2] = { -- ac_panic ["name"] = "ac_panic", ["ac_function"] = ac_panic, ["message"] = "We're trying to escape fire%!", ["resume"] = true, ["w_cat"] = { -- weapon category [0] = 0.3, -- shell [1] = 2, -- missile [2] = 0.5, -- rocket [3] = 0, -- bomb }, ["s_cat"] = { -- unit category [0] = 2, -- airplane [1] = 2.5, -- helicopter [2] = 1, -- ground unit [3] = 1.5, -- ship [4] = 0, -- structure }, ["s_indirect"] = { -- unit category [0] = 0, -- not an indirect fire unit [1] = 0.5, -- is an indirect fire unit }, ["s_close"] = { -- shooter is within wpn range [0] = 0.5, -- not so close [1] = 0, -- close }, ["s_fireMis"] = { -- shooter position and speed [0] = 2, -- detailed shooter position not known [1] = 0, -- detailed shooter position known }, ["o_cls"] = { ["MBT"] = 1.5, ["ATGM"] = 1.3, ["MLRS"] = 1, ["ARTY"] = 1, ["MISSILE"] = 2, ["MANPADS"] = 2, ["SHORAD"] = 0, ["AAA"] = 0, ["SAM"] = 0, ["IFV"] = 0.7, ["APC"] = 0.5, ["RECCE"] = 0, ["LOGI"] = 0.2, ["INF"] = 0, ["UNKN"] = 0.1, }, ["s_cls"] = { ["MBT"] = 1.7, ["ATGM"] = 1.6, ["MLRS"] = 1.3, ["ARTY"] = 1, ["MISSILE"] = 0.5, ["MANPADS"] = 0, ["SHORAD"] = 0, ["AAA"] = 0, ["SAM"] = 0, ["IFV"] = 1.4, ["APC"] = 0.9, ["RECCE"] = 0.3, ["LOGI"] = 0.2, ["INF"] = 0.1, ["UNKN"] = 1, ["ARBN"] = 1.8, }, }, [3] = { -- ac_disperse ["name"] = "ac_disperse", ["ac_function"] = ac_disperse, ["message"] = "We're stuck here, we ask support if available", ["resume"] = true, ["w_cat"] = { -- weapon category [0] = 1, -- shell [1] = 2, -- missile [2] = 1, -- rocket [3] = 2, -- bomb }, ["s_cat"] = { -- unit category [0] = 0, -- airplane [1] = 0, -- helicopter [2] = 2, -- ground unit [3] = 3, -- ship [4] = 0, -- structure }, ["s_indirect"] = { -- unit category [0] = 0, -- not an indirect fire unit [1] = 2, -- is an indirect fire unit }, ["s_close"] = { -- shooter is within wpn range [0] = 1, -- not so close [1] = 0, -- close }, ["s_fireMis"] = { -- shooter position and speed [0] = 0, -- detailed shooter position not known [1] = 0, -- detailed shooter position known }, ["o_cls"] = { ["MBT"] = 1.5, ["ATGM"] = 1.3, ["MLRS"] = 1, ["ARTY"] = 1, ["MISSILE"] = 2, ["MANPADS"] = 2, ["SHORAD"] = 0, ["AAA"] = 0, ["SAM"] = 0, ["IFV"] = 0.7, ["APC"] = 0.5, ["RECCE"] = 0, ["LOGI"] = 0.2, ["INF"] = 0, ["UNKN"] = 0.1, }, ["s_cls"] = { ["MBT"] = 1, ["ATGM"] = 1.2, ["MLRS"] = 2.5, ["ARTY"] = 2.5, ["MISSILE"] = 2.8, ["MANPADS"] = 0.3, ["SHORAD"] = 1.5, ["AAA"] = 0.6, ["SAM"] = 0.6, ["IFV"] = 0.8, ["APC"] = 0.7, ["RECCE"] = 0.3, ["LOGI"] = 0.2, ["INF"] = 0.1, ["UNKN"] = 1, ["ARBN"] = 0.4, }, }, [4] = { -- ac_dropSmoke ["name"] = "ac_dropSmoke", ["ac_function"] = ac_dropSmoke, ["message"] = "Dropping smoke cover", ["resume"] = true, ["w_cat"] = { -- weapon category [0] = 0, -- shell [1] = 5, -- missile [2] = 1, -- rocket [3] = 2, -- bomb }, ["s_cat"] = { -- unit category [0] = 3, -- airplane [1] = 5, -- helicopter [2] = 1, -- ground unit [3] = 0, -- ship [4] = 0, -- structure }, ["s_indirect"] = { -- unit category [0] = 1, -- not an indirect fire unit [1] = 0, -- is an indirect fire unit }, ["s_close"] = { -- shooter is within wpn range [0] = 0, -- not so close [1] = 1, -- close }, ["s_fireMis"] = { -- shooter position and speed [0] = 0, -- detailed shooter position not known [1] = 0, -- detailed shooter position known }, ["o_cls"] = { ["MBT"] = 3, ["ATGM"] = 2, ["MLRS"] = 0, ["ARTY"] = 0, ["MISSILE"] = 0, ["MANPADS"] = 0, ["SHORAD"] = 0, ["AAA"] = 0, ["SAM"] = 0, ["IFV"] = 2, ["APC"] = 2.2, ["RECCE"] = 0, ["LOGI"] = 0, ["INF"] = 0, ["UNKN"] = 0, }, ["s_cls"] = { ["MBT"] = 2.1, ["ATGM"] = 1.8, ["MLRS"] = 0, ["ARTY"] = 0, ["MISSILE"] = 0, ["MANPADS"] = 0, ["SHORAD"] = 0, ["AAA"] = 0, ["SAM"] = 0, ["IFV"] = 2, ["APC"] = 2.3, ["RECCE"] = 0, ["LOGI"] = 0, ["INF"] = 0, ["UNKN"] = 0, ["ARBN"] = 3, }, }, [5] = { -- ac_withdraw ["name"] = "ac_withdraw", ["ac_function"] = ac_withdraw, ["message"] = "We're moving in safer area", ["resume"] = true, ["w_cat"] = { -- weapon category [0] = 3, -- shell [1] = 2, -- missile [2] = 1, -- rocket [3] = 1, -- bomb }, ["s_cat"] = { -- unit category [0] = 0, -- airplane [1] = 0, -- helicopter [2] = 2, -- ground unit [3] = 2, -- ship [4] = 1, -- structure }, ["s_indirect"] = { -- unit category [0] = 0, -- not an indirect fire unit [1] = 2, -- is an indirect fire unit }, ["s_close"] = { -- shooter is within wpn range [0] = 2, -- not so close [1] = 0, -- close }, ["s_fireMis"] = { -- shooter position and speed [0] = 0, -- detailed shooter position not known [1] = 3, -- detailed shooter position known }, ["o_cls"] = { ["MBT"] = 0, ["ATGM"] = 0, ["MLRS"] = 0, ["ARTY"] = 1, ["MISSILE"] = 1, ["MANPADS"] = 1, ["SHORAD"] = 2, ["AAA"] = 2, ["SAM"] = 0, ["IFV"] = 0, ["APC"] = 0, ["RECCE"] = 0, ["LOGI"] = 3, ["INF"] = 3, ["UNKN"] = 1, }, ["s_cls"] = { ["MBT"] = 0, ["ATGM"] = 1.8, ["MLRS"] = 0.6, ["ARTY"] = 0.4, ["MISSILE"] = 0.5, ["MANPADS"] = 0.1, ["SHORAD"] = 0.3, ["AAA"] = 0.2, ["SAM"] = 0.2, ["IFV"] = 1.5, ["APC"] = 0.7, ["RECCE"] = 0.3, ["LOGI"] = 0.2, ["INF"] = 0.1, ["UNKN"] = 1, ["ARBN"] = 0.7, }, }, [6] = { -- ac_attack ["name"] = "ac_attack", ["ac_function"] = ac_attack, ["message"] = "We're going to ambush the enemy", ["resume"] = true, ["w_cat"] = { -- weapon category [0] = 2, -- shell [1] = 0, -- missile [2] = 1, -- rocket [3] = 0, -- bomb }, ["s_cat"] = { -- unit category [0] = 0, -- airplane [1] = 0, -- helicopter [2] = 2, -- ground unit [3] = 0, -- ship [4] = 2, -- structure }, ["s_indirect"] = { -- unit category [0] = 2, -- not an indirect fire unit [1] = 0, -- is an indirect fire unit }, ["s_close"] = { -- shooter is within wpn range [0] = 1, -- not so close [1] = 2, -- close }, ["s_fireMis"] = { -- shooter position and speed [0] = 2, -- detailed shooter position not known [1] = 0, -- detailed shooter position known }, ["o_cls"] = { ["MBT"] = 0, ["ATGM"] = 0, ["MLRS"] = 2, ["ARTY"] = 0, ["MISSILE"] = 0, ["MANPADS"] = 0, ["SHORAD"] = 2, ["AAA"] = 0, ["SAM"] = 0, ["IFV"] = 1, ["APC"] = 0, ["RECCE"] = 0, ["LOGI"] = 0, ["INF"] = 0, ["UNKN"] = 0, }, ["s_cls"] = { ["MBT"] = 0, ["ATGM"] = 0, ["MLRS"] = 3, ["ARTY"] = 2.5, ["MISSILE"] = 3, ["MANPADS"] = 3, ["SHORAD"] = 1.3, ["AAA"] = 2.2, ["SAM"] = 2.2, ["IFV"] = 0.6, ["APC"] = 0.9, ["RECCE"] = 1.7, ["LOGI"] = 4, ["INF"] = 2.4, ["UNKN"] = 1, ["ARBN"] = 0, }, }, [7] = { -- ac_coverBuildings ["name"] = "ac_coverBuildings", ["ac_function"] = ac_coverBuildings, ["message"] = "We're moving nearby the closest urbanized area for concealment", ["resume"] = true, ["w_cat"] = { -- weapon category [0] = 2, -- shell [1] = 2, -- missile [2] = 2, -- rocket [3] = 2, -- bomb }, ["s_cat"] = { -- unit category [0] = 2, -- airplane [1] = 2, -- helicopter [2] = 1, -- ground unit [3] = 2, -- ship [4] = 0, -- structure }, ["s_indirect"] = { -- unit category [0] = 2, -- not an indirect fire unit [1] = 0, -- is an indirect fire unit }, ["s_close"] = { -- shooter is within wpn range [0] = 1, -- not so close [1] = 2, -- close }, ["s_fireMis"] = { -- shooter position and speed [0] = 0, -- detailed shooter position not known [1] = 2, -- detailed shooter position known }, ["o_cls"] = { ["MBT"] = 0, ["ATGM"] = 1, ["MLRS"] = 2, ["ARTY"] = 1, ["MISSILE"] = 0, ["MANPADS"] = 3, ["SHORAD"] = 3, ["AAA"] = 2, ["SAM"] = 0, ["IFV"] = 2, ["APC"] = 3, ["RECCE"] = 3, ["LOGI"] = 3, ["INF"] = 3, ["UNKN"] = 3, }, ["s_cls"] = { ["MBT"] = 1.3, ["ATGM"] = 1.8, ["MLRS"] = 2, ["ARTY"] = 2, ["MISSILE"] = 0.5, ["MANPADS"] = 0.1, ["SHORAD"] = 0.3, ["AAA"] = 0.2, ["SAM"] = 0.2, ["IFV"] = 1.4, ["APC"] = 1, ["RECCE"] = 0.3, ["LOGI"] = 0.2, ["INF"] = 0.1, ["UNKN"] = 1, ["ARBN"] = 2, }, }, [8] = { -- ac_groundSupport ["name"] = "ac_groundSupport", ["ac_function"] = ac_groundSupport, ["message"] = "We asked for ground support, they're on the way", ["resume"] = true, ["w_cat"] = { -- weapon category [0] = 0, -- shell [1] = 1, -- missile [2] = 0, -- rocket [3] = 0, -- bomb }, ["s_cat"] = { -- unit category [0] = 0, -- airplane [1] = 0, -- helicopter [2] = 4, -- ground unit [3] = 0, -- ship [4] = 2, -- structure }, ["s_indirect"] = { -- unit category [0] = 1, -- not an indirect fire unit [1] = 0, -- is an indirect fire unit }, ["s_close"] = { -- shooter is within wpn range [0] = 0, -- not so close [1] = 2, -- close }, ["s_fireMis"] = { -- shooter position and speed [0] = 0, -- detailed shooter position not known [1] = 0, -- detailed shooter position known }, ["o_cls"] = { ["MBT"] = 1, ["ATGM"] = 1, ["MLRS"] = 2, ["ARTY"] = 2, ["MISSILE"] = 3, ["MANPADS"] = 2, ["SHORAD"] = 2, ["AAA"] = 2, ["SAM"] = 2, ["IFV"] = 1, ["APC"] = 2, ["RECCE"] = 1, ["LOGI"] = 2, ["INF"] = 3, ["UNKN"] = 1, }, ["s_cls"] = { ["MBT"] = 1.8, ["ATGM"] = 1.4, ["MLRS"] = 3, ["ARTY"] = 2.5, ["MISSILE"] = 3, ["MANPADS"] = 3, ["SHORAD"] = 1.3, ["AAA"] = 1, ["SAM"] = 0.9, ["IFV"] = 2, ["APC"] = 2.2, ["RECCE"] = 1.7, ["LOGI"] = 1.1, ["INF"] = 0.4, ["UNKN"] = 1, ["ARBN"] = 0, }, }, [9] = { -- ac_coverADS ["name"] = "ac_coverADS", ["ac_function"] = ac_coverADS, ["resume"] = false, ["message"] = "Attack comes from airborne asset, we are moving within the closest air defence covered area", ["w_cat"] = { -- weapon category [0] = 1, -- shell [1] = 3, -- missile [2] = 2, -- rocket [3] = 3, -- bomb }, ["s_cat"] = { -- unit category [0] = 5, -- airplane [1] = 5, -- helicopter [2] = 0, -- ground unit [3] = 0, -- ship [4] = 0, -- structure }, ["s_indirect"] = { -- unit category [0] = 2, -- not an indirect fire unit [1] = 2, -- is an indirect fire unit }, ["s_close"] = { -- shooter is within wpn range [0] = 2, -- not so close [1] = 2, -- close }, ["s_fireMis"] = { -- shooter position and speed [0] = 0, -- detailed shooter position not known [1] = 0, -- detailed shooter position known }, ["o_cls"] = { ["MBT"] = 3, ["ATGM"] = 2, ["MLRS"] = 3, ["ARTY"] = 3, ["MISSILE"] = 3, ["MANPADS"] = 1, ["SHORAD"] = 0, ["AAA"] = 1, ["SAM"] = 0, ["IFV"] = 3, ["APC"] = 3, ["RECCE"] = 3, ["LOGI"] = 3, ["INF"] = 3, ["UNKN"] = 3, }, ["s_cls"] = { ["MBT"] = 1.8, ["ATGM"] = 1.4, ["MLRS"] = 3, ["ARTY"] = 2.5, ["MISSILE"] = 3, ["MANPADS"] = 3, ["SHORAD"] = 1.3, ["AAA"] = 1, ["SAM"] = 0.9, ["IFV"] = 2, ["APC"] = 2.2, ["RECCE"] = 1.7, ["LOGI"] = 1.1, ["INF"] = 0.4, ["UNKN"] = 1, ["ARBN"] = 5, }, }, [10] = { -- ac_fireMissionOnShooter ["name"] = "ac_fireMissionOnShooter", ["ac_function"] = ac_fireMissionOnShooter, ["resume"] = true, ["message"] = "We got the enemy position and asked for indirect fire mission.", ["w_cat"] = { -- weapon category [0] = 3, -- shell [1] = 3, -- missile [2] = 3, -- rocket [3] = 3, -- bomb }, ["s_cat"] = { -- unit category [0] = 0, -- airplane [1] = 3, -- helicopter [2] = 2, -- ground unit [3] = 3, -- ship [4] = 5, -- structure }, ["s_indirect"] = { -- unit category [0] = 2, -- not an indirect fire unit [1] = 0, -- is an indirect fire unit }, ["s_close"] = { -- shooter is within wpn range [0] = 5, -- not so close [1] = 0, -- close }, ["s_fireMis"] = { -- shooter position and speed [0] = 0, -- detailed shooter position not known [1] = 5, -- detailed shooter position known }, ["o_cls"] = { ["MBT"] = 1, ["ATGM"] = 1, ["MLRS"] = 3, ["ARTY"] = 3, ["MISSILE"] = 1, ["MANPADS"] = 1, ["SHORAD"] = 1, ["AAA"] = 1, ["SAM"] = 1, ["IFV"] = 1, ["APC"] = 1, ["RECCE"] = 1, ["LOGI"] = 1, ["INF"] = 1, ["UNKN"] = 2, ["ARBN"] = 1, }, ["s_cls"] = { ["MBT"] = 1.8, ["ATGM"] = 1.4, ["MLRS"] = 3, ["ARTY"] = 2.5, ["MISSILE"] = 3, ["MANPADS"] = 3, ["SHORAD"] = 1.3, ["AAA"] = 1, ["SAM"] = 0.9, ["IFV"] = 2, ["APC"] = 2.2, ["RECCE"] = 1.7, ["LOGI"] = 1.1, ["INF"] = 0.4, ["UNKN"] = 1, ["ARBN"] = 5, }, }, --[[ [11] = { ["name"] = "ac_airSupport", ["ac_function"] = ac_airSupport, ["w_cat"] = { -- weapon category [0] = 2, -- shell [1] = 2, -- missile [2] = 2, -- rocket [3] = 2, -- bomb }, ["s_cat"] = { -- unit category [0] = 0, -- airplane [1] = 2, -- helicopter [2] = 4, -- ground unit [3] = 0, -- ship [4] = 3, -- structure }, ["s_indirect"] = { -- unit category [0] = 1, -- not an indirect fire unit [1] = 1, -- is an indirect fire unit }, ["s_close"] = { -- shooter is within wpn range [0] = 2, -- not so close [1] = 3, -- close }, ["s_fireMis"] = { -- shooter position and speed [0] = 0, -- detailed shooter position not known [1] = 5, -- detailed shooter position known }, ["o_cls"] = { ["MBT"] = 2, ["ATGM"] = 2, ["MLRS"] = 2, ["ARTY"] = 2, ["MISSILE"] = 2, ["MANPADS"] = 1, ["SHORAD"] = 2, ["AAA"] = 2, ["SAM"] = 2, ["IFV"] = 2, ["APC"] = 2, ["RECCE"] = 2, ["LOGI"] = 1, ["INF"] = 1, ["UNKN"] = 0, }, ["o_cls"] = { ["MBT"] = 3, ["ATGM"] = 3, ["MLRS"] = 3, ["ARTY"] = 3, ["MISSILE"] = 3, ["MANPADS"] = 0.4, ["SHORAD"] = 0.2, ["AAA"] = 0.4, ["SAM"] = 0, ["IFV"] = 2.8, ["APC"] = 2, ["RECCE"] = 2, ["LOGI"] = 1, ["INF"] = 1, ["UNKN"] = 0, ["ARBN"] = 1.9, }, }, --]]-- } -- the functions that handles the reactions, using priorities local function executeActions(gr, ownPos, tgtPos, actTbl, saTbl, skill) local act = nil if gr and gr:isExist() and ownPos and tgtPos and actTbl and saTbl and skill then if actTbl and #actTbl>0 then for aId, aData in pairs(actTbl) do for _, dbActData in pairs(actionsDb) do if aData.name == dbActData.name then local f = dbActData.ac_function if f then trigger.action.groupContinueMoving(gr) local success = f(gr, ownPos, tgtPos, dbActData.resume, saTbl, skill) if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.executeActions, action success = " .. tostring(success))) end if success and success == true then -- message feedback if AIEN.config.message_feed == true then local lat, lon = coord.LOtoLL(ownPos) local MGRS = coord.LLtoMGRS(coord.LOtoLL(ownPos)) if lat and lon then local LL_string = tostringLL(lat, lon, 0, true) local MGRS_string = tostringMGRS(MGRS ,4) local txt = "" local txt = txt .. "C2, " .. tostring(gr:getName()) .. ", report under attack. Coordinates: " .. tostring(LL_string) .. ", " .. tostring(MGRS_string) .. "." .. dbActData.message local vars = {"text", txt, 30, nil, nil, nil, gr:getCoalition()} multyTypeMessage(vars) end end return dbActData.name end end end end end else if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.executeActions, actTbl missing or void")) end return false end else if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.executeActions error, missing one or more variables:")) env.info(("AIEN.executeActions error: " .. tostring(gr))) env.info(("AIEN.executeActions error: " .. tostring(ownPos))) env.info(("AIEN.executeActions error: " .. tostring(tgtPos))) env.info(("AIEN.executeActions error: " .. tostring(actTbl))) env.info(("AIEN.executeActions error: " .. tostring(saTbl))) env.info(("AIEN.executeActions error: " .. tostring(skill))) end return false end end -- this 'global' test function let you test a specific reaction of your choice, using the group name and the reaction name to execute. -- for those action where the shooter is required for evaluation, the test function will look for the nearest target within 20 km. -- if data are not gathered, it will print an advice function AIEN_testActions(groupName, actionName) if groupName and type(groupName) == "string" and actionName and type(actionName) == "string" then --local gr = Group.getByName(groupName) -- get group info local gr = nil local saTbl = nil local ownPos = nil local skill = nil local coa = nil for _, gData in pairs(groundgroupsDb) do if groupName == gData.n then gr = gData.group saTbl = gData.sa ownPos = gData.sa.pos skill = gData.skill coa = gData.coa end end -- get action info local actionFunc = nil local actionResume = nil local actionMess = nil for _, aData in pairs(actionsDb) do if actionName == aData.name then actionFunc = aData.ac_function actionResume = aData.resume actionMess = aData.message end end -- filter conditions if gr and saTbl and ownPos and skill and coa and actionFunc then -- get nearest enemy local tgtPos = nil local maxDist = 20000 local _volume = { id = world.VolumeType.SPHERE, params = { point = ownPos, radius = 20000, }, } local _search = function(_obj) pcall(function() if _obj ~= nil and Object.getCategory(_obj) == 1 and _obj:isExist() and _obj:getCoalition() ~= coa then local _objPos = _obj:getPoint() if _objPos then local d = getDist(_objPos, ownPos) if d and d < maxDist then maxDist = d tgtPos = _objPos end end end end) end world.searchObjects(Object.Category.UNIT, _volume, _search) -- tgtPos might be unnecessary, therefore I don't check it. local success = actionFunc(gr, ownPos, tgtPos, actionResume, saTbl, skill) if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.AIEN_testActions, result " .. tostring(success))) end if success and success == true then -- message feedback if AIEN.config.message_feed == true then local lat, lon = coord.LOtoLL(ownPos) if lat and lon then local LL_string = tostringLL(lat, lon, 0, true) local txt = "" local txt = txt .. "C2, " .. tostring(gr:getName()) .. ", report under attack. Coordinates: " .. tostring(LL_string) .. "." .. tostring(actionMess) local vars = {"text", txt, 20, nil, nil, nil, gr:getCoalition()} multyTypeMessage(vars) end end return actionName end end return nil end end --###### DB CONSTRUCTION & HANDLING ################################################################ --[[ DB structure each db element is added as this: [objectID] = {group = Group Object, class = Result of getGroupClass function, i = index in table} They're not array to speed up the object calls when needed, cause you can simply do a referencedDB[objectID] call w/o coding for table loop when a db is referred to a unit, to skip units loop in the group (i.e. droneunitDb), the "group" key is replaced by "unit" DBs are used mostly for FSM loops, that are needed to keep a low impact on the process (FSM 1st level will loop db's, while FSM 2nd level will loop each entry one every phaseCycleTimer timer (default 0.2 seconds)). Not all the DBs are used in loops, some are only event-related like the ones used for dismount options. ]]-- local function populate_Db() -- this one is launched once at mission start and collect everything relevant that is already there. -- only ground groups groundgroupsDb = {} for i = 0, 2 do for _, gp in pairs(coalition.getGroups(i,2)) do -- ground only if gp:isExist() then local c = getGroupClass(gp) local gpcoa = gp:getCoalition() -- classes reminder from getGroupClass: -- MBT -- ATGM -- IFV -- APC -- RECCE -- LOGI -- MLRS -- ARTY -- MISSILE -- MANPADS -- SHORAD -- AAA -- SAM -- INF -- UNKN local s = getGroupSkillNum(gp) env.info((tostring(ModuleName) .. ", populate_Db: s " .. tostring(s))) local det, thr = getRanges(gp) if c then --local r = getMEroute(gp) groundgroupsDb[gp:getID()] = {group = gp, class = c, n = gp:getName(), coa = gpcoa, detection = det, threat = thr, tasked = false, skill = s} --, route = r env.info((tostring(ModuleName) .. ", populate_Db: adding to groundgroupsDb " .. tostring(gp:getName() .. ", class " .. tostring(c) ))) else env.info((tostring(ModuleName) .. ", populate_Db: skipping group due to unable to identify class " .. tostring(gp:getName() ))) end -- dismount dbs if AIEN.config.dismount == true then if gp:getUnits() and #gp:getUnits() > 0 then for _, un in pairs(gp:getUnits()) do if un:hasAttribute("IFV") or un:hasAttribute("APC") or un:hasAttribute("Trucks") then -- define dismount capacity local people = defineTroopsNumber(un) infcarrierDb[un:getID()] = people -- define pre-loaded groups! local function loadTeam(unit) local refTbl = dismountTeamsWest if gpcoa == 1 then refTbl = dismountTeamsEast end local r = aie_random(1,100) --env.info((tostring(ModuleName) .. ", populate_Db: random for " .. tostring(unit:getName()) .. ": " .. tostring(r) )) local c = nil local i = nil local lim = 0 for tName, tData in pairs(refTbl) do if r > tData.p and tData.p > lim then c = tData.c i = tData.id lim = tData.p --env.info((tostring(ModuleName) .. ", populate_Db: found " .. tostring(i) )) end end if c then local curMount = mountedDb[unit:getID()] or {} env.info((tostring(ModuleName) .. ", populate_Db: adding " .. tostring(i) .. " to " .. tostring(unit:getName()) )) curMount[#curMount+1] = c mountedDb[unit:getID()] = curMount local freePlace = infcarrierDb[un:getID()] freePlace = freePlace - #c infcarrierDb[un:getID()] = freePlace end end local loadings = math.floor(people/4) for i = 1, loadings do if infcarrierDb[un:getID()] >=4 then loadTeam(un) end end end end end end -- set prevent disperse groupPreventDisperse(gp) end end end -- only drone units droneunitDb = {} for i = 0, 2 do for _, gp in pairs(coalition.getGroups(i,0)) do -- airplane only if gp:isExist() then local c = nil if gp:getUnits() then for _, un in pairs(gp:getUnits()) do if un:hasAttribute("UAVs") then -- drone only c = "UAV" end end end if c then env.info((tostring(ModuleName) .. ", populate_Db: adding to droneunitDb " .. tostring(gp:getName() ))) droneunitDb[gp:getID()] = {group = gp, class = c, n = gp:getName(), coa = gp:getCoalition()} end end end end if AIEN.config.AIEN_debugProcessDetail and AIEN_io and AIEN_lfs then dumpTableAIEN("groundgroupsDb.lua", groundgroupsDb, "int") dumpTableAIEN("droneunitDb.lua", droneunitDb, "int") dumpTableAIEN("infcarrierDb.lua", infcarrierDb, "int") dumpTableAIEN("mountedDb.lua", mountedDb, "int") end end --###### FINITE STATE MACHINE LOOP ################################################################# --[[ FSM is the key element that allow this script to be as lightweight as possibile (for my low skills), cause basically make all the recurring functions to run each every "n" time instead of all-together at once every second. There are 2 levels of FSM: - 1st level is the "bigger" one that is divided in phases: each phase update a DB table, plus a fourth one that handle the artillery groups fire missions. - 2nd level is the "group cycle", that handle each database entry update. Each time a 2nd level cycle is complete, the subsequent 1st level start and at the end it will simply re-start from the first. Check AIEN.performPhaseCycle() for 1st level cycle. ]]-- -- utils local function createIterator(t) local keys = {} for key in pairs(t) do table.insert(keys, key) end table.sort(keys) return keys end local function getNextKey(keys, currentKey) for i, key in ipairs(keys) do if key == currentKey then return keys[i + 1] end end return nil end -- 2ND LEVEL CYCLE FUNCTIONS -- SA update, PHASE "A" local function update_GROUND() if PHASE == "A" then -- confirm correct PHASE of performPhaseCycle if groundgroupsDb and next(groundgroupsDb) ~= nil then -- check that table exist and that it's not void if not phase_index then -- escape condition from the 2nd loop! AIEN.changePhase() timer.scheduleFunction(AIEN.performPhaseCycle, {}, timer.getTime() + phaseCycleTimer) -- debug steps if AIEN.config.AIEN_debugProcessDetail and AIEN_io and AIEN_lfs then dumpTableAIEN("groundgroupsDb.lua", groundgroupsDb, "int") end if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", update_GROUND: phase A completed")) end else local gData = groundgroupsDb[phase_index] if gData then local remove = false if gData.group then if gData.group and gData.group:isExist() == true and gData.group:getUnits() then -- filter under attack, SA already gained and need to focus on reactions if not underAttack[phase_index] then -- update/create sa gData.sa = getSA(gData.group) -- check tasked if gData.tasked == true and gData.taskTime then if timer.getTime() - gData.taskTime >= AIEN.config.taskTimeout then if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", update_GROUND, group name " .. tostring(gData.n) .. " is still tasked. Removing it")) end gData.tasked = false gData.taskTime = nil end end else local t = timer.getTime() - underAttack[phase_index] if t > AIEN.config.taskTimeout*2 then underAttack[phase_index] = nil if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", update_GROUND, group name " .. tostring(gData.n) .. " removed from the under attack table")) end else if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", update_GROUND, group name " .. tostring(gData.n) .. " is still under attack")) end end end else if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", update_GROUND, group name " .. tostring(gData.n) .. " other variables does not exist, remove true")) end remove = true end else if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", update_GROUND, group name " .. tostring(gData.n) .. " gData.group does not exist, remove true")) end remove = true end if remove == true then if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", update_GROUND, group name " .. tostring(gData.n) .. " missing. Removing it")) end groundgroupsDb[phase_index] = nil phase_keys = createIterator(groundgroupsDb) end end phase_index = getNextKey(phase_keys, phase_index) timer.scheduleFunction(AIEN.performPhaseCycle, {}, timer.getTime() + phaseCycleTimer) end else PHASE = "Initialization" if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", update_GROUND, reinizializzazione dei DB, poiché groundgroupsDb sembra vuoto o inesistente!")) end timer.scheduleFunction(AIEN.performPhaseCycle, {}, timer.getTime() + phaseCycleTimer) end end end -- ISR update, PHASE "B" local function update_ISR() -- basically clean old ISR data if PHASE == "B" then -- confirm correct PHASE of performPhaseCycle if intelDb and next(intelDb) ~= nil then -- check that table exist and that it's not void if not phase_index then -- escape condition from the 2nd loop! AIEN.changePhase() timer.scheduleFunction(AIEN.performPhaseCycle, {}, timer.getTime() + phaseCycleTimer) -- debug steps if AIEN.config.AIEN_debugProcessDetail and AIEN_io and AIEN_lfs then dumpTableAIEN("intelDb.lua", intelDb, "int") end if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", update_ISR: fase B completed")) end else local tData = intelDb[phase_index] if tData then --local remove = false if not tData.obj or tData.obj:isExist() == false then if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", update_ISR, target id " .. tostring(phase_index) .. " missing. Removing it")) end intelDb[phase_index] = nil phase_keys = createIterator(intelDb) else if tData.targeted then if type(tData.targeted) == "number" then if timer.getTime() - tData.targeted >= AIEN.config.targetedTimeout then if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", update_ISR, target id " .. tostring(phase_index) .. " is still targeted. Removing it")) end intelDb[phase_index].targeted = nil end end end end end phase_index = getNextKey(phase_keys, phase_index) timer.scheduleFunction(AIEN.performPhaseCycle, {}, timer.getTime() + phaseCycleTimer) end else AIEN.changePhase() timer.scheduleFunction(AIEN.performPhaseCycle, {}, timer.getTime() + phaseCycleTimer) -- debug steps if AIEN.config.AIEN_debugProcessDetail and AIEN_io and AIEN_lfs then dumpTableAIEN("intelDb.lua", intelDb, "int") end if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", update_ISR: fase B skipped")) end end end end -- DRONE update, PHASE "C" local function update_DRONE() if PHASE == "C" then -- confirm correct PHASE of performPhaseCycle if droneunitDb and next(droneunitDb) ~= nil then -- check that table exist and that it's not void if not phase_index then -- escape condition from the 2nd loop! AIEN.changePhase() timer.scheduleFunction(AIEN.performPhaseCycle, {}, timer.getTime() + phaseCycleTimer) -- debug steps if AIEN.config.AIEN_debugProcessDetail and AIEN_io and AIEN_lfs then dumpTableAIEN("droneunitDb.lua", droneunitDb, "int") end if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", update_DRONE: fase B completata")) end else local dData = droneunitDb[phase_index] if dData then local remove = false if dData.group then if dData.group and dData.group:isExist() == true then -- update/create sa if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", update_DRONE, add SA " .. tostring(dData.n))) end dData.sa = getSA(dData.group) else remove = true end else remove = true end if remove == true then if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", update_DRONE, group name " .. tostring(dData.n) .. " missing. Removing it")) end droneunitDb[phase_index] = nil phase_keys = createIterator(droneunitDb) end end phase_index = getNextKey(phase_keys, phase_index) timer.scheduleFunction(AIEN.performPhaseCycle, {}, timer.getTime() + phaseCycleTimer) end else if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", update_DRONE, no drone available!")) end AIEN.changePhase() timer.scheduleFunction(AIEN.performPhaseCycle, {}, timer.getTime() + phaseCycleTimer) if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", update_DRONE: fase B completata")) end end end end -- ARTY update, PHASE "D" local function update_ARTY() if PHASE == "D" then -- confirm correct PHASE of performPhaseCycle if groundgroupsDb and next(groundgroupsDb) ~= nil then -- check that table exist and that it's not void if not phase_index or AIEN.config.firemissions == false then -- escape condition from the 2nd loop! AIEN.changePhase() timer.scheduleFunction(AIEN.performPhaseCycle, {}, timer.getTime() + phaseCycleTimer) if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", update_ARTY: phase D completed or skipped")) end else if not underAttack[phase_index] then local gData = groundgroupsDb[phase_index] if gData then local AI_consent = true if gData.coa == 2 and AIEN.config.blueAI == false then AI_consent = false end if gData.coa == 1 and AIEN.config.redAI == false then AI_consent = false end local remove = false if AI_consent == true and groupAllowedForAI(gData.group) == true then -- both coalition AI should be on and group exclusion tag shouldn't be there if gData.group then if gData.group and gData.group:isExist() == true and gData.sa then if not underAttack[phase_index] and gData.tasked == false then if gData.class == "MLRS" or gData.class == "ARTY" then if gData.threat then -- check ammo local ammoAvail = 0 local units = gData.group:getUnits() local curPos = nil for uId, uData in pairs(units) do local ammoTbl = uData:getAmmo() if ammoTbl then for aId, aData in pairs(ammoTbl) do if aId == 1 then if aData.count > ammoAvail then ammoAvail = aData.count + ammoAvail end end end end end local roundsToFire = 0 if ammoAvail > 30 then roundsToFire = 30 else roundsToFire = ammoAvail end if roundsToFire > 0 then -- check targets local firePoint = nil local firePoints = {} local targetId = nil local _volume = { id = world.VolumeType.SPHERE, params = { point = gData.sa.pos, radius = gData.threat*0.85, }, } local curPri = 0 local _search = function(_obj) pcall(function() if _obj ~= nil and Object.getCategory(_obj) == 1 and _obj:isExist() and _obj:getCoalition() ~= gData.coa then local _obj_id = _obj:getID() local report = intelDb[_obj_id] if report and report.speed < 1 and report.targeted == nil then local lastContact = (timer.getTime() - report.record ) if lastContact < AIEN.config.artyFireLastContactThereshold then local timeFactor = (AIEN.config.artyFireLastContactThereshold-lastContact)/AIEN.config.artyFireLastContactThereshold local pri = classPriority[report.cls] if not pri then pri = 0.5 end pri = pri * timeFactor if pri > curPri then local go = getDangerClose(report.pos, gData.coa) if go == false then curPri = pri firePoint = report.pos targetId = report.cls report.targeted = timer.getTime() else if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", update_ARTY, target skipped for danger close")) end end end end end end end) end world.searchObjects(Object.Category.UNIT, _volume, _search) -- issuing mission if firePoint then if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", update_ARTY, suitable target found for : " .. tostring(gData.n) .. ": " .. tostring(targetId) .. ", will fire " .. tostring(roundsToFire) .. " rounds")) end gData.tasked = true gData.taskTime = timer.getTime() gData.firePoint = firePoint local description = nil if targetId then description = "Target is " .. tostring(targetId) end groupfireAtPoint({gData.group, firePoint, roundsToFire, description}) end end else if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", update_ARTY, threat range not available")) end end end end else remove = true end else remove = true end end if remove == true then if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", update_ARTY, group name " .. tostring(gData.n) .. " missing. Removing it")) end groundgroupsDb[phase_index] = nil phase_keys = createIterator(groundgroupsDb) end end end phase_index = getNextKey(phase_keys, phase_index) timer.scheduleFunction(AIEN.performPhaseCycle, {}, timer.getTime() + phaseCycleTimer) end else PHASE = "Initialization" if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", update_ARTY, reinizializzazione dei DB, poiché groundgroupsDb sembra vuoto o inesistente!")) end timer.scheduleFunction(AIEN.performPhaseCycle, {}, timer.getTime() + phaseCycleTimer) end end end -- 1ST LEVEL CYCLE FUNCTIONS function AIEN.changePhase() if PHASE == "Initialization" then -- udpate terrain data PHASE = "A" if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", AIEN.changePhase, new PHASE: " .. tostring(PHASE))) end elseif PHASE == "A" then -- udpate terrain data PHASE = "B" phase_keys = nil phase_keys = createIterator(intelDb) -- focus phase_keys on groundgroupsDb phase_index = phase_keys[1] if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", AIEN.changePhase, new PHASE: " .. tostring(PHASE))) end elseif PHASE == "B" then PHASE = "C" phase_keys = nil phase_keys = createIterator(droneunitDb) -- focus phase_keys on groundgroupsDb phase_index = phase_keys[1] if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", AIEN.changePhase, new PHASE: " .. tostring(PHASE))) end elseif PHASE == "C" then PHASE = "D" phase_keys = nil phase_keys = createIterator(groundgroupsDb) -- focus phase_keys on groundgroupsDb phase_index = phase_keys[1] if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", AIEN.changePhase, new PHASE: " .. tostring(PHASE))) end elseif PHASE == "D" then PHASE = "Z" -- LAST STEP AIEN.changePhase() elseif PHASE == "Z" then PHASE = "A" phase_keys = nil phase_keys = createIterator(groundgroupsDb) -- focus phase_keys on groundgroupsDb phase_index = phase_keys[1] if AIEN.config.AIEN_debugProcessDetail then env.info((tostring(ModuleName) .. ", AIEN.changePhase, new PHASE: " .. tostring(PHASE))) end end end function AIEN.performPhaseCycle() if PHASE == "Initialization" then populate_Db() phase_keys = nil phase_keys = createIterator(groundgroupsDb) -- focus phase_keys on groundgroupsDb phase_index = phase_keys[1] AIEN.changePhase() timer.scheduleFunction(AIEN.performPhaseCycle, {}, timer.getTime() + phaseCycleTimer) elseif PHASE == "A" then update_GROUND() elseif PHASE == "B" then update_ISR() elseif PHASE == "C" then update_DRONE() elseif PHASE == "D" then update_ARTY() end end --###### EVENT HANDLER FUNCTIONS ################################################################### -- I believe this is self explanatory. Still, the event_hit function holds a lot on reactions and decision making. I'm sorry if it appear confuse, but currently it fits my condition XD. local function event_hit(unit, shooter, weapon) -- this functions run eacht time a unit gets an hit. Unit only, no statics. That's basically the core for reactions if AIEN.config.reactions == true then local unitCat = pcallGetCategory(unit) local shooterCat = pcallGetCategory(shooter) if unitCat == 1 and shooterCat == 1 then local vehicle = unit:hasAttribute("Vehicles") local infantry = unit:hasAttribute("Infantry") local armoured = unit:hasAttribute("Armored vehicles") local position = unit:getPoint() local ground_unit = nil if vehicle == true then -- or infantry == true ground_unit = true end if ground_unit then local group = unit:getGroup() if group and group:isExist() and groupAllowedForAI(group) == true then -- filtering both for existance and for exclusion tag being not there local AI_consent = true -- filter for coalition if group:getCoalition() == 2 and AIEN.config.blueAI == false then AI_consent = false end if group:getCoalition() == 1 and AIEN.config.redAI == false then AI_consent = false end if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.event_hit, S_EVENT_HIT, coalition check return AI_consent " .. tostring(AI_consent) )) end if AI_consent == true then if AIEN.config.AIEN_zoneFilter and AIEN.config.AIEN_zoneFilter ~= "" then AI_consent = groupInZone(group) if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.event_hit, S_EVENT_HIT, group zone check return AI_consent " .. tostring(AI_consent) )) end end end if AI_consent == true then trigger.action.groupStopMoving(group) -- suppression part if AIEN.config.suppression == true and armoured then local suppressEffects = false if shooter:hasAttribute("Air") or shooter:hasAttribute("Ships") or shooter:hasAttribute("Indirect fire") then suppressEffects = true end if suppressEffects == true then if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.event_hit, S_EVENT_HIT, group is suppressed: " .. tostring(group:getName()) )) end groupSuppress(group) end end -- dismount part if AIEN.config.dismount == true then if not underAttack[group:getID()] then if shooter:hasAttribute("Air") then timer.scheduleFunction(groupDeployManpad, group, timer.getTime() + aie_random(8, 15)) if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.event_hit, S_EVENT_HIT, shooter is airborne, manpad dismount happens")) end elseif shooter:hasAttribute("Ground Units") then local d = AIEN.config.infantrySearchDist local dist = getDist(shooter:getPoint(), position) if dist < d then timer.scheduleFunction(groupDeployTroop, group, timer.getTime() + aie_random(8, 15)) if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.event_hit, S_EVENT_HIT, distance is close, infantry dismount happens")) end end end end end -- reaction part local choosenAct = nil if not underAttack[group:getID()] then -- if a group has already been identified as "attacked", it won't repeat all the whole process every time or it could became a freaking mess in case of multiple hits if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.event_hit, S_EVENT_HIT, group " .. tostring(group:getName()) )) end -- retrieve SA & Controller local con = group:getController() local db_group = groundgroupsDb[group:getID()] if con and db_group and db_group.sa then -- define if the attacker is known and and with what details local s_detected, s_visible, s_lastTime, s_type, s_distance, s_lastPos, s_lastVel, s_cat, w_cat, o_cat, s_indirect, s_close, s_fireMis, a_pos, o_cls, s_cls, o_pos w_cat = 0 s_cat = nil s_indirect = 0 s_close = 0 s_fireMis = 0 o_cls = db_group.sa.cls s_cls = "UNKN" o_pos = unit:getPoint() -- define weapon info, used to identify arty attack if weapon and weapon:isExist() then w_cat = weapon:getDesc().category --[[-- Weapon.Category SHELL 0 MISSILE 1 ROCKET 2 BOMB 3 --]]-- if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.event_hit, S_EVENT_HIT, group " .. tostring(group:getName()) .. ", w_cat: " .. tostring(w_cat) )) end end if shooter and con then if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.event_hit, S_EVENT_HIT, shooter known")) end -- revise a_pos a_pos = shooter:getPoint() -- parameters identification s_detected , s_visible , s_lastTime , s_type , s_distance , s_lastPos , s_lastVel = con:isTargetDetected(shooter) o_cat, s_cat = shooter:getCategory() s_cls = getUnitClass(shooter) --[[ o_cat: UNIT 1 WEAPON 2 STATIC 3 BASE 4 SCENERY 5 Cargo 6 --]]-- --[[ s_cat: AIRPLANE = 0, HELICOPTER = 1, GROUND_UNIT = 2, SHIP = 3, STRUCTURE = 4 --]]-- -- shooter is indirect fire if shooter:hasAttribute("Indirect fire") then s_indirect = 1 end -- shooter is close if a_pos and position then local d = db_group.sa.rng or 1500 local dist = getDist(a_pos, position) if dist < d then s_close = 1 end end -- position and speed --[[ removed cause of issues with isTargetDetected function returned variables if a_pos and s_lastVel and s_lastTime then if timer.getTime() - s_lastTime < 30 and s_lastVel < 1 then s_fireMis = 1 end end --]]-- local rnd = math.random(1,100) if rnd > 70 then s_fireMis = 1 end else -- try to address things when the shooter is unknown, based on weapon and effects if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.event_hit, S_EVENT_HIT, shooter unknown")) end if w_cat then --[[-- Weapon.Category SHELL 0 MISSILE 1 ROCKET 2 BOMB 3 --]]-- if w_cat == 0 or w_cat == 2 then -- shooter is unknown, and the weapon is a shell or a rocket: artillery is possibile s_indirect = 1 if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.event_hit, S_EVENT_HIT, shooter unknown but arty fire possibile")) end elseif w_cat == 1 or w_cat == 3 then -- shooter is unknown, and the weapon is a missile or a bomb: airborne threat is possibile s_cls = "ARBN" if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.event_hit, S_EVENT_HIT, shooter unknown but airborne fire possibile")) end end end end if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.event_hit, S_EVENT_HIT, group " .. tostring(group:getName()) .. ", w_cat: " .. tostring(w_cat) )) env.info(("AIEN.event_hit, S_EVENT_HIT, group " .. tostring(group:getName()) .. ", s_cat: " .. tostring(s_cat) )) env.info(("AIEN.event_hit, S_EVENT_HIT, group " .. tostring(group:getName()) .. ", s_indirect: " .. tostring(s_indirect) )) env.info(("AIEN.event_hit, S_EVENT_HIT, group " .. tostring(group:getName()) .. ", s_close: " .. tostring(s_close) )) env.info(("AIEN.event_hit, S_EVENT_HIT, group " .. tostring(group:getName()) .. ", s_fireMis: " .. tostring(s_fireMis) )) env.info(("AIEN.event_hit, S_EVENT_HIT, group " .. tostring(group:getName()) .. ", o_cls: " .. tostring(o_cls) )) env.info(("AIEN.event_hit, S_EVENT_HIT, group " .. tostring(group:getName()) .. ", s_cls: " .. tostring(s_cls) )) env.info(("AIEN.event_hit, S_EVENT_HIT, group " .. tostring(group:getName()) .. ", a_pos: " .. tostring(a_pos) )) end local av_ac = deepCopy(actionsDb) -- remove not doable actions due to missin informations if s_fireMis < 1 or AI_consent == false then -- shooter position is not sufficiently recent if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.event_hit, S_EVENT_HIT, s_fireMis is 0, won't be able to call fire support")) end av_ac[9] = nil end if not a_pos or not s_detected then -- enemy position unknown if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.event_hit, S_EVENT_HIT, enemy not detected, won't be able to move toward the enemy")) end av_ac[5] = nil end if s_cat == 0 or s_cat == 1 or s_cls == "ARBN" then -- shooter is airborne if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.event_hit, S_EVENT_HIT, shooter is airborne, removing less sensed decision")) end av_ac[5] = nil -- remove attack av_ac[7] = nil -- remove ground support av_ac[3] = nil -- remove disperse end if s_cls ~= "ARBN" then -- shooter is not airborne av_ac[8] = nil -- remove counter ADS end if not unit:hasAttribute("Armored vehicles") then av_ac[4] = nil -- remove drop smoke end -- filter available actions by skill local filter = db_group.skill if AIEN.config.skill_action_const == false then filter = filter * 2 end for aSk, action in pairs(av_ac) do if aSk > filter then av_ac[aSk] = nil end end if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.event_hit, S_EVENT_HIT, available actions " .. tostring(#av_ac) )) end -- calculate points for each remaining actions local bc_ac = {} for aId, aData in pairs(av_ac) do local points = 0 local px1 = aData["w_cat"][w_cat] or 0 local px2 = aData["s_cat"][s_cat] or 0 local px3 = aData["s_indirect"][s_indirect] or 0 local px4 = aData["s_close"][s_close] or 0 local px5 = aData["s_fireMis"][s_fireMis] or 0 local px6 = aData["o_cls"][o_cls] or 0 local px7 = aData["s_cls"][s_cls] or 0 points = px1 + px2 + px3 + px4 + px5 + px6 + px7 if AIEN.config.AIEN_debugProcessDetail == true then --env.info(("AIEN.event_hit, S_EVENT_HIT," .. tostring(aData.name) .. ", points for w_cat: " .. tostring(aData["w_cat"][w_cat]))) --env.info(("AIEN.event_hit, S_EVENT_HIT," .. tostring(aData.name) .. ", points for s_cat: " .. tostring(aData["s_cat"][s_cat]))) --env.info(("AIEN.event_hit, S_EVENT_HIT," .. tostring(aData.name) .. ", points for s_indirect: " .. tostring(aData["s_indirect"][s_indirect]))) --env.info(("AIEN.event_hit, S_EVENT_HIT," .. tostring(aData.name) .. ", points for s_close: " .. tostring(aData["s_close"][s_close]))) --env.info(("AIEN.event_hit, S_EVENT_HIT," .. tostring(aData.name) .. ", points for s_fireMis: " .. tostring(aData["s_fireMis"][s_fireMis]))) --env.info(("AIEN.event_hit, S_EVENT_HIT," .. tostring(aData.name) .. ", points for o_cls: " .. tostring(aData["o_cls"][o_cls]))) --env.info(("AIEN.event_hit, S_EVENT_HIT," .. tostring(aData.name) .. ", points for s_cls: " .. tostring(aData["s_cls"][s_cls]))) env.info(("AIEN.event_hit, S_EVENT_HIT," .. tostring(aData.name) .. ", points total: " .. tostring(points))) end bc_ac[#bc_ac+1] = {name = aData.name, action = aData.action, rank = points} end table.sort(bc_ac, function(a,b) if a.rank and b.rank then return a.rank > b.rank end end) -- record the attack, for preventing phases to act for 10 mins underAttack[group:getID()] = timer.getTime() choosenAct = executeActions(group, o_pos, a_pos, bc_ac, db_group.sa, db_group.skill) end end -- counter battery part if choosenAct ~= "ac_fireMissionOnShooter" then if AIEN.config.firemissions == true then if shooter:getPoint() and position then counterBattery(position, shooter:getPoint(), group:getCoalition()) end end end else if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.event_hit, S_EVENT_HIT, AI consent is false")) end end end else if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.event_hit, missing unit")) end end else if AIEN.config.AIEN_debugProcessDetail == true then env.info(("AIEN.event_hit, either shooter or unit are not valid units")) end end end end local function event_birth(initiator) local check = pcallGetCategory(initiator) if check then local objCat = nil local subCat = nil objCat, subCat = initiator:getCategory() if objCat == 1 and subCat == 2 then -- unit, ground unit if not initiator:hasAttribute("Infantry") then local gp = initiator:getGroup() if gp then if not groundgroupsDb[gp:getID()] then -- since event is launched for each unit, this prevent re-adding the same group multiple times local c = getGroupClass(gp) local det, thr = getRanges(gp) local s = getGroupSkillNum(gp) groupPreventDisperse(gp) --env.info((tostring(ModuleName) .. ", event_birth: s " .. tostring(s))) groundgroupsDb[gp:getID()] = {group = gp, class = c, n = gp:getName(), coa = gp:getCoalition(), detection = det, threat = thr, tasked = false, skill = s} --env.info((tostring(ModuleName) .. ", event_birth: adding to groundgroupsDb " .. tostring(gp:getName() ))) end end elseif objCat == 1 and subCat == 0 then -- unit, plane unit (drone) local gp = initiator:getGroup() if gp then local c = nil if gp:getUnits() and #gp:getUnits() > 0 then for _, un in pairs(gp:getUnits()) do if un:hasAttribute("UAVs") then -- drone only c = "UAV" end end end if c then if AIEN.config.AIEN_debugProcessDetail == true then env.info((tostring(ModuleName) .. ", event_birth: adding to droneunitDb " .. tostring(un:getName() ))) end droneunitDb[gp:getID()] = {group = gp, class = c, n = gp:getName(), coa = gp:getCoalition()} end end end end end end local function event_dead(initiator) local check = pcallGetCategory(initiator) if check then local objCat = nil local subCat = nil objCat, subCat = Object.getCategory(initiator) if objCat and subCat then if objCat == 1 and subCat == 2 then -- unit, ground unit mountedDb[initiator:getID()] = nil infcarrierDb[initiator:getID()] = nil end end end end --## EVENTS HANDLING CALLS AIEN.eventHandler = {} -- define event based real time unit reactions. I prefer to have 1 single handler that then will route itself on the right directions event based. function AIEN.eventHandler:onEvent(event) if event.id == world.event.S_EVENT_HIT then local u = event.target local s = event.initiator local w = event.weapon event_hit(u, s, w) elseif event.id == world.event.S_EVENT_BIRTH then local i = event.initiator if i then event_birth(i) end elseif event.id == world.event.S_EVENT_DEAD or event.id == world.event.S_EVENT_UNIT_LOST then local i = event.initiator if i then event_dead(i) end end end world.addEventHandler(AIEN.eventHandler) --## INIT SCRIPT if AIEN.config.dontInitialize then env.info((ModuleName .. ": Loaded (BUT NOT INITIALIZED) " .. MainVersion .. "." .. SubVersion .. "." .. Build .. ", released " .. Date)) else AIEN.performPhaseCycle() env.info((ModuleName .. ": Loaded " .. MainVersion .. "." .. SubVersion .. "." .. Build .. ", released " .. Date)) end --~=