diff --git a/CTLD.lua b/CTLD.lua new file mode 100644 index 0000000..1791a50 --- /dev/null +++ b/CTLD.lua @@ -0,0 +1,6427 @@ +--[[ + Combat Troop and Logistics Drop + + Allows Huey, Mi-8 and C130 to transport troops internally and Helicopters to transport Logistic / Vehicle units to the field via sling-loads + without requiring external mods. + + Supports all of the original CTTS functionality such as AI auto troop load and unload as well as group spawning and preloading of troops into units. + + Supports deployment of Auto Lasing JTAC to the field + + See https://github.com/ciribob/DCS-CTLD for a user manual and the latest version + + Contributors: + - Steggles - https://github.com/Bob7heBuilder + - mvee - https://github.com/mvee + - jmontleon - https://github.com/jmontleon + - emilianomolina - https://github.com/emilianomolina + - davidp57 - https://github.com/veaf + + - Allow minimum distance from friendly logistics to be set + ]] + +ctld = {} -- DONT REMOVE! + +--- Identifier. All output in DCS.log will start with this. +ctld.Id = "CTLD - " + +--- Version. +ctld.Version = "20211113.01" + +-- debug level, specific to this module +ctld.Debug = true +-- trace level, specific to this module +ctld.Trace = true + +ctld.alreadyInitialized = false -- if true, ctld.initialize() will not run + +-- ************************************************************************ +-- ********************* USER CONFIGURATION ****************************** +-- ************************************************************************ +ctld.staticBugWorkaround = false -- DCS had a bug where destroying statics would cause a crash. If this happens again, set this to TRUE + +ctld.disableAllSmoke = false -- if true, all smoke is diabled at pickup and drop off zones regardless of settings below. Leave false to respect settings below + +ctld.hoverPickup = true -- if set to false you can load crates with the F10 menu instead of hovering... Only if not using real crates! + +ctld.enableCrates = true -- if false, Helis will not be able to spawn or unpack crates so will be normal CTTS +ctld.slingLoad = false -- if false, crates can be used WITHOUT slingloading, by hovering above the crate, simulating slingloading but not the weight... +-- There are some bug with Sling-loading that can cause crashes, if these occur set slingLoad to false +-- to use the other method. +-- Set staticBugFix to FALSE if use set ctld.slingLoad to TRUE + +ctld.enableSmokeDrop = true -- if false, helis and c-130 will not be able to drop smoke + +ctld.maxExtractDistance = 125 -- max distance from vehicle to troops to allow a group extraction +ctld.maximumDistanceLogistic = 200 -- max distance from vehicle to logistics to allow a loading or spawning operation +ctld.maximumSearchDistance = 4000 -- max distance for troops to search for enemy +ctld.maximumMoveDistance = 2000 -- max distance for troops to move from drop point if no enemy is nearby + +ctld.minimumDeployDistance = 1000 -- minimum distance from a friendly pickup zone where you can deploy a crate + +ctld.numberOfTroops = 10 -- default number of troops to load on a transport heli or C-130 + -- also works as maximum size of group that'll fit into a helicopter unless overridden +ctld.enableFastRopeInsertion = true -- allows you to drop troops by fast rope +ctld.fastRopeMaximumHeight = 18.28 -- in meters which is 60 ft max fast rope (not rappell) safe height + +ctld.vehiclesForTransportRED = { "BRDM-2", "BTR_D" } -- vehicles to load onto Il-76 - Alternatives {"Strela-1 9P31","BMP-1"} +ctld.vehiclesForTransportBLUE = { "M1045 HMMWV TOW", "M1043 HMMWV Armament" } -- vehicles to load onto c130 - Alternatives {"M1128 Stryker MGS","M1097 Avenger"} +ctld.vehiclesWeight = { + ["BRDM-2"] = 7000, + ["BTR_D"] = 8000, + ["M1045 HMMWV TOW"] = 3220, + ["M1043 HMMWV Armament"] = 2500 +} + +ctld.aaLaunchers = 3 -- controls how many launchers to add to the kub/buk when its spawned. +ctld.hawkLaunchers = 8 -- controls how many launchers to add to the hawk when its spawned. + +ctld.spawnRPGWithCoalition = true --spawns a friendly RPG unit with Coalition forces +ctld.spawnStinger = false -- spawns a stinger / igla soldier with a group of 6 or more soldiers! + +ctld.enabledFOBBuilding = true -- if true, you can load a crate INTO a C-130 than when unpacked creates a Forward Operating Base (FOB) which is a new place to spawn (crates) and carry crates from +-- In future i'd like it to be a FARP but so far that seems impossible... +-- You can also enable troop Pickup at FOBS + +ctld.cratesRequiredForFOB = 3 -- The amount of crates required to build a FOB. Once built, helis can spawn crates at this outpost to be carried and deployed in another area. +-- The large crates can only be loaded and dropped by large aircraft, like the C-130 and listed in ctld.vehicleTransportEnabled +-- Small FOB crates can be moved by helicopter. The FOB will require ctld.cratesRequiredForFOB larges crates and small crates are 1/3 of a large fob crate +-- To build the FOB entirely out of small crates you will need ctld.cratesRequiredForFOB * 3 + +ctld.troopPickupAtFOB = true -- if true, troops can also be picked up at a created FOB + +ctld.buildTimeFOB = 120 --time in seconds for the FOB to be built + +ctld.crateWaitTime = 120 -- time in seconds to wait before you can spawn another crate + +ctld.forceCrateToBeMoved = true -- a crate must be picked up at least once and moved before it can be unpacked. Helps to reduce crate spam + +ctld.radioSound = "beacon.ogg" -- the name of the sound file to use for the FOB radio beacons. If this isnt added to the mission BEACONS WONT WORK! +ctld.radioSoundFC3 = "beaconsilent.ogg" -- name of the second silent radio file, used so FC3 aircraft dont hear ALL the beacon noises... :) + +ctld.deployedBeaconBattery = 30 -- the battery on deployed beacons will last for this number minutes before needing to be re-deployed + +ctld.enabledRadioBeaconDrop = true -- if its set to false then beacons cannot be dropped by units + +ctld.allowRandomAiTeamPickups = false -- Allows the AI to randomize the loading of infantry teams (specified below) at pickup zones + +-- Simulated Sling load configuration + +ctld.minimumHoverHeight = 7.5 -- Lowest allowable height for crate hover +ctld.maximumHoverHeight = 12.0 -- Highest allowable height for crate hover +ctld.maxDistanceFromCrate = 5.5 -- Maximum distance from from crate for hover +ctld.hoverTime = 10 -- Time to hold hover above a crate for loading in seconds + +-- end of Simulated Sling load configuration + +-- AA SYSTEM CONFIG -- +-- Sets a limit on the number of active AA systems that can be built for RED. +-- A system is counted as Active if its fully functional and has all parts +-- If a system is partially destroyed, it no longer counts towards the total +-- When this limit is hit, a player will still be able to get crates for an AA system, just unable +-- to unpack them + +ctld.AASystemLimitRED = 20 -- Red side limit + +ctld.AASystemLimitBLUE = 20 -- Blue side limit + +--END AA SYSTEM CONFIG -- + +-- ***************** JTAC CONFIGURATION ***************** + +ctld.JTAC_LIMIT_RED = 10 -- max number of JTAC Crates for the RED Side +ctld.JTAC_LIMIT_BLUE = 10 -- max number of JTAC Crates for the BLUE Side + +ctld.JTAC_dropEnabled = true -- allow JTAC Crate spawn from F10 menu + +ctld.JTAC_maxDistance = 10000 -- How far a JTAC can "see" in meters (with Line of Sight) + +ctld.JTAC_smokeOn_RED = true -- enables marking of target with smoke for RED forces +ctld.JTAC_smokeOn_BLUE = true -- enables marking of target with smoke for BLUE forces + +ctld.JTAC_smokeColour_RED = 4 -- RED side smoke colour -- Green = 0 , Red = 1, White = 2, Orange = 3, Blue = 4 +ctld.JTAC_smokeColour_BLUE = 1 -- BLUE side smoke colour -- Green = 0 , Red = 1, White = 2, Orange = 3, Blue = 4 + +ctld.JTAC_jtacStatusF10 = true -- enables F10 JTAC Status menu + +ctld.JTAC_location = true -- shows location of target in JTAC message +ctld.location_DMS = false -- shows coordinates as Degrees Minutes Seconds instead of Degrees Decimal minutes + +ctld.JTAC_lock = "all" -- "vehicle" OR "troop" OR "all" forces JTAC to only lock vehicles or troops or all ground units + +-- ***************** Pickup, dropoff and waypoint zones ***************** + +-- Available colors (anything else like "none" disables smoke): "green", "red", "white", "orange", "blue", "none", + +-- Use any of the predefined names or set your own ones + +-- You can add number as a third option to limit the number of soldier or vehicle groups that can be loaded from a zone. +-- Dropping back a group at a limited zone will add one more to the limit + +-- If a zone isn't ACTIVE then you can't pickup from that zone until the zone is activated by ctld.activatePickupZone +-- using the Mission editor + +-- You can pickup from a SHIP by adding the SHIP UNIT NAME instead of a zone name + +-- Side - Controls which side can load/unload troops at the zone + +-- Flag Number - Optional last field. If set the current number of groups remaining can be obtained from the flag value + +--pickupZones = { "Zone name or Ship Unit Name", "smoke color", "limit (-1 unlimited)", "ACTIVE (yes/no)", "side (0 = Both sides / 1 = Red / 2 = Blue )", flag number (optional) } +ctld.pickupZones = { + { "pickzone1", "blue", -1, "yes", 0 }, + { "pickzone2", "red", -1, "yes", 0 }, + { "pickzone3", "none", -1, "yes", 0 }, + { "pickzone4", "none", -1, "yes", 0 }, + { "pickzone5", "none", -1, "yes", 0 }, + { "pickzone6", "none", -1, "yes", 0 }, + { "pickzone7", "none", -1, "yes", 0 }, + { "pickzone8", "none", -1, "yes", 0 }, + { "pickzone9", "none", 5, "yes", 1 }, -- limits pickup zone 9 to 5 groups of soldiers or vehicles, only red can pick up + { "pickzone10", "none", 10, "yes", 2 }, -- limits pickup zone 10 to 10 groups of soldiers or vehicles, only blue can pick up + + { "pickzone11", "blue", 20, "no", 2 }, -- limits pickup zone 11 to 20 groups of soldiers or vehicles, only blue can pick up. Zone starts inactive! + { "pickzone12", "red", 20, "no", 1 }, -- limits pickup zone 11 to 20 groups of soldiers or vehicles, only blue can pick up. Zone starts inactive! + { "pickzone13", "none", -1, "yes", 0 }, + { "pickzone14", "none", -1, "yes", 0 }, + { "pickzone15", "none", -1, "yes", 0 }, + { "pickzone16", "none", -1, "yes", 0 }, + { "pickzone17", "none", -1, "yes", 0 }, + { "pickzone18", "none", -1, "yes", 0 }, + { "pickzone19", "none", 5, "yes", 0 }, + { "pickzone20", "none", 10, "yes", 0, 1000 }, -- optional extra flag number to store the current number of groups available in + + { "USA Carrier", "blue", 10, "yes", 0, 1001 }, -- instead of a Zone Name you can also use the UNIT NAME of a ship +} + + +-- dropOffZones = {"name","smoke colour",0,side 1 = Red or 2 = Blue or 0 = Both sides} +ctld.dropOffZones = { + { "dropzone1", "green", 2 }, + { "dropzone2", "blue", 2 }, + { "dropzone3", "orange", 2 }, + { "dropzone4", "none", 2 }, + { "dropzone5", "none", 1 }, + { "dropzone6", "none", 1 }, + { "dropzone7", "none", 1 }, + { "dropzone8", "none", 1 }, + { "dropzone9", "none", 1 }, + { "dropzone10", "none", 1 }, +} + + +--wpZones = { "Zone name", "smoke color", "ACTIVE (yes/no)", "side (0 = Both sides / 1 = Red / 2 = Blue )", } +ctld.wpZones = { + { "wpzone1", "green","yes", 2 }, + { "wpzone2", "blue","yes", 2 }, + { "wpzone3", "orange","yes", 2 }, + { "wpzone4", "none","yes", 2 }, + { "wpzone5", "none","yes", 2 }, + { "wpzone6", "none","yes", 1 }, + { "wpzone7", "none","yes", 1 }, + { "wpzone8", "none","yes", 1 }, + { "wpzone9", "none","yes", 1 }, + { "wpzone10", "none","no", 0 }, -- Both sides as its set to 0 +} + + +-- ******************** Transports names ********************** + +-- Use any of the predefined names or set your own ones +ctld.transportPilotNames = { + "helicargo1", + "helicargo2", + "helicargo3", + "helicargo4", + "helicargo5", + "helicargo6", + "helicargo7", + "helicargo8", + "helicargo9", + "helicargo10", + + "helicargo11", + "helicargo12", + "helicargo13", + "helicargo14", + "helicargo15", + "helicargo16", + "helicargo17", + "helicargo18", + "helicargo19", + "helicargo20", + + "helicargo21", + "helicargo22", + "helicargo23", + "helicargo24", + "helicargo25", + + "MEDEVAC #1", + "MEDEVAC #2", + "MEDEVAC #3", + "MEDEVAC #4", + "MEDEVAC #5", + "MEDEVAC #6", + "MEDEVAC #7", + "MEDEVAC #8", + "MEDEVAC #9", + "MEDEVAC #10", + "MEDEVAC #11", + "MEDEVAC #12", + "MEDEVAC #13", + "MEDEVAC #14", + "MEDEVAC #15", + "MEDEVAC #16", + + "MEDEVAC RED #1", + "MEDEVAC RED #2", + "MEDEVAC RED #3", + "MEDEVAC RED #4", + "MEDEVAC RED #5", + "MEDEVAC RED #6", + "MEDEVAC RED #7", + "MEDEVAC RED #8", + "MEDEVAC RED #9", + "MEDEVAC RED #10", + "MEDEVAC RED #11", + "MEDEVAC RED #12", + "MEDEVAC RED #13", + "MEDEVAC RED #14", + "MEDEVAC RED #15", + "MEDEVAC RED #16", + "MEDEVAC RED #17", + "MEDEVAC RED #18", + "MEDEVAC RED #19", + "MEDEVAC RED #20", + "MEDEVAC RED #21", + + "MEDEVAC BLUE #1", + "MEDEVAC BLUE #2", + "MEDEVAC BLUE #3", + "MEDEVAC BLUE #4", + "MEDEVAC BLUE #5", + "MEDEVAC BLUE #6", + "MEDEVAC BLUE #7", + "MEDEVAC BLUE #8", + "MEDEVAC BLUE #9", + "MEDEVAC BLUE #10", + "MEDEVAC BLUE #11", + "MEDEVAC BLUE #12", + "MEDEVAC BLUE #13", + "MEDEVAC BLUE #14", + "MEDEVAC BLUE #15", + "MEDEVAC BLUE #16", + "MEDEVAC BLUE #17", + "MEDEVAC BLUE #18", + "MEDEVAC BLUE #19", + "MEDEVAC BLUE #20", + "MEDEVAC BLUE #21", + + -- *** AI transports names (different names only to ease identification in mission) *** + + -- Use any of the predefined names or set your own ones + + "transport1", + "transport2", + "transport3", + "transport4", + "transport5", + "transport6", + "transport7", + "transport8", + "transport9", + "transport10", + + "transport11", + "transport12", + "transport13", + "transport14", + "transport15", + "transport16", + "transport17", + "transport18", + "transport19", + "transport20", + + "transport21", + "transport22", + "transport23", + "transport24", + "transport25", +} + +-- *************** Optional Extractable GROUPS ***************** + +-- Use any of the predefined names or set your own ones + +ctld.extractableGroups = { + "extract1", + "extract2", + "extract3", + "extract4", + "extract5", + "extract6", + "extract7", + "extract8", + "extract9", + "extract10", + + "extract11", + "extract12", + "extract13", + "extract14", + "extract15", + "extract16", + "extract17", + "extract18", + "extract19", + "extract20", + + "extract21", + "extract22", + "extract23", + "extract24", + "extract25", +} + +-- ************** Logistics UNITS FOR CRATE SPAWNING ****************** + +-- Use any of the predefined names or set your own ones +-- When a logistic unit is destroyed, you will no longer be able to spawn crates + +ctld.logisticUnits = { + "logistic1", + "logistic2", + "logistic3", + "logistic4", + "logistic5", + "logistic6", + "logistic7", + "logistic8", + "logistic9", + "logistic10", +} + +-- ************** UNITS ABLE TO TRANSPORT VEHICLES ****************** +-- Add the model name of the unit that you want to be able to transport and deploy vehicles +-- units db has all the names or you can extract a mission.miz file by making it a zip and looking +-- in the contained mission file +ctld.vehicleTransportEnabled = { + "76MD", -- the il-76 mod doesnt use a normal - sign so il-76md wont match... !!!! GRR + "Hercules", +} + + +-- ************** Maximum Units SETUP for UNITS ****************** + +-- Put the name of the Unit you want to limit group sizes too +-- i.e +-- ["UH-1H"] = 10, +-- +-- Will limit UH1 to only transport groups with a size 10 or less +-- Make sure the unit name is exactly right or it wont work + +ctld.unitLoadLimits = { + + -- Remove the -- below to turn on options + -- ["SA342Mistral"] = 4, + -- ["SA342L"] = 4, + -- ["SA342M"] = 4, + +} + + +-- ************** Allowable actions for UNIT TYPES ****************** + +-- Put the name of the Unit you want to limit actions for +-- NOTE - the unit must've been listed in the transportPilotNames list above +-- This can be used in conjunction with the options above for group sizes +-- By default you can load both crates and troops unless overriden below +-- i.e +-- ["UH-1H"] = {crates=true, troops=false}, +-- +-- Will limit UH1 to only transport CRATES but NOT TROOPS +-- +-- ["SA342Mistral"] = {crates=fales, troops=true}, +-- Will allow Mistral Gazelle to only transport crates, not troops + +ctld.unitActions = { + + -- Remove the -- below to turn on options + -- ["SA342Mistral"] = {crates=true, troops=true}, + -- ["SA342L"] = {crates=false, troops=true}, + -- ["SA342M"] = {crates=false, troops=true}, + +} + +-- ************** WEIGHT CALCULATIONS FOR INFANTRY GROUPS ****************** + +-- Infantry groups weight is calculated based on the soldiers' roles, and the weight of their kit +-- Every soldier weights between 90% and 120% of ctld.SOLDIER_WEIGHT, and they all carry a backpack and their helmet (ctld.KIT_WEIGHT) +-- Standard grunts have a rifle and ammo (ctld.RIFLE_WEIGHT) +-- AA soldiers have a MANPAD tube (ctld.MANPAD_WEIGHT) +-- Anti-tank soldiers have a RPG and a rocket (ctld.RPG_WEIGHT) +-- Machine gunners have the squad MG and 200 bullets (ctld.MG_WEIGHT) +-- JTAC have the laser sight, radio and binoculars (ctld.JTAC_WEIGHT) +-- Mortar servants carry their tube and a few rounds (ctld.MORTAR_WEIGHT) + +ctld.SOLDIER_WEIGHT = 80 -- kg, will be randomized between 90% and 120% +ctld.KIT_WEIGHT = 20 -- kg +ctld.RIFLE_WEIGHT = 5 -- kg +ctld.MANPAD_WEIGHT = 18 -- kg +ctld.RPG_WEIGHT = 7.6 -- kg +ctld.MG_WEIGHT = 10 -- kg +ctld.MORTAR_WEIGHT = 26 -- kg +ctld.JTAC_WEIGHT = 15 -- kg + +-- ************** INFANTRY GROUPS FOR PICKUP ****************** +-- Unit Types +-- inf is normal infantry +-- mg is M249 +-- at is RPG-16 +-- aa is Stinger or Igla +-- mortar is a 2B11 mortar unit +-- jtac is a JTAC soldier, which will use JTACAutoLase +-- You must add a name to the group for it to work +-- You can also add an optional coalition side to limit the group to one side +-- for the side - 2 is BLUE and 1 is RED +ctld.loadableGroups = { + {name = "Standard Group", inf = 6, mg = 2, at = 2 }, -- will make a loadable group with 6 infantry, 2 MGs and 2 anti-tank for both coalitions + {name = "Anti Air", inf = 2, aa = 3 }, + {name = "Anti Tank", inf = 2, at = 6 }, + {name = "Mortar Squad", mortar = 6 }, + {name = "JTAC Group", inf = 4, jtac = 1 }, -- will make a loadable group with 4 infantry and a JTAC soldier for both coalitions + {name = "Single JTAC", jtac = 1 }, -- will make a loadable group witha single JTAC soldier for both coalitions + -- {name = "Mortar Squad Red", inf = 2, mortar = 5, side =1 }, --would make a group loadable by RED only +} + +-- ************** SPAWNABLE CRATES ****************** +-- Weights must be unique as we use the weight to change the cargo to the correct unit +-- when we unpack +-- +ctld.spawnableCrates = { + -- name of the sub menu on F10 for spawning crates + ["Ground Forces"] = { + --crates you can spawn + -- weight in KG + -- Desc is the description on the F10 MENU + -- unit is the model name of the unit to spawn + -- cratesRequired - if set requires that many crates of the same type within 100m of each other in order build the unit + -- side is optional but 2 is BLUE and 1 is RED + -- dont use that option with the HAWK Crates + { weight = 500, desc = "HMMWV - TOW", unit = "M1045 HMMWV TOW", side = 2 }, + { weight = 505, desc = "HMMWV - MG", unit = "M1043 HMMWV Armament", side = 2 }, + + { weight = 510, desc = "BTR-D", unit = "BTR_D", side = 1 }, + { weight = 515, desc = "BRDM-2", unit = "BRDM-2", side = 1 }, + + { weight = 520, desc = "HMMWV - JTAC", unit = "Hummer", side = 2, }, -- used as jtac and unarmed, not on the crate list if JTAC is disabled + { weight = 525, desc = "SKP-11 - JTAC", unit = "SKP-11", side = 1, }, -- used as jtac and unarmed, not on the crate list if JTAC is disabled + + { weight = 100, desc = "2B11 Mortar", unit = "2B11 mortar" }, + + { weight = 250, desc = "SPH 2S19 Msta", unit = "SAU Msta", side = 1, cratesRequired = 3 }, + { weight = 255, desc = "M-109", unit = "M-109", side = 2, cratesRequired = 3 }, + + { weight = 252, desc = "Ural-375 Ammo Truck", unit = "Ural-375", side = 1, cratesRequired = 2 }, + { weight = 253, desc = "M-818 Ammo Truck", unit = "M 818", side = 2, cratesRequired = 2 }, + + { weight = 800, desc = "FOB Crate - Small", unit = "FOB-SMALL" }, -- Builds a FOB! - requires 3 * ctld.cratesRequiredForFOB + }, + ["AA short range"] = { + { weight = 50, desc = "Stinger", unit = "Soldier stinger", side = 2 }, + { weight = 55, desc = "Igla", unit = "SA-18 Igla manpad", side = 1 }, + + { weight = 405, desc = "Strela-1 9P31", unit = "Strela-1 9P31", side = 1, cratesRequired = 3 }, + { weight = 400, desc = "M1097 Avenger", unit = "M1097 Avenger", side = 2, cratesRequired = 3 }, + }, + ["AA mid range"] = { + -- HAWK System + { weight = 540, desc = "HAWK Launcher", unit = "Hawk ln", side = 2}, + { weight = 545, desc = "HAWK Search Radar", unit = "Hawk sr", side = 2 }, + { weight = 546, desc = "HAWK Track Radar", unit = "Hawk tr", side = 2 }, + { weight = 547, desc = "HAWK PCP", unit = "Hawk pcp" , side = 2 }, -- Remove this if on 1.2 + { weight = 548, desc = "HAWK CWAR", unit = "Hawk cwar" , side = 2 }, -- Remove this if on 2.5 + { weight = 549, desc = "HAWK Repair", unit = "HAWK Repair" , side = 2 }, + -- End of HAWK + + -- KUB SYSTEM + { weight = 560, desc = "KUB Launcher", unit = "Kub 2P25 ln", side = 1}, + { weight = 565, desc = "KUB Radar", unit = "Kub 1S91 str", side = 1 }, + { weight = 570, desc = "KUB Repair", unit = "KUB Repair", side = 1}, + -- End of KUB + + -- BUK System + -- { weight = 575, desc = "BUK Launcher", unit = "SA-11 Buk LN 9A310M1"}, + -- { weight = 580, desc = "BUK Search Radar", unit = "SA-11 Buk SR 9S18M1"}, + -- { weight = 585, desc = "BUK CC Radar", unit = "SA-11 Buk CC 9S470M1"}, + -- { weight = 590, desc = "BUK Repair", unit = "BUK Repair"}, + -- END of BUK + }, + ["AA long range"] = { + -- Patriot System + { weight = 555, desc = "Patriot Launcher", unit = "Patriot ln", side = 2 }, + { weight = 556, desc = "Patriot Radar", unit = "Patriot str" , side = 2 }, + { weight = 557, desc = "Patriot ECS", unit = "Patriot ECS", side = 2 }, + -- { weight = 553, desc = "Patriot ICC", unit = "Patriot cp", side = 2 }, + -- { weight = 554, desc = "Patriot EPP", unit = "Patriot EPP", side = 2 }, + { weight = 558, desc = "Patriot AMG (optional)", unit = "Patriot AMG" , side = 2 }, + { weight = 559, desc = "Patriot Repair", unit = "Patriot Repair" , side = 2 }, + -- End of Patriot + + { weight = 595, desc = "Early Warning Radar", unit = "1L13 EWR", side = 1 }, -- cant be used by BLUE coalition + }, +} + +--- 3D model that will be used to represent a loadable crate ; by default, a generator +ctld.spawnableCratesModel_load = { + ["category"] = "Fortifications", + ["shape_name"] = "GeneratorF", + ["type"] = "GeneratorF" +} + +--- 3D model that will be used to represent a slingable crate ; by default, a crate +ctld.spawnableCratesModel_sling = { + ["category"] = "Cargos", + ["shape_name"] = "bw_container_cargo", + ["type"] = "container_cargo" +} + +--[[ Placeholder for different type of cargo containers. Let's say pipes and trunks, fuel for FOB building + ["shape_name"] = "ab-212_cargo", + ["type"] = "uh1h_cargo" --new type for the container previously used + + ["shape_name"] = "ammo_box_cargo", + ["type"] = "ammo_cargo", + + ["shape_name"] = "barrels_cargo", + ["type"] = "barrels_cargo", + + ["shape_name"] = "bw_container_cargo", + ["type"] = "container_cargo", + + ["shape_name"] = "f_bar_cargo", + ["type"] = "f_bar_cargo", + + ["shape_name"] = "fueltank_cargo", + ["type"] = "fueltank_cargo", + + ["shape_name"] = "iso_container_cargo", + ["type"] = "iso_container", + + ["shape_name"] = "iso_container_small_cargo", + ["type"] = "iso_container_small", + + ["shape_name"] = "oiltank_cargo", + ["type"] = "oiltank_cargo", + + ["shape_name"] = "pipes_big_cargo", + ["type"] = "pipes_big_cargo", + + ["shape_name"] = "pipes_small_cargo", + ["type"] = "pipes_small_cargo", + + ["shape_name"] = "tetrapod_cargo", + ["type"] = "tetrapod_cargo", + + ["shape_name"] = "trunks_long_cargo", + ["type"] = "trunks_long_cargo", + + ["shape_name"] = "trunks_small_cargo", + ["type"] = "trunks_small_cargo", +]]-- + +-- if the unit is on this list, it will be made into a JTAC when deployed +ctld.jtacUnitTypes = { + "SKP", "Hummer" -- there are some wierd encoding issues so if you write SKP-11 it wont match as the - sign is encoded differently... +} + + +-- *************************************************************** +-- **************** Mission Editor Functions ********************* +-- *************************************************************** + + +----------------------------------------------------------------- +-- Spawn group at a trigger and set them as extractable. Usage: +-- ctld.spawnGroupAtTrigger("groupside", number, "triggerName", radius) +-- Variables: +-- "groupSide" = "red" for Russia "blue" for USA +-- _number = number of groups to spawn OR Group description +-- "triggerName" = trigger name in mission editor between commas +-- _searchRadius = random distance for units to move from spawn zone (0 will leave troops at the spawn position - no search for enemy) +-- +-- Example: ctld.spawnGroupAtTrigger("red", 2, "spawn1", 1000) +-- +-- This example will spawn 2 groups of russians at the specified point +-- and they will search for enemy or move randomly withing 1000m +-- OR +-- +-- ctld.spawnGroupAtTrigger("blue", {mg=1,at=2,aa=3,inf=4,mortar=5},"spawn2", 2000) +-- Spawns 1 machine gun, 2 anti tank, 3 anti air, 4 standard soldiers and 5 mortars +-- +function ctld.spawnGroupAtTrigger(_groupSide, _number, _triggerName, _searchRadius) + local _spawnTrigger = trigger.misc.getZone(_triggerName) -- trigger to use as reference position + + if _spawnTrigger == nil then + trigger.action.outText("CTLD.lua ERROR: Cant find trigger called " .. _triggerName, 10) + return + end + + local _country + if _groupSide == "red" then + _groupSide = 1 + _country = 0 + else + _groupSide = 2 + _country = 2 + end + + if _searchRadius < 0 then + _searchRadius = 0 + end + + local _pos2 = { x = _spawnTrigger.point.x, y = _spawnTrigger.point.z } + local _alt = land.getHeight(_pos2) + local _pos3 = { x = _pos2.x, y = _alt, z = _pos2.y } + + local _groupDetails = ctld.generateTroopTypes(_groupSide, _number, _country) + + local _droppedTroops = ctld.spawnDroppedGroup(_pos3, _groupDetails, false, _searchRadius); + + if _groupSide == 1 then + table.insert(ctld.droppedTroopsRED, _droppedTroops:getName()) + else + table.insert(ctld.droppedTroopsBLUE, _droppedTroops:getName()) + end +end + + +----------------------------------------------------------------- +-- Spawn group at a Vec3 Point and set them as extractable. Usage: +-- ctld.spawnGroupAtPoint("groupside", number,Vec3 Point, radius) +-- Variables: +-- "groupSide" = "red" for Russia "blue" for USA +-- _number = number of groups to spawn OR Group Description +-- Vec3 Point = A vec3 point like {x=1,y=2,z=3}. Can be obtained from a unit like so: Unit.getName("Unit1"):getPoint() +-- _searchRadius = random distance for units to move from spawn zone (0 will leave troops at the spawn position - no search for enemy) +-- +-- Example: ctld.spawnGroupAtPoint("red", 2, {x=1,y=2,z=3}, 1000) +-- +-- This example will spawn 2 groups of russians at the specified point +-- and they will search for enemy or move randomly withing 1000m +-- OR +-- +-- ctld.spawnGroupAtPoint("blue", {mg=1,at=2,aa=3,inf=4,mortar=5}, {x=1,y=2,z=3}, 2000) +-- Spawns 1 machine gun, 2 anti tank, 3 anti air, 4 standard soldiers and 5 mortars +function ctld.spawnGroupAtPoint(_groupSide, _number, _point, _searchRadius) + + local _country + if _groupSide == "red" then + _groupSide = 1 + _country = 0 + else + _groupSide = 2 + _country = 2 + end + + if _searchRadius < 0 then + _searchRadius = 0 + end + + local _groupDetails = ctld.generateTroopTypes(_groupSide, _number, _country) + + local _droppedTroops = ctld.spawnDroppedGroup(_point, _groupDetails, false, _searchRadius); + + if _groupSide == 1 then + table.insert(ctld.droppedTroopsRED, _droppedTroops:getName()) + else + table.insert(ctld.droppedTroopsBLUE, _droppedTroops:getName()) + end +end + + +-- Preloads a transport with troops or vehicles +-- replaces any troops currently on board +function ctld.preLoadTransport(_unitName, _number, _troops) + + local _unit = ctld.getTransportUnit(_unitName) + + if _unit ~= nil then + + -- will replace any units currently on board + -- if not ctld.troopsOnboard(_unit,_troops) then + ctld.loadTroops(_unit, _troops, _number) + -- end + end +end + + +-- Continuously counts the number of crates in a zone and sets the value of the passed in flag +-- to the count amount +-- This means you can trigger actions based on the count and also trigger messages before the count is reached +-- Just pass in the zone name and flag number like so as a single (NOT Continuous) Trigger +-- This will now work for Mission Editor and Spawned Crates +-- e.g. ctld.cratesInZone("DropZone1", 5) +function ctld.cratesInZone(_zone, _flagNumber) + local _triggerZone = trigger.misc.getZone(_zone) -- trigger to use as reference position + + if _triggerZone == nil then + trigger.action.outText("CTLD.lua ERROR: Cant find zone called " .. _zone, 10) + return + end + + local _zonePos = mist.utils.zoneToVec3(_zone) + + --ignore side, if crate has been used its discounted from the count + local _crateTables = { ctld.spawnedCratesRED, ctld.spawnedCratesBLUE, ctld.missionEditorCargoCrates } + + local _crateCount = 0 + + for _, _crates in pairs(_crateTables) do + + for _crateName, _dontUse in pairs(_crates) do + + --get crate + local _crate = ctld.getCrateObject(_crateName) + + --in air seems buggy with crates so if in air is true, get the height above ground and the speed magnitude + if _crate ~= nil and _crate:getLife() > 0 + and (ctld.inAir(_crate) == false) then + + local _dist = ctld.getDistance(_crate:getPoint(), _zonePos) + + if _dist <= _triggerZone.radius then + _crateCount = _crateCount + 1 + end + end + end + end + + --set flag stuff + trigger.action.setUserFlag(_flagNumber, _crateCount) + + -- env.info("FLAG ".._flagNumber.." crates ".._crateCount) + + --retrigger in 5 seconds + timer.scheduleFunction(function(_args) + + ctld.cratesInZone(_args[1], _args[2]) + end, { _zone, _flagNumber }, timer.getTime() + 5) +end + +-- Creates an extraction zone +-- any Soldiers (not vehicles) dropped at this zone by a helicopter will disappear +-- and be added to a running total of soldiers for a set flag number +-- The idea is you can then drop say 20 troops in a zone and trigger an action using the mission editor triggers +-- and the flag value +-- +-- The ctld.createExtractZone function needs to be called once in a trigger action do script. +-- if you dont want smoke, pass -1 to the function. +--Green = 0 , Red = 1, White = 2, Orange = 3, Blue = 4, NO SMOKE = -1 +-- +-- e.g. ctld.createExtractZone("extractzone1", 2, -1) will create an extraction zone at trigger zone "extractzone1", store the number of troops dropped at +-- the zone in flag 2 and not have smoke +-- +-- +-- +function ctld.createExtractZone(_zone, _flagNumber, _smoke) + local _triggerZone = trigger.misc.getZone(_zone) -- trigger to use as reference position + + if _triggerZone == nil then + trigger.action.outText("CTLD.lua ERROR: Cant find zone called " .. _zone, 10) + return + end + + local _pos2 = { x = _triggerZone.point.x, y = _triggerZone.point.z } + local _alt = land.getHeight(_pos2) + local _pos3 = { x = _pos2.x, y = _alt, z = _pos2.y } + + trigger.action.setUserFlag(_flagNumber, 0) --start at 0 + + local _details = { point = _pos3, name = _zone, smoke = _smoke, flag = _flagNumber, radius = _triggerZone.radius} + + ctld.extractZones[_zone.."-".._flagNumber] = _details + + if _smoke ~= nil and _smoke > -1 then + + local _smokeFunction + + _smokeFunction = function(_args) + + local _extractDetails = ctld.extractZones[_zone.."-".._flagNumber] + -- check zone is still active + if _extractDetails == nil then + -- stop refreshing smoke, zone is done + return + end + + + trigger.action.smoke(_args.point, _args.smoke) + --refresh in 5 minutes + timer.scheduleFunction(_smokeFunction, _args, timer.getTime() + 300) + end + + --run local function + _smokeFunction(_details) + end +end + + +-- Removes an extraction zone +-- +-- The smoke will take up to 5 minutes to disappear depending on the last time the smoke was activated +-- +-- The ctld.removeExtractZone function needs to be called once in a trigger action do script. +-- +-- e.g. ctld.removeExtractZone("extractzone1", 2) will remove an extraction zone at trigger zone "extractzone1" +-- that was setup with flag 2 +-- +-- +-- +function ctld.removeExtractZone(_zone,_flagNumber) + + local _extractDetails = ctld.extractZones[_zone.."-".._flagNumber] + + if _extractDetails ~= nil then + --remove zone + ctld.extractZones[_zone.."-".._flagNumber] = nil + + end +end + +-- CONTINUOUS TRIGGER FUNCTION +-- This function will count the current number of extractable RED and BLUE +-- GROUPS in a zone and store the values in two flags +-- A group is only counted as being in a zone when the leader of that group +-- is in the zone +-- Use: ctld.countDroppedGroupsInZone("Zone Name", flagBlue, flagRed) +function ctld.countDroppedGroupsInZone(_zone, _blueFlag, _redFlag) + + local _triggerZone = trigger.misc.getZone(_zone) -- trigger to use as reference position + + if _triggerZone == nil then + trigger.action.outText("CTLD.lua ERROR: Cant find zone called " .. _zone, 10) + return + end + + local _zonePos = mist.utils.zoneToVec3(_zone) + + local _redCount = 0; + local _blueCount = 0; + + local _allGroups = {ctld.droppedTroopsRED,ctld.droppedTroopsBLUE,ctld.droppedVehiclesRED,ctld.droppedVehiclesBLUE} + for _, _extractGroups in pairs(_allGroups) do + for _,_groupName in pairs(_extractGroups) do + local _groupUnits = ctld.getGroup(_groupName) + + if #_groupUnits > 0 then + local _zonePos = mist.utils.zoneToVec3(_zone) + local _dist = ctld.getDistance(_groupUnits[1]:getPoint(), _zonePos) + + if _dist <= _triggerZone.radius then + + if (_groupUnits[1]:getCoalition() == 1) then + _redCount = _redCount + 1; + else + _blueCount = _blueCount + 1; + end + end + end + end + end + --set flag stuff + trigger.action.setUserFlag(_blueFlag, _blueCount) + trigger.action.setUserFlag(_redFlag, _redCount) + + -- env.info("Groups in zone ".._blueCount.." ".._redCount) + +end + +-- CONTINUOUS TRIGGER FUNCTION +-- This function will count the current number of extractable RED and BLUE +-- UNITS in a zone and store the values in two flags + +-- Use: ctld.countDroppedUnitsInZone("Zone Name", flagBlue, flagRed) +function ctld.countDroppedUnitsInZone(_zone, _blueFlag, _redFlag) + + local _triggerZone = trigger.misc.getZone(_zone) -- trigger to use as reference position + + if _triggerZone == nil then + trigger.action.outText("CTLD.lua ERROR: Cant find zone called " .. _zone, 10) + return + end + + local _zonePos = mist.utils.zoneToVec3(_zone) + + local _redCount = 0; + local _blueCount = 0; + + local _allGroups = {ctld.droppedTroopsRED,ctld.droppedTroopsBLUE,ctld.droppedVehiclesRED,ctld.droppedVehiclesBLUE} + + for _, _extractGroups in pairs(_allGroups) do + for _,_groupName in pairs(_extractGroups) do + local _groupUnits = ctld.getGroup(_groupName) + + if #_groupUnits > 0 then + + local _zonePos = mist.utils.zoneToVec3(_zone) + for _,_unit in pairs(_groupUnits) do + local _dist = ctld.getDistance(_unit:getPoint(), _zonePos) + + if _dist <= _triggerZone.radius then + + if (_unit:getCoalition() == 1) then + _redCount = _redCount + 1; + else + _blueCount = _blueCount + 1; + end + end + end + end + end + end + + + --set flag stuff + trigger.action.setUserFlag(_blueFlag, _blueCount) + trigger.action.setUserFlag(_redFlag, _redCount) + + -- env.info("Units in zone ".._blueCount.." ".._redCount) +end + + +-- Creates a radio beacon on a random UHF - VHF and HF/FM frequency for homing +-- This WILL NOT WORK if you dont add beacon.ogg and beaconsilent.ogg to the mission!!! +-- e.g. ctld.createRadioBeaconAtZone("beaconZone","red", 1440,"Waypoint 1") will create a beacon at trigger zone "beaconZone" for the Red side +-- that will last 1440 minutes (24 hours ) and named "Waypoint 1" in the list of radio beacons +-- +-- e.g. ctld.createRadioBeaconAtZone("beaconZoneBlue","blue", 20) will create a beacon at trigger zone "beaconZoneBlue" for the Blue side +-- that will last 20 minutes +function ctld.createRadioBeaconAtZone(_zone, _coalition, _batteryLife, _name) + local _triggerZone = trigger.misc.getZone(_zone) -- trigger to use as reference position + + if _triggerZone == nil then + trigger.action.outText("CTLD.lua ERROR: Cant find zone called " .. _zone, 10) + return + end + + local _zonePos = mist.utils.zoneToVec3(_zone) + + ctld.beaconCount = ctld.beaconCount + 1 + + if _name == nil or _name == "" then + _name = "Beacon #" .. ctld.beaconCount + end + + if _coalition == "red" then + ctld.createRadioBeacon(_zonePos, 1, 0, _name, _batteryLife) --1440 + else + ctld.createRadioBeacon(_zonePos, 2, 2, _name, _batteryLife) --1440 + end +end + + +-- Activates a pickup zone +-- Activates a pickup zone when called from a trigger +-- EG: ctld.activatePickupZone("pickzone3") +-- This is enable pickzone3 to be used as a pickup zone for the team set +function ctld.activatePickupZone(_zoneName) + ctld.logDebug(string.format("ctld.activatePickupZone(_zoneName=%s)", ctld.p(_zoneName))) + + local _triggerZone = trigger.misc.getZone(_zoneName) -- trigger to use as reference position + + if _triggerZone == nil then + local _ship = ctld.getTransportUnit(_triggerZone) + + if _ship then + local _point = _ship:getPoint() + _triggerZone = {} + _triggerZone.point = _point + end + + end + + if _triggerZone == nil then + trigger.action.outText("CTLD.lua ERROR: Cant find zone or ship called " .. _zoneName, 10) + end + + for _, _zoneDetails in pairs(ctld.pickupZones) do + + if _zoneName == _zoneDetails[1] then + + --smoke could get messy if designer keeps calling this on an active zone, check its not active first + if _zoneDetails[4] == 1 then + -- they might have a continuous trigger so i've hidden the warning + --trigger.action.outText("CTLD.lua ERROR: Pickup Zone already active: " .. _zoneName, 10) + return + end + + _zoneDetails[4] = 1 --activate zone + + if ctld.disableAllSmoke == true then --smoke disabled + return + end + + if _zoneDetails[2] >= 0 then + + -- Trigger smoke marker + -- This will cause an overlapping smoke marker on next refreshsmoke call + -- but will only happen once + local _pos2 = { x = _triggerZone.point.x, y = _triggerZone.point.z } + local _alt = land.getHeight(_pos2) + local _pos3 = { x = _pos2.x, y = _alt, z = _pos2.y } + + trigger.action.smoke(_pos3, _zoneDetails[2]) + end + end + end +end + + +-- Deactivates a pickup zone +-- Deactivates a pickup zone when called from a trigger +-- EG: ctld.deactivatePickupZone("pickzone3") +-- This is disables pickzone3 and can no longer be used to as a pickup zone +-- These functions can be called by triggers, like if a set of buildings is used, you can trigger the zone to be 'not operational' +-- once they are destroyed +function ctld.deactivatePickupZone(_zoneName) + + local _triggerZone = trigger.misc.getZone(_zoneName) -- trigger to use as reference position + + if _triggerZone == nil then + local _ship = ctld.getTransportUnit(_triggerZone) + + if _ship then + local _point = _ship:getPoint() + _triggerZone = {} + _triggerZone.point = _point + end + + end + + if _triggerZone == nil then + trigger.action.outText("CTLD.lua ERROR: Cant find zone called " .. _zoneName, 10) + return + end + + for _, _zoneDetails in pairs(ctld.pickupZones) do + + if _zoneName == _zoneDetails[1] then + + -- i'd just ignore it if its already been deactivated + -- if _zoneDetails[4] == 0 then --this really needed?? + -- trigger.action.outText("CTLD.lua ERROR: Pickup Zone already deactiveated: " .. _zoneName, 10) + -- return + -- end + + _zoneDetails[4] = 0 --deactivate zone + end + end +end + +-- Change the remaining groups currently available for pickup at a zone +-- e.g. ctld.changeRemainingGroupsForPickupZone("pickup1", 5) -- adds 5 groups +-- ctld.changeRemainingGroupsForPickupZone("pickup1", -3) -- remove 3 groups +function ctld.changeRemainingGroupsForPickupZone(_zoneName, _amount) + local _triggerZone = trigger.misc.getZone(_zoneName) -- trigger to use as reference position + + if _triggerZone == nil then + local _ship = ctld.getTransportUnit(_triggerZone) + + if _ship then + local _point = _ship:getPoint() + _triggerZone = {} + _triggerZone.point = _point + end + + end + + if _triggerZone == nil then + trigger.action.outText("CTLD.lua ctld.changeRemainingGroupsForPickupZone ERROR: Cant find zone called " .. _zoneName, 10) + return + end + + for _, _zoneDetails in pairs(ctld.pickupZones) do + + if _zoneName == _zoneDetails[1] then + ctld.updateZoneCounter(_zoneName, _amount) + end + end + + +end + +-- Activates a Waypoint zone +-- Activates a Waypoint zone when called from a trigger +-- EG: ctld.activateWaypointZone("pickzone3") +-- This means that troops dropped within the radius of the zone will head to the center +-- of the zone instead of searching for troops +function ctld.activateWaypointZone(_zoneName) + local _triggerZone = trigger.misc.getZone(_zoneName) -- trigger to use as reference position + + + if _triggerZone == nil then + trigger.action.outText("CTLD.lua ERROR: Cant find zone called " .. _zoneName, 10) + + return + end + + for _, _zoneDetails in pairs(ctld.wpZones) do + + if _zoneName == _zoneDetails[1] then + + --smoke could get messy if designer keeps calling this on an active zone, check its not active first + if _zoneDetails[3] == 1 then + -- they might have a continuous trigger so i've hidden the warning + --trigger.action.outText("CTLD.lua ERROR: Pickup Zone already active: " .. _zoneName, 10) + return + end + + _zoneDetails[3] = 1 --activate zone + + if ctld.disableAllSmoke == true then --smoke disabled + return + end + + if _zoneDetails[2] >= 0 then + + -- Trigger smoke marker + -- This will cause an overlapping smoke marker on next refreshsmoke call + -- but will only happen once + local _pos2 = { x = _triggerZone.point.x, y = _triggerZone.point.z } + local _alt = land.getHeight(_pos2) + local _pos3 = { x = _pos2.x, y = _alt, z = _pos2.y } + + trigger.action.smoke(_pos3, _zoneDetails[2]) + end + end + end +end + + +-- Deactivates a Waypoint zone +-- Deactivates a Waypoint zone when called from a trigger +-- EG: ctld.deactivateWaypointZone("wpzone3") +-- This disables wpzone3 so that troops dropped in this zone will search for troops as normal +-- These functions can be called by triggers +function ctld.deactivateWaypointZone(_zoneName) + + local _triggerZone = trigger.misc.getZone(_zoneName) + + if _triggerZone == nil then + trigger.action.outText("CTLD.lua ERROR: Cant find zone called " .. _zoneName, 10) + return + end + + for _, _zoneDetails in pairs(ctld.pickupZones) do + + if _zoneName == _zoneDetails[1] then + + _zoneDetails[3] = 0 --deactivate zone + end + end +end + +-- Continuous Trigger Function +-- Causes an AI unit with the specified name to unload troops / vehicles when +-- an enemy is detected within a specified distance +-- The enemy must have Line or Sight to the unit to be detected +function ctld.unloadInProximityToEnemy(_unitName,_distance) + + local _unit = ctld.getTransportUnit(_unitName) + + if _unit ~= nil and _unit:getPlayerName() == nil then + + -- no player name means AI! + -- the findNearest visible enemy you'd want to modify as it'll find enemies quite far away + -- limited by ctld.JTAC_maxDistance + local _nearestEnemy = ctld.findNearestVisibleEnemy(_unit,"all",_distance) + + if _nearestEnemy ~= nil then + + if ctld.troopsOnboard(_unit, true) then + ctld.deployTroops(_unit, true) + return true + end + + if ctld.unitCanCarryVehicles(_unit) and ctld.troopsOnboard(_unit, false) then + ctld.deployTroops(_unit, false) + return true + end + end + end + + return false + +end + + + +-- Unit will unload any units onboard if the unit is on the ground +-- when this function is called +function ctld.unloadTransport(_unitName) + + local _unit = ctld.getTransportUnit(_unitName) + + if _unit ~= nil then + + if ctld.troopsOnboard(_unit, true) then + ctld.unloadTroops({_unitName,true}) + end + + if ctld.unitCanCarryVehicles(_unit) and ctld.troopsOnboard(_unit, false) then + ctld.unloadTroops({_unitName,false}) + end + end + +end + +-- Loads Troops and Vehicles from a zone or picks up nearby troops or vehicles +function ctld.loadTransport(_unitName) + + local _unit = ctld.getTransportUnit(_unitName) + + if _unit ~= nil then + + ctld.loadTroopsFromZone({ _unitName, true,"",true }) + + if ctld.unitCanCarryVehicles(_unit) then + ctld.loadTroopsFromZone({ _unitName, false,"",true }) + end + + end + +end + +-- adds a callback that will be called for many actions ingame +function ctld.addCallback(_callback) + + table.insert(ctld.callbacks,_callback) + +end + +-- Spawns a sling loadable crate at a Trigger Zone +-- +-- Weights can be found in the ctld.spawnableCrates list +-- e.g. ctld.spawnCrateAtZone("red", 500,"triggerzone1") -- spawn a humvee at triggerzone 1 for red side +-- e.g. ctld.spawnCrateAtZone("blue", 505,"triggerzone1") -- spawn a tow humvee at triggerzone1 for blue side +-- +function ctld.spawnCrateAtZone(_side, _weight,_zone) + local _spawnTrigger = trigger.misc.getZone(_zone) -- trigger to use as reference position + + if _spawnTrigger == nil then + trigger.action.outText("CTLD.lua ERROR: Cant find zone called " .. _zone, 10) + return + end + + local _crateType = ctld.crateLookupTable[tostring(_weight)] + + if _crateType == nil then + trigger.action.outText("CTLD.lua ERROR: Cant find crate with weight " .. _weight, 10) + return + end + + local _country + if _side == "red" then + _side = 1 + _country = 0 + else + _side = 2 + _country = 2 + end + + local _pos2 = { x = _spawnTrigger.point.x, y = _spawnTrigger.point.z } + local _alt = land.getHeight(_pos2) + local _point = { x = _pos2.x, y = _alt, z = _pos2.y } + + local _unitId = ctld.getNextUnitId() + + local _name = string.format("%s #%i", _crateType.desc, _unitId) + + local _spawnedCrate = ctld.spawnCrateStatic(_country, _unitId, _point, _name, _crateType.weight,_side) + +end + +-- Spawns a sling loadable crate at a Point +-- +-- Weights can be found in the ctld.spawnableCrates list +-- Points can be made by hand or obtained from a Unit position by Unit.getByName("PilotName"):getPoint() +-- e.g. ctld.spawnCrateAtZone("red", 500,{x=1,y=2,z=3}) -- spawn a humvee at triggerzone 1 for red side at a specified point +-- e.g. ctld.spawnCrateAtZone("blue", 505,{x=1,y=2,z=3}) -- spawn a tow humvee at triggerzone1 for blue side at a specified point +-- +-- +function ctld.spawnCrateAtPoint(_side, _weight,_point) + + + local _crateType = ctld.crateLookupTable[tostring(_weight)] + + if _crateType == nil then + trigger.action.outText("CTLD.lua ERROR: Cant find crate with weight " .. _weight, 10) + return + end + + local _country + if _side == "red" then + _side = 1 + _country = 0 + else + _side = 2 + _country = 2 + end + + local _unitId = ctld.getNextUnitId() + + local _name = string.format("%s #%i", _crateType.desc, _unitId) + + local _spawnedCrate = ctld.spawnCrateStatic(_country, _unitId, _point, _name, _crateType.weight,_side) + +end + +-- *************************************************************** +-- **************** BE CAREFUL BELOW HERE ************************ +-- *************************************************************** + +--- Tells CTLD What multipart AA Systems there are and what parts they need +-- A New system added here also needs the launcher added +ctld.AASystemTemplate = { + + { + name = "HAWK AA System", + count = 4, + parts = { + {name = "Hawk ln", desc = "HAWK Launcher", launcher = true}, + {name = "Hawk tr", desc = "HAWK Track Radar"}, + {name = "Hawk sr", desc = "HAWK Search Radar"}, + {name = "Hawk pcp", desc = "HAWK PCP"}, + {name = "Hawk cwar", desc = "HAWK CWAR"}, + }, + repair = "HAWK Repair", + }, + { + name = "Patriot AA System", + count = 4, + parts = { + {name = "Patriot ln", desc = "Patriot Launcher", launcher = true}, + {name = "Patriot ECS", desc = "Patriot Control Unit"}, + {name = "Patriot str", desc = "Patriot Search and Track Radar"}, + }, + repair = "Patriot Repair", + }, + { + name = "BUK AA System", + count = 3, + parts = { + {name = "SA-11 Buk LN 9A310M1", desc = "BUK Launcher" , launcher = true}, + {name = "SA-11 Buk CC 9S470M1", desc = "BUK CC Radar"}, + {name = "SA-11 Buk SR 9S18M1", desc = "BUK Search Radar"}, + }, + repair = "BUK Repair", + }, + { + name = "KUB AA System", + count = 2, + parts = { + {name = "Kub 2P25 ln", desc = "KUB Launcher", launcher = true}, + {name = "Kub 1S91 str", desc = "KUB Radar"}, + }, + repair = "KUB Repair", + }, +} + + +ctld.crateWait = {} +ctld.crateMove = {} + +---------------- INTERNAL FUNCTIONS ---------------- +--- +--- +------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Utility methods +------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- print an object for a debugging log +function ctld.p(o, level) + local MAX_LEVEL = 20 + if level == nil then level = 0 end + if level > MAX_LEVEL then + ctld.logError("max depth reached in ctld.p : "..tostring(MAX_LEVEL)) + return "" + end + local text = "" + if (type(o) == "table") then + text = "\n" + for key,value in pairs(o) do + for i=0, level do + text = text .. " " + end + text = text .. ".".. key.."="..ctld.p(value, level+1) .. "\n" + end + elseif (type(o) == "function") then + text = "[function]" + elseif (type(o) == "boolean") then + if o == true then + text = "[true]" + else + text = "[false]" + end + else + if o == nil then + text = "[nil]" + else + text = tostring(o) + end + end + return text +end + +function ctld.logError(message) + env.info(" E - " .. ctld.Id .. message) +end + +function ctld.logInfo(message) + env.info(" I - " .. ctld.Id .. message) +end + +function ctld.logDebug(message) + if message and ctld.Debug then + env.info(" D - " .. ctld.Id .. message) + end +end + +function ctld.logTrace(message) + if message and ctld.Trace then + env.info(" T - " .. ctld.Id .. message) + end +end + +ctld.nextUnitId = 1; +ctld.getNextUnitId = function() + ctld.nextUnitId = ctld.nextUnitId + 1 + + return ctld.nextUnitId +end + +ctld.nextGroupId = 1; + +ctld.getNextGroupId = function() + ctld.nextGroupId = ctld.nextGroupId + 1 + + return ctld.nextGroupId +end + +function ctld.getTransportUnit(_unitName) + + if _unitName == nil then + return nil + end + + local _heli = Unit.getByName(_unitName) + + if _heli ~= nil and _heli:isActive() and _heli:getLife() > 0 then + + return _heli + end + + return nil +end + +function ctld.spawnCrateStatic(_country, _unitId, _point, _name, _weight,_side) + + local _crate + local _spawnedCrate + + if ctld.staticBugWorkaround and ctld.slingLoad == false then + local _groupId = ctld.getNextGroupId() + local _groupName = "Crate Group #".._groupId + + local _group = { + ["visible"] = false, + -- ["groupId"] = _groupId, + ["hidden"] = false, + ["units"] = {}, + -- ["y"] = _positions[1].z, + -- ["x"] = _positions[1].x, + ["name"] = _groupName, + ["task"] = {}, + } + + _group.units[1] = ctld.createUnit(_point.x , _point.z , 0, {type="UAZ-469",name=_name,unitId=_unitId}) + + --switch to MIST + _group.category = Group.Category.GROUND; + _group.country = _country; + + local _spawnedGroup = Group.getByName(mist.dynAdd(_group).name) + + -- Turn off AI + trigger.action.setGroupAIOff(_spawnedGroup) + + _spawnedCrate = Unit.getByName(_name) + else + + if ctld.slingLoad then + _crate = mist.utils.deepCopy(ctld.spawnableCratesModel_sling) + _crate["canCargo"] = true + else + _crate = mist.utils.deepCopy(ctld.spawnableCratesModel_load) + _crate["canCargo"] = false + end + + _crate["y"] = _point.z + _crate["x"] = _point.x + _crate["mass"] = _weight + _crate["name"] = _name + _crate["heading"] = 0 + _crate["country"] = _country + + ctld.logTrace(string.format("_crate=%s", ctld.p(_crate))) + mist.dynAddStatic(_crate) + + _spawnedCrate = StaticObject.getByName(_crate["name"]) + end + + + local _crateType = ctld.crateLookupTable[tostring(_weight)] + + if _side == 1 then + ctld.spawnedCratesRED[_name] =_crateType + else + ctld.spawnedCratesBLUE[_name] = _crateType + end + + return _spawnedCrate +end + +function ctld.spawnFOBCrateStatic(_country, _unitId, _point, _name) + + local _crate = { + ["category"] = "Fortifications", + ["shape_name"] = "konteiner_red1", + ["type"] = "Container red 1", + -- ["unitId"] = _unitId, + ["y"] = _point.z, + ["x"] = _point.x, + ["name"] = _name, + ["canCargo"] = false, + ["heading"] = 0, + } + + _crate["country"] = _country + + mist.dynAddStatic(_crate) + + local _spawnedCrate = StaticObject.getByName(_crate["name"]) + --local _spawnedCrate = coalition.addStaticObject(_country, _crate) + + return _spawnedCrate +end + + +function ctld.spawnFOB(_country, _unitId, _point, _name) + + local _crate = { + ["category"] = "Fortifications", + ["type"] = "outpost", + -- ["unitId"] = _unitId, + ["y"] = _point.z, + ["x"] = _point.x, + ["name"] = _name, + ["canCargo"] = false, + ["heading"] = 0, + } + + _crate["country"] = _country + mist.dynAddStatic(_crate) + local _spawnedCrate = StaticObject.getByName(_crate["name"]) + --local _spawnedCrate = coalition.addStaticObject(_country, _crate) + + local _id = ctld.getNextUnitId() + local _tower = { + ["type"] = "house2arm", + -- ["unitId"] = _id, + ["rate"] = 100, + ["y"] = _point.z + -36.57142857, + ["x"] = _point.x + 14.85714286, + ["name"] = "FOB Watchtower #" .. _id, + ["category"] = "Fortifications", + ["canCargo"] = false, + ["heading"] = 0, + } + --coalition.addStaticObject(_country, _tower) + _tower["country"] = _country + + mist.dynAddStatic(_tower) + + return _spawnedCrate +end + + +function ctld.spawnCrate(_arguments) + + local _status, _err = pcall(function(_args) + + -- use the cargo weight to guess the type of unit as no way to add description :( + + local _crateType = ctld.crateLookupTable[tostring(_args[2])] + local _heli = ctld.getTransportUnit(_args[1]) + + if _crateType ~= nil and _heli ~= nil and ctld.inAir(_heli) == false then + + if ctld.inLogisticsZone(_heli) == false then + + ctld.displayMessageToGroup(_heli, "You are not close enough to friendly logistics to get a crate!", 10) + + return + end + + if ctld.isJTACUnitType(_crateType.unit) then + + local _limitHit = false + + if _heli:getCoalition() == 1 then + + if ctld.JTAC_LIMIT_RED == 0 then + _limitHit = true + else + ctld.JTAC_LIMIT_RED = ctld.JTAC_LIMIT_RED - 1 + end + else + if ctld.JTAC_LIMIT_BLUE == 0 then + _limitHit = true + else + ctld.JTAC_LIMIT_BLUE = ctld.JTAC_LIMIT_BLUE - 1 + end + end + + if _limitHit then + ctld.displayMessageToGroup(_heli, "No more JTAC Crates Left!", 10) + return + end + end + + local _position = _heli:getPosition() + + -- check crate spam + if _heli:getPlayerName() ~= nil and ctld.crateWait[_heli:getPlayerName()] and ctld.crateWait[_heli:getPlayerName()] > timer.getTime() then + + ctld.displayMessageToGroup(_heli,"Sorry you must wait "..(ctld.crateWait[_heli:getPlayerName()] - timer.getTime()).. " seconds before you can get another crate", 20) + return + end + + if _heli:getPlayerName() ~= nil then + ctld.crateWait[_heli:getPlayerName()] = timer.getTime() + ctld.crateWaitTime + end + -- trigger.action.outText("Spawn Crate".._args[1].." ".._args[2],10) + + local _heli = ctld.getTransportUnit(_args[1]) + + local _point = ctld.getPointAt12Oclock(_heli, 30) + + local _unitId = ctld.getNextUnitId() + + local _side = _heli:getCoalition() + + local _name = string.format("%s #%i", _crateType.desc, _unitId) + + local _spawnedCrate = ctld.spawnCrateStatic(_heli:getCountry(), _unitId, _point, _name, _crateType.weight,_side) + + -- add to move table + ctld.crateMove[_name] = _name + + ctld.displayMessageToGroup(_heli, string.format("A %s crate weighing %s kg has been brought out and is at your 12 o'clock ", _crateType.desc, _crateType.weight), 20) + + else + env.info("Couldn't find crate item to spawn") + end + end, _arguments) + + if (not _status) then + env.error(string.format("CTLD ERROR: %s", _err)) + end +end + +function ctld.getPointAt12Oclock(_unit, _offset) + + local _position = _unit:getPosition() + local _angle = math.atan2(_position.x.z, _position.x.x) + local _xOffset = math.cos(_angle) * _offset + local _yOffset = math.sin(_angle) * _offset + + local _point = _unit:getPoint() + return { x = _point.x + _xOffset, z = _point.z + _yOffset, y = _point.y } +end + +function ctld.troopsOnboard(_heli, _troops) + + if ctld.inTransitTroops[_heli:getName()] ~= nil then + + local _onboard = ctld.inTransitTroops[_heli:getName()] + + if _troops then + + if _onboard.troops ~= nil and _onboard.troops.units ~= nil and #_onboard.troops.units > 0 then + return true + else + return false + end + else + + if _onboard.vehicles ~= nil and _onboard.vehicles.units ~= nil and #_onboard.vehicles.units > 0 then + return true + else + return false + end + end + + else + return false + end +end + +-- if its dropped by AI then there is no player name so return the type of unit +function ctld.getPlayerNameOrType(_heli) + + if _heli:getPlayerName() == nil then + + return _heli:getTypeName() + else + return _heli:getPlayerName() + end +end + +function ctld.inExtractZone(_heli) + + local _heliPoint = _heli:getPoint() + + for _, _zoneDetails in pairs(ctld.extractZones) do + + --get distance to center + local _dist = ctld.getDistance(_heliPoint, _zoneDetails.point) + + if _dist <= _zoneDetails.radius then + return _zoneDetails + end + end + + return false +end + +-- safe to fast rope if speed is less than 0.5 Meters per second +function ctld.safeToFastRope(_heli) + + if ctld.enableFastRopeInsertion == false then + return false + end + + --landed or speed is less than 8 km/h and height is less than fast rope height + if (ctld.inAir(_heli) == false or (ctld.heightDiff(_heli) <= ctld.fastRopeMaximumHeight + 3.0 and mist.vec.mag(_heli:getVelocity()) < 2.2)) then + return true + end +end + +function ctld.metersToFeet(_meters) + + local _feet = _meters * 3.2808399 + + return mist.utils.round(_feet) +end + +function ctld.inAir(_heli) + + if _heli:inAir() == false then + return false + end + + -- less than 5 cm/s a second so landed + -- BUT AI can hold a perfect hover so ignore AI + if mist.vec.mag(_heli:getVelocity()) < 0.05 and _heli:getPlayerName() ~= nil then + return false + end + return true +end + +function ctld.deployTroops(_heli, _troops) + + local _onboard = ctld.inTransitTroops[_heli:getName()] + + -- deloy troops + if _troops then + if _onboard.troops ~= nil and #_onboard.troops.units > 0 then + if ctld.inAir(_heli) == false or ctld.safeToFastRope(_heli) then + + -- check we're not in extract zone + local _extractZone = ctld.inExtractZone(_heli) + + if _extractZone == false then + + local _droppedTroops = ctld.spawnDroppedGroup(_heli:getPoint(), _onboard.troops, false) + ctld.logTrace(string.format("_onboard.troops=%s", ctld.p(_onboard.troops))) + if _onboard.troops.jtac or _droppedTroops:getName():lower():find("jtac") then + local _code = table.remove(ctld.jtacGeneratedLaserCodes, 1) + ctld.logTrace(string.format("_code=%s", ctld.p(_code))) + table.insert(ctld.jtacGeneratedLaserCodes, _code) + ctld.logTrace(string.format("_droppedTroops:getName()=%s", ctld.p(_droppedTroops:getName()))) + ctld.JTACAutoLase(_droppedTroops:getName(), _code) + end + + if _heli:getCoalition() == 1 then + + table.insert(ctld.droppedTroopsRED, _droppedTroops:getName()) + else + + table.insert(ctld.droppedTroopsBLUE, _droppedTroops:getName()) + end + + ctld.inTransitTroops[_heli:getName()].troops = nil + ctld.adaptWeightToCargo(_heli:getName()) + + if ctld.inAir(_heli) then + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " fast-ropped troops from " .. _heli:getTypeName() .. " into combat", 10) + else + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " dropped troops from " .. _heli:getTypeName() .. " into combat", 10) + end + + ctld.processCallback({unit = _heli, unloaded = _droppedTroops, action = "dropped_troops"}) + + + else + --extract zone! + local _droppedCount = trigger.misc.getUserFlag(_extractZone.flag) + + _droppedCount = (#_onboard.troops.units) + _droppedCount + + trigger.action.setUserFlag(_extractZone.flag, _droppedCount) + + ctld.inTransitTroops[_heli:getName()].troops = nil + ctld.adaptWeightToCargo(_heli:getName()) + + if ctld.inAir(_heli) then + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " troops fast-ropped from " .. _heli:getTypeName() .. " into " .. _extractZone.name, 10) + else + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " troops dropped from " .. _heli:getTypeName() .. " into " .. _extractZone.name, 10) + end + end + else + ctld.displayMessageToGroup(_heli, "Too high or too fast to drop troops into combat! Hover below " .. ctld.metersToFeet(ctld.fastRopeMaximumHeight) .. " feet or land.", 10) + end + end + + else + if ctld.inAir(_heli) == false then + if _onboard.vehicles ~= nil and #_onboard.vehicles.units > 0 then + + local _droppedVehicles = ctld.spawnDroppedGroup(_heli:getPoint(), _onboard.vehicles, true) + + if _heli:getCoalition() == 1 then + + table.insert(ctld.droppedVehiclesRED, _droppedVehicles:getName()) + else + + table.insert(ctld.droppedVehiclesBLUE, _droppedVehicles:getName()) + end + + ctld.inTransitTroops[_heli:getName()].vehicles = nil + ctld.adaptWeightToCargo(_heli:getName()) + + ctld.processCallback({unit = _heli, unloaded = _droppedVehicles, action = "dropped_vehicles"}) + + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " dropped vehicles from " .. _heli:getTypeName() .. " into combat", 10) + end + end + end +end + +function ctld.insertIntoTroopsArray(_troopType,_count,_troopArray,_troopName) + + for _i = 1, _count do + local _unitId = ctld.getNextUnitId() + table.insert(_troopArray, { type = _troopType, unitId = _unitId, name = string.format("Dropped %s #%i", _troopName or _troopType, _unitId) }) + end + + return _troopArray + +end + + +function ctld.generateTroopTypes(_side, _countOrTemplate, _country) + local _troops = {} + local _weight = 0 + local _hasJTAC = false + + local function getSoldiersWeight(count, additionalWeight) + local _weight = 0 + for i = 1, count do + local _soldierWeight = math.random(90, 120) * ctld.SOLDIER_WEIGHT / 100 + ctld.logTrace(string.format("_soldierWeight=%s", ctld.p(_soldierWeight))) + _weight = _weight + _soldierWeight + ctld.KIT_WEIGHT + additionalWeight + end + return _weight + end + + if type(_countOrTemplate) == "table" then + + if _countOrTemplate.aa then + ctld.logTrace(string.format("_countOrTemplate.aa=%s", ctld.p(_countOrTemplate.aa))) + if _side == 2 then + _troops = ctld.insertIntoTroopsArray("Soldier stinger",_countOrTemplate.aa,_troops) + else + _troops = ctld.insertIntoTroopsArray("SA-18 Igla manpad",_countOrTemplate.aa,_troops) + end + _weight = _weight + getSoldiersWeight(_countOrTemplate.aa, ctld.MANPAD_WEIGHT) + ctld.logTrace(string.format("_weight=%s", ctld.p(_weight))) + end + + if _countOrTemplate.inf then + ctld.logTrace(string.format("_countOrTemplate.inf=%s", ctld.p(_countOrTemplate.inf))) + if _side == 2 then + _troops = ctld.insertIntoTroopsArray("Soldier M4",_countOrTemplate.inf,_troops) + else + _troops = ctld.insertIntoTroopsArray("Soldier AK",_countOrTemplate.inf,_troops) + end + _weight = _weight + getSoldiersWeight(_countOrTemplate.inf, ctld.RIFLE_WEIGHT) + ctld.logTrace(string.format("_weight=%s", ctld.p(_weight))) + end + + if _countOrTemplate.mg then + ctld.logTrace(string.format("_countOrTemplate.mg=%s", ctld.p(_countOrTemplate.mg))) + if _side == 2 then + _troops = ctld.insertIntoTroopsArray("Soldier M249",_countOrTemplate.mg,_troops) + else + _troops = ctld.insertIntoTroopsArray("Paratrooper AKS-74",_countOrTemplate.mg,_troops) + end + _weight = _weight + getSoldiersWeight(_countOrTemplate.mg, ctld.MG_WEIGHT) + ctld.logTrace(string.format("_weight=%s", ctld.p(_weight))) + end + + if _countOrTemplate.at then + ctld.logTrace(string.format("_countOrTemplate.at=%s", ctld.p(_countOrTemplate.at))) + _troops = ctld.insertIntoTroopsArray("Paratrooper RPG-16",_countOrTemplate.at,_troops) + _weight = _weight + getSoldiersWeight(_countOrTemplate.at, ctld.RPG_WEIGHT) + ctld.logTrace(string.format("_weight=%s", ctld.p(_weight))) + end + + if _countOrTemplate.mortar then + ctld.logTrace(string.format("_countOrTemplate.mortar=%s", ctld.p(_countOrTemplate.mortar))) + _troops = ctld.insertIntoTroopsArray("2B11 mortar",_countOrTemplate.mortar,_troops) + _weight = _weight + getSoldiersWeight(_countOrTemplate.mortar, ctld.MORTAR_WEIGHT) + ctld.logTrace(string.format("_weight=%s", ctld.p(_weight))) + end + + if _countOrTemplate.jtac then + ctld.logTrace(string.format("_countOrTemplate.jtac=%s", ctld.p(_countOrTemplate.jtac))) + if _side == 2 then + _troops = ctld.insertIntoTroopsArray("Soldier M4",_countOrTemplate.jtac,_troops, "JTAC") + else + _troops = ctld.insertIntoTroopsArray("Soldier AK",_countOrTemplate.jtac,_troops, "JTAC") + end + _hasJTAC = true + _weight = _weight + getSoldiersWeight(_countOrTemplate.jtac, ctld.JTAC_WEIGHT + ctld.RIFLE_WEIGHT) + ctld.logTrace(string.format("_weight=%s", ctld.p(_weight))) + end + + else + for _i = 1, _countOrTemplate do + + local _unitType = "Soldier AK" + + if _side == 2 then + if _i <=2 then + _unitType = "Soldier M249" + _weight = _weight + getSoldiersWeight(1, ctld.MG_WEIGHT) + ctld.logTrace(string.format("_unitType=%s, _weight=%s", ctld.p(_unitType), ctld.p(_weight))) + elseif ctld.spawnRPGWithCoalition and _i > 2 and _i <= 4 then + _unitType = "Paratrooper RPG-16" + _weight = _weight + getSoldiersWeight(1, ctld.RPG_WEIGHT) + ctld.logTrace(string.format("_unitType=%s, _weight=%s", ctld.p(_unitType), ctld.p(_weight))) + elseif ctld.spawnStinger and _i > 4 and _i <= 5 then + _unitType = "Soldier stinger" + _weight = _weight + getSoldiersWeight(1, ctld.MANPAD_WEIGHT) + ctld.logTrace(string.format("_unitType=%s, _weight=%s", ctld.p(_unitType), ctld.p(_weight))) + else + _unitType = "Soldier M4" + _weight = _weight + getSoldiersWeight(1, ctld.RIFLE_WEIGHT) + ctld.logTrace(string.format("_unitType=%s, _weight=%s", ctld.p(_unitType), ctld.p(_weight))) + end + else + if _i <=2 then + _unitType = "Paratrooper AKS-74" + _weight = _weight + getSoldiersWeight(1, ctld.MG_WEIGHT) + ctld.logTrace(string.format("_unitType=%s, _weight=%s", ctld.p(_unitType), ctld.p(_weight))) + elseif ctld.spawnRPGWithCoalition and _i > 2 and _i <= 4 then + _unitType = "Paratrooper RPG-16" + _weight = _weight + getSoldiersWeight(1, ctld.RPG_WEIGHT) + ctld.logTrace(string.format("_unitType=%s, _weight=%s", ctld.p(_unitType), ctld.p(_weight))) + elseif ctld.spawnStinger and _i > 4 and _i <= 5 then + _unitType = "SA-18 Igla manpad" + _weight = _weight + getSoldiersWeight(1, ctld.MANPAD_WEIGHT) + ctld.logTrace(string.format("_unitType=%s, _weight=%s", ctld.p(_unitType), ctld.p(_weight))) + else + _unitType = "Infantry AK" + _weight = _weight + getSoldiersWeight(1, ctld.RIFLE_WEIGHT) + ctld.logTrace(string.format("_unitType=%s, _weight=%s", ctld.p(_unitType), ctld.p(_weight))) + end + end + + local _unitId = ctld.getNextUnitId() + + _troops[_i] = { type = _unitType, unitId = _unitId, name = string.format("Dropped %s #%i", _unitType, _unitId) } + end + end + + local _groupId = ctld.getNextGroupId() + local _groupName = "Dropped Group" + if _hasJTAC then + _groupName = "Dropped JTAC Group" + end + local _details = { units = _troops, groupId = _groupId, groupName = string.format("%s %i", _groupName, _groupId), side = _side, country = _country, weight = _weight, jtac = _hasJTAC } + ctld.logTrace(string.format("total weight=%s", ctld.p(_weight))) + + return _details +end + +--Special F10 function for players for troops +function ctld.unloadExtractTroops(_args) + + local _heli = ctld.getTransportUnit(_args[1]) + + if _heli == nil then + return false + end + + + local _extract = nil + if not ctld.inAir(_heli) then + if _heli:getCoalition() == 1 then + _extract = ctld.findNearestGroup(_heli, ctld.droppedTroopsRED) + else + _extract = ctld.findNearestGroup(_heli, ctld.droppedTroopsBLUE) + end + + end + + if _extract ~= nil and not ctld.troopsOnboard(_heli, true) then + -- search for nearest troops to pickup + return ctld.extractTroops({_heli:getName(), true}) + else + return ctld.unloadTroops({_heli:getName(),true,true}) + end + + +end + +-- load troops onto vehicle +function ctld.loadTroops(_heli, _troops, _numberOrTemplate) + + -- load troops + vehicles if c130 or herc + -- "M1045 HMMWV TOW" + -- "M1043 HMMWV Armament" + local _onboard = ctld.inTransitTroops[_heli:getName()] + + --number doesnt apply to vehicles + if _numberOrTemplate == nil or (type(_numberOrTemplate) ~= "table" and type(_numberOrTemplate) ~= "number") then + _numberOrTemplate = ctld.numberOfTroops + end + + if _onboard == nil then + _onboard = { troops = {}, vehicles = {} } + end + + local _list + if _heli:getCoalition() == 1 then + _list = ctld.vehiclesForTransportRED + else + _list = ctld.vehiclesForTransportBLUE + end + + ctld.logTrace(string.format("_troops=%s", ctld.p(_troops))) + if _troops then + _onboard.troops = ctld.generateTroopTypes(_heli:getCoalition(), _numberOrTemplate, _heli:getCountry()) + ctld.logTrace(string.format("_onboard.troops=%s", ctld.p(_onboard.troops))) + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " loaded troops into " .. _heli:getTypeName(), 10) + + ctld.processCallback({unit = _heli, onboard = _onboard.troops, action = "load_troops"}) + else + + _onboard.vehicles = ctld.generateVehiclesForTransport(_heli:getCoalition(), _heli:getCountry()) + + local _count = #_list + + ctld.processCallback({unit = _heli, onboard = _onboard.vehicles, action = "load_vehicles"}) + + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " loaded " .. _count .. " vehicles into " .. _heli:getTypeName(), 10) + end + + ctld.inTransitTroops[_heli:getName()] = _onboard + ctld.logTrace(string.format("ctld.inTransitTroops=%s", ctld.p(ctld.inTransitTroops[_heli:getName()]))) + ctld.adaptWeightToCargo(_heli:getName()) +end + +function ctld.generateVehiclesForTransport(_side, _country) + + local _vehicles = {} + local _list + if _side == 1 then + _list = ctld.vehiclesForTransportRED + else + _list = ctld.vehiclesForTransportBLUE + end + + + for _i, _type in ipairs(_list) do + + local _unitId = ctld.getNextUnitId() + local _weight = ctld.vehiclesWeight[_type] or 2500 + _vehicles[_i] = { type = _type, unitId = _unitId, name = string.format("Dropped %s #%i", _type, _unitId), weight = _weight } + end + + + local _groupId = ctld.getNextGroupId() + local _details = { units = _vehicles, groupId = _groupId, groupName = string.format("Dropped Group %i", _groupId), side = _side, country = _country } + + return _details +end + +function ctld.loadUnloadFOBCrate(_args) + + local _heli = ctld.getTransportUnit(_args[1]) + local _troops = _args[2] + + if _heli == nil then + return + end + + if ctld.inAir(_heli) == true then + return + end + + + local _side = _heli:getCoalition() + + local _inZone = ctld.inLogisticsZone(_heli) + local _crateOnboard = ctld.inTransitFOBCrates[_heli:getName()] ~= nil + + if _inZone == false and _crateOnboard == true then + + ctld.inTransitFOBCrates[_heli:getName()] = nil + + local _position = _heli:getPosition() + + --try to spawn at 6 oclock to us + local _angle = math.atan2(_position.x.z, _position.x.x) + local _xOffset = math.cos(_angle) * -60 + local _yOffset = math.sin(_angle) * -60 + + local _point = _heli:getPoint() + + local _side = _heli:getCoalition() + + local _unitId = ctld.getNextUnitId() + + local _name = string.format("FOB Crate #%i", _unitId) + + local _spawnedCrate = ctld.spawnFOBCrateStatic(_heli:getCountry(), ctld.getNextUnitId(), { x = _point.x + _xOffset, z = _point.z + _yOffset }, _name) + + if _side == 1 then + ctld.droppedFOBCratesRED[_name] = _name + else + ctld.droppedFOBCratesBLUE[_name] = _name + end + + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " delivered a FOB Crate", 10) + + ctld.displayMessageToGroup(_heli, "Delivered FOB Crate 60m at 6'oclock to you", 10) + + elseif _inZone == true and _crateOnboard == true then + + ctld.displayMessageToGroup(_heli, "FOB Crate dropped back to base", 10) + + ctld.inTransitFOBCrates[_heli:getName()] = nil + + elseif _inZone == true and _crateOnboard == false then + ctld.displayMessageToGroup(_heli, "FOB Crate Loaded", 10) + + ctld.inTransitFOBCrates[_heli:getName()] = true + + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " loaded a FOB Crate ready for delivery!", 10) + + else + + -- nearest Crate + local _crates = ctld.getCratesAndDistance(_heli) + local _nearestCrate = ctld.getClosestCrate(_heli, _crates, "FOB") + + if _nearestCrate ~= nil and _nearestCrate.dist < 150 then + + ctld.displayMessageToGroup(_heli, "FOB Crate Loaded", 10) + ctld.inTransitFOBCrates[_heli:getName()] = true + + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " loaded a FOB Crate ready for delivery!", 10) + + if _side == 1 then + ctld.droppedFOBCratesRED[_nearestCrate.crateUnit:getName()] = nil + else + ctld.droppedFOBCratesBLUE[_nearestCrate.crateUnit:getName()] = nil + end + + --remove + _nearestCrate.crateUnit:destroy() + + else + ctld.displayMessageToGroup(_heli, "There are no friendly logistic units nearby to load a FOB crate from!", 10) + end + end +end + +function ctld.loadTroopsFromZone(_args) + + local _heli = ctld.getTransportUnit(_args[1]) + local _troops = _args[2] + local _groupTemplate = _args[3] or "" + local _allowExtract = _args[4] + + if _heli == nil then + return false + end + + local _zone = ctld.inPickupZone(_heli) + + if ctld.troopsOnboard(_heli, _troops) then + + if _troops then + ctld.displayMessageToGroup(_heli, "You already have troops onboard.", 10) + else + ctld.displayMessageToGroup(_heli, "You already have vehicles onboard.", 10) + end + + return false + end + + local _extract + + if _allowExtract then + -- first check for extractable troops regardless of if we're in a zone or not + if _troops then + if _heli:getCoalition() == 1 then + _extract = ctld.findNearestGroup(_heli, ctld.droppedTroopsRED) + else + _extract = ctld.findNearestGroup(_heli, ctld.droppedTroopsBLUE) + end + else + + if _heli:getCoalition() == 1 then + _extract = ctld.findNearestGroup(_heli, ctld.droppedVehiclesRED) + else + _extract = ctld.findNearestGroup(_heli, ctld.droppedVehiclesBLUE) + end + end + end + + if _extract ~= nil then + -- search for nearest troops to pickup + return ctld.extractTroops({_heli:getName(), _troops}) + elseif _zone.inZone == true then + + if _zone.limit - 1 >= 0 then + -- decrease zone counter by 1 + ctld.updateZoneCounter(_zone.index, -1) + + ctld.loadTroops(_heli, _troops,_groupTemplate) + + return true + else + ctld.displayMessageToGroup(_heli, "This area has no more reinforcements available!", 20) + + return false + end + + else + if _allowExtract then + ctld.displayMessageToGroup(_heli, "You are not in a pickup zone and no one is nearby to extract", 10) + else + ctld.displayMessageToGroup(_heli, "You are not in a pickup zone", 10) + end + + return false + end +end + + + +function ctld.unloadTroops(_args) + + local _heli = ctld.getTransportUnit(_args[1]) + local _troops = _args[2] + + if _heli == nil then + return false + end + + local _zone = ctld.inPickupZone(_heli) + if not ctld.troopsOnboard(_heli, _troops) then + + ctld.displayMessageToGroup(_heli, "No one to unload", 10) + + return false + else + + -- troops must be onboard to get here + if _zone.inZone == true then + + if _troops then + ctld.displayMessageToGroup(_heli, "Dropped troops back to base", 20) + + ctld.processCallback({unit = _heli, unloaded = ctld.inTransitTroops[_heli:getName()].troops, action = "unload_troops_zone"}) + + ctld.inTransitTroops[_heli:getName()].troops = nil + + else + ctld.displayMessageToGroup(_heli, "Dropped vehicles back to base", 20) + + ctld.processCallback({unit = _heli, unloaded = ctld.inTransitTroops[_heli:getName()].vehicles, action = "unload_vehicles_zone"}) + + ctld.inTransitTroops[_heli:getName()].vehicles = nil + end + + ctld.adaptWeightToCargo(_heli:getName()) + + -- increase zone counter by 1 + ctld.updateZoneCounter(_zone.index, 1) + + return true + + elseif ctld.troopsOnboard(_heli, _troops) then + + return ctld.deployTroops(_heli, _troops) + end + end + +end + +function ctld.extractTroops(_args) + + local _heli = ctld.getTransportUnit(_args[1]) + local _troops = _args[2] + + if _heli == nil then + return false + end + + if ctld.inAir(_heli) then + return false + end + + if ctld.troopsOnboard(_heli, _troops) then + if _troops then + ctld.displayMessageToGroup(_heli, "You already have troops onboard.", 10) + else + ctld.displayMessageToGroup(_heli, "You already have vehicles onboard.", 10) + end + + return false + end + + local _onboard = ctld.inTransitTroops[_heli:getName()] + + if _onboard == nil then + _onboard = { troops = nil, vehicles = nil } + end + + local _extracted = false + + if _troops then + + local _extractTroops + + if _heli:getCoalition() == 1 then + _extractTroops = ctld.findNearestGroup(_heli, ctld.droppedTroopsRED) + else + _extractTroops = ctld.findNearestGroup(_heli, ctld.droppedTroopsBLUE) + end + + + if _extractTroops ~= nil then + + local _limit = ctld.getTransportLimit(_heli:getTypeName()) + + local _size = #_extractTroops.group:getUnits() + + if _limit < #_extractTroops.group:getUnits() then + + ctld.displayMessageToGroup(_heli, "Sorry - The group of ".._size.." is too large to fit. \n\nLimit is ".._limit.." for ".._heli:getTypeName(), 20) + + return + end + + _onboard.troops = _extractTroops.details + _onboard.troops.weight = #_extractTroops.group:getUnits() * 130 -- default to 130kg per soldier + + if _extractTroops.group:getName():lower():find("jtac") then + _onboard.troops.jtac = true + end + + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " extracted troops in " .. _heli:getTypeName() .. " from combat", 10) + + if _heli:getCoalition() == 1 then + ctld.droppedTroopsRED[_extractTroops.group:getName()] = nil + else + ctld.droppedTroopsBLUE[_extractTroops.group:getName()] = nil + end + + ctld.processCallback({unit = _heli, extracted = _extractTroops, action = "extract_troops"}) + + --remove + _extractTroops.group:destroy() + + _extracted = true + else + _onboard.troops = nil + ctld.displayMessageToGroup(_heli, "No extractable troops nearby!", 20) + end + + else + + local _extractVehicles + + + if _heli:getCoalition() == 1 then + + _extractVehicles = ctld.findNearestGroup(_heli, ctld.droppedVehiclesRED) + else + + _extractVehicles = ctld.findNearestGroup(_heli, ctld.droppedVehiclesBLUE) + end + + if _extractVehicles ~= nil then + _onboard.vehicles = _extractVehicles.details + + if _heli:getCoalition() == 1 then + + ctld.droppedVehiclesRED[_extractVehicles.group:getName()] = nil + else + + ctld.droppedVehiclesBLUE[_extractVehicles.group:getName()] = nil + end + + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " extracted vehicles in " .. _heli:getTypeName() .. " from combat", 10) + + ctld.processCallback({unit = _heli, extracted = _extractVehicles, action = "extract_vehicles"}) + --remove + _extractVehicles.group:destroy() + _extracted = true + + else + _onboard.vehicles = nil + ctld.displayMessageToGroup(_heli, "No extractable vehicles nearby!", 20) + end + end + + ctld.inTransitTroops[_heli:getName()] = _onboard + ctld.adaptWeightToCargo(_heli:getName()) + + return _extracted +end + + +function ctld.checkTroopStatus(_args) + local _unitName = _args[1] + --list onboard troops, if c130 + local _heli = ctld.getTransportUnit(_unitName) + + if _heli == nil then + return + end + + local _, _message = ctld.getWeightOfCargo(_unitName) + ctld.logTrace(string.format("_message=%s", ctld.p(_message))) + if _message and _message ~= "" then + ctld.displayMessageToGroup(_heli, _message, 10) + end +end + +-- Removes troops from transport when it dies +function ctld.checkTransportStatus() + + timer.scheduleFunction(ctld.checkTransportStatus, nil, timer.getTime() + 3) + + for _, _name in ipairs(ctld.transportPilotNames) do + + local _transUnit = ctld.getTransportUnit(_name) + + if _transUnit == nil then + --env.info("CTLD Transport Unit Dead event") + ctld.inTransitTroops[_name] = nil + ctld.inTransitFOBCrates[_name] = nil + ctld.inTransitSlingLoadCrates[_name] = nil + end + end +end + +function ctld.adaptWeightToCargo(unitName) + local _weight = ctld.getWeightOfCargo(unitName) + trigger.action.setUnitInternalCargo(unitName, _weight) +end + +function ctld.getWeightOfCargo(unitName) + ctld.logDebug(string.format("ctld.getWeightOfCargo(%s)", ctld.p(unitName))) + + local FOB_CRATE_WEIGHT = 800 + local _weight = 0 + local _description = "" + + -- add troops weight + if ctld.inTransitTroops[unitName] then + ctld.logTrace("ctld.inTransitTroops = true") + local _inTransit = ctld.inTransitTroops[unitName] + if _inTransit then + ctld.logTrace(string.format("_inTransit=%s", ctld.p(_inTransit))) + local _troops = _inTransit.troops + if _troops and _troops.units then + ctld.logTrace(string.format("_troops.weight=%s", ctld.p(_troops.weight))) + _description = _description .. string.format("%s troops onboard (%s kg)\n", #_troops.units, _troops.weight) + _weight = _weight + _troops.weight + end + local _vehicles = _inTransit.vehicles + if _vehicles and _vehicles.units then + for _, _unit in pairs(_vehicles.units) do + _weight = _weight + _unit.weight + end + ctld.logTrace(string.format("_weight=%s", ctld.p(_weight))) + _description = _description .. string.format("%s vehicles onboard (%s kg)\n", #_vehicles.units, _weight) + end + end + end + ctld.logTrace(string.format("with troops and vehicles : weight = %s", tostring(_weight))) + + -- add FOB crates weight + if ctld.inTransitFOBCrates[unitName] then + ctld.logTrace("ctld.inTransitFOBCrates = true") + _weight = _weight + FOB_CRATE_WEIGHT + _description = _description .. string.format("1 FOB Crate oboard (%s kg)\n", FOB_CRATE_WEIGHT) + end + ctld.logTrace(string.format("with FOB crates : weight = %s", tostring(_weight))) + + -- add simulated slingload crates weight + local _crate = ctld.inTransitSlingLoadCrates[unitName] + if _crate then + ctld.logTrace(string.format("_crate=%s", ctld.p(_crate))) + if _crate.simulatedSlingload then + ctld.logTrace(string.format("_crate.weight=%s", ctld.p(_crate.weight))) + _weight = _weight + _crate.weight + _description = _description .. string.format("1 %s crate onboard (%s kg)\n", _crate.desc, _crate.weight) + end + end + ctld.logTrace(string.format("with simulated slingload crates : weight = %s", tostring(_weight))) + if _description ~= "" then + _description = _description .. string.format("Total weight of cargo : %s kg\n", _weight) + else + _description = "No cargo." + end + ctld.logTrace(string.format("_description = %s", tostring(_description))) + + return _weight, _description +end + +function ctld.checkHoverStatus() + --ctld.logDebug(string.format("ctld.checkHoverStatus()")) + timer.scheduleFunction(ctld.checkHoverStatus, nil, timer.getTime() + 1.0) + + local _status, _result = pcall(function() + + for _, _name in ipairs(ctld.transportPilotNames) do + + local _reset = true + local _transUnit = ctld.getTransportUnit(_name) + + --only check transports that are hovering and not planes + if _transUnit ~= nil and ctld.inTransitSlingLoadCrates[_name] == nil and ctld.inAir(_transUnit) and ctld.unitCanCarryVehicles(_transUnit) == false then + + --ctld.logTrace(string.format("%s - capable of slingloading", ctld.p(_name))) + + local _crates = ctld.getCratesAndDistance(_transUnit) + --ctld.logTrace(string.format("_crates = %s", ctld.p(_crates))) + + for _, _crate in pairs(_crates) do + --ctld.logTrace(string.format("_crate = %s", ctld.p(_crate))) + if _crate.dist < ctld.maxDistanceFromCrate and _crate.details.unit ~= "FOB" then + + --check height! + local _height = _transUnit:getPoint().y - _crate.crateUnit:getPoint().y + --env.info("HEIGHT " .. _name .. " " .. _height .. " " .. _transUnit:getPoint().y .. " " .. _crate.crateUnit:getPoint().y) + -- ctld.heightDiff(_transUnit) + --env.info("HEIGHT ABOVE GROUD ".._name.." ".._height.." ".._transUnit:getPoint().y.." ".._crate.crateUnit:getPoint().y) + --ctld.logTrace(string.format("_height = %s", ctld.p(_height))) + + if _height > ctld.minimumHoverHeight and _height <= ctld.maximumHoverHeight then + + local _time = ctld.hoverStatus[_transUnit:getName()] + --ctld.logTrace(string.format("_time = %s", ctld.p(_time))) + + if _time == nil then + ctld.hoverStatus[_transUnit:getName()] = ctld.hoverTime + _time = ctld.hoverTime + else + _time = ctld.hoverStatus[_transUnit:getName()] - 1 + ctld.hoverStatus[_transUnit:getName()] = _time + end + + if _time > 0 then + ctld.displayMessageToGroup(_transUnit, "Hovering above " .. _crate.details.desc .. " crate. \n\nHold hover for " .. _time .. " seconds! \n\nIf the countdown stops you're too far away!", 10,true) + else + ctld.hoverStatus[_transUnit:getName()] = nil + ctld.displayMessageToGroup(_transUnit, "Loaded " .. _crate.details.desc .. " crate!", 10,true) + + --crates been moved once! + ctld.crateMove[_crate.crateUnit:getName()] = nil + + if _transUnit:getCoalition() == 1 then + ctld.spawnedCratesRED[_crate.crateUnit:getName()] = nil + else + ctld.spawnedCratesBLUE[_crate.crateUnit:getName()] = nil + end + + _crate.crateUnit:destroy() + + local _copiedCrate = mist.utils.deepCopy(_crate.details) + _copiedCrate.simulatedSlingload = true + --ctld.logTrace(string.format("_copiedCrate = %s", ctld.p(_copiedCrate))) + ctld.inTransitSlingLoadCrates[_name] = _copiedCrate + ctld.adaptWeightToCargo(_name) + end + + _reset = false + + break + elseif _height <= ctld.minimumHoverHeight then + ctld.displayMessageToGroup(_transUnit, "Too low to hook " .. _crate.details.desc .. " crate.\n\nHold hover for " .. ctld.hoverTime .. " seconds", 5,true) + break + else + ctld.displayMessageToGroup(_transUnit, "Too high to hook " .. _crate.details.desc .. " crate.\n\nHold hover for " .. ctld.hoverTime .. " seconds", 5, true) + break + end + end + end + end + + if _reset then + ctld.hoverStatus[_name] = nil + end + end + end) + + if (not _status) then + env.error(string.format("CTLD ERROR: %s", _result)) + end +end + +function ctld.loadNearbyCrate(_name) + local _transUnit = ctld.getTransportUnit(_name) + + if _transUnit ~= nil then + + if ctld.inAir(_transUnit) then + ctld.displayMessageToGroup(_transUnit, "You must land before you can load a crate!", 10,true) + return + end + + if ctld.inTransitSlingLoadCrates[_name] == nil then + local _crates = ctld.getCratesAndDistance(_transUnit) + + for _, _crate in pairs(_crates) do + + if _crate.dist < 50.0 then + ctld.displayMessageToGroup(_transUnit, "Loaded " .. _crate.details.desc .. " crate!", 10,true) + + if _transUnit:getCoalition() == 1 then + ctld.spawnedCratesRED[_crate.crateUnit:getName()] = nil + else + ctld.spawnedCratesBLUE[_crate.crateUnit:getName()] = nil + end + + ctld.crateMove[_crate.crateUnit:getName()] = nil + + _crate.crateUnit:destroy() + + local _copiedCrate = mist.utils.deepCopy(_crate.details) + _copiedCrate.simulatedSlingload = true + ctld.inTransitSlingLoadCrates[_name] = _copiedCrate + ctld.adaptWeightToCargo(_name) + return + end + end + + ctld.displayMessageToGroup(_transUnit, "No Crates within 50m to load!", 10,true) + + else + -- crate onboard + ctld.displayMessageToGroup(_transUnit, "You already have a "..ctld.inTransitSlingLoadCrates[_name].desc.." crate onboard!", 10,true) + end + end + + +end + +--recreates beacons to make sure they work! +function ctld.refreshRadioBeacons() + + timer.scheduleFunction(ctld.refreshRadioBeacons, nil, timer.getTime() + 30) + + + for _index, _beaconDetails in ipairs(ctld.deployedRadioBeacons) do + + --trigger.action.outTextForCoalition(_beaconDetails.coalition,_beaconDetails.text,10) + if ctld.updateRadioBeacon(_beaconDetails) == false then + + --search used frequencies + remove, add back to unused + + for _i, _freq in ipairs(ctld.usedUHFFrequencies) do + if _freq == _beaconDetails.uhf then + + table.insert(ctld.freeUHFFrequencies, _freq) + table.remove(ctld.usedUHFFrequencies, _i) + end + end + + for _i, _freq in ipairs(ctld.usedVHFFrequencies) do + if _freq == _beaconDetails.vhf then + + table.insert(ctld.freeVHFFrequencies, _freq) + table.remove(ctld.usedVHFFrequencies, _i) + end + end + + for _i, _freq in ipairs(ctld.usedFMFrequencies) do + if _freq == _beaconDetails.fm then + + table.insert(ctld.freeFMFrequencies, _freq) + table.remove(ctld.usedFMFrequencies, _i) + end + end + + --clean up beacon table + table.remove(ctld.deployedRadioBeacons, _index) + end + end +end + +function ctld.getClockDirection(_heli, _crate) + + -- Source: Helicopter Script - Thanks! + + local _position = _crate:getPosition().p -- get position of crate + local _playerPosition = _heli:getPosition().p -- get position of helicopter + local _relativePosition = mist.vec.sub(_position, _playerPosition) + + local _playerHeading = mist.getHeading(_heli) -- the rest of the code determines the 'o'clock' bearing of the missile relative to the helicopter + + local _headingVector = { x = math.cos(_playerHeading), y = 0, z = math.sin(_playerHeading) } + + local _headingVectorPerpendicular = { x = math.cos(_playerHeading + math.pi / 2), y = 0, z = math.sin(_playerHeading + math.pi / 2) } + + local _forwardDistance = mist.vec.dp(_relativePosition, _headingVector) + + local _rightDistance = mist.vec.dp(_relativePosition, _headingVectorPerpendicular) + + local _angle = math.atan2(_rightDistance, _forwardDistance) * 180 / math.pi + + if _angle < 0 then + _angle = 360 + _angle + end + _angle = math.floor(_angle * 12 / 360 + 0.5) + if _angle == 0 then + _angle = 12 + end + + return _angle +end + + +function ctld.getCompassBearing(_ref, _unitPos) + + _ref = mist.utils.makeVec3(_ref, 0) -- turn it into Vec3 if it is not already. + _unitPos = mist.utils.makeVec3(_unitPos, 0) -- turn it into Vec3 if it is not already. + + local _vec = { x = _unitPos.x - _ref.x, y = _unitPos.y - _ref.y, z = _unitPos.z - _ref.z } + + local _dir = mist.utils.getDir(_vec, _ref) + + local _bearing = mist.utils.round(mist.utils.toDegree(_dir), 0) + + return _bearing +end + +function ctld.listNearbyCrates(_args) + + local _message = "" + + local _heli = ctld.getTransportUnit(_args[1]) + + if _heli == nil then + + return -- no heli! + end + + local _crates = ctld.getCratesAndDistance(_heli) + + --sort + local _sort = function( a,b ) return a.dist < b.dist end + table.sort(_crates,_sort) + + for _, _crate in pairs(_crates) do + + if _crate.dist < 1000 and _crate.details.unit ~= "FOB" then + _message = string.format("%s\n%s crate - kg %i - %i m - %d o'clock", _message, _crate.details.desc, _crate.details.weight, _crate.dist, ctld.getClockDirection(_heli, _crate.crateUnit)) + end + end + + + local _fobMsg = "" + for _, _fobCrate in pairs(_crates) do + + if _fobCrate.dist < 1000 and _fobCrate.details.unit == "FOB" then + _fobMsg = _fobMsg .. string.format("FOB Crate - %d m - %d o'clock\n", _fobCrate.dist, ctld.getClockDirection(_heli, _fobCrate.crateUnit)) + end + end + + if _message ~= "" or _fobMsg ~= "" then + + local _txt = "" + + if _message ~= "" then + _txt = "Nearby Crates:\n" .. _message + end + + if _fobMsg ~= "" then + + if _message ~= "" then + _txt = _txt .. "\n\n" + end + + _txt = _txt .. "Nearby FOB Crates (Not Slingloadable):\n" .. _fobMsg + end + + ctld.displayMessageToGroup(_heli, _txt, 20) + + else + --no crates nearby + + local _txt = "No Nearby Crates" + + ctld.displayMessageToGroup(_heli, _txt, 20) + end +end + + +function ctld.listFOBS(_args) + + local _msg = "FOB Positions:" + + local _heli = ctld.getTransportUnit(_args[1]) + + if _heli == nil then + + return -- no heli! + end + + -- get fob positions + + local _fobs = ctld.getSpawnedFobs(_heli) + + -- now check spawned fobs + for _, _fob in ipairs(_fobs) do + _msg = string.format("%s\nFOB @ %s", _msg, ctld.getFOBPositionString(_fob)) + end + + if _msg == "FOB Positions:" then + ctld.displayMessageToGroup(_heli, "Sorry, there are no active FOBs!", 20) + else + ctld.displayMessageToGroup(_heli, _msg, 20) + end +end + +function ctld.getFOBPositionString(_fob) + + local _lat, _lon = coord.LOtoLL(_fob:getPosition().p) + + local _latLngStr = mist.tostringLL(_lat, _lon, 3, ctld.location_DMS) + + -- local _mgrsString = mist.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(_fob:getPosition().p)), 5) + + local _message = _latLngStr + + local _beaconInfo = ctld.fobBeacons[_fob:getName()] + + if _beaconInfo ~= nil then + _message = string.format("%s - %.2f KHz ", _message, _beaconInfo.vhf / 1000) + _message = string.format("%s - %.2f MHz ", _message, _beaconInfo.uhf / 1000000) + _message = string.format("%s - %.2f MHz ", _message, _beaconInfo.fm / 1000000) + end + + return _message +end + + +function ctld.displayMessageToGroup(_unit, _text, _time,_clear) + + local _groupId = ctld.getGroupId(_unit) + if _groupId then + if _clear == true then + trigger.action.outTextForGroup(_groupId, _text, _time,_clear) + else + trigger.action.outTextForGroup(_groupId, _text, _time) + end + end +end + +function ctld.heightDiff(_unit) + + local _point = _unit:getPoint() + + -- env.info("heightunit " .. _point.y) + --env.info("heightland " .. land.getHeight({ x = _point.x, y = _point.z })) + + return _point.y - land.getHeight({ x = _point.x, y = _point.z }) +end + +--includes fob crates! +function ctld.getCratesAndDistance(_heli) + + local _crates = {} + + local _allCrates + if _heli:getCoalition() == 1 then + _allCrates = ctld.spawnedCratesRED + else + _allCrates = ctld.spawnedCratesBLUE + end + + for _crateName, _details in pairs(_allCrates) do + + --get crate + local _crate = ctld.getCrateObject(_crateName) + + --in air seems buggy with crates so if in air is true, get the height above ground and the speed magnitude + if _crate ~= nil and _crate:getLife() > 0 + and (ctld.inAir(_crate) == false) then + + local _dist = ctld.getDistance(_crate:getPoint(), _heli:getPoint()) + + local _crateDetails = { crateUnit = _crate, dist = _dist, details = _details } + + table.insert(_crates, _crateDetails) + end + end + + local _fobCrates + if _heli:getCoalition() == 1 then + _fobCrates = ctld.droppedFOBCratesRED + else + _fobCrates = ctld.droppedFOBCratesBLUE + end + + for _crateName, _details in pairs(_fobCrates) do + + --get crate + local _crate = ctld.getCrateObject(_crateName) + + if _crate ~= nil and _crate:getLife() > 0 then + + local _dist = ctld.getDistance(_crate:getPoint(), _heli:getPoint()) + + local _crateDetails = { crateUnit = _crate, dist = _dist, details = { unit = "FOB" }, } + + table.insert(_crates, _crateDetails) + end + end + + return _crates +end + + +function ctld.getClosestCrate(_heli, _crates, _type) + + local _closetCrate = nil + local _shortestDistance = -1 + local _distance = 0 + + for _, _crate in pairs(_crates) do + + if (_crate.details.unit == _type or _type == nil) then + _distance = _crate.dist + + if _distance ~= nil and (_shortestDistance == -1 or _distance < _shortestDistance) then + _shortestDistance = _distance + _closetCrate = _crate + end + end + end + + return _closetCrate +end + +function ctld.findNearestAASystem(_heli,_aaSystem) + + local _closestHawkGroup = nil + local _shortestDistance = -1 + local _distance = 0 + + for _groupName, _hawkDetails in pairs(ctld.completeAASystems) do + + local _hawkGroup = Group.getByName(_groupName) + + -- env.info(_groupName..": "..mist.utils.tableShow(_hawkDetails)) + if _hawkGroup ~= nil and _hawkGroup:getCoalition() == _heli:getCoalition() and _hawkDetails[1].system.name == _aaSystem.name then + + local _units = _hawkGroup:getUnits() + + for _, _leader in pairs(_units) do + + if _leader ~= nil and _leader:getLife() > 0 then + + _distance = ctld.getDistance(_leader:getPoint(), _heli:getPoint()) + + if _distance ~= nil and (_shortestDistance == -1 or _distance < _shortestDistance) then + _shortestDistance = _distance + _closestHawkGroup = _hawkGroup + end + + break + end + end + end + end + + if _closestHawkGroup ~= nil then + + return { group = _closestHawkGroup, dist = _shortestDistance } + end + return nil +end + +function ctld.getCrateObject(_name) + local _crate + + if ctld.staticBugWorkaround then + _crate = Unit.getByName(_name) + else + _crate = StaticObject.getByName(_name) + end + return _crate +end + + + +function ctld.unpackCrates(_arguments) + + local _status, _err = pcall(function(_args) + + -- trigger.action.outText("Unpack Crates".._args[1],10) + + local _heli = ctld.getTransportUnit(_args[1]) + + if _heli ~= nil and ctld.inAir(_heli) == false then + + local _crates = ctld.getCratesAndDistance(_heli) + local _crate = ctld.getClosestCrate(_heli, _crates) + + + if ctld.inLogisticsZone(_heli) == true or ctld.farEnoughFromLogisticZone(_heli) == false then + + ctld.displayMessageToGroup(_heli, "You can't unpack that here! Take it to where it's needed!", 20) + + return + end + + + + if _crate ~= nil and _crate.dist < 750 + and (_crate.details.unit == "FOB" or _crate.details.unit == "FOB-SMALL") then + + ctld.unpackFOBCrates(_crates, _heli) + + return + + elseif _crate ~= nil and _crate.dist < 200 then + + if ctld.forceCrateToBeMoved and ctld.crateMove[_crate.crateUnit:getName()] then + ctld.displayMessageToGroup(_heli,"Sorry you must move this crate before you unpack it!", 20) + return + end + + + local _aaTemplate = ctld.getAATemplate(_crate.details.unit) + + if _aaTemplate then + + if _crate.details.unit == _aaTemplate.repair then + ctld.repairAASystem(_heli, _crate,_aaTemplate) + else + ctld.unpackAASystem(_heli, _crate, _crates,_aaTemplate) + end + + return -- stop processing + -- is multi crate? + elseif _crate.details.cratesRequired ~= nil and _crate.details.cratesRequired > 1 then + -- multicrate + + ctld.unpackMultiCrate(_heli, _crate, _crates) + + return + + else + -- single crate + local _cratePoint = _crate.crateUnit:getPoint() + local _crateName = _crate.crateUnit:getName() + + -- ctld.spawnCrateStatic( _heli:getCoalition(),ctld.getNextUnitId(),{x=100,z=100},_crateName,100) + + --remove crate + -- if ctld.slingLoad == false then + _crate.crateUnit:destroy() + -- end + + local _spawnedGroups = ctld.spawnCrateGroup(_heli, { _cratePoint }, { _crate.details.unit }) + + if _heli:getCoalition() == 1 then + ctld.spawnedCratesRED[_crateName] = nil + else + ctld.spawnedCratesBLUE[_crateName] = nil + end + + ctld.processCallback({unit = _heli, crate = _crate , spawnedGroup = _spawnedGroups, action = "unpack"}) + + if _crate.details.unit == "1L13 EWR" then + ctld.addEWRTask(_spawnedGroups) + + -- env.info("Added EWR") + end + + + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " successfully deployed " .. _crate.details.desc .. " to the field", 10) + + if ctld.isJTACUnitType(_crate.details.unit) and ctld.JTAC_dropEnabled then + + local _code = table.remove(ctld.jtacGeneratedLaserCodes, 1) + --put to the end + table.insert(ctld.jtacGeneratedLaserCodes, _code) + + ctld.JTACAutoLase(_spawnedGroups:getName(), _code) --(_jtacGroupName, _laserCode, _smoke, _lock, _colour) + end + end + + else + + ctld.displayMessageToGroup(_heli, "No friendly crates close enough to unpack", 20) + end + end + end, _arguments) + + if (not _status) then + env.error(string.format("CTLD ERROR: %s", _err)) + end +end + + +-- builds a fob! +function ctld.unpackFOBCrates(_crates, _heli) + + if ctld.inLogisticsZone(_heli) == true then + + ctld.displayMessageToGroup(_heli, "You can't unpack that here! Take it to where it's needed!", 20) + + return + end + + -- unpack multi crate + local _nearbyMultiCrates = {} + + local _bigFobCrates = 0 + local _smallFobCrates = 0 + local _totalCrates = 0 + + for _, _nearbyCrate in pairs(_crates) do + + if _nearbyCrate.dist < 750 then + + if _nearbyCrate.details.unit == "FOB" then + _bigFobCrates = _bigFobCrates + 1 + table.insert(_nearbyMultiCrates, _nearbyCrate) + elseif _nearbyCrate.details.unit == "FOB-SMALL" then + _smallFobCrates = _smallFobCrates + 1 + table.insert(_nearbyMultiCrates, _nearbyCrate) + end + + --catch divide by 0 + if _smallFobCrates > 0 then + _totalCrates = _bigFobCrates + (_smallFobCrates/3.0) + else + _totalCrates = _bigFobCrates + end + + if _totalCrates >= ctld.cratesRequiredForFOB then + break + end + end + end + + --- check crate count + if _totalCrates >= ctld.cratesRequiredForFOB then + + -- destroy crates + + local _points = {} + + for _, _crate in pairs(_nearbyMultiCrates) do + + if _heli:getCoalition() == 1 then + ctld.droppedFOBCratesRED[_crate.crateUnit:getName()] = nil + ctld.spawnedCratesRED[_crate.crateUnit:getName()] = nil + else + ctld.droppedFOBCratesBLUE[_crate.crateUnit:getName()] = nil + ctld.spawnedCratesBLUE[_crate.crateUnit:getName()] = nil + end + + table.insert(_points, _crate.crateUnit:getPoint()) + + --destroy + _crate.crateUnit:destroy() + end + + local _centroid = ctld.getCentroid(_points) + + timer.scheduleFunction(function(_args) + + local _unitId = ctld.getNextUnitId() + local _name = "Deployed FOB #" .. _unitId + + local _fob = ctld.spawnFOB(_args[2], _unitId, _args[1], _name) + + --make it able to deploy crates + table.insert(ctld.logisticUnits, _fob:getName()) + + ctld.beaconCount = ctld.beaconCount + 1 + + local _radioBeaconName = "FOB Beacon #" .. ctld.beaconCount + + local _radioBeaconDetails = ctld.createRadioBeacon(_args[1], _args[3], _args[2], _radioBeaconName, nil, true) + + ctld.fobBeacons[_name] = { vhf = _radioBeaconDetails.vhf, uhf = _radioBeaconDetails.uhf, fm = _radioBeaconDetails.fm } + + if ctld.troopPickupAtFOB == true then + table.insert(ctld.builtFOBS, _fob:getName()) + + trigger.action.outTextForCoalition(_args[3], "Finished building FOB! Crates and Troops can now be picked up.", 10) + else + trigger.action.outTextForCoalition(_args[3], "Finished building FOB! Crates can now be picked up.", 10) + end + end, { _centroid, _heli:getCountry(), _heli:getCoalition() }, timer.getTime() + ctld.buildTimeFOB) + + local _txt = string.format("%s started building FOB using %d FOB crates, it will be finished in %d seconds.\nPosition marked with smoke.", ctld.getPlayerNameOrType(_heli), _totalCrates, ctld.buildTimeFOB) + + ctld.processCallback({unit = _heli, position = _centroid, action = "fob"}) + + trigger.action.smoke(_centroid, trigger.smokeColor.Green) + + trigger.action.outTextForCoalition(_heli:getCoalition(), _txt, 10) + else + local _txt = string.format("Cannot build FOB!\n\nIt requires %d Large FOB crates ( 3 small FOB crates equal 1 large FOB Crate) and there are the equivalent of %d large FOB crates nearby\n\nOr the crates are not within 750m of each other", ctld.cratesRequiredForFOB, _totalCrates) + ctld.displayMessageToGroup(_heli, _txt, 20) + end +end + +--unloads the sling crate when the helicopter is on the ground or between 4.5 - 10 meters +function ctld.dropSlingCrate(_args) + local _heli = ctld.getTransportUnit(_args[1]) + + if _heli == nil then + return -- no heli! + end + + local _currentCrate = ctld.inTransitSlingLoadCrates[_heli:getName()] + + if _currentCrate == nil then + if ctld.hoverPickup then + ctld.displayMessageToGroup(_heli, "You are not currently transporting any crates. \n\nTo Pickup a crate, hover for "..ctld.hoverTime.." seconds above the crate", 10) + else + ctld.displayMessageToGroup(_heli, "You are not currently transporting any crates. \n\nTo Pickup a crate - land and use F10 Crate Commands to load one.", 10) + end + else + + local _heli = ctld.getTransportUnit(_args[1]) + + local _point = _heli:getPoint() + + local _unitId = ctld.getNextUnitId() + + local _side = _heli:getCoalition() + + local _name = string.format("%s #%i", _currentCrate.desc, _unitId) + + + local _heightDiff = ctld.heightDiff(_heli) + + if ctld.inAir(_heli) == false or _heightDiff <= 7.5 then + ctld.displayMessageToGroup(_heli, _currentCrate.desc .. " crate has been safely unhooked and is at your 12 o'clock", 10) + _point = ctld.getPointAt12Oclock(_heli, 30) + -- elseif _heightDiff > 40.0 then + -- ctld.inTransitSlingLoadCrates[_heli:getName()] = nil + -- ctld.displayMessageToGroup(_heli, "You were too high! The crate has been destroyed", 10) + -- return + elseif _heightDiff > 7.5 and _heightDiff <= 40.0 then + ctld.displayMessageToGroup(_heli, _currentCrate.desc .. " crate has been safely dropped below you", 10) + else -- _heightDiff > 40.0 + ctld.inTransitSlingLoadCrates[_heli:getName()] = nil + ctld.displayMessageToGroup(_heli, "You were too high! The crate has been destroyed", 10) + return + end + + + --remove crate from cargo + ctld.inTransitSlingLoadCrates[_heli:getName()] = nil + ctld.adaptWeightToCargo(_heli:getName()) + local _spawnedCrate = ctld.spawnCrateStatic(_heli:getCountry(), _unitId, _point, _name, _currentCrate.weight,_side) + end +end + +--spawns a radio beacon made up of two units, +-- one for VHF and one for UHF +-- The units are set to to NOT engage +function ctld.createRadioBeacon(_point, _coalition, _country, _name, _batteryTime, _isFOB) + + local _uhfGroup = ctld.spawnRadioBeaconUnit(_point, _country, "UHF") + local _vhfGroup = ctld.spawnRadioBeaconUnit(_point, _country, "VHF") + local _fmGroup = ctld.spawnRadioBeaconUnit(_point, _country, "FM") + + local _freq = ctld.generateADFFrequencies() + + --create timeout + local _battery + + if _batteryTime == nil then + _battery = timer.getTime() + (ctld.deployedBeaconBattery * 60) + else + _battery = timer.getTime() + (_batteryTime * 60) + end + + local _lat, _lon = coord.LOtoLL(_point) + + local _latLngStr = mist.tostringLL(_lat, _lon, 3, ctld.location_DMS) + + --local _mgrsString = mist.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(_point)), 5) + + local _message = _name + + if _isFOB then + -- _message = "FOB " .. _message + _battery = -1 --never run out of power! + end + + _message = _message .. " - " .. _latLngStr + + -- env.info("GEN UHF: ".. _freq.uhf) + -- env.info("GEN VHF: ".. _freq.vhf) + + _message = string.format("%s - %.2f KHz", _message, _freq.vhf / 1000) + + _message = string.format("%s - %.2f MHz", _message, _freq.uhf / 1000000) + + _message = string.format("%s - %.2f MHz ", _message, _freq.fm / 1000000) + + + + local _beaconDetails = { + vhf = _freq.vhf, + vhfGroup = _vhfGroup:getName(), + uhf = _freq.uhf, + uhfGroup = _uhfGroup:getName(), + fm = _freq.fm, + fmGroup = _fmGroup:getName(), + text = _message, + battery = _battery, + coalition = _coalition, + } + ctld.updateRadioBeacon(_beaconDetails) + + table.insert(ctld.deployedRadioBeacons, _beaconDetails) + + return _beaconDetails +end + +function ctld.generateADFFrequencies() + + if #ctld.freeUHFFrequencies <= 3 then + ctld.freeUHFFrequencies = ctld.usedUHFFrequencies + ctld.usedUHFFrequencies = {} + end + + --remove frequency at RANDOM + local _uhf = table.remove(ctld.freeUHFFrequencies, math.random(#ctld.freeUHFFrequencies)) + table.insert(ctld.usedUHFFrequencies, _uhf) + + + if #ctld.freeVHFFrequencies <= 3 then + ctld.freeVHFFrequencies = ctld.usedVHFFrequencies + ctld.usedVHFFrequencies = {} + end + + local _vhf = table.remove(ctld.freeVHFFrequencies, math.random(#ctld.freeVHFFrequencies)) + table.insert(ctld.usedVHFFrequencies, _vhf) + + if #ctld.freeFMFrequencies <= 3 then + ctld.freeFMFrequencies = ctld.usedFMFrequencies + ctld.usedFMFrequencies = {} + end + + local _fm = table.remove(ctld.freeFMFrequencies, math.random(#ctld.freeFMFrequencies)) + table.insert(ctld.usedFMFrequencies, _fm) + + return { uhf = _uhf, vhf = _vhf, fm = _fm } + --- return {uhf=_uhf,vhf=_vhf} +end + + + +function ctld.spawnRadioBeaconUnit(_point, _country, _type) + + local _groupId = ctld.getNextGroupId() + + local _unitId = ctld.getNextUnitId() + + local _radioGroup = { + ["visible"] = false, + -- ["groupId"] = _groupId, + ["hidden"] = false, + ["units"] = { + [1] = { + ["y"] = _point.z, + ["type"] = "TACAN_beacon", + ["name"] = _type .. " Radio Beacon Unit #" .. _unitId, + -- ["unitId"] = _unitId, + ["heading"] = 0, + ["playerCanDrive"] = true, + ["skill"] = "Excellent", + ["x"] = _point.x, + } + }, + -- ["y"] = _positions[1].z, + -- ["x"] = _positions[1].x, + ["name"] = _type .. " Radio Beacon Group #" .. _groupId, + ["task"] = {}, + --added two fields below for MIST + ["category"] = Group.Category.GROUND, + ["country"] = _country + } + + -- return coalition.addGroup(_country, Group.Category.GROUND, _radioGroup) + return Group.getByName(mist.dynAdd(_radioGroup).name) +end + +function ctld.updateRadioBeacon(_beaconDetails) + + local _vhfGroup = Group.getByName(_beaconDetails.vhfGroup) + + local _uhfGroup = Group.getByName(_beaconDetails.uhfGroup) + + local _fmGroup = Group.getByName(_beaconDetails.fmGroup) + + local _radioLoop = {} + + if _vhfGroup ~= nil and _vhfGroup:getUnits() ~= nil and #_vhfGroup:getUnits() == 1 then + table.insert(_radioLoop, { group = _vhfGroup, freq = _beaconDetails.vhf, silent = false, mode = 0 }) + end + + if _uhfGroup ~= nil and _uhfGroup:getUnits() ~= nil and #_uhfGroup:getUnits() == 1 then + table.insert(_radioLoop, { group = _uhfGroup, freq = _beaconDetails.uhf, silent = true, mode = 0 }) + end + + if _fmGroup ~= nil and _fmGroup:getUnits() ~= nil and #_fmGroup:getUnits() == 1 then + table.insert(_radioLoop, { group = _fmGroup, freq = _beaconDetails.fm, silent = false, mode = 1 }) + end + + local _batLife = _beaconDetails.battery - timer.getTime() + + if (_batLife <= 0 and _beaconDetails.battery ~= -1) or #_radioLoop ~= 3 then + -- ran out of batteries + + if _vhfGroup ~= nil then + _vhfGroup:destroy() + end + if _uhfGroup ~= nil then + _uhfGroup:destroy() + end + if _fmGroup ~= nil then + _fmGroup:destroy() + end + + return false + end + + --fobs have unlimited battery life + -- if _battery ~= -1 then + -- _text = _text.." "..mist.utils.round(_batLife).." seconds of battery" + -- end + + for _, _radio in pairs(_radioLoop) do + + local _groupController = _radio.group:getController() + + local _sound = ctld.radioSound + if _radio.silent then + _sound = ctld.radioSoundFC3 + end + + _sound = "l10n/DEFAULT/".._sound + + _groupController:setOption(AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.WEAPON_HOLD) + + trigger.action.radioTransmission(_sound, _radio.group:getUnit(1):getPoint(), _radio.mode, false, _radio.freq, 1000) + --This function doesnt actually stop transmitting when then sound is false. My hope is it will stop if a new beacon is created on the same + -- frequency... OR they fix the bug where it wont stop. + -- end + + -- + end + + return true + + -- trigger.action.radioTransmission(ctld.radioSound, _point, 1, true, _frequency, 1000) +end + +function ctld.listRadioBeacons(_args) + + local _heli = ctld.getTransportUnit(_args[1]) + local _message = "" + + if _heli ~= nil then + + for _x, _details in pairs(ctld.deployedRadioBeacons) do + + if _details.coalition == _heli:getCoalition() then + _message = _message .. _details.text .. "\n" + end + end + + if _message ~= "" then + ctld.displayMessageToGroup(_heli, "Radio Beacons:\n" .. _message, 20) + else + ctld.displayMessageToGroup(_heli, "No Active Radio Beacons", 20) + end + end +end + +function ctld.dropRadioBeacon(_args) + + local _heli = ctld.getTransportUnit(_args[1]) + local _message = "" + + if _heli ~= nil and ctld.inAir(_heli) == false then + + --deploy 50 m infront + --try to spawn at 12 oclock to us + local _point = ctld.getPointAt12Oclock(_heli, 50) + + ctld.beaconCount = ctld.beaconCount + 1 + local _name = "Beacon #" .. ctld.beaconCount + + local _radioBeaconDetails = ctld.createRadioBeacon(_point, _heli:getCoalition(), _heli:getCountry(), _name, nil, false) + + -- mark with flare? + + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " deployed a Radio Beacon.\n\n" .. _radioBeaconDetails.text, 20) + + else + ctld.displayMessageToGroup(_heli, "You need to land before you can deploy a Radio Beacon!", 20) + end +end + +--remove closet radio beacon +function ctld.removeRadioBeacon(_args) + + local _heli = ctld.getTransportUnit(_args[1]) + local _message = "" + + if _heli ~= nil and ctld.inAir(_heli) == false then + + -- mark with flare? + + local _closetBeacon = nil + local _shortestDistance = -1 + local _distance = 0 + + for _x, _details in pairs(ctld.deployedRadioBeacons) do + + if _details.coalition == _heli:getCoalition() then + + local _group = Group.getByName(_details.vhfGroup) + + if _group ~= nil and #_group:getUnits() == 1 then + + _distance = ctld.getDistance(_heli:getPoint(), _group:getUnit(1):getPoint()) + if _distance ~= nil and (_shortestDistance == -1 or _distance < _shortestDistance) then + _shortestDistance = _distance + _closetBeacon = _details + end + end + end + end + + if _closetBeacon ~= nil and _shortestDistance then + local _vhfGroup = Group.getByName(_closetBeacon.vhfGroup) + + local _uhfGroup = Group.getByName(_closetBeacon.uhfGroup) + + local _fmGroup = Group.getByName(_closetBeacon.fmGroup) + + if _vhfGroup ~= nil then + _vhfGroup:destroy() + end + if _uhfGroup ~= nil then + _uhfGroup:destroy() + end + if _fmGroup ~= nil then + _fmGroup:destroy() + end + + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " removed a Radio Beacon.\n\n" .. _closetBeacon.text, 20) + else + ctld.displayMessageToGroup(_heli, "No Radio Beacons within 500m.", 20) + end + + else + ctld.displayMessageToGroup(_heli, "You need to land before remove a Radio Beacon", 20) + end +end + +-- gets the center of a bunch of points! +-- return proper DCS point with height +function ctld.getCentroid(_points) + local _tx, _ty = 0, 0 + for _index, _point in ipairs(_points) do + _tx = _tx + _point.x + _ty = _ty + _point.z + end + + local _npoints = #_points + + local _point = { x = _tx / _npoints, z = _ty / _npoints } + + _point.y = land.getHeight({ _point.x, _point.z }) + + return _point +end + +function ctld.getAATemplate(_unitName) + + for _,_system in pairs(ctld.AASystemTemplate) do + + if _system.repair == _unitName then + return _system + end + + for _,_part in pairs(_system.parts) do + + if _unitName == _part.name then + return _system + end + end + end + + return nil + +end + +function ctld.getLauncherUnitFromAATemplate(_aaTemplate) + for _,_part in pairs(_aaTemplate.parts) do + + if _part.launcher then + return _part.name + end + end + + return nil +end + +function ctld.rearmAASystem(_heli, _nearestCrate, _nearbyCrates, _aaSystemTemplate) + + -- are we adding to existing aa system? + -- check to see if the crate is a launcher + if ctld.getLauncherUnitFromAATemplate(_aaSystemTemplate) == _nearestCrate.details.unit then + + -- find nearest COMPLETE AA system + local _nearestSystem = ctld.findNearestAASystem(_heli, _aaSystemTemplate) + + if _nearestSystem ~= nil and _nearestSystem.dist < 300 then + + local _uniqueTypes = {} -- stores each unique part of system + local _types = {} + local _points = {} + + local _units = _nearestSystem.group:getUnits() + + if _units ~= nil and #_units > 0 then + + for x = 1, #_units do + if _units[x]:getLife() > 0 then + + --this allows us to count each type once + _uniqueTypes[_units[x]:getTypeName()] = _units[x]:getTypeName() + + table.insert(_points, _units[x]:getPoint()) + table.insert(_types, _units[x]:getTypeName()) + end + end + end + + -- do we have the correct number of unique pieces and do we have enough points for all the pieces + if ctld.countTableEntries(_uniqueTypes) == _aaSystemTemplate.count and #_points >= _aaSystemTemplate.count then + + -- rearm aa system + -- destroy old group + ctld.completeAASystems[_nearestSystem.group:getName()] = nil + + _nearestSystem.group:destroy() + + local _spawnedGroup = ctld.spawnCrateGroup(_heli, _points, _types) + + ctld.completeAASystems[_spawnedGroup:getName()] = ctld.getAASystemDetails(_spawnedGroup, _aaSystemTemplate) + + ctld.processCallback({unit = _heli, crate = _nearestCrate , spawnedGroup = _spawnedGroup, action = "rearm"}) + + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " successfully rearmed a full ".._aaSystemTemplate.name.." in the field", 10) + + if _heli:getCoalition() == 1 then + ctld.spawnedCratesRED[_nearestCrate.crateUnit:getName()] = nil + else + ctld.spawnedCratesBLUE[_nearestCrate.crateUnit:getName()] = nil + end + + -- remove crate + -- if ctld.slingLoad == false then + _nearestCrate.crateUnit:destroy() + -- end + + return true -- all done so quit + end + end + end + + return false +end + +function ctld.getAASystemDetails(_hawkGroup,_aaSystemTemplate) + + local _units = _hawkGroup:getUnits() + + local _hawkDetails = {} + + for _, _unit in pairs(_units) do + table.insert(_hawkDetails, { point = _unit:getPoint(), unit = _unit:getTypeName(), name = _unit:getName(), system =_aaSystemTemplate}) + end + + return _hawkDetails +end + +function ctld.countTableEntries(_table) + + if _table == nil then + return 0 + end + + + local _count = 0 + + for _key, _value in pairs(_table) do + + _count = _count + 1 + end + + return _count +end + +function ctld.unpackAASystem(_heli, _nearestCrate, _nearbyCrates,_aaSystemTemplate) + + if ctld.rearmAASystem(_heli, _nearestCrate, _nearbyCrates,_aaSystemTemplate) then + -- rearmed hawk + return + end + + -- are there all the pieces close enough together + local _systemParts = {} + + --initialise list of parts + for _,_part in pairs(_aaSystemTemplate.parts) do + _systemParts[_part.name] = {name = _part.name,desc = _part.desc,found = false} + end + + -- find all nearest crates and add them to the list if they're part of the AA System + for _, _nearbyCrate in pairs(_nearbyCrates) do + + if _nearbyCrate.dist < 500 then + + if _systemParts[_nearbyCrate.details.unit] ~= nil and _systemParts[_nearbyCrate.details.unit].found == false then + local _foundPart = _systemParts[_nearbyCrate.details.unit] + + _foundPart.found = true + _foundPart.crate = _nearbyCrate + + _systemParts[_nearbyCrate.details.unit] = _foundPart + end + end + end + + local _count = 0 + local _txt = "" + + local _posArray = {} + local _typeArray = {} + for _name, _systemPart in pairs(_systemParts) do + + if _systemPart.found == false then + _txt = _txt.."Missing ".._systemPart.desc.."\n" + else + + local _launcherPart = ctld.getLauncherUnitFromAATemplate(_aaSystemTemplate) + + --handle multiple launchers from one crate + if (_name == "Hawk ln" and ctld.hawkLaunchers > 1) + or (_launcherPart == _name and ctld.aaLaunchers > 1) then + + --add multiple launcher + local _launchers = ctld.aaLaunchers + + if _name == "Hawk ln" then + _launchers = ctld.hawkLaunchers + end + + for _i = 1, _launchers do + + -- spawn in a circle around the crate + local _angle = math.pi * 2 * (_i - 1) / _launchers + local _xOffset = math.cos(_angle) * 12 + local _yOffset = math.sin(_angle) * 12 + + local _point = _systemPart.crate.crateUnit:getPoint() + + _point = { x = _point.x + _xOffset, y = _point.y, z = _point.z + _yOffset } + + table.insert(_posArray, _point) + table.insert(_typeArray, _name) + end + else + table.insert(_posArray, _systemPart.crate.crateUnit:getPoint()) + table.insert(_typeArray, _name) + end + end + end + + local _activeLaunchers = ctld.countCompleteAASystems(_heli) + + local _allowed = ctld.getAllowedAASystems(_heli) + + env.info("Active: ".._activeLaunchers.." Allowed: ".._allowed) + + if _activeLaunchers + 1 > _allowed then + trigger.action.outTextForCoalition(_heli:getCoalition(), "Out of parts for AA Systems. Current limit is ".._allowed.." \n", 10) + return + end + + if _txt ~= "" then + ctld.displayMessageToGroup(_heli, "Cannot build ".._aaSystemTemplate.name.."\n" .. _txt .. "\n\nOr the crates are not close enough together", 20) + return + else + + -- destroy crates + for _name, _systemPart in pairs(_systemParts) do + + if _heli:getCoalition() == 1 then + ctld.spawnedCratesRED[_systemPart.crate.crateUnit:getName()] = nil + else + ctld.spawnedCratesBLUE[_systemPart.crate.crateUnit:getName()] = nil + end + + --destroy + -- if ctld.slingLoad == false then + _systemPart.crate.crateUnit:destroy() + --end + end + + -- HAWK / BUK READY! + local _spawnedGroup = ctld.spawnCrateGroup(_heli, _posArray, _typeArray) + + ctld.completeAASystems[_spawnedGroup:getName()] = ctld.getAASystemDetails(_spawnedGroup,_aaSystemTemplate) + + ctld.processCallback({unit = _heli, crate = _nearestCrate , spawnedGroup = _spawnedGroup, action = "unpack"}) + + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " successfully deployed a full ".._aaSystemTemplate.name.." to the field. \n\nAA Active System limit is: ".._allowed.."\nActive: "..(_activeLaunchers+1), 10) + + end +end + +--count the number of captured cities, sets the amount of allowed AA Systems +function ctld.getAllowedAASystems(_heli) + + if _heli:getCoalition() == 1 then + return ctld.AASystemLimitBLUE + else + return ctld.AASystemLimitRED + end + + +end + + +function ctld.countCompleteAASystems(_heli) + + local _count = 0 + + for _groupName, _hawkDetails in pairs(ctld.completeAASystems) do + + local _hawkGroup = Group.getByName(_groupName) + + -- env.info(_groupName..": "..mist.utils.tableShow(_hawkDetails)) + if _hawkGroup ~= nil and _hawkGroup:getCoalition() == _heli:getCoalition() then + + local _units = _hawkGroup:getUnits() + + if _units ~=nil and #_units > 0 then + --get the system template + local _aaSystemTemplate = _hawkDetails[1].system + + local _uniqueTypes = {} -- stores each unique part of system + local _types = {} + local _points = {} + + if _units ~= nil and #_units > 0 then + + for x = 1, #_units do + if _units[x]:getLife() > 0 then + + --this allows us to count each type once + _uniqueTypes[_units[x]:getTypeName()] = _units[x]:getTypeName() + + table.insert(_points, _units[x]:getPoint()) + table.insert(_types, _units[x]:getTypeName()) + end + end + end + + -- do we have the correct number of unique pieces and do we have enough points for all the pieces + if ctld.countTableEntries(_uniqueTypes) == _aaSystemTemplate.count and #_points >= _aaSystemTemplate.count then + _count = _count +1 + end + end + end + end + + return _count +end + + +function ctld.repairAASystem(_heli, _nearestCrate,_aaSystem) + + -- find nearest COMPLETE AA system + local _nearestHawk = ctld.findNearestAASystem(_heli,_aaSystem) + + + + if _nearestHawk ~= nil and _nearestHawk.dist < 300 then + + local _oldHawk = ctld.completeAASystems[_nearestHawk.group:getName()] + + --spawn new one + + local _types = {} + local _points = {} + + for _, _part in pairs(_oldHawk) do + table.insert(_points, _part.point) + table.insert(_types, _part.unit) + end + + --remove old system + ctld.completeAASystems[_nearestHawk.group:getName()] = nil + _nearestHawk.group:destroy() + + local _spawnedGroup = ctld.spawnCrateGroup(_heli, _points, _types) + + ctld.completeAASystems[_spawnedGroup:getName()] = ctld.getAASystemDetails(_spawnedGroup,_aaSystem) + + ctld.processCallback({unit = _heli, crate = _nearestCrate , spawnedGroup = _spawnedGroup, action = "repair"}) + + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " successfully repaired a full ".._aaSystem.name.." in the field", 10) + + if _heli:getCoalition() == 1 then + ctld.spawnedCratesRED[_nearestCrate.crateUnit:getName()] = nil + else + ctld.spawnedCratesBLUE[_nearestCrate.crateUnit:getName()] = nil + end + + -- remove crate + -- if ctld.slingLoad == false then + _nearestCrate.crateUnit:destroy() + -- end + + else + ctld.displayMessageToGroup(_heli, "Cannot repair ".._aaSystem.name..". No damaged ".._aaSystem.name.." within 300m", 10) + end +end + +function ctld.unpackMultiCrate(_heli, _nearestCrate, _nearbyCrates) + + -- unpack multi crate + local _nearbyMultiCrates = {} + + for _, _nearbyCrate in pairs(_nearbyCrates) do + + if _nearbyCrate.dist < 300 then + + if _nearbyCrate.details.unit == _nearestCrate.details.unit then + + table.insert(_nearbyMultiCrates, _nearbyCrate) + + if #_nearbyMultiCrates == _nearestCrate.details.cratesRequired then + break + end + end + end + end + + --- check crate count + if #_nearbyMultiCrates == _nearestCrate.details.cratesRequired then + + local _point = _nearestCrate.crateUnit:getPoint() + + -- destroy crates + for _, _crate in pairs(_nearbyMultiCrates) do + + if _point == nil then + _point = _crate.crateUnit:getPoint() + end + + if _heli:getCoalition() == 1 then + ctld.spawnedCratesRED[_crate.crateUnit:getName()] = nil + else + ctld.spawnedCratesBLUE[_crate.crateUnit:getName()] = nil + end + + --destroy + -- if ctld.slingLoad == false then + _crate.crateUnit:destroy() + -- end + end + + + local _spawnedGroup = ctld.spawnCrateGroup(_heli, { _point }, { _nearestCrate.details.unit }) + + ctld.processCallback({unit = _heli, crate = _nearestCrate , spawnedGroup = _spawnedGroup, action = "unpack"}) + + local _txt = string.format("%s successfully deployed %s to the field using %d crates", ctld.getPlayerNameOrType(_heli), _nearestCrate.details.desc, #_nearbyMultiCrates) + + trigger.action.outTextForCoalition(_heli:getCoalition(), _txt, 10) + + else + + local _txt = string.format("Cannot build %s!\n\nIt requires %d crates and there are %d \n\nOr the crates are not within 300m of each other", _nearestCrate.details.desc, _nearestCrate.details.cratesRequired, #_nearbyMultiCrates) + + ctld.displayMessageToGroup(_heli, _txt, 20) + end +end + + +function ctld.spawnCrateGroup(_heli, _positions, _types) + + local _id = ctld.getNextGroupId() + + local _groupName = _types[1] .. " #" .. _id + + local _side = _heli:getCoalition() + + local _group = { + ["visible"] = false, + -- ["groupId"] = _id, + ["hidden"] = false, + ["units"] = {}, + -- ["y"] = _positions[1].z, + -- ["x"] = _positions[1].x, + ["name"] = _groupName, + ["task"] = {}, + } + + if #_positions == 1 then + + local _unitId = ctld.getNextUnitId() + local _details = { type = _types[1], unitId = _unitId, name = string.format("Unpacked %s #%i", _types[1], _unitId) } + + _group.units[1] = ctld.createUnit(_positions[1].x + 5, _positions[1].z + 5, 120, _details) + + else + + for _i, _pos in ipairs(_positions) do + + local _unitId = ctld.getNextUnitId() + local _details = { type = _types[_i], unitId = _unitId, name = string.format("Unpacked %s #%i", _types[_i], _unitId) } + + _group.units[_i] = ctld.createUnit(_pos.x + 5, _pos.z + 5, 120, _details) + end + end + + --mist function + _group.category = Group.Category.GROUND + _group.country = _heli:getCountry() + + local _spawnedGroup = Group.getByName(mist.dynAdd(_group).name) + + return _spawnedGroup +end + + + +-- spawn normal group +function ctld.spawnDroppedGroup(_point, _details, _spawnBehind, _maxSearch) + + local _groupName = _details.groupName + + local _group = { + ["visible"] = false, + -- ["groupId"] = _details.groupId, + ["hidden"] = false, + ["units"] = {}, + -- ["y"] = _positions[1].z, + -- ["x"] = _positions[1].x, + ["name"] = _groupName, + ["task"] = {}, + } + + + if _spawnBehind == false then + + -- spawn in circle around heli + + local _pos = _point + + for _i, _detail in ipairs(_details.units) do + + local _angle = math.pi * 2 * (_i - 1) / #_details.units + local _xOffset = math.cos(_angle) * 30 + local _yOffset = math.sin(_angle) * 30 + + _group.units[_i] = ctld.createUnit(_pos.x + _xOffset, _pos.z + _yOffset, _angle, _detail) + end + + else + + local _pos = _point + + --try to spawn at 6 oclock to us + local _angle = math.atan2(_pos.z, _pos.x) + local _xOffset = math.cos(_angle) * -30 + local _yOffset = math.sin(_angle) * -30 + + + for _i, _detail in ipairs(_details.units) do + _group.units[_i] = ctld.createUnit(_pos.x + (_xOffset + 10 * _i), _pos.z + (_yOffset + 10 * _i), _angle, _detail) + end + end + + --switch to MIST + _group.category = Group.Category.GROUND; + _group.country = _details.country; + + local _spawnedGroup = Group.getByName(mist.dynAdd(_group).name) + + --local _spawnedGroup = coalition.addGroup(_details.country, Group.Category.GROUND, _group) + + + -- find nearest enemy and head there + if _maxSearch == nil then + _maxSearch = ctld.maximumSearchDistance + end + + local _wpZone = ctld.inWaypointZone(_point,_spawnedGroup:getCoalition()) + + if _wpZone.inZone then + ctld.orderGroupToMoveToPoint(_spawnedGroup:getUnit(1), _wpZone.point) + env.info("Heading to waypoint - In Zone ".._wpZone.name) + else + local _enemyPos = ctld.findNearestEnemy(_details.side, _point, _maxSearch) + + ctld.orderGroupToMoveToPoint(_spawnedGroup:getUnit(1), _enemyPos) + end + + return _spawnedGroup +end + +function ctld.findNearestEnemy(_side, _point, _searchDistance) + + local _closestEnemy = nil + + local _groups + + local _closestEnemyDist = _searchDistance + + local _heliPoint = _point + + if _side == 2 then + _groups = coalition.getGroups(1, Group.Category.GROUND) + else + _groups = coalition.getGroups(2, Group.Category.GROUND) + end + + for _, _group in pairs(_groups) do + + if _group ~= nil then + local _units = _group:getUnits() + + if _units ~= nil and #_units > 0 then + + local _leader = nil + + -- find alive leader + for x = 1, #_units do + if _units[x]:getLife() > 0 then + _leader = _units[x] + break + end + end + + if _leader ~= nil then + local _leaderPos = _leader:getPoint() + local _dist = ctld.getDistance(_heliPoint, _leaderPos) + if _dist < _closestEnemyDist then + _closestEnemyDist = _dist + _closestEnemy = _leaderPos + end + end + end + end + end + + + -- no enemy - move to random point + if _closestEnemy ~= nil then + + -- env.info("found enemy") + return _closestEnemy + else + + local _x = _heliPoint.x + math.random(0, ctld.maximumMoveDistance) - math.random(0, ctld.maximumMoveDistance) + local _z = _heliPoint.z + math.random(0, ctld.maximumMoveDistance) - math.random(0, ctld.maximumMoveDistance) + local _y = _heliPoint.y + math.random(0, ctld.maximumMoveDistance) - math.random(0, ctld.maximumMoveDistance) + + return { x = _x, z = _z,y=_y } + end +end + +function ctld.findNearestGroup(_heli, _groups) + + local _closestGroupDetails = {} + local _closestGroup = nil + + local _closestGroupDist = ctld.maxExtractDistance + + local _heliPoint = _heli:getPoint() + + for _, _groupName in pairs(_groups) do + + local _group = Group.getByName(_groupName) + + if _group ~= nil then + local _units = _group:getUnits() + + if _units ~= nil and #_units > 0 then + + local _leader = nil + + local _groupDetails = { groupId = _group:getID(), groupName = _group:getName(), side = _group:getCoalition(), units = {} } + + -- find alive leader + for x = 1, #_units do + if _units[x]:getLife() > 0 then + + if _leader == nil then + _leader = _units[x] + -- set country based on leader + _groupDetails.country = _leader:getCountry() + end + + local _unitDetails = { type = _units[x]:getTypeName(), unitId = _units[x]:getID(), name = _units[x]:getName() } + + table.insert(_groupDetails.units, _unitDetails) + end + end + + if _leader ~= nil then + local _leaderPos = _leader:getPoint() + local _dist = ctld.getDistance(_heliPoint, _leaderPos) + if _dist < _closestGroupDist then + _closestGroupDist = _dist + _closestGroupDetails = _groupDetails + _closestGroup = _group + end + end + end + end + end + + + if _closestGroup ~= nil then + + return { group = _closestGroup, details = _closestGroupDetails } + else + + return nil + end +end + + +function ctld.createUnit(_x, _y, _angle, _details) + + local _newUnit = { + ["y"] = _y, + ["type"] = _details.type, + ["name"] = _details.name, + -- ["unitId"] = _details.unitId, + ["heading"] = _angle, + ["playerCanDrive"] = true, + ["skill"] = "Excellent", + ["x"] = _x, + } + + return _newUnit +end + +function ctld.addEWRTask(_group) + + -- delayed 2 second to work around bug + timer.scheduleFunction(function(_ewrGroup) + local _grp = ctld.getAliveGroup(_ewrGroup) + + if _grp ~= nil then + local _controller = _grp:getController(); + local _EWR = { + id = 'EWR', + auto = true, + params = { + } + } + _controller:setTask(_EWR) + end + end + , _group:getName(), timer.getTime() + 2) + +end + +function ctld.orderGroupToMoveToPoint(_leader, _destination) + + local _group = _leader:getGroup() + + local _path = {} + table.insert(_path, mist.ground.buildWP(_leader:getPoint(), 'Off Road', 50)) + table.insert(_path, mist.ground.buildWP(_destination, 'Off Road', 50)) + + local _mission = { + id = 'Mission', + params = { + route = { + points =_path + }, + }, + } + + + -- delayed 2 second to work around bug + timer.scheduleFunction(function(_arg) + local _grp = ctld.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:getName(), _mission}, timer.getTime() + 2) + +end + +-- are we in pickup zone +function ctld.inPickupZone(_heli) + ctld.logDebug(string.format("ctld.inPickupZone(_heli=%s)", ctld.p(_heli))) + + if ctld.inAir(_heli) then + return { inZone = false, limit = -1, index = -1 } + end + + local _heliPoint = _heli:getPoint() + + for _i, _zoneDetails in pairs(ctld.pickupZones) do + ctld.logTrace(string.format("_zoneDetails=%s", ctld.p(_zoneDetails))) + + local _triggerZone = trigger.misc.getZone(_zoneDetails[1]) + + if _triggerZone == nil then + local _ship = ctld.getTransportUnit(_zoneDetails[1]) + + if _ship then + local _point = _ship:getPoint() + _triggerZone = {} + _triggerZone.point = _point + _triggerZone.radius = 200 -- should be big enough for ship + end + + end + + if _triggerZone ~= nil then + + --get distance to center + + local _dist = ctld.getDistance(_heliPoint, _triggerZone.point) + ctld.logTrace(string.format("_dist=%s", ctld.p(_dist))) + if _dist <= _triggerZone.radius then + local _heliCoalition = _heli:getCoalition() + if _zoneDetails[4] == 1 and (_zoneDetails[5] == _heliCoalition or _zoneDetails[5] == 0) then + return { inZone = true, limit = _zoneDetails[3], index = _i } + end + end + end + end + + local _fobs = ctld.getSpawnedFobs(_heli) + + -- now check spawned fobs + for _, _fob in ipairs(_fobs) do + + --get distance to center + + local _dist = ctld.getDistance(_heliPoint, _fob:getPoint()) + + if _dist <= 150 then + return { inZone = true, limit = 10000, index = -1 }; + end + end + + + + return { inZone = false, limit = -1, index = -1 }; +end + +function ctld.getSpawnedFobs(_heli) + + local _fobs = {} + + for _, _fobName in ipairs(ctld.builtFOBS) do + + local _fob = StaticObject.getByName(_fobName) + + if _fob ~= nil and _fob:isExist() and _fob:getCoalition() == _heli:getCoalition() and _fob:getLife() > 0 then + + table.insert(_fobs, _fob) + end + end + + return _fobs +end + +-- are we in a dropoff zone +function ctld.inDropoffZone(_heli) + + if ctld.inAir(_heli) then + return false + end + + local _heliPoint = _heli:getPoint() + + for _, _zoneDetails in pairs(ctld.dropOffZones) do + + local _triggerZone = trigger.misc.getZone(_zoneDetails[1]) + + if _triggerZone ~= nil and (_zoneDetails[3] == _heli:getCoalition() or _zoneDetails[3]== 0) then + + --get distance to center + + local _dist = ctld.getDistance(_heliPoint, _triggerZone.point) + + if _dist <= _triggerZone.radius then + return true + end + end + end + + return false +end + +-- are we in a waypoint zone +function ctld.inWaypointZone(_point,_coalition) + + for _, _zoneDetails in pairs(ctld.wpZones) do + + local _triggerZone = trigger.misc.getZone(_zoneDetails[1]) + + --right coalition and active? + if _triggerZone ~= nil and (_zoneDetails[4] == _coalition or _zoneDetails[4]== 0) and _zoneDetails[3] == 1 then + + --get distance to center + + local _dist = ctld.getDistance(_point, _triggerZone.point) + + if _dist <= _triggerZone.radius then + return {inZone = true, point = _triggerZone.point, name = _zoneDetails[1]} + end + end + end + + return {inZone = false} +end + +-- are we near friendly logistics zone +function ctld.inLogisticsZone(_heli) + + if ctld.inAir(_heli) then + return false + end + + local _heliPoint = _heli:getPoint() + + for _, _name in pairs(ctld.logisticUnits) do + + local _logistic = StaticObject.getByName(_name) + + if _logistic ~= nil and _logistic:getCoalition() == _heli:getCoalition() then + + --get distance + local _dist = ctld.getDistance(_heliPoint, _logistic:getPoint()) + + if _dist <= ctld.maximumDistanceLogistic then + return true + end + end + end + + return false +end + + +-- are far enough from a friendly logistics zone +function ctld.farEnoughFromLogisticZone(_heli) + + if ctld.inAir(_heli) then + return false + end + + local _heliPoint = _heli:getPoint() + + local _farEnough = true + + for _, _name in pairs(ctld.logisticUnits) do + + local _logistic = StaticObject.getByName(_name) + + if _logistic ~= nil and _logistic:getCoalition() == _heli:getCoalition() then + + --get distance + local _dist = ctld.getDistance(_heliPoint, _logistic:getPoint()) + -- env.info("DIST ".._dist) + if _dist <= ctld.minimumDeployDistance then + -- env.info("TOO CLOSE ".._dist) + _farEnough = false + end + end + end + + return _farEnough +end + +function ctld.refreshSmoke() + + if ctld.disableAllSmoke == true then + return + end + + for _, _zoneGroup in pairs({ ctld.pickupZones, ctld.dropOffZones }) do + + for _, _zoneDetails in pairs(_zoneGroup) do + + local _triggerZone = trigger.misc.getZone(_zoneDetails[1]) + + if _triggerZone == nil then + local _ship = ctld.getTransportUnit(_triggerZone) + + if _ship then + local _point = _ship:getPoint() + _triggerZone = {} + _triggerZone.point = _point + end + + end + + + --only trigger if smoke is on AND zone is active + if _triggerZone ~= nil and _zoneDetails[2] >= 0 and _zoneDetails[4] == 1 then + + -- Trigger smoke markers + + local _pos2 = { x = _triggerZone.point.x, y = _triggerZone.point.z } + local _alt = land.getHeight(_pos2) + local _pos3 = { x = _pos2.x, y = _alt, z = _pos2.y } + + trigger.action.smoke(_pos3, _zoneDetails[2]) + end + end + end + + --waypoint zones + for _, _zoneDetails in pairs(ctld.wpZones) do + + local _triggerZone = trigger.misc.getZone(_zoneDetails[1]) + + --only trigger if smoke is on AND zone is active + if _triggerZone ~= nil and _zoneDetails[2] >= 0 and _zoneDetails[3] == 1 then + + -- Trigger smoke markers + + local _pos2 = { x = _triggerZone.point.x, y = _triggerZone.point.z } + local _alt = land.getHeight(_pos2) + local _pos3 = { x = _pos2.x, y = _alt, z = _pos2.y } + + trigger.action.smoke(_pos3, _zoneDetails[2]) + end + end + + + --refresh in 5 minutes + timer.scheduleFunction(ctld.refreshSmoke, nil, timer.getTime() + 300) +end + +function ctld.dropSmoke(_args) + + local _heli = ctld.getTransportUnit(_args[1]) + + if _heli ~= nil then + + local _colour = "" + + if _args[2] == trigger.smokeColor.Red then + + _colour = "RED" + elseif _args[2] == trigger.smokeColor.Blue then + + _colour = "BLUE" + elseif _args[2] == trigger.smokeColor.Green then + + _colour = "GREEN" + elseif _args[2] == trigger.smokeColor.Orange then + + _colour = "ORANGE" + end + + local _point = _heli:getPoint() + + local _pos2 = { x = _point.x, y = _point.z } + local _alt = land.getHeight(_pos2) + local _pos3 = { x = _point.x, y = _alt, z = _point.z } + + trigger.action.smoke(_pos3, _args[2]) + + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " dropped " .. _colour .. " smoke ", 10) + end +end + +function ctld.unitCanCarryVehicles(_unit) + + local _type = string.lower(_unit:getTypeName()) + + for _, _name in ipairs(ctld.vehicleTransportEnabled) do + local _nameLower = string.lower(_name) + if string.match(_type, _nameLower) then + return true + end + end + + return false +end + +function ctld.isJTACUnitType(_type) + + _type = string.lower(_type) + + for _, _name in ipairs(ctld.jtacUnitTypes) do + local _nameLower = string.lower(_name) + if string.match(_type, _nameLower) then + return true + end + end + + return false +end + +function ctld.updateZoneCounter(_index, _diff) + + if ctld.pickupZones[_index] ~= nil then + + ctld.pickupZones[_index][3] = ctld.pickupZones[_index][3] + _diff + + if ctld.pickupZones[_index][3] < 0 then + ctld.pickupZones[_index][3] = 0 + end + + if ctld.pickupZones[_index][6] ~= nil then + trigger.action.setUserFlag(ctld.pickupZones[_index][6], ctld.pickupZones[_index][3]) + end + -- env.info(ctld.pickupZones[_index][1].." = " ..ctld.pickupZones[_index][3]) + end +end + +function ctld.processCallback(_callbackArgs) + + for _, _callback in pairs(ctld.callbacks) do + + local _status, _result = pcall(function() + + _callback(_callbackArgs) + + end) + + if (not _status) then + env.error(string.format("CTLD Callback Error: %s", _result)) + end + end +end + + +-- checks the status of all AI troop carriers and auto loads and unloads troops +-- as long as the troops are on the ground +function ctld.checkAIStatus() + + timer.scheduleFunction(ctld.checkAIStatus, nil, timer.getTime() + 2) + + + for _, _unitName in pairs(ctld.transportPilotNames) do + local status, error = pcall(function() + + local _unit = ctld.getTransportUnit(_unitName) + + -- no player name means AI! + if _unit ~= nil and _unit:getPlayerName() == nil then + local _zone = ctld.inPickupZone(_unit) + -- env.error("Checking.. ".._unit:getName()) + if _zone.inZone == true and not ctld.troopsOnboard(_unit, true) then + -- env.error("in zone, loading.. ".._unit:getName()) + + if ctld.allowRandomAiTeamPickups == true then + -- Random troop pickup implementation + local _team = nil + if _unit:getCoalition() == 1 then + _team = math.floor((math.random(#ctld.redTeams * 100) / 100) + 1) + ctld.loadTroopsFromZone({ _unitName, true,ctld.loadableGroups[ctld.redTeams[_team]],true }) + else + _team = math.floor((math.random(#ctld.blueTeams * 100) / 100) + 1) + ctld.loadTroopsFromZone({ _unitName, true,ctld.loadableGroups[ctld.blueTeams[_team]],true }) + end + else + ctld.loadTroopsFromZone({ _unitName, true,"",true }) + end + + elseif ctld.inDropoffZone(_unit) and ctld.troopsOnboard(_unit, true) then + -- env.error("in dropoff zone, unloading.. ".._unit:getName()) + ctld.unloadTroops( { _unitName, true }) + end + + if ctld.unitCanCarryVehicles(_unit) then + + if _zone.inZone == true and not ctld.troopsOnboard(_unit, false) then + + ctld.loadTroopsFromZone({ _unitName, false,"",true }) + + elseif ctld.inDropoffZone(_unit) and ctld.troopsOnboard(_unit, false) then + + ctld.unloadTroops( { _unitName, false }) + end + end + end + end) + + if (not status) then + env.error(string.format("Error with ai status: %s", error), false) + end + end + + +end + +function ctld.getTransportLimit(_unitType) + + if ctld.unitLoadLimits[_unitType] then + + return ctld.unitLoadLimits[_unitType] + end + + return ctld.numberOfTroops + +end + +function ctld.getUnitActions(_unitType) + + if ctld.unitActions[_unitType] then + return ctld.unitActions[_unitType] + end + + return {crates=true,troops=true} + +end + +-- Adds menuitem to all heli units that are active +function ctld.addF10MenuOptions() + -- Loop through all Heli units + + timer.scheduleFunction(ctld.addF10MenuOptions, nil, timer.getTime() + 10) + + for _, _unitName in pairs(ctld.transportPilotNames) do + + local status, error = pcall(function() + + local _unit = ctld.getTransportUnit(_unitName) + + if _unit ~= nil then + + local _groupId = ctld.getGroupId(_unit) + + if _groupId then + + if ctld.addedTo[tostring(_groupId)] == nil then + + local _rootPath = missionCommands.addSubMenuForGroup(_groupId, "CTLD") + + local _unitActions = ctld.getUnitActions(_unit:getTypeName()) + ctld.logTrace(string.format("_unitActions=%s", ctld.p(_unitActions))) + + missionCommands.addCommandForGroup(_groupId, "Check Cargo", _rootPath, ctld.checkTroopStatus, { _unitName }) + + if _unitActions.troops then + + local _troopCommandsPath = missionCommands.addSubMenuForGroup(_groupId, "Troop Transport", _rootPath) + + missionCommands.addCommandForGroup(_groupId, "Unload / Extract Troops", _troopCommandsPath, ctld.unloadExtractTroops, { _unitName }) + + + -- local _loadPath = missionCommands.addSubMenuForGroup(_groupId, "Load From Zone", _troopCommandsPath) + local _transportLimit = ctld.getTransportLimit(_unit:getTypeName()) + ctld.logTrace(string.format("_transportLimit=%s", ctld.p(_transportLimit))) + for _,_loadGroup in pairs(ctld.loadableGroups) do + ctld.logTrace(string.format("_loadGroup=%s", ctld.p(_loadGroup))) + if not _loadGroup.side or _loadGroup.side == _unit:getCoalition() then + + -- check size & unit + if _transportLimit >= _loadGroup.total then + missionCommands.addCommandForGroup(_groupId, "Load ".._loadGroup.name, _troopCommandsPath, ctld.loadTroopsFromZone, { _unitName, true,_loadGroup,false }) + end + end + end + + if ctld.unitCanCarryVehicles(_unit) then + + local _vehicleCommandsPath = missionCommands.addSubMenuForGroup(_groupId, "Vehicle / FOB Transport", _rootPath) + + missionCommands.addCommandForGroup(_groupId, "Unload Vehicles", _vehicleCommandsPath, ctld.unloadTroops, { _unitName, false }) + missionCommands.addCommandForGroup(_groupId, "Load / Extract Vehicles", _vehicleCommandsPath, ctld.loadTroopsFromZone, { _unitName, false,"",true }) + + if ctld.enabledFOBBuilding and ctld.staticBugWorkaround == false then + + missionCommands.addCommandForGroup(_groupId, "Load / Unload FOB Crate", _vehicleCommandsPath, ctld.loadUnloadFOBCrate, { _unitName, false }) + end + missionCommands.addCommandForGroup(_groupId, "Check Cargo", _vehicleCommandsPath, ctld.checkTroopStatus, { _unitName }) + end + + end + + + if ctld.enableCrates and _unitActions.crates then + + if ctld.unitCanCarryVehicles(_unit) == false then + + -- local _cratePath = missionCommands.addSubMenuForGroup(_groupId, "Spawn Crate", _rootPath) + -- add menu for spawning crates + for _subMenuName, _crates in pairs(ctld.spawnableCrates) do + + local _cratePath = missionCommands.addSubMenuForGroup(_groupId, _subMenuName, _rootPath) + for _, _crate in pairs(_crates) do + + if ctld.isJTACUnitType(_crate.unit) == false + or (ctld.isJTACUnitType(_crate.unit) == true and ctld.JTAC_dropEnabled) then + if _crate.side == nil or (_crate.side == _unit:getCoalition()) then + + local _crateRadioMsg = _crate.desc + + --add in the number of crates required to build something + if _crate.cratesRequired ~= nil and _crate.cratesRequired > 1 then + _crateRadioMsg = _crateRadioMsg.." (".._crate.cratesRequired..")" + end + + missionCommands.addCommandForGroup(_groupId,_crateRadioMsg, _cratePath, ctld.spawnCrate, { _unitName, _crate.weight }) + end + end + end + end + end + end + + if (ctld.enabledFOBBuilding or ctld.enableCrates) and _unitActions.crates then + + local _crateCommands = missionCommands.addSubMenuForGroup(_groupId, "CTLD Commands", _rootPath) + if ctld.hoverPickup == false then + if ctld.slingLoad == false then + missionCommands.addCommandForGroup(_groupId, "Load Nearby Crate", _crateCommands, ctld.loadNearbyCrate, _unitName ) + end + end + + missionCommands.addCommandForGroup(_groupId, "Unpack Any Crate", _crateCommands, ctld.unpackCrates, { _unitName }) + + if ctld.slingLoad == false then + missionCommands.addCommandForGroup(_groupId, "Drop Crate", _crateCommands, ctld.dropSlingCrate, { _unitName }) + end + + missionCommands.addCommandForGroup(_groupId, "List Nearby Crates", _crateCommands, ctld.listNearbyCrates, { _unitName }) + + if ctld.enabledFOBBuilding then + missionCommands.addCommandForGroup(_groupId, "List FOBs", _crateCommands, ctld.listFOBS, { _unitName }) + end + end + + + if ctld.enableSmokeDrop then + local _smokeMenu = missionCommands.addSubMenuForGroup(_groupId, "Smoke Markers", _rootPath) + missionCommands.addCommandForGroup(_groupId, "Drop Red Smoke", _smokeMenu, ctld.dropSmoke, { _unitName, trigger.smokeColor.Red }) + missionCommands.addCommandForGroup(_groupId, "Drop Blue Smoke", _smokeMenu, ctld.dropSmoke, { _unitName, trigger.smokeColor.Blue }) + missionCommands.addCommandForGroup(_groupId, "Drop Orange Smoke", _smokeMenu, ctld.dropSmoke, { _unitName, trigger.smokeColor.Orange }) + missionCommands.addCommandForGroup(_groupId, "Drop Green Smoke", _smokeMenu, ctld.dropSmoke, { _unitName, trigger.smokeColor.Green }) + end + + if ctld.enabledRadioBeaconDrop then + local _radioCommands = missionCommands.addSubMenuForGroup(_groupId, "Radio Beacons", _rootPath) + missionCommands.addCommandForGroup(_groupId, "List Beacons", _radioCommands, ctld.listRadioBeacons, { _unitName }) + missionCommands.addCommandForGroup(_groupId, "Drop Beacon", _radioCommands, ctld.dropRadioBeacon, { _unitName }) + missionCommands.addCommandForGroup(_groupId, "Remove Closet Beacon", _radioCommands, ctld.removeRadioBeacon, { _unitName }) + elseif ctld.deployedRadioBeacons ~= {} then + local _radioCommands = missionCommands.addSubMenuForGroup(_groupId, "Radio Beacons", _rootPath) + missionCommands.addCommandForGroup(_groupId, "List Beacons", _radioCommands, ctld.listRadioBeacons, { _unitName }) + end + + ctld.addedTo[tostring(_groupId)] = true + end + end + else + -- env.info(string.format("unit nil %s",_unitName)) + end + end) + + if (not status) then + env.error(string.format("Error adding f10 to transport: %s", error), false) + end + end + + local status, error = pcall(function() + + -- now do any player controlled aircraft that ARENT transport units + if ctld.enabledRadioBeaconDrop then + -- get all BLUE players + ctld.addRadioListCommand(2) + + -- get all RED players + ctld.addRadioListCommand(1) + end + + + if ctld.JTAC_jtacStatusF10 then + -- get all BLUE players + ctld.addJTACRadioCommand(2) + + -- get all RED players + ctld.addJTACRadioCommand(1) + end + + end) + + if (not status) then + env.error(string.format("Error adding f10 to other players: %s", error), false) + end + + +end + +--add to all players that arent transport +function ctld.addRadioListCommand(_side) + + local _players = coalition.getPlayers(_side) + + if _players ~= nil then + + for _, _playerUnit in pairs(_players) do + + local _groupId = ctld.getGroupId(_playerUnit) + + if _groupId then + + if ctld.addedTo[tostring(_groupId)] == nil then + missionCommands.addCommandForGroup(_groupId, "List Radio Beacons", nil, ctld.listRadioBeacons, { _playerUnit:getName() }) + ctld.addedTo[tostring(_groupId)] = true + end + end + end + end +end + +function ctld.addJTACRadioCommand(_side) + + local _players = coalition.getPlayers(_side) + + if _players ~= nil then + + for _, _playerUnit in pairs(_players) do + + local _groupId = ctld.getGroupId(_playerUnit) + + if _groupId then + -- env.info("adding command for "..index) + if ctld.jtacRadioAdded[tostring(_groupId)] == nil then + -- env.info("about command for "..index) + missionCommands.addCommandForGroup(_groupId, "JTAC Status", nil, ctld.getJTACStatus, { _playerUnit:getName() }) + ctld.jtacRadioAdded[tostring(_groupId)] = true + -- env.info("Added command for " .. index) + end + end + + + end + end +end + +function ctld.getGroupId(_unit) + + local _unitDB = mist.DBs.unitsById[tonumber(_unit:getID())] + if _unitDB ~= nil and _unitDB.groupId then + return _unitDB.groupId + end + + return nil +end + +--get distance in meters assuming a Flat world +function ctld.getDistance(_point1, _point2) + + 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) +end + + +------------ JTAC ----------- + + +ctld.jtacLaserPoints = {} +ctld.jtacIRPoints = {} +ctld.jtacSmokeMarks = {} +ctld.jtacUnits = {} -- list of JTAC units for f10 command +ctld.jtacStop = {} -- jtacs to tell to stop lasing +ctld.jtacCurrentTargets = {} +ctld.jtacRadioAdded = {} --keeps track of who's had the radio command added +ctld.jtacGeneratedLaserCodes = {} -- keeps track of generated codes, cycles when they run out +ctld.jtacLaserPointCodes = {} +ctld.jtacRadioData = {} + +function ctld.JTACAutoLase(_jtacGroupName, _laserCode, _smoke, _lock, _colour, _radio) + ctld.logDebug(string.format("ctld.JTACAutoLase(_jtacGroupName=%s, _laserCode=%s", ctld.p(_jtacGroupName), ctld.p(_laserCode))) + + local _radio = _radio + if not _radio then + _radio = {} + if _laserCode then + local _laserCode = tonumber(_laserCode) + if _laserCode and _laserCode >= 1111 and _laserCode <= 1688 then + local _laserB = math.floor((_laserCode - 1000)/100) + local _laserCD = _laserCode - 1000 - _laserB*100 + local _frequency = tostring(30+_laserB+_laserCD*0.05) + ctld.logTrace(string.format("_laserB=%s", ctld.p(_laserB))) + ctld.logTrace(string.format("_laserCD=%s", ctld.p(_laserCD))) + ctld.logTrace(string.format("_frequency=%s", ctld.p(_frequency))) + _radio.freq = _frequency + _radio.mod = "fm" + end + end + end + + if _radio and not _radio.name then + _radio.name = _jtacGroupName + end + + if ctld.jtacStop[_jtacGroupName] == true then + ctld.jtacStop[_jtacGroupName] = nil -- allow it to be started again + ctld.cleanupJTAC(_jtacGroupName) + return + end + + if _lock == nil then + + _lock = ctld.JTAC_lock + end + + + ctld.jtacLaserPointCodes[_jtacGroupName] = _laserCode + ctld.jtacRadioData[_jtacGroupName] = _radio + + local _jtacGroup = ctld.getGroup(_jtacGroupName) + local _jtacUnit + + if _jtacGroup == nil or #_jtacGroup == 0 then + + --check not in a heli + if ctld.inTransitTroops then + for _, _onboard in pairs(ctld.inTransitTroops) do + if _onboard ~= nil then + if _onboard.troops ~= nil and _onboard.troops.groupName ~= nil and _onboard.troops.groupName == _jtacGroupName then + + --jtac soldier being transported by heli + ctld.cleanupJTAC(_jtacGroupName) + + env.info(_jtacGroupName .. ' in Transport - Waiting 10 seconds') + timer.scheduleFunction(ctld.timerJTACAutoLase, { _jtacGroupName, _laserCode, _smoke, _lock, _colour, _radio }, timer.getTime() + 10) + return + end + + if _onboard.vehicles ~= nil and _onboard.vehicles.groupName ~= nil and _onboard.vehicles.groupName == _jtacGroupName then + --jtac vehicle being transported by heli + ctld.cleanupJTAC(_jtacGroupName) + + env.info(_jtacGroupName .. ' in Transport - Waiting 10 seconds') + timer.scheduleFunction(ctld.timerJTACAutoLase, { _jtacGroupName, _laserCode, _smoke, _lock, _colour, _radio }, timer.getTime() + 10) + return + end + end + end + end + + + if ctld.jtacUnits[_jtacGroupName] ~= nil then + ctld.notifyCoalition("JTAC Group " .. _jtacGroupName .. " KIA!", 10, ctld.jtacUnits[_jtacGroupName].side, _radio) + end + + --remove from list + ctld.jtacUnits[_jtacGroupName] = nil + + ctld.cleanupJTAC(_jtacGroupName) + + return + else + + _jtacUnit = _jtacGroup[1] + --add to list + ctld.jtacUnits[_jtacGroupName] = { name = _jtacUnit:getName(), side = _jtacUnit:getCoalition(), radio = _radio } + + -- work out smoke colour + if _colour == nil then + + if _jtacUnit:getCoalition() == 1 then + _colour = ctld.JTAC_smokeColour_RED + else + _colour = ctld.JTAC_smokeColour_BLUE + end + end + + + if _smoke == nil then + + if _jtacUnit:getCoalition() == 1 then + _smoke = ctld.JTAC_smokeOn_RED + else + _smoke = ctld.JTAC_smokeOn_BLUE + end + end + end + + + -- search for current unit + + if _jtacUnit:isActive() == false then + + ctld.cleanupJTAC(_jtacGroupName) + + env.info(_jtacGroupName .. ' Not Active - Waiting 30 seconds') + timer.scheduleFunction(ctld.timerJTACAutoLase, { _jtacGroupName, _laserCode, _smoke, _lock, _colour, _radio }, timer.getTime() + 30) + + return + end + + local _enemyUnit = ctld.getCurrentUnit(_jtacUnit, _jtacGroupName) + local targetDestroyed = false + local targetLost = false + + if _enemyUnit == nil and ctld.jtacCurrentTargets[_jtacGroupName] ~= nil then + + local _tempUnitInfo = ctld.jtacCurrentTargets[_jtacGroupName] + + -- env.info("TEMP UNIT INFO: " .. tempUnitInfo.name .. " " .. tempUnitInfo.unitType) + + local _tempUnit = Unit.getByName(_tempUnitInfo.name) + + if _tempUnit ~= nil and _tempUnit:getLife() > 0 and _tempUnit:isActive() == true then + targetLost = true + else + targetDestroyed = true + end + + --remove from smoke list + ctld.jtacSmokeMarks[_tempUnitInfo.name] = nil + + -- JTAC Unit: resume his route ------------ + trigger.action.groupContinueMoving(Group.getByName(_jtacGroupName)) + + -- remove from target list + ctld.jtacCurrentTargets[_jtacGroupName] = nil + + --stop lasing + ctld.cancelLase(_jtacGroupName) + end + + + if _enemyUnit == nil then + _enemyUnit = ctld.findNearestVisibleEnemy(_jtacUnit, _lock) + + if _enemyUnit ~= nil then + + -- store current target for easy lookup + ctld.jtacCurrentTargets[_jtacGroupName] = { name = _enemyUnit:getName(), unitType = _enemyUnit:getTypeName(), unitId = _enemyUnit:getID() } + local action = ", lasing new target, " + if targetLost then + action = ", target lost " .. action + targetLost = false + elseif targetDestroyed then + action = ", target destroyed " .. action + targetDestroyed = false + end + + local message = _jtacGroupName .. action .. _enemyUnit:getTypeName() + local fullMessage = message .. '. CODE: ' .. _laserCode .. ". POSITION: " .. ctld.getPositionString(_enemyUnit) + ctld.notifyCoalition(fullMessage, 10, _jtacUnit:getCoalition(), _radio, message) + + -- JTAC Unit stop his route ----------------- + trigger.action.groupStopMoving(Group.getByName(_jtacGroupName)) -- stop JTAC + + -- create smoke + if _smoke == true then + + --create first smoke + ctld.createSmokeMarker(_enemyUnit, _colour) + end + end + end + + if _enemyUnit ~= nil then + + ctld.laseUnit(_enemyUnit, _jtacUnit, _jtacGroupName, _laserCode) + + -- env.info('Timer timerSparkleLase '..jtacGroupName.." "..laserCode.." "..enemyUnit:getName()) + timer.scheduleFunction(ctld.timerJTACAutoLase, { _jtacGroupName, _laserCode, _smoke, _lock, _colour, _radio }, timer.getTime() + 15) + + + if _smoke == true then + local _nextSmokeTime = ctld.jtacSmokeMarks[_enemyUnit:getName()] + + --recreate smoke marker after 5 mins + if _nextSmokeTime ~= nil and _nextSmokeTime < timer.getTime() then + + ctld.createSmokeMarker(_enemyUnit, _colour) + end + end + + else + -- env.info('LASE: No Enemies Nearby') + + -- stop lazing the old spot + ctld.cancelLase(_jtacGroupName) + -- env.info('Timer Slow timerSparkleLase '..jtacGroupName.." "..laserCode.." "..enemyUnit:getName()) + + timer.scheduleFunction(ctld.timerJTACAutoLase, { _jtacGroupName, _laserCode, _smoke, _lock, _colour, _radio }, timer.getTime() + 5) + end + + if targetLost then + ctld.notifyCoalition(_jtacGroupName .. ", target lost.", 10, _jtacUnit:getCoalition(), _radio) + elseif targetDestroyed then + ctld.notifyCoalition(_jtacGroupName .. ", target destroyed.", 10, _jtacUnit:getCoalition(), _radio) + end +end + +function ctld.JTACAutoLaseStop(_jtacGroupName) + ctld.jtacStop[_jtacGroupName] = true +end + +-- used by the timer function +function ctld.timerJTACAutoLase(_args) + + ctld.JTACAutoLase(_args[1], _args[2], _args[3], _args[4], _args[5], _args[6]) +end + +function ctld.cleanupJTAC(_jtacGroupName) + -- clear laser - just in case + ctld.cancelLase(_jtacGroupName) + + -- Cleanup + ctld.jtacUnits[_jtacGroupName] = nil + + ctld.jtacCurrentTargets[_jtacGroupName] = nil + + ctld.jtacRadioData[_jtacGroupName] = nil +end + + +--- send a message to the coalition +--- if _radio is set, the message will be read out loud via SRS +function ctld.notifyCoalition(_message, _displayFor, _side, _radio, _shortMessage) + ctld.logDebug(string.format("ctld.notifyCoalition(_message=%s)", ctld.p(_message))) + ctld.logTrace(string.format("_radio=%s", ctld.p(_radio))) + + local _shortMessage = _shortMessage + if _shortMessage == nil then + _shortMessage = _message + end + + if STTS and STTS.TextToSpeech and _radio and _radio.freq then + local _freq = _radio.freq + local _modulation = _radio.mod or "FM" + local _volume = _radio.volume or "1.0" + local _name = _radio.name or "JTAC" + local _gender = _radio.gender or "male" + local _culture = _radio.culture or "en-US" + local _voice = _radio.voice + local _googleTTS = _radio.googleTTS or false + ctld.logTrace(string.format("calling STTS.TextToSpeech(%s)", ctld.p(_shortMessage))) + ctld.logTrace(string.format("_freq=%s", ctld.p(_freq))) + ctld.logTrace(string.format("_modulation=%s", ctld.p(_modulation))) + ctld.logTrace(string.format("_volume=%s", ctld.p(_volume))) + ctld.logTrace(string.format("_name=%s", ctld.p(_name))) + ctld.logTrace(string.format("_gender=%s", ctld.p(_gender))) + ctld.logTrace(string.format("_culture=%s", ctld.p(_culture))) + ctld.logTrace(string.format("_voice=%s", ctld.p(_voice))) + ctld.logTrace(string.format("_googleTTS=%s", ctld.p(_googleTTS))) + STTS.TextToSpeech(_shortMessage, _freq, _modulation, _volume, _name, _side, nil, 1, _gender, _culture, _voice, _googleTTS) + end + + trigger.action.outTextForCoalition(_side, _message, _displayFor) + trigger.action.outSoundForCoalition(_side, "radiobeep.ogg") +end + +function ctld.createSmokeMarker(_enemyUnit, _colour) + + --recreate in 5 mins + ctld.jtacSmokeMarks[_enemyUnit:getName()] = timer.getTime() + 300.0 + + -- move smoke 2 meters above target for ease + local _enemyPoint = _enemyUnit:getPoint() + trigger.action.smoke({ x = _enemyPoint.x, y = _enemyPoint.y + 2.0, z = _enemyPoint.z }, _colour) +end + +function ctld.cancelLase(_jtacGroupName) + + --local index = "JTAC_"..jtacUnit:getID() + + local _tempLase = ctld.jtacLaserPoints[_jtacGroupName] + + if _tempLase ~= nil then + Spot.destroy(_tempLase) + ctld.jtacLaserPoints[_jtacGroupName] = nil + + -- env.info('Destroy laze '..index) + + _tempLase = nil + end + + local _tempIR = ctld.jtacIRPoints[_jtacGroupName] + + if _tempIR ~= nil then + Spot.destroy(_tempIR) + ctld.jtacIRPoints[_jtacGroupName] = nil + + -- env.info('Destroy laze '..index) + + _tempIR = nil + end +end + +function ctld.laseUnit(_enemyUnit, _jtacUnit, _jtacGroupName, _laserCode) + + --cancelLase(jtacGroupName) + + local _spots = {} + + local _enemyVector = _enemyUnit:getPoint() + local _enemyVectorUpdated = { x = _enemyVector.x, y = _enemyVector.y + 2.0, z = _enemyVector.z } + + local _oldLase = ctld.jtacLaserPoints[_jtacGroupName] + local _oldIR = ctld.jtacIRPoints[_jtacGroupName] + + if _oldLase == nil or _oldIR == nil then + + -- create lase + + local _status, _result = pcall(function() + _spots['irPoint'] = Spot.createInfraRed(_jtacUnit, { x = 0, y = 2.0, z = 0 }, _enemyVectorUpdated) + _spots['laserPoint'] = Spot.createLaser(_jtacUnit, { x = 0, y = 2.0, z = 0 }, _enemyVectorUpdated, _laserCode) + return _spots + end) + + if not _status then + env.error('ERROR: ' .. _result, false) + else + if _result.irPoint then + + -- env.info(jtacUnit:getName() .. ' placed IR Pointer on '..enemyUnit:getName()) + + ctld.jtacIRPoints[_jtacGroupName] = _result.irPoint --store so we can remove after + end + if _result.laserPoint then + + -- env.info(jtacUnit:getName() .. ' is Lasing '..enemyUnit:getName()..'. CODE:'..laserCode) + + ctld.jtacLaserPoints[_jtacGroupName] = _result.laserPoint + end + end + + else + + -- update lase + + if _oldLase ~= nil then + _oldLase:setPoint(_enemyVectorUpdated) + end + + if _oldIR ~= nil then + _oldIR:setPoint(_enemyVectorUpdated) + end + end +end + +-- get currently selected unit and check they're still in range +function ctld.getCurrentUnit(_jtacUnit, _jtacGroupName) + + + local _unit = nil + + if ctld.jtacCurrentTargets[_jtacGroupName] ~= nil then + _unit = Unit.getByName(ctld.jtacCurrentTargets[_jtacGroupName].name) + end + + local _tempPoint = nil + local _tempDist = nil + local _tempPosition = nil + + local _jtacPosition = _jtacUnit:getPosition() + local _jtacPoint = _jtacUnit:getPoint() + + if _unit ~= nil and _unit:getLife() > 0 and _unit:isActive() == true then + + -- calc distance + _tempPoint = _unit:getPoint() + -- tempPosition = unit:getPosition() + + _tempDist = ctld.getDistance(_unit:getPoint(), _jtacUnit:getPoint()) + if _tempDist < ctld.JTAC_maxDistance then + -- calc visible + + -- check slightly above the target as rounding errors can cause issues, plus the unit has some height anyways + local _offsetEnemyPos = { x = _tempPoint.x, y = _tempPoint.y + 2.0, z = _tempPoint.z } + local _offsetJTACPos = { x = _jtacPoint.x, y = _jtacPoint.y + 2.0, z = _jtacPoint.z } + + if land.isVisible(_offsetEnemyPos, _offsetJTACPos) then + return _unit + end + end + end + return nil +end + + +-- Find nearest enemy to JTAC that isn't blocked by terrain +function ctld.findNearestVisibleEnemy(_jtacUnit, _targetType,_distance) + + --local startTime = os.clock() + + local _maxDistance = _distance or ctld.JTAC_maxDistance + + local _nearestDistance = _maxDistance + + local _jtacPoint = _jtacUnit:getPoint() + local _coa = _jtacUnit:getCoalition() + + local _offsetJTACPos = { x = _jtacPoint.x, y = _jtacPoint.y + 2.0, z = _jtacPoint.z } + + local _volume = { + id = world.VolumeType.SPHERE, + params = { + point = _offsetJTACPos, + radius = _maxDistance + } + } + + local _unitList = {} + + + local _search = function(_unit, _coa) + pcall(function() + + if _unit ~= nil + and _unit:getLife() > 0 + and _unit:isActive() + and _unit:getCoalition() ~= _coa + and not _unit:inAir() + and not ctld.alreadyTarget(_jtacUnit,_unit) then + + local _tempPoint = _unit:getPoint() + local _offsetEnemyPos = { x = _tempPoint.x, y = _tempPoint.y + 2.0, z = _tempPoint.z } + + if land.isVisible(_offsetJTACPos,_offsetEnemyPos ) then + + local _dist = ctld.getDistance(_offsetJTACPos, _offsetEnemyPos) + + if _dist < _maxDistance then + table.insert(_unitList,{unit=_unit, dist=_dist}) + + end + end + end + end) + + return true + end + + world.searchObjects(Object.Category.UNIT, _volume, _search, _coa) + + --log.info(string.format("JTAC Search elapsed time: %.4f\n", os.clock() - startTime)) + + -- generate list order by distance & visible + + -- first check + -- hpriority + -- priority + -- vehicle + -- unit + + local _sort = function( a,b ) return a.dist < b.dist end + table.sort(_unitList,_sort) + -- sort list + + -- check for hpriority + for _, _enemyUnit in ipairs(_unitList) do + local _enemyName = _enemyUnit.unit:getName() + + if string.match(_enemyName, "hpriority") then + return _enemyUnit.unit + end + end + + for _, _enemyUnit in ipairs(_unitList) do + local _enemyName = _enemyUnit.unit:getName() + + if string.match(_enemyName, "priority") then + return _enemyUnit.unit + end + end + + local result = nil + for _, _enemyUnit in ipairs(_unitList) do + local _enemyName = _enemyUnit.unit:getName() + --log.info(string.format("CTLD - checking _enemyName=%s", _enemyName)) + + -- check for air defenses + --log.info(string.format("CTLD - _enemyUnit.unit:getDesc()[attributes]=%s", ctld.p(_enemyUnit.unit:getDesc()["attributes"]))) + local airdefense = (_enemyUnit.unit:getDesc()["attributes"]["Air Defence"] ~= nil) + --log.info(string.format("CTLD - airdefense=%s", tostring(airdefense))) + + if (_targetType == "vehicle" and ctld.isVehicle(_enemyUnit.unit)) or _targetType == "all" then + if airdefense then + return _enemyUnit.unit + else + result = _enemyUnit.unit + end + + elseif (_targetType == "troop" and ctld.isInfantry(_enemyUnit.unit)) or _targetType == "all" then + if airdefense then + return _enemyUnit.unit + else + result = _enemyUnit.unit + end + end + end + + return result + +end + + +function ctld.listNearbyEnemies(_jtacUnit) + + local _maxDistance = ctld.JTAC_maxDistance + + local _jtacPoint = _jtacUnit:getPoint() + local _coa = _jtacUnit:getCoalition() + + local _offsetJTACPos = { x = _jtacPoint.x, y = _jtacPoint.y + 2.0, z = _jtacPoint.z } + + local _volume = { + id = world.VolumeType.SPHERE, + params = { + point = _offsetJTACPos, + radius = _maxDistance + } + } + local _enemies = nil + + local _search = function(_unit, _coa) + pcall(function() + + if _unit ~= nil + and _unit:getLife() > 0 + and _unit:isActive() + and _unit:getCoalition() ~= _coa + and not _unit:inAir() then + + local _tempPoint = _unit:getPoint() + local _offsetEnemyPos = { x = _tempPoint.x, y = _tempPoint.y + 2.0, z = _tempPoint.z } + + if land.isVisible(_offsetJTACPos,_offsetEnemyPos ) then + + if not _enemies then + _enemies = {} + end + + _enemies[_unit:getTypeName()] = _unit:getTypeName() + + end + end + end) + + return true + end + + world.searchObjects(Object.Category.UNIT, _volume, _search, _coa) + + return _enemies +end + +-- tests whether the unit is targeted by another JTAC +function ctld.alreadyTarget(_jtacUnit, _enemyUnit) + + for _, _jtacTarget in pairs(ctld.jtacCurrentTargets) do + + if _jtacTarget.unitId == _enemyUnit:getID() then + -- env.info("ALREADY TARGET") + return true + end + end + + return false +end + + +-- Returns only alive units from group but the group / unit may not be active + +function ctld.getGroup(groupName) + + local _groupUnits = Group.getByName(groupName) + + local _filteredUnits = {} --contains alive units + local _x = 1 + + if _groupUnits ~= nil and _groupUnits:isExist() then + + _groupUnits = _groupUnits:getUnits() + + if _groupUnits ~= nil and #_groupUnits > 0 then + for _x = 1, #_groupUnits do + if _groupUnits[_x]:getLife() > 0 then -- removed and _groupUnits[_x]:isExist() as isExist doesnt work on single units! + table.insert(_filteredUnits, _groupUnits[_x]) + end + end + end + end + + return _filteredUnits +end + +function ctld.getAliveGroup(_groupName) + + local _group = Group.getByName(_groupName) + + if _group and _group:isExist() == true and #_group:getUnits() > 0 then + return _group + end + + return nil +end + +-- gets the JTAC status and displays to coalition units +function ctld.getJTACStatus(_args) + + --returns the status of all JTAC units + + local _playerUnit = ctld.getTransportUnit(_args[1]) + + if _playerUnit == nil then + return + end + + local _side = _playerUnit:getCoalition() + + local _jtacGroupName = nil + local _jtacUnit = nil + + local _message = "JTAC STATUS: \n\n" + + for _jtacGroupName, _jtacDetails in pairs(ctld.jtacUnits) do + + --look up units + _jtacUnit = Unit.getByName(_jtacDetails.name) + + if _jtacUnit ~= nil and _jtacUnit:getLife() > 0 and _jtacUnit:isActive() == true and _jtacUnit:getCoalition() == _side then + + local _enemyUnit = ctld.getCurrentUnit(_jtacUnit, _jtacGroupName) + + local _laserCode = ctld.jtacLaserPointCodes[_jtacGroupName] + + local _start = _jtacGroupName + if (_jtacDetails.radio) then + _start = _start .. ", available on ".._jtacDetails.radio.freq.." ".._jtacDetails.radio.mod .."," + end + + if _laserCode == nil then + _laserCode = "UNKNOWN" + end + + if _enemyUnit ~= nil and _enemyUnit:getLife() > 0 and _enemyUnit:isActive() == true then + _message = _message .. "" .. _start .. " targeting " .. _enemyUnit:getTypeName() .. " CODE: " .. _laserCode .. ctld.getPositionString(_enemyUnit) .. "\n" + + local _list = ctld.listNearbyEnemies(_jtacUnit) + + if _list then + _message = _message.."Visual On: " + + for _,_type in pairs(_list) do + _message = _message.._type.." " + end + _message = _message.."\n" + end + + else + _message = _message .. "" .. _start .. " searching for targets" .. ctld.getPositionString(_jtacUnit) .. "\n" + end + end + end + + if _message == "JTAC STATUS: \n\n" then + _message = "No Active JTACs" + end + + + ctld.notifyCoalition(_message, 10, _side) +end + + + +function ctld.isInfantry(_unit) + + local _typeName = _unit:getTypeName() + + --type coerce tostring + _typeName = string.lower(_typeName .. "") + + local _soldierType = { "infantry", "paratrooper", "stinger", "manpad", "mortar" } + + for _key, _value in pairs(_soldierType) do + if string.match(_typeName, _value) then + return true + end + end + + return false +end + +-- assume anything that isnt soldier is vehicle +function ctld.isVehicle(_unit) + + if ctld.isInfantry(_unit) then + return false + end + + return true +end + +-- The entered value can range from 1111 - 1788, +-- -- but the first digit of the series must be a 1 or 2 +-- -- and the last three digits must be between 1 and 8. +-- The range used to be bugged so its not 1 - 8 but 0 - 7. +-- function below will use the range 1-7 just incase +function ctld.generateLaserCode() + + ctld.jtacGeneratedLaserCodes = {} + + -- generate list of laser codes + local _code = 1111 + + local _count = 1 + + while _code < 1777 and _count < 30 do + + while true do + + _code = _code + 1 + + if not ctld.containsDigit(_code, 8) + and not ctld.containsDigit(_code, 9) + and not ctld.containsDigit(_code, 0) then + + table.insert(ctld.jtacGeneratedLaserCodes, _code) + + --env.info(_code.." Code") + break + end + end + _count = _count + 1 + end +end + +function ctld.containsDigit(_number, _numberToFind) + + local _thisNumber = _number + local _thisDigit = 0 + + while _thisNumber ~= 0 do + + _thisDigit = _thisNumber % 10 + _thisNumber = math.floor(_thisNumber / 10) + + if _thisDigit == _numberToFind then + return true + end + end + + return false +end + +-- 200 - 400 in 10KHz +-- 400 - 850 in 10 KHz +-- 850 - 1250 in 50 KHz +function ctld.generateVHFrequencies() + + --ignore list + --list of all frequencies in KHZ that could conflict with + -- 191 - 1290 KHz, beacon range + local _skipFrequencies = { + 745, --Astrahan + 381, + 384, + 300.50, + 312.5, + 1175, + 342, + 735, + 300.50, + 353.00, + 440, + 795, + 525, + 520, + 690, + 625, + 291.5, + 300.50, + 435, + 309.50, + 920, + 1065, + 274, + 312.50, + 580, + 602, + 297.50, + 750, + 485, + 950, + 214, + 1025, 730, 995, 455, 307, 670, 329, 395, 770, + 380, 705, 300.5, 507, 740, 1030, 515, + 330, 309.5, + 348, 462, 905, 352, 1210, 942, 435, + 324, + 320, 420, 311, 389, 396, 862, 680, 297.5, + 920, 662, + 866, 907, 309.5, 822, 515, 470, 342, 1182, 309.5, 720, 528, + 337, 312.5, 830, 740, 309.5, 641, 312, 722, 682, 1050, + 1116, 935, 1000, 430, 577, + 326 -- Nevada + } + + ctld.freeVHFFrequencies = {} + local _start = 200000 + + -- first range + while _start < 400000 do + + -- skip existing NDB frequencies + local _found = false + for _, value in pairs(_skipFrequencies) do + if value * 1000 == _start then + _found = true + break + end + end + + + if _found == false then + table.insert(ctld.freeVHFFrequencies, _start) + end + + _start = _start + 10000 + end + + _start = 400000 + -- second range + while _start < 850000 do + + -- skip existing NDB frequencies + local _found = false + for _, value in pairs(_skipFrequencies) do + if value * 1000 == _start then + _found = true + break + end + end + + if _found == false then + table.insert(ctld.freeVHFFrequencies, _start) + end + + + _start = _start + 10000 + end + + _start = 850000 + -- third range + while _start <= 1250000 do + + -- skip existing NDB frequencies + local _found = false + for _, value in pairs(_skipFrequencies) do + if value * 1000 == _start then + _found = true + break + end + end + + if _found == false then + table.insert(ctld.freeVHFFrequencies, _start) + end + + _start = _start + 50000 + end +end + +-- 220 - 399 MHZ, increments of 0.5MHZ +function ctld.generateUHFrequencies() + + ctld.freeUHFFrequencies = {} + local _start = 220000000 + + while _start < 399000000 do + table.insert(ctld.freeUHFFrequencies, _start) + _start = _start + 500000 + end +end + + +-- 220 - 399 MHZ, increments of 0.5MHZ +-- -- first digit 3-7MHz +-- -- second digit 0-5KHz +-- -- third digit 0-9 +-- -- fourth digit 0 or 5 +-- -- times by 10000 +-- +function ctld.generateFMFrequencies() + + ctld.freeFMFrequencies = {} + local _start = 220000000 + + while _start < 399000000 do + + _start = _start + 500000 + end + + for _first = 3, 7 do + for _second = 0, 5 do + for _third = 0, 9 do + local _frequency = ((100 * _first) + (10 * _second) + _third) * 100000 --extra 0 because we didnt bother with 4th digit + table.insert(ctld.freeFMFrequencies, _frequency) + end + end + end +end + +function ctld.getPositionString(_unit) + + if ctld.JTAC_location == false then + return "" + end + + local _lat, _lon = coord.LOtoLL(_unit:getPosition().p) + + local _latLngStr = mist.tostringLL(_lat, _lon, 3, ctld.location_DMS) + + local _mgrsString = mist.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(_unit:getPosition().p)), 5) + + return " @ " .. _latLngStr .. " - MGRS " .. _mgrsString +end + + +-- ***************** SETUP SCRIPT **************** +function ctld.initialize(force) + ctld.logInfo(string.format("Initializing version %s", ctld.Version)) + ctld.logTrace(string.format("ctld.alreadyInitialized=%s", ctld.p(ctld.alreadyInitialized))) + ctld.logTrace(string.format("force=%s", ctld.p(force))) + + if ctld.alreadyInitialized and not force then + ctld.logInfo(string.format("Bypassing initialization because ctld.alreadyInitialized = true")) + return + end + + assert(mist ~= nil, "\n\n** HEY MISSION-DESIGNER! **\n\nMiST has not been loaded!\n\nMake sure MiST 3.6 or higher is running\n*before* running this script!\n") + + ctld.addedTo = {} + ctld.spawnedCratesRED = {} -- use to store crates that have been spawned + ctld.spawnedCratesBLUE = {} -- use to store crates that have been spawned + + ctld.droppedTroopsRED = {} -- stores dropped troop groups + ctld.droppedTroopsBLUE = {} -- stores dropped troop groups + + ctld.droppedVehiclesRED = {} -- stores vehicle groups for c-130 / hercules + ctld.droppedVehiclesBLUE = {} -- stores vehicle groups for c-130 / hercules + + ctld.inTransitTroops = {} + + ctld.inTransitFOBCrates = {} + + ctld.inTransitSlingLoadCrates = {} -- stores crates that are being transported by helicopters for alternative to real slingload + + ctld.droppedFOBCratesRED = {} + ctld.droppedFOBCratesBLUE = {} + + ctld.builtFOBS = {} -- stores fully built fobs + + ctld.completeAASystems = {} -- stores complete spawned groups from multiple crates + + ctld.fobBeacons = {} -- stores FOB radio beacon details, refreshed every 60 seconds + + ctld.deployedRadioBeacons = {} -- stores details of deployed radio beacons + + ctld.beaconCount = 1 + + ctld.usedUHFFrequencies = {} + ctld.usedVHFFrequencies = {} + ctld.usedFMFrequencies = {} + + ctld.freeUHFFrequencies = {} + ctld.freeVHFFrequencies = {} + ctld.freeFMFrequencies = {} + + --used to lookup what the crate will contain + ctld.crateLookupTable = {} + + ctld.extractZones = {} -- stored extract zones + + ctld.missionEditorCargoCrates = {} --crates added by mission editor for triggering cratesinzone + ctld.hoverStatus = {} -- tracks status of a helis hover above a crate + + ctld.callbacks = {} -- function callback + + + -- Remove intransit troops when heli / cargo plane dies + --ctld.eventHandler = {} + --function ctld.eventHandler:onEvent(_event) + -- + -- if _event == nil or _event.initiator == nil then + -- env.info("CTLD null event") + -- elseif _event.id == 9 then + -- -- Pilot dead + -- ctld.inTransitTroops[_event.initiator:getName()] = nil + -- + -- elseif world.event.S_EVENT_EJECTION == _event.id or _event.id == 8 then + -- -- env.info("Event unit - Pilot Ejected or Unit Dead") + -- ctld.inTransitTroops[_event.initiator:getName()] = nil + -- + -- -- env.info(_event.initiator:getName()) + -- end + -- + --end + + -- create crate lookup table + for _subMenuName, _crates in pairs(ctld.spawnableCrates) do + + for _, _crate in pairs(_crates) do + -- convert number to string otherwise we'll have a pointless giant + -- table. String means 'hashmap' so it will only contain the right number of elements + ctld.crateLookupTable[tostring(_crate.weight)] = _crate + end + end + + + --sort out pickup zones + for _, _zone in pairs(ctld.pickupZones) do + + local _zoneName = _zone[1] + local _zoneColor = _zone[2] + local _zoneActive = _zone[4] + + if _zoneColor == "green" then + _zone[2] = trigger.smokeColor.Green + elseif _zoneColor == "red" then + _zone[2] = trigger.smokeColor.Red + elseif _zoneColor == "white" then + _zone[2] = trigger.smokeColor.White + elseif _zoneColor == "orange" then + _zone[2] = trigger.smokeColor.Orange + elseif _zoneColor == "blue" then + _zone[2] = trigger.smokeColor.Blue + else + _zone[2] = -1 -- no smoke colour + end + + -- add in counter for troops or units + if _zone[3] == -1 then + _zone[3] = 10000; + end + + -- change active to 1 / 0 + if _zoneActive == "yes" then + _zone[4] = 1 + else + _zone[4] = 0 + end + end + + --sort out dropoff zones + for _, _zone in pairs(ctld.dropOffZones) do + + local _zoneColor = _zone[2] + + if _zoneColor == "green" then + _zone[2] = trigger.smokeColor.Green + elseif _zoneColor == "red" then + _zone[2] = trigger.smokeColor.Red + elseif _zoneColor == "white" then + _zone[2] = trigger.smokeColor.White + elseif _zoneColor == "orange" then + _zone[2] = trigger.smokeColor.Orange + elseif _zoneColor == "blue" then + _zone[2] = trigger.smokeColor.Blue + else + _zone[2] = -1 -- no smoke colour + end + + --mark as active for refresh smoke logic to work + _zone[4] = 1 + end + + --sort out waypoint zones + for _, _zone in pairs(ctld.wpZones) do + + local _zoneColor = _zone[2] + + if _zoneColor == "green" then + _zone[2] = trigger.smokeColor.Green + elseif _zoneColor == "red" then + _zone[2] = trigger.smokeColor.Red + elseif _zoneColor == "white" then + _zone[2] = trigger.smokeColor.White + elseif _zoneColor == "orange" then + _zone[2] = trigger.smokeColor.Orange + elseif _zoneColor == "blue" then + _zone[2] = trigger.smokeColor.Blue + else + _zone[2] = -1 -- no smoke colour + end + + --mark as active for refresh smoke logic to work + -- change active to 1 / 0 + if _zone[3] == "yes" then + _zone[3] = 1 + else + _zone[3] = 0 + end + end + + -- Sort out extractable groups + for _, _groupName in pairs(ctld.extractableGroups) do + + local _group = Group.getByName(_groupName) + + if _group ~= nil then + + if _group:getCoalition() == 1 then + table.insert(ctld.droppedTroopsRED, _group:getName()) + else + table.insert(ctld.droppedTroopsBLUE, _group:getName()) + end + end + end + + + -- Seperate troop teams into red and blue for random AI pickups + if ctld.allowRandomAiTeamPickups == true then + ctld.redTeams = {} + ctld.blueTeams = {} + for _,_loadGroup in pairs(ctld.loadableGroups) do + if not _loadGroup.side then + table.insert(ctld.redTeams, _) + table.insert(ctld.blueTeams, _) + elseif _loadGroup.side == 1 then + table.insert(ctld.redTeams, _) + elseif _loadGroup.side == 2 then + table.insert(ctld.blueTeams, _) + end + end + end + + -- add total count + + for _,_loadGroup in pairs(ctld.loadableGroups) do + + _loadGroup.total = 0 + if _loadGroup.aa then + _loadGroup.total = _loadGroup.aa + _loadGroup.total + end + + if _loadGroup.inf then + _loadGroup.total = _loadGroup.inf + _loadGroup.total + end + + + if _loadGroup.mg then + _loadGroup.total = _loadGroup.mg + _loadGroup.total + end + + if _loadGroup.at then + _loadGroup.total = _loadGroup.at + _loadGroup.total + end + + if _loadGroup.mortar then + _loadGroup.total = _loadGroup.mortar + _loadGroup.total + end + + end + + + -- Scheduled functions (run cyclically) -- but hold execution for a second so we can override parts + + timer.scheduleFunction(ctld.checkAIStatus, nil, timer.getTime() + 1) + timer.scheduleFunction(ctld.checkTransportStatus, nil, timer.getTime() + 5) + + timer.scheduleFunction(function() + + timer.scheduleFunction(ctld.refreshRadioBeacons, nil, timer.getTime() + 5) + timer.scheduleFunction(ctld.refreshSmoke, nil, timer.getTime() + 5) + timer.scheduleFunction(ctld.addF10MenuOptions, nil, timer.getTime() + 5) + + if ctld.enableCrates == true and ctld.slingLoad == false and ctld.hoverPickup == true then + timer.scheduleFunction(ctld.checkHoverStatus, nil, timer.getTime() + 1) + end + + end,nil, timer.getTime()+1 ) + + --event handler for deaths + --world.addEventHandler(ctld.eventHandler) + + --env.info("CTLD event handler added") + + env.info("Generating Laser Codes") + ctld.generateLaserCode() + env.info("Generated Laser Codes") + + + + env.info("Generating UHF Frequencies") + ctld.generateUHFrequencies() + env.info("Generated UHF Frequencies") + + env.info("Generating VHF Frequencies") + ctld.generateVHFrequencies() + env.info("Generated VHF Frequencies") + + + env.info("Generating FM Frequencies") + ctld.generateFMFrequencies() + env.info("Generated FM Frequencies") + + -- Search for crates + -- Crates are NOT returned by coalition.getStaticObjects() for some reason + -- Search for crates in the mission editor instead + env.info("Searching for Crates") + for _coalitionName, _coalitionData in pairs(env.mission.coalition) do + + if (_coalitionName == 'red' or _coalitionName == 'blue') + and type(_coalitionData) == 'table' then + if _coalitionData.country then --there is a country table + for _, _countryData in pairs(_coalitionData.country) do + + if type(_countryData) == 'table' then + for _objectTypeName, _objectTypeData in pairs(_countryData) do + if _objectTypeName == "static" then + + if ((type(_objectTypeData) == 'table') + and _objectTypeData.group + and (type(_objectTypeData.group) == 'table') + and (#_objectTypeData.group > 0)) then + + for _groupId, _group in pairs(_objectTypeData.group) do + if _group and _group.units and type(_group.units) == 'table' then + for _unitNum, _unit in pairs(_group.units) do + if _unit.canCargo == true then + local _cargoName = env.getValueDictByKey(_unit.name) + ctld.missionEditorCargoCrates[_cargoName] = _cargoName + env.info("Crate Found: " .. _unit.name.." - Unit: ".._cargoName) + end + end + end + end + end + end + end + end + end + end + end + end + env.info("END search for crates") + + -- don't initialize more than once + ctld.alreadyInitialized = true + + env.info("CTLD READY") +end + + +-- initialize the random number generator to make it almost random +math.random(); math.random(); math.random() + +--- Enable/Disable error boxes displayed on screen. +env.setErrorMessageBoxEnabled(false) + +-- initialize CTLD in 2 seconds, so other scripts have a chance to modify the configuration before initialization +ctld.logInfo(string.format("Loading version %s in 2 seconds", ctld.Version)) +timer.scheduleFunction(ctld.initialize, nil, timer.getTime() + 2) + +--DEBUG FUNCTION +-- for key, value in pairs(getmetatable(_spawnedCrate)) do +-- env.info(tostring(key)) +-- env.info(tostring(value)) +-- end diff --git a/RotaryOps.lua b/RotaryOps.lua index eabe268..856b973 100644 --- a/RotaryOps.lua +++ b/RotaryOps.lua @@ -11,6 +11,10 @@ local function dispMsg(text) return text end +local function tableHasKey(table,key) + return table[key] ~= nil +end + local function getObjectVolume(obj) local length = (obj:getDesc().box.max.x + math.abs(obj:getDesc().box.min.x)) local height = (obj:getDesc().box.max.y + math.abs(obj:getDesc().box.min.y)) @@ -105,13 +109,13 @@ function RotaryOps.chargeEnemy(vars) end world.searchObjects(Object.Category.UNIT, volS, ifFound) mist.goRoute(grp, path) - local id = timer.scheduleFunction(RotaryOps.chargeEnemy, vars, timer.getTime() + 60) + local id = timer.scheduleFunction(RotaryOps.chargeEnemy, vars, timer.getTime() + math.random(50,70)) end function RotaryOps.patrolRadius(vars) - --trigger.action.outText("patrol radius: "..mist.utils.tableShow(vars), 5) + trigger.action.outText("patrol radius: "..mist.utils.tableShow(vars), 5) local grp = vars.grp local search_radius = vars.radius or 100 local first_valid_unit @@ -173,9 +177,163 @@ function RotaryOps.patrolRadius(vars) --trigger.action.outText("new waypoints created: "..(#path - 1), 5) mist.goRoute(grp, path) --local timing = mist.getPathLength(path) / 5 - local id = timer.scheduleFunction(RotaryOps.patrolRadius, vars, timer.getTime() + 60) + local id = timer.scheduleFunction(RotaryOps.patrolRadius, vars, timer.getTime() + math.random(50,70)) + +end + + +function RotaryOps.knowEnemy(vars) + --trigger.action.outText("charge enemies: "..mist.utils.tableShow(vars), 5) + local grp = vars.grp + local search_radius = vars.radius or 5000 + ---- + local first_valid_unit + if grp:isExist() ~= true then return end + for index, unit in pairs(grp:getUnits()) + do + if unit:isExist() == true then + first_valid_unit = unit + break + else --trigger.action.outText("a unit no longer exists", 15) + end + end + ---- + + if first_valid_unit == nil then return end + local start_point = first_valid_unit:getPoint() + if not vars.spawn_point then vars.spawn_point = start_point end + + local enemy_coal + if grp:getCoalition() == 1 then enemy_coal = 2 end + if grp:getCoalition() == 2 then enemy_coal = 1 end + + --local sphere = trigger.misc.getZone('town') + local volS = { + id = world.VolumeType.SPHERE, + params = { + point = grp:getUnit(1):getPoint(), --check if exists, maybe itterate through grp + radius = search_radius + } + } + local enemy_unit + local path = {} + local ifFound = function(foundItem, val) + --trigger.action.outText("found item: "..foundItem:getTypeName(), 5) + if foundItem:getCoalition() == enemy_coal then + enemy_unit = foundItem + trigger.action.outText("found enemy! "..foundItem:getTypeName(), 5) + + path[1] = mist.ground.buildWP(start_point, '', 5) + path[2] = mist.ground.buildWP(enemy_unit:getPoint(), '', 5) + --path[3] = mist.ground.buildWP(vars.spawn_point, '', 5) + grp:getUnit(1):getController():knowTarget(enemy_unit, true, true) + + + else + + --trigger.action.outText("object found is not enemy inf in "..search_radius, 5) + end + + return true + end + --default path if no units found + if false then + trigger.action.outText("group going back to origin", 5) + path[1] = mist.ground.buildWP(start_point, '', 5) + path[2] = mist.ground.buildWP(vars.spawn_point, '', 5) + + end + world.searchObjects(Object.Category.UNIT, volS, ifFound) + --mist.goRoute(grp, path) + local id = timer.scheduleFunction(RotaryOps.knowEnemy, vars, timer.getTime() + 15) + +end + + +------------------------------------------ + + + +RotaryOps.zones = {} +RotaryOps.active_zone = 'ALPHA' + +function RotaryOps.sortOutInfantry(mixed_units) + local _infantry = {} + local _not_infantry = {} + for index, unit in pairs(mixed_units) + do + if unit:hasAttribute("Infantry") then + _infantry[#_infantry + 1] = unit + else _not_infantry[#_not_infantry + 1] = unit + end + end + return {infantry = _infantry, not_infantry = _not_infantry} +end + +function RotaryOps.assessUnitsInZone(var) + --consider adding other unit types + local red_ground_units = mist.getUnitsInZones(mist.makeUnitTable({'[red][vehicle]'}), {RotaryOps.active_zone}) --consider adding other unit types + local red_infantry = RotaryOps.sortOutInfantry(red_ground_units).infantry + local red_vehicles = RotaryOps.sortOutInfantry(red_ground_units).not_infantry + local blue_ground_units = mist.getUnitsInZones(mist.makeUnitTable({'[blue][vehicle]'}), {RotaryOps.active_zone}) --consider adding other unit types + local blue_infantry = RotaryOps.sortOutInfantry(blue_ground_units).infantry + local blue_vehicles = RotaryOps.sortOutInfantry(blue_ground_units).not_infantry + + trigger.action.outText("["..RotaryOps.active_zone .. "] RED: " ..#red_infantry.. " infantry, " .. #red_vehicles .. " vehicles. BLUE: "..#blue_infantry.. " infantry, " .. #blue_vehicles.." vehicles.", 5, true) + local id = timer.scheduleFunction(RotaryOps.assessUnitsInZone, 1, timer.getTime() + 5) +end +local id = timer.scheduleFunction(RotaryOps.assessUnitsInZone, 1, timer.getTime() + 5) + + +function RotaryOps.drawZones(zones) --should be drawZones and itterate through all zones, getting the active zone, and incrementing id + local previous_point + for index, zone in pairs(zones) + do + local point = trigger.misc.getZone(zone.outter_zone_name).point + local radius = trigger.misc.getZone(zone.outter_zone_name).radius + local coalition = -1 + local id = index --this must be UNIQUE! + local color = {1, 1, 1, 0.5} + local fill_color = {1, 1, 1, 0.1} + local text_fill_color = {0, 0, 0, 0} + local line_type = 5 --1 Solid 2 Dashed 3 Dotted 4 Dot Dash 5 Long Dash 6 Two Dash + local font_size = 20 + local read_only = false + local text = zone.outter_zone_name + if zone.outter_zone_name == RotaryOps.active_zone then + color = {1, 1, 1, 0.5} + fill_color = {1, 0, 1, 0.1} + end + if previous_point ~= nill then + trigger.action.lineToAll(coalition, id + 200, point, previous_point, color, line_type) + end + previous_point = point + trigger.action.outText("drawing map circle", 5) + trigger.action.circleToAll(coalition, id, point, radius, color, fill_color, line_type) + trigger.action.textToAll(coalition, id + 100, point, color, text_fill_color, font_size, read_only, text) + end + end + +function RotaryOps.addZone(_outter_zone_name, _vars, group_id) --todo: implement zone group ids + group_id = group_id or 1 + table.insert(RotaryOps.zones, {outter_zone_name = _outter_zone_name, vars = _vars}) + RotaryOps.drawZones(RotaryOps.zones) + ctld.dropOffZones[#ctld.dropOffZones + 1] = { _outter_zone_name, "green", 0 } + --trigger.action.outText("zones: ".. mist.utils.tableShow(RotaryOps.zones), 5) +end + +--[[ +vars = { +inner_zone = '', +infantry_spawn = 10, +infantry_respawn = 50, +infantry_spawn_zone = '' +defender_coal = 'red' +} +]] + diff --git a/ScriptLoader.lua b/ScriptLoader.lua index 193c1bc..fee98db 100644 --- a/ScriptLoader.lua +++ b/ScriptLoader.lua @@ -1,4 +1,6 @@ -assert(loadfile("C:\\Users\\spenc\\OneDrive\\Documents\\Eclipe_LDT\\dcs splash damage\\src\\mist.lua"))() +--assert(loadfile("C:\\Users\\spenc\\OneDrive\\Documents\\Eclipe_LDT\\dcs splash damage\\src\\mist.lua"))() +assert(loadfile("C:\\Users\\spenc\\OneDrive\\Documents\\Eclipe_LDT\\Rotary Ops\\src\\mist_4_4_90.lua"))() --assert(loadfile("C:\\Users\\spenc\\OneDrive\\Documents\\Eclipe_LDT\\Rotary Ops\\src\\RotaryOps.lua"))() +assert(loadfile("C:\\Users\\spenc\\OneDrive\\Documents\\Eclipe_LDT\\Rotary Ops\\src\\CTLD.lua"))() assert(loadfile("C:\\Users\\spenc\\OneDrive\\Documents\\Eclipe_LDT\\Rotary Ops\\src\\RotaryOps.lua"))() --assert(require("RotaryOps")) \ No newline at end of file diff --git a/mist_4_4_90.lua b/mist_4_4_90.lua new file mode 100644 index 0000000..fbf4f6a --- /dev/null +++ b/mist_4_4_90.lua @@ -0,0 +1,7347 @@ +--[[-- +MIST Mission Scripting Tools. +## Description: +MIssion Scripting Tools (MIST) is a collection of Lua functions +and databases that is intended to be a supplement to the standard +Lua functions included in the simulator scripting engine. + +MIST functions and databases provide ready-made solutions to many common +scripting tasks and challenges, enabling easier scripting and saving +mission scripters time. The table mist.flagFuncs contains a set of +Lua functions (that are similar to Slmod functions) that do not +require detailed Lua knowledge to use. + +However, the majority of MIST does require knowledge of the Lua language, +and, if you are going to utilize these components of MIST, it is necessary +that you read the Simulator Scripting Engine guide on the official ED wiki. + +## Links: + +ED Forum Thread: + +##Github: + +Development + +Official Releases + +@script MIST +@author Speed +@author Grimes +@author lukrop +]] +mist = {} + +-- don't change these +mist.majorVersion = 4 +mist.minorVersion = 4 +mist.build = 90 + +-- forward declaration of log shorthand +local log + +local mistSettings = { + errorPopup = false, -- errors printed by mist logger will create popup warning you + warnPopup = false, + infoPopup = false, + logLevel = 'warn', +} + +do -- the main scope + local coroutines = {} + + local tempSpawnedUnits = {} -- birth events added here + local tempSpawnedGroups = {} + local tempSpawnGroupsCounter = 0 + + local mistAddedObjects = {} -- mist.dynAdd unit data added here + local mistAddedGroups = {} -- mist.dynAdd groupdata added here + local writeGroups = {} + local lastUpdateTime = 0 + + local updateAliveUnitsCounter = 0 + local updateTenthSecond = 0 + + local mistGpId = 7000 + local mistUnitId = 7000 + local mistDynAddIndex = {[' air '] = 0, [' hel '] = 0, [' gnd '] = 0, [' bld '] = 0, [' static '] = 0, [' shp '] = 0} + + local scheduledTasks = {} + local taskId = 0 + local idNum = 0 + + mist.nextGroupId = 1 + mist.nextUnitId = 1 + + local dbLog + + local function initDBs() -- mist.DBs scope + mist.DBs = {} + + mist.DBs.missionData = {} + if env.mission then + + mist.DBs.missionData.startTime = env.mission.start_time + mist.DBs.missionData.theatre = env.mission.theatre + mist.DBs.missionData.version = env.mission.version + mist.DBs.missionData.files = {} + if type(env.mission.resourceCounter) == 'table' then + for fIndex, fData in pairs (env.mission.resourceCounter) do + mist.DBs.missionData.files[#mist.DBs.missionData.files + 1] = mist.utils.deepCopy(fIndex) + end + end + -- if we add more coalition specific data then bullsye should be categorized by coaliton. For now its just the bullseye table + mist.DBs.missionData.bullseye = {} + end + + mist.DBs.zonesByName = {} + mist.DBs.zonesByNum = {} + + + if env.mission.triggers and env.mission.triggers.zones then + for zone_ind, zone_data in pairs(env.mission.triggers.zones) do + if type(zone_data) == 'table' then + local zone = mist.utils.deepCopy(zone_data) + zone.point = {} -- point is used by SSE + zone.point.x = zone_data.x + zone.point.y = 0 + zone.point.z = zone_data.y + + mist.DBs.zonesByName[zone_data.name] = zone + mist.DBs.zonesByNum[#mist.DBs.zonesByNum + 1] = mist.utils.deepCopy(zone) --[[deepcopy so that the zone in zones_by_name and the zone in + zones_by_num se are different objects.. don't want them linked.]] + end + end + end + + mist.DBs.navPoints = {} + mist.DBs.units = {} + --Build mist.db.units and mist.DBs.navPoints + for coa_name_miz, coa_data in pairs(env.mission.coalition) do + local coa_name = coa_name_miz + if string.lower(coa_name_miz) == 'neutrals' then + coa_name = 'neutral' + end + if type(coa_data) == 'table' then + mist.DBs.units[coa_name] = {} + + if coa_data.bullseye then + mist.DBs.missionData.bullseye[coa_name] = {} + mist.DBs.missionData.bullseye[coa_name].x = coa_data.bullseye.x + mist.DBs.missionData.bullseye[coa_name].y = coa_data.bullseye.y + end + -- build nav points DB + mist.DBs.navPoints[coa_name] = {} + if coa_data.nav_points then --navpoints + --mist.debug.writeData (mist.utils.serialize,{'NavPoints',coa_data.nav_points}, 'NavPoints.txt') + for nav_ind, nav_data in pairs(coa_data.nav_points) do + + if type(nav_data) == 'table' then + mist.DBs.navPoints[coa_name][nav_ind] = mist.utils.deepCopy(nav_data) + + mist.DBs.navPoints[coa_name][nav_ind].name = nav_data.callsignStr -- name is a little bit more self-explanatory. + mist.DBs.navPoints[coa_name][nav_ind].point = {} -- point is used by SSE, support it. + mist.DBs.navPoints[coa_name][nav_ind].point.x = nav_data.x + mist.DBs.navPoints[coa_name][nav_ind].point.y = 0 + mist.DBs.navPoints[coa_name][nav_ind].point.z = nav_data.y + end + end + end + if coa_data.country then --there is a country table + for cntry_id, cntry_data in pairs(coa_data.country) do + + local countryName = string.lower(cntry_data.name) + mist.DBs.units[coa_name][countryName] = {} + mist.DBs.units[coa_name][countryName].countryId = cntry_data.id + + if type(cntry_data) == 'table' then --just making sure + + 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" or obj_type_name == "static" then --should be an unncessary check + + local category = obj_type_name + + 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! + + mist.DBs.units[coa_name][countryName][category] = {} + + for group_num, group_data in pairs(obj_type_data.group) do + + if group_data and group_data.units and type(group_data.units) == 'table' then --making sure again- this is a valid group + + mist.DBs.units[coa_name][countryName][category][group_num] = {} + local groupName = group_data.name + if env.mission.version > 7 then + groupName = env.getValueDictByKey(groupName) + end + mist.DBs.units[coa_name][countryName][category][group_num].groupName = groupName + mist.DBs.units[coa_name][countryName][category][group_num].groupId = group_data.groupId + mist.DBs.units[coa_name][countryName][category][group_num].category = category + mist.DBs.units[coa_name][countryName][category][group_num].coalition = coa_name + mist.DBs.units[coa_name][countryName][category][group_num].country = countryName + mist.DBs.units[coa_name][countryName][category][group_num].countryId = cntry_data.id + mist.DBs.units[coa_name][countryName][category][group_num].startTime = group_data.start_time + mist.DBs.units[coa_name][countryName][category][group_num].task = group_data.task + mist.DBs.units[coa_name][countryName][category][group_num].hidden = group_data.hidden + + mist.DBs.units[coa_name][countryName][category][group_num].units = {} + + mist.DBs.units[coa_name][countryName][category][group_num].radioSet = group_data.radioSet + mist.DBs.units[coa_name][countryName][category][group_num].uncontrolled = group_data.uncontrolled + mist.DBs.units[coa_name][countryName][category][group_num].frequency = group_data.frequency + mist.DBs.units[coa_name][countryName][category][group_num].modulation = group_data.modulation + + for unit_num, unit_data in pairs(group_data.units) do + local units_tbl = mist.DBs.units[coa_name][countryName][category][group_num].units --pointer to the units table for this group + + units_tbl[unit_num] = {} + if env.mission.version > 7 then + units_tbl[unit_num].unitName = env.getValueDictByKey(unit_data.name) + else + units_tbl[unit_num].unitName = unit_data.name + end + units_tbl[unit_num].type = unit_data.type + units_tbl[unit_num].skill = unit_data.skill --will be nil for statics + units_tbl[unit_num].unitId = unit_data.unitId + units_tbl[unit_num].category = category + units_tbl[unit_num].coalition = coa_name + units_tbl[unit_num].country = countryName + units_tbl[unit_num].countryId = cntry_data.id + units_tbl[unit_num].heading = unit_data.heading + units_tbl[unit_num].playerCanDrive = unit_data.playerCanDrive + units_tbl[unit_num].alt = unit_data.alt + units_tbl[unit_num].alt_type = unit_data.alt_type + units_tbl[unit_num].speed = unit_data.speed + units_tbl[unit_num].livery_id = unit_data.livery_id + if unit_data.point then --ME currently does not work like this, but it might one day + units_tbl[unit_num].point = unit_data.point + else + units_tbl[unit_num].point = {} + units_tbl[unit_num].point.x = unit_data.x + units_tbl[unit_num].point.y = unit_data.y + end + units_tbl[unit_num].x = unit_data.x + units_tbl[unit_num].y = unit_data.y + + units_tbl[unit_num].callsign = unit_data.callsign + units_tbl[unit_num].onboard_num = unit_data.onboard_num + units_tbl[unit_num].hardpoint_racks = unit_data.hardpoint_racks + units_tbl[unit_num].psi = unit_data.psi + + + units_tbl[unit_num].groupName = groupName + units_tbl[unit_num].groupId = group_data.groupId + + if unit_data.AddPropAircraft then + units_tbl[unit_num].AddPropAircraft = unit_data.AddPropAircraft + end + + if category == 'static' then + units_tbl[unit_num].categoryStatic = unit_data.category + units_tbl[unit_num].shape_name = unit_data.shape_name + if unit_data.mass then + units_tbl[unit_num].mass = unit_data.mass + end + + if unit_data.canCargo then + units_tbl[unit_num].canCargo = unit_data.canCargo + end + end + + end --for unit_num, unit_data in pairs(group_data.units) do + end --if group_data and group_data.units then + 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 --if type(cntry_data) == 'table' then + 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 + + mist.DBs.unitsByName = {} + mist.DBs.unitsById = {} + mist.DBs.unitsByCat = {} + + mist.DBs.unitsByCat.helicopter = {} -- adding default categories + mist.DBs.unitsByCat.plane = {} + mist.DBs.unitsByCat.ship = {} + mist.DBs.unitsByCat.static = {} + mist.DBs.unitsByCat.vehicle = {} + + mist.DBs.unitsByNum = {} + + mist.DBs.groupsByName = {} + mist.DBs.groupsById = {} + mist.DBs.humansByName = {} + mist.DBs.humansById = {} + + mist.DBs.dynGroupsAdded = {} -- will be filled by mist.dbUpdate from dynamically spawned groups + mist.DBs.activeHumans = {} + + mist.DBs.aliveUnits = {} -- will be filled in by the "updateAliveUnits" coroutine in mist.main. + + mist.DBs.removedAliveUnits = {} -- will be filled in by the "updateAliveUnits" coroutine in mist.main. + + mist.DBs.const = {} + + -- not accessible by SSE, must use static list :-/ + mist.DBs.const.callsigns = { + ['NATO'] = { + ['rules'] = { + ['groupLimit'] = 9, + }, + ['AWACS'] = { + ['Overlord'] = 1, + ['Magic'] = 2, + ['Wizard'] = 3, + ['Focus'] = 4, + ['Darkstar'] = 5, + }, + ['TANKER'] = { + ['Texaco'] = 1, + ['Arco'] = 2, + ['Shell'] = 3, + }, + ['JTAC'] = { + ['Axeman'] = 1, + ['Darknight'] = 2, + ['Warrior'] = 3, + ['Pointer'] = 4, + ['Eyeball'] = 5, + ['Moonbeam'] = 6, + ['Whiplash'] = 7, + ['Finger'] = 8, + ['Pinpoint'] = 9, + ['Ferret'] = 10, + ['Shaba'] = 11, + ['Playboy'] = 12, + ['Hammer'] = 13, + ['Jaguar'] = 14, + ['Deathstar'] = 15, + ['Anvil'] = 16, + ['Firefly'] = 17, + ['Mantis'] = 18, + ['Badger'] = 19, + }, + ['aircraft'] = { + ['Enfield'] = 1, + ['Springfield'] = 2, + ['Uzi'] = 3, + ['Colt'] = 4, + ['Dodge'] = 5, + ['Ford'] = 6, + ['Chevy'] = 7, + ['Pontiac'] = 8, + }, + + ['unique'] = { + ['A10'] = { + ['Hawg'] = 9, + ['Boar'] = 10, + ['Pig'] = 11, + ['Tusk'] = 12, + ['rules'] = { + ['canUseAircraft'] = true, + ['appliesTo'] = { + 'A-10C', + 'A-10A', + }, + }, + }, + }, + }, + } + mist.DBs.const.shapeNames = { + ["Landmine"] = "landmine", + ["FARP CP Blindage"] = "kp_ug", + ["Subsidiary structure C"] = "saray-c", + ["Barracks 2"] = "kazarma2", + ["Small house 2C"] = "dom2c", + ["Military staff"] = "aviashtab", + ["Tech hangar A"] = "ceh_ang_a", + ["Oil derrick"] = "neftevyshka", + ["Tech combine"] = "kombinat", + ["Garage B"] = "garage_b", + ["Airshow_Crowd"] = "Crowd1", + ["Hangar A"] = "angar_a", + ["Repair workshop"] = "tech", + ["Subsidiary structure D"] = "saray-d", + ["FARP Ammo Dump Coating"] = "SetkaKP", + ["Small house 1C area"] = "dom2c-all", + ["Tank 2"] = "airbase_tbilisi_tank_01", + ["Boiler-house A"] = "kotelnaya_a", + ["Workshop A"] = "tec_a", + ["Small werehouse 1"] = "s1", + ["Garage small B"] = "garagh-small-b", + ["Small werehouse 4"] = "s4", + ["Shop"] = "magazin", + ["Subsidiary structure B"] = "saray-b", + ["FARP Fuel Depot"] = "GSM Rus", + ["Coach cargo"] = "wagon-gruz", + ["Electric power box"] = "tr_budka", + ["Tank 3"] = "airbase_tbilisi_tank_02", + ["Red_Flag"] = "H-flag_R", + ["Container red 3"] = "konteiner_red3", + ["Garage A"] = "garage_a", + ["Hangar B"] = "angar_b", + ["Black_Tyre"] = "H-tyre_B", + ["Cafe"] = "stolovaya", + ["Restaurant 1"] = "restoran1", + ["Subsidiary structure A"] = "saray-a", + ["Container white"] = "konteiner_white", + ["Warehouse"] = "sklad", + ["Tank"] = "bak", + ["Railway crossing B"] = "pereezd_small", + ["Subsidiary structure F"] = "saray-f", + ["Farm A"] = "ferma_a", + ["Small werehouse 3"] = "s3", + ["Water tower A"] = "wodokachka_a", + ["Railway station"] = "r_vok_sd", + ["Coach a tank blue"] = "wagon-cisterna_blue", + ["Supermarket A"] = "uniwersam_a", + ["Coach a platform"] = "wagon-platforma", + ["Garage small A"] = "garagh-small-a", + ["TV tower"] = "tele_bash", + ["Comms tower M"] = "tele_bash_m", + ["Small house 1A"] = "domik1a", + ["Farm B"] = "ferma_b", + ["GeneratorF"] = "GeneratorF", + ["Cargo1"] = "ab-212_cargo", + ["Container red 2"] = "konteiner_red2", + ["Subsidiary structure E"] = "saray-e", + ["Coach a passenger"] = "wagon-pass", + ["Black_Tyre_WF"] = "H-tyre_B_WF", + ["Electric locomotive"] = "elektrowoz", + ["Shelter"] = "ukrytie", + ["Coach a tank yellow"] = "wagon-cisterna_yellow", + ["Railway crossing A"] = "pereezd_big", + [".Ammunition depot"] = "SkladC", + ["Small werehouse 2"] = "s2", + ["Windsock"] = "H-Windsock_RW", + ["Shelter B"] = "ukrytie_b", + ["Fuel tank"] = "toplivo-bak", + ["Locomotive"] = "teplowoz", + [".Command Center"] = "ComCenter", + ["Pump station"] = "nasos", + ["Black_Tyre_RF"] = "H-tyre_B_RF", + ["Coach cargo open"] = "wagon-gruz-otkr", + ["Subsidiary structure 3"] = "hozdomik3", + ["FARP Tent"] = "PalatkaB", + ["White_Tyre"] = "H-tyre_W", + ["Subsidiary structure G"] = "saray-g", + ["Container red 1"] = "konteiner_red1", + ["Small house 1B area"] = "domik1b-all", + ["Subsidiary structure 1"] = "hozdomik1", + ["Container brown"] = "konteiner_brown", + ["Small house 1B"] = "domik1b", + ["Subsidiary structure 2"] = "hozdomik2", + ["Chemical tank A"] = "him_bak_a", + ["WC"] = "WC", + ["Small house 1A area"] = "domik1a-all", + ["White_Flag"] = "H-Flag_W", + ["Airshow_Cone"] = "Comp_cone", + } + + + -- create mist.DBs.oldAliveUnits + -- do + -- local intermediate_alive_units = {} -- between 0 and 0.5 secs old + -- local function make_old_alive_units() -- called every 0.5 secs, makes the old_alive_units DB which is just a copy of alive_units that is 0.5 to 1 sec old + -- if intermediate_alive_units then + -- mist.DBs.oldAliveUnits = mist.utils.deepCopy(intermediate_alive_units) + -- end + -- intermediate_alive_units = mist.utils.deepCopy(mist.DBs.aliveUnits) + -- timer.scheduleFunction(make_old_alive_units, nil, timer.getTime() + 0.5) + -- end + + -- make_old_alive_units() + -- end + + --Build DBs + for coa_name, coa_data in pairs(mist.DBs.units) do + for cntry_name, cntry_data in pairs(coa_data) do + for category_name, category_data in pairs(cntry_data) do + if type(category_data) == 'table' then + for group_ind, group_data in pairs(category_data) do + if type(group_data) == 'table' and group_data.units and type(group_data.units) == 'table' and #group_data.units > 0 then -- OCD paradigm programming + mist.DBs.groupsByName[group_data.groupName] = mist.utils.deepCopy(group_data) + mist.DBs.groupsById[group_data.groupId] = mist.utils.deepCopy(group_data) + for unit_ind, unit_data in pairs(group_data.units) do + mist.DBs.unitsByName[unit_data.unitName] = mist.utils.deepCopy(unit_data) + mist.DBs.unitsById[unit_data.unitId] = mist.utils.deepCopy(unit_data) + + mist.DBs.unitsByCat[unit_data.category] = mist.DBs.unitsByCat[unit_data.category] or {} -- future-proofing against new categories... + table.insert(mist.DBs.unitsByCat[unit_data.category], mist.utils.deepCopy(unit_data)) + --dbLog:info('inserting $1', unit_data.unitName) + table.insert(mist.DBs.unitsByNum, mist.utils.deepCopy(unit_data)) + + if unit_data.skill and (unit_data.skill == "Client" or unit_data.skill == "Player") then + mist.DBs.humansByName[unit_data.unitName] = mist.utils.deepCopy(unit_data) + mist.DBs.humansById[unit_data.unitId] = mist.utils.deepCopy(unit_data) + --if Unit.getByName(unit_data.unitName) then + -- mist.DBs.activeHumans[unit_data.unitName] = mist.utils.deepCopy(unit_data) + -- mist.DBs.activeHumans[unit_data.unitName].playerName = Unit.getByName(unit_data.unitName):getPlayerName() + --end + end + end + end + end + end + end + end + end + + --DynDBs + mist.DBs.MEunits = mist.utils.deepCopy(mist.DBs.units) + mist.DBs.MEunitsByName = mist.utils.deepCopy(mist.DBs.unitsByName) + mist.DBs.MEunitsById = mist.utils.deepCopy(mist.DBs.unitsById) + mist.DBs.MEunitsByCat = mist.utils.deepCopy(mist.DBs.unitsByCat) + mist.DBs.MEunitsByNum = mist.utils.deepCopy(mist.DBs.unitsByNum) + mist.DBs.MEgroupsByName = mist.utils.deepCopy(mist.DBs.groupsByName) + mist.DBs.MEgroupsById = mist.utils.deepCopy(mist.DBs.groupsById) + + mist.DBs.deadObjects = {} + + do + local mt = {} + + function mt.__newindex(t, key, val) + local original_key = key --only for duplicate runtime IDs. + local key_ind = 1 + while mist.DBs.deadObjects[key] do + --dbLog:warn('duplicate runtime id of previously dead object key: $1', key) + key = tostring(original_key) .. ' #' .. tostring(key_ind) + key_ind = key_ind + 1 + end + + if mist.DBs.aliveUnits and mist.DBs.aliveUnits[val.object.id_] then + ----dbLog:info('object found in alive_units') + val.objectData = mist.utils.deepCopy(mist.DBs.aliveUnits[val.object.id_]) + local pos = Object.getPosition(val.object) + if pos then + val.objectPos = pos.p + end + val.objectType = mist.DBs.aliveUnits[val.object.id_].category + + elseif mist.DBs.removedAliveUnits and mist.DBs.removedAliveUnits[val.object.id_] then -- it didn't exist in alive_units, check old_alive_units + ----dbLog:info('object found in old_alive_units') + val.objectData = mist.utils.deepCopy(mist.DBs.removedAliveUnits[val.object.id_]) + local pos = Object.getPosition(val.object) + if pos then + val.objectPos = pos.p + end + val.objectType = mist.DBs.removedAliveUnits[val.object.id_].category + + else --attempt to determine if static object... + ----dbLog:info('object not found in alive units or old alive units') + local pos = Object.getPosition(val.object) + if pos then + local static_found = false + for ind, static in pairs(mist.DBs.unitsByCat.static) do + if ((pos.p.x - static.point.x)^2 + (pos.p.z - static.point.y)^2)^0.5 < 0.1 then --really, it should be zero... + --dbLog:info('correlated dead static object to position') + val.objectData = static + val.objectPos = pos.p + val.objectType = 'static' + static_found = true + break + end + end + if not static_found then + val.objectPos = pos.p + val.objectType = 'building' + end + else + val.objectType = 'unknown' + end + end + rawset(t, key, val) + end + + setmetatable(mist.DBs.deadObjects, mt) + end + + do -- mist unitID funcs + for id, idData in pairs(mist.DBs.unitsById) do + if idData.unitId > mist.nextUnitId then + mist.nextUnitId = mist.utils.deepCopy(idData.unitId) + end + if idData.groupId > mist.nextGroupId then + mist.nextGroupId = mist.utils.deepCopy(idData.groupId) + end + end + end + + + end + + local function updateAliveUnits() -- coroutine function + local lalive_units = mist.DBs.aliveUnits -- local references for faster execution + local lunits = mist.DBs.unitsByNum + local ldeepcopy = mist.utils.deepCopy + local lUnit = Unit + local lremovedAliveUnits = mist.DBs.removedAliveUnits + local updatedUnits = {} + + if #lunits > 0 then + local units_per_run = math.ceil(#lunits/20) + if units_per_run < 5 then + units_per_run = 5 + end + + for i = 1, #lunits do + if lunits[i].category ~= 'static' then -- can't get statics with Unit.getByName :( + local unit = lUnit.getByName(lunits[i].unitName) + if unit then + ----dbLog:info("unit named $1 alive!", lunits[i].unitName) -- spammy + local pos = unit:getPosition() + local newtbl = ldeepcopy(lunits[i]) + if pos then + newtbl.pos = pos.p + end + newtbl.unit = unit + --newtbl.rt_id = unit.id_ + lalive_units[unit.id_] = newtbl + updatedUnits[unit.id_] = true + end + end + if i%units_per_run == 0 then + coroutine.yield() + end + end + -- All units updated, remove any "alive" units that were not updated- they are dead! + for unit_id, unit in pairs(lalive_units) do + if not updatedUnits[unit_id] then + lremovedAliveUnits[unit_id] = unit + lalive_units[unit_id] = nil + end + end + end + end + + local function dbUpdate(event, objType) + --dbLog:info('dbUpdate') + local newTable = {} + newTable.startTime = 0 + if type(event) == 'string' then -- if name of an object. + local newObject + if Group.getByName(event) then + newObject = Group.getByName(event) + elseif StaticObject.getByName(event) then + newObject = StaticObject.getByName(event) + -- log:info('its static') + else + log:warn('$1 is not a Group or Static Object. This should not be possible. Sent category is: $2', event, objType) + return false + end + + newTable.name = newObject:getName() + newTable.groupId = tonumber(newObject:getID()) + newTable.groupName = newObject:getName() + local unitOneRef + if objType == 'static' then + unitOneRef = newObject + newTable.countryId = tonumber(newObject:getCountry()) + newTable.coalitionId = tonumber(newObject:getCoalition()) + newTable.category = 'static' + else + unitOneRef = newObject:getUnits() + newTable.countryId = tonumber(unitOneRef[1]:getCountry()) + newTable.coalitionId = tonumber(unitOneRef[1]:getCoalition()) + newTable.category = tonumber(newObject:getCategory()) + end + for countryData, countryId in pairs(country.id) do + if newTable.country and string.upper(countryData) == string.upper(newTable.country) or countryId == newTable.countryId then + newTable.countryId = countryId + newTable.country = string.lower(countryData) + for coaData, coaId in pairs(coalition.side) do + if coaId == coalition.getCountryCoalition(countryId) then + newTable.coalition = string.lower(coaData) + end + end + end + end + for catData, catId in pairs(Unit.Category) do + if objType == 'group' and Group.getByName(newTable.groupName):isExist() then + if catId == Group.getByName(newTable.groupName):getCategory() then + newTable.category = string.lower(catData) + end + elseif objType == 'static' and StaticObject.getByName(newTable.groupName):isExist() then + if catId == StaticObject.getByName(newTable.groupName):getCategory() then + newTable.category = string.lower(catData) + end + + end + end + local gfound = false + for index, data in pairs(mistAddedGroups) do + if mist.stringMatch(data.name, newTable.groupName) == true then + gfound = true + newTable.task = data.task + newTable.modulation = data.modulation + newTable.uncontrolled = data.uncontrolled + newTable.radioSet = data.radioSet + newTable.hidden = data.hidden + newTable.startTime = data.start_time + mistAddedGroups[index] = nil + end + end + + if gfound == false then + newTable.uncontrolled = false + newTable.hidden = false + end + + newTable.units = {} + if objType == 'group' then + for unitId, unitData in pairs(unitOneRef) do + newTable.units[unitId] = {} + newTable.units[unitId].unitName = unitData:getName() + + newTable.units[unitId].x = mist.utils.round(unitData:getPosition().p.x) + newTable.units[unitId].y = mist.utils.round(unitData:getPosition().p.z) + newTable.units[unitId].point = {} + newTable.units[unitId].point.x = newTable.units[unitId].x + newTable.units[unitId].point.y = newTable.units[unitId].y + newTable.units[unitId].alt = mist.utils.round(unitData:getPosition().p.y) + newTable.units[unitId].speed = mist.vec.mag(unitData:getVelocity()) + + newTable.units[unitId].heading = mist.getHeading(unitData, true) + + newTable.units[unitId].type = unitData:getTypeName() + newTable.units[unitId].unitId = tonumber(unitData:getID()) + + + newTable.units[unitId].groupName = newTable.groupName + newTable.units[unitId].groupId = newTable.groupId + newTable.units[unitId].countryId = newTable.countryId + newTable.units[unitId].coalitionId = newTable.coalitionId + newTable.units[unitId].coalition = newTable.coalition + newTable.units[unitId].country = newTable.country + local found = false + for index, data in pairs(mistAddedObjects) do + if mist.stringMatch(data.name, newTable.units[unitId].unitName) == true then + found = true + newTable.units[unitId].livery_id = data.livery_id + newTable.units[unitId].skill = data.skill + newTable.units[unitId].alt_type = data.alt_type + newTable.units[unitId].callsign = data.callsign + newTable.units[unitId].psi = data.psi + mistAddedObjects[index] = nil + end + if found == false then + newTable.units[unitId].skill = "High" + newTable.units[unitId].alt_type = "BARO" + end + if newTable.units[unitId].alt_type == "RADIO" then -- raw postition MSL was grabbed for group, but spawn is AGL, so re-offset it + newTable.units[unitId].alt = (newTable.units[unitId].alt - land.getHeight({x = newTable.units[unitId].x, y = newTable.units[unitId].y})) + end + end + + end + else -- its a static + newTable.category = 'static' + newTable.units[1] = {} + newTable.units[1].unitName = newObject:getName() + newTable.units[1].category = 'static' + newTable.units[1].x = mist.utils.round(newObject:getPosition().p.x) + newTable.units[1].y = mist.utils.round(newObject:getPosition().p.z) + newTable.units[1].point = {} + newTable.units[1].point.x = newTable.units[1].x + newTable.units[1].point.y = newTable.units[1].y + newTable.units[1].alt = mist.utils.round(newObject:getPosition().p.y) + newTable.units[1].heading = mist.getHeading(newObject, true) + newTable.units[1].type = newObject:getTypeName() + newTable.units[1].unitId = tonumber(newObject:getID()) + newTable.units[1].groupName = newTable.name + newTable.units[1].groupId = newTable.groupId + newTable.units[1].countryId = newTable.countryId + newTable.units[1].country = newTable.country + newTable.units[1].coalitionId = newTable.coalitionId + newTable.units[1].coalition = newTable.coalition + if newObject:getCategory() == 6 and newObject:getCargoDisplayName() then + local mass = newObject:getCargoDisplayName() + mass = string.gsub(mass, ' ', '') + mass = string.gsub(mass, 'kg', '') + newTable.units[1].mass = tonumber(mass) + newTable.units[1].categoryStatic = 'Cargos' + newTable.units[1].canCargo = true + newTable.units[1].shape_name = 'ab-212_cargo' + end + + ----- search mist added objects for extra data if applicable + for index, data in pairs(mistAddedObjects) do + if mist.stringMatch(data.name, newTable.units[1].unitName) == true then + newTable.units[1].shape_name = data.shape_name -- for statics + newTable.units[1].livery_id = data.livery_id + newTable.units[1].airdromeId = data.airdromeId + newTable.units[1].mass = data.mass + newTable.units[1].canCargo = data.canCargo + newTable.units[1].categoryStatic = data.categoryStatic + newTable.units[1].type = data.type + mistAddedObjects[index] = nil + break + end + end + end + end + --mist.debug.writeData(mist.utils.serialize,{'msg', newTable}, timer.getAbsTime() ..'Group.lua') + newTable.timeAdded = timer.getAbsTime() -- only on the dynGroupsAdded table. For other reference, see start time + --mist.debug.dumpDBs() + --end + --dbLog:info('endDbUpdate') + return newTable + end + + --[[DB update code... FRACK. I need to refactor some of it. + + The problem is that the DBs need to account better for shared object names. Needs to write over some data and outright remove other. + + If groupName is used then entire group needs to be rewritten + what to do with old groups units DB entries?. Names cant be assumed to be the same. + + + -- new spawn event check. + -- event handler filters everything into groups: tempSpawnedGroups + -- this function then checks DBs to see if data has changed + ]] + local function checkSpawnedEventsNew() + if tempSpawnGroupsCounter > 0 then + --[[local updatesPerRun = math.ceil(#tempSpawnedGroupsCounter/20) + if updatesPerRun < 5 then + updatesPerRun = 5 + end]] + + --dbLog:info('iterate') + for name, gData in pairs(tempSpawnedGroups) do + --env.info(name) + local updated = false + local stillExists = false + if not gData.checked then + tempSpawnedGroups[name].checked = true -- so if there was an error it will get cleared. + local _g = gData.gp or Group.getByName(name) + if mist.DBs.groupsByName[name] then + -- first check group level properties, groupId, countryId, coalition + -- dbLog:info('Found in DBs, check if updated') + local dbTable = mist.DBs.groupsByName[name] + -- dbLog:info(dbTable) + if gData.type ~= 'static' then + -- dbLog:info('Not static') + + if _g and _g:isExist() == true then + stillExists = true + local _u = _g:getUnit(1) + + if _u and (dbTable.groupId ~= tonumber(_g:getID()) or _u:getCountry() ~= dbTable.countryId or _u:getCoalition() ~= dbTable.coaltionId) then + --dbLog:info('Group Data mismatch') + updated = true + else + -- dbLog:info('No Mismatch') + end + else + dbLog:warn('$1 : Group was not accessible', name) + end + end + end + --dbLog:info('Updated: $1', updated) + if updated == false and gData.type ~= 'static' then -- time to check units + --dbLog:info('No Group Mismatch, Check Units') + if _g and _g:isExist() == true then + stillExists = true + for index, uObject in pairs(_g:getUnits()) do + --dbLog:info(index) + if mist.DBs.unitsByName[uObject:getName()] then + --dbLog:info('UnitByName table exists') + local uTable = mist.DBs.unitsByName[uObject:getName()] + if tonumber(uObject:getID()) ~= uTable.unitId or uObject:getTypeName() ~= uTable.type then + --dbLog:info('Unit Data mismatch') + updated = true + break + end + end + end + end + else + stillExists = true + end + + if stillExists == true and (updated == true or not mist.DBs.groupsByName[name]) then + --dbLog:info('Get Table') + writeGroups[#writeGroups+1] = {data = dbUpdate(name, gData.type), isUpdated = updated} + + end + -- Work done, so remove + end + tempSpawnedGroups[name] = nil + tempSpawnGroupsCounter = tempSpawnGroupsCounter - 1 + end + end + end + + local function updateDBTables() + local i = #writeGroups + + local savesPerRun = math.ceil(i/10) + if savesPerRun < 5 then + savesPerRun = 5 + end + if i > 0 then + --dbLog:info('updateDBTables') + local ldeepCopy = mist.utils.deepCopy + for x = 1, i do + --dbLog:info(writeGroups[x]) + local newTable = writeGroups[x].data + local updated = writeGroups[x].isUpdated + local mistCategory + if type(newTable.category) == 'string' then + mistCategory = string.lower(newTable.category) + end + + if string.upper(newTable.category) == 'GROUND_UNIT' then + mistCategory = 'vehicle' + newTable.category = mistCategory + elseif string.upper(newTable.category) == 'AIRPLANE' then + mistCategory = 'plane' + newTable.category = mistCategory + elseif string.upper(newTable.category) == 'HELICOPTER' then + mistCategory = 'helicopter' + newTable.category = mistCategory + elseif string.upper(newTable.category) == 'SHIP' then + mistCategory = 'ship' + newTable.category = mistCategory + end + --dbLog:info('Update unitsBy') + for newId, newUnitData in pairs(newTable.units) do + --dbLog:info(newId) + newUnitData.category = mistCategory + if newUnitData.unitId then + --dbLog:info('byId') + mist.DBs.unitsById[tonumber(newUnitData.unitId)] = ldeepCopy(newUnitData) + end + --dbLog:info(updated) + if mist.DBs.unitsByName[newUnitData.unitName] and updated == true then--if unit existed before and something was updated, write over the entry for a given unit name just in case. + --dbLog:info('Updating Unit Tables') + for i = 1, #mist.DBs.unitsByCat[mistCategory] do + if mist.DBs.unitsByCat[mistCategory][i].unitName == newUnitData.unitName then + --dbLog:info('Entry Found, Rewriting for unitsByCat') + mist.DBs.unitsByCat[mistCategory][i] = ldeepCopy(newUnitData) + break + end + end + for i = 1, #mist.DBs.unitsByNum do + if mist.DBs.unitsByNum[i].unitName == newUnitData.unitName then + --dbLog:info('Entry Found, Rewriting for unitsByNum') + mist.DBs.unitsByNum[i] = ldeepCopy(newUnitData) + break + end + end + + else + --dbLog:info('Unitname not in use, add as normal') + mist.DBs.unitsByCat[mistCategory][#mist.DBs.unitsByCat[mistCategory] + 1] = ldeepCopy(newUnitData) + mist.DBs.unitsByNum[#mist.DBs.unitsByNum + 1] = ldeepCopy(newUnitData) + end + mist.DBs.unitsByName[newUnitData.unitName] = ldeepCopy(newUnitData) + + + end + -- this is a really annoying DB to populate. Gotta create new tables in case its missing + --dbLog:info('write mist.DBs.units') + if not mist.DBs.units[newTable.coalition] then + mist.DBs.units[newTable.coalition] = {} + end + + if not mist.DBs.units[newTable.coalition][newTable.country] then + mist.DBs.units[newTable.coalition][(newTable.country)] = {} + mist.DBs.units[newTable.coalition][(newTable.country)].countryId = newTable.countryId + end + if not mist.DBs.units[newTable.coalition][newTable.country][mistCategory] then + mist.DBs.units[newTable.coalition][(newTable.country)][mistCategory] = {} + end + + if updated == true then + --dbLog:info('Updating DBsUnits') + for i = 1, #mist.DBs.units[newTable.coalition][(newTable.country)][mistCategory] do + if mist.DBs.units[newTable.coalition][(newTable.country)][mistCategory][i].groupName == newTable.groupName then + --dbLog:info('Entry Found, Rewriting') + mist.DBs.units[newTable.coalition][(newTable.country)][mistCategory][i] = ldeepCopy(newTable) + break + end + end + else + mist.DBs.units[newTable.coalition][(newTable.country)][mistCategory][#mist.DBs.units[newTable.coalition][(newTable.country)][mistCategory] + 1] = ldeepCopy(newTable) + end + + + if newTable.groupId then + mist.DBs.groupsById[newTable.groupId] = ldeepCopy(newTable) + end + + mist.DBs.groupsByName[newTable.name] = ldeepCopy(newTable) + mist.DBs.dynGroupsAdded[#mist.DBs.dynGroupsAdded + 1] = ldeepCopy(newTable) + + writeGroups[x] = nil + if x%savesPerRun == 0 then + coroutine.yield() + end + end + if timer.getTime() > lastUpdateTime then + lastUpdateTime = timer.getTime() + end + --dbLog:info('endUpdateTables') + end + end + + local function groupSpawned(event) + -- dont need to add units spawned in at the start of the mission if mist is loaded in init line + if event.id == world.event.S_EVENT_BIRTH and timer.getTime0() < timer.getAbsTime() then + --dbLog:info('unitSpawnEvent') + + --table.insert(tempSpawnedUnits,(event.initiator)) + ------- + -- New functionality below. + ------- + if Object.getCategory(event.initiator) == 1 and not Unit.getPlayerName(event.initiator) then -- simple player check, will need to later check to see if unit was spawned with a player in a flight + --dbLog:info('Object is a Unit') + if Unit.getGroup(event.initiator) then + --dbLog:info(Unit.getGroup(event.initiator):getName()) + local g = Unit.getGroup(event.initiator) + if not tempSpawnedGroups[g:getName()] then + --dbLog:info('added') + tempSpawnedGroups[g:getName()] = {type = 'group', gp = g} + tempSpawnGroupsCounter = tempSpawnGroupsCounter + 1 + end + else + log:error('Group not accessible by unit in event handler. This is a DCS bug') + end + elseif Object.getCategory(event.initiator) == 3 or Object.getCategory(event.initiator) == 6 then + --dbLog:info('Object is Static') + tempSpawnedGroups[StaticObject.getName(event.initiator)] = {type = 'static'} + tempSpawnGroupsCounter = tempSpawnGroupsCounter + 1 + end + + + end + end + + local function doScheduledFunctions() + local i = 1 + while i <= #scheduledTasks do + if not scheduledTasks[i].rep then -- not a repeated process + if scheduledTasks[i].t <= timer.getTime() then + local task = scheduledTasks[i] -- local reference + table.remove(scheduledTasks, i) + local err, errmsg = pcall(task.f, unpack(task.vars, 1, table.maxn(task.vars))) + if not err then + log:error('Error in scheduled function: $1', errmsg) + end + --task.f(unpack(task.vars, 1, table.maxn(task.vars))) -- do the task, do not increment i + else + i = i + 1 + end + else + if scheduledTasks[i].st and scheduledTasks[i].st <= timer.getTime() then --if a stoptime was specified, and the stop time exceeded + table.remove(scheduledTasks, i) -- stop time exceeded, do not execute, do not increment i + elseif scheduledTasks[i].t <= timer.getTime() then + local task = scheduledTasks[i] -- local reference + task.t = timer.getTime() + task.rep --schedule next run + local err, errmsg = pcall(task.f, unpack(task.vars, 1, table.maxn(task.vars))) + if not err then + log:error('Error in scheduled function: $1' .. errmsg) + end + --scheduledTasks[i].f(unpack(scheduledTasks[i].vars, 1, table.maxn(scheduledTasks[i].vars))) -- do the task + i = i + 1 + else + i = i + 1 + end + end + end + end + + -- Event handler to start creating the dead_objects table + local function addDeadObject(event) + if event.id == world.event.S_EVENT_DEAD or event.id == world.event.S_EVENT_CRASH then + if event.initiator and event.initiator.id_ and event.initiator.id_ > 0 then + + local id = event.initiator.id_ -- initial ID, could change if there is a duplicate id_ already dead. + local val = {object = event.initiator} -- the new entry in mist.DBs.deadObjects. + + local original_id = id --only for duplicate runtime IDs. + local id_ind = 1 + while mist.DBs.deadObjects[id] do + --log:info('duplicate runtime id of previously dead object id: $1', id) + id = tostring(original_id) .. ' #' .. tostring(id_ind) + id_ind = id_ind + 1 + end + + if mist.DBs.aliveUnits and mist.DBs.aliveUnits[val.object.id_] then + --log:info('object found in alive_units') + val.objectData = mist.utils.deepCopy(mist.DBs.aliveUnits[val.object.id_]) + local pos = Object.getPosition(val.object) + if pos then + val.objectPos = pos.p + end + val.objectType = mist.DBs.aliveUnits[val.object.id_].category + --[[if mist.DBs.activeHumans[Unit.getName(val.object)] then + --trigger.action.outText('remove via death: ' .. Unit.getName(val.object),20) + mist.DBs.activeHumans[Unit.getName(val.object)] = nil + end]] + elseif mist.DBs.removedAliveUnits and mist.DBs.removedAliveUnits[val.object.id_] then -- it didn't exist in alive_units, check old_alive_units + --log:info('object found in old_alive_units') + val.objectData = mist.utils.deepCopy(mist.DBs.removedAliveUnits[val.object.id_]) + local pos = Object.getPosition(val.object) + if pos then + val.objectPos = pos.p + end + val.objectType = mist.DBs.removedAliveUnits[val.object.id_].category + + else --attempt to determine if static object... + --log:info('object not found in alive units or old alive units') + local pos = Object.getPosition(val.object) + if pos then + local static_found = false + for ind, static in pairs(mist.DBs.unitsByCat.static) do + if ((pos.p.x - static.point.x)^2 + (pos.p.z - static.point.y)^2)^0.5 < 0.1 then --really, it should be zero... + --log:info('correlated dead static object to position') + val.objectData = static + val.objectPos = pos.p + val.objectType = 'static' + static_found = true + break + end + end + if not static_found then + val.objectPos = pos.p + val.objectType = 'building' + end + else + val.objectType = 'unknown' + end + end + mist.DBs.deadObjects[id] = val + end + end + end + + --[[ + local function addClientsToActive(event) + if event.id == world.event.S_EVENT_PLAYER_ENTER_UNIT or event.id == world.event.S_EVENT_BIRTH then + log:info(event) + if Unit.getPlayerName(event.initiator) then + log:info(Unit.getPlayerName(event.initiator)) + local newU = mist.utils.deepCopy(mist.DBs.unitsByName[Unit.getName(event.initiator)]) + newU.playerName = Unit.getPlayerName(event.initiator) + mist.DBs.activeHumans[Unit.getName(event.initiator)] = newU + --trigger.action.outText('added: ' .. Unit.getName(event.initiator), 20) + end + elseif event.id == world.event.S_EVENT_PLAYER_LEAVE_UNIT and event.initiator then + if mist.DBs.activeHumans[Unit.getName(event.initiator)] then + mist.DBs.activeHumans[Unit.getName(event.initiator)] = nil + -- trigger.action.outText('removed via control: ' .. Unit.getName(event.initiator), 20) + end + end + end + + mist.addEventHandler(addClientsToActive) + ]] + local function verifyDB() + --log:warn('verfy Run') + for coaName, coaId in pairs(coalition.side) do + --env.info(coaName) + local gps = coalition.getGroups(coaId) + for i = 1, #gps do + if gps[i] and Group.getSize(gps[i]) > 0 then + local gName = Group.getName(gps[i]) + if not mist.DBs.groupsByName[gName] then + --env.info(Unit.getID(gUnits[j]) .. ' Not found in DB yet') + if not tempSpawnedGroups[gName] then + --dbLog:info('added') + tempSpawnedGroups[gName] = {type = 'group', gp = gps[i]} + tempSpawnGroupsCounter = tempSpawnGroupsCounter + 1 + end + end + end + end + local st = coalition.getStaticObjects(coaId) + for i = 1, #st do + local s = st[i] + if StaticObject.isExist(s) then + if not mist.DBs.unitsByName[s:getName()] then + --env.info(StaticObject.getID(s) .. ' Not found in DB yet') + tempSpawnedGroups[s:getName()] = {type = 'static'} + tempSpawnGroupsCounter = tempSpawnGroupsCounter + 1 + end + end + end + + end + + end + + --- init function. + -- creates logger, adds default event handler + -- and calls main the first time. + -- @function mist.init + function mist.init() + + -- create logger + mist.log = mist.Logger:new("MIST", mistSettings.logLevel) + dbLog = mist.Logger:new('MISTDB', 'warn') + + log = mist.log -- log shorthand + -- set warning log level, showing only + -- warnings and errors + --log:setLevel("warning") + + log:info("initializing databases") + initDBs() + + -- add event handler for group spawns + mist.addEventHandler(groupSpawned) + mist.addEventHandler(addDeadObject) + + log:warn('Init time: $1', timer.getTime()) + + -- call main the first time therafter it reschedules itself. + mist.main() + --log:msg('MIST version $1.$2.$3 loaded', mist.majorVersion, mist.minorVersion, mist.build) + + mist.scheduleFunction(verifyDB, {}, timer.getTime() + 1) + return + end + + --- The main function. + -- Run 100 times per second. + -- You shouldn't call this function. + function mist.main() + timer.scheduleFunction(mist.main, {}, timer.getTime() + 0.01) --reschedule first in case of Lua error + + updateTenthSecond = updateTenthSecond + 1 + if updateTenthSecond == 20 then + updateTenthSecond = 0 + + checkSpawnedEventsNew() + + if not coroutines.updateDBTables then + coroutines.updateDBTables = coroutine.create(updateDBTables) + end + + coroutine.resume(coroutines.updateDBTables) + + if coroutine.status(coroutines.updateDBTables) == 'dead' then + coroutines.updateDBTables = nil + end + end + + --updating alive units + updateAliveUnitsCounter = updateAliveUnitsCounter + 1 + if updateAliveUnitsCounter == 5 then + updateAliveUnitsCounter = 0 + + if not coroutines.updateAliveUnits then + coroutines.updateAliveUnits = coroutine.create(updateAliveUnits) + end + + coroutine.resume(coroutines.updateAliveUnits) + + if coroutine.status(coroutines.updateAliveUnits) == 'dead' then + coroutines.updateAliveUnits = nil + end + end + + doScheduledFunctions() + end -- end of mist.main + + --- Returns next unit id. + -- @treturn number next unit id. + function mist.getNextUnitId() + mist.nextUnitId = mist.nextUnitId + 1 + if mist.nextUnitId > 6900 and mist.nextUnitId < 30000 then + mist.nextUnitId = 30000 + end + return mist.utils.deepCopy(mist.nextUnitId) + end + + --- Returns next group id. + -- @treturn number next group id. + function mist.getNextGroupId() + mist.nextGroupId = mist.nextGroupId + 1 + if mist.nextGroupId > 6900 and mist.nextGroupId < 30000 then + mist.nextGroupId = 30000 + end + return mist.utils.deepCopy(mist.nextGroupId) + end + + --- Returns timestamp of last database update. + -- @treturn timestamp of last database update + function mist.getLastDBUpdateTime() + return lastUpdateTime + end + + --- Spawns a static object to the game world. + -- @todo write good docs + -- @tparam table staticObj table containing data needed for the object creation + function mist.dynAddStatic(newObj) + log:info(newObj) + if newObj.units and newObj.units[1] then -- if its mist format + for entry, val in pairs(newObj.units[1]) do + if newObj[entry] and newObj[entry] ~= val or not newObj[entry] then + newObj[entry] = val + end + end + end + --log:info(newObj) + + local cntry = newObj.country + if newObj.countryId then + cntry = newObj.countryId + end + + local newCountry = '' + + 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 + log:error("Country not found: $1", cntry) + return false + end + + if newObj.clone or not newObj.groupId then + mistGpId = mistGpId + 1 + newObj.groupId = mistGpId + end + + if newObj.clone or not newObj.unitId then + mistUnitId = mistUnitId + 1 + newObj.unitId = mistUnitId + end + + + newObj.name = newObj.name or newObj.unitName + + if newObj.clone or not newObj.name then + mistDynAddIndex[' static '] = mistDynAddIndex[' static '] + 1 + newObj.name = (newCountry .. ' static ' .. mistDynAddIndex[' static ']) + end + + if not newObj.dead then + newObj.dead = false + end + + if not newObj.heading then + newObj.heading = math.random(360) + end + + if newObj.categoryStatic then + newObj.category = newObj.categoryStatic + end + if newObj.mass then + newObj.category = 'Cargos' + end + + if newObj.shapeName then + newObj.shape_name = newObj.shapeName + end + + if not newObj.shape_name then + log:info('shape_name not present') + if mist.DBs.const.shapeNames[newObj.type] then + newObj.shape_name = mist.DBs.const.shapeNames[newObj.type] + end + end + + mistAddedObjects[#mistAddedObjects + 1] = mist.utils.deepCopy(newObj) + if newObj.x and newObj.y and newObj.type and type(newObj.x) == 'number' and type(newObj.y) == 'number' and type(newObj.type) == 'string' then + log:info(newObj) + coalition.addStaticObject(country.id[newCountry], newObj) + + return newObj + end + log:error("Failed to add static object due to missing or incorrect value. X: $1, Y: $2, Type: $3", newObj.x, newObj.y, newObj.type) + return false + end + + --- Spawns a dynamic group into the game world. + -- Same as coalition.add function in SSE. checks the passed data to see if its valid. + -- Will generate groupId, groupName, unitId, and unitName if needed + -- @tparam table newGroup table containting values needed for spawning a group. + function mist.dynAdd(newGroup) + + --mist.debug.writeData(mist.utils.serialize,{'msg', newGroup}, 'newGroupOrig.lua') + 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 + log:error("Country not found: $1", cntry) + 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.clone or not newGroup.groupId then + mistDynAddIndex[typeName] = mistDynAddIndex[typeName] + 1 + mistGpId = mistGpId + 1 + newGroup.groupId = mistGpId + 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 and mist.DBs.groupsByName[newGroup.name] or not newGroup.name then + newGroup.name = tostring(newCountry .. tostring(typeName) .. mistDynAddIndex[typeName]) + 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 = mist.utils.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.clone or not unitData.unitId then + mistUnitId = mistUnitId + 1 + newGroup.units[unitIndex].unitId = mistUnitId + end + 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' + 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 = mist.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 + mistAddedObjects[#mistAddedObjects + 1] = mist.utils.deepCopy(newGroup.units[unitIndex]) + end + mistAddedGroups[#mistAddedGroups + 1] = mist.utils.deepCopy(newGroup) + if newGroup.route then + if newGroup.route and not newGroup.route.points then + if newGroup.route[1] then + local copyRoute = mist.utils.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 + + + --mist.debug.writeData(mist.utils.serialize,{'msg', newGroup}, 'newGroup.lua') + --log:warn(newGroup) + -- 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) + + return newGroup + + end + + --- Schedules a function. + -- Modified Slmod task scheduler, superior to timer.scheduleFunction + -- @tparam function f function to schedule + -- @tparam table vars array containing all parameters passed to the function + -- @tparam number t time in seconds from mission start to schedule the function to. + -- @tparam[opt] number rep time between repetitions of the function + -- @tparam[opt] number st time in seconds from mission start at which the function + -- should stop to be rescheduled. + -- @treturn number scheduled function id. + function mist.scheduleFunction(f, vars, t, rep, st) + --verify correct types + assert(type(f) == 'function', 'variable 1, expected function, got ' .. type(f)) + assert(type(vars) == 'table' or vars == nil, 'variable 2, expected table or nil, got ' .. type(f)) + assert(type(t) == 'number', 'variable 3, expected number, got ' .. type(t)) + assert(type(rep) == 'number' or rep == nil, 'variable 4, expected number or nil, got ' .. type(rep)) + assert(type(st) == 'number' or st == nil, 'variable 5, expected number or nil, got ' .. type(st)) + if not vars then + vars = {} + end + taskId = taskId + 1 + table.insert(scheduledTasks, {f = f, vars = vars, t = t, rep = rep, st = st, id = taskId}) + return taskId + end + + --- Removes a scheduled function. + -- @tparam number id function id + -- @treturn boolean true if function was successfully removed, false otherwise. + function mist.removeFunction(id) + local i = 1 + while i <= #scheduledTasks do + if scheduledTasks[i].id == id then + table.remove(scheduledTasks, i) + return true + else + i = i + 1 + end + end + return false + end + + --- Registers an event handler. + -- @tparam function f function handling event + -- @treturn number id of the event handler + function mist.addEventHandler(f) --id is optional! + local handler = {} + idNum = idNum + 1 + handler.id = idNum + handler.f = f + function handler:onEvent(event) + self.f(event) + end + world.addEventHandler(handler) + return handler.id + end + + --- Removes event handler with given id. + -- @tparam number id event handler id + -- @treturn boolean true on success, false otherwise + function mist.removeEventHandler(id) + for key, handler in pairs(world.eventHandlers) do + if handler.id and handler.id == id then + world.eventHandlers[key] = nil + return true + end + end + return false + end +end + +-- Begin common funcs +do + --- Returns MGRS coordinates as string. + -- @tparam string MGRS MGRS coordinates + -- @tparam number acc the accuracy of each easting/northing. + -- Can be: 0, 1, 2, 3, 4, or 5. + function mist.tostringMGRS(MGRS, acc) + if acc == 0 then + return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph + else + return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph .. ' ' .. string.format('%0' .. acc .. 'd', mist.utils.round(MGRS.Easting/(10^(5-acc)), 0)) + .. ' ' .. string.format('%0' .. acc .. 'd', mist.utils.round(MGRS.Northing/(10^(5-acc)), 0)) + end + end + + --[[acc: + in DM: decimal point of minutes. + In DMS: decimal point of seconds. + position after the decimal of the least significant digit: + So: + 42.32 - acc of 2. + ]] + function mist.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 = mist.utils.round((oldLatMin - latMin)*60, acc) + + local oldLonMin = lonMin + lonMin = math.floor(lonMin) + local lonSec = mist.utils.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 = mist.utils.round(latMin, acc) + lonMin = mist.utils.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 + + --[[ required: az - radian + required: dist - meters + optional: alt - meters (set to false or nil if you don't want to use it). + optional: metric - set true to get dist and alt in km and m. + precision will always be nearest degree and NM or km.]] + function mist.tostringBR(az, dist, alt, metric) + az = mist.utils.round(mist.utils.toDegree(az), 0) + + if metric then + dist = mist.utils.round(dist/1000, 0) + else + dist = mist.utils.round(mist.utils.metersToNM(dist), 0) + end + + local s = string.format('%03d', az) .. ' for ' .. dist + + if alt then + if metric then + s = s .. ' at ' .. mist.utils.round(alt, 0) + else + s = s .. ' at ' .. mist.utils.round(mist.utils.metersToFeet(alt), 0) + end + end + return s + end + + function mist.getNorthCorrection(gPoint) --gets the correction needed for true north + local point = mist.utils.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 + + --- Returns skill of the given unit. + -- @tparam string unitName unit name + -- @return skill of the unit + function mist.getUnitSkill(unitName) + if mist.DBs.unitsByName[unitName] then + if Unit.getByName(unitName) then + local lunit = Unit.getByName(unitName) + local data = mist.DBs.unitsByName[unitName] + if data.unitName == unitName and data.type == lunit:getTypeName() and data.unitId == tonumber(lunit:getID()) and data.skill then + return data.skill + end + end + end + log:error("Unit not found in DB: $1", unitName) + return false + end + + --- Returns an array containing a group's units positions. + -- e.g. + -- { + -- [1] = {x = 299435.224, y = -1146632.6773}, + -- [2] = {x = 663324.6563, y = 322424.1112} + -- } + -- @tparam number|string groupIdent group id or name + -- @treturn table array containing positions of each group member + function mist.getGroupPoints(groupIdent) + -- search by groupId and allow groupId and groupName as inputs + local gpId = groupIdent + if type(groupIdent) == 'string' and not tonumber(groupIdent) then + if mist.DBs.MEgroupsByName[groupIdent] then + gpId = mist.DBs.MEgroupsByName[groupIdent].groupId + else + log:error("Group not found in mist.DBs.MEgroupsByName: $1", groupIdent) + end + end + + for coa_name, coa_data in pairs(env.mission.coalition) do + if 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 + if not point.point then + points[point_num] = { x = point.x, y = point.y } + else + points[point_num] = point.point --it's possible that the ME could move to the point = Vec2 notation. + end + end + return points + end + return + end --if group_data and group_data.name and group_data.name == 'groupname' + end --for group_num, group_data in pairs(obj_type_data.group) do + end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then + end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then + end --for obj_type_name, obj_type_data in pairs(cntry_data) do + end --for cntry_id, cntry_data in pairs(coa_data.country) do + end --if coa_data.country then --there is a country table + end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then + end --for coa_name, coa_data in pairs(mission.coalition) do + end + + --- getUnitAttitude(unit) return values. + -- Yaw, AoA, ClimbAngle - relative to earth reference + -- DOES NOT TAKE INTO ACCOUNT WIND. + -- @table attitude + -- @tfield number Heading in radians, range of 0 to 2*pi, + -- relative to true north. + -- @tfield number Pitch in radians, range of -pi/2 to pi/2 + -- @tfield number Roll in radians, range of 0 to 2*pi, + -- right roll is positive direction. + -- @tfield number Yaw in radians, range of -pi to pi, + -- right yaw is positive direction. + -- @tfield number AoA in radians, range of -pi to pi, + -- rotation of aircraft to the right in comparison to + -- flight direction being positive. + -- @tfield number ClimbAngle in radians, range of -pi/2 to pi/2 + + --- Returns the attitude of a given unit. + -- Will work on any unit, even if not an aircraft. + -- @tparam Unit unit unit whose attitude is returned. + -- @treturn table @{attitude} + function mist.getAttitude(unit) + local unitpos = unit:getPosition() + if unitpos then + + local Heading = math.atan2(unitpos.x.z, unitpos.x.x) + + Heading = Heading + mist.getNorthCorrection(unitpos.p) + + if Heading < 0 then + Heading = Heading + 2*math.pi -- put heading in range of 0 to 2*pi + end + ---- heading complete.---- + + local Pitch = math.asin(unitpos.x.y) + ---- pitch complete.---- + + -- now get roll: + --maybe not the best way to do it, but it works. + + --first, make a vector that is perpendicular to y and unitpos.x with cross product + local cp = mist.vec.cp(unitpos.x, {x = 0, y = 1, z = 0}) + + --now, get dot product of of this cross product with unitpos.z + local dp = mist.vec.dp(cp, unitpos.z) + + --now get the magnitude of the roll (magnitude of the angle between two vectors is acos(vec1.vec2/|vec1||vec2|) + local Roll = math.acos(dp/(mist.vec.mag(cp)*mist.vec.mag(unitpos.z))) + + --now, have to get sign of roll. + -- by convention, making right roll positive + -- to get sign of roll, use the y component of unitpos.z. For right roll, y component is negative. + + if unitpos.z.y > 0 then -- left roll, flip the sign of the roll + Roll = -Roll + end + ---- roll complete. ---- + + --now, work on yaw, AoA, climb, and abs velocity + local Yaw + local AoA + local ClimbAngle + + -- get unit velocity + local unitvel = unit:getVelocity() + if mist.vec.mag(unitvel) ~= 0 then --must have non-zero velocity! + local AxialVel = {} --unit velocity transformed into aircraft axes directions + + --transform velocity components in direction of aircraft axes. + AxialVel.x = mist.vec.dp(unitpos.x, unitvel) + AxialVel.y = mist.vec.dp(unitpos.y, unitvel) + AxialVel.z = mist.vec.dp(unitpos.z, unitvel) + + --Yaw is the angle between unitpos.x and the x and z velocities + --define right yaw as positive + Yaw = math.acos(mist.vec.dp({x = 1, y = 0, z = 0}, {x = AxialVel.x, y = 0, z = AxialVel.z})/mist.vec.mag({x = AxialVel.x, y = 0, z = AxialVel.z})) + + --now set correct direction: + if AxialVel.z > 0 then + Yaw = -Yaw + end + + -- AoA is angle between unitpos.x and the x and y velocities + AoA = math.acos(mist.vec.dp({x = 1, y = 0, z = 0}, {x = AxialVel.x, y = AxialVel.y, z = 0})/mist.vec.mag({x = AxialVel.x, y = AxialVel.y, z = 0})) + + --now set correct direction: + if AxialVel.y > 0 then + AoA = -AoA + end + + ClimbAngle = math.asin(unitvel.y/mist.vec.mag(unitvel)) + end + return { Heading = Heading, Pitch = Pitch, Roll = Roll, Yaw = Yaw, AoA = AoA, ClimbAngle = ClimbAngle} + else + log:error("Couldn't get unit's position") + end + end + + --- Returns heading of given unit. + -- @tparam Unit unit unit whose heading is returned. + -- @param rawHeading + -- @treturn number heading of the unit, in range + -- of 0 to 2*pi. + function mist.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 + mist.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 + + --- Returns given unit's pitch + -- @tparam Unit unit unit whose pitch is returned. + -- @treturn number pitch of given unit + function mist.getPitch(unit) + local unitpos = unit:getPosition() + if unitpos then + return math.asin(unitpos.x.y) + end + end + + --- Returns given unit's roll. + -- @tparam Unit unit unit whose roll is returned. + -- @treturn number roll of given unit + function mist.getRoll(unit) + local unitpos = unit:getPosition() + if unitpos then + -- now get roll: + --maybe not the best way to do it, but it works. + + --first, make a vector that is perpendicular to y and unitpos.x with cross product + local cp = mist.vec.cp(unitpos.x, {x = 0, y = 1, z = 0}) + + --now, get dot product of of this cross product with unitpos.z + local dp = mist.vec.dp(cp, unitpos.z) + + --now get the magnitude of the roll (magnitude of the angle between two vectors is acos(vec1.vec2/|vec1||vec2|) + local Roll = math.acos(dp/(mist.vec.mag(cp)*mist.vec.mag(unitpos.z))) + + --now, have to get sign of roll. + -- by convention, making right roll positive + -- to get sign of roll, use the y component of unitpos.z. For right roll, y component is negative. + + if unitpos.z.y > 0 then -- left roll, flip the sign of the roll + Roll = -Roll + end + return Roll + end + end + + --- Returns given unit's yaw. + -- @tparam Unit unit unit whose yaw is returned. + -- @treturn number yaw of given unit. + function mist.getYaw(unit) + local unitpos = unit:getPosition() + if unitpos then + -- get unit velocity + local unitvel = unit:getVelocity() + if mist.vec.mag(unitvel) ~= 0 then --must have non-zero velocity! + local AxialVel = {} --unit velocity transformed into aircraft axes directions + + --transform velocity components in direction of aircraft axes. + AxialVel.x = mist.vec.dp(unitpos.x, unitvel) + AxialVel.y = mist.vec.dp(unitpos.y, unitvel) + AxialVel.z = mist.vec.dp(unitpos.z, unitvel) + + --Yaw is the angle between unitpos.x and the x and z velocities + --define right yaw as positive + local Yaw = math.acos(mist.vec.dp({x = 1, y = 0, z = 0}, {x = AxialVel.x, y = 0, z = AxialVel.z})/mist.vec.mag({x = AxialVel.x, y = 0, z = AxialVel.z})) + + --now set correct direction: + if AxialVel.z > 0 then + Yaw = -Yaw + end + return Yaw + end + end + end + + --- Returns given unit's angle of attack. + -- @tparam Unit unit unit to get AoA from. + -- @treturn number angle of attack of the given unit. + function mist.getAoA(unit) + local unitpos = unit:getPosition() + if unitpos then + local unitvel = unit:getVelocity() + if mist.vec.mag(unitvel) ~= 0 then --must have non-zero velocity! + local AxialVel = {} --unit velocity transformed into aircraft axes directions + + --transform velocity components in direction of aircraft axes. + AxialVel.x = mist.vec.dp(unitpos.x, unitvel) + AxialVel.y = mist.vec.dp(unitpos.y, unitvel) + AxialVel.z = mist.vec.dp(unitpos.z, unitvel) + + -- AoA is angle between unitpos.x and the x and y velocities + local AoA = math.acos(mist.vec.dp({x = 1, y = 0, z = 0}, {x = AxialVel.x, y = AxialVel.y, z = 0})/mist.vec.mag({x = AxialVel.x, y = AxialVel.y, z = 0})) + + --now set correct direction: + if AxialVel.y > 0 then + AoA = -AoA + end + return AoA + end + end + end + + --- Returns given unit's climb angle. + -- @tparam Unit unit unit to get climb angle from. + -- @treturn number climb angle of given unit. + function mist.getClimbAngle(unit) + local unitpos = unit:getPosition() + if unitpos then + local unitvel = unit:getVelocity() + if mist.vec.mag(unitvel) ~= 0 then --must have non-zero velocity! + return math.asin(unitvel.y/mist.vec.mag(unitvel)) + end + end + end + + --[[-- + Unit name table. + Many Mist functions require tables of unit names, which are known + in Mist as UnitNameTables. These follow a special set of shortcuts + borrowed from Slmod. These shortcuts alleviate the problem of entering + huge lists of unit names by hand, and in many cases, they remove the + need to even know the names of the units in the first place! + + These are the unit table "short-cut" commands: + + Prefixes: + "[-u]" - subtract this unit if its in the table + "[g]" - add this group to the table + "[-g]" - subtract this group from the table + "[c]" - add this country's units + "[-c]" - subtract this country's units if any are in the table + + Stand-alone identifiers + "[all]" - add all units + "[-all]" - subtract all units (not very useful by itself) + "[blue]" - add all blue units + "[-blue]" - subtract all blue units + "[red]" - add all red coalition units + "[-red]" - subtract all red units + + Compound Identifiers: + "[c][helicopter]" - add all of this country's helicopters + "[-c][helicopter]" - subtract all of this country's helicopters + "[c][plane]" - add all of this country's planes + "[-c][plane]" - subtract all of this country's planes + "[c][ship]" - add all of this country's ships + "[-c][ship]" - subtract all of this country's ships + "[c][vehicle]" - add all of this country's vehicles + "[-c][vehicle]" - subtract all of this country's vehicles + + "[all][helicopter]" - add all helicopters + "[-all][helicopter]" - subtract all helicopters + "[all][plane]" - add all planes + "[-all][plane]" - subtract all planes + "[all][ship]" - add all ships + "[-all][ship]" - subtract all ships + "[all][vehicle]" - add all vehicles + "[-all][vehicle]" - subtract all vehicles + + "[blue][helicopter]" - add all blue coalition helicopters + "[-blue][helicopter]" - subtract all blue coalition helicopters + "[blue][plane]" - add all blue coalition planes + "[-blue][plane]" - subtract all blue coalition planes + "[blue][ship]" - add all blue coalition ships + "[-blue][ship]" - subtract all blue coalition ships + "[blue][vehicle]" - add all blue coalition vehicles + "[-blue][vehicle]" - subtract all blue coalition vehicles + + "[red][helicopter]" - add all red coalition helicopters + "[-red][helicopter]" - subtract all red coalition helicopters + "[red][plane]" - add all red coalition planes + "[-red][plane]" - subtract all red coalition planes + "[red][ship]" - add all red coalition ships + "[-red][ship]" - subtract all red coalition ships + "[red][vehicle]" - add all red coalition vehicles + "[-red][vehicle]" - subtract all red coalition vehicles + + Country names to be used in [c] and [-c] short-cuts: + Turkey + Norway + The Netherlands + Spain + 11 + UK + Denmark + USA + Georgia + Germany + Belgium + Canada + France + Israel + Ukraine + Russia + South Ossetia + Abkhazia + Italy + Australia + Austria + Belarus + Bulgaria + Czech Republic + China + Croatia + Finland + Greece + Hungary + India + Iran + Iraq + Japan + Kazakhstan + North Korea + Pakistan + Poland + Romania + Saudi Arabia + Serbia, Slovakia + South Korea + Sweden + Switzerland + Syria + USAF Aggressors + + Do NOT use a '[u]' notation for single units. Single units are referenced + the same way as before: Simply input their names as strings. + + These unit tables are evaluated in order, and you cannot subtract a unit + from a table before it is added. For example: + + {'[blue]', '[-c]Georgia'} + + will evaluate to all of blue coalition except those units owned by the + country named "Georgia"; however: + + {'[-c]Georgia', '[blue]'} + + will evaluate to all of the units in blue coalition, because the addition + of all units owned by blue coalition occurred AFTER the subtraction of all + units owned by Georgia (which actually subtracted nothing at all, since + there were no units in the table when the subtraction occurred). + + More examples: + + {'[blue][plane]', '[-c]Georgia', '[-g]Hawg 1'} + + Evaluates to all blue planes, except those blue units owned by the country + named "Georgia" and the units in the group named "Hawg1". + + + {'[g]arty1', '[g]arty2', '[-u]arty1_AD', '[-u]arty2_AD', 'Shark 11' } + + Evaluates to the unit named "Shark 11", plus all the units in groups named + "arty1" and "arty2" except those that are named "arty1\_AD" and "arty2\_AD". + + @table UnitNameTable + ]] + + --- Returns a table containing unit names. + -- @tparam table tbl sequential strings + -- @treturn table @{UnitNameTable} + function mist.makeUnitTable(tbl) + --Assumption: will be passed a table of strings, sequential + --log:info(tbl) + local units_by_name = {} + + local l_munits = mist.DBs.units --local reference for faster execution + for i = 1, #tbl do + local unit = tbl[i] + if unit:sub(1,4) == '[-u]' then --subtract a unit + if units_by_name[unit:sub(5)] then -- 5 to end + units_by_name[unit:sub(5)] = nil --remove + end + elseif unit:sub(1,3) == '[g]' then -- add a group + for coa, coa_tbl in pairs(l_munits) do + for country, country_table in pairs(coa_tbl) do + for unit_type, unit_type_tbl in pairs(country_table) do + if type(unit_type_tbl) == 'table' then + for group_ind, group_tbl in pairs(unit_type_tbl) do + if type(group_tbl) == 'table' and group_tbl.groupName == unit:sub(4) then + -- index 4 to end + for unit_ind, unit in pairs(group_tbl.units) do + units_by_name[unit.unitName] = true --add + end + end + end + end + end + end + end + elseif unit:sub(1,4) == '[-g]' then -- subtract a group + for coa, coa_tbl in pairs(l_munits) do + for country, country_table in pairs(coa_tbl) do + for unit_type, unit_type_tbl in pairs(country_table) do + if type(unit_type_tbl) == 'table' then + for group_ind, group_tbl in pairs(unit_type_tbl) do + if type(group_tbl) == 'table' and group_tbl.groupName == unit:sub(5) then + -- index 5 to end + for unit_ind, unit in pairs(group_tbl.units) do + if units_by_name[unit.unitName] then + units_by_name[unit.unitName] = nil --remove + end + end + end + end + end + end + end + end + elseif unit:sub(1,3) == '[c]' then -- add a country + local category = '' + local country_start = 4 + if unit:sub(4,15) == '[helicopter]' then + category = 'helicopter' + country_start = 16 + elseif unit:sub(4,10) == '[plane]' then + category = 'plane' + country_start = 11 + elseif unit:sub(4,9) == '[ship]' then + category = 'ship' + country_start = 10 + elseif unit:sub(4,12) == '[vehicle]' then + category = 'vehicle' + country_start = 13 + end + for coa, coa_tbl in pairs(l_munits) do + for country, country_table in pairs(coa_tbl) do + if country == string.lower(unit:sub(country_start)) then -- match + for unit_type, unit_type_tbl in pairs(country_table) do + if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then + for group_ind, group_tbl in pairs(unit_type_tbl) do + if type(group_tbl) == 'table' then + for unit_ind, unit in pairs(group_tbl.units) do + units_by_name[unit.unitName] = true --add + end + end + end + end + end + end + end + end + elseif unit:sub(1,4) == '[-c]' then -- subtract a country + local category = '' + local country_start = 5 + if unit:sub(5,16) == '[helicopter]' then + category = 'helicopter' + country_start = 17 + elseif unit:sub(5,11) == '[plane]' then + category = 'plane' + country_start = 12 + elseif unit:sub(5,10) == '[ship]' then + category = 'ship' + country_start = 11 + elseif unit:sub(5,13) == '[vehicle]' then + category = 'vehicle' + country_start = 14 + end + for coa, coa_tbl in pairs(l_munits) do + for country, country_table in pairs(coa_tbl) do + if country == string.lower(unit:sub(country_start)) then -- match + for unit_type, unit_type_tbl in pairs(country_table) do + if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then + for group_ind, group_tbl in pairs(unit_type_tbl) do + if type(group_tbl) == 'table' then + for unit_ind, unit in pairs(group_tbl.units) do + if units_by_name[unit.unitName] then + units_by_name[unit.unitName] = nil --remove + end + end + end + end + end + end + end + end + end + elseif unit:sub(1,6) == '[blue]' then -- add blue coalition + local category = '' + if unit:sub(7) == '[helicopter]' then + category = 'helicopter' + elseif unit:sub(7) == '[plane]' then + category = 'plane' + elseif unit:sub(7) == '[ship]' then + category = 'ship' + elseif unit:sub(7) == '[vehicle]' then + category = 'vehicle' + end + for coa, coa_tbl in pairs(l_munits) do + if coa == 'blue' then + for country, country_table in pairs(coa_tbl) do + for unit_type, unit_type_tbl in pairs(country_table) do + if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then + for group_ind, group_tbl in pairs(unit_type_tbl) do + if type(group_tbl) == 'table' then + for unit_ind, unit in pairs(group_tbl.units) do + units_by_name[unit.unitName] = true --add + end + end + end + end + end + end + end + end + elseif unit:sub(1,7) == '[-blue]' then -- subtract blue coalition + local category = '' + if unit:sub(8) == '[helicopter]' then + category = 'helicopter' + elseif unit:sub(8) == '[plane]' then + category = 'plane' + elseif unit:sub(8) == '[ship]' then + category = 'ship' + elseif unit:sub(8) == '[vehicle]' then + category = 'vehicle' + end + for coa, coa_tbl in pairs(l_munits) do + if coa == 'blue' then + for country, country_table in pairs(coa_tbl) do + for unit_type, unit_type_tbl in pairs(country_table) do + if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then + for group_ind, group_tbl in pairs(unit_type_tbl) do + if type(group_tbl) == 'table' then + for unit_ind, unit in pairs(group_tbl.units) do + if units_by_name[unit.unitName] then + units_by_name[unit.unitName] = nil --remove + end + end + end + end + end + end + end + end + end + elseif unit:sub(1,5) == '[red]' then -- add red coalition + local category = '' + if unit:sub(6) == '[helicopter]' then + category = 'helicopter' + elseif unit:sub(6) == '[plane]' then + category = 'plane' + elseif unit:sub(6) == '[ship]' then + category = 'ship' + elseif unit:sub(6) == '[vehicle]' then + category = 'vehicle' + end + for coa, coa_tbl in pairs(l_munits) do + if coa == 'red' then + for country, country_table in pairs(coa_tbl) do + for unit_type, unit_type_tbl in pairs(country_table) do + if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then + for group_ind, group_tbl in pairs(unit_type_tbl) do + if type(group_tbl) == 'table' then + for unit_ind, unit in pairs(group_tbl.units) do + units_by_name[unit.unitName] = true --add + end + end + end + end + end + end + end + end + elseif unit:sub(1,6) == '[-red]' then -- subtract red coalition + local category = '' + if unit:sub(7) == '[helicopter]' then + category = 'helicopter' + elseif unit:sub(7) == '[plane]' then + category = 'plane' + elseif unit:sub(7) == '[ship]' then + category = 'ship' + elseif unit:sub(7) == '[vehicle]' then + category = 'vehicle' + end + for coa, coa_tbl in pairs(l_munits) do + if coa == 'red' then + for country, country_table in pairs(coa_tbl) do + for unit_type, unit_type_tbl in pairs(country_table) do + if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then + for group_ind, group_tbl in pairs(unit_type_tbl) do + if type(group_tbl) == 'table' then + for unit_ind, unit in pairs(group_tbl.units) do + if units_by_name[unit.unitName] then + units_by_name[unit.unitName] = nil --remove + end + end + end + end + end + end + end + end + end + elseif unit:sub(1,5) == '[all]' then -- add all of a certain category (or all categories) + local category = '' + if unit:sub(6) == '[helicopter]' then + category = 'helicopter' + elseif unit:sub(6) == '[plane]' then + category = 'plane' + elseif unit:sub(6) == '[ship]' then + category = 'ship' + elseif unit:sub(6) == '[vehicle]' then + category = 'vehicle' + end + for coa, coa_tbl in pairs(l_munits) do + for country, country_table in pairs(coa_tbl) do + for unit_type, unit_type_tbl in pairs(country_table) do + if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then + for group_ind, group_tbl in pairs(unit_type_tbl) do + if type(group_tbl) == 'table' then + for unit_ind, unit in pairs(group_tbl.units) do + units_by_name[unit.unitName] = true --add + end + end + end + end + end + end + end + elseif unit:sub(1,6) == '[-all]' then -- subtract all of a certain category (or all categories) + local category = '' + if unit:sub(7) == '[helicopter]' then + category = 'helicopter' + elseif unit:sub(7) == '[plane]' then + category = 'plane' + elseif unit:sub(7) == '[ship]' then + category = 'ship' + elseif unit:sub(7) == '[vehicle]' then + category = 'vehicle' + end + for coa, coa_tbl in pairs(l_munits) do + for country, country_table in pairs(coa_tbl) do + for unit_type, unit_type_tbl in pairs(country_table) do + if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then + for group_ind, group_tbl in pairs(unit_type_tbl) do + if type(group_tbl) == 'table' then + for unit_ind, unit in pairs(group_tbl.units) do + if units_by_name[unit.unitName] then + units_by_name[unit.unitName] = nil --remove + end + end + end + end + end + end + end + end + else -- just a regular unit + units_by_name[unit] = true --add + end + end + + local units_tbl = {} -- indexed sequentially + for unit_name, val in pairs(units_by_name) do + if val then + units_tbl[#units_tbl + 1] = unit_name -- add all the units to the table + end + end + + + units_tbl.processed = timer.getTime() --add the processed flag + return units_tbl +end + +function mist.getDeadMapObjsInZones(zone_names) + -- zone_names: table of zone names + -- returns: table of dead map objects (indexed numerically) + local map_objs = {} + local zones = {} + for i = 1, #zone_names do + if mist.DBs.zonesByName[zone_names[i]] then + zones[#zones + 1] = mist.DBs.zonesByName[zone_names[i]] + end + end + for obj_id, obj in pairs(mist.DBs.deadObjects) do + if obj.objectType and obj.objectType == 'building' then --dead map object + for i = 1, #zones do + if ((zones[i].point.x - obj.objectPos.x)^2 + (zones[i].point.z - obj.objectPos.z)^2)^0.5 <= zones[i].radius then + map_objs[#map_objs + 1] = mist.utils.deepCopy(obj) + end + end + end + end + return map_objs +end + +function mist.getDeadMapObjsInPolygonZone(zone) + -- zone_names: table of zone names + -- returns: table of dead map objects (indexed numerically) + local map_objs = {} + for obj_id, obj in pairs(mist.DBs.deadObjects) do + if obj.objectType and obj.objectType == 'building' then --dead map object + if mist.pointInPolygon(obj.objectPos, zone) then + map_objs[#map_objs + 1] = mist.utils.deepCopy(obj) + end + end + end + return map_objs +end + +function mist.pointInPolygon(point, poly, maxalt) --raycasting point in polygon. Code from http://softsurfer.com/Archive/algorithm_0103/algorithm_0103.htm + --[[local type_tbl = { + point = {'table'}, + poly = {'table'}, + maxalt = {'number', 'nil'}, + } + + local err, errmsg = mist.utils.typeCheck('mist.pointInPolygon', type_tbl, {point, poly, maxalt}) + assert(err, errmsg) + ]] + point = mist.utils.makeVec3(point) + local px = point.x + local pz = point.z + local cn = 0 + local newpoly = mist.utils.deepCopy(poly) + + if not maxalt or (point.y <= maxalt) then + local polysize = #newpoly + newpoly[#newpoly + 1] = newpoly[1] + + newpoly[1] = mist.utils.makeVec3(newpoly[1]) + + for k = 1, polysize do + newpoly[k+1] = mist.utils.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 + else + return false + end +end + +function mist.getUnitsInPolygon(unit_names, polyZone, max_alt) + local units = {} + + for i = 1, #unit_names do + units[#units + 1] = Unit.getByName(unit_names[i]) + end + + local inZoneUnits = {} + for i =1, #units do + if units[i]:isActive() and mist.pointInPolygon(units[i]:getPosition().p, polyZone, max_alt) then + inZoneUnits[#inZoneUnits + 1] = units[i] + end + end + + return inZoneUnits +end + +function mist.getUnitsInZones(unit_names, zone_names, zone_type) + + zone_type = zone_type or 'cylinder' + if zone_type == 'c' or zone_type == 'cylindrical' or zone_type == 'C' then + zone_type = 'cylinder' + end + if zone_type == 's' or zone_type == 'spherical' or zone_type == 'S' then + zone_type = 'sphere' + end + + assert(zone_type == 'cylinder' or zone_type == 'sphere', 'invalid zone_type: ' .. tostring(zone_type)) + + local units = {} + local zones = {} + + for k = 1, #unit_names do + local unit = Unit.getByName(unit_names[k]) + if unit then + units[#units + 1] = unit + end + end + + + for k = 1, #zone_names do + local zone = trigger.misc.getZone(zone_names[k]) + if zone then + zones[#zones + 1] = {radius = zone.radius, x = zone.point.x, y = zone.point.y, z = zone.point.z} + end + end + + local in_zone_units = {} + + for units_ind = 1, #units do + for zones_ind = 1, #zones do + if zone_type == 'sphere' then --add land height value for sphere zone type + local alt = land.getHeight({x = zones[zones_ind].x, y = zones[zones_ind].z}) + if alt then + zones[zones_ind].y = alt + end + end + local unit_pos = units[units_ind]:getPosition().p + if unit_pos and units[units_ind]:isActive() == true then + if zone_type == 'cylinder' and (((unit_pos.x - zones[zones_ind].x)^2 + (unit_pos.z - zones[zones_ind].z)^2)^0.5 <= zones[zones_ind].radius) then + in_zone_units[#in_zone_units + 1] = units[units_ind] + break + elseif zone_type == 'sphere' and (((unit_pos.x - zones[zones_ind].x)^2 + (unit_pos.y - zones[zones_ind].y)^2 + (unit_pos.z - zones[zones_ind].z)^2)^0.5 <= zones[zones_ind].radius) then + in_zone_units[#in_zone_units + 1] = units[units_ind] + break + end + end + end + end + return in_zone_units +end + +function mist.getUnitsInMovingZones(unit_names, zone_unit_names, radius, zone_type) + + zone_type = zone_type or 'cylinder' + if zone_type == 'c' or zone_type == 'cylindrical' or zone_type == 'C' then + zone_type = 'cylinder' + end + if zone_type == 's' or zone_type == 'spherical' or zone_type == 'S' then + zone_type = 'sphere' + end + + assert(zone_type == 'cylinder' or zone_type == 'sphere', 'invalid zone_type: ' .. tostring(zone_type)) + + local units = {} + local zone_units = {} + + for k = 1, #unit_names do + local unit = Unit.getByName(unit_names[k]) + if unit then + units[#units + 1] = unit + end + end + + for k = 1, #zone_unit_names do + local unit = Unit.getByName(zone_unit_names[k]) + if unit then + zone_units[#zone_units + 1] = unit + end + end + + local in_zone_units = {} + + for units_ind = 1, #units do + for zone_units_ind = 1, #zone_units do + local unit_pos = units[units_ind]:getPosition().p + local zone_unit_pos = zone_units[zone_units_ind]:getPosition().p + if unit_pos and zone_unit_pos and units[units_ind]:isActive() == true then + if zone_type == 'cylinder' and (((unit_pos.x - zone_unit_pos.x)^2 + (unit_pos.z - zone_unit_pos.z)^2)^0.5 <= radius) then + in_zone_units[#in_zone_units + 1] = units[units_ind] + break + elseif zone_type == 'sphere' and (((unit_pos.x - zone_unit_pos.x)^2 + (unit_pos.y - zone_unit_pos.y)^2 + (unit_pos.z - zone_unit_pos.z)^2)^0.5 <= radius) then + in_zone_units[#in_zone_units + 1] = units[units_ind] + break + end + end + end + end + return in_zone_units +end + +function mist.getUnitsLOS(unitset1, altoffset1, unitset2, altoffset2, radius) + log:info("$1, $2, $3, $4, $5", unitset1, altoffset1, unitset2, altoffset2, radius) + radius = radius or math.huge + local unit_info1 = {} + local unit_info2 = {} + + -- get the positions all in one step, saves execution time. + for unitset1_ind = 1, #unitset1 do + local unit1 = Unit.getByName(unitset1[unitset1_ind]) + if unit1 and unit1:isActive() == true then + unit_info1[#unit_info1 + 1] = {} + unit_info1[#unit_info1].unit = unit1 + unit_info1[#unit_info1].pos = unit1:getPosition().p + end + end + + for unitset2_ind = 1, #unitset2 do + local unit2 = Unit.getByName(unitset2[unitset2_ind]) + if unit2 and unit2:isActive() == true then + unit_info2[#unit_info2 + 1] = {} + unit_info2[#unit_info2].unit = unit2 + unit_info2[#unit_info2].pos = unit2:getPosition().p + end + end + + local LOS_data = {} + -- now compute los + for unit1_ind = 1, #unit_info1 do + local unit_added = false + for unit2_ind = 1, #unit_info2 do + if radius == math.huge or (mist.vec.mag(mist.vec.sub(unit_info1[unit1_ind].pos, unit_info2[unit2_ind].pos)) < radius) then -- inside radius + local point1 = { x = unit_info1[unit1_ind].pos.x, y = unit_info1[unit1_ind].pos.y + altoffset1, z = unit_info1[unit1_ind].pos.z} + local point2 = { x = unit_info2[unit2_ind].pos.x, y = unit_info2[unit2_ind].pos.y + altoffset2, z = unit_info2[unit2_ind].pos.z} + if land.isVisible(point1, point2) then + if unit_added == false then + unit_added = true + LOS_data[#LOS_data + 1] = {} + LOS_data[#LOS_data].unit = unit_info1[unit1_ind].unit + LOS_data[#LOS_data].vis = {} + LOS_data[#LOS_data].vis[#LOS_data[#LOS_data].vis + 1] = unit_info2[unit2_ind].unit + else + LOS_data[#LOS_data].vis[#LOS_data[#LOS_data].vis + 1] = unit_info2[unit2_ind].unit + end + end + end + end + end + + return LOS_data +end + +function mist.getAvgPoint(points) + local avgX, avgY, avgZ, totNum = 0, 0, 0, 0 + for i = 1, #points do + local nPoint = mist.utils.makeVec3(points[i]) + if nPoint.z then + avgX = avgX + nPoint.x + avgY = avgY + nPoint.y + avgZ = avgZ + nPoint.z + totNum = totNum + 1 + end + end + if totNum ~= 0 then + return {x = avgX/totNum, y = avgY/totNum, z = avgZ/totNum} + end +end + +--Gets the average position of a group of units (by name) +function mist.getAvgPos(unitNames) + local avgX, avgY, avgZ, totNum = 0, 0, 0, 0 + for i = 1, #unitNames do + local unit + if Unit.getByName(unitNames[i]) then + unit = Unit.getByName(unitNames[i]) + elseif StaticObject.getByName(unitNames[i]) then + unit = StaticObject.getByName(unitNames[i]) + end + if unit then + local pos = unit:getPosition().p + if pos then -- you never know O.o + avgX = avgX + pos.x + avgY = avgY + pos.y + avgZ = avgZ + pos.z + totNum = totNum + 1 + end + end + end + if totNum ~= 0 then + return {x = avgX/totNum, y = avgY/totNum, z = avgZ/totNum} + end +end + +function mist.getAvgGroupPos(groupName) + if type(groupName) == 'string' and Group.getByName(groupName) and Group.getByName(groupName):isExist() == true then + groupName = Group.getByName(groupName) + end + local units = {} + for i = 1, groupName:getSize() do + table.insert(units, groupName:getUnit(i):getName()) + end + + return mist.getAvgPos(units) + +end + +--[[ vars for mist.getMGRSString: +vars.units - table of unit names (NOT unitNameTable- maybe this should change). +vars.acc - integer between 0 and 5, inclusive +]] +function mist.getMGRSString(vars) + local units = vars.units + local acc = vars.acc or 5 + local avgPos = mist.getAvgPos(units) + if avgPos then + return mist.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(avgPos)), acc) + end +end + +--[[ vars for mist.getLLString +vars.units - table of unit names (NOT unitNameTable- maybe this should change). +vars.acc - integer, number of numbers after decimal place +vars.DMS - if true, output in degrees, minutes, seconds. Otherwise, output in degrees, minutes. +]] +function mist.getLLString(vars) + local units = vars.units + local acc = vars.acc or 3 + local DMS = vars.DMS + local avgPos = mist.getAvgPos(units) + if avgPos then + local lat, lon = coord.LOtoLL(avgPos) + return mist.tostringLL(lat, lon, acc, DMS) + end +end + +--[[ +vars.units- table of unit names (NOT unitNameTable- maybe this should change). +vars.ref - vec3 ref point, maybe overload for vec2 as well? +vars.alt - boolean, if used, includes altitude in string +vars.metric - boolean, gives distance in km instead of NM. +]] +function mist.getBRString(vars) + local units = vars.units + local ref = mist.utils.makeVec3(vars.ref, 0) -- turn it into Vec3 if it is not already. + local alt = vars.alt + local metric = vars.metric + local avgPos = mist.getAvgPos(units) + if avgPos then + local vec = {x = avgPos.x - ref.x, y = avgPos.y - ref.y, z = avgPos.z - ref.z} + local dir = mist.utils.getDir(vec, ref) + local dist = mist.utils.get2DDist(avgPos, ref) + if alt then + alt = avgPos.y + end + return mist.tostringBR(dir, dist, alt, metric) + end +end + +-- Returns the Vec3 coordinates of the average position of the concentration of units most in the heading direction. +--[[ vars for mist.getLeadingPos: +vars.units - table of unit names +vars.heading - direction +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees +]] +function mist.getLeadingPos(vars) + local units = vars.units + local heading = vars.heading + local radius = vars.radius + if vars.headingDegrees then + heading = mist.utils.toRadian(vars.headingDegrees) + end + + local unitPosTbl = {} + for i = 1, #units do + local unit = Unit.getByName(units[i]) + if unit and unit:isExist() then + unitPosTbl[#unitPosTbl + 1] = unit:getPosition().p + end + end + if #unitPosTbl > 0 then -- one more more units found. + -- first, find the unit most in the heading direction + local maxPos = -math.huge + + local maxPosInd -- maxPos - the furthest in direction defined by heading; maxPosInd = + for i = 1, #unitPosTbl do + local rotatedVec2 = mist.vec.rotateVec2(mist.utils.makeVec2(unitPosTbl[i]), heading) + if (not maxPos) or maxPos < rotatedVec2.x then + maxPos = rotatedVec2.x + maxPosInd = i + end + end + + --now, get all the units around this unit... + local avgPos + if radius then + local maxUnitPos = unitPosTbl[maxPosInd] + local avgx, avgy, avgz, totNum = 0, 0, 0, 0 + for i = 1, #unitPosTbl do + if mist.utils.get2DDist(maxUnitPos, unitPosTbl[i]) <= radius then + avgx = avgx + unitPosTbl[i].x + avgy = avgy + unitPosTbl[i].y + avgz = avgz + unitPosTbl[i].z + totNum = totNum + 1 + end + end + avgPos = { x = avgx/totNum, y = avgy/totNum, z = avgz/totNum} + else + avgPos = unitPosTbl[maxPosInd] + end + + return avgPos + end +end + +--[[ vars for mist.getLeadingMGRSString: +vars.units - table of unit names +vars.heading - direction +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees +vars.acc - number, 0 to 5. +]] +function mist.getLeadingMGRSString(vars) + local pos = mist.getLeadingPos(vars) + if pos then + local acc = vars.acc or 5 + return mist.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(pos)), acc) + end +end + +--[[ vars for mist.getLeadingLLString: +vars.units - table of unit names +vars.heading - direction, number +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees +vars.acc - number of digits after decimal point (can be negative) +vars.DMS - boolean, true if you want DMS. +]] +function mist.getLeadingLLString(vars) + local pos = mist.getLeadingPos(vars) + if pos then + local acc = vars.acc or 3 + local DMS = vars.DMS + local lat, lon = coord.LOtoLL(pos) + return mist.tostringLL(lat, lon, acc, DMS) + end +end + +--[[ vars for mist.getLeadingBRString: +vars.units - table of unit names +vars.heading - direction, number +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees +vars.metric - boolean, if true, use km instead of NM. +vars.alt - boolean, if true, include altitude. +vars.ref - vec3/vec2 reference point. +]] +function mist.getLeadingBRString(vars) + local pos = mist.getLeadingPos(vars) + if pos then + local ref = vars.ref + local alt = vars.alt + local metric = vars.metric + + local vec = {x = pos.x - ref.x, y = pos.y - ref.y, z = pos.z - ref.z} + local dir = mist.utils.getDir(vec, ref) + local dist = mist.utils.get2DDist(pos, ref) + if alt then + alt = pos.y + end + return mist.tostringBR(dir, dist, alt, metric) + end +end + +end + +--- Group functions. +-- @section groups +do -- group functions scope + + --- Check table used for group creation. + -- @tparam table groupData table to check. + -- @treturn boolean true if a group can be spawned using + -- this table, false otherwise. + function mist.groupTableCheck(groupData) + -- return false if country, category + -- or units are missing + if not groupData.country or + not groupData.category or + not groupData.units then + return false + end + -- return false if unitData misses + -- x, y or type + for unitId, unitData in pairs(groupData.units) do + if not unitData.x or + not unitData.y or + not unitData.type then + return false + end + end + -- everything we need is here return true + return true + end + + --- Returns group data table of give group. + function mist.getCurrentGroupData(gpName) + local dbData = mist.getGroupData(gpName) + + if Group.getByName(gpName) and Group.getByName(gpName):isExist() == true then + local newGroup = Group.getByName(gpName) + local newData = {} + newData.name = gpName + newData.groupId = tonumber(newGroup:getID()) + newData.category = newGroup:getCategory() + newData.groupName = gpName + newData.hidden = dbData.hidden + + if newData.category == 2 then + newData.category = 'vehicle' + elseif newData.category == 3 then + newData.category = 'ship' + end + + newData.units = {} + local newUnits = newGroup:getUnits() + for unitNum, unitData in pairs(newGroup:getUnits()) do + newData.units[unitNum] = {} + local uName = unitData:getName() + + if mist.DBs.unitsByName[uName] and unitData:getTypeName() == mist.DBs.unitsByName[uName].type and mist.DBs.unitsByName[uName].unitId == tonumber(unitData:getID()) then -- If old data matches most of new data + newData.units[unitNum] = mist.utils.deepCopy(mist.DBs.unitsByName[uName]) + else + newData.units[unitNum].unitId = tonumber(unitData:getID()) + newData.units[unitNum].type = unitData:getTypeName() + newData.units[unitNum].skill = mist.getUnitSkill(uName) + newData.country = string.lower(country.name[unitData:getCountry()]) + newData.units[unitNum].callsign = unitData:getCallsign() + newData.units[unitNum].unitName = uName + end + + newData.units[unitNum].x = unitData:getPosition().p.x + newData.units[unitNum].y = unitData:getPosition().p.z + newData.units[unitNum].point = {x = newData.units[unitNum].x, y = newData.units[unitNum].y} + newData.units[unitNum].heading = mist.getHeading(unitData, true) -- added to DBs + newData.units[unitNum].alt = unitData:getPosition().p.y + newData.units[unitNum].speed = mist.vec.mag(unitData:getVelocity()) + + end + + return newData + elseif StaticObject.getByName(gpName) and StaticObject.getByName(gpName):isExist() == true then + local staticObj = StaticObject.getByName(gpName) + dbData.units[1].x = staticObj:getPosition().p.x + dbData.units[1].y = staticObj:getPosition().p.z + dbData.units[1].alt = staticObj:getPosition().p.y + dbData.units[1].heading = mist.getHeading(staticObj, true) + + return dbData + end + + end + + function mist.getGroupData(gpName) + local found = false + local newData = {} + if mist.DBs.groupsByName[gpName] then + newData = mist.utils.deepCopy(mist.DBs.groupsByName[gpName]) + found = true + end + + if found == false then + for groupName, groupData in pairs(mist.DBs.groupsByName) do + if mist.stringMatch(groupName, gpName) == true then + newData = mist.utils.deepCopy(groupData) + newData.groupName = groupName + found = true + break + end + end + end + + local payloads + if newData.category == 'plane' or newData.category == 'helicopter' then + payloads = mist.getGroupPayload(newData.groupName) + end + if found == true then + --newData.hidden = false -- maybe add this to DBs + + for unitNum, unitData in pairs(newData.units) do + newData.units[unitNum] = {} + + newData.units[unitNum].unitId = unitData.unitId + --newData.units[unitNum].point = unitData.point + newData.units[unitNum].x = unitData.point.x + newData.units[unitNum].y = unitData.point.y + newData.units[unitNum].alt = unitData.alt + newData.units[unitNum].alt_type = unitData.alt_type + newData.units[unitNum].speed = unitData.speed + newData.units[unitNum].type = unitData.type + newData.units[unitNum].skill = unitData.skill + newData.units[unitNum].unitName = unitData.unitName + newData.units[unitNum].heading = unitData.heading -- added to DBs + newData.units[unitNum].playerCanDrive = unitData.playerCanDrive -- added to DBs + + + if newData.category == 'plane' or newData.category == 'helicopter' then + newData.units[unitNum].payload = payloads[unitNum] + newData.units[unitNum].livery_id = unitData.livery_id + newData.units[unitNum].onboard_num = unitData.onboard_num + newData.units[unitNum].callsign = unitData.callsign + newData.units[unitNum].AddPropAircraft = unitData.AddPropAircraft + end + if newData.category == 'static' then + newData.units[unitNum].categoryStatic = unitData.categoryStatic + newData.units[unitNum].mass = unitData.mass + newData.units[unitNum].canCargo = unitData.canCargo + newData.units[unitNum].shape_name = unitData.shape_name + end + end + --log:info(newData) + return newData + else + log:error('$1 not found in MIST database', gpName) + return + end + end + + function mist.getPayload(unitIdent) + -- refactor to search by groupId and allow groupId and groupName as inputs + local unitId = unitIdent + if type(unitIdent) == 'string' and not tonumber(unitIdent) then + if mist.DBs.MEunitsByName[unitIdent] then + unitId = mist.DBs.MEunitsByName[unitIdent].unitId + else + log:error("Unit not found in mist.DBs.MEunitsByName: $1", unitIdent) + end + end + local gpId = mist.DBs.MEunitsById[unitId].groupId + + 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 + log:error('Need string or number. Got: $1', type(unitIdent)) + return false + end + log:warn("Couldn't find payload for unit: $1", unitIdent) + return + end + + function mist.getGroupPayload(groupIdent) + local gpId = groupIdent + if type(groupIdent) == 'string' and not tonumber(groupIdent) then + if mist.DBs.MEgroupsByName[groupIdent] then + gpId = mist.DBs.MEgroupsByName[groupIdent].groupId + else + log:error('$1 not found in mist.DBs.MEgroupsByName', groupIdent) + end + end + + if gpId then + for coa_name, coa_data in pairs(env.mission.coalition) do + if 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 + local payloads = {} + for unitIndex, unitData in pairs(group_data.units) do --group index + payloads[unitIndex] = unitData.payload + end + return payloads + end + end + end + end + end + end + end + end + end + else + log:error('Need string or number. Got: $1', type(groupIdent)) + return false + end + log:warn("Couldn't find payload for group: $1", groupIdent) + return + + end + + function mist.getValidRandomPoint(vars) + + + end + + function mist.teleportToPoint(vars) -- main teleport function that all of teleport/respawn functions call + --log:info(vars) + local point = vars.point + local gpName + if vars.gpName then + gpName = vars.gpName + elseif vars.groupName then + gpName = vars.groupName + else + log:error('Missing field groupName or gpName in variable table') + end + + local action = vars.action + + local disperse = vars.disperse or false + local maxDisp = vars.maxDisp or 200 + local radius = vars.radius or 0 + local innerRadius = vars.innerRadius + + local route = vars.route + local dbData = false + + local newGroupData + if gpName and not vars.groupData then + if string.lower(action) == 'teleport' or string.lower(action) == 'tele' then + newGroupData = mist.getCurrentGroupData(gpName) + elseif string.lower(action) == 'respawn' then + newGroupData = mist.getGroupData(gpName) + dbData = true + elseif string.lower(action) == 'clone' then + newGroupData = mist.getGroupData(gpName) + newGroupData.clone = 'order66' + dbData = true + else + action = 'tele' + newGroupData = mist.getCurrentGroupData(gpName) + end + else + action = 'tele' + newGroupData = vars.groupData + end + + --log:info('get Randomized Point') + local diff = {x = 0, y = 0} + local newCoord, origCoord + + local validTerrain = {'LAND', 'ROAD', 'SHALLOW_WATER', 'WATER', 'RUNWAY'} + if string.lower(newGroupData.category) == 'ship' then + validTerrain = {'SHALLOW_WATER' , 'WATER'} + elseif string.lower(newGroupData.category) == 'vehicle' then + validTerrain = {'LAND', 'ROAD'} + end + local offsets = {} + if point and radius >= 0 then + local valid = false + -- new thoughts + --[[ Get AVG position of group and max radius distance to that avg point, otherwise use disperse data to get zone area to check + if disperse then + + else + + end + -- ]] + + + + + + + ---- old + for i = 1, 100 do + newCoord = mist.getRandPointInCircle(point, radius, innerRadius) + if mist.isTerrainValid(newCoord, validTerrain) then + origCoord = mist.utils.deepCopy(newCoord) + diff = {x = (newCoord.x - newGroupData.units[1].x), y = (newCoord.y - newGroupData.units[1].y)} + valid = true + break + end + end + if valid == false then + log:error('Point supplied in variable table is not a valid coordinate. Valid coords: $1', validTerrain) + return false + end + end + if not newGroupData.country and mist.DBs.groupsByName[newGroupData.groupName].country then + newGroupData.country = mist.DBs.groupsByName[newGroupData.groupName].country + end + if not newGroupData.category and mist.DBs.groupsByName[newGroupData.groupName].category then + newGroupData.category = mist.DBs.groupsByName[newGroupData.groupName].category + end + --log:info(point) + for unitNum, unitData in pairs(newGroupData.units) do + --log:info(unitNum) + if disperse then + local unitCoord + if maxDisp and type(maxDisp) == 'number' and unitNum ~= 1 then + for i = 1, 100 do + unitCoord = mist.getRandPointInCircle(origCoord, maxDisp) + if mist.isTerrainValid(unitCoord, validTerrain) == true then + --log:warn('Index: $1, Itered: $2. AT: $3', unitNum, i, unitCoord) + break + end + end + + --else + --newCoord = mist.getRandPointInCircle(zone.point, zone.radius) + end + if unitNum == 1 then + unitCoord = mist.utils.deepCopy(newCoord) + end + if unitCoord then + newGroupData.units[unitNum].x = unitCoord.x + newGroupData.units[unitNum].y = unitCoord.y + end + else + newGroupData.units[unitNum].x = unitData.x + diff.x + newGroupData.units[unitNum].y = unitData.y + diff.y + end + if point then + if (newGroupData.category == 'plane' or newGroupData.category == 'helicopter') then + if point.z and point.y > 0 and point.y > land.getHeight({newGroupData.units[unitNum].x, newGroupData.units[unitNum].y}) + 10 then + newGroupData.units[unitNum].alt = point.y + --log:info('far enough from ground') + else + + if newGroupData.category == 'plane' then + --log:info('setNewAlt') + newGroupData.units[unitNum].alt = land.getHeight({newGroupData.units[unitNum].x, newGroupData.units[unitNum].y}) + math.random(300, 9000) + else + newGroupData.units[unitNum].alt = land.getHeight({newGroupData.units[unitNum].x, newGroupData.units[unitNum].y}) + math.random(200, 3000) + end + end + end + end + end + + if newGroupData.start_time then + newGroupData.startTime = newGroupData.start_time + end + + if newGroupData.startTime and newGroupData.startTime ~= 0 and dbData == true then + local timeDif = timer.getAbsTime() - timer.getTime0() + if timeDif > newGroupData.startTime then + newGroupData.startTime = 0 + else + newGroupData.startTime = newGroupData.startTime - timeDif + end + + end + + if route then + newGroupData.route = route + end + --log:info(newGroupData) + --mist.debug.writeData(mist.utils.serialize,{'teleportToPoint', newGroupData}, 'newGroupData.lua') + if string.lower(newGroupData.category) == 'static' then + --log:info(newGroupData) + return mist.dynAddStatic(newGroupData) + end + return mist.dynAdd(newGroupData) + + end + + function mist.respawnInZone(gpName, zone, disperse, maxDisp) + + if type(gpName) == 'table' and gpName:getName() then + gpName = gpName:getName() + elseif type(gpName) == 'table' and gpName[1]:getName() then + gpName = math.random(#gpName) + else + gpName = tostring(gpName) + end + + if type(zone) == 'string' then + zone = trigger.misc.getZone(zone) + elseif type(zone) == 'table' and not zone.radius then + zone = trigger.misc.getZone(zone[math.random(1, #zone)]) + end + local vars = {} + vars.gpName = gpName + vars.action = 'respawn' + vars.point = zone.point + vars.radius = zone.radius + vars.disperse = disperse + vars.maxDisp = maxDisp + return mist.teleportToPoint(vars) + end + + function mist.cloneInZone(gpName, zone, disperse, maxDisp) + --log:info('cloneInZone') + if type(gpName) == 'table' then + gpName = gpName:getName() + else + gpName = tostring(gpName) + end + + if type(zone) == 'string' then + zone = trigger.misc.getZone(zone) + elseif type(zone) == 'table' and not zone.radius then + zone = trigger.misc.getZone(zone[math.random(1, #zone)]) + end + local vars = {} + vars.gpName = gpName + vars.action = 'clone' + vars.point = zone.point + vars.radius = zone.radius + vars.disperse = disperse + vars.maxDisp = maxDisp + --log:info('do teleport') + return mist.teleportToPoint(vars) + end + + function mist.teleportInZone(gpName, zone, disperse, maxDisp) -- groupName, zoneName or table of Zone Names, keepForm is a boolean + if type(gpName) == 'table' and gpName:getName() then + gpName = gpName:getName() + else + gpName = tostring(gpName) + end + + if type(zone) == 'string' then + zone = trigger.misc.getZone(zone) + elseif type(zone) == 'table' and not zone.radius then + zone = trigger.misc.getZone(zone[math.random(1, #zone)]) + end + + local vars = {} + vars.gpName = gpName + vars.action = 'tele' + vars.point = zone.point + vars.radius = zone.radius + vars.disperse = disperse + vars.maxDisp = maxDisp + return mist.teleportToPoint(vars) + end + + function mist.respawnGroup(gpName, task) + local vars = {} + vars.gpName = gpName + vars.action = 'respawn' + if task and type(task) ~= 'number' then + vars.route = mist.getGroupRoute(gpName, 'task') + end + local newGroup = mist.teleportToPoint(vars) + if task and type(task) == 'number' then + local newRoute = mist.getGroupRoute(gpName, 'task') + mist.scheduleFunction(mist.goRoute, {newGroup, newRoute}, timer.getTime() + task) + end + return newGroup + end + + function mist.cloneGroup(gpName, task) + local vars = {} + vars.gpName = gpName + vars.action = 'clone' + if task and type(task) ~= 'number' then + vars.route = mist.getGroupRoute(gpName, 'task') + end + local newGroup = mist.teleportToPoint(vars) + if task and type(task) == 'number' then + local newRoute = mist.getGroupRoute(gpName, 'task') + mist.scheduleFunction(mist.goRoute, {newGroup, newRoute}, timer.getTime() + task) + end + return newGroup + end + + function mist.teleportGroup(gpName, task) + local vars = {} + vars.gpName = gpName + vars.action = 'teleport' + if task and type(task) ~= 'number' then + vars.route = mist.getGroupRoute(gpName, 'task') + end + local newGroup = mist.teleportToPoint(vars) + if task and type(task) == 'number' then + local newRoute = mist.getGroupRoute(gpName, 'task') + mist.scheduleFunction(mist.goRoute, {newGroup, newRoute}, timer.getTime() + task) + end + return newGroup + end + + function mist.spawnRandomizedGroup(groupName, vars) -- need to debug + if Group.getByName(groupName) and Group.getByName(groupName):isExist() == true then + local gpData = mist.getGroupData(groupName) + gpData.units = mist.randomizeGroupOrder(gpData.units, vars) + gpData.route = mist.getGroupRoute(groupName, 'task') + + mist.dynAdd(gpData) + end + + return true + end + + function mist.randomizeNumTable(vars) + local newTable = {} + + local excludeIndex = {} + local randomTable = {} + + if vars and vars.exclude and type(vars.exclude) == 'table' then + for index, data in pairs(vars.exclude) do + excludeIndex[data] = true + end + end + + local low, hi, size + + if vars.size then + size = vars.size + end + + if vars and vars.lowerLimit and type(vars.lowerLimit) == 'number' then + low = mist.utils.round(vars.lowerLimit) + else + low = 1 + end + + if vars and vars.upperLimit and type(vars.upperLimit) == 'number' then + hi = mist.utils.round(vars.upperLimit) + else + hi = size + end + + local choices = {} + -- add to exclude list and create list of what to randomize + for i = 1, size do + if not (i >= low and i <= hi) then + + excludeIndex[i] = true + end + if not excludeIndex[i] then + table.insert(choices, i) + else + newTable[i] = i + end + end + + for ind, num in pairs(choices) do + local found = false + local x = 0 + while found == false do + x = mist.random(size) -- get random number from list + local addNew = true + for index, _ in pairs(excludeIndex) do + if index == x then + addNew = false + break + end + end + if addNew == true then + excludeIndex[x] = true + found = true + end + excludeIndex[x] = true + + end + newTable[num] = x + end + --[[ + for i = 1, #newTable do + log:info(newTable[i]) + end + ]] + return newTable + end + + function mist.randomizeGroupOrder(passedUnits, vars) + -- figure out what to exclude, and send data to other func + local units = passedUnits + + if passedUnits.units then + units = passUnits.units + end + + local exclude = {} + local excludeNum = {} + if vars and vars.excludeType and type(vars.excludeType) == 'table' then + exclude = vars.excludeType + end + + if vars and vars.excludeNum and type(vars.excludeNum) == 'table' then + excludeNum = vars.excludeNum + end + + local low, hi + + if vars and vars.lowerLimit and type(vars.lowerLimit) == 'number' then + low = mist.utils.round(vars.lowerLimit) + else + low = 1 + end + + if vars and vars.upperLimit and type(vars.upperLimit) == 'number' then + hi = mist.utils.round(vars.upperLimit) + else + hi = #units + end + + + local excludeNum = {} + for unitIndex, unitData in pairs(units) do + if unitIndex >= low and unitIndex <= hi then -- if within range + local found = false + if #exclude > 0 then + for excludeType, index in pairs(exclude) do -- check if excluded + if mist.stringMatch(excludeType, unitData.type) then -- if excluded + excludeNum[unitIndex] = unitIndex + found = true + end + end + end + else -- unitIndex is either to low, or to high: added to exclude list + excludeNum[unitIndex] = unitId + end + end + + local newGroup = {} + local newOrder = mist.randomizeNumTable({exclude = excludeNum, size = #units}) + + for unitIndex, unitData in pairs(units) do + for i = 1, #newOrder do + if newOrder[i] == unitIndex then + newGroup[i] = mist.utils.deepCopy(units[i]) -- gets all of the unit data + newGroup[i].type = mist.utils.deepCopy(unitData.type) + newGroup[i].skill = mist.utils.deepCopy(unitData.skill) + newGroup[i].unitName = mist.utils.deepCopy(unitData.unitName) + newGroup[i].unitIndex = mist.utils.deepCopy(unitData.unitIndex) -- replaces the units data with a new type + end + end + end + return newGroup + end + + function mist.random(firstNum, secondNum) -- no support for decimals + 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 + + function mist.stringMatch(s1, s2, bool) + local exclude = {'%-', '%(', '%)', '%_', '%[', '%]', '%.', '%#', '% ', '%{', '%}', '%$', '%%', '%?', '%+', '%^'} + if type(s1) == 'string' and type(s2) == 'string' then + for i , str in pairs(exclude) do + s1 = string.gsub(s1, str, '') + s2 = string.gsub(s2, str, '') + end + if not bool then + s1 = string.lower(s1) + s2 = string.lower(s2) + end + --log:info('Comparing: $1 and $2', s1, s2) + if s1 == s2 then + return true + else + return false + end + else + log:error('Either the first or second variable were not a string') + return false + end + end + + mist.matchString = mist.stringMatch -- both commands work because order out type of I + + --[[ scope: +{ + units = {...}, -- unit names. + coa = {...}, -- coa names + countries = {...}, -- country names + CA = {...}, -- looks just like coa. + unitTypes = { red = {}, blue = {}, all = {}, Russia = {},} +} + + +scope examples: + +{ units = { 'Hawg11', 'Hawg12' }, CA = {'blue'} } + +{ countries = {'Georgia'}, unitTypes = {blue = {'A-10C', 'A-10A'}}} + +{ coa = {'all'}} + +{unitTypes = { blue = {'A-10C'}}} +]] +end + +--- Utility functions. +-- E.g. conversions between units etc. +-- @section mist.utils +do -- mist.util scope + mist.utils = {} + + --- Converts angle in radians to degrees. + -- @param angle angle in radians + -- @return angle in degrees + function mist.utils.toDegree(angle) + return angle*180/math.pi + end + + --- Converts angle in degrees to radians. + -- @param angle angle in degrees + -- @return angle in degrees + function mist.utils.toRadian(angle) + return angle*math.pi/180 + end + + --- Converts meters to nautical miles. + -- @param meters distance in meters + -- @return distance in nautical miles + function mist.utils.metersToNM(meters) + return meters/1852 + end + + --- Converts meters to feet. + -- @param meters distance in meters + -- @return distance in feet + function mist.utils.metersToFeet(meters) + return meters/0.3048 + end + + --- Converts nautical miles to meters. + -- @param nm distance in nautical miles + -- @return distance in meters + function mist.utils.NMToMeters(nm) + return nm*1852 + end + + --- Converts feet to meters. + -- @param feet distance in feet + -- @return distance in meters + function mist.utils.feetToMeters(feet) + return feet*0.3048 + end + + --- Converts meters per second to knots. + -- @param mps speed in m/s + -- @return speed in knots + function mist.utils.mpsToKnots(mps) + return mps*3600/1852 + end + + --- Converts meters per second to kilometers per hour. + -- @param mps speed in m/s + -- @return speed in km/h + function mist.utils.mpsToKmph(mps) + return mps*3.6 + end + + --- Converts knots to meters per second. + -- @param knots speed in knots + -- @return speed in m/s + function mist.utils.knotsToMps(knots) + return knots*1852/3600 + end + + --- Converts kilometers per hour to meters per second. + -- @param kmph speed in km/h + -- @return speed in m/s + function mist.utils.kmphToMps(kmph) + return kmph/3.6 + end + + function mist.utils.kelvinToCelsius(t) + return t - 273.15 + end + + function mist.utils.FahrenheitToCelsius(f) + return (f - 32) * (5/9) + end + + function mist.utils.celsiusToFahrenheit(c) + return c*(9/5)+32 + end + + function mist.utils.converter(t1, t2, val) + if type(t1) == 'string' then + t1 = string.lower(t1) + end + if type(t2) == 'string' then + t2 = string.lower(t2) + end + if val and type(val) ~= 'number' then + if tonumber(val) then + val = tonumber(val) + else + log:warn("Value given is not a number: $1", val) + return 0 + end + end + + -- speed + if t1 == 'mps' then + if t2 == 'kmph' then + return val * 3.6 + elseif t2 == 'knots' or t2 == 'knot' then + return val * 3600/1852 + end + elseif t1 == 'kmph' then + if t2 == 'mps' then + return val/3.6 + elseif t2 == 'knots' or t2 == 'knot' then + return val*0.539957 + end + elseif t1 == 'knot' or t1 == 'knots' then + if t2 == 'kmph' then + return val * 1.852 + elseif t2 == 'mps' then + return val * 0.514444 + end + + -- Distance + elseif t1 == 'feet' or t1 == 'ft' then + if t2 == 'nm' then + return val/6076.12 + elseif t2 == 'km' then + return (val*0.3048)/1000 + elseif t2 == 'm' then + return val*0.3048 + end + elseif t1 == 'nm' then + if t2 == 'feet' or t2 == 'ft' then + return val*6076.12 + elseif t2 == 'km' then + return val*1.852 + elseif t2 == 'm' then + return val*1852 + end + elseif t1 == 'km' then + if t2 == 'nm' then + return val/1.852 + elseif t2 == 'feet' or t2 == 'ft' then + return (val/0.3048)*1000 + elseif t2 == 'm' then + return val*1000 + end + elseif t1 == 'm' then + if t2 == 'nm' then + return val/1852 + elseif t2 == 'km' then + return val/1000 + elseif t2 == 'feet' or t2 == 'ft' then + return val/0.3048 + end + + -- Temperature + elseif t1 == 'f' or t1 == 'fahrenheit' then + if t2 == 'c' or t2 == 'celsius' then + return (val - 32) * (5/9) + elseif t2 == 'k' or t2 == 'kelvin' then + return (val + 459.67) * (5/9) + end + elseif t1 == 'c' or t1 == 'celsius' then + if t2 == 'f' or t2 == 'fahrenheit' then + return val*(9/5)+32 + elseif t2 == 'k' or t2 == 'kelvin' then + return val + 273.15 + end + elseif t1 == 'k' or t1 == 'kelvin' then + if t2 == 'c' or t2 == 'celsius' then + return val - 273.15 + elseif t2 == 'f' or t2 == 'fahrenheit' then + return ((val*(9/5))-459.67) + end + + -- Pressure + elseif t1 == 'p' or t1 == 'pascal' or t1 == 'pascals' then + if t2 == 'hpa' or t2 == 'hectopascal' then + return val/100 + elseif t2 == 'mmhg' then + return val * 0.00750061561303 + elseif t2 == 'inhg' then + return val * 0.0002953 + end + elseif t1 == 'hpa' or t1 == 'hectopascal' then + if t2 == 'p' or t2 == 'pascal' or t2 == 'pascals' then + return val*100 + elseif t2 == 'mmhg' then + return val * 0.00750061561303 + elseif t2 == 'inhg' then + return val * 0.02953 + end + elseif t1 == 'mmhg' then + if t2 == 'p' or t2 == 'pascal' or t2 == 'pascals' then + return val / 0.00750061561303 + elseif t2 == 'hpa' or t2 == 'hectopascal' then + return val * 1.33322 + elseif t2 == 'inhg' then + return val/25.4 + end + elseif t1 == 'inhg' then + if t2 == 'p' or t2 == 'pascal' or t2 == 'pascals' then + return val*3386.39 + elseif t2 == 'mmhg' then + return val*25.4 + elseif t2 == 'hpa' or t2 == 'hectopascal' then + return val * 33.8639 + end + else + log:warn("First value doesn't match with list. Value given: $1", t1) + end + log:warn("Match not found. Unable to convert: $1 into $2", t1, t2) + + end + + mist.converter = mist.utils.converter + + function mist.utils.getQFE(point, inchHg) + + local t, p = 0, 0 + if atmosphere.getTemperatureAndPressure then + t, p = atmosphere.getTemperatureAndPressure(mist.utils.makeVec3GL(point)) + end + if p == 0 then + local h = land.getHeight(mist.utils.makeVec2(point))/0.3048 -- convert to feet + if inchHg then + return (env.mission.weather.qnh - (h/30)) * 0.0295299830714 + else + return env.mission.weather.qnh - (h/30) + end + else + if inchHg then + return mist.converter('p', 'inhg', p) + else + return mist.converter('p', 'hpa', p) + end + end + + end + --- Converts a Vec3 to a Vec2. + -- @tparam Vec3 vec the 3D vector + -- @return vector converted to Vec2 + function mist.utils.makeVec2(vec) + if vec.z then + return {x = vec.x, y = vec.z} + else + return {x = vec.x, y = vec.y} -- it was actually already vec2. + end + end + + --- Converts a Vec2 to a Vec3. + -- @tparam Vec2 vec the 2D vector + -- @param y optional new y axis (altitude) value. If omitted it's 0. + function mist.utils.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 + + --- Converts a Vec2 to a Vec3 using ground level as altitude. + -- The ground level at the specific point is used as altitude (y-axis) + -- for the new vector. Optionally a offset can be specified. + -- @tparam Vec2 vec the 2D vector + -- @param[opt] offset offset to be applied to the ground level + -- @return new 3D vector + function mist.utils.makeVec3GL(vec, offset) + local adj = offset or 0 + + if not vec.z then + return {x = vec.x, y = (land.getHeight(vec) + adj), z = vec.y} + else + return {x = vec.x, y = (land.getHeight({x = vec.x, y = vec.z}) + adj), z = vec.z} + end + end + + --- Returns the center of a zone as Vec3. + -- @tparam string|table zone trigger zone name or table + -- @treturn Vec3 center of the zone + function mist.utils.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 + + --- Returns heading-error corrected direction. + -- True-north corrected direction from point along vector vec. + -- @tparam Vec3 vec + -- @tparam Vec2 point + -- @return heading-error corrected direction from point. + function mist.utils.getDir(vec, point) + local dir = math.atan2(vec.z, vec.x) + if point then + dir = dir + mist.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 + + --- Returns distance in meters between two points. + -- @tparam Vec2|Vec3 point1 first point + -- @tparam Vec2|Vec3 point2 second point + -- @treturn number distance between given points. + function mist.utils.get2DDist(point1, point2) + point1 = mist.utils.makeVec3(point1) + point2 = mist.utils.makeVec3(point2) + return mist.vec.mag({x = point1.x - point2.x, y = 0, z = point1.z - point2.z}) + end + + --- Returns distance in meters between two points in 3D space. + -- @tparam Vec3 point1 first point + -- @tparam Vec3 point2 second point + -- @treturn number distancen between given points in 3D space. + function mist.utils.get3DDist(point1, point2) + return mist.vec.mag({x = point1.x - point2.x, y = point1.y - point2.y, z = point1.z - point2.z}) + end + + --- Creates a waypoint from a vector. + -- @tparam Vec2|Vec3 vec position of the new waypoint + -- @treturn Waypoint a new waypoint to be used inside paths. + function mist.utils.vecToWP(vec) + local newWP = {} + newWP.x = vec.x + newWP.y = vec.y + if vec.z then + newWP.alt = vec.y + newWP.y = vec.z + else + newWP.alt = land.getHeight({x = vec.x, y = vec.y}) + end + return newWP + end + + --- Creates a waypoint from a unit. + -- This function also considers the units speed. + -- The alt_type of this waypoint is set to "BARO". + -- @tparam Unit pUnit Unit whose position and speed will be used. + -- @treturn Waypoint new waypoint. + function mist.utils.unitToWP(pUnit) + local unit = mist.utils.deepCopy(pUnit) + if type(unit) == 'string' then + if Unit.getByName(unit) then + unit = Unit.getByName(unit) + end + end + if unit:isExist() == true then + local new = mist.utils.vecToWP(unit:getPosition().p) + new.speed = mist.vec.mag(unit:getVelocity()) + new.alt_type = "BARO" + + return new + end + log:error("$1 not found or doesn't exist", pUnit) + return false + end + + --- Creates a deep copy of a object. + -- Usually this object is a table. + -- See also: from http://lua-users.org/wiki/CopyTable + -- @param object object to copy + -- @return copy of object + function mist.utils.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 + + --- Simple rounding function. + -- From http://lua-users.org/wiki/SimpleRound + -- use negative idp for rounding ahead of decimal place, positive for rounding after decimal place + -- @tparam number num number to round + -- @param idp + function mist.utils.round(num, idp) + local mult = 10^(idp or 0) + return math.floor(num * mult + 0.5) / mult + end + + --- Rounds all numbers inside a table. + -- @tparam table tbl table in which to round numbers + -- @param idp + function mist.utils.roundTbl(tbl, idp) + for id, val in pairs(tbl) do + if type(val) == 'number' then + tbl[id] = mist.utils.round(val, idp) + end + end + return tbl + end + + --- Executes the given string. + -- borrowed from Slmod + -- @tparam string s string containing LUA code. + -- @treturn boolean true if successfully executed, false otherwise + function mist.utils.dostring(s) + local f, err = loadstring(s) + if f then + return true, f() + else + return false, err + end + end + + --- Checks a table's types. + -- This function checks a tables types against a specifically forged type table. + -- @param fname + -- @tparam table type_tbl + -- @tparam table var_tbl + -- @usage -- specifically forged type table + -- type_tbl = { + -- {'table', 'number'}, + -- 'string', + -- 'number', + -- 'number', + -- {'string','nil'}, + -- {'number', 'nil'} + -- } + -- -- my_tbl index 1 must be a table or a number; + -- -- index 2, a string; index 3, a number; + -- -- index 4, a number; index 5, either a string or nil; + -- -- and index 6, either a number or nil. + -- mist.utils.typeCheck(type_tbl, my_tb) + -- @return true if table passes the check, false otherwise. + function mist.utils.typeCheck(fname, type_tbl, var_tbl) + -- log:info('type check') + for type_key, type_val in pairs(type_tbl) do + -- log:info('type_key: $1 type_val: $2', type_key, type_val) + + --type_key can be a table of accepted keys- so try to find one that is not nil + local type_key_str = '' + local act_key = type_key -- actual key within var_tbl - necessary to use for multiple possible key variables. Initialize to type_key + if type(type_key) == 'table' then + + for i = 1, #type_key do + if i ~= 1 then + type_key_str = type_key_str .. '/' + end + type_key_str = type_key_str .. tostring(type_key[i]) + if var_tbl[type_key[i]] ~= nil then + act_key = type_key[i] -- found a non-nil entry, make act_key now this val. + end + end + else + type_key_str = tostring(type_key) + end + + local err_msg = 'Error in function ' .. fname .. ', parameter "' .. type_key_str .. '", expected: ' + local passed_check = false + + if type(type_tbl[type_key]) == 'table' then + -- log:info('err_msg, before: $1', err_msg) + for j = 1, #type_tbl[type_key] do + + if j == 1 then + err_msg = err_msg .. type_tbl[type_key][j] + else + err_msg = err_msg .. ' or ' .. type_tbl[type_key][j] + end + + if type(var_tbl[act_key]) == type_tbl[type_key][j] then + passed_check = true + end + end + -- log:info('err_msg, after: $1', err_msg) + else + -- log:info('err_msg, before: $1', err_msg) + err_msg = err_msg .. type_tbl[type_key] + -- log:info('err_msg, after: $1', err_msg) + if type(var_tbl[act_key]) == type_tbl[type_key] then + passed_check = true + end + + end + + if not passed_check then + err_msg = err_msg .. ', got ' .. type(var_tbl[act_key]) + return false, err_msg + end + end + return true + end + + --- Serializes the give variable to a string. + -- borrowed from slmod + -- @param var variable to serialize + -- @treturn string variable serialized to string + function mist.utils.basicSerialize(var) + if var == nil then + return "\"\"" + else + if ((type(var) == 'number') or + (type(var) == 'boolean') or + (type(var) == 'function') or + (type(var) == 'table') or + (type(var) == 'userdata') ) then + return tostring(var) + elseif type(var) == 'string' then + var = string.format('%q', var) + return var + end + end +end + +--- Serialize value +-- borrowed from slmod (serialize_slmod) +-- @param name +-- @param value value to serialize +-- @param level +function mist.utils.serialize(name, value, level) + --Based on ED's serialize_simple2 + local function basicSerialize(o) + if type(o) == "number" then + return tostring(o) + elseif type(o) == "boolean" then + return tostring(o) + else -- assume it is a string + return mist.utils.basicSerialize(o) + end + end + + local function serializeToTbl(name, value, level) + 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, mist.utils.serialize(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 + log:error('Cannot serialize a $1', type(value)) + end + return var_str_tbl + end + + local t_str = serializeToTbl(name, value, level) + + return table.concat(t_str) +end + +--- Serialize value supporting cycles. +-- borrowed from slmod (serialize_wcycles) +-- @param name +-- @param value value to serialize +-- @param saved +function mist.utils.serializeWithCycles(name, value, saved) + --mostly straight out of Programming in Lua + local function basicSerialize(o) + if type(o) == "number" then + return tostring(o) + elseif type(o) == "boolean" then + return tostring(o) + else -- assume it is a string + return mist.utils.basicSerialize(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, mist.utils.serializeWithCycles(fieldname, v, saved)) + end + end + end + return table.concat(t_str) + else + return "" + end +end + +--- Serialize a table to a single line string. +-- serialization of a table all on a single line, no comments, made to replace old get_table_string function +-- borrowed from slmod +-- @tparam table tbl table to serialize. +-- @treturn string string containing serialized table +function mist.utils.oneLineSerialize(tbl) + if type(tbl) == 'table' then --function only works for tables! + + local tbl_str = {} + + tbl_str[#tbl_str + 1] = '{ ' + + for ind,val in pairs(tbl) do -- serialize its fields + if type(ind) == "number" then + tbl_str[#tbl_str + 1] = '[' + tbl_str[#tbl_str + 1] = tostring(ind) + tbl_str[#tbl_str + 1] = '] = ' + else --must be a string + tbl_str[#tbl_str + 1] = '[' + tbl_str[#tbl_str + 1] = mist.utils.basicSerialize(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] = ', ' + elseif type(val) == 'string' then + tbl_str[#tbl_str + 1] = mist.utils.basicSerialize(val) + tbl_str[#tbl_str + 1] = ', ' + elseif type(val) == 'nil' then -- won't ever happen, right? + tbl_str[#tbl_str + 1] = 'nil, ' + elseif type(val) == 'table' then + tbl_str[#tbl_str + 1] = mist.utils.oneLineSerialize(val) + tbl_str[#tbl_str + 1] = ', ' --I think this is right, I just added it + else + log:warn('Unable to serialize value type $1 at index $2', mist.utils.basicSerialize(type(val)), tostring(ind)) + end + + end + tbl_str[#tbl_str + 1] = '}' + return table.concat(tbl_str) + else + return mist.utils.basicSerialize(tbl) + end +end + +--- Returns table in a easy readable string representation. +-- this function is not meant for serialization because it uses +-- newlines for better readability. +-- @param tbl table to show +-- @param loc +-- @param indent +-- @param tableshow_tbls +-- @return human readable string representation of given table +function mist.utils.tableShow(tbl, loc, indent, tableshow_tbls) --based on serialize_slmod, this is a _G serialization + 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] = mist.utils.basicSerialize(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] = mist.utils.basicSerialize(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 .. '[' .. mist.utils.basicSerialize(ind) .. ']' + tbl_str[#tbl_str + 1] = tostring(val) .. ' ' + tbl_str[#tbl_str + 1] = mist.utils.tableShow(val, loc .. '[' .. mist.utils.basicSerialize(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 ' .. mist.utils.basicSerialize(type(val)) .. ' at index ' .. tostring(ind) + end + end + + tbl_str[#tbl_str + 1] = indent .. '}' + return table.concat(tbl_str) + end +end +end + +--- Debug functions +-- @section mist.debug +do -- mist.debug scope + mist.debug = {} + + --- Dumps the global table _G. + -- This dumps the global table _G to a file in + -- the DCS\Logs directory. + -- This function requires you to disable script sanitization + -- in $DCS_ROOT\Scripts\MissionScripting.lua to access lfs and io + -- libraries. + -- @param fname + function mist.debug.dump_G(fname) + if lfs and io then + local fdir = lfs.writedir() .. [[Logs\]] .. fname + local f = io.open(fdir, 'w') + f:write(mist.utils.tableShow(_G)) + f:close() + log:info('Wrote debug data to $1', fdir) + --trigger.action.outText(errmsg, 10) + else + log:alert('insufficient libraries to run mist.debug.dump_G, you must disable the sanitization of the io and lfs libraries in ./Scripts/MissionScripting.lua') + --trigger.action.outText(errmsg, 10) + end + end + + --- Write debug data to file. + -- This function requires you to disable script sanitization + -- in $DCS_ROOT\Scripts\MissionScripting.lua to access lfs and io + -- libraries. + -- @param fcn + -- @param fcnVars + -- @param fname + function mist.debug.writeData(fcn, fcnVars, fname) + if lfs and io then + local fdir = lfs.writedir() .. [[Logs\]] .. fname + local f = io.open(fdir, 'w') + f:write(fcn(unpack(fcnVars, 1, table.maxn(fcnVars)))) + f:close() + log:info('Wrote debug data to $1', fdir) + local errmsg = 'mist.debug.writeData wrote data to ' .. fdir + trigger.action.outText(errmsg, 10) + else + local errmsg = 'Error: insufficient libraries to run mist.debug.writeData, you must disable the sanitization of the io and lfs libraries in ./Scripts/MissionScripting.lua' + log:alert(errmsg) + trigger.action.outText(errmsg, 10) + end + end + + --- Write mist databases to file. + -- This function requires you to disable script sanitization + -- in $DCS_ROOT\Scripts\MissionScripting.lua to access lfs and io + -- libraries. + function mist.debug.dumpDBs() + for DBname, DB in pairs(mist.DBs) do + if type(DB) == 'table' and type(DBname) == 'string' then + mist.debug.writeData(mist.utils.serialize, {DBname, DB}, 'mist_DBs_' .. DBname .. '.lua') + end + end + end +end + +--- 3D Vector functions +-- @section mist.vec +do -- mist.vec scope + mist.vec = {} + + --- Vector addition. + -- @tparam Vec3 vec1 first vector + -- @tparam Vec3 vec2 second vector + -- @treturn Vec3 new vector, sum of vec1 and vec2. + function mist.vec.add(vec1, vec2) + return {x = vec1.x + vec2.x, y = vec1.y + vec2.y, z = vec1.z + vec2.z} + end + + --- Vector substraction. + -- @tparam Vec3 vec1 first vector + -- @tparam Vec3 vec2 second vector + -- @treturn Vec3 new vector, vec2 substracted from vec1. + function mist.vec.sub(vec1, vec2) + return {x = vec1.x - vec2.x, y = vec1.y - vec2.y, z = vec1.z - vec2.z} + end + + --- Vector scalar multiplication. + -- @tparam Vec3 vec vector to multiply + -- @tparam number mult scalar multiplicator + -- @treturn Vec3 new vector multiplied with the given scalar + function mist.vec.scalarMult(vec, mult) + return {x = vec.x*mult, y = vec.y*mult, z = vec.z*mult} + end + + mist.vec.scalar_mult = mist.vec.scalarMult + + --- Vector dot product. + -- @tparam Vec3 vec1 first vector + -- @tparam Vec3 vec2 second vector + -- @treturn number dot product of given vectors + function mist.vec.dp (vec1, vec2) + return vec1.x*vec2.x + vec1.y*vec2.y + vec1.z*vec2.z + end + + --- Vector cross product. + -- @tparam Vec3 vec1 first vector + -- @tparam Vec3 vec2 second vector + -- @treturn Vec3 new vector, cross product of vec1 and vec2. + function mist.vec.cp(vec1, vec2) + return { x = vec1.y*vec2.z - vec1.z*vec2.y, y = vec1.z*vec2.x - vec1.x*vec2.z, z = vec1.x*vec2.y - vec1.y*vec2.x} + end + + --- Vector magnitude + -- @tparam Vec3 vec vector + -- @treturn number magnitude of vector vec + function mist.vec.mag(vec) + return (vec.x^2 + vec.y^2 + vec.z^2)^0.5 + end + + --- Unit vector + -- @tparam Vec3 vec + -- @treturn Vec3 unit vector of vec + function mist.vec.getUnitVec(vec) + local mag = mist.vec.mag(vec) + return { x = vec.x/mag, y = vec.y/mag, z = vec.z/mag } + end + + --- Rotate vector. + -- @tparam Vec2 vec2 to rotoate + -- @tparam number theta + -- @return Vec2 rotated vector. + function mist.vec.rotateVec2(vec2, theta) + return { x = vec2.x*math.cos(theta) - vec2.y*math.sin(theta), y = vec2.x*math.sin(theta) + vec2.y*math.cos(theta)} + end +end + +--- Flag functions. +-- The mist "Flag functions" are functions that are similar to Slmod functions +-- that detect a game condition and set a flag when that game condition is met. +-- +-- They are intended to be used by persons with little or no experience in Lua +-- programming, but with a good knowledge of the DCS mission editor. +-- @section mist.flagFunc +do -- mist.flagFunc scope + mist.flagFunc = {} + + --- Sets a flag if map objects are destroyed inside a zone. + -- Once this function is run, it will start a continuously evaluated process + -- that will set a flag true if map objects (such as bridges, buildings in + -- town, etc.) die (or have died) in a mission editor zone (or set of zones). + -- This will only happen once; once the flag is set true, the process ends. + -- @usage + -- -- Example vars table + -- vars = { + -- zones = { "zone1", "zone2" }, -- can also be a single string + -- flag = 3, -- number of the flag + -- stopflag = 4, -- optional number of the stop flag + -- req_num = 10, -- optional minimum amount of map objects needed to die + -- } + -- mist.flagFuncs.mapobjs_dead_zones(vars) + -- @tparam table vars table containing parameters. + function mist.flagFunc.mapobjs_dead_zones(vars) + --[[vars needs to be: +zones = table or string, +flag = number, +stopflag = number or nil, +req_num = number or nil + +AND used by function, +initial_number + +]] + -- type_tbl + local type_tbl = { + [{'zones', 'zone'}] = {'table', 'string'}, + flag = {'number', 'string'}, + [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'}, + [{'req_num', 'reqnum'}] = {'number', 'nil'}, + } + + local err, errmsg = mist.utils.typeCheck('mist.flagFunc.mapobjs_dead_zones', type_tbl, vars) + assert(err, errmsg) + local zones = vars.zones or vars.zone + local flag = vars.flag + local stopflag = vars.stopflag or vars.stopFlag or -1 + local req_num = vars.req_num or vars.reqnum or 1 + local initial_number = vars.initial_number + + if type(zones) == 'string' then + zones = {zones} + end + + if not initial_number then + initial_number = #mist.getDeadMapObjsInZones(zones) + end + + if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then + if (#mist.getDeadMapObjsInZones(zones) - initial_number) >= req_num and trigger.misc.getUserFlag(flag) == 0 then + trigger.action.setUserFlag(flag, true) + return + else + mist.scheduleFunction(mist.flagFunc.mapobjs_dead_zones, {{zones = zones, flag = flag, stopflag = stopflag, req_num = req_num, initial_number = initial_number}}, timer.getTime() + 1) + end + end + end + + --- Sets a flag if map objects are destroyed inside a polygon. + -- Once this function is run, it will start a continuously evaluated process + -- that will set a flag true if map objects (such as bridges, buildings in + -- town, etc.) die (or have died) in a polygon. + -- This will only happen once; once the flag is set true, the process ends. + -- @usage + -- -- Example vars table + -- vars = { + -- zone = { + -- [1] = mist.DBs.unitsByName['NE corner'].point, + -- [2] = mist.DBs.unitsByName['SE corner'].point, + -- [3] = mist.DBs.unitsByName['SW corner'].point, + -- [4] = mist.DBs.unitsByName['NW corner'].point + -- } + -- flag = 3, -- number of the flag + -- stopflag = 4, -- optional number of the stop flag + -- req_num = 10, -- optional minimum amount of map objects needed to die + -- } + -- mist.flagFuncs.mapobjs_dead_zones(vars) + -- @tparam table vars table containing parameters. + function mist.flagFunc.mapobjs_dead_polygon(vars) + --[[vars needs to be: +zone = table, +flag = number, +stopflag = number or nil, +req_num = number or nil + +AND used by function, +initial_number + +]] + -- type_tbl + local type_tbl = { + [{'zone', 'polyzone'}] = 'table', + flag = {'number', 'string'}, + [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'}, + [{'req_num', 'reqnum'}] = {'number', 'nil'}, + } + + local err, errmsg = mist.utils.typeCheck('mist.flagFunc.mapobjs_dead_polygon', type_tbl, vars) + assert(err, errmsg) + local zone = vars.zone or vars.polyzone + local flag = vars.flag + local stopflag = vars.stopflag or vars.stopFlag or -1 + local req_num = vars.req_num or vars.reqnum or 1 + local initial_number = vars.initial_number + + if not initial_number then + initial_number = #mist.getDeadMapObjsInPolygonZone(zone) + end + + if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then + if (#mist.getDeadMapObjsInPolygonZone(zone) - initial_number) >= req_num and trigger.misc.getUserFlag(flag) == 0 then + trigger.action.setUserFlag(flag, true) + return + else + mist.scheduleFunction(mist.flagFunc.mapobjs_dead_polygon, {{zone = zone, flag = flag, stopflag = stopflag, req_num = req_num, initial_number = initial_number}}, timer.getTime() + 1) + end + end + end + + --- Sets a flag if unit(s) is/are inside a polygon. + -- @tparam table vars @{unitsInPolygonVars} + -- @usage -- set flag 11 to true as soon as any blue vehicles + -- -- are inside the polygon shape created off of the waypoints + -- -- of the group forest1 + -- mist.flagFunc.units_in_polygon { + -- units = {'[blue][vehicle]'}, + -- zone = mist.getGroupPoints('forest1'), + -- flag = 11 + -- } + function mist.flagFunc.units_in_polygon(vars) + --[[vars needs to be: +units = table, +zone = table, +flag = number, +stopflag = number or nil, +maxalt = number or nil, +interval = number or nil, +req_num = number or nil +toggle = boolean or nil +unitTableDef = table or nil +]] + -- type_tbl + local type_tbl = { + [{'units', 'unit'}] = 'table', + [{'zone', 'polyzone'}] = 'table', + flag = {'number', 'string'}, + [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'}, + [{'maxalt', 'alt'}] = {'number', 'nil'}, + interval = {'number', 'nil'}, + [{'req_num', 'reqnum'}] = {'number', 'nil'}, + toggle = {'boolean', 'nil'}, + unitTableDef = {'table', 'nil'}, + } + + local err, errmsg = mist.utils.typeCheck('mist.flagFunc.units_in_polygon', type_tbl, vars) + assert(err, errmsg) + local units = vars.units or vars.unit + local zone = vars.zone or vars.polyzone + local flag = vars.flag + local stopflag = vars.stopflag or vars.stopFlag or -1 + local interval = vars.interval or 1 + local maxalt = vars.maxalt or vars.alt + local req_num = vars.req_num or vars.reqnum or 1 + local toggle = vars.toggle or nil + local unitTableDef = vars.unitTableDef + + if not units.processed then + unitTableDef = mist.utils.deepCopy(units) + end + + if (units.processed and units.processed < mist.getLastDBUpdateTime()) or not units.processed then -- run unit table short cuts + if unitTableDef then + units = mist.makeUnitTable(unitTableDef) + end + end + + if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == 0) then + local num_in_zone = 0 + for i = 1, #units do + local unit = Unit.getByName(units[i]) + if unit then + local pos = unit:getPosition().p + if mist.pointInPolygon(pos, zone, maxalt) then + num_in_zone = num_in_zone + 1 + if num_in_zone >= req_num and trigger.misc.getUserFlag(flag) == 0 then + trigger.action.setUserFlag(flag, true) + break + end + end + end + end + if toggle and (num_in_zone < req_num) and trigger.misc.getUserFlag(flag) > 0 then + trigger.action.setUserFlag(flag, false) + end + -- do another check in case stopflag was set true by this function + if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == 0) then + mist.scheduleFunction(mist.flagFunc.units_in_polygon, {{units = units, zone = zone, flag = flag, stopflag = stopflag, interval = interval, req_num = req_num, maxalt = maxalt, toggle = toggle, unitTableDef = unitTableDef}}, timer.getTime() + interval) + end + end + + end + + --- Sets a flag if unit(s) is/are inside a trigger zone. + -- @todo document + function mist.flagFunc.units_in_zones(vars) + --[[vars needs to be: + units = table, + zones = table, + flag = number, + stopflag = number or nil, + zone_type = string or nil, + req_num = number or nil, + interval = number or nil + toggle = boolean or nil + ]] + -- type_tbl + local type_tbl = { + units = 'table', + zones = 'table', + flag = {'number', 'string'}, + [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'}, + [{'zone_type', 'zonetype'}] = {'string', 'nil'}, + [{'req_num', 'reqnum'}] = {'number', 'nil'}, + interval = {'number', 'nil'}, + toggle = {'boolean', 'nil'}, + unitTableDef = {'table', 'nil'}, + } + + local err, errmsg = mist.utils.typeCheck('mist.flagFunc.units_in_zones', type_tbl, vars) + assert(err, errmsg) + local units = vars.units + local zones = vars.zones + local flag = vars.flag + local stopflag = vars.stopflag or vars.stopFlag or -1 + local zone_type = vars.zone_type or vars.zonetype or 'cylinder' + local req_num = vars.req_num or vars.reqnum or 1 + local interval = vars.interval or 1 + local toggle = vars.toggle or nil + local unitTableDef = vars.unitTableDef + + if not units.processed then + unitTableDef = mist.utils.deepCopy(units) + end + + if (units.processed and units.processed < mist.getLastDBUpdateTime()) or not units.processed then -- run unit table short cuts + if unitTableDef then + units = mist.makeUnitTable(unitTableDef) + end + end + + if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then + + local in_zone_units = mist.getUnitsInZones(units, zones, zone_type) + + if #in_zone_units >= req_num and trigger.misc.getUserFlag(flag) == 0 then + trigger.action.setUserFlag(flag, true) + elseif #in_zone_units < req_num and toggle then + trigger.action.setUserFlag(flag, false) + end + -- do another check in case stopflag was set true by this function + if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then + mist.scheduleFunction(mist.flagFunc.units_in_zones, {{units = units, zones = zones, flag = flag, stopflag = stopflag, zone_type = zone_type, req_num = req_num, interval = interval, toggle = toggle, unitTableDef = unitTableDef}}, timer.getTime() + interval) + end + end + + end + + --- Sets a flag if unit(s) is/are inside a moving zone. + -- @todo document + function mist.flagFunc.units_in_moving_zones(vars) + --[[vars needs to be: + units = table, + zone_units = table, + radius = number, + flag = number, + stopflag = number or nil, + zone_type = string or nil, + req_num = number or nil, + interval = number or nil + toggle = boolean or nil + ]] + -- type_tbl + local type_tbl = { + units = 'table', + [{'zone_units', 'zoneunits'}] = 'table', + radius = 'number', + flag = {'number', 'string'}, + [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'}, + [{'zone_type', 'zonetype'}] = {'string', 'nil'}, + [{'req_num', 'reqnum'}] = {'number', 'nil'}, + interval = {'number', 'nil'}, + toggle = {'boolean', 'nil'}, + unitTableDef = {'table', 'nil'}, + zUnitTableDef = {'table', 'nil'}, + } + + local err, errmsg = mist.utils.typeCheck('mist.flagFunc.units_in_moving_zones', type_tbl, vars) + assert(err, errmsg) + local units = vars.units + local zone_units = vars.zone_units or vars.zoneunits + local radius = vars.radius + local flag = vars.flag + local stopflag = vars.stopflag or vars.stopFlag or -1 + local zone_type = vars.zone_type or vars.zonetype or 'cylinder' + local req_num = vars.req_num or vars.reqnum or 1 + local interval = vars.interval or 1 + local toggle = vars.toggle or nil + local unitTableDef = vars.unitTableDef + local zUnitTableDef = vars.zUnitTableDef + + if not units.processed then + unitTableDef = mist.utils.deepCopy(units) + end + + if not zone_units.processed then + zUnitTableDef = mist.utils.deepCopy(zone_units) + end + + if (units.processed and units.processed < mist.getLastDBUpdateTime()) or not units.processed then -- run unit table short cuts + if unitTableDef then + units = mist.makeUnitTable(unitTableDef) + end + end + + if (zone_units.processed and zone_units.processed < mist.getLastDBUpdateTime()) or not zone_units.processed then -- run unit table short cuts + if zUnitTableDef then + zone_units = mist.makeUnitTable(zUnitTableDef) + end + + end + + if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then + + local in_zone_units = mist.getUnitsInMovingZones(units, zone_units, radius, zone_type) + + if #in_zone_units >= req_num and trigger.misc.getUserFlag(flag) == 0 then + trigger.action.setUserFlag(flag, true) + elseif #in_zone_units < req_num and toggle then + trigger.action.setUserFlag(flag, false) + end + -- do another check in case stopflag was set true by this function + if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then + mist.scheduleFunction(mist.flagFunc.units_in_moving_zones, {{units = units, zone_units = zone_units, radius = radius, flag = flag, stopflag = stopflag, zone_type = zone_type, req_num = req_num, interval = interval, toggle = toggle, unitTableDef = unitTableDef, zUnitTableDef = zUnitTableDef}}, timer.getTime() + interval) + end + end + + end + + --- Sets a flag if units have line of sight to each other. + -- @todo document + function mist.flagFunc.units_LOS(vars) + --[[vars needs to be: +unitset1 = table, +altoffset1 = number, +unitset2 = table, +altoffset2 = number, +flag = number, +stopflag = number or nil, +radius = number or nil, +interval = number or nil, +req_num = number or nil +toggle = boolean or nil +]] + -- type_tbl + local type_tbl = { + [{'unitset1', 'units1'}] = 'table', + [{'altoffset1', 'alt1'}] = 'number', + [{'unitset2', 'units2'}] = 'table', + [{'altoffset2', 'alt2'}] = 'number', + flag = {'number', 'string'}, + [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'}, + [{'req_num', 'reqnum'}] = {'number', 'nil'}, + interval = {'number', 'nil'}, + radius = {'number', 'nil'}, + toggle = {'boolean', 'nil'}, + unitTableDef1 = {'table', 'nil'}, + unitTableDef2 = {'table', 'nil'}, + } + + local err, errmsg = mist.utils.typeCheck('mist.flagFunc.units_LOS', type_tbl, vars) + assert(err, errmsg) + local unitset1 = vars.unitset1 or vars.units1 + local altoffset1 = vars.altoffset1 or vars.alt1 + local unitset2 = vars.unitset2 or vars.units2 + local altoffset2 = vars.altoffset2 or vars.alt2 + local flag = vars.flag + local stopflag = vars.stopflag or vars.stopFlag or -1 + local interval = vars.interval or 1 + local radius = vars.radius or math.huge + local req_num = vars.req_num or vars.reqnum or 1 + local toggle = vars.toggle or nil + local unitTableDef1 = vars.unitTableDef1 + local unitTableDef2 = vars.unitTableDef2 + + if not unitset1.processed then + unitTableDef1 = mist.utils.deepCopy(unitset1) + end + + if not unitset2.processed then + unitTableDef2 = mist.utils.deepCopy(unitset2) + end + + if (unitset1.processed and unitset1.processed < mist.getLastDBUpdateTime()) or not unitset1.processed then -- run unit table short cuts + if unitTableDef1 then + unitset1 = mist.makeUnitTable(unitTableDef1) + end + end + + if (unitset2.processed and unitset2.processed < mist.getLastDBUpdateTime()) or not unitset2.processed then -- run unit table short cuts + if unitTableDef2 then + unitset2 = mist.makeUnitTable(unitTableDef2) + end + end + + + if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then + + local unitLOSdata = mist.getUnitsLOS(unitset1, altoffset1, unitset2, altoffset2, radius) + + if #unitLOSdata >= req_num and trigger.misc.getUserFlag(flag) == 0 then + trigger.action.setUserFlag(flag, true) + elseif #unitLOSdata < req_num and toggle then + trigger.action.setUserFlag(flag, false) + end + -- do another check in case stopflag was set true by this function + if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then + mist.scheduleFunction(mist.flagFunc.units_LOS, {{unitset1 = unitset1, altoffset1 = altoffset1, unitset2 = unitset2, altoffset2 = altoffset2, flag = flag, stopflag = stopflag, radius = radius, req_num = req_num, interval = interval, toggle = toggle, unitTableDef1 = unitTableDef1, unitTableDef2 = unitTableDef2}}, timer.getTime() + interval) + end + end + end + + --- Sets a flag if group is alive. + -- @todo document + function mist.flagFunc.group_alive(vars) + --[[vars +groupName +flag +toggle +interval +stopFlag + +]] + local type_tbl = { + [{'group', 'groupname', 'gp', 'groupName'}] = 'string', + flag = {'number', 'string'}, + [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'}, + interval = {'number', 'nil'}, + toggle = {'boolean', 'nil'}, + } + + local err, errmsg = mist.utils.typeCheck('mist.flagFunc.group_alive', type_tbl, vars) + assert(err, errmsg) + + local groupName = vars.groupName or vars.group or vars.gp or vars.Groupname + local flag = vars.flag + local stopflag = vars.stopflag or vars.stopFlag or -1 + local interval = vars.interval or 1 + local toggle = vars.toggle or nil + + + if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then + if Group.getByName(groupName) and Group.getByName(groupName):isExist() == true and #Group.getByName(groupName):getUnits() > 0 then + if trigger.misc.getUserFlag(flag) == 0 then + trigger.action.setUserFlag(flag, true) + end + else + if toggle then + trigger.action.setUserFlag(flag, false) + end + end + end + + if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then + mist.scheduleFunction(mist.flagFunc.group_alive, {{groupName = groupName, flag = flag, stopflag = stopflag, interval = interval, toggle = toggle}}, timer.getTime() + interval) + end + + end + + --- Sets a flag if group is dead. + -- @todo document + function mist.flagFunc.group_dead(vars) + local type_tbl = { + [{'group', 'groupname', 'gp', 'groupName'}] = 'string', + flag = {'number', 'string'}, + [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'}, + interval = {'number', 'nil'}, + toggle = {'boolean', 'nil'}, + } + + local err, errmsg = mist.utils.typeCheck('mist.flagFunc.group_dead', type_tbl, vars) + assert(err, errmsg) + + local groupName = vars.groupName or vars.group or vars.gp or vars.Groupname + local flag = vars.flag + local stopflag = vars.stopflag or vars.stopFlag or -1 + local interval = vars.interval or 1 + local toggle = vars.toggle or nil + + + if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then + if (Group.getByName(groupName) and Group.getByName(groupName):isExist() == false) or (Group.getByName(groupName) and #Group.getByName(groupName):getUnits() < 1) or not Group.getByName(groupName) then + if trigger.misc.getUserFlag(flag) == 0 then + trigger.action.setUserFlag(flag, true) + end + else + if toggle then + trigger.action.setUserFlag(flag, false) + end + end + end + + if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then + mist.scheduleFunction(mist.flagFunc.group_dead, {{groupName = groupName, flag = flag, stopflag = stopflag, interval = interval, toggle = toggle}}, timer.getTime() + interval) + end + end + + --- Sets a flag if less than given percent of group is alive. + -- @todo document + function mist.flagFunc.group_alive_less_than(vars) + local type_tbl = { + [{'group', 'groupname', 'gp', 'groupName'}] = 'string', + percent = 'number', + flag = {'number', 'string'}, + [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'}, + interval = {'number', 'nil'}, + toggle = {'boolean', 'nil'}, + } + + local err, errmsg = mist.utils.typeCheck('mist.flagFunc.group_alive_less_than', type_tbl, vars) + assert(err, errmsg) + + local groupName = vars.groupName or vars.group or vars.gp or vars.Groupname + local flag = vars.flag + local percent = vars.percent + local stopflag = vars.stopflag or vars.stopFlag or -1 + local interval = vars.interval or 1 + local toggle = vars.toggle or nil + + + if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then + if Group.getByName(groupName) and Group.getByName(groupName):isExist() == true then + if Group.getByName(groupName):getSize()/Group.getByName(groupName):getInitialSize() < percent/100 then + if trigger.misc.getUserFlag(flag) == 0 then + trigger.action.setUserFlag(flag, true) + end + else + if toggle then + trigger.action.setUserFlag(flag, false) + end + end + else + if trigger.misc.getUserFlag(flag) == 0 then + trigger.action.setUserFlag(flag, true) + end + end + end + + if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then + mist.scheduleFunction(mist.flagFunc.group_alive_less_than, {{groupName = groupName, flag = flag, stopflag = stopflag, interval = interval, toggle = toggle, percent = percent}}, timer.getTime() + interval) + end + end + + --- Sets a flag if more than given percent of group is alive. + -- @todo document + function mist.flagFunc.group_alive_more_than(vars) + local type_tbl = { + [{'group', 'groupname', 'gp', 'groupName'}] = 'string', + percent = 'number', + flag = {'number', 'string'}, + [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'}, + interval = {'number', 'nil'}, + toggle = {'boolean', 'nil'}, + } + + local err, errmsg = mist.utils.typeCheck('mist.flagFunc.group_alive_more_than', type_tbl, vars) + assert(err, errmsg) + + local groupName = vars.groupName or vars.group or vars.gp or vars.Groupname + local flag = vars.flag + local percent = vars.percent + local stopflag = vars.stopflag or vars.stopFlag or -1 + local interval = vars.interval or 1 + local toggle = vars.toggle or nil + + + if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then + if Group.getByName(groupName) and Group.getByName(groupName):isExist() == true then + if Group.getByName(groupName):getSize()/Group.getByName(groupName):getInitialSize() > percent/100 then + if trigger.misc.getUserFlag(flag) == 0 then + trigger.action.setUserFlag(flag, true) + end + else + if toggle and trigger.misc.getUserFlag(flag) == 1 then + trigger.action.setUserFlag(flag, false) + end + end + else --- just in case + if toggle and trigger.misc.getUserFlag(flag) == 1 then + trigger.action.setUserFlag(flag, false) + end + end + end + + if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then + mist.scheduleFunction(mist.flagFunc.group_alive_more_than, {{groupName = groupName, flag = flag, stopflag = stopflag, interval = interval, toggle = toggle, percent = percent}}, timer.getTime() + interval) + end + end + + mist.flagFunc.mapobjsDeadPolygon = mist.flagFunc.mapobjs_dead_polygon + mist.flagFunc.mapobjsDeadZones = mist.flagFunc.Mapobjs_dead_zones + mist.flagFunc.unitsInZones = mist.flagFunc.units_in_zones + mist.flagFunc.unitsInMovingZones = mist.flagFunc.units_in_moving_zones + mist.flagFunc.unitsInPolygon = mist.flagFunc.units_in_polygon + mist.flagFunc.unitsLOS = mist.flagFunc.units_LOS + mist.flagFunc.groupAlive = mist.flagFunc.group_alive + mist.flagFunc.groupDead = mist.flagFunc.group_dead + mist.flagFunc.groupAliveMoreThan = mist.flagFunc.group_alive_more_than + mist.flagFunc.groupAliveLessThan = mist.flagFunc.group_alive_less_than + +end + +--- Message functions. +-- Messaging system +-- @section mist.msg +do -- mist.msg scope + local messageList = {} + -- this defines the max refresh rate of the message box it honestly only needs to + -- go faster than this for precision timing stuff (which could be its own function) + local messageDisplayRate = 0.1 + local messageID = 0 + local displayActive = false + local displayFuncId = 0 + + local caSlots = false + local caMSGtoGroup = false + + if env.mission.groundControl then -- just to be sure? + for index, value in pairs(env.mission.groundControl) do + if type(value) == 'table' then + for roleName, roleVal in pairs(value) do + for rIndex, rVal in pairs(roleVal) do + if env.mission.groundControl[index][roleName][rIndex] > 0 then + caSlots = true + break + end + + end + end + elseif type(value) == 'boolean' and value == true then + caSlots = true + break + end + end + end + + local function mistdisplayV5() + --[[thoughts to improve upon + event handler based activeClients table. + display messages only when there is an update + possibly co-routine it. + ]] + end + + local function mistdisplayV4() + local activeClients = {} + + for clientId, clientData in pairs(mist.DBs.humansById) do + if Unit.getByName(clientData.unitName) and Unit.getByName(clientData.unitName):isExist() == true then + activeClients[clientData.groupId] = clientData.groupName + end + end + + --[[if caSlots == true and caMSGtoGroup == true then + + end]] + + + if #messageList > 0 then + if displayActive == false then + displayActive = true + end + --mist.debug.writeData(mist.utils.serialize,{'msg', messageList}, 'messageList.lua') + local msgTableText = {} + local msgTableSound = {} + + for messageId, messageData in pairs(messageList) do + if messageData.displayedFor > messageData.displayTime then + messageData:remove() -- now using the remove/destroy function. + else + if messageData.displayedFor then + messageData.displayedFor = messageData.displayedFor + messageDisplayRate + end + local nextSound = 1000 + local soundIndex = 0 + + if messageData.multSound and #messageData.multSound > 0 then + for index, sData in pairs(messageData.multSound) do + if sData.time <= messageData.displayedFor and sData.played == false and sData.time < nextSound then -- find index of the next sound to be played + nextSound = sData.time + soundIndex = index + end + end + if soundIndex ~= 0 then + messageData.multSound[soundIndex].played = true + end + end + + for recIndex, recData in pairs(messageData.msgFor) do -- iterate recipiants + if recData == 'RED' or recData == 'BLUE' or activeClients[recData] then -- rec exists + if messageData.text then -- text + if not msgTableText[recData] then -- create table entry for text + msgTableText[recData] = {} + msgTableText[recData].text = {} + if recData == 'RED' or recData == 'BLUE' then + msgTableText[recData].text[1] = '-------Combined Arms Message-------- \n' + end + msgTableText[recData].text[#msgTableText[recData].text + 1] = messageData.text + msgTableText[recData].displayTime = messageData.displayTime - messageData.displayedFor + else -- add to table entry and adjust display time if needed + if recData == 'RED' or recData == 'BLUE' then + msgTableText[recData].text[#msgTableText[recData].text + 1] = '\n ---------------- Combined Arms Message: \n' + else + msgTableText[recData].text[#msgTableText[recData].text + 1] = '\n ---------------- \n' + end + msgTableText[recData].text[#msgTableText[recData].text + 1] = messageData.text + if msgTableText[recData].displayTime < messageData.displayTime - messageData.displayedFor then + msgTableText[recData].displayTime = messageData.displayTime - messageData.displayedFor + else + msgTableText[recData].displayTime = 1 + end + end + end + if soundIndex ~= 0 then + msgTableSound[recData] = messageData.multSound[soundIndex].file + end + end + end + + + end + end + ------- new display + + if caSlots == true and caMSGtoGroup == false then + if msgTableText.RED then + trigger.action.outTextForCoalition(coalition.side.RED, table.concat(msgTableText.RED.text), msgTableText.RED.displayTime, true) + + end + if msgTableText.BLUE then + trigger.action.outTextForCoalition(coalition.side.BLUE, table.concat(msgTableText.BLUE.text), msgTableText.BLUE.displayTime, true) + end + end + + for index, msgData in pairs(msgTableText) do + if type(index) == 'number' then -- its a groupNumber + trigger.action.outTextForGroup(index, table.concat(msgData.text), msgData.displayTime, true) + end + end + --- new audio + if msgTableSound.RED then + trigger.action.outSoundForCoalition(coalition.side.RED, msgTableSound.RED) + end + if msgTableSound.BLUE then + trigger.action.outSoundForCoalition(coalition.side.BLUE, msgTableSound.BLUE) + end + + + for index, file in pairs(msgTableSound) do + if type(index) == 'number' then -- its a groupNumber + trigger.action.outSoundForGroup(index, file) + end + end + else + mist.removeFunction(displayFuncId) + displayActive = false + end + + end + + local typeBase = { + ['Mi-8MT'] = {'Mi-8MTV2', 'Mi-8MTV', 'Mi-8'}, + ['MiG-21Bis'] = {'Mig-21'}, + ['MiG-15bis'] = {'Mig-15'}, + ['FW-190D9'] = {'FW-190'}, + ['Bf-109K-4'] = {'Bf-109'}, + } + + --[[function mist.setCAGroupMSG(val) + if type(val) == 'boolean' then + caMSGtoGroup = val + return true + end + return false +end]] + + mist.message = { + + add = function(vars) + local function msgSpamFilter(recList, spamBlockOn) + for id, name in pairs(recList) do + if name == spamBlockOn then + -- log:info('already on recList') + return recList + end + end + --log:info('add to recList') + table.insert(recList, spamBlockOn) + return recList + end + + --[[ + local vars = {} + vars.text = 'Hello World' + vars.displayTime = 20 + vars.msgFor = {coa = {'red'}, countries = {'Ukraine', 'Georgia'}, unitTypes = {'A-10C'}} + mist.message.add(vars) + + Displays the message for all red coalition players. Players belonging to Ukraine and Georgia, and all A-10Cs on the map + + ]] + + + local new = {} + new.text = vars.text -- The actual message + new.displayTime = vars.displayTime -- How long will the message appear for + new.displayedFor = 0 -- how long the message has been displayed so far + new.name = vars.name -- ID to overwrite the older message (if it exists) Basically it replaces a message that is displayed with new text. + new.addedAt = timer.getTime() + new.update = true + + if vars.multSound and vars.multSound[1] then + new.multSound = vars.multSound + else + new.multSound = {} + end + + if vars.sound or vars.fileName then -- converts old sound file system into new multSound format + local sound = vars.sound + if vars.fileName then + sound = vars.fileName + end + new.multSound[#new.multSound+1] = {time = 0.1, file = sound} + end + + if #new.multSound > 0 then + for i, data in pairs(new.multSound) do + data.played = false + end + end + + local newMsgFor = {} -- list of all groups message displays for + for forIndex, forData in pairs(vars.msgFor) do + for list, listData in pairs(forData) do + for clientId, clientData in pairs(mist.DBs.humansById) do + forIndex = string.lower(forIndex) + if type(listData) == 'string' then + listData = string.lower(listData) + end + if (forIndex == 'coa' and (listData == string.lower(clientData.coalition) or listData == 'all')) or (forIndex == 'countries' and string.lower(clientData.country) == listData) or (forIndex == 'units' and string.lower(clientData.unitName) == listData) then -- + newMsgFor = msgSpamFilter(newMsgFor, clientData.groupId) -- so units dont get the same message twice if complex rules are given + --table.insert(newMsgFor, clientId) + elseif forIndex == 'unittypes' then + for typeId, typeData in pairs(listData) do + local found = false + for clientDataEntry, clientDataVal in pairs(clientData) do + if type(clientDataVal) == 'string' then + if mist.matchString(list, clientDataVal) == true or list == 'all' then + local sString = typeData + for rName, pTbl in pairs(typeBase) do -- just a quick check to see if the user may have meant something and got the specific type of the unit wrong + for pIndex, pName in pairs(pTbl) do + if mist.stringMatch(sString, pName) then + sString = rName + end + end + end + if sString == clientData.type then + found = true + newMsgFor = msgSpamFilter(newMsgFor, clientData.groupId) -- sends info oto other function to see if client is already recieving the current message. + --table.insert(newMsgFor, clientId) + end + end + end + if found == true then -- shouldn't this be elsewhere too? + break + end + end + end + + end + end + for coaData, coaId in pairs(coalition.side) do + if string.lower(forIndex) == 'coa' or string.lower(forIndex) == 'ca' then + if listData == string.lower(coaData) or listData == 'all' then + newMsgFor = msgSpamFilter(newMsgFor, coaData) + end + end + end + end + end + + if #newMsgFor > 0 then + new.msgFor = newMsgFor -- I swear its not confusing + + else + return false + end + + + if vars.name and type(vars.name) == 'string' then + for i = 1, #messageList do + if messageList[i].name then + if messageList[i].name == vars.name then + --log:info('updateMessage') + messageList[i].displayedFor = 0 + messageList[i].addedAt = timer.getTime() + messageList[i].sound = new.sound + messageList[i].text = new.text + messageList[i].msgFor = new.msgFor + messageList[i].multSound = new.multSound + messageList[i].update = true + return messageList[i].messageID + end + end + end + end + + messageID = messageID + 1 + new.messageID = messageID + + --mist.debug.writeData(mist.utils.serialize,{'msg', new}, 'newMsg.lua') + + + messageList[#messageList + 1] = new + + local mt = { __index = mist.message} + setmetatable(new, mt) + + if displayActive == false then + displayActive = true + displayFuncId = mist.scheduleFunction(mistdisplayV4, {}, timer.getTime() + messageDisplayRate, messageDisplayRate) + end + + return messageID + + end, + + remove = function(self) -- Now a self variable; the former functionality taken up by mist.message.removeById. + for i, msgData in pairs(messageList) do + if messageList[i] == self then + table.remove(messageList, i) + return true --removal successful + end + end + return false -- removal not successful this script fails at life! + end, + + removeById = function(id) -- This function is NOT passed a self variable; it is the remove by id function. + for i, msgData in pairs(messageList) do + if messageList[i].messageID == id then + table.remove(messageList, i) + return true --removal successful + end + end + return false -- removal not successful this script fails at life! + end, + } + + --[[ vars for mist.msgMGRS +vars.units - table of unit names (NOT unitNameTable- maybe this should change). +vars.acc - integer between 0 and 5, inclusive +vars.text - text in the message +vars.displayTime - self explanatory +vars.msgFor - scope +]] + function mist.msgMGRS(vars) + local units = vars.units + local acc = vars.acc + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = mist.getMGRSString{units = units, acc = acc} + local newText + if text then + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else + -- just append to the end. + newText = text .. s + end + else + newText = s + end + mist.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } + end + + --[[ vars for mist.msgLL +vars.units - table of unit names (NOT unitNameTable- maybe this should change) (Yes). +vars.acc - integer, number of numbers after decimal place +vars.DMS - if true, output in degrees, minutes, seconds. Otherwise, output in degrees, minutes. +vars.text - text in the message +vars.displayTime - self explanatory +vars.msgFor - scope +]] + function mist.msgLL(vars) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local acc = vars.acc + local DMS = vars.DMS + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = mist.getLLString{units = units, acc = acc, DMS = DMS} + local newText + if text then + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else + -- just append to the end. + newText = text .. s + end + else + newText = s + end + + mist.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } + + end + + --[[ +vars.units- table of unit names (NOT unitNameTable- maybe this should change). +vars.ref - vec3 ref point, maybe overload for vec2 as well? +vars.alt - boolean, if used, includes altitude in string +vars.metric - boolean, gives distance in km instead of NM. +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] + function mist.msgBR(vars) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local ref = vars.ref -- vec2/vec3 will be handled in mist.getBRString + local alt = vars.alt + local metric = vars.metric + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = mist.getBRString{units = units, ref = ref, alt = alt, metric = metric} + local newText + if text then + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else + -- just append to the end. + newText = text .. s + end + else + newText = s + end + + mist.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } + + end + + -- basically, just sub-types of mist.msgBR... saves folks the work of getting the ref point. + --[[ +vars.units- table of unit names (NOT unitNameTable- maybe this should change). +vars.ref - string red, blue +vars.alt - boolean, if used, includes altitude in string +vars.metric - boolean, gives distance in km instead of NM. +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] + function mist.msgBullseye(vars) + if mist.DBs.missionData.bullseye[string.lower(vars.ref)] then + vars.ref = mist.DBs.missionData.bullseye[string.lower(vars.ref)] + mist.msgBR(vars) + end + end + + --[[ +vars.units- table of unit names (NOT unitNameTable- maybe this should change). +vars.ref - unit name of reference point +vars.alt - boolean, if used, includes altitude in string +vars.metric - boolean, gives distance in km instead of NM. +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] + function mist.msgBRA(vars) + if Unit.getByName(vars.ref) and Unit.getByName(vars.ref):isExist() == true then + vars.ref = Unit.getByName(vars.ref):getPosition().p + if not vars.alt then + vars.alt = true + end + mist.msgBR(vars) + end + end + + --[[ vars for mist.msgLeadingMGRS: +vars.units - table of unit names +vars.heading - direction +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees (optional) +vars.acc - number, 0 to 5. +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] + function mist.msgLeadingMGRS(vars) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local heading = vars.heading + local radius = vars.radius + local headingDegrees = vars.headingDegrees + local acc = vars.acc + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = mist.getLeadingMGRSString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc} + local newText + if text then + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else + -- just append to the end. + newText = text .. s + end + else + newText = s + end + + mist.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } + + + end + + --[[ vars for mist.msgLeadingLL: +vars.units - table of unit names +vars.heading - direction, number +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees (optional) +vars.acc - number of digits after decimal point (can be negative) +vars.DMS - boolean, true if you want DMS. (optional) +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] + function mist.msgLeadingLL(vars) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local heading = vars.heading + local radius = vars.radius + local headingDegrees = vars.headingDegrees + local acc = vars.acc + local DMS = vars.DMS + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = mist.getLeadingLLString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc, DMS = DMS} + local newText + + if text then + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else + -- just append to the end. + newText = text .. s + end + else + newText = s + end + + mist.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } + + end + + --[[ +vars.units - table of unit names +vars.heading - direction, number +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees (optional) +vars.metric - boolean, if true, use km instead of NM. (optional) +vars.alt - boolean, if true, include altitude. (optional) +vars.ref - vec3/vec2 reference point. +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] + function mist.msgLeadingBR(vars) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local heading = vars.heading + local radius = vars.radius + local headingDegrees = vars.headingDegrees + local metric = vars.metric + local alt = vars.alt + local ref = vars.ref -- vec2/vec3 will be handled in mist.getBRString + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = mist.getLeadingBRString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, metric = metric, alt = alt, ref = ref} + local newText + + if text then + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else + -- just append to the end. + newText = text .. s + end + else + newText = s + end + + mist.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } + end +end + +--- Demo functions. +-- @section mist.demos +do -- mist.demos scope + mist.demos = {} + + function mist.demos.printFlightData(unit) + if unit:isExist() then + local function printData(unit, prevVel, prevE, prevTime) + local angles = mist.getAttitude(unit) + if angles then + local Heading = angles.Heading + local Pitch = angles.Pitch + local Roll = angles.Roll + local Yaw = angles.Yaw + local AoA = angles.AoA + local ClimbAngle = angles.ClimbAngle + + if not Heading then + Heading = 'NA' + else + Heading = string.format('%12.2f', mist.utils.toDegree(Heading)) + end + + if not Pitch then + Pitch = 'NA' + else + Pitch = string.format('%12.2f', mist.utils.toDegree(Pitch)) + end + + if not Roll then + Roll = 'NA' + else + Roll = string.format('%12.2f', mist.utils.toDegree(Roll)) + end + + local AoAplusYaw = 'NA' + if AoA and Yaw then + AoAplusYaw = string.format('%12.2f', mist.utils.toDegree((AoA^2 + Yaw^2)^0.5)) + end + + if not Yaw then + Yaw = 'NA' + else + Yaw = string.format('%12.2f', mist.utils.toDegree(Yaw)) + end + + if not AoA then + AoA = 'NA' + else + AoA = string.format('%12.2f', mist.utils.toDegree(AoA)) + end + + if not ClimbAngle then + ClimbAngle = 'NA' + else + ClimbAngle = string.format('%12.2f', mist.utils.toDegree(ClimbAngle)) + end + local unitPos = unit:getPosition() + local unitVel = unit:getVelocity() + local curTime = timer.getTime() + local absVel = string.format('%12.2f', mist.vec.mag(unitVel)) + + + local unitAcc = 'NA' + local Gs = 'NA' + local axialGs = 'NA' + local transGs = 'NA' + if prevVel and prevTime then + local xAcc = (unitVel.x - prevVel.x)/(curTime - prevTime) + local yAcc = (unitVel.y - prevVel.y)/(curTime - prevTime) + local zAcc = (unitVel.z - prevVel.z)/(curTime - prevTime) + + unitAcc = string.format('%12.2f', mist.vec.mag({x = xAcc, y = yAcc, z = zAcc})) + Gs = string.format('%12.2f', mist.vec.mag({x = xAcc, y = yAcc + 9.81, z = zAcc})/9.81) + axialGs = string.format('%12.2f', mist.vec.dp({x = xAcc, y = yAcc + 9.81, z = zAcc}, unitPos.x)/9.81) + transGs = string.format('%12.2f', mist.vec.mag(mist.vec.cp({x = xAcc, y = yAcc + 9.81, z = zAcc}, unitPos.x))/9.81) + end + + local E = 0.5*mist.vec.mag(unitVel)^2 + 9.81*unitPos.p.y + + local energy = string.format('%12.2e', E) + + local dEdt = 'NA' + if prevE and prevTime then + dEdt = string.format('%12.2e', (E - prevE)/(curTime - prevTime)) + end + + trigger.action.outText(string.format('%-25s', 'Heading: ') .. Heading .. ' degrees\n' .. string.format('%-25s', 'Roll: ') .. Roll .. ' degrees\n' .. string.format('%-25s', 'Pitch: ') .. Pitch + .. ' degrees\n' .. string.format('%-25s', 'Yaw: ') .. Yaw .. ' degrees\n' .. string.format('%-25s', 'AoA: ') .. AoA .. ' degrees\n' .. string.format('%-25s', 'AoA plus Yaw: ') .. AoAplusYaw .. ' degrees\n' .. string.format('%-25s', 'Climb Angle: ') .. + ClimbAngle .. ' degrees\n' .. string.format('%-25s', 'Absolute Velocity: ') .. absVel .. ' m/s\n' .. string.format('%-25s', 'Absolute Acceleration: ') .. unitAcc ..' m/s^2\n' + .. string.format('%-25s', 'Axial G loading: ') .. axialGs .. ' g\n' .. string.format('%-25s', 'Transverse G loading: ') .. transGs .. ' g\n' .. string.format('%-25s', 'Absolute G loading: ') .. Gs .. ' g\n' .. string.format('%-25s', 'Energy: ') .. energy .. ' J/kg\n' .. string.format('%-25s', 'dE/dt: ') .. dEdt ..' J/(kg*s)', 1) + return unitVel, E, curTime + end + end + + local function frameFinder(unit, prevVel, prevE, prevTime) + if unit:isExist() then + local currVel = unit:getVelocity() + if prevVel and (prevVel.x ~= currVel.x or prevVel.y ~= currVel.y or prevVel.z ~= currVel.z) or (prevTime and (timer.getTime() - prevTime) > 0.25) then + prevVel, prevE, prevTime = printData(unit, prevVel, prevE, prevTime) + end + mist.scheduleFunction(frameFinder, {unit, prevVel, prevE, prevTime}, timer.getTime() + 0.005) -- it can't go this fast, limited to the 100 times a sec check right now. + end + end + + + local curVel = unit:getVelocity() + local curTime = timer.getTime() + local curE = 0.5*mist.vec.mag(curVel)^2 + 9.81*unit:getPosition().p.y + frameFinder(unit, curVel, curE, curTime) + + end + + end + +end +do + --[[ stuff for marker panels + marker.add() add marker. Point of these functions is to simplify process and to store all mark panels added. + -- generates Id if not specified or if multiple marks created. + -- makes marks for countries by creating a mark for each client group in the country + -- can create multiple marks if needed for groups and countries. + -- adds marks to table for parsing and removing + -- Uses similar structure as messages. Big differences is it doesn't only mark to groups. + If to All, then mark is for All + if to coa mark is to coa + if to specific units, mark is to group + + + -------- + STUFF TO Check + -------- + If mark added to a group before a client joins slot is synced. + Mark made for cliet A in Slot A. Client A leaves, Client B joins in slot A. What do they see? + + May need to automate process... + + ]] + --[[ + local typeBase = { + ['Mi-8MT'] = {'Mi-8MTV2', 'Mi-8MTV', 'Mi-8'}, + ['MiG-21Bis'] = {'Mig-21'}, + ['MiG-15bis'] = {'Mig-15'}, + ['FW-190D9'] = {'FW-190'}, + ['Bf-109K-4'] = {'Bf-109'}, + } + + + local mId = 1337 + + mist.marker = {} + mist.marker.list = {} + local function markSpamFilter(recList, spamBlockOn) + + for id, name in pairs(recList) do + if name == spamBlockOn then + log:info('already on recList') + return recList + end + end + log:info('add to recList') + table.insert(recList, spamBlockOn) + return recList + end + + local function iterate() + mId = mId + 1 + return mId + end + + function mist.marker.add(pos, text, markFor, id) + log:warn('markerFunc') + log:info('Pos: $1, Text: $2, markFor: $3, id: $4', pos, text, markFor, id) + if not id then + + else + + end + local markType = 'all' + local markForTable = {} + if pos then + pos = mist.utils.makeVec3(pos) + end + if text and type(text) ~= string then + text = tostring(text) + else + text = '' + end + + if markFor then + if type(markFor) == 'number' then -- groupId + if mist.DBs.groupsById[markFor] then + markType = 'group' + end + elseif type(markFor) == 'string' then -- groupName + if mist.DBs.groupsByName[markFor] then + markType = 'group' + markFor = mist.DBs.groupsByName[markFor].groupId + end + elseif type(markFor) == 'table' then -- multiple groupName, country, coalition, all + markType = 'table' + log:info(markFor) + for forIndex, forData in pairs(markFor) do -- need to rethink this part and organization. Gotta be a more logical way to send messages to coa, groups, or all. + log:info(forIndex) + log:info(forData) + for list, listData in pairs(forData) do + log:info(listData) + forIndex = string.lower(forIndex) + if type(listData) == 'string' then + listData = string.lower(listData) + end + if listData == 'all' then + markType = 'all' + break + elseif (forIndex == 'coa' or forIndex == 'ca') then -- mark for coa or CA. + for name, index in pairs (coalition.side) do + if listData == string.lower(name) then + markType = 'coalition' + end + end + elseif (forIndex == 'countries' and string.lower(clientData.country) == listData) or (forIndex == 'units' and string.lower(clientData.unitName) == listData) then + markForTable = markSpamFilter(markForTable, clientData.groupId) + elseif forIndex == 'unittypes' then -- mark to group + -- iterate play units + for clientId, clientData in pairs(mist.DBs.humansById) do + for typeId, typeData in pairs(listData) do + log:info(typeData) + local found = false + if list == 'all' or clientData.coalition and type(clientData.coalition) == 'string' and mist.stringMatch(clientData.coalition, list) then + if mist.matchString(typeData, clientData.type) then + found = true + else + -- check other known names for aircraft + end + end + if found == true then + markForTable = markSpamFilter(markForTable, clientData.groupId) -- sends info to other function to see if client is already recieving the current message. + end + for clientDataEntry, clientDataVal in pairs(clientData) do + if type(clientDataVal) == 'string' then + + if mist.matchString(list, clientDataVal) == true or list == 'all' then + local sString = typeData + for rName, pTbl in pairs(typeBase) do -- just a quick check to see if the user may have meant something and got the specific type of the unit wrong + for pIndex, pName in pairs(pTbl) do + if mist.stringMatch(sString, pName) then + sString = rName + end + end + end + if mist.stringMatch(sString, clientData.type) then + found = true + markForTable = markSpamFilter(markForTable, clientData.groupId) -- sends info oto other function to see if client is already recieving the current message. + --table.insert(newMsgFor, clientId) + end + end + end + if found == true then -- shouldn't this be elsewhere too? + break + end + end + end + + end + end + end + end + end + else + markType = 'all' + end + + + + + + + if markType ~= 'table' then + local newId = iterate() + local data = {markId = newId, text = text, pos = pos, markType = markType, markFor = markFor} + + -- create marks + if markType == 'coa' then + trigger.action.markToCoalition(newId, text, pos, markFor) + elseif markType == 'group' then + trigger.action.markToGroup(newId, text, pos, markFor) + else + trigger.action.markToAll(iterate(), text, pos) + end + table.insert(mist.marker.list, data) -- add to the DB + else + if #markForTable > 0 then + log:info('iterate') + for i = 1, #markForTable do + local newId = iterate() + local data = {markId = newId, text = text, pos = pos, markFor = markFor} + log:info(data) + table.insert(mist.marker.list, data) + trigger.action.markToGroup(newId, text, pos, markForTable[i]) + end + end + end + + + + end + + function mist.marker.remove(id) + for i, data in pairs(mist.marker.list) do + if id == data.markId then + trigger.action.removeMark(id) + end + end + end + + function mist.marker.get(id) + + end + + function mist.marker.coords(pos, cType, markFor, id) -- wrapper function to just display coordinates of a specific format at location + + + end + ]] +end +--- Time conversion functions. +-- @section mist.time +do -- mist.time scope + mist.time = {} + -- returns a string for specified military time + -- theTime is optional + -- if present current time in mil time is returned + -- if number or table the time is converted into mil tim + function mist.time.convertToSec(timeTable) + + local timeInSec = 0 + if timeTable and type(timeTable) == 'number' then + timeInSec = timeTable + elseif timeTable and type(timeTable) == 'table' and (timeTable.d or timeTable.h or timeTable.m or timeTable.s) then + if timeTable.d and type(timeTable.d) == 'number' then + timeInSec = timeInSec + (timeTable.d*86400) + end + if timeTable.h and type(timeTable.h) == 'number' then + timeInSec = timeInSec + (timeTable.h*3600) + end + if timeTable.m and type(timeTable.m) == 'number' then + timeInSec = timeInSec + (timeTable.m*60) + end + if timeTable.s and type(timeTable.s) == 'number' then + timeInSec = timeInSec + timeTable.s + end + + end + return timeInSec + end + + function mist.time.getDHMS(timeInSec) + if timeInSec and type(timeInSec) == 'number' then + local tbl = {d = 0, h = 0, m = 0, s = 0} + if timeInSec > 86400 then + while timeInSec > 86400 do + tbl.d = tbl.d + 1 + timeInSec = timeInSec - 86400 + end + end + if timeInSec > 3600 then + while timeInSec > 3600 do + tbl.h = tbl.h + 1 + timeInSec = timeInSec - 3600 + end + end + if timeInSec > 60 then + while timeInSec > 60 do + tbl.m = tbl.m + 1 + timeInSec = timeInSec - 60 + end + end + tbl.s = timeInSec + return tbl + else + log:error("Didn't recieve number") + return + end + end + + function mist.getMilString(theTime) + local timeInSec = 0 + if theTime then + timeInSec = mist.time.convertToSec(theTime) + else + timeInSec = mist.utils.round(timer.getAbsTime(), 0) + end + + local DHMS = mist.time.getDHMS(timeInSec) + + return tostring(string.format('%02d', DHMS.h) .. string.format('%02d',DHMS.m)) + end + + function mist.getClockString(theTime, hour) + local timeInSec = 0 + if theTime then + timeInSec = mist.time.convertToSec(theTime) + else + timeInSec = mist.utils.round(timer.getAbsTime(), 0) + end + local DHMS = mist.time.getDHMS(timeInSec) + if hour then + if DHMS.h > 12 then + DHMS.h = DHMS.h - 12 + return tostring(string.format('%02d', DHMS.h) .. ':' .. string.format('%02d',DHMS.m) .. ':' .. string.format('%02d',DHMS.s) .. ' PM') + else + return tostring(string.format('%02d', DHMS.h) .. ':' .. string.format('%02d',DHMS.m) .. ':' .. string.format('%02d',DHMS.s) .. ' AM') + end + else + return tostring(string.format('%02d', DHMS.h) .. ':' .. string.format('%02d',DHMS.m) .. ':' .. string.format('%02d',DHMS.s)) + end + end + + -- returns the date in string format + -- both variables optional + -- first val returns with the month as a string + -- 2nd val defins if it should be written the American way or the wrong way. + function mist.time.getDate(convert) + local cal = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} -- + local date = {} + + if not env.mission.date then -- Not likely to happen. Resaving mission auto updates this to remove it. + date.d = 0 + date.m = 6 + date.y = 2011 + else + date.d = env.mission.date.Day + date.m = env.mission.date.Month + date.y = env.mission.date.Year + end + local start = 86400 + local timeInSec = mist.utils.round(timer.getAbsTime()) + if convert and type(convert) == 'number' then + timeInSec = convert + end + if timeInSec > 86400 then + while start < timeInSec do + if date.d >= cal[date.m] then + if date.m == 2 and date.d == 28 then -- HOLY COW we can edit years now. Gotta re-add this! + if date.y % 4 == 0 and date.y % 100 == 0 and date.y % 400 ~= 0 or date.y % 4 > 0 then + date.m = date.m + 1 + date.d = 0 + end + --date.d = 29 + else + date.m = date.m + 1 + date.d = 0 + end + end + if date.m == 13 then + date.m = 1 + date.y = date.y + 1 + end + date.d = date.d + 1 + start = start + 86400 + + end + end + return date + end + + function mist.time.relativeToStart(time) + if type(time) == 'number' then + return time - timer.getTime0() + end + end + + function mist.getDateString(rtnType, murica, oTime) -- returns date based on time + local word = {'January', 'Feburary', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' } -- 'etc + local curTime = 0 + if oTime then + curTime = oTime + else + curTime = mist.utils.round(timer.getAbsTime()) + end + local tbl = mist.time.getDate(curTime) + + if rtnType then + if murica then + return tostring(word[tbl.m] .. ' ' .. tbl.d .. ' ' .. tbl.y) + else + return tostring(tbl.d .. ' ' .. word[tbl.m] .. ' ' .. tbl.y) + end + else + if murica then + return tostring(tbl.m .. '.' .. tbl.d .. '.' .. tbl.y) + else + return tostring(tbl.d .. '.' .. tbl.m .. '.' .. tbl.y) + end + end + end + --WIP + function mist.time.milToGame(milString, rtnType) --converts a military time. By default returns the abosolute time that event would occur. With optional value it returns how many seconds from time of call till that time. + local curTime = mist.utils.round(timer.getAbsTime()) + local milTimeInSec = 0 + + if milString and type(milString) == 'string' and string.len(milString) >= 4 then + local hr = tonumber(string.sub(milString, 1, 2)) + local mi = tonumber(string.sub(milString, 3)) + milTimeInSec = milTimeInSec + (mi*60) + (hr*3600) + elseif milString and type(milString) == 'table' and (milString.d or milString.h or milString.m or milString.s) then + milTimeInSec = mist.time.convertToSec(milString) + end + + local startTime = timer.getTime0() + local daysOffset = 0 + if startTime > 86400 then + daysOffset = mist.utils.round(startTime/86400) + if daysOffset > 0 then + milTimeInSec = milTimeInSec *daysOffset + end + end + + if curTime > milTimeInSec then + milTimeInSec = milTimeInSec + 86400 + end + if rtnType then + milTimeInSec = milTimeInSec - startTime + end + return milTimeInSec + end + + +end + +--- Group task functions. +-- @section tasks +do -- group tasks scope + mist.ground = {} + mist.fixedWing = {} + mist.heli = {} + mist.air = {} + mist.air.fixedWing = {} + mist.air.heli = {} + + --- Tasks group to follow a route. + -- This sets the mission task for the given group. + -- Any wrapped actions inside the path (like enroute + -- tasks) will be executed. + -- @tparam Group group group to task. + -- @tparam table path containing + -- points defining a route. + function mist.goRoute(group, path) + local misTask = { + id = 'Mission', + params = { + route = { + points = mist.utils.deepCopy(path), + }, + }, + } + if type(group) == 'string' then + group = Group.getByName(group) + end + if group then + local groupCon = group:getController() + if groupCon then + log:warn(misTask) + groupCon:setTask(misTask) + return true + end + end + return false + end + + -- same as getGroupPoints but returns speed and formation type along with vec2 of point} + function mist.getGroupRoute(groupIdent, task) + -- refactor to search by groupId and allow groupId and groupName as inputs + local gpId = groupIdent + if mist.DBs.MEgroupsByName[groupIdent] then + gpId = mist.DBs.MEgroupsByName[groupIdent].groupId + else + log:error('$1 not found in mist.DBs.MEgroupsByName', groupIdent) + end + + for coa_name, coa_data in pairs(env.mission.coalition) do + if type(coa_data) == 'table' then + if coa_data.country then --there is a country table + for cntry_id, cntry_data in pairs(coa_data.country) do + for obj_type_name, obj_type_data in pairs(cntry_data) do + if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" then -- only these types have points + if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! + for group_num, group_data in pairs(obj_type_data.group) do + if group_data and group_data.groupId == gpId then -- this is the group we are looking for + if group_data.route and group_data.route.points and #group_data.route.points > 0 then + local points = {} + + for point_num, point in pairs(group_data.route.points) do + local routeData = {} + if env.mission.version > 7 then + routeData.name = env.getValueDictByKey(point.name) + else + routeData.name = point.name + end + if not point.point then + routeData.x = point.x + routeData.y = point.y + else + routeData.point = point.point --it's possible that the ME could move to the point = Vec2 notation. + end + routeData.form = point.action + routeData.speed = point.speed + routeData.alt = point.alt + routeData.alt_type = point.alt_type + routeData.airdromeId = point.airdromeId + routeData.helipadId = point.helipadId + routeData.type = point.type + routeData.action = point.action + if task then + routeData.task = point.task + end + points[point_num] = routeData + end + + return points + end + log:error('Group route not defined in mission editor for groupId: $1', gpId) + return + end --if group_data and group_data.name and group_data.name == 'groupname' + end --for group_num, group_data in pairs(obj_type_data.group) do + end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then + end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then + end --for obj_type_name, obj_type_data in pairs(cntry_data) do + end --for cntry_id, cntry_data in pairs(coa_data.country) do + end --if coa_data.country then --there is a country table + end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then + end --for coa_name, coa_data in pairs(mission.coalition) do + end + + -- function mist.ground.buildPath() end -- ???? + + function mist.ground.patrolRoute(vars) + --log:info('patrol') + local tempRoute = {} + local useRoute = {} + local gpData = vars.gpData + if type(gpData) == 'string' then + gpData = Group.getByName(gpData) + end + + local useGroupRoute + if not vars.useGroupRoute then + useGroupRoute = vars.gpData + else + useGroupRoute = vars.useGroupRoute + end + local routeProvided = false + if not vars.route then + if useGroupRoute then + tempRoute = mist.getGroupRoute(useGroupRoute) + end + else + useRoute = vars.route + local posStart = mist.getLeadPos(gpData) + useRoute[1] = mist.ground.buildWP(posStart, useRoute[1].action, useRoute[1].speed) + routeProvided = true + end + + + local overRideSpeed = vars.speed or 'default' + local pType = vars.pType + local offRoadForm = vars.offRoadForm or 'default' + local onRoadForm = vars.onRoadForm or 'default' + + if routeProvided == false and #tempRoute > 0 then + local posStart = mist.getLeadPos(gpData) + + + useRoute[#useRoute + 1] = mist.ground.buildWP(posStart, offRoadForm, overRideSpeed) + for i = 1, #tempRoute do + local tempForm = tempRoute[i].action + local tempSpeed = tempRoute[i].speed + + if offRoadForm == 'default' then + tempForm = tempRoute[i].action + end + if onRoadForm == 'default' then + onRoadForm = 'On Road' + end + if (string.lower(tempRoute[i].action) == 'on road' or string.lower(tempRoute[i].action) == 'onroad' or string.lower(tempRoute[i].action) == 'on_road') then + tempForm = onRoadForm + else + tempForm = offRoadForm + end + + if type(overRideSpeed) == 'number' then + tempSpeed = overRideSpeed + end + + + useRoute[#useRoute + 1] = mist.ground.buildWP(tempRoute[i], tempForm, tempSpeed) + end + + if pType and string.lower(pType) == 'doubleback' then + local curRoute = mist.utils.deepCopy(useRoute) + for i = #curRoute, 2, -1 do + useRoute[#useRoute + 1] = mist.ground.buildWP(curRoute[i], curRoute[i].action, curRoute[i].speed) + end + end + + useRoute[1].action = useRoute[#useRoute].action -- make it so the first WP matches the last WP + end + + local cTask3 = {} + local newPatrol = {} + newPatrol.route = useRoute + newPatrol.gpData = gpData:getName() + cTask3[#cTask3 + 1] = 'mist.ground.patrolRoute(' + cTask3[#cTask3 + 1] = mist.utils.oneLineSerialize(newPatrol) + cTask3[#cTask3 + 1] = ')' + cTask3 = table.concat(cTask3) + local tempTask = { + id = 'WrappedAction', + params = { + action = { + id = 'Script', + params = { + command = cTask3, + + }, + }, + }, + } + + useRoute[#useRoute].task = tempTask + log:info(useRoute) + mist.goRoute(gpData, useRoute) + + return + end + + function mist.ground.patrol(gpData, pType, form, speed) + local vars = {} + + if type(gpData) == 'table' and gpData:getName() then + gpData = gpData:getName() + end + + vars.useGroupRoute = gpData + vars.gpData = gpData + vars.pType = pType + vars.offRoadForm = form + vars.speed = speed + + mist.ground.patrolRoute(vars) + + return + end + + -- No longer accepts path + function mist.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 = mist.utils.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 + + function mist.fixedWing.buildWP(point, WPtype, speed, alt, altType) + + local wp = {} + wp.x = point.x + + if point.z then + wp.y = point.z + else + wp.y = point.y + end + + if alt and type(alt) == 'number' then + wp.alt = alt + else + wp.alt = 2000 + end + + if altType then + altType = string.lower(altType) + if altType == 'radio' or altType == 'agl' then + wp.alt_type = 'RADIO' + elseif altType == 'baro' or altType == 'asl' then + wp.alt_type = 'BARO' + end + else + wp.alt_type = 'RADIO' + end + + if point.speed then + speed = point.speed + end + + if point.type then + WPtype = point.type + end + + if not speed then + wp.speed = mist.utils.kmphToMps(500) + else + wp.speed = speed + end + + if not WPtype then + wp.action = 'Turning Point' + else + WPtype = string.lower(WPtype) + if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then + wp.action = 'Fly Over Point' + elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then + wp.action = 'Turning Point' + else + wp.action = 'Turning Point' + end + end + + wp.type = 'Turning Point' + return wp + end + + function mist.heli.buildWP(point, WPtype, speed, alt, altType) + + local wp = {} + wp.x = point.x + + if point.z then + wp.y = point.z + else + wp.y = point.y + end + + if alt and type(alt) == 'number' then + wp.alt = alt + else + wp.alt = 500 + end + + if altType then + altType = string.lower(altType) + if altType == 'radio' or altType == 'agl' then + wp.alt_type = 'RADIO' + elseif altType == 'baro' or altType == 'asl' then + wp.alt_type = 'BARO' + end + else + wp.alt_type = 'RADIO' + end + + if point.speed then + speed = point.speed + end + + if point.type then + WPtype = point.type + end + + if not speed then + wp.speed = mist.utils.kmphToMps(200) + else + wp.speed = speed + end + + if not WPtype then + wp.action = 'Turning Point' + else + WPtype = string.lower(WPtype) + if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then + wp.action = 'Fly Over Point' + elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then + wp.action = 'Turning Point' + else + wp.action = 'Turning Point' + end + end + + wp.type = 'Turning Point' + return wp + end + + -- need to return a Vec3 or Vec2? + function mist.getRandPointInCircle(p, radius, innerRadius, maxA, minA) + local point = mist.utils.makeVec3(p) + local theta = 2*math.pi*math.random() + local minR = innerRadius or 0 + if maxA and not minA then + theta = math.rad(math.random(0, maxA - math.random())) + elseif maxA and minA and minA < maxA then + theta = math.rad(math.random(minA, maxA) - math.random()) + end + local rad = math.random() + math.random() + if rad > 1 then + rad = 2 - rad + end + + local radMult + if minR and minR <= radius then + --radMult = (radius - innerRadius)*rad + innerRadius + radMult = radius * math.sqrt((minR^2 + (radius^2 - minR^2) * math.random()) / radius^2) + else + radMult = radius*rad + end + + local rndCoord + if radius > 0 then + rndCoord = {x = math.cos(theta)*radMult + point.x, y = math.sin(theta)*radMult + point.z} + else + rndCoord = {x = point.x, y = point.z} + end + return rndCoord + end + + function mist.getRandomPointInZone(zoneName, innerRadius, maxA, minA) + if type(zoneName) == 'string' and type(trigger.misc.getZone(zoneName)) == 'table' then + return mist.getRandPointInCircle(trigger.misc.getZone(zoneName).point, trigger.misc.getZone(zoneName).radius, innerRadius, maxA, minA) + end + return false + end + + function mist.getRandomPointInPoly(zone) + local avg = mist.getAvgPoint(zone) + local radius = 0 + local minR = math.huge + local newCoord = {} + for i = 1, #zone do + if mist.utils.get2DDist(avg, zone[i]) > radius then + radius = mist.utils.get2DDist(avg, zone[i]) + end + if mist.utils.get2DDist(avg, zone[i]) < minR then + minR = mist.utils.get2DDist(avg, zone[i]) + end + end + local lSpawnPos = {} + for j = 1, 100 do + newCoord = mist.getRandPointInCircle(avg, radius) + if mist.pointInPolygon(newCoord, zone) then + break + end + if j == 100 then + newCoord = mist.getRandPointInCircle(avg, 50000) + log:warn("Failed to find point in poly; Giving random point from center of the poly") + end + end + return newCoord + end + + function mist.groupToRandomPoint(vars) + local group = vars.group --Required + local point = vars.point --required + local radius = vars.radius or 0 + local innerRadius = vars.innerRadius + local form = vars.form or 'Cone' + local heading = vars.heading or math.random()*2*math.pi + local headingDegrees = vars.headingDegrees + local speed = vars.speed or mist.utils.kmphToMps(20) + + + local useRoads + if not vars.disableRoads then + useRoads = true + else + useRoads = false + end + + local path = {} + + if headingDegrees then + heading = headingDegrees*math.pi/180 + end + + if heading >= 2*math.pi then + heading = heading - 2*math.pi + end + + local rndCoord = mist.getRandPointInCircle(point, radius, innerRadius) + + local offset = {} + local posStart = mist.getLeadPos(group) + if posStart then + offset.x = mist.utils.round(math.sin(heading - (math.pi/2)) * 50 + rndCoord.x, 3) + offset.z = mist.utils.round(math.cos(heading + (math.pi/2)) * 50 + rndCoord.y, 3) + path[#path + 1] = mist.ground.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] = mist.ground.buildWP({x = posStart.x + 11, z = posStart.z + 11}, 'off_road', speed) + path[#path + 1] = mist.ground.buildWP(posStart, 'on_road', speed) + path[#path + 1] = mist.ground.buildWP(offset, 'on_road', speed) + else + path[#path + 1] = mist.ground.buildWP({x = posStart.x + 25, z = posStart.z + 25}, form, speed) + end + end + path[#path + 1] = mist.ground.buildWP(offset, form, speed) + path[#path + 1] = mist.ground.buildWP(rndCoord, form, speed) + + mist.goRoute(group, path) + + return + end + + function mist.groupRandomDistSelf(gpData, dist, form, heading, speed) + local pos = mist.getLeadPos(gpData) + local fakeZone = {} + fakeZone.radius = dist or math.random(300, 1000) + fakeZone.point = {x = pos.x, y = pos.y, z = pos.z} + mist.groupToRandomZone(gpData, fakeZone, form, heading, speed) + + return + end + + function mist.groupToRandomZone(gpData, zone, form, heading, speed) + if type(gpData) == 'string' then + gpData = Group.getByName(gpData) + end + + if type(zone) == 'string' then + zone = trigger.misc.getZone(zone) + elseif type(zone) == 'table' and not zone.radius then + zone = trigger.misc.getZone(zone[math.random(1, #zone)]) + end + + if speed then + speed = mist.utils.kmphToMps(speed) + end + + local vars = {} + vars.group = gpData + vars.radius = zone.radius + vars.form = form + vars.headingDegrees = heading + vars.speed = speed + vars.point = mist.utils.zoneToVec3(zone) + + mist.groupToRandomPoint(vars) + + return + end + + function mist.isTerrainValid(coord, terrainTypes) -- vec2/3 and enum or table of acceptable terrain types + if coord.z then + coord.y = coord.z + end + local typeConverted = {} + + if type(terrainTypes) == 'string' then -- if its a string it does this check + for constId, constData in pairs(land.SurfaceType) do + if string.lower(constId) == string.lower(terrainTypes) or string.lower(constData) == string.lower(terrainTypes) then + table.insert(typeConverted, constId) + end + end + elseif type(terrainTypes) == 'table' then -- if its a table it does this check + for typeId, typeData in pairs(terrainTypes) do + for constId, constData in pairs(land.SurfaceType) do + if string.lower(constId) == string.lower(typeData) or string.lower(constData) == string.lower(typeData) then + table.insert(typeConverted, constId) + end + end + end + end + for validIndex, validData in pairs(typeConverted) do + if land.getSurfaceType(coord) == land.SurfaceType[validData] then + log:info('Surface is : $1', validData) + return true + end + end + return false + end + + function mist.terrainHeightDiff(coord, searchSize) + local samples = {} + local searchRadius = 5 + if searchSize then + searchRadius = searchSize + end + if type(coord) == 'string' then + coord = mist.utils.zoneToVec3(coord) + end + + coord = mist.utils.makeVec2(coord) + + samples[#samples + 1] = land.getHeight(coord) + for i = 0, 360, 30 do + samples[#samples + 1] = land.getHeight({x = (coord.x + (math.sin(math.rad(i))*searchRadius)), y = (coord.y + (math.cos(math.rad(i))*searchRadius))}) + if searchRadius >= 20 then -- if search radius is sorta large, take a sample halfway between center and outer edge + samples[#samples + 1] = land.getHeight({x = (coord.x + (math.sin(math.rad(i))*(searchRadius/2))), y = (coord.y + (math.cos(math.rad(i))*(searchRadius/2)))}) + end + end + local tMax, tMin = 0, 1000000 + for index, height in pairs(samples) do + if height > tMax then + tMax = height + end + if height < tMin then + tMin = height + end + end + return mist.utils.round(tMax - tMin, 2) + end + + function mist.groupToPoint(gpData, point, form, heading, speed, useRoads) + if type(point) == 'string' then + point = trigger.misc.getZone(point) + end + if speed then + speed = mist.utils.kmphToMps(speed) + end + + local vars = {} + vars.group = gpData + vars.form = form + vars.headingDegrees = heading + vars.speed = speed + vars.disableRoads = useRoads + vars.point = mist.utils.zoneToVec3(point) + mist.groupToRandomPoint(vars) + + return + end + + function mist.getLeadPos(group) + if type(group) == 'string' then -- group name + group = Group.getByName(group) + end + + local units = group:getUnits() + + local leader = units[1] + if Unit.getLife(leader) == 0 or 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 + if leader and Unit.isExist(leader) then -- maybe a little too paranoid now... + return leader:getPosition().p + end + end + +end + +--- Database tables. +-- @section mist.DBs + +--- Mission data +-- @table mist.DBs.missionData +-- @field startTime mission start time +-- @field theatre mission theatre/map e.g. Caucasus +-- @field version mission version +-- @field files mission resources + +--- Tables used as parameters. +-- @section varTables + +--- mist.flagFunc.units_in_polygon parameter table. +-- @table unitsInPolygonVars +-- @tfield table unit name table @{UnitNameTable}. +-- @tfield table zone table defining a polygon. +-- @tfield number|string flag flag to set to true. +-- @tfield[opt] number|string stopflag if set to true the function +-- will stop evaluating. +-- @tfield[opt] number maxalt maximum altitude (MSL) for the +-- polygon. +-- @tfield[opt] number req_num minimum number of units that have +-- to be in the polygon. +-- @tfield[opt] number interval sets the interval for +-- checking if units are inside of the polygon in seconds. Default: 1. +-- @tfield[opt] boolean toggle switch the flag to false if required +-- conditions are not met. Default: false. +-- @tfield[opt] table unitTableDef +--- Logger class. +-- @type mist.Logger +do -- mist.Logger scope + mist.Logger = {} + + --- parses text and substitutes keywords with values from given array. + -- @param text string containing keywords to substitute with values + -- or a variable. + -- @param ... variables to use for substitution in string. + -- @treturn string new string with keywords substituted or + -- value of variable as string. + local function formatText(text, ...) + if type(text) ~= 'string' then + if type(text) == 'table' then + text = mist.utils.oneLineSerialize(text) + else + text = tostring(text) + end + else + for index,value in ipairs(arg) do + -- TODO: check for getmetatabel(value).__tostring + if type(value) == 'table' then + value = mist.utils.oneLineSerialize(value) + else + value = tostring(value) + end + text = text:gsub('$' .. index, value) + end + end + local fName = nil + local cLine = nil + if debug then + local dInfo = debug.getinfo(3) + fName = dInfo.name + cLine = dInfo.currentline + -- local fsrc = dinfo.short_src + --local fLine = dInfo.linedefined + end + if fName and cLine then + return fName .. '|' .. cLine .. ': ' .. text + elseif cLine then + return cLine .. ': ' .. text + else + return ' ' .. text + end + end + + local function splitText(text) + local tbl = {} + while text:len() > 4000 do + local sub = text:sub(1, 4000) + text = text:sub(4001) + table.insert(tbl, sub) + end + table.insert(tbl, text) + return tbl + end + + --- Creates a new logger. + -- Each logger has it's own tag and log level. + -- @tparam string tag tag which appears at the start of + -- every log line produced by this logger. + -- @tparam[opt] number|string level the log level defines which messages + -- will be logged and which will be omitted. Log level 3 beeing the most verbose + -- and 0 disabling all output. This can also be a string. Allowed strings are: + -- "none" (0), "error" (1), "warning" (2) and "info" (3). + -- @usage myLogger = mist.Logger:new("MyScript") + -- @usage myLogger = mist.Logger:new("MyScript", 2) + -- @usage myLogger = mist.Logger:new("MyScript", "info") + -- @treturn mist.Logger + function mist.Logger:new(tag, level) + local l = {tag = tag} + setmetatable(l, self) + self.__index = self + l:setLevel(level) + return l + end + + --- Sets the level of verbosity for this logger. + -- @tparam[opt] number|string level the log level defines which messages + -- will be logged and which will be omitted. Log level 3 beeing the most verbose + -- and 0 disabling all output. This can also[ be a string. Allowed strings are: + -- "none" (0), "error" (1), "warning" (2) and "info" (3). + -- @usage myLogger:setLevel("info") + -- @usage -- log everything + --myLogger:setLevel(3) + function mist.Logger:setLevel(level) + if not level then + self.level = 2 + else + if type(level) == 'string' then + if level == 'none' or level == 'off' then + self.level = 0 + elseif level == 'error' then + self.level = 1 + elseif level == 'warning' or level == 'warn' then + self.level = 2 + elseif level == 'info' then + self.level = 3 + end + elseif type(level) == 'number' then + self.level = level + else + self.level = 2 + end + end + end + + --- Logs error and shows alert window. + -- This logs an error to the dcs.log and shows a popup window, + -- pausing the simulation. This works always even if logging is + -- disabled by setting a log level of "none" or 0. + -- @tparam string text the text with keywords to substitute. + -- @param ... variables to be used for substitution. + -- @usage myLogger:alert("Shit just hit the fan! WEEEE!!!11") + function mist.Logger:alert(text, ...) + text = formatText(text, unpack(arg)) + if text:len() > 4000 then + local texts = splitText(text) + for i = 1, #texts do + if i == 1 then + env.error(self.tag .. '|' .. texts[i], true) + else + env.error(texts[i]) + end + end + else + env.error(self.tag .. '|' .. text, true) + end + end + + --- Logs a message, disregarding the log level. + -- @tparam string text the text with keywords to substitute. + -- @param ... variables to be used for substitution. + -- @usage myLogger:msg("Always logged!") + function mist.Logger:msg(text, ...) + text = formatText(text, unpack(arg)) + if text:len() > 4000 then + local texts = splitText(text) + for i = 1, #texts do + if i == 1 then + env.info(self.tag .. '|' .. texts[i]) + else + env.info(texts[i]) + end + end + else + env.info(self.tag .. '|' .. text) + end + end + + --- Logs an error. + -- logs a message prefixed with this loggers tag to dcs.log as + -- long as at least the "error" log level (1) is set. + -- @tparam string text the text with keywords to substitute. + -- @param ... variables to be used for substitution. + -- @usage myLogger:error("Just an error!") + -- @usage myLogger:error("Foo is $1 instead of $2", foo, "bar") + function mist.Logger:error(text, ...) + if self.level >= 1 then + text = formatText(text, unpack(arg)) + if text:len() > 4000 then + local texts = splitText(text) + for i = 1, #texts do + if i == 1 then + env.error(self.tag .. '|' .. texts[i]) + else + env.error(texts[i]) + end + end + else + env.error(self.tag .. '|' .. text, mistSettings.errorPopup) + end + end + end + + --- Logs a warning. + -- logs a message prefixed with this loggers tag to dcs.log as + -- long as at least the "warning" log level (2) is set. + -- @tparam string text the text with keywords to substitute. + -- @param ... variables to be used for substitution. + -- @usage myLogger:warn("Mother warned you! Those $1 from the interwebs are $2", {"geeks", 1337}) + function mist.Logger:warn(text, ...) + if self.level >= 2 then + text = formatText(text, unpack(arg)) + if text:len() > 4000 then + local texts = splitText(text) + for i = 1, #texts do + if i == 1 then + env.warning(self.tag .. '|' .. texts[i]) + else + env.warning(texts[i]) + end + end + else + env.warning(self.tag .. '|' .. text, mistSettings.warnPopup) + end + end + end + + --- Logs a info. + -- logs a message prefixed with this loggers tag to dcs.log as + -- long as the highest log level (3) "info" is set. + -- @tparam string text the text with keywords to substitute. + -- @param ... variables to be used for substitution. + -- @see warn + function mist.Logger:info(text, ...) + if self.level >= 3 then + text = formatText(text, unpack(arg)) + if text:len() > 4000 then + local texts = splitText(text) + for i = 1, #texts do + if i == 1 then + env.info(self.tag .. '|' .. texts[i]) + else + env.info(texts[i]) + end + end + else + env.info(self.tag .. '|' .. text, mistSettings.infoPopup) + end + end + end + +end + + +-- initialize mist +mist.init() +env.info(('Mist version ' .. mist.majorVersion .. '.' .. mist.minorVersion .. '.' .. mist.build .. ' loaded.')) + +-- vim: noet:ts=2:sw=2