[
  {
    "path": ".gitattributes",
    "content": "# Auto detect text files and perform LF normalization\n* text=auto\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_STORE\n/demo-missions/spikes/"
  },
  {
    "path": "LICENSE.md",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "# Skynet-IADS\n![logo](/images/SA3_2.jpg)\n\nAn IADS (Integrated Air Defence System) script for DCS (Digital Combat Simulator).\n\n# Abstract\nThis script simulates an IADS within the scripting possibilities of DCS. Early Warning Radar Stations (EW Radar) scan the sky for contacts. These contacts are correlated with SAM (Surface to Air Missile) sites. If a contact is within firing range of the SAM site it will become active.\n\nA modern IADS also depends on command centers and datalinks to the SAM sites. The IADS can be set up with this infrastructure. Destroying it will degrade the capability of the IADS.\n\nThis all sounds gibberish to you? Watch [this video by Covert Cabal on modern IADS](https://www.youtube.com/watch?v=9J9kntzkSQY).\n\nVisit [this DCS forum thread](https://forums.eagle.ru/topic/226173-skynet-an-iads-for-mission-builders) for development updates.\n\nJoin the [Skynet discord group](https://discord.gg/pz8wcQs) and get support setting up your mission.\n\nSkynet supports the [HighDigitSAMs Mod](https://github.com/Auranis/HighDigitSAMs).\n\nYou can also connect [Skynet with the AI_A2A_DISPATCHER](#how-do-i-connect-skynet-with-the-moose-ai_a2a_dispatcher-and-what-are-the-benefits-of-that) by MOOSE to add interceptors to the IADS.\n\n**So far over 200 hours of work went in to the development of Skynet.  \nIf you like using it, please consider a donation:**\n\n[![Skynet IADS donation](/images/btn_donateCC_LG.gif.png)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=7GSVFH448BWFQ&source=url)\n\n\n\n\n Table of Contents\n =================\n  * [Skynet\\-IADS](#skynet-iads)\n * [Abstract](#abstract)\n * [Quick start](#quick-start)\n * [Skynet IADS Elements](#skynet-iads-elements)\n   * [IADS](#iads)\n   * [Track files](#track-files)\n   * [Comand Centers](#comand-centers)\n   * [SAM Sites](#sam-sites)\n   * [Early Warning Radars](#early-warning-radars)\n   * [Power Sources](#power-sources)\n   * [Connection Nodes](#connection-nodes)\n   * [AWACS (Airborne Early Warning and Control System)\n](#awacs-airborne-early-warning-and-control-system)\n   * [Ships](#ships)\n * [Tactics](#tactics)\n   * [HARM defence](#harm-defence)\n     * [HARM detection](#harm-detection)\n     * [HARM flight path analysis](#harm-flight-path-analysis)\n   * [HARM radar shutdown](#harm-radar-shutdown)\n   * [Point defence](#point-defence)\n   * [Electronic Warfare](#electronic-warfare)\n * [Using Skynet in the mission editor](#using-skynet-in-the-mission-editor)\n   * [Placing units](#placing-units)\n   * [Preparing a SAM site](#preparing-a-sam-site)\n   * [Preparing an EW radar](#preparing-an-ew-radar)\n   * [Adding the Skynet code](#adding-the-skynet-code)\n   * [Adding the Skynet IADS](#adding-the-skynet-iads)\n * [Advanced setup](#advanced-setup)\n   * [IADS configuration](#iads-configuration)\n   * [Adding a command center](#adding-a-command-center)\n   * [Power sources and connection nodes](#power-sources-and-connection-nodes)\n   * [Warm up the SAM sites of an IADS](#warm-up-the-sam-sites-of-an-iads)\n   * [Connecting Skynet to the MOOSE AI\\_A2A\\_DISPATCHER](#connecting-skynet-to-the-moose-ai_a2a_dispatcher)\n   * [SAM site configuration](#sam-site-configuration)\n     * [Adding SAM sites](#adding-sam-sites)\n       * [Add multiple SAM sites](#add-multiple-sam-sites)\n       * [Add a SAM site manually](#add-a-sam-site-manually)\n     * [Accessing SAM sites in the IADS](#accessing-sam-sites-in-the-iads)\n     * [Act as EW radar](#act-as-ew-radar)\n     * [Engagement zone](#engagement-zone)\n       * [Engagement zone options](#engagement-zone-options)\n     * [Engage air weapons](#engage-air-weapons)\n     * [Engage HARM](#engage-harm)\n   * [Add go live constraints](#add-go-live-constraints)\n     * [Use cases](#use-cases)\n   * [Contact](#contact)\n   * [EW radar configuration](#ew-radar-configuration)\n     * [Adding EW radars](#adding-ew-radars)\n       * [Add multiple EW radars](#add-multiple-ew-radars)\n       * [Add an EW radar manually](#add-an-ew-radar-manually)\n     * [Accessing EW radars in the IADS](#accessing-ew-radars-in-the-iads)\n   * [Options for SAM sites and EW radars](#options-for-sam-sites-and-ew-radars)\n     * [Setting an option](#setting-an-option)\n     * [Daisy chaining options](#daisy-chaining-options)\n     * [HARM Defence](#harm-defence-1)\n     * [Point defence](#point-defence-1)\n     * [Autonomous mode behaviour](#autonomous-mode-behaviour)\n       * [Autonomous mode options](#autonomous-mode-options)\n   * [Adding a jammer](#adding-a-jammer)\n     * [Advanced functions](#advanced-functions)\n   * [Setting debug information](#setting-debug-information)\n * [Example Setup](#example-setup)\n * [FAQ](#faq)\n   * [Does Skynet IADS have an impact on game performance?](#does-skynet-iads-have-an-impact-on-game-performance)\n   * [What air defence units shall I add to the Skynet IADS?](#what-air-defence-units-shall-i-add-to-the-skynet-iads)\n   * [Which SAM systems can engage HARMS?](#which-sam-systems-can-engage-harms)\n   * [What exactly does Skynet do with the SAMS?](#what-exactly-does-skynet-do-with-the-sams)\n   * [Are there known bugs?](#are-there-known-bugs)\n   * [How do I know if a SAM site is in range of an EW site or a SAM site in EW mode?](#how-do-i-know-if-a-sam-site-is-in-range-of-an-ew-site-or-a-sam-site-in-ew-mode)\n   * [How do I connect Skynet with the MOOSE AI\\_A2A\\_DISPATCHER and what are the benefits of that?](#how-do-i-connect-skynet-with-the-moose-ai_a2a_dispatcher-and-what-are-the-benefits-of-that)\n * [Thanks](#thanks)\n \n\n# Quick start\nTired of reading already? Download the [demo mission](/demo-missions/skynet-test-persian-gulf.miz) in the persian gulf map and see Skynet in action. More complex demo missions will follow soon.\n\n# Skynet IADS Elements\n![Skynet IADS overview](/images/skynet-overview.jpg)\n\n## IADS\nA Skynet IADS is a complete operational network. You can have multiple Skynet IADS instances per coalition in a DCS mission. A simple setup would be one IADS for the blue side and one IADS for the red side.\n\n## Track files\nSkynet keeps a global track file of all detected targets. It queries all its units with radars and deduplicates contacts. By default lost contacts are stored up to 32 seconds in memory. \n\n## Comand Centers\nYou can add multiple command centers to a Skynet IADS. Once all command centers are destroyed the IADS will go in to autonomous mode.\n\n## SAM Sites\nSkynet can handle multiple SAM sites, it will try and keep emissions to a minimum, therefore by default SAM sites will be turned on only if a target is in range. \nEvery single launcher and radar unit's distance of a SAM site is analysed individually. \nIf at least one launcher and radar is within range, the SAM Site will become active. \nThis allows for a scattered placement of radar and launcher units as in real life.\n\nIf SAM sites or radar guided AAA run out of ammo they will go dark. In the case of a SAM site it will wait with going dark as long as the last fired missile is still in the air.\n\nIf an EW radar or a SAM site acting as EW radar is destoyed surrounding SAM sites can be left withouth EW radar coverage. This can also happen if a SAM site is outside of AWACS coverage.\nSAM sites will go autonomous in such a case meaning they will use their organic radars or just stay dark depending on setup.\nOnce a SAM site is within EW radar coverage again it will be updated by the IADS.\n\n## Early Warning Radars\nSkynet can handle 0-n EW radars. For detection of a target the DCS radar detection logic is used. You can use any type of radar listed in [skynet-iads-supported-types.lua](/skynet-iads-source/skynet-iads-supported-types.lua) in an EW role in Skynet. \nSome modern SAM radars have a greater detection range than older EW radars, e.g. the S-300PS 64H6E (160 km) vs EWR 55G6 (120 km).\n\nYou can also designate SAM sites to act as EW radars, in this case a SAM site will constantly have their radar on. Long range systems like the S-300 are used as EW radars in real life.\nSAM sites that are out of ammo will stay live if they are set to act as EW radars.\n\nNice to know:\nTerrain elevation around an EW radar will create blinds spots, allowing low and fast movers to penetrate radar networks through valleys.\n\n##  Power Sources\nBy default Skynet IADS will run without having to add power sources. You can add multiple power sources to SAM sites, EW radars and command centers.\nOnce a power source is fully damaged the Skynet IADS unit will stop working.\n\nNice to know:\nTaking out the power source of a command center is a real life tactic used in SEAD (Suppression of Enemy Air Defence).\n\n## Connection Nodes\nBy default Skynet IADS will run without having to add connection nodes. You can add multiple connection nodes to SAM sites, EW radars and command centers.\n\nWhen all the unit's connection nodes are fully damaged an EW radar or SAM site will go in to autonomous mode. For a SAM site this means it will behave in its autonomous mode setting. \nIf an EW Radar looses its node it will no longer contribute information to the IADS but otherwise the IADS will still work. Command centers do not have an autonomous mode.\n\nNice to know:\nA single node can be used to connect an arbitrary number of Skynet IADS units. This way you can add a single point of failure in to an IADS.\n\n## AWACS (Airborne Early Warning and Control System)\nAny aircraft with an air to air radar can be added as AWACS. Contacts detected will be added to the IADS. The AWACS will also detect ground units like ships.\nThese will however not be passed to the SAM sites.\n\nYou can add a connection node for the AWACS like an antenna, if it is destroyed, the AWACS will no longer be able to contribute contacts to the IADS.\nTechnically you can also add a power source. In this context it would represent the power source for the connection node, since an aircraft provides its own power.\n\n## Ships\nShips will contribute to the IADS the same way AWACS units do. Add them as a regular EW radar. \n\n# Tactics\n\n## HARM defence\nSAM sites and EW radars will shut down their radars if they believe a HARM (High speed anti radiation missile) is heading for them. For this to happen, the IADS will evaluate contacts and determine if they are likely to be HARMs.\nEach SAM site or EW radar has HARM detection chance set. If a HARM is detected by more than one radar, the chance of it being identified as a HARM is increased.  \nSee [skynet-iads-supported-types.lua](/skynet-iads-source/skynet-iads-supported-types.lua) field ```['harm_detection_chance']``` for the probability per radar system.\n\n### HARM detection\nlet's say SAM site A has a 60% HARM detection chance and SAM site B has a 50% HARM detection cance. If a HARM is picked up by both radars the chance the IADS will identify the HARM will be 80%.  \n\nWith the radar cross section updates of HARMs in DCS 2.7 older radars like the ones used in the SA-2 and SA-6 can only identifiy a HARM at very close range usualy less than 10 seconds before impact. These systems will not have a very good HARM defence with Skynet.\n\n![Skynet IADS overview](/images/skynet-harm-detection.jpg)\n\n### HARM flight path analysis\nThe contact needs to be traveling faster than 800 kt and it may not have changed its flight path more than 2 times (eg ```climb-descend```, ```climb``` or ```descend```).This is to minimise false positives, for example a fighter flying very fast.\n\n![Skynet IADS overview](/images/skynet-harm-flightpath.jpg)\n\nThis implementation is closer to real life. SAM sites like the patriot and most likely modern Russian systems calculate the flight path and analyse the radar cross section to determine if a contact heading inbound is a HARM.\n\nIf identified as a HARM the IADS will shut down radars 15 degrees left and right of the HARM's fight path up to a distance of 20 nautical miles in front of the HARM.\nThe IADS will calculate time to impact and shut down radar emitters up to a maximum of 180 seconds after time to impact. \n\n## HARM radar shutdown\nOnce a HARM has been identified by Skynet, radars up to 20 nm ahead and 15 degrees left or right of the HARM will be notified. Depending on their settins radar emitters will shut down or start defending against the HARM.\n\n![Skynet IADS overview](/images/skynet-harm-radar-shutdown.jpg)\n\n## Point defence\nWhen a radar emitter (EW radar or SAM site) is attacked by a HARM there is a chance it may detect the HARM and go dark. If this radar emitter is acting as the sole EW radar in the area, surrounding SAM sites will not be able to go live since they rely on the EW radar for target information. This is an issue if you have SA-15 Tors next to the EW radar for point defence protection. They will stay dark and not engange the HARM.\n\nUse this feature if you don't want the IADS to loose situational awareness just because a HARM is inbound. The radar emitter will shut down, if it believes its point defences won't be able to handle the number of HARMs inbound. As long as there is one point defence launcher and missile per HARM inbound the radar emitter will keep emitting. If the HARMs exeed the number of point defence launchers and missiles the protected asset will shut down. Tests in DCS have shown that this is roughly the saturation point. If the SAM site reling on point defence can engagen HARMs its launchers an missiles will also count to the saturation point.\n\nSee FAQ [Which SAM systems can engage HARMS?](#which-sam-systems-can-engage-harms)\n\n[Point defence setup example](#point-defence-1)\n\n## Electronic Warfare\nA simple form of jamming is part of the Skynet IADS package. It's off by default. The jamming works by setting the ROE state of a SAM Site. \nThe closer the jamming emitter gets to a SAM site the less effective jamming will become (burn through). For the jammer to work it will need LOS (line of sight) to a radar unit. \nOlder SAM sites are more susceptible to jamming. EW radars are currently not jammable.\n\nI recommend you add an AI unit that follows the strike package you're flying in to act as a jammer aircraft. This will give you the most realistic experience. \nThe jammer emitter will toggle the ROE state of a SAM site which affects how the SAM site reacts to all threats near or far.\n\nI presume an aircraft very close to a SAM site beeing jammed by a emitter very far away would most likely be detected.\nSo the farther away you are from the jammer source the more unrealistic your experience will be.\n\nHere is a [list of SAM sites currently supported by the jammer](https://docs.google.com/spreadsheets/d/16rnaU49ZpOczPEsdGJ6nfD0SLPxYLEYKmmo4i2Vfoe0/edit#gid=0) and the jammer's effectiveness on them. \nWhen setting up a jammer you can decide which SAM sites it is able to jam. For example you could design a mission in which the jammer is not able to jam a SA-6 but is able to jam a SA-2. \nThe jammer effectiveness is not based on any real world data I just read about the different types and made my own conclusions.\n\nHere is an old school documentary [showing the Prowler in action](https://www.youtube.com/watch?v=su44ZU7NcQU). They brief to turn on their jamming equipement at 60 nm from the target.\nI suppose that must have been the effective range of 70's jamming tech.\n\n# Using Skynet in the mission editor\nIt's quite simple to setup an IADS have a look at the demo missions in the [/demo-missions/](/demo-missions) folder.\n\n## Placing units\nThis tutorial assumes you are familiar on how to set up a SAM site in DCS. If not I suggest you watch [this video](https://www.youtube.com/watch?v=YZPh-JNf6Ww) by the Grim Reapers.\nPlace the IADS elements you wish to add on the map.\n\n![Mission Editor IADS Setup](/images/iads-setup.png)  \n\n## Preparing a SAM site\nThere may be only be **one type of SAM site per group**. More than one type of SAM site per group will result in Skynet no being able to properly controll the group. Also please refrain from from adding units to the SAM group that are not required for the SAM like trucks, tanks and soldiers.\nThe skill level you set on a SAM group is retained by Skynet. Make sure you name the **SAM site group** in a consistent manner with a prefix e.g. 'SAM-SA-2'.\n\n![Mission Editor add SAM site](/images/add-sam-site.png)  \n\n## Preparing an EW radar\nYou can use any type of radar as an EW radar. Make sure you **name the unit** in a consistent manner with a prefix, e.g. 'EW-center3'. Make sure you have only **one EW radar in a group** otherwise Skynet will not be able to control single EW radars.\n\n![Mission Editor EW radar](/images/ew-setup.png)  \n\n## Adding the Skynet code\nSkynet requires MIST. A version is provided in this repository or you can download the most current version [here](https://github.com/mrSkortch/MissionScriptingTools).\nMake sure you load MIST and the compiled skynet code in to a mission. The [skynet-iads-compiled.lua](/demo-missions/skynet-iads-compiled.lua) and [mist_4_5_107.lua](/demo-missions/mist_4_5_107.lua) files are located in the [/demo-missions/](/demo-missions) folder. \n\nI recommend you create a text file e.g. 'my-iads-setup.lua' and then add the code needed to get the IADS runing. When updating the setup remember to reload the file in the mission editor. Otherwise changes will not become effective.\nYou can also add the code directly in the mission editor, however that input field is quite small if you write more than a few lines of code.\n\n![Mission Editor IADS Setup](/images/load-scripts.png)  \n\n## Adding the Skynet IADS\nFor the IADS to work you need four lines of code.\n\ncreate an instance of the IADS, the name string is optional and will be displayed in status output:\n```lua\nredIADS = SkynetIADS:create('name')\n``` \n\n\nGive all SAM groups you want to add a common prefix in the mission editor eg: 'SAM-SA-10 west', then add this line of code:  \n```lua\nredIADS:addSAMSitesByPrefix('SAM')\n``` \n\n\nSame for the EW radars, name all units with a common prefix in the mission editor eg: 'EW-radar-south':  \n```lua\nredIADS:addEarlyWarningRadarsByPrefix('EW')\n``` \n\n\nActivate the IADS:  \n```lua\nredIADS:activate()\n```\n\n# Advanced setup\nThis is the danger zone. Call Kenny Loggins. Some experience with scripting is recommended.\nYou can handcraft your IADS with the following functions. If you refrence units that don't exist a message will be displayed when the mission loads.\nThe following examples use static objects for command centers, connection nodes and power sources, you can also use units instead.\n\n## IADS configuration\nCall this method to add or remove a radio menu to toggle the status output of the IADS. By default the radio menu option is not visible:\n```lua\nredIADS:addRadioMenu()  \n```\n```lua\nredIADS:removeRadioMenu()\n```\n\nIf you dereference the IADS remember to call ```deactivate()``` otherwise background tasks of the IADS will continue running, resulting in unexpected behaviour:\n```lua\nredIADS:deactivate()\n```\n\nSet the update interval in seconds of the IADS. This determines in what interval the IADS wil turn SAM sites of or on according to targets it has detected:\n```lua\nredIADS:setUpdateInterval(5)\n```\n\n## Adding a command center\nThe command center represents the place where information is collected and analysed. It if is destroyed the IADS disintegrates.\n\nAdd a command center like this:\n```lua\nlocal commandCenter = StaticObject.getByName(\"Command Center\")\nredIADS:addCommandCenter(commandCenter)\n```\n\n## Power sources and connection nodes\nYou can use units or static objects. Call the function multiple times to add more than one power source or connection node:\n\n```unit``` refers to a SAM site, or EW Radar you retrieved from the IADS, see [setting an option for Radar units](#setting-an-option).\n```lua\nlocal powerSource = StaticObject.getByName(\"EW Power Source\")  \nunit:addPowerSource(powerSource)\n```\n\n```lua\nlocal connectionNode = Unit.getByName(\"EW connection node\") \nunit:addConnectionNode(connectionNode)\n```\n\nFor command centers use:\n```lua\nlocal commandCenter = StaticObject.getByName(\"Command Center2\")\nlocal comPowerSource = StaticObject.getByName(\"Command Center2 Power Source\")\nredIADS:addCommandCenter(commandCenter):addPowerSource(comPowerSource)\n```\n\n## Warm up the SAM sites of an IADS\nThis function is deprecated and will be removed in a future release.\n\n```lua\nredIADS:setupSAMSitesAndThenActivate()\n```\n\n\n## Connecting Skynet to the MOOSE AI_A2A_DISPATCHER\nYou can connect Skynet with MOOSE's [AI_A2A_DISPATCHER](https://flightcontrol-master.github.io/MOOSE_DOCS/Documentation/AI.AI_A2A_Dispatcher.html). This allows the IADS not only to direct SAM sites but also to scramble fighters.\nSkynet will set the radars it can use on the SET_GROUP object of a dispatcher. Meaning that if a radar is lost in Skynet it will no longer be availabe to detect and scramble interceptors.\n\nAdd the object of type SET_GROUP to the iads like this (in this example ```DectionSetGroup```):\n```lua\nredIADS:addMooseSetGroup(DetectionSetGroup)\n```\n\n## SAM site configuration\n\n### Adding SAM sites\n\n#### Add multiple SAM sites\nAdds SAM sites with prefix in group name to the IADS. Previously added SAM sites are cleared:\n```lua\nredIADS:addSAMSitesByPrefix('SAM')\n```\n\n#### Add a SAM site manually\nYou can manually add a SAM site, must be a valid group name:\n```lua\nredIADS:addSAMSite('SA-6 Group2')\n```\n\n### Accessing SAM sites in the IADS\nThe following functions exist to access SAM sites added to the IADS. They all support daisy chaining options:\n\nReturns all SAM sites with the corresponding Nato name, see [skynet-iads-supported-types.lua](/skynet-iads-source/skynet-iads-supported-types.lua). For all units beginning with 'SA-': Don't add Nato code names (Guideline, Gainful), just write 'SA-2', 'SA-6':\n```lua\nredIADS:getSAMSitesByNatoName('SA-6')\n```\n\nReturns all SAM sites in the IADS:\n```lua\nredIADS:getSAMSites()\n```\n\nReturns a SAM site with the specified group name:\n```lua\nredIADS:getSAMSiteByGroupName('SAM-SA-6')\n```\n\nReturns a SAM site with the specified group name prefix. Let's say you have a bunch of SAM sites that all will share the same power source. \nGive these sites a special prefix in the group name, e.g.: ```'SAM-SECTOR-A'```. Once you have added the SAM sites you can access them via the prefix to set whatever options you want:\n\n```lua\nredIADS:getSAMSitesByPrefix('SAM-SECTOR-A')\n```\n\n### Act as EW radar\nWill set the SAM site to act as an EW radar. This will result in the SAM site always having its radar on. Contacts the SAM site sees are reported to the IADS. This option is recomended for long range systems like the S-300: \n```lua\nsamSite:setActAsEW(true)\n```\n\n### Engagement zone\nSet the distance at which a SAM site will switch on its radar:\n```lua\nsamSite:setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE)\n```\n\n#### Engagement zone options  \n\nSAM site will go live when target is within the red circle in the mission editor (default Skynet behaviour): \n```lua\nSkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE\n```\n\nSAM site will go live when target is within the yelow circle in the mission editor: \n```lua\nSkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE\n```\n\nThis option sets the range in relation to the zone you set in ``setEngagementZone`` for a SAM site to go live. Be careful not to set the value too low. Some SAM sites need up to 30 seconds until they can fire. \nDuring this time a target might have already left the engagement zone of SAM site. This option is intended for long range systems like the S-300. You can also set the range above 100 this will have the effect that the SAM site goes live earlier:\n\n```lua\nsamSite:setGoLiveRangeInPercent(90)\n```\n\n### Engage air weapons\nWill set the SAM site to engage air weapons, if it is able to do so in DCS. It is a wrapper for the [ENGAGE_AIR_WEAPONS](https://wiki.hoggitworld.com/view/DCS_option_engage_air_weapons) setting.\n\n```lua\nsamSite:setCanEngageAirWeapons(true)\n```\n\n### Engage HARM\nWill set the SAM site to engage HARMs, if it is able to do so in DCS. If set to false the SAM site will shut down if a HARM that has been identified by the IADS is inbound. SAM sites that can engage HARMS are set to true by default.\n\n```lua\nsamSite:setCanEngageHARM(true)\n```\n\n## Add go live constraints\nYou can include constraints wich must be satisfied for the SAM site to go live. Please note this only controls activation of the SAM site. \nThere is currently no way to tell a SAM site to only target a certain contact via the lua scripting engine in DCS. \n\nThe constraint must evaluate to true and the contact must be in range of the SAM site (handled by Skynet). \n\n### Use cases\nPlace a SAM site on an flight path that you suspect strike strike fighters will pass. Add a heading constraint to ensure that the SAM site will only go ive when fighters are on their way back from the target.  \n\nSet a SAM site to only go live if aircraft are in a certain altitude band.\n\nSAM site shall only go live once a strike package has destroyed a certain building or unit.  \n\nYou do not have to use the contact provided in the function to evaluate the constraint. You can make any assertion you want.\n\nCreate a function that will evaluate if the constraint is satisfied. The function will have access to the [contact](#contact) the SAM site is evaluating:\n```lua\n--SAM site will only go live if the contact is below 1000 feet.\nlocal function goLiveConstraint(contact)\n\treturn ( contact:getHeightInFeetMSL() < 1000 )\nend\n```\n\nAdd the function to the SAM site and give it a name. You can add as many constraints as you wish:\n```lua\nself.samSite:addGoLiveConstraint('ignore-low-flying-contacts', goLiveConstraint)\n```\n\nRemove constraint you no longer wish to use:\n```lua\nself.samSite:removeGoLiveConstraint('ignore-low-flying-contacts')\n```\n\nGet a table of all constraints:\n```lua\nself.samSite:getGoLiveConstraints()\n```\n\n## Contact\nYou can use the following methods to get information about a contact.\n\nWill return true if contact has been identified as a HARM by Skynet:\n```lua\ncontact:isIdentifiedAsHARM()\n```  \n\nWill return the height of a contact:\n```lua\ncontact:getHeightInFeetMSL()\n```  \n\nWill return the current magnetic heading of a contact. Note the heading is availble only after a contact has been tracked in more than one cycle by the IADS. Until that has happened heading will be 0:\n```lua\ncontact:getMagneticHeading()\n```  \n\nWill return the current ground speed of a contact. Note the speed is availble only after a contact has been tracked in more than one cycle by the IADS. Until that has happend speed will be 0:\n```lua\ncontact:getMagneticHeading()\n```  \n\nWill return the time in seconds a contact has been known to the IADS:\n```lua\ncontact:getAge()\n```  \n\nWill return the type as a ```Object.Category```:\n```lua\ncontact:getTypeName()\n```  \n\nWill return the unit name:\n```lua\ncontact:getName()\n```  \n\n\n## EW radar configuration\n\n### Adding EW radars\n\n#### Add multiple EW radars\nAdds EW radars with prefix in unit name to the IADS. Previously added EW sites are cleared:\n```lua\nredIADS:addEarlyWarningRadarsByPrefix('EW')\n``` \n\n#### Add an EW radar manually\nYou can add EW radars manually, must be a valid unit name: \n```lua\nredIADS:addEarlyWarningRadar('EWR West')\n```\n\n### Accessing EW radars in the IADS\nThe following functions exist to access EW radars added to the IADS. They all support daisy chaining options. \n\n\nReturns all EW radars in the IADS:\n```lua\nredIADS:getEarlyWarningRadars()\n```\n\nReturns the EW radar with the specified unit name:\n```lua\nredIADS:getEarlyWarningRadarByUnitName('EW-west')\n```\n\n## Options for SAM sites and EW radars\n\n### Setting an option\nIn the following examples ```ewRadarOrSamSite``` refers to an single EW radar or SAM site or a table of EW radars and SAM sites you got from the Skynet IADS, by calling one of the functions named in [accessing EW radars](#accessing-ew-radars-in-the-iads) or [accessing SAM sites](#accessing-sam-sites-in-the-iads).\n\n### Daisy chaining options\n You can daisy chain options on a single SAM site / EW Radar or a table of SAM sites / EW radars like this:\n ```lua\n redIADS:getSAMSites():setActAsEW(true):addPowerSource(powerSource):addConnectionNode(connectionNode):setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE):setGoLiveRangeInPercent(90):setAutonomousBehaviour(SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK)\n ```  \n\n### HARM Defence\nYou can set the reaction probability (between 0 and 100 percent). See [skynet-iads-supported-types.lua](/skynet-iads-source/skynet-iads-supported-types.lua) field ```['harm_detection_chance']``` for default detection probabilities:\n```lua\newRadarOrSamSite:setHARMDetectionChance(50)\n```\n\n### Point defence\nYou must use a point defence SAM that can engage HARM missiles. Can be used to protect SAM sites or EW radars. See [point defence](#point-defence) for information what this does:\n\nIf you want the point defences to coordinate their HARM defence then you can add multiple point defence SAM sites in to one group. **This is the only place where you should add multiple SAM sites in to one group in Skynet**.\nLet's assume you have two SA-15 units defending a radar. If the SA-15 units are in separate groups they will both fire at the same HARM inbound. However if they are in the same group and multiple HARMS are inbound they will each pick a separate HARM to engage.\n\n```lua\n--first get the SAM site you want to use as point defence from the IADS:\nlocal sa15 = redIADS:getSAMSiteByGroupName('SAM-SA-15')\n--then add it to the SAM site it should protect:\nredIADS:getSAMSiteByGroupName('SAM-SA-10'):addPointDefence(sa15)\n```\n\nThis function is deprecated and will be removed in a future release.\n```lua\newRadarOrSamSite:setIgnoreHARMSWhilePointDefencesHaveAmmo(true)\n```\n\n### Autonomous mode behaviour\nSet how the SAM site or EW radar will behave if it looses connection to the IADS:\n```lua\newRadarOrSamSite:setAutonomousBehaviour(SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK)\n```\n\n#### Autonomous mode options \nSAM site or EW radar will behave in the default DCS AI. Alarm State will be red and ROE weapons free (default Skynet behaviour for SAM sites):\n```lua\nSkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DCS_AI\n```\n\nSAM Site or EW radar will go dark if it looses connection to IADS (default behaviour for EW radars):\n```lua\nSkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK\n```\n\n## Adding a jammer\nThe jammer is quite easy to set up. You need a unit that acts as a jammer source, preferably it will be an aircraft in the strike package.\nOnce the jammer detects an emitter it starts jamming the radar. Set the [coresponding debug variable jammerProbability](#setting-debug-information) to see what the jammer is doing.\nCheck [skynet-iads-jammer.lua](/skynet-iads-source/skynet-iads-jammer.lua) to see which SAM sites are supported.\n\nRemember to set the AI aircraft acting as jammer in the Mission editor to ```Reaction to Threat = EVADE FIRE``` otherwise the AI will try and actively attack the SAM site.\nThis way it will stick to the preset flight plan.\n\nCreate a jammer and assign it to an unit. Also make sure you add the IADS you wan't the jammer to work for:\n```lua\nlocal jammerSource = Unit.getByName(\"F-4 AI\")\njammer = SkynetIADSJammer:create(jammerSource, iads)\n```\n\nThe jammer will start listening for emitters and if it finds one of the emitters it is able to jam it will start jamming it:\n```lua\njammer:masterArmOn()\n```\n\nWill disable jamming for the specified SAM type, pass the Nato name:\n```lua\njammer:disableFor('SA-2')\n```\n\nWill turn off the jammer. Make sure you call this function before you dereference a jammer in the code, otherwise a background task will keep on jamming:\n```lua\njammer:masterArmSafe()\n```\n\nWill add jammer on / off to the radio menu:\n```lua\njammer:addRadioMenu()\n```\n\nWill remove jammer on / off from the radio menu:\n```lua\njammer:removeRadioMenu()\n```\n\n### Advanced functions\n\nAdd a second IADS the jammer should be able to jam, for example if you have two separate IADS running:\n```lua\njammer:addIADS(iads2)\n```\n\nAdd a new jammer function:\n\n```lua\n-- write a lambda function that expects one parameter:\n-- given public available data on jammers their effeciveness drastically decreases the closer you get, so a non-linear function would make sense:\nlocal function f(distanceNM)\n\treturn ( 1.4 ^ distanceNM ) + 80\nend\n\n-- add the function: specify which SAM type it should apply for:\nself.jammer:addFunction('SA-10', f)\n```\n\nSet the maximum range the jammer will work, the default value is set to 200 nautical miles:\n```lua\njammer:setMaximumEffectiveDistance(100)\n```\n\n## Setting debug information\nWhen developing a mission I suggest you add debug output to check how the IADS reacts to threats. Debug output may slow down DCS, so it's recommended to turn it off in a live environment:\n\nAccess the debug settings:\n```lua\nlocal iadsDebug = redIADS:getDebugSettings()  \n```\n\nOutput in game:\n```lua\niadsDebug.IADSStatus = true\niadsDebug.contacts = true\niadsDebug.jammerProbability = true\n```\n\nOutput to dcs.log:\n```lua\niadsDebug.addedEWRadar = true\niadsDebug.addedSAMSite = true\niadsDebug.warnings = true\niadsDebug.radarWentLive = true\niadsDebug.radarWentDark = true\niadsDebug.harmDefence = true\n```\n\nThese three options will output detailed information on every radar in the IADS to the dcs.log file. Enabling these may have an impact on performance:\n```lua\niadsDebug.samSiteStatusEnvOutput = true\niadsDebug.earlyWarningRadarStatusEnvOutput = true\niadsDebug.commandCenterStatusEnvOutput = true\n```\n![Mission Editor IADS Setup](/images/skynet-debug.png)  \n\n# Example Setup\nThis is an example of how you can set up your IADS used in the [demo mission](/demo-missions/skynet-test-persian-gulf.miz):\n```lua\ndo\n\n--create an instance of the IADS\nredIADS = SkynetIADS:create('RED IADS')\n\n---debug settings remove from here on if you do not wan't any output on what the IADS is doing by default\nlocal iadsDebug = redIADS:getDebugSettings()\niadsDebug.IADSStatus = true\niadsDebug.radarWentDark = true\niadsDebug.contacts = true\niadsDebug.radarWentLive = true\niadsDebug.noWorkingCommmandCenter = true\niadsDebug.samNoConnection = true\niadsDebug.jammerProbability = true\niadsDebug.addedEWRadar = true\niadsDebug.harmDefence = true\n---end remove debug ---\n\n--add all units with unit name beginning with 'EW' to the IADS:\nredIADS:addEarlyWarningRadarsByPrefix('EW')\n\n--add all groups begining with group name 'SAM' to the IADS:\nredIADS:addSAMSitesByPrefix('SAM')\n\n--add a command center:\ncommandCenter = StaticObject.getByName('Command-Center')\nredIADS:addCommandCenter(commandCenter)\n\n---we add a K-50 AWACs, manually. This could just as well be automated by adding an 'EW' prefix to the unit name:\nredIADS:addEarlyWarningRadar('AWACS-K-50')\n\n--add a power source and a connection node for this EW radar:\nlocal powerSource = StaticObject.getByName('Power-Source-EW-Center3')\nlocal connectionNodeEW = StaticObject.getByName('Connection-Node-EW-Center3')\nredIADS:getEarlyWarningRadarByUnitName('EW-Center3'):addPowerSource(powerSource):addConnectionNode(connectionNodeEW)\n\n--add a connection node to this SA-2 site, and set the option for it to go dark, if it looses connection to the IADS:\nlocal connectionNode = Unit.getByName('Mobile-Command-Post-SAM-SA-2')\nredIADS:getSAMSiteByGroupName('SAM-SA-2'):addConnectionNode(connectionNode):setAutonomousBehaviour(SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK)\n\n--this SA-2 site will go live at 70% of its max search range:\nredIADS:getSAMSiteByGroupName('SAM-SA-2'):setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE):setGoLiveRangeInPercent(70)\n\n--all SA-10 sites shall act as EW sites, meaning their radars will be on all the time:\nredIADS:getSAMSitesByNatoName('SA-10'):setActAsEW(true)\n\n--set the SA-15's as point defence for the SA-10 site. We set the SA-10 to always identify HARMs so we can demonstrate the point defence mechanism in Skynet.\n--the SA-10 will stay online when shot at by HARMS as long as the point defences and SAM site have ammo and the saturation point is not reached.\nlocal sa15 = redIADS:getSAMSiteByGroupName('SAM-SA-15-point-defence-SA-10')\nredIADS:getSAMSiteByGroupName('SAM-SA-10'):addPointDefence(sa15):setHARMDetectionChance(100)\n\n--set this SA-11 site to go live 70% of max range of its missiles (default value: 100%), its HARM detection probability is set to 50% (default value: 70%)\nredIADS:getSAMSiteByGroupName('SAM-SA-11'):setGoLiveRangeInPercent(70):setHARMDetectionChance(50)\n\n--this SA-6 site will always react to a HARM being fired at it:\nredIADS:getSAMSiteByGroupName('SAM-SA-6'):setHARMDetectionChance(100)\n\n--set this SA-11 site to go live at maximunm search range (default is at maximung firing range):\nredIADS:getSAMSiteByGroupName('SAM-SA-11-2'):setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE)\n\n--activate the radio menu to toggle IADS Status output\nredIADS:addRadioMenu()\n\n--activate the IADS\nredIADS:activate()\t\n\n--add the jammer\nlocal jammer = SkynetIADSJammer:create(Unit.getByName('jammer-emitter'), redIADS)\njammer:masterArmOn()\n\n--setup blue IADS:\nblueIADS = SkynetIADS:create('BLUE IADS')\nblueIADS:addSAMSitesByPrefix('BLUE-SAM')\nblueIADS:addEarlyWarningRadarsByPrefix('BLUE-EW')\nblueIADS:activate()\nblueIADS:addRadioMenu()\n\nlocal iadsDebug = blueIADS:getDebugSettings()\niadsDebug.IADSStatus = true\niadsDebug.contacts = true\n\nend\n```\n\n# FAQ\n\n## Does Skynet IADS have an impact on game performance?\nSkynet may actually improve game performance when using a lot of SAM AI units. This is because Skynet will turn off radar emissions of all SAM groups currently not in range of a target. By default these SAM groups would otherwise have their radars on. Skynet caches target information for a few seconds to reduce expensive calls on DCS radar detection.\n\n## What air defence units shall I add to the Skynet IADS?\nIn theory you can add all the types that are listed in the [skynet-iads-supported-types.lua](skynet-iads-source/skynet-iads-supported-types.lua) file. \nVery short range units (like the Shilka AAA, Rapier) won't really benefit from the IADS apart from reacting to HARMs. These are better just placed in a mission and handeled by the default AI of DCS.\nThis is due to the short range of their radars. By the time the IADS wakes them up, the contact has likely passed their engagement range.\nThe strength of the Skynet IADS lies with handling long range systems that operate by radar.\n\n## Which SAM systems can engage HARMS?\nAs of July 2022 I have only been able to get the SA-15, SA-10, NASAMS and Patriot to engage HARMS. The best option for a solid HARM defence is to add SA-15's around EW radars or high value SAM sites.\n\n## What exactly does Skynet do with the SAMS?\nVia the scripting engine one can toggle the radar emitters on and off. Further options are the alarm state and the rules of engagement. In a nutshell that's all that Skynet does. Skynet does also read the radar and firing range properties of a SAM site. Based on that data and the setup options a mission designer provides Skynet will turn a SAM site on or off. \n\nNo god like intervention is used (like magically exploding HARMS via the scripting engine).\nIf a SAM site or EW radar detects an inbound HARM it just turns off its radar as in real life. The HARM as it is programmed in DCS will try and glide in to the last known position mostly resulting in misses by 50-100 meters.\n\n## Are there known bugs?\nYes, when placing multi unit SAM sites (e.g. SA-3, Patriot..) make sure the first unit you place is the search radar. If you add any other element as the first unit, Skynet will not be able to read radar data.\nThe result will be that the SAM site won't go live. This bug was observed in DCS 2.5.5. The SAM site will work fine when used as a standalone unit outside of Skynet.\n\n## How do I know if a SAM site is in range of an EW site or a SAM site in EW mode?\nTo get a rough idea you can look at the range circles in the mission editor. However these ranges are greater than the actual in game detection ranges of an EW radar or SAM site.\nThe following screenshot shows the range of the 1L13 EWR. The mission editor shows a range of 64 NM (nautical miles) where as the in game range is 43 NM.\n\nIn this example the SAM site to the north east would not be in range of the EW radar, therefore it would go in to autonomous mode once the mission starts. \n\n\n![1L13 EWR range differences](/images/ew-detection-distance-example.png)  \n\nSet the debug options ```samSiteStatusEnvOutput``` and ```earlyWarningRadarStatusEnvOutput``` to get detailed information on every SAM site and EW radar.\nThe text marked in the red box will show you which SAM sites are in the covered area of a SAM site or EW radar.\n\n\n![SAM sites in covered area](/images/radar-emitter-status-dcs-log.png) \n\n## How do I connect Skynet with the MOOSE AI_A2A_DISPATCHER and what are the benefits of that?\nIRL an IADS would most likely not only handle SAM sites but also pass information to interceptor aircraft. By connecting Skynet to the [AI_A2A_DISPATCHER](https://flightcontrol-master.github.io/MOOSE_DOCS/Documentation/AI.AI_A2A_Dispatcher.html) by MOOSE you are able\nto add interceptors to the IADS. See [Skynet Moose AI_A2A_DISPATCHER](#connecting-skynet-to-the-moose-ai_a2a_dispatcher) and the [moose_a2a_connector demo mission](demo-missions/moose_a2a_connector) for more information.\n\nAn example setup of Skynet and the [AI_A2A_DISPATCHER](https://flightcontrol-master.github.io/MOOSE_DOCS/Documentation/AI.AI_A2A_Dispatcher.html) :\n```lua\n\n--Setup Syknet IADS:\nredIADS = SkynetIADS:create('Enemy IADS')\nredIADS:addSAMSitesByPrefix('SAM')\nredIADS:addEarlyWarningRadarsByPrefix('EW')\nredIADS:activate()\n\n-- START MOOSE CODE:\n-- Define a SET_GROUP object that builds a collection of groups that define the EWR network.\nDetectionSetGroup = SET_GROUP:New()\n\n-- Setup the detection and group targets to a 30km range!\nDetection = DETECTION_AREAS:New( DetectionSetGroup, 30000 )\n\n-- Setup the A2A dispatcher, and initialize it.\nA2ADispatcher = AI_A2A_DISPATCHER:New( Detection )\n\n-- Set 100km as the radius to engage any target by airborne friendlies.\nA2ADispatcher:SetEngageRadius() -- 100000 is the default value.\n\n-- Set 200km as the radius to ground control intercept.\nA2ADispatcher:SetGciRadius() -- 200000 is the default value.\n\nCCCPBorderZone = ZONE_POLYGON:New( \"RED-BORDER\", GROUP:FindByName( \"RED-BORDER\" ) )\nA2ADispatcher:SetBorderZone( CCCPBorderZone )\nA2ADispatcher:SetSquadron( \"Kutaisi\", AIRBASE.Caucasus.Kutaisi, { \"Squadron red SU-27\" }, 2 )\nA2ADispatcher:SetSquadronGrouping( \"Kutaisi\", 2 )\nA2ADispatcher:SetSquadronGci( \"Kutaisi\", 900, 1200 )\nA2ADispatcher:SetTacticalDisplay(true)\nA2ADispatcher:Start()\n--END MOOSE CODE\n\n-- add the MOOSE SET_GROUP to the IADS, from now on Skynet will update active radars that the MOOSE SET_GROUP can use for EW detection.\nredIADS:addMooseSetGroup(DetectionSetGroup)\n```\n\n# Thanks\nSpecial thaks to Spearzone and Coranthia for researching public available information on IADS networks and getting me up to speed on how such a system works.\nI based the SAM site setup on [Grimes SAM DB](https://forums.eagle.ru/showthread.php?t=118175) from his IADS script, however I removed range data since Skynet loads that from DCS.\n\n\n"
  },
  {
    "path": "build-tools/build-compiled-script.ps1",
    "content": "$version=$args[0]\nif ($version -eq $null){\n\techo \"No Version supplied, not bulding script\"\n\treturn\n}\nif (Test-Path ./tmp/){\n\tRemove-Item ./tmp/\n}\nNew-Item ./tmp/ -ItemType Directory\nif (Test-Path ./tmp/skynet-iads-compiled.lua) {\n\tRemove-Item ./tmp/skynet-iads-compiled.lua\n}\nAdd-Content ./tmp/tmp-time.lua (\"env.info(`\"--- SKYNET VERSION: \"+$version+\" | BUILD TIME: \"+(Get-Date -date (Get-Date).ToUniversalTime()-uformat \"%d.%m.%Y %H%MZ\")+\" ---`\")\")  \ncat ../skynet-iads-source/skynet-iads-supported-types.lua, ../skynet-iads-source/highdigitsams/skynet-iads-high-digit-sams-suported-types.lua, ../skynet-iads-source/skynet-iads-logger.lua, ../skynet-iads-source/skynet-iads.lua, ../skynet-iads-source/skynet-mooose-a2a-dispatcher-connector.lua, ../skynet-iads-source/skynet-iads-table-delegator.lua, ../skynet-iads-source/skynet-iads-abstract-dcs-object-wrapper.lua, ../skynet-iads-source/skynet-iads-abstract-element.lua, ../skynet-iads-source/skynet-iads-abstract-radar-element.lua, ../skynet-iads-source/skynet-iads-awacs-radar.lua, ../skynet-iads-source/skynet-iads-command-center.lua, ../skynet-iads-source/skynet-iads-contact.lua, ../skynet-iads-source/skynet-iads-early-warning-radar.lua, ../skynet-iads-source/skynet-iads-jammer.lua, ../skynet-iads-source/skynet-iads-sam-search-radar.lua, ../skynet-iads-source/skynet-iads-sam-site.lua, ../skynet-iads-source/skynet-iads-sam-tracking-radar.lua, ../skynet-iads-source/syknet-iads-sam-launcher.lua, ../skynet-iads-source/skynet-iads-harm-detection.lua | sc ./tmp/tmp-code.lua \n$code = Get-Content ./tmp/tmp-code.lua\nAdd-Content ./tmp/tmp-time.lua $code\nRename-Item -Path ./tmp/tmp-time.lua -NewName skynet-iads-compiled.lua\nRemove-Item ./tmp/tmp-code.lua\n\nif (Test-Path ../demo-missions/skynet-iads-compiled.lua) {\n\tRemove-Item ../demo-missions/skynet-iads-compiled.lua\n}\n\nMove-Item -Path ./tmp/skynet-iads-compiled.lua ../demo-missions/skynet-iads-compiled.lua\n\n$toc = ./bin/gh-md-toc.exe --hide-footer ../skynet-iads-source/README_source.md\n$toc = $toc -replace \"=================\", \"=================`n\"\n$toc = $toc -replace \"Table of Contents\", \"Table of Contents`n\"\n$toc = $toc -replace \"\\)\", \"`)`n\"\n$readme = Get-Content ../skynet-iads-source/README_source.md\n$readmeWithTOC = $readme -replace \"{TOC_PLACEHOLDER}\", $toc\n\nif (Test-Path ../README.md) {\n\tRemove-Item ../README.md\n}\n\nAdd-Content ../README.md $readmeWithTOC\nRemove-Item ./tmp/"
  },
  {
    "path": "contributing.md",
    "content": "This guide is work in progress and will be updated.\n\n# Contributing\nThanks for your interest in contributing to Skynet!\nIf you think you have a good idea on how to improve or enhance Skynet, please propose your idea on the [discord channel](https://discord.gg/ZEyp3g).\nIt's encuraged that you run your idea by the community before you spend time coding. This way you will get feedback on how the feature is perceived by the community \nand you also may get tips on how to best implement an enhancement.\n\n# Versioning\nSkynet uses [semantic versioning](https://semver.org/).\n\n# Required software\nYou will need a working copy of DCS [([Digital Combat Simulator)](https://www.digitalcombatsimulator.com/en/) to contribute to Skynet development.\n\n# Test first design philosophy\nSkynet is developed with the [test first philosophy](https://resources.collab.net/agile-101/test-first-programming). Once you get the hang of it test first development is really great.\nIt may take a bit longer to develop a new feature but you will save a lot of time not having to test existing code after a small change. Writing unit tests also makes the code more modular and therefore understandable.\n\n## Writing a unit test\nHave a look at the existing unit tests to get an idea on how to write one yourself. Unit tests shall be added to the skynet-unit-tests.miz file.\nCheck the output of the dcs.log file for information whether the tests have passed or not. Please don't create a pull request with tests failing.\n\n\n# setting up your editor\nI recomend you use [notepad++](https://notepad-plus-plus.org/downloads/) to edit lua files.\nThis [Post](https://community.notepad-plus-plus.org/topic/15662/help-function-list-doesn-t-support-my-language/12) has some information on how you can add a function list for LUA in notepad++\n\n# Build workflow\nAll the .miz files in this respository load the skynet-iads-compiled.lua file including the unit test mission. To create a new build run build-compiled-script.ps1 in the Power Shell on Windows. After that you can load the skynet-iads-compiled.lua in to the .miz file of your choosing. Rund build-compiled-script.ps1 1.0.0 to add the version number in the script.\n\n## Editing the readme file\nThe README.md in the root of this repository is autogenerated. Don't add new documentation in this file. Instead edit README_source.md. When running build-compiled-script.ps1 the README.md is updated with a table of contents.  "
  },
  {
    "path": "demo-missions/mist_4_5_107.lua",
    "content": "--[[--\nMIST Mission Scripting Tools.\n## Description:\nMIssion Scripting Tools (MIST) is a collection of Lua functions\nand databases that is intended to be a supplement to the standard\nLua functions included in the simulator scripting engine.\n\nMIST functions and databases provide ready-made solutions to many common\nscripting tasks and challenges, enabling easier scripting and saving\nmission scripters time. The table mist.flagFuncs contains a set of\nLua functions (that are similar to Slmod functions) that do not\nrequire detailed Lua knowledge to use.\n\nHowever, the majority of MIST does require knowledge of the Lua language,\nand, if you are going to utilize these components of MIST, it is necessary\nthat you read the Simulator Scripting Engine guide on the official ED wiki.\n\n## Links:\n\nED Forum Thread: <http://forums.eagle.ru/showthread.php?t=98616>\n\n##Github:\n\nDevelopment <https://github.com/mrSkortch/MissionScriptingTools>\n\nOfficial Releases <https://github.com/mrSkortch/MissionScriptingTools/tree/master>\n\n@script MIST\n@author Speed\n@author Grimes\n@author lukrop\n]]\nmist = {}\n\n-- don't change these\nmist.majorVersion = 4\nmist.minorVersion = 5\nmist.build = 107\n\n-- forward declaration of log shorthand\nlocal log\nlocal dbLog\n    \nlocal mistSettings = {\n\terrorPopup = false, -- errors printed by mist logger will create popup warning you\n\twarnPopup = false,\n\tinfoPopup = false,\n\tlogLevel = 'warn',\n    dbLog = 'warn',\n}\n\ndo -- the main scope\n\tlocal coroutines = {}\n\n\tlocal tempSpawnedUnits = {} -- birth events added here\n\tlocal tempSpawnedGroups = {}\n\tlocal tempSpawnGroupsCounter = 0\n\t\n\tlocal mistAddedObjects = {} -- mist.dynAdd unit data added here\n\tlocal mistAddedGroups = {} -- mist.dynAdd groupdata added here\n\tlocal writeGroups = {}\n\tlocal lastUpdateTime = 0\n\n\tlocal updateAliveUnitsCounter = 0\n\tlocal updateTenthSecond = 0\n\t\n\tlocal mistGpId = 7000\n\tlocal mistUnitId = 7000\n\tlocal mistDynAddIndex = {[' air '] = 0, [' hel '] = 0, [' gnd '] = 0, [' bld '] = 0, [' static '] = 0, [' shp '] = 0}\n\n\tlocal scheduledTasks = {}\n\tlocal taskId = 0\n\tlocal idNum = 0\n\n\tmist.nextGroupId = 1\n\tmist.nextUnitId = 1\n\n\n\t\n\tlocal function initDBs() -- mist.DBs scope\n\t\tmist.DBs = {}\n        mist.DBs.markList = {}\n\t\tmist.DBs.missionData = {}\n\t\tif env.mission then\n\n\t\t\tmist.DBs.missionData.startTime = env.mission.start_time\n\t\t\tmist.DBs.missionData.theatre = env.mission.theatre\n\t\t\tmist.DBs.missionData.version = env.mission.version\n\t\t\tmist.DBs.missionData.files = {}\n\t\t\tif type(env.mission.resourceCounter) == 'table' then\n\t\t\t\tfor fIndex, fData in pairs (env.mission.resourceCounter) do\n\t\t\t\t\tmist.DBs.missionData.files[#mist.DBs.missionData.files + 1] =\tmist.utils.deepCopy(fIndex)\n\t\t\t\tend\n\t\t\tend\n\t\t\t-- if we add more coalition specific data then bullsye should be categorized by coaliton. For now its just the bullseye table\n            mist.DBs.missionData.bullseye = {}\n\t\tend\n\n\t\tmist.DBs.zonesByName = {}\n\t\tmist.DBs.zonesByNum = {}\n\n\n\t\tif env.mission.triggers and env.mission.triggers.zones then\n\t\t\tfor zone_ind, zone_data in pairs(env.mission.triggers.zones) do\n\t\t\t\tif type(zone_data) == 'table' then\n\t\t\t\t\tlocal zone = mist.utils.deepCopy(zone_data)\n\t\t\t\t\tzone.point = {}\t-- point is used by SSE\n\t\t\t\t\tzone.point.x = zone_data.x\n\t\t\t\t\tzone.point.y = 0\n\t\t\t\t\tzone.point.z = zone_data.y\n                    zone.properties = {}\n                    if zone_data.properties then\n                        for propInd, prop in pairs(zone_data.properties) do\n                            if prop.value and type(prop.value) == 'string' and prop.value ~= \"\" then\n                                zone.properties[prop.key] = prop.value                                \n                            end\n                        end\n                    end\n                    if zone.verticies then -- trust but verify\n                        local r = 0\n                        for i = 1, #zone.verticies do\n                            local dist = mist.utils.get2DDist(zone.point, zone.verticies[i])\n                            if dist > r then\n                                r = mist.utils.deepCopy(dist)\n                            end\n                        end\n                        zone.radius = r\n                    \n                    end\n\n\t\t\t\t\tmist.DBs.zonesByName[zone_data.name] = zone\n\t\t\t\t\tmist.DBs.zonesByNum[#mist.DBs.zonesByNum + 1] = mist.utils.deepCopy(zone)\t--[[deepcopy so that the zone in zones_by_name and the zone in\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tzones_by_num se are different objects.. don't want them linked.]]\n\t\t\t\tend\n\t\t\tend\n\t\tend\n        \n        mist.DBs.drawingByName = {}\n        mist.DBs.drawingIndexed = {}\n        \n        if env.mission.drawings and env.mission.drawings.layers then\n            for i = 1, #env.mission.drawings.layers do\n                local l = env.mission.drawings.layers[i]\n                \n                for j = 1, #l.objects do\n                    local copy = mist.utils.deepCopy(l.objects[j])\n                    --log:warn(copy)\n                    local doOffset = false\n                    copy.layer = l.name\n\n                    local theta = copy.angle or 0\n                    theta = math.rad(theta)\n                    if copy.primitiveType == \"Polygon\" then\n                        \n                        if copy.polygonMode == 'rect' then\n                            local h, w = copy.height, copy.width\n                            copy.points = {}\n                            copy.points[1] = {x = h/2, y = w/2}\n                            copy.points[2] = {x = -h/2, y = w/2}\n                            copy.points[3] = {x = -h/2, y = -w/2}\n                            copy.points[4] = {x = h/2, y = -w/2}\n                            doOffset = true\n                        elseif copy.polygonMode == \"circle\" then\n                            copy.points = {x = copy.mapX, y = copy.mapY}\n                        elseif copy.polygonMode == 'oval' then\n                            copy.points = {}\n                            local numPoints = 24\n                            local angleStep = (math.pi*2)/numPoints\n                            doOffset = true\n                            for v = 1, numPoints do\n                                local pointAngle = v * angleStep\n                                local x = copy.r1 * math.cos(pointAngle) \n                                local y = copy.r2 * math.sin(pointAngle) \n                                \n                                table.insert(copy.points,{x=x,y=y})\n                                \n                            end\n                        elseif copy.polygonMode == \"arrow\" then\n                            doOffset = true\n                         end\n                       \n\n                        if theta ~= 0 and copy.points and doOffset == true then\n                            \n                            --log:warn('offsetting Values')\n                            for p = 1, #copy.points do\n                                local offset = mist.vec.rotateVec2(copy.points[p], theta)\n                                copy.points[p] = offset \n                            end\n                           --log:warn(copy.points[1])\n                        end\n                    \n                    elseif copy.primitiveType == \"Line\" and copy.closed == true then\n                       table.insert(copy.points, mist.utils.deepCopy(copy.points[1]))\n                    end\n                    if copy.points and #copy.points > 1 then\n                        for u = 1, #copy.points do\n                            copy.points[u].x = mist.utils.round(copy.points[u].x + copy.mapX, 2)\n                            copy.points[u].y = mist.utils.round(copy.points[u].y + copy.mapY, 2)\n                        end\n                    \n                    end\n                    if mist.DBs.drawingByName[copy.name] then\n                        log:warn(\"Drawing by the name of [ $1 ] already exists in DB. Failed to add to mist.DBs.drawingByName.\", copy.name)\n                    else\n                    \n                        mist.DBs.drawingByName[copy.name] = copy\n                    end\n                    table.insert(mist.DBs.drawingIndexed, copy)\n                end\n            \n            end\n        \n        end\n        \n\n\t\tmist.DBs.navPoints = {}\n\t\tmist.DBs.units = {}\n\t\t--Build mist.db.units and mist.DBs.navPoints\n\t\tfor coa_name_miz, coa_data in pairs(env.mission.coalition) do\n            local coa_name = coa_name_miz\n            if string.lower(coa_name_miz) == 'neutrals' then\n                coa_name = 'neutral'\n            end\n\t\t\tif type(coa_data) == 'table' then\n\t\t\t\tmist.DBs.units[coa_name] = {}\n                \n                if coa_data.bullseye then \n                    mist.DBs.missionData.bullseye[coa_name] = {}\n                    mist.DBs.missionData.bullseye[coa_name].x = coa_data.bullseye.x\n                    mist.DBs.missionData.bullseye[coa_name].y = coa_data.bullseye.y\n                end\n\t\t\t\t-- build nav points DB\n\t\t\t\tmist.DBs.navPoints[coa_name] = {}\n\t\t\t\tif coa_data.nav_points then --navpoints\n\t\t\t\t\t--mist.debug.writeData (mist.utils.serialize,{'NavPoints',coa_data.nav_points}, 'NavPoints.txt')\n\t\t\t\t\tfor nav_ind, nav_data in pairs(coa_data.nav_points) do\n\n\t\t\t\t\t\tif type(nav_data) == 'table' then\n\t\t\t\t\t\t\tmist.DBs.navPoints[coa_name][nav_ind] = mist.utils.deepCopy(nav_data)\n\n\t\t\t\t\t\t\tmist.DBs.navPoints[coa_name][nav_ind].name = nav_data.callsignStr\t-- name is a little bit more self-explanatory.\n\t\t\t\t\t\t\tmist.DBs.navPoints[coa_name][nav_ind].point = {}\t-- point is used by SSE, support it.\n\t\t\t\t\t\t\tmist.DBs.navPoints[coa_name][nav_ind].point.x = nav_data.x\n\t\t\t\t\t\t\tmist.DBs.navPoints[coa_name][nav_ind].point.y = 0\n\t\t\t\t\t\t\tmist.DBs.navPoints[coa_name][nav_ind].point.z = nav_data.y\n\t\t\t\t\t\tend\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\t\tif coa_data.country then --there is a country table\n\t\t\t\t\tfor cntry_id, cntry_data in pairs(coa_data.country) do\n\n\t\t\t\t\t\tlocal countryName = string.lower(cntry_data.name)\n                        if cntry_data.id and country.names[cntry_data.id] then\n                            countryName = string.lower(country.names[cntry_data.id])\n                        end\n\t\t\t\t\t\tmist.DBs.units[coa_name][countryName] = {}\n\t\t\t\t\t\tmist.DBs.units[coa_name][countryName].countryId = cntry_data.id\n\n\t\t\t\t\t\tif type(cntry_data) == 'table' then\t--just making sure\n\n\t\t\t\t\t\t\tfor obj_cat_name, obj_cat_data in pairs(cntry_data) do\n\n\t\t\t\t\t\t\t\tif obj_cat_name == \"helicopter\" or obj_cat_name == \"ship\" or obj_cat_name == \"plane\" or obj_cat_name == \"vehicle\" or obj_cat_name == \"static\" then --should be an unncessary check\n\n\t\t\t\t\t\t\t\t\tlocal category = obj_cat_name\n\n\t\t\t\t\t\t\t\t\tif ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then\t--there's a group!\n\n\t\t\t\t\t\t\t\t\t\tmist.DBs.units[coa_name][countryName][category] = {}\n\n\t\t\t\t\t\t\t\t\t\tfor group_num, group_data in pairs(obj_cat_data.group) do\n\n\t\t\t\t\t\t\t\t\t\t\tif group_data and group_data.units and type(group_data.units) == 'table' then\t--making sure again- this is a valid group\n\n\t\t\t\t\t\t\t\t\t\t\t\tmist.DBs.units[coa_name][countryName][category][group_num] = {}\n\t\t\t\t\t\t\t\t\t\t\t\tlocal groupName = group_data.name\n\t\t\t\t\t\t\t\t\t\t\t\tif env.mission.version > 7 and env.mission.version < 19 then\n\t\t\t\t\t\t\t\t\t\t\t\t\tgroupName = env.getValueDictByKey(groupName)\n\t\t\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\t\t\t\tmist.DBs.units[coa_name][countryName][category][group_num].groupName = groupName\n\t\t\t\t\t\t\t\t\t\t\t\tmist.DBs.units[coa_name][countryName][category][group_num].groupId = group_data.groupId\n\t\t\t\t\t\t\t\t\t\t\t\tmist.DBs.units[coa_name][countryName][category][group_num].category = category\n\t\t\t\t\t\t\t\t\t\t\t\tmist.DBs.units[coa_name][countryName][category][group_num].coalition = coa_name\n\t\t\t\t\t\t\t\t\t\t\t\tmist.DBs.units[coa_name][countryName][category][group_num].country = countryName\n\t\t\t\t\t\t\t\t\t\t\t\tmist.DBs.units[coa_name][countryName][category][group_num].countryId = cntry_data.id\n\t\t\t\t\t\t\t\t\t\t\t\tmist.DBs.units[coa_name][countryName][category][group_num].startTime = group_data.start_time\n\t\t\t\t\t\t\t\t\t\t\t\tmist.DBs.units[coa_name][countryName][category][group_num].task = group_data.task\n\t\t\t\t\t\t\t\t\t\t\t\tmist.DBs.units[coa_name][countryName][category][group_num].hidden = group_data.hidden\n\n\t\t\t\t\t\t\t\t\t\t\t\tmist.DBs.units[coa_name][countryName][category][group_num].units = {}\n\n\t\t\t\t\t\t\t\t\t\t\t\tmist.DBs.units[coa_name][countryName][category][group_num].radioSet = group_data.radioSet\n\t\t\t\t\t\t\t\t\t\t\t\tmist.DBs.units[coa_name][countryName][category][group_num].uncontrolled = group_data.uncontrolled\n\t\t\t\t\t\t\t\t\t\t\t\tmist.DBs.units[coa_name][countryName][category][group_num].frequency = group_data.frequency\n\t\t\t\t\t\t\t\t\t\t\t\tmist.DBs.units[coa_name][countryName][category][group_num].modulation = group_data.modulation\n\n\t\t\t\t\t\t\t\t\t\t\t\tfor unit_num, unit_data in pairs(group_data.units) do\n\t\t\t\t\t\t\t\t\t\t\t\t\tlocal units_tbl = mist.DBs.units[coa_name][countryName][category][group_num].units\t--pointer to the units table for this group\n\n\t\t\t\t\t\t\t\t\t\t\t\t\tunits_tbl[unit_num] = {}\n\t\t\t\t\t\t\t\t\t\t\t\t\tif env.mission.version > 7 and env.mission.version < 19 then\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tunits_tbl[unit_num].unitName = env.getValueDictByKey(unit_data.name)\n\t\t\t\t\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tunits_tbl[unit_num].unitName = unit_data.name\n\t\t\t\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\t\t\t\t\tunits_tbl[unit_num].type = unit_data.type\n\t\t\t\t\t\t\t\t\t\t\t\t\tunits_tbl[unit_num].skill = unit_data.skill\t--will be nil for statics\n\t\t\t\t\t\t\t\t\t\t\t\t\tunits_tbl[unit_num].unitId = unit_data.unitId\n\t\t\t\t\t\t\t\t\t\t\t\t\tunits_tbl[unit_num].category = category\n\t\t\t\t\t\t\t\t\t\t\t\t\tunits_tbl[unit_num].coalition = coa_name\n\t\t\t\t\t\t\t\t\t\t\t\t\tunits_tbl[unit_num].country = countryName\n\t\t\t\t\t\t\t\t\t\t\t\t\tunits_tbl[unit_num].countryId = cntry_data.id\n\t\t\t\t\t\t\t\t\t\t\t\t\tunits_tbl[unit_num].heading = unit_data.heading\n\t\t\t\t\t\t\t\t\t\t\t\t\tunits_tbl[unit_num].playerCanDrive = unit_data.playerCanDrive\n\t\t\t\t\t\t\t\t\t\t\t\t\tunits_tbl[unit_num].alt = unit_data.alt\n\t\t\t\t\t\t\t\t\t\t\t\t\tunits_tbl[unit_num].alt_type = unit_data.alt_type\n\t\t\t\t\t\t\t\t\t\t\t\t\tunits_tbl[unit_num].speed = unit_data.speed\n\t\t\t\t\t\t\t\t\t\t\t\t\tunits_tbl[unit_num].livery_id = unit_data.livery_id\n\t\t\t\t\t\t\t\t\t\t\t\t\tif unit_data.point then\t--ME currently does not work like this, but it might one day\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tunits_tbl[unit_num].point = unit_data.point\n\t\t\t\t\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tunits_tbl[unit_num].point = {}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tunits_tbl[unit_num].point.x = unit_data.x\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tunits_tbl[unit_num].point.y = unit_data.y\n\t\t\t\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\t\t\t\t\tunits_tbl[unit_num].x = unit_data.x\n\t\t\t\t\t\t\t\t\t\t\t\t\tunits_tbl[unit_num].y = unit_data.y\n\n\t\t\t\t\t\t\t\t\t\t\t\t\tunits_tbl[unit_num].callsign = unit_data.callsign\n\t\t\t\t\t\t\t\t\t\t\t\t\tunits_tbl[unit_num].onboard_num = unit_data.onboard_num\n\t\t\t\t\t\t\t\t\t\t\t\t\tunits_tbl[unit_num].hardpoint_racks = unit_data.hardpoint_racks\n\t\t\t\t\t\t\t\t\t\t\t\t\tunits_tbl[unit_num].psi = unit_data.psi\n\n\n\t\t\t\t\t\t\t\t\t\t\t\t\tunits_tbl[unit_num].groupName = groupName\n\t\t\t\t\t\t\t\t\t\t\t\t\tunits_tbl[unit_num].groupId = group_data.groupId\n\n\t\t\t\t\t\t\t\t\t\t\t\t\tif unit_data.AddPropAircraft then\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tunits_tbl[unit_num].AddPropAircraft = unit_data.AddPropAircraft\n\t\t\t\t\t\t\t\t\t\t\t\t\tend\n\n\t\t\t\t\t\t\t\t\t\t\t\t\tif category == 'static' then\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tunits_tbl[unit_num].categoryStatic = unit_data.category\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tunits_tbl[unit_num].shape_name = unit_data.shape_name\n                                                        units_tbl[unit_num].linkUnit = unit_data.linkUnit\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tif unit_data.mass then\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tunits_tbl[unit_num].mass = unit_data.mass\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tend\n\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tif unit_data.canCargo then\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tunits_tbl[unit_num].canCargo = unit_data.canCargo\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\t\t\t\t\tend\n\n\t\t\t\t\t\t\t\t\t\t\t\tend --for unit_num, unit_data in pairs(group_data.units) do\n\t\t\t\t\t\t\t\t\t\t\tend --if group_data and group_data.units then\n\t\t\t\t\t\t\t\t\t\tend --for group_num, group_data in pairs(obj_cat_data.group) do\n\t\t\t\t\t\t\t\t\tend --if ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then\n\t\t\t\t\t\t\t\tend --if obj_cat_name == \"helicopter\" or obj_cat_name == \"ship\" or obj_cat_name == \"plane\" or obj_cat_name == \"vehicle\" or obj_cat_name == \"static\" then\n\t\t\t\t\t\t\tend --for obj_cat_name, obj_cat_data in pairs(cntry_data) do\n\t\t\t\t\t\tend --if type(cntry_data) == 'table' then\n\t\t\t\t\tend --for cntry_id, cntry_data in pairs(coa_data.country) do\n\t\t\t\tend --if coa_data.country then --there is a country table\n\t\t\tend --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then\n\t\tend --for coa_name, coa_data in pairs(mission.coalition) do\n\n\t\tmist.DBs.unitsByName = {}\n\t\tmist.DBs.unitsById = {}\n\t\tmist.DBs.unitsByCat = {}\n\n\t\tmist.DBs.unitsByCat.helicopter = {}\t-- adding default categories\n\t\tmist.DBs.unitsByCat.plane = {}\n\t\tmist.DBs.unitsByCat.ship = {}\n\t\tmist.DBs.unitsByCat.static = {}\n\t\tmist.DBs.unitsByCat.vehicle = {}\n\n\t\tmist.DBs.unitsByNum = {}\n\n\t\tmist.DBs.groupsByName = {}\n\t\tmist.DBs.groupsById = {}\n\t\tmist.DBs.humansByName = {}\n\t\tmist.DBs.humansById = {}\n\n\t\tmist.DBs.dynGroupsAdded = {} -- will be filled by mist.dbUpdate from dynamically spawned groups\n\t\tmist.DBs.activeHumans = {}\n\n\t\tmist.DBs.aliveUnits = {}\t-- will be filled in by the \"updateAliveUnits\" coroutine in mist.main.\n\n\t\tmist.DBs.removedAliveUnits = {} -- will be filled in by the \"updateAliveUnits\" coroutine in mist.main.\n\n\t\tmist.DBs.const = {}\n\n\t\t-- not accessible by SSE, must use static list :-/\n\t\tmist.DBs.const.callsigns = {\n\t\t\t['NATO'] = {\n\t\t\t\t['rules'] = {\n\t\t\t\t\t['groupLimit'] = 9,\n\t\t\t\t},\n\t\t\t\t['AWACS'] = {\n\t\t\t\t\t['Overlord'] = 1,\n\t\t\t\t\t['Magic'] = 2,\n\t\t\t\t\t['Wizard'] = 3,\n\t\t\t\t\t['Focus'] =\t 4,\n\t\t\t\t\t['Darkstar'] =\t 5,\n\t\t\t\t},\n                ['TANKER'] = {\n\t\t\t\t\t['Texaco'] = 1,\n\t\t\t\t\t['Arco'] = 2,\n\t\t\t\t\t['Shell'] = 3,\n\t\t\t\t},\n                ['TRANSPORT'] = {\n                    ['Heavy'] = 9,\n                    ['Trash'] = 10,\n                    ['Cargo'] = 11,\n                    ['Ascot'] = 12,\n\t\t\t\t['JTAC'] = {\n\t\t\t\t\t['Axeman'] = 1,\n\t\t\t\t\t['Darknight'] = 2,\n\t\t\t\t\t['Warrior']\t= 3,\n\t\t\t\t\t['Pointer']\t= 4,\n\t\t\t\t\t['Eyeball'] = 5,\n\t\t\t\t\t['Moonbeam'] = 6,\n\t\t\t\t\t['Whiplash'] = 7,\n\t\t\t\t\t['Finger'] = 8,\n\t\t\t\t\t['Pinpoint'] = 9,\n\t\t\t\t\t['Ferret'] = 10,\n\t\t\t\t\t['Shaba'] = 11,\n\t\t\t\t\t['Playboy'] = 12,\n\t\t\t\t\t['Hammer'] = 13,\n\t\t\t\t\t['Jaguar'] = 14,\n\t\t\t\t\t['Deathstar'] =\t15,\n\t\t\t\t\t['Anvil'] = 16,\n\t\t\t\t\t['Firefly']\t= 17,\n\t\t\t\t\t['Mantis'] = 18,\n\t\t\t\t\t['Badger'] = 19,\n\t\t\t\t},\n\t\t\t\t['aircraft'] = {\n\t\t\t\t\t['Enfield'] = 1,\n\t\t\t\t\t['Springfield'] = 2,\n\t\t\t\t\t['Uzi']\t= 3,\n\t\t\t\t\t['Colt'] = 4,\n\t\t\t\t\t['Dodge'] =\t5,\n\t\t\t\t\t['Ford'] = 6,\n\t\t\t\t\t['Chevy'] = 7,\n\t\t\t\t\t['Pontiac'] = 8,\n\t\t\t\t},\n\n\t\t\t\t['unique'] = {\n\t\t\t\t\t['A10'] = {\n\t\t\t\t\t\t['Hawg'] = 9,\n\t\t\t\t\t\t['Boar'] = 10,\n\t\t\t\t\t\t['Pig'] = 11,\n\t\t\t\t\t\t['Tusk'] = 12,\n\t\t\t\t\t\t['rules'] = {\n\t\t\t\t\t\t\t['canUseAircraft'] = true,\n\t\t\t\t\t\t\t['appliesTo'] = {\n\t\t\t\t\t\t\t\t'A-10C_2',\n                                'A-10C',\n\t\t\t\t\t\t\t\t'A-10A',\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n                    },\n\t\t\t\t\t['f16'] = {\n                         Viper = 9,\n                         Venom = 10,\n                         Lobo = 11,\n                         Cowboy = 12,\n                         Python = 13,\n                         Rattler =14,\n                         Panther = 15,\n                         Wolf = 16,\n                         Weasel = 17,\n                         Wild = 18,\n                         Ninja = 19,\n                         Jedi = 20,\n                         rules = {\n                            ['canUseAircraft'] = true,\n                            ['appliesTo'] = {\n                                'F-16C_50',\n                                'F-16C bl.52d',\n                                'F-16C bl.50',\n                                'F-16A MLU',\n                                'F-16A',\n                            },\n                         },\n\n                    },\n\t\t\t\t\t['f18'] = {\n\t\t\t\t\t\t['Hornet'] = 9,\n\t\t\t\t\t\t['Squid'] = 10,\n\t\t\t\t\t\t['Ragin'] = 11,\n\t\t\t\t\t\t['Roman'] = 12,\n                         Sting = 13,\n                         Jury =14,\n                         Jokey = 15,\n                         Ram = 16,\n                         Hawk = 17,\n                         Devil = 18,\n                         Check = 19,\n                         Snake = 20,\n\t\t\t\t\t\t['rules'] = {\n\t\t\t\t\t\t\t['canUseAircraft'] = true,\n\t\t\t\t\t\t\t['appliesTo'] = {\n\t\t\t\t\t\t\t\t\n                                \"FA-18C_hornet\",\n\t\t\t\t\t\t\t\t'F/A-18C',\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n                    },\n                    ['b1'] = {\n\t\t\t\t\t\t['Bone'] = 9,\n\t\t\t\t\t\t['Dark'] = 10,\n\t\t\t\t\t\t['Vader'] = 11,\n\t\t\t\t\t\t['rules'] = {\n\t\t\t\t\t\t\t['canUseAircraft'] = true,\n\t\t\t\t\t\t\t['appliesTo'] = {\n\t\t\t\t\t\t\t\t'B-1B',\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n                    },\n                    ['b52'] = {\n\t\t\t\t\t\t['Buff'] = 9,\n\t\t\t\t\t\t['Dump'] = 10,\n\t\t\t\t\t\t['Kenworth'] = 11,\n\t\t\t\t\t\t['rules'] = {\n\t\t\t\t\t\t\t['canUseAircraft'] = true,\n\t\t\t\t\t\t\t['appliesTo'] = {\n\t\t\t\t\t\t\t\t'B-52H',\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n                    },\n                    ['f15e'] = {\n\t\t\t\t\t\t['Dude'] = 9,\n\t\t\t\t\t\t['Thud'] = 10,\n\t\t\t\t\t\t['Gunny'] = 11,\n\t\t\t\t\t\t['Trek'] = 12,\n                         Sniper = 13,\n                         Sled =14,\n                         Best = 15,\n                         Jazz = 16,\n                         Rage = 17,\n                         Tahoe = 18,\n\t\t\t\t\t\t['rules'] = {\n\t\t\t\t\t\t\t['canUseAircraft'] = true,\n\t\t\t\t\t\t\t['appliesTo'] = {\n\t\t\t\t\t\t\t\t'F-15E',\n                                --'F-15ERAZBAM',\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n                    },\n\n                },\n            },\n        },\n    }\n\t\tmist.DBs.const.shapeNames = {\n\t\t\t[\"Landmine\"] = \"landmine\",\n\t\t\t[\"FARP CP Blindage\"] = \"kp_ug\",\n\t\t\t[\"Subsidiary structure C\"] = \"saray-c\",\n\t\t\t[\"Barracks 2\"] = \"kazarma2\",\n\t\t\t[\"Small house 2C\"] = \"dom2c\",\n\t\t\t[\"Military staff\"] = \"aviashtab\",\n\t\t\t[\"Tech hangar A\"] = \"ceh_ang_a\",\n\t\t\t[\"Oil derrick\"] = \"neftevyshka\",\n\t\t\t[\"Tech combine\"] = \"kombinat\",\n\t\t\t[\"Garage B\"] = \"garage_b\",\n\t\t\t[\"Airshow_Crowd\"] = \"Crowd1\",\n\t\t\t[\"Hangar A\"] = \"angar_a\",\n\t\t\t[\"Repair workshop\"] = \"tech\",\n\t\t\t[\"Subsidiary structure D\"] = \"saray-d\",\n\t\t\t[\"FARP Ammo Dump Coating\"] = \"SetkaKP\",\n\t\t\t[\"Small house 1C area\"] = \"dom2c-all\",\n\t\t\t[\"Tank 2\"] = \"airbase_tbilisi_tank_01\",\n\t\t\t[\"Boiler-house A\"] = \"kotelnaya_a\",\n\t\t\t[\"Workshop A\"] = \"tec_a\",\n\t\t\t[\"Small werehouse 1\"] = \"s1\",\n\t\t\t[\"Garage small B\"] = \"garagh-small-b\",\n\t\t\t[\"Small werehouse 4\"] = \"s4\",\n\t\t\t[\"Shop\"] = \"magazin\",\n\t\t\t[\"Subsidiary structure B\"] = \"saray-b\",\n\t\t\t[\"FARP Fuel Depot\"] = \"GSM Rus\",\n\t\t\t[\"Coach cargo\"] = \"wagon-gruz\",\n\t\t\t[\"Electric power box\"] = \"tr_budka\",\n\t\t\t[\"Tank 3\"] = \"airbase_tbilisi_tank_02\",\n\t\t\t[\"Red_Flag\"] = \"H-flag_R\",\n\t\t\t[\"Container red 3\"] = \"konteiner_red3\",\n\t\t\t[\"Garage A\"] = \"garage_a\",\n\t\t\t[\"Hangar B\"] = \"angar_b\",\n\t\t\t[\"Black_Tyre\"] = \"H-tyre_B\",\n\t\t\t[\"Cafe\"] = \"stolovaya\",\n\t\t\t[\"Restaurant 1\"] = \"restoran1\",\n\t\t\t[\"Subsidiary structure A\"] = \"saray-a\",\n\t\t\t[\"Container white\"] = \"konteiner_white\",\n\t\t\t[\"Warehouse\"] = \"sklad\",\n\t\t\t[\"Tank\"] = \"bak\",\n\t\t\t[\"Railway crossing B\"] = \"pereezd_small\",\n\t\t\t[\"Subsidiary structure F\"] = \"saray-f\",\n\t\t\t[\"Farm A\"] = \"ferma_a\",\n\t\t\t[\"Small werehouse 3\"] = \"s3\",\n\t\t\t[\"Water tower A\"] = \"wodokachka_a\",\n\t\t\t[\"Railway station\"] = \"r_vok_sd\",\n\t\t\t[\"Coach a tank blue\"] = \"wagon-cisterna_blue\",\n\t\t\t[\"Supermarket A\"] = \"uniwersam_a\",\n\t\t\t[\"Coach a platform\"] = \"wagon-platforma\",\n\t\t\t[\"Garage small A\"] = \"garagh-small-a\",\n\t\t\t[\"TV tower\"] = \"tele_bash\",\n\t\t\t[\"Comms tower M\"] = \"tele_bash_m\",\n\t\t\t[\"Small house 1A\"] = \"domik1a\",\n\t\t\t[\"Farm B\"] = \"ferma_b\",\n\t\t\t[\"GeneratorF\"] = \"GeneratorF\",\n\t\t\t[\"Cargo1\"] = \"ab-212_cargo\",\n\t\t\t[\"Container red 2\"] = \"konteiner_red2\",\n\t\t\t[\"Subsidiary structure E\"] = \"saray-e\",\n\t\t\t[\"Coach a passenger\"] = \"wagon-pass\",\n\t\t\t[\"Black_Tyre_WF\"] = \"H-tyre_B_WF\",\n\t\t\t[\"Electric locomotive\"] = \"elektrowoz\",\n\t\t\t[\"Shelter\"] = \"ukrytie\",\n\t\t\t[\"Coach a tank yellow\"] = \"wagon-cisterna_yellow\",\n\t\t\t[\"Railway crossing A\"] = \"pereezd_big\",\n\t\t\t[\".Ammunition depot\"] = \"SkladC\",\n\t\t\t[\"Small werehouse 2\"] = \"s2\",\n\t\t\t[\"Windsock\"] = \"H-Windsock_RW\",\n\t\t\t[\"Shelter B\"] = \"ukrytie_b\",\n\t\t\t[\"Fuel tank\"] = \"toplivo-bak\",\n\t\t\t[\"Locomotive\"] = \"teplowoz\",\n\t\t\t[\".Command Center\"] = \"ComCenter\",\n\t\t\t[\"Pump station\"] = \"nasos\",\n\t\t\t[\"Black_Tyre_RF\"] = \"H-tyre_B_RF\",\n\t\t\t[\"Coach cargo open\"] = \"wagon-gruz-otkr\",\n\t\t\t[\"Subsidiary structure 3\"] = \"hozdomik3\",\n\t\t\t[\"FARP Tent\"] = \"PalatkaB\",\n\t\t\t[\"White_Tyre\"] = \"H-tyre_W\",\n\t\t\t[\"Subsidiary structure G\"] = \"saray-g\",\n\t\t\t[\"Container red 1\"] = \"konteiner_red1\",\n\t\t\t[\"Small house 1B area\"] = \"domik1b-all\",\n\t\t\t[\"Subsidiary structure 1\"] = \"hozdomik1\",\n\t\t\t[\"Container brown\"] = \"konteiner_brown\",\n\t\t\t[\"Small house 1B\"] = \"domik1b\",\n\t\t\t[\"Subsidiary structure 2\"] = \"hozdomik2\",\n\t\t\t[\"Chemical tank A\"] = \"him_bak_a\",\n\t\t\t[\"WC\"] = \"WC\",\n\t\t\t[\"Small house 1A area\"] = \"domik1a-all\",\n\t\t\t[\"White_Flag\"] = \"H-Flag_W\",\n\t\t\t[\"Airshow_Cone\"] = \"Comp_cone\",\n            [\"Bulk Cargo Ship Ivanov\"] = \"barge-1\",\n            [\"Bulk Cargo Ship Yakushev\"] = \"barge-2\",\n            [\"Outpost\"]=\"block\",\n            [\"Road outpost\"]=\"block-onroad\",\n            [\"Container camo\"] = \"bw_container_cargo\",\n            [\"Tech Hangar A\"] = \"ceh_ang_a\",\n            [\"Bunker 1\"] = \"dot\",\n            [\"Bunker 2\"] = \"dot2\",\n            [\"Tanker Elnya 160\"] = \"elnya\",\n            [\"F-shape barrier\"] = \"f_bar_cargo\",\n            [\"Helipad Single\"] = \"farp\",\n            [\"FARP\"] = \"farps\",\n            [\"Fueltank\"] = \"fueltank_cargo\",\n            [\"Gate\"] = \"gate\",\n            [\"FARP Fuel Depot\"] = \"gsm rus\",\n            [\"Armed house\"] = \"home1_a\",\n            [\"FARP Command Post\"] = \"kp-ug\",\n            [\"Watch Tower Armed\"] = \"ohr-vyshka\",\n            [\"Oiltank\"] = \"oiltank_cargo\",\n            [\"Pipes small\"] = \"pipes_small_cargo\",\n            [\"Pipes big\"] = \"pipes_big_cargo\",\n            [\"Oil platform\"] = \"plavbaza\",\n            [\"Tetrapod\"] = \"tetrapod_cargo\",\n            [\"Fuel tank\"] = \"toplivo\",\n            [\"Trunks long\"] = \"trunks_long_cargo\",\n            [\"Trunks small\"] = \"trunks_small_cargo\",\n            [\"Passenger liner\"] = \"yastrebow\",\n            [\"Passenger boat\"] = \"zwezdny\",\n            [\"Oil rig\"] = \"oil_platform\",\n            [\"Gas platform\"] = \"gas_platform\",\n            [\"Container 20ft\"] = \"container_20ft\",\n            [\"Container 40ft\"] = \"container_40ft\",\n            [\"Downed pilot\"] = \"cadaver\",\n            [\"Parachute\"] = \"parash\",\n            [\"Pilot F15 Parachute\"] = \"pilot_f15_parachute\",\n            [\"Pilot standing\"] = \"pilot_parashut\",\n\t\t}\n\t\t\n\t\t\n\t\t-- create mist.DBs.oldAliveUnits\n\t\t-- do\n\t\t-- local intermediate_alive_units = {}\t-- between 0 and 0.5 secs old\n\t\t-- 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\n\t\t-- if intermediate_alive_units then\n\t\t-- mist.DBs.oldAliveUnits = mist.utils.deepCopy(intermediate_alive_units)\n\t\t-- end\n\t\t-- intermediate_alive_units = mist.utils.deepCopy(mist.DBs.aliveUnits)\n\t\t-- timer.scheduleFunction(make_old_alive_units, nil, timer.getTime() + 0.5)\n\t\t-- end\n\n\t\t-- make_old_alive_units()\n\t\t-- end\n\n\t\t--Build DBs\n\t\tfor coa_name, coa_data in pairs(mist.DBs.units) do\n\t\t\tfor cntry_name, cntry_data in pairs(coa_data) do\n\t\t\t\tfor category_name, category_data in pairs(cntry_data) do\n\t\t\t\t\tif type(category_data) == 'table' then\n\t\t\t\t\t\tfor group_ind, group_data in pairs(category_data) do\n\t\t\t\t\t\t\tif type(group_data) == 'table' and group_data.units and type(group_data.units) == 'table' and #group_data.units > 0 then\t-- OCD paradigm programming\n\t\t\t\t\t\t\t\tmist.DBs.groupsByName[group_data.groupName] = mist.utils.deepCopy(group_data)\n\t\t\t\t\t\t\t\tmist.DBs.groupsById[group_data.groupId] = mist.utils.deepCopy(group_data)\n\t\t\t\t\t\t\t\tfor unit_ind, unit_data in pairs(group_data.units) do\n\t\t\t\t\t\t\t\t\tmist.DBs.unitsByName[unit_data.unitName] = mist.utils.deepCopy(unit_data)\n\t\t\t\t\t\t\t\t\tmist.DBs.unitsById[unit_data.unitId] = mist.utils.deepCopy(unit_data)\n\n\t\t\t\t\t\t\t\t\tmist.DBs.unitsByCat[unit_data.category] = mist.DBs.unitsByCat[unit_data.category] or {} -- future-proofing against new categories...\n\t\t\t\t\t\t\t\t\ttable.insert(mist.DBs.unitsByCat[unit_data.category], mist.utils.deepCopy(unit_data))\n\t\t\t\t\t\t\t\t\t--dbLog:info('inserting $1', unit_data.unitName)\n\t\t\t\t\t\t\t\t\ttable.insert(mist.DBs.unitsByNum, mist.utils.deepCopy(unit_data))\n\n\t\t\t\t\t\t\t\t\tif unit_data.skill and (unit_data.skill == \"Client\" or unit_data.skill == \"Player\") then\n\t\t\t\t\t\t\t\t\t\tmist.DBs.humansByName[unit_data.unitName] = mist.utils.deepCopy(unit_data)\n\t\t\t\t\t\t\t\t\t\tmist.DBs.humansById[unit_data.unitId] = mist.utils.deepCopy(unit_data)\n\t\t\t\t\t\t\t\t\t\t--if Unit.getByName(unit_data.unitName) then\n\t\t\t\t\t\t\t\t\t\t--\tmist.DBs.activeHumans[unit_data.unitName] = mist.utils.deepCopy(unit_data)\n\t\t\t\t\t\t\t\t\t\t--\tmist.DBs.activeHumans[unit_data.unitName].playerName = Unit.getByName(unit_data.unitName):getPlayerName()\n\t\t\t\t\t\t\t\t\t\t--end\n\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\tend\n\t\t\t\t\t\tend\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\n\t\t--DynDBs\n\t\tmist.DBs.MEunits = mist.utils.deepCopy(mist.DBs.units)\n\t\tmist.DBs.MEunitsByName = mist.utils.deepCopy(mist.DBs.unitsByName)\n\t\tmist.DBs.MEunitsById = mist.utils.deepCopy(mist.DBs.unitsById)\n\t\tmist.DBs.MEunitsByCat = mist.utils.deepCopy(mist.DBs.unitsByCat)\n\t\tmist.DBs.MEunitsByNum = mist.utils.deepCopy(mist.DBs.unitsByNum)\n\t\tmist.DBs.MEgroupsByName = mist.utils.deepCopy(mist.DBs.groupsByName)\n\t\tmist.DBs.MEgroupsById = mist.utils.deepCopy(mist.DBs.groupsById)\n\n\t\tmist.DBs.deadObjects = {}\n               \n        do\n\t\t\tlocal mt = {}\n\n\t\t\tfunction mt.__newindex(t, key, val)\n\t\t\t\tlocal original_key = key --only for duplicate runtime IDs.\n\t\t\t\tlocal key_ind = 1\n\t\t\t\twhile mist.DBs.deadObjects[key] do\n\t\t\t\t\t--dbLog:warn('duplicate runtime id of previously dead object key: $1', key)\n\t\t\t\t\tkey = tostring(original_key) .. ' #' .. tostring(key_ind)\n\t\t\t\t\tkey_ind = key_ind + 1\n\t\t\t\tend\n\n\t\t\t\tif mist.DBs.aliveUnits and mist.DBs.aliveUnits[val.object.id_] then\n\t\t\t\t\t----dbLog:info('object found in alive_units')\n\t\t\t\t\tval.objectData = mist.utils.deepCopy(mist.DBs.aliveUnits[val.object.id_])\n\t\t\t\t\tlocal pos = Object.getPosition(val.object)\n\t\t\t\t\tif pos then\n\t\t\t\t\t\tval.objectPos = pos.p\n\t\t\t\t\tend\n\t\t\t\t\tval.objectType = mist.DBs.aliveUnits[val.object.id_].category\n\n\t\t\t\telseif mist.DBs.removedAliveUnits and mist.DBs.removedAliveUnits[val.object.id_] then\t-- it didn't exist in alive_units, check old_alive_units\n\t\t\t\t\t----dbLog:info('object found in old_alive_units')\n\t\t\t\t\tval.objectData = mist.utils.deepCopy(mist.DBs.removedAliveUnits[val.object.id_])\n\t\t\t\t\tlocal pos = Object.getPosition(val.object)\n\t\t\t\t\tif pos then\n\t\t\t\t\t\tval.objectPos = pos.p\n\t\t\t\t\tend\n\t\t\t\t\tval.objectType = mist.DBs.removedAliveUnits[val.object.id_].category\n\n\t\t\t\telse\t--attempt to determine if static object...\n\t\t\t\t\t----dbLog:info('object not found in alive units or old alive units')\n\t\t\t\t\tlocal pos = Object.getPosition(val.object)\n\t\t\t\t\tif pos then\n\t\t\t\t\t\tlocal static_found = false\n\t\t\t\t\t\tfor ind, static in pairs(mist.DBs.unitsByCat.static) do\n\t\t\t\t\t\t\tif ((pos.p.x - static.point.x)^2 + (pos.p.z - static.point.y)^2)^0.5 < 0.1 then --really, it should be zero...\n\t\t\t\t\t\t\t\t--dbLog:info('correlated dead static object to position')\n\t\t\t\t\t\t\t\tval.objectData = static\n\t\t\t\t\t\t\t\tval.objectPos = pos.p\n\t\t\t\t\t\t\t\tval.objectType = 'static'\n\t\t\t\t\t\t\t\tstatic_found = true\n\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\tend\n\t\t\t\t\t\tend\n\t\t\t\t\t\tif not static_found then\n\t\t\t\t\t\t\tval.objectPos = pos.p\n\t\t\t\t\t\t\tval.objectType = 'building'\n\t\t\t\t\t\tend\n\t\t\t\t\telse\n\t\t\t\t\t\tval.objectType = 'unknown'\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\t\trawset(t, key, val)\n\t\t\tend\n\n\t\t\tsetmetatable(mist.DBs.deadObjects, mt)\n\t\tend\n\n\t\tdo -- mist unitID funcs\n\t\t\tfor id, idData in pairs(mist.DBs.unitsById) do\n\t\t\t\tif idData.unitId > mist.nextUnitId then\n\t\t\t\t\tmist.nextUnitId = mist.utils.deepCopy(idData.unitId)\n\t\t\t\tend\n\t\t\t\tif idData.groupId > mist.nextGroupId then\n\t\t\t\t\tmist.nextGroupId = mist.utils.deepCopy(idData.groupId)\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\n\n\tend\n\n\tlocal function updateAliveUnits()\t-- coroutine function\n\t\tlocal lalive_units = mist.DBs.aliveUnits -- local references for faster execution\n\t\tlocal lunits = mist.DBs.unitsByNum\n\t\tlocal ldeepcopy = mist.utils.deepCopy\n\t\tlocal lUnit = Unit\n\t\tlocal lremovedAliveUnits = mist.DBs.removedAliveUnits\n\t\tlocal updatedUnits = {}\n\n\t\tif #lunits > 0 then\n\t\t\tlocal units_per_run = math.ceil(#lunits/20)\n\t\t\tif units_per_run < 5 then\n\t\t\t\tunits_per_run = 5\n\t\t\tend\n\n\t\t\tfor i = 1, #lunits do\n\t\t\t\tif lunits[i].category ~= 'static' then -- can't get statics with Unit.getByName :(\n\t\t\t\t\tlocal unit = lUnit.getByName(lunits[i].unitName)\n\t\t\t\t\tif unit then\n\t\t\t\t\t\t----dbLog:info(\"unit named $1 alive!\", lunits[i].unitName) -- spammy\n\t\t\t\t\t\tlocal pos = unit:getPosition()\n\t\t\t\t\t\tlocal newtbl = ldeepcopy(lunits[i])\n\t\t\t\t\t\tif pos then\n\t\t\t\t\t\t\tnewtbl.pos = pos.p\n\t\t\t\t\t\tend\n\t\t\t\t\t\tnewtbl.unit = unit\n\t\t\t\t\t\t--newtbl.rt_id = unit.id_\n\t\t\t\t\t\tlalive_units[unit.id_] = newtbl\n\t\t\t\t\t\tupdatedUnits[unit.id_] = true\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\t\tif i%units_per_run == 0 then\n\t\t\t\t\tcoroutine.yield()\n\t\t\t\tend\n\t\t\tend\n\t\t\t-- All units updated, remove any \"alive\" units that were not updated- they are dead!\n\t\t\tfor unit_id, unit in pairs(lalive_units) do\n\t\t\t\tif not updatedUnits[unit_id] then\n\t\t\t\t\tlremovedAliveUnits[unit_id] = unit\n\t\t\t\t\tlalive_units[unit_id] = nil\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\tend\n\n\tlocal function dbUpdate(event, objType)\n\t\t--dbLog:info('dbUpdate')\n\t\tlocal newTable = {}\n\t\tnewTable.startTime =\t0\n\t\tif type(event) == 'string' then -- if name of an object.\n\t\t\tlocal newObject\n\t\t\tif Group.getByName(event) then\n\t\t\t\tnewObject = Group.getByName(event)\n\t\t\telseif StaticObject.getByName(event) then\n\t\t\t\tnewObject = StaticObject.getByName(event)\n\t\t\t\t--\tlog:info('its static')\n\t\t\telse\n\t\t\t\tlog:warn('$1 is not a Group or Static Object. This should not be possible. Sent category is: $2', event, objType)\n\t\t\t\treturn false\n\t\t\tend\n\n\t\t\tnewTable.name = newObject:getName()\n\t\t\tnewTable.groupId = tonumber(newObject:getID())\n\t\t\tnewTable.groupName = newObject:getName()\n\t\t\tlocal unitOneRef\n\t\t\tif objType == 'static' then\n\t\t\t\tunitOneRef = newObject\n\t\t\t\tnewTable.countryId = tonumber(newObject:getCountry())\n\t\t\t\tnewTable.coalitionId = tonumber(newObject:getCoalition())\n\t\t\t\tnewTable.category = 'static'\n\t\t\telse\n\t\t\t\tunitOneRef = newObject:getUnits()\n\t\t\t\tif #unitOneRef > 0 and unitOneRef[1] and type(unitOneRef[1]) == 'table' then\n                    newTable.countryId = tonumber(unitOneRef[1]:getCountry())\n                    newTable.coalitionId = tonumber(unitOneRef[1]:getCoalition())\n                    newTable.category = tonumber(newObject:getCategory())\n                else\n                    log:warn('getUnits failed to return on $1 ; Built Data: $2.', event, newTable)\n                    return false\n                end\n\t\t\tend\n\t\t\tfor countryData, countryId in pairs(country.id) do\n\t\t\t\tif newTable.country and string.upper(countryData) == string.upper(newTable.country) or countryId == newTable.countryId then\n\t\t\t\t\tnewTable.countryId = countryId\n\t\t\t\t\tnewTable.country = string.lower(countryData)\n\t\t\t\t\tfor coaData, coaId in pairs(coalition.side) do\n\t\t\t\t\t\tif coaId == coalition.getCountryCoalition(countryId) then\n\t\t\t\t\t\t\tnewTable.coalition = string.lower(coaData)\n\t\t\t\t\t\tend\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\t\tfor catData, catId in pairs(Unit.Category) do\n\t\t\t\tif objType == 'group' and Group.getByName(newTable.groupName):isExist() then\n\t\t\t\t\tif catId == Group.getByName(newTable.groupName):getCategory() then\n\t\t\t\t\t\tnewTable.category = string.lower(catData)\n\t\t\t\t\tend\n\t\t\t\telseif objType == 'static' and StaticObject.getByName(newTable.groupName):isExist() then\n\t\t\t\t\tif catId == StaticObject.getByName(newTable.groupName):getCategory() then\n\t\t\t\t\t\tnewTable.category = string.lower(catData)\n\t\t\t\t\tend\n\n\t\t\t\tend\n\t\t\tend\n\t\t\tlocal gfound = false\n\t\t\tfor index, data in pairs(mistAddedGroups) do\n\t\t\t\tif mist.stringMatch(data.name, newTable.groupName) == true then\n\t\t\t\t\tgfound = true\n\t\t\t\t\tnewTable.task = data.task\n\t\t\t\t\tnewTable.modulation = data.modulation\n\t\t\t\t\tnewTable.uncontrolled = data.uncontrolled\n\t\t\t\t\tnewTable.radioSet = data.radioSet\n\t\t\t\t\tnewTable.hidden = data.hidden\n\t\t\t\t\tnewTable.startTime = data.start_time\n\t\t\t\t\tmistAddedGroups[index] = nil\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tif gfound == false then\n\t\t\t\tnewTable.uncontrolled = false\n\t\t\t\tnewTable.hidden = false\n\t\t\tend\n\n\t\t\tnewTable.units = {}\n\t\t\tif objType == 'group' then\n\t\t\t\tfor unitId, unitData in pairs(unitOneRef) do\n\t\t\t\t\tnewTable.units[unitId] = {}\n\t\t\t\t\tnewTable.units[unitId].unitName = unitData:getName()\n\n\t\t\t\t\tnewTable.units[unitId].x = mist.utils.round(unitData:getPosition().p.x)\n\t\t\t\t\tnewTable.units[unitId].y = mist.utils.round(unitData:getPosition().p.z)\n\t\t\t\t\tnewTable.units[unitId].point = {}\n\t\t\t\t\tnewTable.units[unitId].point.x = newTable.units[unitId].x\n\t\t\t\t\tnewTable.units[unitId].point.y = newTable.units[unitId].y\n\t\t\t\t\tnewTable.units[unitId].alt = mist.utils.round(unitData:getPosition().p.y)\n\t\t\t\t\tnewTable.units[unitId].speed = mist.vec.mag(unitData:getVelocity())\n\n\t\t\t\t\tnewTable.units[unitId].heading = mist.getHeading(unitData, true)\n\n\t\t\t\t\tnewTable.units[unitId].type = unitData:getTypeName()\n\t\t\t\t\tnewTable.units[unitId].unitId = tonumber(unitData:getID())\n\n\n\t\t\t\t\tnewTable.units[unitId].groupName = newTable.groupName\n\t\t\t\t\tnewTable.units[unitId].groupId = newTable.groupId\n\t\t\t\t\tnewTable.units[unitId].countryId = newTable.countryId\n\t\t\t\t\tnewTable.units[unitId].coalitionId = newTable.coalitionId\n\t\t\t\t\tnewTable.units[unitId].coalition = newTable.coalition\n\t\t\t\t\tnewTable.units[unitId].country = newTable.country\n\t\t\t\t\tlocal found = false\n\t\t\t\t\tfor index, data in pairs(mistAddedObjects) do\n\t\t\t\t\t\tif mist.stringMatch(data.name, newTable.units[unitId].unitName) == true then\n\t\t\t\t\t\t\tfound = true\n\t\t\t\t\t\t\tnewTable.units[unitId].livery_id = data.livery_id\n\t\t\t\t\t\t\tnewTable.units[unitId].skill = data.skill\n\t\t\t\t\t\t\tnewTable.units[unitId].alt_type = data.alt_type\n\t\t\t\t\t\t\tnewTable.units[unitId].callsign = data.callsign\n\t\t\t\t\t\t\tnewTable.units[unitId].psi = data.psi\n\t\t\t\t\t\t\tmistAddedObjects[index] = nil\n\t\t\t\t\t\tend\n\t\t\t\t\t\tif found == false then\n\t\t\t\t\t\t\tnewTable.units[unitId].skill = \"High\"\n\t\t\t\t\t\t\tnewTable.units[unitId].alt_type = \"BARO\"\n\t\t\t\t\t\tend\n\t\t\t\t\t\tif newTable.units[unitId].alt_type == \"RADIO\" then -- raw postition MSL was grabbed for group, but spawn is AGL, so re-offset it\n\t\t\t\t\t\t\tnewTable.units[unitId].alt = (newTable.units[unitId].alt - land.getHeight({x = newTable.units[unitId].x, y = newTable.units[unitId].y}))\n\t\t\t\t\t\tend\n\t\t\t\t\tend\n\n\t\t\t\tend\n\t\t\telse -- its a static\n                newTable.category = 'static'\n\t\t\t\tnewTable.units[1] = {}\n\t\t\t\tnewTable.units[1].unitName = newObject:getName()\n\t\t\t\tnewTable.units[1].category = 'static'\n\t\t\t\tnewTable.units[1].x = mist.utils.round(newObject:getPosition().p.x)\n\t\t\t\tnewTable.units[1].y = mist.utils.round(newObject:getPosition().p.z)\n\t\t\t\tnewTable.units[1].point = {}\n\t\t\t\tnewTable.units[1].point.x = newTable.units[1].x\n\t\t\t\tnewTable.units[1].point.y = newTable.units[1].y\n\t\t\t\tnewTable.units[1].alt = mist.utils.round(newObject:getPosition().p.y)\n\t\t\t\tnewTable.units[1].heading = mist.getHeading(newObject, true)\n\t\t\t\tnewTable.units[1].type = newObject:getTypeName()\n\t\t\t\tnewTable.units[1].unitId = tonumber(newObject:getID())\n\t\t\t\tnewTable.units[1].groupName = newTable.name\n\t\t\t\tnewTable.units[1].groupId = newTable.groupId\n\t\t\t\tnewTable.units[1].countryId = newTable.countryId\n\t\t\t\tnewTable.units[1].country = newTable.country\n\t\t\t\tnewTable.units[1].coalitionId = newTable.coalitionId\n\t\t\t\tnewTable.units[1].coalition = newTable.coalition\n\t\t\t\tif newObject:getCategory() == 6 and newObject:getCargoDisplayName() then\n\t\t\t\t\tlocal mass = newObject:getCargoDisplayName()\n\t\t\t\t\tmass = string.gsub(mass, ' ', '')\n\t\t\t\t\tmass = string.gsub(mass, 'kg', '')\n\t\t\t\t\tnewTable.units[1].mass = tonumber(mass)\n\t\t\t\t\tnewTable.units[1].categoryStatic = 'Cargos'\n\t\t\t\t\tnewTable.units[1].canCargo = true\n\t\t\t\t\tnewTable.units[1].shape_name = 'ab-212_cargo'\n\t\t\t\tend\n\n\t\t\t\t----- search mist added objects for extra data if applicable\n\t\t\t\tfor index, data in pairs(mistAddedObjects) do\n\t\t\t\t\tif mist.stringMatch(data.name, newTable.units[1].unitName) == true then\n\t\t\t\t\t\tnewTable.units[1].shape_name = data.shape_name -- for statics\n\t\t\t\t\t\tnewTable.units[1].livery_id = data.livery_id\n\t\t\t\t\t\tnewTable.units[1].airdromeId = data.airdromeId\n\t\t\t\t\t\tnewTable.units[1].mass = data.mass\n\t\t\t\t\t\tnewTable.units[1].canCargo = data.canCargo\n\t\t\t\t\t\tnewTable.units[1].categoryStatic = data.categoryStatic\n\t\t\t\t\t\tnewTable.units[1].type = data.type\n                        newTable.units[1].linkUnit = data.linkUnit\n                        \n\t\t\t\t\t\tmistAddedObjects[index] = nil\n                        break\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\t\t--mist.debug.writeData(mist.utils.serialize,{'msg', newTable}, timer.getAbsTime() ..'Group.lua')\n\t\tnewTable.timeAdded = timer.getAbsTime() -- only on the dynGroupsAdded table. For other reference, see start time\n\t\t--mist.debug.dumpDBs()\n\t\t--end\n\t\t--dbLog:info('endDbUpdate')\n\t\treturn newTable\n\tend\n\n\t--[[DB update code... FRACK. I need to refactor some of it. \n\t\n\tThe problem is that the DBs need to account better for shared object names. Needs to write over some data and outright remove other.\n\t\n\tIf groupName is used then entire group needs to be rewritten\n\t\twhat to do with old groups units DB entries?. Names cant be assumed to be the same.\n\t\n\t\n\t-- new spawn event check.\n\t-- event handler filters everything into groups: tempSpawnedGroups\n\t-- this function then checks DBs to see if data has changed\n\t]]\n\tlocal function checkSpawnedEventsNew()\n\t\tif tempSpawnGroupsCounter > 0 then\n\t\t\t--[[local updatesPerRun = math.ceil(#tempSpawnedGroupsCounter/20)\n\t\t\tif updatesPerRun < 5 then\n\t\t\t\tupdatesPerRun = 5\n\t\t\tend]]\n\t\t\t\n\t\t\t--dbLog:info('iterate')\n\t\t\tfor name, gData in pairs(tempSpawnedGroups) do\n\t\t\t\t--env.info(name)\n                --dbLog:info(gData)\n\t\t\t\tlocal updated = false\n                local stillExists = false\n                if not gData.checked then \n                    tempSpawnedGroups[name].checked = true -- so if there was an error it will get cleared.\n                    local _g = gData.gp or Group.getByName(name)\n                    if mist.DBs.groupsByName[name] then\n                        -- first check group level properties, groupId, countryId, coalition\n                        --dbLog:info('Found in DBs, check if updated')\n                        local dbTable = mist.DBs.groupsByName[name]\n                        --dbLog:info(dbTable)\n                        if gData.type ~= 'static' then\n                           -- dbLog:info('Not static')\n                          \n                            if _g and _g:isExist() == true then \n                                stillExists = true\n                                local _u = _g:getUnit(1)\n\n                                if _u and (dbTable.groupId ~= tonumber(_g:getID()) or _u:getCountry() ~= dbTable.countryId or _u:getCoalition() ~= dbTable.coaltionId) then\n                                    --dbLog:info('Group Data mismatch')\n                                    updated = true\n                                else\n                                  --  dbLog:info('No Mismatch')\n                                end\n                            else\n                                dbLog:warn('$1 : Group was not accessible', name)\n                            end\n                        end\n                    end\t\t\t\n                    --dbLog:info('Updated: $1', updated)\n                    if updated == false and gData.type ~= 'static' then -- time to check units\n                       --dbLog:info('No Group Mismatch, Check Units')\n                        if _g and _g:isExist() == true then \n                            stillExists = true\n                            for index, uObject in pairs(_g:getUnits()) do\n                             --dbLog:info(index)\n                                if mist.DBs.unitsByName[uObject:getName()] then\n                                    --dbLog:info('UnitByName table exists')\n                                    local uTable = mist.DBs.unitsByName[uObject:getName()]\n                                    if tonumber(uObject:getID()) ~= uTable.unitId or uObject:getTypeName() ~= uTable.type  then\n                                        --dbLog:info('Unit Data mismatch')\n                                        updated = true\n                                        break\n                                    end\n                                end\n                            end\n                        end\n                    else\n                        stillExists = true\n                    end\n\n                    if stillExists == true and (updated == true or not mist.DBs.groupsByName[name]) then\n                        --dbLog:info('Get Table')\n                        local dbData =  dbUpdate(name, gData.type)\n                        if dbData and type(dbData) == 'table' then \n                            writeGroups[#writeGroups+1] = {data = dbData, isUpdated = updated}\n                        end\n                    end\n                    -- Work done, so remove\n                end\n                tempSpawnedGroups[name] = nil\n                tempSpawnGroupsCounter = tempSpawnGroupsCounter - 1\n\t\t\tend\t\t\t\n\t\tend\t\n\tend\n\t\n\tlocal function updateDBTables()\n\t\tlocal i = #writeGroups\n\n\t\tlocal savesPerRun = math.ceil(i/10)\n\t\tif savesPerRun < 5 then\n\t\t\tsavesPerRun = 5\n\t\tend\n\t\tif i > 0 then\n\t\t\t--dbLog:info('updateDBTables')\n\t\t\tlocal ldeepCopy = mist.utils.deepCopy\n\t\t\tfor x = 1, i do\n\t\t\t\t--dbLog:info(writeGroups[x])\n\t\t\t\tlocal newTable = writeGroups[x].data\n\t\t\t\tlocal updated = writeGroups[x].isUpdated\n\t\t\t\tlocal mistCategory\n\t\t\t\tif type(newTable.category) == 'string' then\n\t\t\t\t\tmistCategory = string.lower(newTable.category)\n\t\t\t\tend\n\n\t\t\t\tif string.upper(newTable.category) == 'GROUND_UNIT' then\n\t\t\t\t\tmistCategory = 'vehicle'\n\t\t\t\t\tnewTable.category = mistCategory\n\t\t\t\telseif string.upper(newTable.category) == 'AIRPLANE' then\n\t\t\t\t\tmistCategory = 'plane'\n\t\t\t\t\tnewTable.category = mistCategory\n\t\t\t\telseif string.upper(newTable.category) == 'HELICOPTER' then\n\t\t\t\t\tmistCategory = 'helicopter'\n\t\t\t\t\tnewTable.category = mistCategory\n\t\t\t\telseif string.upper(newTable.category) == 'SHIP' then\n\t\t\t\t\tmistCategory = 'ship'\n\t\t\t\t\tnewTable.category = mistCategory\n\t\t\t\tend\n\t\t\t\t--dbLog:info('Update unitsBy')\n\t\t\t\tfor newId, newUnitData in pairs(newTable.units) do\n\t\t\t\t\t--dbLog:info(newId)\n\t\t\t\t\tnewUnitData.category = mistCategory\n\t\t\t\t\tif newUnitData.unitId then\n\t\t\t\t\t\t--dbLog:info('byId')\n\t\t\t\t\t\tmist.DBs.unitsById[tonumber(newUnitData.unitId)] = ldeepCopy(newUnitData)\n\t\t\t\t\tend\n\t\t\t\t\t--dbLog:info(updated)\n\t\t\t\t\tif 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.\n\t\t\t\t\t\t--dbLog:info('Updating Unit Tables')\n\t\t\t\t\t\tfor i = 1, #mist.DBs.unitsByCat[mistCategory] do\n\t\t\t\t\t\t\tif mist.DBs.unitsByCat[mistCategory][i].unitName == newUnitData.unitName then\n\t\t\t\t\t\t\t\t--dbLog:info('Entry Found, Rewriting for unitsByCat')\n\t\t\t\t\t\t\t\tmist.DBs.unitsByCat[mistCategory][i] = ldeepCopy(newUnitData)\n\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\tend\n\t\t\t\t\t\tend \n\t\t\t\t\t\tfor i = 1, #mist.DBs.unitsByNum do\n\t\t\t\t\t\t\tif mist.DBs.unitsByNum[i].unitName == newUnitData.unitName then\n\t\t\t\t\t\t\t\t--dbLog:info('Entry Found, Rewriting for unitsByNum')\n\t\t\t\t\t\t\t\tmist.DBs.unitsByNum[i] = ldeepCopy(newUnitData)\n\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\tend\n\t\t\t\t\t\tend\n\t\t\t\t\t\t\n\t\t\t\t\telse\n\t\t\t\t\t\t--dbLog:info('Unitname not in use, add as normal')\n\t\t\t\t\t\tmist.DBs.unitsByCat[mistCategory][#mist.DBs.unitsByCat[mistCategory] + 1] = ldeepCopy(newUnitData)\n\t\t\t\t\t\tmist.DBs.unitsByNum[#mist.DBs.unitsByNum + 1] = ldeepCopy(newUnitData)\n\t\t\t\t\tend\n\t\t\t\t\tmist.DBs.unitsByName[newUnitData.unitName] = ldeepCopy(newUnitData)\n\n\t\t\t\t\t\n\t\t\t\tend\n\t\t\t\t-- this is a really annoying DB to populate. Gotta create new tables in case its missing\n\t\t\t\t--dbLog:info('write mist.DBs.units')\n\t\t\t\tif not mist.DBs.units[newTable.coalition] then\n\t\t\t\t\tmist.DBs.units[newTable.coalition] = {}\n\t\t\t\tend\n\n\t\t\t\tif not mist.DBs.units[newTable.coalition][newTable.country] then\n\t\t\t\t\tmist.DBs.units[newTable.coalition][(newTable.country)] = {}\n\t\t\t\t\tmist.DBs.units[newTable.coalition][(newTable.country)].countryId = newTable.countryId\n\t\t\t\tend\n\t\t\t\tif not mist.DBs.units[newTable.coalition][newTable.country][mistCategory] then\n\t\t\t\t\tmist.DBs.units[newTable.coalition][(newTable.country)][mistCategory] = {}\n\t\t\t\tend\n\t\t\t\t\n\t\t\t\tif updated == true then\n\t\t\t\t\t--dbLog:info('Updating DBsUnits')\n\t\t\t\t\tfor i = 1, #mist.DBs.units[newTable.coalition][(newTable.country)][mistCategory] do\n\t\t\t\t\t\tif mist.DBs.units[newTable.coalition][(newTable.country)][mistCategory][i].groupName == newTable.groupName then\n\t\t\t\t\t\t\t--dbLog:info('Entry Found, Rewriting')\n\t\t\t\t\t\t\tmist.DBs.units[newTable.coalition][(newTable.country)][mistCategory][i] = ldeepCopy(newTable)\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\tend\n\t\t\t\t\tend\n\t\t\t\telse\n\t\t\t\t\tmist.DBs.units[newTable.coalition][(newTable.country)][mistCategory][#mist.DBs.units[newTable.coalition][(newTable.country)][mistCategory] + 1] = ldeepCopy(newTable)\n\t\t\t\tend\n\t\t\t\t\n\n\t\t\t\tif newTable.groupId then\n\t\t\t\t\tmist.DBs.groupsById[newTable.groupId] = ldeepCopy(newTable)\n\t\t\t\tend\n\n\t\t\t\tmist.DBs.groupsByName[newTable.name] = ldeepCopy(newTable)\n\t\t\t\tmist.DBs.dynGroupsAdded[#mist.DBs.dynGroupsAdded + 1] = ldeepCopy(newTable)\n\n\t\t\t\twriteGroups[x] = nil\n\t\t\t\tif x%savesPerRun == 0 then\n\t\t\t\t\tcoroutine.yield()\n\t\t\t\tend\n\t\t\tend\n\t\t\tif timer.getTime() > lastUpdateTime then\n\t\t\t\tlastUpdateTime = timer.getTime()\n\t\t\tend\n\t\t\t--dbLog:info('endUpdateTables')\n\t\tend\n\tend\n\n\tlocal function groupSpawned(event)\n\t\t-- dont need to add units spawned in at the start of the mission if mist is loaded in init line\n\t\tif event.id == world.event.S_EVENT_BIRTH and timer.getTime0() < timer.getAbsTime() then\n\t\t\t--log:info('unitSpawnEvent')\n\t\t\t--log:info(event)\n            --log:info(event.initiator:getTypeName())\n\t\t\t\t--table.insert(tempSpawnedUnits,(event.initiator))\n\t\t\t\t-------\n\t\t\t\t-- New functionality below. \n\t\t\t\t-------\n\t\t\tif 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\n\t\t\t\t--log:info('Object is a Unit')\n\t\t\t\tif Unit.getGroup(event.initiator) then\n\t\t\t\t--\tlog:info(Unit.getGroup(event.initiator):getName())\n                    local g = Unit.getGroup(event.initiator)\n\t\t\t\t\tif not tempSpawnedGroups[g:getName()] then\n\t\t\t\t\t\t--log:info('added')\n\t\t\t\t\t\ttempSpawnedGroups[g:getName()] = {type = 'group', gp = g}\n\t\t\t\t\t\ttempSpawnGroupsCounter = tempSpawnGroupsCounter + 1\n\t\t\t\t\tend\n\t\t\t\telse\n\t\t\t\t\tlog:error('Group not accessible by unit in event handler. This is a DCS bug')\n\t\t\t\tend\n\t\t\telseif Object.getCategory(event.initiator) == 3 or Object.getCategory(event.initiator) == 6 then\n\t\t\t\t--log:info('Object is Static')\n\t\t\t\ttempSpawnedGroups[StaticObject.getName(event.initiator)] = {type = 'static'}\n\t\t\t\ttempSpawnGroupsCounter = tempSpawnGroupsCounter + 1\n\t\t\tend\n\t\t\t\t\n\t\t\t\n\t\tend\n\tend\n\n\tlocal function doScheduledFunctions()\n\t\tlocal i = 1\n\t\twhile i <= #scheduledTasks do\n\t\t\tif not scheduledTasks[i].rep then -- not a repeated process\n\t\t\t\tif scheduledTasks[i].t <= timer.getTime() then\n\t\t\t\t\tlocal task = scheduledTasks[i] -- local reference\n\t\t\t\t\ttable.remove(scheduledTasks, i)\n\t\t\t\t\tlocal err, errmsg = pcall(task.f, unpack(task.vars, 1, table.maxn(task.vars)))\n\t\t\t\t\tif not err then\n\t\t\t\t\t\tlog:error('Error in scheduled function: $1', errmsg)\n\t\t\t\t\tend\n\t\t\t\t\t--task.f(unpack(task.vars, 1, table.maxn(task.vars)))\t-- do the task, do not increment i\n\t\t\t\telse\n\t\t\t\t\ti = i + 1\n\t\t\t\tend\n\t\t\telse\n\t\t\t\tif scheduledTasks[i].st and scheduledTasks[i].st <= timer.getTime() then\t --if a stoptime was specified, and the stop time exceeded\n\t\t\t\t\ttable.remove(scheduledTasks, i) -- stop time exceeded, do not execute, do not increment i\n\t\t\t\telseif scheduledTasks[i].t <= timer.getTime() then\n\t\t\t\t\tlocal task = scheduledTasks[i] -- local reference\n\t\t\t\t\ttask.t = timer.getTime() + task.rep\t--schedule next run\n\t\t\t\t\tlocal err, errmsg = pcall(task.f, unpack(task.vars, 1, table.maxn(task.vars)))\n\t\t\t\t\tif not err then\n\t\t\t\t\t\tlog:error('Error in scheduled function: $1' .. errmsg)\n\t\t\t\t\tend\n\t\t\t\t\t--scheduledTasks[i].f(unpack(scheduledTasks[i].vars, 1, table.maxn(scheduledTasks[i].vars)))\t-- do the task\n\t\t\t\t\ti = i + 1\n\t\t\t\telse\n\t\t\t\t\ti = i + 1\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\tend\n\n\t-- Event handler to start creating the dead_objects table\n\tlocal function addDeadObject(event)\n\t\tif event.id == world.event.S_EVENT_DEAD or event.id == world.event.S_EVENT_CRASH then\n\t\t\tif event.initiator and event.initiator.id_ and event.initiator.id_ > 0 then\n\n\t\t\t\tlocal id = event.initiator.id_\t-- initial ID, could change if there is a duplicate id_ already dead.\n\t\t\t\tlocal val = {object = event.initiator} -- the new entry in mist.DBs.deadObjects.\n\n\t\t\t\tlocal original_id = id\t--only for duplicate runtime IDs.\n\t\t\t\tlocal id_ind = 1\n\t\t\t\twhile mist.DBs.deadObjects[id] do\n\t\t\t\t\t--log:info('duplicate runtime id of previously dead object id: $1', id)\n\t\t\t\t\tid = tostring(original_id) .. ' #' .. tostring(id_ind)\n\t\t\t\t\tid_ind = id_ind + 1\n\t\t\t\tend\n\n\t\t\t\tif mist.DBs.aliveUnits and mist.DBs.aliveUnits[val.object.id_] then\n\t\t\t\t\t--log:info('object found in alive_units')\n\t\t\t\t\tval.objectData = mist.utils.deepCopy(mist.DBs.aliveUnits[val.object.id_])\n\t\t\t\t\tlocal pos = Object.getPosition(val.object)\n\t\t\t\t\tif pos then\n\t\t\t\t\t\tval.objectPos = pos.p\n\t\t\t\t\tend\n\t\t\t\t\tval.objectType = mist.DBs.aliveUnits[val.object.id_].category\n\t\t\t\t\t--[[if mist.DBs.activeHumans[Unit.getName(val.object)] then\n\t\t\t\t\t--trigger.action.outText('remove via death: ' .. Unit.getName(val.object),20)\n\t\t\t\t\t\tmist.DBs.activeHumans[Unit.getName(val.object)] = nil\n\t\t\t\t\tend]]\n\t\t\t\telseif mist.DBs.removedAliveUnits and mist.DBs.removedAliveUnits[val.object.id_] then\t-- it didn't exist in alive_units, check old_alive_units\n\t\t\t\t\t--log:info('object found in old_alive_units')\n\t\t\t\t\tval.objectData = mist.utils.deepCopy(mist.DBs.removedAliveUnits[val.object.id_])\n\t\t\t\t\tlocal pos = Object.getPosition(val.object)\n\t\t\t\t\tif pos then\n\t\t\t\t\t\tval.objectPos = pos.p\n\t\t\t\t\tend\n\t\t\t\t\tval.objectType = mist.DBs.removedAliveUnits[val.object.id_].category\n\n\t\t\t\telse\t--attempt to determine if static object...\n\t\t\t\t\t--log:info('object not found in alive units or old alive units')\n\t\t\t\t\tlocal pos = Object.getPosition(val.object)\n\t\t\t\t\tif pos then\n\t\t\t\t\t\tlocal static_found = false\n\t\t\t\t\t\tfor ind, static in pairs(mist.DBs.unitsByCat.static) do\n\t\t\t\t\t\t\tif ((pos.p.x - static.point.x)^2 + (pos.p.z - static.point.y)^2)^0.5 < 0.1 then --really, it should be zero...\n\t\t\t\t\t\t\t\t--log:info('correlated dead static object to position')\n\t\t\t\t\t\t\t\tval.objectData = static\n\t\t\t\t\t\t\t\tval.objectPos = pos.p\n\t\t\t\t\t\t\t\tval.objectType = 'static'\n\t\t\t\t\t\t\t\tstatic_found = true\n\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\tend\n\t\t\t\t\t\tend\n\t\t\t\t\t\tif not static_found then\n\t\t\t\t\t\t\tval.objectPos = pos.p\n\t\t\t\t\t\t\tval.objectType = 'building'\n\t\t\t\t\t\tend\n\t\t\t\t\telse\n\t\t\t\t\t\tval.objectType = 'unknown'\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\t\tmist.DBs.deadObjects[id] = val\n\t\t\tend\n\t\tend\n\tend\n\n\t--[[\n\t\tlocal function addClientsToActive(event)\n\t\t\tif event.id == world.event.S_EVENT_PLAYER_ENTER_UNIT or event.id == world.event.S_EVENT_BIRTH then\n\t\t\t\tlog:info(event)\n\t\t\t\tif Unit.getPlayerName(event.initiator) then\n\t\t\t\t\tlog:info(Unit.getPlayerName(event.initiator))\n\t\t\t\t\tlocal newU = mist.utils.deepCopy(mist.DBs.unitsByName[Unit.getName(event.initiator)])\n\t\t\t\t\tnewU.playerName = Unit.getPlayerName(event.initiator)\n\t\t\t\t\tmist.DBs.activeHumans[Unit.getName(event.initiator)] = newU\n\t\t\t\t\t--trigger.action.outText('added: ' .. Unit.getName(event.initiator), 20)\n\t\t\t\tend\n\t\t\telseif event.id == world.event.S_EVENT_PLAYER_LEAVE_UNIT and event.initiator then\n\t\t\t\tif mist.DBs.activeHumans[Unit.getName(event.initiator)] then\n\t\t\t\t\tmist.DBs.activeHumans[Unit.getName(event.initiator)] = nil\n\t\t\t\t\t-- trigger.action.outText('removed via control: ' .. Unit.getName(event.initiator), 20)\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\n\tmist.addEventHandler(addClientsToActive)\n\t]]\n    local function verifyDB()\n        --log:warn('verfy Run')\n        for coaName, coaId in pairs(coalition.side) do\n            --env.info(coaName)\n            local gps = coalition.getGroups(coaId)\n            for i = 1, #gps do\n                if gps[i] and Group.getSize(gps[i]) > 0 then\n                    local gName = Group.getName(gps[i])\n                    if not mist.DBs.groupsByName[gName] then\n                            --env.info(Unit.getID(gUnits[j]) .. ' Not found in DB yet')\n                        if not tempSpawnedGroups[gName] then\n                            --dbLog:info('added')\n                            tempSpawnedGroups[gName] = {type = 'group', gp = gps[i]}\n                            tempSpawnGroupsCounter = tempSpawnGroupsCounter + 1\n                        end\n                    end\n                end\n            end\n            local st = coalition.getStaticObjects(coaId)\n            for i = 1, #st do\n                local s = st[i]\n                if StaticObject.isExist(s) then\n                    local name = s:getName()\n                    if not mist.DBs.unitsByName[name] then\n                       dbLog:warn('$1 Not found in DB yet. ID: $2', name, StaticObject.getID(s))\n                       if string.len(name) > 0 then  -- because in this mission someone sent the name was returning as an empty string. Gotta be careful. \n                            tempSpawnedGroups[s:getName()] = {type = 'static'}\n                            tempSpawnGroupsCounter = tempSpawnGroupsCounter + 1\n                       end\n                    end\n                end\n            end\n        \n        end\n    \n    end\n    \n\n\t--- init function.\n\t-- creates logger, adds default event handler\n\t-- and calls main the first time.\n\t-- @function mist.init\n\tfunction mist.init()\n        \n\t\t-- create logger\n\t\tmist.log = mist.Logger:new(\"MIST\", mistSettings.logLevel)\n\t\tdbLog = mist.Logger:new('MISTDB', 'warn')\n\t\t\n\t\tlog = mist.log -- log shorthand\n\t\t-- set warning log level, showing only\n\t\t-- warnings and errors\n\t\t--log:setLevel(\"warning\")\n\n\t\tlog:info(\"initializing databases\")\n\t\tinitDBs()\n\n\t\t-- add event handler for group spawns\n\t\tmist.addEventHandler(groupSpawned)\n\t\tmist.addEventHandler(addDeadObject)\n        \n        log:warn('Init time: $1', timer.getTime())\n\n\t\t-- call main the first time therafter it reschedules itself.\n\t\tmist.main()\n\t\t--log:msg('MIST version $1.$2.$3 loaded', mist.majorVersion, mist.minorVersion, mist.build)\n        \n        mist.scheduleFunction(verifyDB, {}, timer.getTime() + 1)\n\t\treturn\n\tend\n\n\t--- The main function.\n\t-- Run 100 times per second.\n\t-- You shouldn't call this function.\n\tfunction mist.main()\n\t\ttimer.scheduleFunction(mist.main, {}, timer.getTime() + 0.01)\t--reschedule first in case of Lua error\n\n\t\tupdateTenthSecond = updateTenthSecond + 1\n\t\tif updateTenthSecond == 20 then\n\t\t\tupdateTenthSecond = 0\n\n\t\t\tcheckSpawnedEventsNew()\n\t\t\t\n\t\t\tif not coroutines.updateDBTables then\n\t\t\t\tcoroutines.updateDBTables = coroutine.create(updateDBTables)\n\t\t\tend\n\n\t\t\tcoroutine.resume(coroutines.updateDBTables)\n\n\t\t\tif coroutine.status(coroutines.updateDBTables) == 'dead' then\n\t\t\t\tcoroutines.updateDBTables = nil\n\t\t\tend\n\t\tend\n\n\t\t--updating alive units\n\t\tupdateAliveUnitsCounter = updateAliveUnitsCounter + 1\n\t\tif updateAliveUnitsCounter == 5 then\n\t\t\tupdateAliveUnitsCounter = 0\n\n\t\t\tif not coroutines.updateAliveUnits then\n\t\t\t\tcoroutines.updateAliveUnits = coroutine.create(updateAliveUnits)\n\t\t\tend\n\n\t\t\tcoroutine.resume(coroutines.updateAliveUnits)\n\n\t\t\tif coroutine.status(coroutines.updateAliveUnits) == 'dead' then\n\t\t\t\tcoroutines.updateAliveUnits = nil\n\t\t\tend\n\t\tend\n\n\t\tdoScheduledFunctions()\n\tend -- end of mist.main\n\n\t--- Returns next unit id.\n\t-- @treturn number next unit id.\n\tfunction mist.getNextUnitId()\n\t\tmist.nextUnitId = mist.nextUnitId + 1\n\t\tif mist.nextUnitId > 6900 and mist.nextUnitId < 30000 then\n\t\t\tmist.nextUnitId = 30000\n\t\tend\n\t\treturn mist.utils.deepCopy(mist.nextUnitId)\n\tend\n\n\t--- Returns next group id.\n\t-- @treturn number next group id.\n\tfunction mist.getNextGroupId()\n\t\tmist.nextGroupId = mist.nextGroupId + 1\n\t\tif mist.nextGroupId > 6900 and mist.nextGroupId < 30000 then\n\t\t\tmist.nextGroupId = 30000\n\t\tend\n\t\treturn mist.utils.deepCopy(mist.nextGroupId)\n\tend\n\n\t--- Returns timestamp of last database update.\n\t-- @treturn timestamp of last database update\n\tfunction mist.getLastDBUpdateTime()\n\t\treturn lastUpdateTime\n\tend\n\n\t--- Spawns a static object to the game world.\n\t-- @todo write good docs\n\t-- @tparam table staticObj table containing data needed for the object creation\n\tfunction mist.dynAddStatic(n)\n        --log:info(newObj)\n        local newObj = mist.utils.deepCopy(n)\n\t\tif newObj.units and newObj.units[1] then -- if its mist format\n\t\t\tfor entry, val in pairs(newObj.units[1]) do\n\t\t\t\tif newObj[entry] and newObj[entry] ~= val or not newObj[entry] then\n\t\t\t\t\tnewObj[entry] = val\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\t\t--log:info(newObj)\n\t\t\n\t\tlocal cntry = newObj.country\n\t\tif newObj.countryId then\n\t\t\tcntry = newObj.countryId\n\t\tend\n\t\n\t\tlocal newCountry = ''\n\n\t\tfor countryId, countryName in pairs(country.name) do\n\t\t\tif type(cntry) == 'string' then\n\t\t\t\tcntry = cntry:gsub(\"%s+\", \"_\")\n\t\t\t\tif tostring(countryName) == string.upper(cntry) then\n\t\t\t\t\tnewCountry = countryName\n\t\t\t\tend\n\t\t\telseif type(cntry) == 'number' then\n\t\t\t\tif countryId == cntry then\n\t\t\t\t\tnewCountry = countryName\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\t\t\n\t\tif newCountry == '' then\n\t\t\tlog:error(\"Country not found: $1\", cntry)\n\t\t\treturn false\n\t\tend\n\t\n\t\tif newObj.clone or not newObj.groupId then\n\t\t\tmistGpId = mistGpId + 1\n\t\t\tnewObj.groupId = mistGpId\n\t\tend\n\n\t\tif newObj.clone or not newObj.unitId then\n\t\t\tmistUnitId = mistUnitId + 1\n\t\t\tnewObj.unitId = mistUnitId\n\t\tend\n\n\n        newObj.name = newObj.name or newObj.unitName\n        \n\t\tif newObj.clone or not newObj.name then\n\t\t\tmistDynAddIndex[' static '] = mistDynAddIndex[' static '] + 1\n\t\t\tnewObj.name = (newCountry .. ' static ' .. mistDynAddIndex[' static '])\n\t\tend\n\n\t\tif not newObj.dead then\n\t\t\tnewObj.dead = false\n\t\tend\n\n\t\tif not newObj.heading then\n\t\t\tnewObj.heading = math.random(360)\n\t\tend\n\t\t\n\t\tif newObj.categoryStatic then\n\t\t\tnewObj.category = newObj.categoryStatic\n\t\tend\n\t\tif newObj.mass then\n\t\t\tnewObj.category = 'Cargos'\n\t\tend\n\t\t\n\t\tif newObj.shapeName then\n\t\t\tnewObj.shape_name = newObj.shapeName\n\t\tend\n\t\t\n\t\tif not newObj.shape_name then\n\t\t\tlog:info('shape_name not present')\n\t\t\tif mist.DBs.const.shapeNames[newObj.type] then\n\t\t\t\tnewObj.shape_name = mist.DBs.const.shapeNames[newObj.type]\n\t\t\tend\n\t\tend\n\t\t\n\t\tmistAddedObjects[#mistAddedObjects + 1] = mist.utils.deepCopy(newObj)\n\t\tif newObj.x and newObj.y and newObj.type and type(newObj.x) == 'number' and type(newObj.y) == 'number' and type(newObj.type) == 'string' then\n\t\t\t--log:warn(newObj)\n\t\t\tcoalition.addStaticObject(country.id[newCountry], newObj)\n\n\t\t\treturn newObj\n\t\tend\n\t\tlog:error(\"Failed to add static object due to missing or incorrect value. X: $1, Y: $2, Type: $3\", newObj.x, newObj.y, newObj.type)\n\t\treturn false\n\tend\n\n\t--- Spawns a dynamic group into the game world.\n\t-- Same as coalition.add function in SSE. checks the passed data to see if its valid.\n\t-- Will generate groupId, groupName, unitId, and unitName if needed\n\t-- @tparam table newGroup table containting values needed for spawning a group.\n\tfunction mist.dynAdd(ng)\n        \n        local newGroup = mist.utils.deepCopy(ng)\n        --log:warn(newGroup)\n\t\t--mist.debug.writeData(mist.utils.serialize,{'msg', newGroup}, 'newGroupOrig.lua')\n\t\tlocal cntry = newGroup.country\n\t\tif newGroup.countryId then\n\t\t\tcntry = newGroup.countryId\n\t\tend\n\n\t\tlocal groupType = newGroup.category\n\t\tlocal newCountry = ''\n\t\t-- validate data\n\t\tfor countryId, countryName in pairs(country.name) do\n\t\t\tif type(cntry) == 'string' then\n\t\t\t\tcntry = cntry:gsub(\"%s+\", \"_\")\n\t\t\t\tif tostring(countryName) == string.upper(cntry) then\n\t\t\t\t\tnewCountry = countryName\n\t\t\t\tend\n\t\t\telseif type(cntry) == 'number' then\n\t\t\t\tif countryId == cntry then\n\t\t\t\t\tnewCountry = countryName\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\n\t\tif newCountry == '' then\n\t\t\tlog:error(\"Country not found: $1\", cntry)\n\t\t\treturn false\n\t\tend\n\n\t\tlocal newCat = ''\n\t\tfor catName, catId in pairs(Unit.Category) do\n\t\t\tif type(groupType) == 'string' then\n\t\t\t\tif tostring(catName) == string.upper(groupType) then\n\t\t\t\t\tnewCat = catName\n\t\t\t\tend\n\t\t\telseif type(groupType) == 'number' then\n\t\t\t\tif catId == groupType then\n\t\t\t\t\tnewCat = catName\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tif catName == 'GROUND_UNIT' and (string.upper(groupType) == 'VEHICLE' or string.upper(groupType) == 'GROUND') then\n\t\t\t\tnewCat = 'GROUND_UNIT'\n\t\t\telseif catName == 'AIRPLANE' and string.upper(groupType) == 'PLANE' then\n\t\t\t\tnewCat = 'AIRPLANE'\n\t\t\tend\n\t\tend\n\t\tlocal typeName\n\t\tif newCat == 'GROUND_UNIT' then\n\t\t\ttypeName = ' gnd '\n\t\telseif newCat == 'AIRPLANE' then\n\t\t\ttypeName = ' air '\n\t\telseif newCat == 'HELICOPTER' then\n\t\t\ttypeName = ' hel '\n\t\telseif newCat == 'SHIP' then\n\t\t\ttypeName = ' shp '\n\t\telseif newCat == 'BUILDING' then\n\t\t\ttypeName = ' bld '\n\t\tend\n\t\tif newGroup.clone or not newGroup.groupId then\n\t\t\tmistDynAddIndex[typeName] = mistDynAddIndex[typeName] + 1\n\t\t\tmistGpId = mistGpId + 1\n\t\t\tnewGroup.groupId = mistGpId\n\t\tend\n\t\tif newGroup.groupName or newGroup.name then\n\t\t\tif newGroup.groupName then\n\t\t\t\tnewGroup.name = newGroup.groupName\n\t\t\telseif newGroup.name then\n\t\t\t\tnewGroup.name = newGroup.name\n\t\t\tend\n\t\tend\n\n\t\tif newGroup.clone and mist.DBs.groupsByName[newGroup.name] or not newGroup.name then\n            --if newGroup.baseName then\n                -- idea of later. So custmozed naming can be created\n           -- else\n                newGroup.name = tostring(newCountry .. tostring(typeName) .. mistDynAddIndex[typeName])\n            --end\n\t\tend\n\n\t\tif not newGroup.hidden then\n\t\t\tnewGroup.hidden = false\n\t\tend\n\n\t\tif not newGroup.visible then\n\t\t\tnewGroup.visible = false\n\t\tend\n\n\t\tif (newGroup.start_time and type(newGroup.start_time) ~= 'number') or not newGroup.start_time then\n\t\t\tif newGroup.startTime then\n\t\t\t\tnewGroup.start_time = mist.utils.round(newGroup.startTime)\n\t\t\telse\n\t\t\t\tnewGroup.start_time = 0\n\t\t\tend\n\t\tend\n\n\n\t\tfor unitIndex, unitData in pairs(newGroup.units) do\n\t\t\tlocal originalName = newGroup.units[unitIndex].unitName or newGroup.units[unitIndex].name\n\t\t\tif newGroup.clone or not unitData.unitId then\n\t\t\t\tmistUnitId = mistUnitId + 1\n\t\t\t\tnewGroup.units[unitIndex].unitId = mistUnitId\n\t\t\tend\n\t\t\tif newGroup.units[unitIndex].unitName or newGroup.units[unitIndex].name then\n\t\t\t\tif newGroup.units[unitIndex].unitName then\n\t\t\t\t\tnewGroup.units[unitIndex].name = newGroup.units[unitIndex].unitName\n\t\t\t\telseif newGroup.units[unitIndex].name then\n\t\t\t\t\tnewGroup.units[unitIndex].name = newGroup.units[unitIndex].name\n\t\t\t\tend\n\t\t\tend\n\t\t\tif newGroup.clone or not unitData.name then\n\t\t\t\tnewGroup.units[unitIndex].name = tostring(newGroup.name .. ' unit' .. unitIndex)\n\t\t\tend\n\n\t\t\tif not unitData.skill then\n\t\t\t\tnewGroup.units[unitIndex].skill = 'Random'\n\t\t\tend\n\n\t\t\tif newCat == 'AIRPLANE' or newCat == 'HELICOPTER' then\n\t\t\t\tif newGroup.units[unitIndex].alt_type and newGroup.units[unitIndex].alt_type ~= 'BARO' or not newGroup.units[unitIndex].alt_type then\n\t\t\t\t\tnewGroup.units[unitIndex].alt_type = 'RADIO'\n\t\t\t\tend\n\t\t\t\tif not unitData.speed then\n\t\t\t\t\tif newCat == 'AIRPLANE' then\n\t\t\t\t\t\tnewGroup.units[unitIndex].speed = 150\n\t\t\t\t\telseif newCat == 'HELICOPTER' then\n\t\t\t\t\t\tnewGroup.units[unitIndex].speed = 60\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\t\tif not unitData.payload then\n\t\t\t\t\tnewGroup.units[unitIndex].payload = mist.getPayload(originalName)\n\t\t\t\tend\n\t\t\t\tif not unitData.alt then\n\t\t\t\t\tif newCat == 'AIRPLANE' then\n\t\t\t\t\t\tnewGroup.units[unitIndex].alt = 2000\n\t\t\t\t\t\tnewGroup.units[unitIndex].alt_type = 'RADIO'\n\t\t\t\t\t\tnewGroup.units[unitIndex].speed = 150\n\t\t\t\t\telseif newCat == 'HELICOPTER' then\n\t\t\t\t\t\tnewGroup.units[unitIndex].alt = 500\n\t\t\t\t\t\tnewGroup.units[unitIndex].alt_type = 'RADIO'\n\t\t\t\t\t\tnewGroup.units[unitIndex].speed = 60\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\t\t\n\t\t\telseif newCat == 'GROUND_UNIT' then\n\t\t\t\tif nil == unitData.playerCanDrive then\n\t\t\t\t\tunitData.playerCanDrive = true\n\t\t\t\tend\n\t\t\t\n\t\t\tend\n\t\t\tmistAddedObjects[#mistAddedObjects + 1] = mist.utils.deepCopy(newGroup.units[unitIndex])\n\t\tend\n\t\tmistAddedGroups[#mistAddedGroups + 1] = mist.utils.deepCopy(newGroup)\n\t\tif newGroup.route then\n            if newGroup.route and not newGroup.route.points then\n                if newGroup.route[1] then\n                    local copyRoute = mist.utils.deepCopy(newGroup.route)\n                    newGroup.route = {}\n                    newGroup.route.points = copyRoute\n                end\n            end\n\t\telse -- if aircraft and no route assigned. make a quick and stupid route so AI doesnt RTB immediately\n\t\t\t--if newCat == 'AIRPLANE' or newCat == 'HELICOPTER' then\n\t\t\t\tnewGroup.route = {}\n\t\t\t\tnewGroup.route.points = {}\n\t\t\t\tnewGroup.route.points[1] = {}\n\t\t\t--end\n\t\tend\n\t\tnewGroup.country = newCountry\n\n        -- update and verify any self tasks\n        if newGroup.route and newGroup.route.points then \n            for i, pData in pairs(newGroup.route.points) do\n                if pData.task and pData.task.params and pData.task.params.tasks and #pData.task.params.tasks > 0 then\n                    for tIndex, tData in pairs(pData.task.params.tasks) do\n                        if tData.params and tData.params.action then  \n                            if tData.params.action.id == \"EPLRS\" then\n                                tData.params.action.params.groupId = newGroup.groupId\n                            elseif tData.params.action.id == \"ActivateBeacon\" or tData.params.action.id == \"ActivateICLS\" then \n                                tData.params.action.params.unitId = newGroup.units[1].unitId\n                            end \n                        end\n                    end\n                end\n            \n            end\n        end\n\t\t--mist.debug.writeData(mist.utils.serialize,{'msg', newGroup}, 'newGroupPushedToAddGroup.lua')\n        --log:warn(newGroup)\n\t\t-- sanitize table\n\t\tnewGroup.groupName = nil\n\t\tnewGroup.clone = nil\n\t\tnewGroup.category = nil\n\t\tnewGroup.country = nil\n\n\t\tnewGroup.tasks = {}\n\n\t\tfor unitIndex, unitData in pairs(newGroup.units) do\n\t\t\tnewGroup.units[unitIndex].unitName = nil\n\t\tend\n        \n\t\tcoalition.addGroup(country.id[newCountry], Unit.Category[newCat], newGroup)\n\n\t\treturn newGroup\n\n\tend\n\n\t--- Schedules a function.\n\t-- Modified Slmod task scheduler, superior to timer.scheduleFunction\n\t-- @tparam function f function to schedule\n\t-- @tparam table vars array containing all parameters passed to the function\n\t-- @tparam number t time in seconds from mission start to schedule the function to.\n\t-- @tparam[opt] number rep time between repetitions of the function\n\t-- @tparam[opt] number st time in seconds from mission start at which the function\n\t-- should stop to be rescheduled.\n\t-- @treturn number scheduled function id.\n\tfunction mist.scheduleFunction(f, vars, t, rep, st)\n\t\t--verify correct types\n\t\tassert(type(f) == 'function', 'variable 1, expected function, got ' .. type(f))\n\t\tassert(type(vars) == 'table' or vars == nil, 'variable 2, expected table or nil, got ' .. type(f))\n\t\tassert(type(t) == 'number', 'variable 3, expected number, got ' .. type(t))\n\t\tassert(type(rep) == 'number' or rep == nil, 'variable 4, expected number or nil, got ' .. type(rep))\n\t\tassert(type(st) == 'number' or st == nil, 'variable 5, expected number or nil, got ' .. type(st))\n\t\tif not vars then\n\t\t\tvars = {}\n\t\tend\n\t\ttaskId = taskId + 1\n\t\ttable.insert(scheduledTasks, {f = f, vars = vars, t = t, rep = rep, st = st, id = taskId})\n\t\treturn taskId\n\tend\n\n\t--- Removes a scheduled function.\n\t-- @tparam number id function id\n\t-- @treturn boolean true if function was successfully removed, false otherwise.\n\tfunction mist.removeFunction(id)\n\t\tlocal i = 1\n\t\twhile i <= #scheduledTasks do\n\t\t\tif scheduledTasks[i].id == id then\n\t\t\t\ttable.remove(scheduledTasks, i)\n                return true\n\t\t\telse\n\t\t\t\ti = i + 1\n\t\t\tend\n\t\tend\n        return false\n\tend\n\n\t--- Registers an event handler.\n\t-- @tparam function f function handling event\n\t-- @treturn number id of the event handler\n\tfunction mist.addEventHandler(f) --id is optional!\n\t\tlocal handler = {}\n\t\tidNum = idNum + 1\n\t\thandler.id = idNum\n\t\thandler.f = f\n\t\tfunction handler:onEvent(event)\n\t\t\tself.f(event)\n\t\tend\n\t\tworld.addEventHandler(handler)\n\t\treturn handler.id\n\tend\n\n\t--- Removes event handler with given id.\n\t-- @tparam number id event handler id\n\t-- @treturn boolean true on success, false otherwise\n\tfunction mist.removeEventHandler(id)\n\t\tfor key, handler in pairs(world.eventHandlers) do\n\t\t\tif handler.id and handler.id == id then\n\t\t\t\tworld.eventHandlers[key] = nil\n\t\t\t\treturn true\n\t\t\tend\n\t\tend\n\t\treturn false\n\tend\nend\n\n-- Begin common funcs\ndo\n\t--- Returns MGRS coordinates as string.\n\t-- @tparam string MGRS MGRS coordinates\n\t-- @tparam number acc the accuracy of each easting/northing.\n\t-- Can be: 0, 1, 2, 3, 4, or 5.\n\tfunction mist.tostringMGRS(MGRS, acc)\n\t\tif acc == 0 then\n\t\t\treturn MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph\n\t\telse\n\t\t\treturn MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph .. ' ' .. string.format('%0' .. acc .. 'd', mist.utils.round(MGRS.Easting/(10^(5-acc)), 0))\n\t\t\t.. ' ' .. string.format('%0' .. acc .. 'd', mist.utils.round(MGRS.Northing/(10^(5-acc)), 0))\n\t\tend\n\tend\n\n\t--[[acc:\n\tin DM: decimal point of minutes.\n\tIn DMS: decimal point of seconds.\n\tposition after the decimal of the least significant digit:\n\tSo:\n\t42.32 - acc of 2.\n\t]]\n\tfunction mist.tostringLL(lat, lon, acc, DMS)\n\n\t\tlocal latHemi, lonHemi\n\t\tif lat > 0 then\n\t\t\tlatHemi = 'N'\n\t\telse\n\t\t\tlatHemi = 'S'\n\t\tend\n\n\t\tif lon > 0 then\n\t\t\tlonHemi = 'E'\n\t\telse\n\t\t\tlonHemi = 'W'\n\t\tend\n\n\t\tlat = math.abs(lat)\n\t\tlon = math.abs(lon)\n\n\t\tlocal latDeg = math.floor(lat)\n\t\tlocal latMin = (lat - latDeg)*60\n\n\t\tlocal lonDeg = math.floor(lon)\n\t\tlocal lonMin = (lon - lonDeg)*60\n\n\t\tif DMS then\t-- degrees, minutes, and seconds.\n\t\t\tlocal oldLatMin = latMin\n\t\t\tlatMin = math.floor(latMin)\n\t\t\tlocal latSec = mist.utils.round((oldLatMin - latMin)*60, acc)\n\n\t\t\tlocal oldLonMin = lonMin\n\t\t\tlonMin = math.floor(lonMin)\n\t\t\tlocal lonSec = mist.utils.round((oldLonMin - lonMin)*60, acc)\n\n\t\t\tif latSec == 60 then\n\t\t\t\tlatSec = 0\n\t\t\t\tlatMin = latMin + 1\n\t\t\tend\n\n\t\t\tif lonSec == 60 then\n\t\t\t\tlonSec = 0\n\t\t\t\tlonMin = lonMin + 1\n\t\t\tend\n\n\t\t\tlocal secFrmtStr -- create the formatting string for the seconds place\n\t\t\tif acc <= 0 then\t-- no decimal place.\n\t\t\t\tsecFrmtStr = '%02d'\n\t\t\telse\n\t\t\t\tlocal width = 3 + acc\t-- 01.310 - that's a width of 6, for example.\n\t\t\t\tsecFrmtStr = '%0' .. width .. '.' .. acc .. 'f'\n\t\t\tend\n\n\t\t\treturn string.format('%02d', latDeg) .. ' ' .. string.format('%02d', latMin) .. '\\' ' .. string.format(secFrmtStr, latSec) .. '\"' .. latHemi .. '\t '\n\t\t\t.. string.format('%02d', lonDeg) .. ' ' .. string.format('%02d', lonMin) .. '\\' ' .. string.format(secFrmtStr, lonSec) .. '\"' .. lonHemi\n\n\t\telse\t-- degrees, decimal minutes.\n\t\t\tlatMin = mist.utils.round(latMin, acc)\n\t\t\tlonMin = mist.utils.round(lonMin, acc)\n\n\t\t\tif latMin == 60 then\n\t\t\t\tlatMin = 0\n\t\t\t\tlatDeg = latDeg + 1\n\t\t\tend\n\n\t\t\tif lonMin == 60 then\n\t\t\t\tlonMin = 0\n\t\t\t\tlonDeg = lonDeg + 1\n\t\t\tend\n\n\t\t\tlocal minFrmtStr -- create the formatting string for the minutes place\n\t\t\tif acc <= 0 then\t-- no decimal place.\n\t\t\t\tminFrmtStr = '%02d'\n\t\t\telse\n\t\t\t\tlocal width = 3 + acc\t-- 01.310 - that's a width of 6, for example.\n\t\t\t\tminFrmtStr = '%0' .. width .. '.' .. acc .. 'f'\n\t\t\tend\n\n\t\t\treturn string.format('%02d', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\\'' .. latHemi .. '\t '\n\t\t\t.. string.format('%02d', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\\'' .. lonHemi\n\n\t\tend\n\tend\n\n\t--[[ required: az - radian\n\t\trequired: dist - meters\n\t\toptional: alt - meters (set to false or nil if you don't want to use it).\n\t\toptional: metric - set true to get dist and alt in km and m.\n\t\tprecision will always be nearest degree and NM or km.]]\n\tfunction mist.tostringBR(az, dist, alt, metric)\n\t\taz = mist.utils.round(mist.utils.toDegree(az), 0)\n\n\t\tif metric then\n\t\t\tdist = mist.utils.round(dist/1000, 0)\n\t\telse\n\t\t\tdist = mist.utils.round(mist.utils.metersToNM(dist), 0)\n\t\tend\n\n\t\tlocal s = string.format('%03d', az) .. ' for ' .. dist\n\n\t\tif alt then\n\t\t\tif metric then\n\t\t\t\ts = s .. ' at ' .. mist.utils.round(alt, 0)\n\t\t\telse\n\t\t\t\ts = s .. ' at ' .. mist.utils.round(mist.utils.metersToFeet(alt), 0)\n\t\t\tend\n\t\tend\n\t\treturn s\n\tend\n\n\tfunction mist.getNorthCorrection(gPoint)\t--gets the correction needed for true north\n\t\tlocal point = mist.utils.deepCopy(gPoint)\n\t\tif not point.z then --Vec2; convert to Vec3\n\t\t\tpoint.z = point.y\n\t\t\tpoint.y = 0\n\t\tend\n\t\tlocal lat, lon = coord.LOtoLL(point)\n\t\tlocal north_posit = coord.LLtoLO(lat + 1, lon)\n\t\treturn math.atan2(north_posit.z - point.z, north_posit.x - point.x)\n\tend\n\n\t--- Returns skill of the given unit.\n\t-- @tparam string unitName unit name\n\t-- @return skill of the unit\n\tfunction mist.getUnitSkill(unitName)\n\t\tif mist.DBs.unitsByName[unitName] then\n\t\t\tif Unit.getByName(unitName) then\n\t\t\t\tlocal lunit = Unit.getByName(unitName)\n\t\t\t\tlocal data = mist.DBs.unitsByName[unitName]\n\t\t\t\tif data.unitName == unitName and data.type == lunit:getTypeName() and data.unitId == tonumber(lunit:getID()) and data.skill then\n\t\t\t\t\treturn data.skill\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\t\tlog:error(\"Unit not found in DB: $1\", unitName)\n\t\treturn false\n\tend\n\n\t--- Returns an array containing a group's units positions.\n\t--\te.g.\n\t--\t\t{\n\t--\t\t\t[1] = {x = 299435.224, y = -1146632.6773},\n\t--\t\t\t[2] = {x = 663324.6563, y = 322424.1112}\n\t--\t\t}\n\t--\t@tparam number|string groupIdent group id or name\n\t--\t@treturn table array containing positions of each group member\n\tfunction mist.getGroupPoints(groupIdent)\n\t\t-- search by groupId and allow groupId and groupName as inputs\n\t\tlocal gpId = groupIdent\n\t\tif type(groupIdent) == 'string' and not tonumber(groupIdent) then\n\t\t\tif mist.DBs.MEgroupsByName[groupIdent] then\n\t\t\t\tgpId = mist.DBs.MEgroupsByName[groupIdent].groupId\n\t\t\telse\n\t\t\t\tlog:error(\"Group not found in mist.DBs.MEgroupsByName: $1\", groupIdent)\n\t\t\tend\n\t\tend\n\n\t\tfor coa_name, coa_data in pairs(env.mission.coalition) do\n\t\t\tif  type(coa_data) == 'table' then\n\t\t\t\tif coa_data.country then --there is a country table\n\t\t\t\t\tfor cntry_id, cntry_data in pairs(coa_data.country) do\n\t\t\t\t\t\tfor obj_cat_name, obj_cat_data in pairs(cntry_data) do\n\t\t\t\t\t\t\tif obj_cat_name == \"helicopter\" or obj_cat_name == \"ship\" or obj_cat_name == \"plane\" or obj_cat_name == \"vehicle\" then\t-- only these types have points\n\t\t\t\t\t\t\t\tif ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then\t--there's a group!\n\t\t\t\t\t\t\t\t\tfor group_num, group_data in pairs(obj_cat_data.group) do\n\t\t\t\t\t\t\t\t\t\tif group_data and group_data.groupId == gpId then -- this is the group we are looking for\n\t\t\t\t\t\t\t\t\t\t\tif group_data.route and group_data.route.points and #group_data.route.points > 0 then\n\t\t\t\t\t\t\t\t\t\t\t\tlocal points = {}\n\t\t\t\t\t\t\t\t\t\t\t\tfor point_num, point in pairs(group_data.route.points) do\n\t\t\t\t\t\t\t\t\t\t\t\t\tif not point.point then\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tpoints[point_num] = { x = point.x, y = point.y }\n\t\t\t\t\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tpoints[point_num] = point.point\t--it's possible that the ME could move to the point = Vec2 notation.\n\t\t\t\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\t\t\t\treturn points\n\t\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t\t\t\tend\t--if group_data and group_data.name and group_data.name == 'groupname'\n\t\t\t\t\t\t\t\t\tend --for group_num, group_data in pairs(obj_cat_data.group) do\n\t\t\t\t\t\t\t\tend --if ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then\n\t\t\t\t\t\t\tend --if obj_cat_name == \"helicopter\" or obj_cat_name == \"ship\" or obj_cat_name == \"plane\" or obj_cat_name == \"vehicle\" or obj_cat_name == \"static\" then\n\t\t\t\t\t\tend --for obj_cat_name, obj_cat_data in pairs(cntry_data) do\n\t\t\t\t\tend --for cntry_id, cntry_data in pairs(coa_data.country) do\n\t\t\t\tend --if coa_data.country then --there is a country table\n\t\t\tend --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then\n\t\tend --for coa_name, coa_data in pairs(mission.coalition) do\n\tend\n\n\t--- getUnitAttitude(unit) return values.\n\t-- Yaw, AoA, ClimbAngle - relative to earth reference\n\t-- DOES NOT TAKE INTO ACCOUNT WIND.\n\t-- @table attitude\n\t-- @tfield number Heading in radians, range of 0 to 2*pi,\n\t-- relative to true north.\n\t-- @tfield number Pitch in radians, range of -pi/2 to pi/2\n\t-- @tfield number Roll in radians, range of 0 to 2*pi,\n\t-- right roll is positive direction.\n\t-- @tfield number Yaw in radians, range of -pi to pi,\n\t-- right yaw is positive direction.\n\t-- @tfield number AoA in radians, range of -pi to pi,\n\t-- rotation of aircraft to the right in comparison to\n\t-- flight direction being positive.\n\t-- @tfield number ClimbAngle in radians, range of -pi/2 to pi/2\n\n\t--- Returns the attitude of a given unit.\n\t-- Will work on any unit, even if not an aircraft.\n\t-- @tparam Unit unit unit whose attitude is returned.\n\t-- @treturn table @{attitude}\n\tfunction mist.getAttitude(unit)\n\t\tlocal unitpos = unit:getPosition()\n\t\tif unitpos then\n\n\t\t\tlocal Heading = math.atan2(unitpos.x.z, unitpos.x.x)\n\n\t\t\tHeading = Heading + mist.getNorthCorrection(unitpos.p)\n\n\t\t\tif Heading < 0 then\n\t\t\t\tHeading = Heading + 2*math.pi\t-- put heading in range of 0 to 2*pi\n\t\t\tend\n\t\t\t---- heading complete.----\n\n\t\t\tlocal Pitch = math.asin(unitpos.x.y)\n\t\t\t---- pitch complete.----\n\n\t\t\t-- now get roll:\n\t\t\t--maybe not the best way to do it, but it works.\n\n\t\t\t--first, make a vector that is perpendicular to y and unitpos.x with cross product\n\t\t\tlocal cp = mist.vec.cp(unitpos.x, {x = 0, y = 1, z = 0})\n\n\t\t\t--now, get dot product of of this cross product with unitpos.z\n\t\t\tlocal dp = mist.vec.dp(cp, unitpos.z)\n\n\t\t\t--now get the magnitude of the roll (magnitude of the angle between two vectors is acos(vec1.vec2/|vec1||vec2|)\n\t\t\tlocal Roll = math.acos(dp/(mist.vec.mag(cp)*mist.vec.mag(unitpos.z)))\n\n\t\t\t--now, have to get sign of roll.\n\t\t\t-- by convention, making right roll positive\n\t\t\t-- to get sign of roll, use the y component of unitpos.z.\tFor right roll, y component is negative.\n\n\t\t\tif unitpos.z.y > 0 then -- left roll, flip the sign of the roll\n\t\t\t\tRoll = -Roll\n\t\t\tend\n\t\t\t---- roll complete. ----\n\n\t\t\t--now, work on yaw, AoA, climb, and abs velocity\n\t\t\tlocal Yaw\n\t\t\tlocal AoA\n\t\t\tlocal ClimbAngle\n\n\t\t\t-- get unit velocity\n\t\t\tlocal unitvel = unit:getVelocity()\n\t\t\tif mist.vec.mag(unitvel) ~= 0 then --must have non-zero velocity!\n\t\t\t\tlocal AxialVel = {}\t--unit velocity transformed into aircraft axes directions\n\n\t\t\t\t--transform velocity components in direction of aircraft axes.\n\t\t\t\tAxialVel.x = mist.vec.dp(unitpos.x, unitvel)\n\t\t\t\tAxialVel.y = mist.vec.dp(unitpos.y, unitvel)\n\t\t\t\tAxialVel.z = mist.vec.dp(unitpos.z, unitvel)\n\n\t\t\t\t--Yaw is the angle between unitpos.x and the x and z velocities\n\t\t\t\t--define right yaw as positive\n\t\t\t\tYaw = 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}))\n\n\t\t\t\t--now set correct direction:\n\t\t\t\tif AxialVel.z > 0 then\n\t\t\t\t\tYaw = -Yaw\n\t\t\t\tend\n\n\t\t\t\t-- AoA is angle between unitpos.x and the x and y velocities\n\t\t\t\tAoA = 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}))\n\n\t\t\t\t--now set correct direction:\n\t\t\t\tif AxialVel.y > 0 then\n\t\t\t\t\tAoA = -AoA\n\t\t\t\tend\n\n\t\t\t\tClimbAngle = math.asin(unitvel.y/mist.vec.mag(unitvel))\n\t\t\tend\n\t\t\treturn { Heading = Heading, Pitch = Pitch, Roll = Roll, Yaw = Yaw, AoA = AoA, ClimbAngle = ClimbAngle}\n\t\telse\n\t\t\tlog:error(\"Couldn't get unit's position\")\n\t\tend\n\tend\n\n\t--- Returns heading of given unit.\n\t-- @tparam Unit unit unit whose heading is returned.\n\t-- @param rawHeading\n\t-- @treturn number heading of the unit, in range\n\t-- of 0 to 2*pi.\n\tfunction mist.getHeading(unit, rawHeading)\n\t\tlocal unitpos = unit:getPosition()\n\t\tif unitpos then\n\t\t\tlocal Heading = math.atan2(unitpos.x.z, unitpos.x.x)\n\t\t\tif not rawHeading then\n\t\t\t\tHeading = Heading + mist.getNorthCorrection(unitpos.p)\n\t\t\tend\n\t\t\tif Heading < 0 then\n\t\t\t\tHeading = Heading + 2*math.pi\t-- put heading in range of 0 to 2*pi\n\t\t\tend\n\t\t\treturn Heading\n\t\tend\n\tend\n\n\t--- Returns given unit's pitch\n\t-- @tparam Unit unit unit whose pitch is returned.\n\t-- @treturn number pitch of given unit\n\tfunction mist.getPitch(unit)\n\t\tlocal unitpos = unit:getPosition()\n\t\tif unitpos then\n\t\t\treturn math.asin(unitpos.x.y)\n\t\tend\n\tend\n\n\t--- Returns given unit's roll.\n\t-- @tparam Unit unit unit whose roll is returned.\n\t-- @treturn number roll of given unit\n\tfunction mist.getRoll(unit)\n\t\tlocal unitpos = unit:getPosition()\n\t\tif unitpos then\n\t\t\t-- now get roll:\n\t\t\t--maybe not the best way to do it, but it works.\n\n\t\t\t--first, make a vector that is perpendicular to y and unitpos.x with cross product\n\t\t\tlocal cp = mist.vec.cp(unitpos.x, {x = 0, y = 1, z = 0})\n\n\t\t\t--now, get dot product of of this cross product with unitpos.z\n\t\t\tlocal dp = mist.vec.dp(cp, unitpos.z)\n\n\t\t\t--now get the magnitude of the roll (magnitude of the angle between two vectors is acos(vec1.vec2/|vec1||vec2|)\n\t\t\tlocal Roll = math.acos(dp/(mist.vec.mag(cp)*mist.vec.mag(unitpos.z)))\n\n\t\t\t--now, have to get sign of roll.\n\t\t\t-- by convention, making right roll positive\n\t\t\t-- to get sign of roll, use the y component of unitpos.z.\tFor right roll, y component is negative.\n\n\t\t\tif unitpos.z.y > 0 then -- left roll, flip the sign of the roll\n\t\t\t\tRoll = -Roll\n\t\t\tend\n\t\t\treturn Roll\n\t\tend\n\tend\n\n\t--- Returns given unit's yaw.\n\t-- @tparam Unit unit unit whose yaw is returned.\n\t-- @treturn number yaw of given unit.\n\tfunction mist.getYaw(unit)\n\t\tlocal unitpos = unit:getPosition()\n\t\tif unitpos then\n\t\t\t-- get unit velocity\n\t\t\tlocal unitvel = unit:getVelocity()\n\t\t\tif mist.vec.mag(unitvel) ~= 0 then --must have non-zero velocity!\n\t\t\t\tlocal AxialVel = {}\t--unit velocity transformed into aircraft axes directions\n\n\t\t\t\t--transform velocity components in direction of aircraft axes.\n\t\t\t\tAxialVel.x = mist.vec.dp(unitpos.x, unitvel)\n\t\t\t\tAxialVel.y = mist.vec.dp(unitpos.y, unitvel)\n\t\t\t\tAxialVel.z = mist.vec.dp(unitpos.z, unitvel)\n\n\t\t\t\t--Yaw is the angle between unitpos.x and the x and z velocities\n\t\t\t\t--define right yaw as positive\n\t\t\t\tlocal 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}))\n\n\t\t\t\t--now set correct direction:\n\t\t\t\tif AxialVel.z > 0 then\n\t\t\t\t\tYaw = -Yaw\n\t\t\t\tend\n\t\t\t\treturn Yaw\n\t\t\tend\n\t\tend\n\tend\n\n\t--- Returns given unit's angle of attack.\n\t-- @tparam Unit unit unit to get AoA from.\n\t-- @treturn number angle of attack of the given unit.\n\tfunction mist.getAoA(unit)\n\t\tlocal unitpos = unit:getPosition()\n\t\tif unitpos then\n\t\t\tlocal unitvel = unit:getVelocity()\n\t\t\tif mist.vec.mag(unitvel) ~= 0 then --must have non-zero velocity!\n\t\t\t\tlocal AxialVel = {}\t--unit velocity transformed into aircraft axes directions\n\n\t\t\t\t--transform velocity components in direction of aircraft axes.\n\t\t\t\tAxialVel.x = mist.vec.dp(unitpos.x, unitvel)\n\t\t\t\tAxialVel.y = mist.vec.dp(unitpos.y, unitvel)\n\t\t\t\tAxialVel.z = mist.vec.dp(unitpos.z, unitvel)\n\n\t\t\t\t-- AoA is angle between unitpos.x and the x and y velocities\n\t\t\t\tlocal 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}))\n\n\t\t\t\t--now set correct direction:\n\t\t\t\tif AxialVel.y > 0 then\n\t\t\t\t\tAoA = -AoA\n\t\t\t\tend\n\t\t\t\treturn AoA\n\t\t\tend\n\t\tend\n\tend\n\n\t--- Returns given unit's climb angle.\n\t-- @tparam Unit unit unit to get climb angle from.\n\t-- @treturn number climb angle of given unit.\n\tfunction mist.getClimbAngle(unit)\n\t\tlocal unitpos = unit:getPosition()\n\t\tif unitpos then\n\t\t\tlocal unitvel = unit:getVelocity()\n\t\t\tif mist.vec.mag(unitvel) ~= 0 then --must have non-zero velocity!\n\t\t\t\treturn math.asin(unitvel.y/mist.vec.mag(unitvel))\n\t\t\tend\n\t\tend\n\tend\n\n\t--[[--\n\tUnit name table.\n\tMany Mist functions require tables of unit names, which are known\n\tin Mist as UnitNameTables. These follow a special set of shortcuts\n\tborrowed from Slmod. These shortcuts alleviate the problem of entering\n\thuge lists of unit names by hand, and in many cases, they remove the\n\tneed to even know the names of the units in the first place!\n\n\tThese are the unit table \"short-cut\" commands:\n\n\tPrefixes:\n\t\t\t\"[-u]<unit name>\" - subtract this unit if its in the table\n\t\t\t\"[g]<group name>\" - add this group to the table\n\t\t\t\"[-g]<group name>\" - subtract this group from the table\n\t\t\t\"[c]<country name>\"\t- add this country's units\n\t\t\t\"[-c]<country name>\" - subtract this country's units if any are in the table\n\n\tStand-alone identifiers\n\t\t\t\"[all]\" - add all units\n\t\t\t\"[-all]\" - subtract all units (not very useful by itself)\n\t\t\t\"[blue]\" - add all blue units\n\t\t\t\"[-blue]\" - subtract all blue units\n\t\t\t\"[red]\" - add all red coalition units\n\t\t\t\"[-red]\" - subtract all red units\n\n\tCompound Identifiers:\n\t\t\t\"[c][helicopter]<country name>\"\t- add all of this country's helicopters\n\t\t\t\"[-c][helicopter]<country name>\" - subtract all of this country's helicopters\n\t\t\t\"[c][plane]<country name>\"\t- add all of this country's planes\n\t\t\t\"[-c][plane]<country name>\" - subtract all of this country's planes\n\t\t\t\"[c][ship]<country name>\"\t- add all of this country's ships\n\t\t\t\"[-c][ship]<country name>\" - subtract all of this country's ships\n\t\t\t\"[c][vehicle]<country name>\"\t- add all of this country's vehicles\n\t\t\t\"[-c][vehicle]<country name>\" - subtract all of this country's vehicles\n\n\t\t\t\"[all][helicopter]\" -\tadd all helicopters\n\t\t\t\"[-all][helicopter]\" - subtract all helicopters\n\t\t\t\"[all][plane]\" - add all\tplanes\n\t\t\t\"[-all][plane]\" - subtract all planes\n\t\t\t\"[all][ship]\" - add all ships\n\t\t\t\"[-all][ship]\" - subtract all ships\n\t\t\t\"[all][vehicle]\" - add all vehicles\n\t\t\t\"[-all][vehicle]\" - subtract all vehicles\n\n\t\t\t\"[blue][helicopter]\" -\tadd all blue coalition helicopters\n\t\t\t\"[-blue][helicopter]\" - subtract all blue coalition helicopters\n\t\t\t\"[blue][plane]\" - add all blue coalition planes\n\t\t\t\"[-blue][plane]\" - subtract all blue coalition planes\n\t\t\t\"[blue][ship]\" - add all blue coalition ships\n\t\t\t\"[-blue][ship]\" - subtract all blue coalition ships\n\t\t\t\"[blue][vehicle]\" - add all blue coalition vehicles\n\t\t\t\"[-blue][vehicle]\" - subtract all blue coalition vehicles\n\n\t\t\t\"[red][helicopter]\" -\tadd all red coalition helicopters\n\t\t\t\"[-red][helicopter]\" - subtract all red coalition helicopters\n\t\t\t\"[red][plane]\" - add all red coalition planes\n\t\t\t\"[-red][plane]\" - subtract all red coalition planes\n\t\t\t\"[red][ship]\" - add all red coalition ships\n\t\t\t\"[-red][ship]\" - subtract all red coalition ships\n\t\t\t\"[red][vehicle]\" - add all red coalition vehicles\n\t\t\t\"[-red][vehicle]\" - subtract all red coalition vehicles\n\n\tCountry names to be used in [c] and [-c] short-cuts:\n\t\t\tTurkey\n\t\t\tNorway\n\t\t\tThe Netherlands\n\t\t\tSpain\n\t\t\t11\n\t\t\tUK\n\t\t\tDenmark\n\t\t\tUSA\n\t\t\tGeorgia\n\t\t\tGermany\n\t\t\tBelgium\n\t\t\tCanada\n\t\t\tFrance\n\t\t\tIsrael\n\t\t\tUkraine\n\t\t\tRussia\n\t\t\tSouth Ossetia\n\t\t\tAbkhazia\n\t\t\tItaly\n\t\t\tAustralia\n\t\t\tAustria\n\t\t\tBelarus\n\t\t\tBulgaria\n\t\t\tCzech Republic\n\t\t\tChina\n\t\t\tCroatia\n\t\t\tFinland\n\t\t\tGreece\n\t\t\tHungary\n\t\t\tIndia\n\t\t\tIran\n\t\t\tIraq\n\t\t\tJapan\n\t\t\tKazakhstan\n\t\t\tNorth Korea\n\t\t\tPakistan\n\t\t\tPoland\n\t\t\tRomania\n\t\t\tSaudi Arabia\n\t\t\tSerbia, Slovakia\n\t\t\tSouth Korea\n\t\t\tSweden\n\t\t\tSwitzerland\n\t\t\tSyria\n\t\t\tUSAF Aggressors\n\n\tDo NOT use a '[u]' notation for single units. Single units are referenced\n\tthe same way as before: Simply input their names as strings.\n\n\tThese unit tables are evaluated in order, and you cannot subtract a unit\n\tfrom a table before it is added. For example:\n\n\t\t\t{'[blue]', '[-c]Georgia'}\n\n\twill evaluate to all of blue coalition except those units owned by the\n\tcountry named \"Georgia\"; however:\n\n\t\t\t{'[-c]Georgia', '[blue]'}\n\n\twill evaluate to all of the units in blue coalition, because the addition\n\tof all units owned by blue coalition occurred AFTER the subtraction of all\n\tunits owned by Georgia (which actually subtracted nothing at all, since\n\tthere were no units in the table when the subtraction occurred).\n\n\tMore examples:\n\n\t\t\t{'[blue][plane]', '[-c]Georgia', '[-g]Hawg 1'}\n\n\tEvaluates to all blue planes, except those blue units owned by the country\n\tnamed \"Georgia\" and the units in the group named \"Hawg1\".\n\n\n\t\t\t{'[g]arty1', '[g]arty2', '[-u]arty1_AD', '[-u]arty2_AD', 'Shark 11' }\n\n\tEvaluates to the unit named \"Shark 11\", plus all the units in groups named\n\t\"arty1\" and \"arty2\" except those that are named \"arty1\\_AD\" and \"arty2\\_AD\".\n\n\t@table UnitNameTable\n\t]]\n\n\t--- Returns a table containing unit names.\n\t-- @tparam table tbl sequential strings\n\t-- @treturn table @{UnitNameTable}\n\tfunction mist.makeUnitTable(tbl, exclude)\n\t\t--Assumption: will be passed a table of strings, sequential\n\t\t--log:info(tbl)\n        \n        \n        local excludeType = {}\n        if exclude then\n            if type(exclude) == 'table' then\n                for x, y in pairs(exclude) do\n                    excludeType[x] = true\n                    excludeType[y] = true\n                end\n            else\n                excludeType[exclude] = true\n            end\n        \n        end\n        \n        \n\t\tlocal units_by_name = {}\n\n\t\tlocal l_munits = mist.DBs.units\t--local reference for faster execution\n\t\tfor i = 1, #tbl do\n\t\t\tlocal unit = tbl[i]\n\t\t\tif unit:sub(1,4) == '[-u]' then --subtract a unit\n\t\t\t\tif units_by_name[unit:sub(5)] then -- 5 to end\n\t\t\t\tunits_by_name[unit:sub(5)] = nil\t--remove\n\t\t\tend\n\t\telseif unit:sub(1,3) == '[g]' then -- add a group\n\t\t\tfor coa, coa_tbl in pairs(l_munits) do\n\t\t\t\tfor country, country_table in pairs(coa_tbl) do\n\t\t\t\t\tfor unit_type, unit_type_tbl in pairs(country_table) do\n\t\t\t\t\t\tif type(unit_type_tbl) == 'table' then\n\t\t\t\t\t\t\tfor group_ind, group_tbl in pairs(unit_type_tbl) do\n\t\t\t\t\t\t\t\tif type(group_tbl) == 'table' and group_tbl.groupName == unit:sub(4) then\n\t\t\t\t\t\t\t\t\t-- index 4 to end\n\t\t\t\t\t\t\t\t\tfor unit_ind, unit in pairs(group_tbl.units) do\n\t\t\t\t\t\t\t\t\t\tunits_by_name[unit.unitName] = true\t--add\n\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\tend\n\t\t\t\t\t\tend\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\telseif unit:sub(1,4) == '[-g]' then -- subtract a group\n\t\t\tfor coa, coa_tbl in pairs(l_munits) do\n\t\t\t\tfor country, country_table in pairs(coa_tbl) do\n\t\t\t\t\tfor unit_type, unit_type_tbl in pairs(country_table) do\n\t\t\t\t\t\tif type(unit_type_tbl) == 'table' then\n\t\t\t\t\t\t\tfor group_ind, group_tbl in pairs(unit_type_tbl) do\n\t\t\t\t\t\t\t\tif type(group_tbl) == 'table' and group_tbl.groupName == unit:sub(5) then\n\t\t\t\t\t\t\t\t\t-- index 5 to end\n\t\t\t\t\t\t\t\t\tfor unit_ind, unit in pairs(group_tbl.units) do\n\t\t\t\t\t\t\t\t\t\tif units_by_name[unit.unitName] then\n\t\t\t\t\t\t\t\t\t\t\tunits_by_name[unit.unitName] = nil --remove\n\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\tend\n\t\t\t\t\t\tend\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\telseif unit:sub(1,3) == '[c]' then -- add a country\n\t\t\tlocal category = ''\n\t\t\tlocal country_start = 4\n\t\t\tif unit:sub(4,15) == '[helicopter]' then\n\t\t\t\tcategory = 'helicopter'\n\t\t\t\tcountry_start = 16\n\t\t\telseif unit:sub(4,10) == '[plane]' then\n\t\t\t\tcategory = 'plane'\n\t\t\t\tcountry_start = 11\n\t\t\telseif unit:sub(4,9) == '[ship]' then\n\t\t\t\tcategory = 'ship'\n\t\t\t\tcountry_start = 10\n\t\t\telseif unit:sub(4,12) == '[vehicle]' then\n\t\t\t\tcategory = 'vehicle'\n\t\t\t\tcountry_start = 13\n            elseif unit:sub(4, 11) == '[static]' then\n\t\t\t\tcategory = 'static'\n                country_start = 12\n\t\t\tend\n\t\t\tfor coa, coa_tbl in pairs(l_munits) do\n\t\t\t\tfor country, country_table in pairs(coa_tbl) do\n\t\t\t\t\tif country == string.lower(unit:sub(country_start)) then\t -- match\n\t\t\t\t\t\tfor unit_type, unit_type_tbl in pairs(country_table) do\n\t\t\t\t\t\t\tif type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then\n\t\t\t\t\t\t\t\tfor group_ind, group_tbl in pairs(unit_type_tbl) do\n\t\t\t\t\t\t\t\t\tif type(group_tbl) == 'table' then\n\t\t\t\t\t\t\t\t\t\tfor unit_ind, unit in pairs(group_tbl.units) do\n\t\t\t\t\t\t\t\t\t\t\tunits_by_name[unit.unitName] = true\t--add\n\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\tend\n\t\t\t\t\t\tend\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\telseif unit:sub(1,4) == '[-c]' then -- subtract a country\n\t\t\tlocal category = ''\n\t\t\tlocal country_start = 5\n\t\t\tif unit:sub(5,16) == '[helicopter]' then\n\t\t\t\tcategory = 'helicopter'\n\t\t\t\tcountry_start = 17\n\t\t\telseif unit:sub(5,11) == '[plane]' then\n\t\t\t\tcategory = 'plane'\n\t\t\t\tcountry_start = 12\n\t\t\telseif unit:sub(5,10) == '[ship]' then\n\t\t\t\tcategory = 'ship'\n\t\t\t\tcountry_start = 11\n\t\t\telseif unit:sub(5,13) == '[vehicle]' then\n\t\t\t\tcategory = 'vehicle'\n\t\t\t\tcountry_start = 14\n            elseif unit:sub(5, 12) == '[static]' then\n\t\t\t\tcategory = 'static'\n                country_start = 13\n\t\t\tend\n\t\t\tfor coa, coa_tbl in pairs(l_munits) do\n\t\t\t\tfor country, country_table in pairs(coa_tbl) do\n\t\t\t\t\tif country == string.lower(unit:sub(country_start)) then\t -- match\n\t\t\t\t\t\tfor unit_type, unit_type_tbl in pairs(country_table) do\n\t\t\t\t\t\t\tif type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type]  then\n\t\t\t\t\t\t\t\tfor group_ind, group_tbl in pairs(unit_type_tbl) do\n\t\t\t\t\t\t\t\t\tif type(group_tbl) == 'table' then\n\t\t\t\t\t\t\t\t\t\tfor unit_ind, unit in pairs(group_tbl.units) do\n\t\t\t\t\t\t\t\t\t\t\tif units_by_name[unit.unitName] then\n\t\t\t\t\t\t\t\t\t\t\t\tunits_by_name[unit.unitName] = nil\t--remove\n\t\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\tend\n\t\t\t\t\t\tend\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\telseif unit:sub(1,6) ==\t'[blue]' then -- add blue coalition\n\t\t\tlocal category = ''\n\t\t\tif unit:sub(7) == '[helicopter]' then\n\t\t\t\tcategory = 'helicopter'\n\t\t\telseif unit:sub(7) == '[plane]' then\n\t\t\t\tcategory = 'plane'\n\t\t\telseif unit:sub(7) == '[ship]' then\n\t\t\t\tcategory = 'ship'\n\t\t\telseif unit:sub(7) == '[vehicle]' then\n\t\t\t\tcategory = 'vehicle'\n            elseif unit:sub(7) == '[static]'  then\n\t\t\t\tcategory = 'static'\n            end\n\t\t\tfor coa, coa_tbl in pairs(l_munits) do\n\t\t\t\tif coa == 'blue' then\n\t\t\t\t\tfor country, country_table in pairs(coa_tbl) do\n\t\t\t\t\t\tfor unit_type, unit_type_tbl in pairs(country_table) do\n\t\t\t\t\t\t\tif type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type]  then\n\t\t\t\t\t\t\t\tfor group_ind, group_tbl in pairs(unit_type_tbl) do\n\t\t\t\t\t\t\t\t\tif type(group_tbl) == 'table' then\n\t\t\t\t\t\t\t\t\t\tfor unit_ind, unit in pairs(group_tbl.units) do\n\t\t\t\t\t\t\t\t\t\t\tunits_by_name[unit.unitName] = true\t--add\n\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\tend\n\t\t\t\t\t\tend\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\telseif unit:sub(1,7) == '[-blue]' then -- subtract blue coalition\n\t\t\tlocal category = ''\n\t\t\tif unit:sub(8) == '[helicopter]' then\n\t\t\t\tcategory = 'helicopter'\n\t\t\telseif unit:sub(8) == '[plane]' then\n\t\t\t\tcategory = 'plane'\n\t\t\telseif unit:sub(8) == '[ship]' then\n\t\t\t\tcategory = 'ship'\n\t\t\telseif unit:sub(8) == '[vehicle]' then\n\t\t\t\tcategory = 'vehicle'\n            elseif unit:sub(8) == '[static]' then\n\t\t\t\tcategory = 'static'\n\t\t\tend\n\t\t\tfor coa, coa_tbl in pairs(l_munits) do\n\t\t\t\tif coa == 'blue' then\n\t\t\t\t\tfor country, country_table in pairs(coa_tbl) do\n\t\t\t\t\t\tfor unit_type, unit_type_tbl in pairs(country_table) do\n\t\t\t\t\t\t\tif type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then\n\t\t\t\t\t\t\t\tfor group_ind, group_tbl in pairs(unit_type_tbl) do\n\t\t\t\t\t\t\t\t\tif type(group_tbl) == 'table' then\n\t\t\t\t\t\t\t\t\t\tfor unit_ind, unit in pairs(group_tbl.units) do\n\t\t\t\t\t\t\t\t\t\t\tif units_by_name[unit.unitName] then\n\t\t\t\t\t\t\t\t\t\t\t\tunits_by_name[unit.unitName] = nil\t--remove\n\t\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\tend\n\t\t\t\t\t\tend\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\telseif unit:sub(1,5) == '[red]' then -- add red coalition\n\t\t\tlocal category = ''\n\t\t\tif unit:sub(6) == '[helicopter]' then\n\t\t\t\tcategory = 'helicopter'\n\t\t\telseif unit:sub(6) == '[plane]' then\n\t\t\t\tcategory = 'plane'\n\t\t\telseif unit:sub(6) == '[ship]' then\n\t\t\t\tcategory = 'ship'\n\t\t\telseif unit:sub(6) == '[vehicle]' then\n\t\t\t\tcategory = 'vehicle'\n            elseif unit:sub(6) == '[static]'  then\n\t\t\t\tcategory = 'static'\n\t\t\tend\n\t\t\tfor coa, coa_tbl in pairs(l_munits) do\n\t\t\t\tif coa == 'red' then\n\t\t\t\t\tfor country, country_table in pairs(coa_tbl) do\n\t\t\t\t\t\tfor unit_type, unit_type_tbl in pairs(country_table) do\n\t\t\t\t\t\t\tif type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then\n\t\t\t\t\t\t\t\tfor group_ind, group_tbl in pairs(unit_type_tbl) do\n\t\t\t\t\t\t\t\t\tif type(group_tbl) == 'table' then\n\t\t\t\t\t\t\t\t\t\tfor unit_ind, unit in pairs(group_tbl.units) do\n\t\t\t\t\t\t\t\t\t\t\tunits_by_name[unit.unitName] = true\t--add\n\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\tend\n\t\t\t\t\t\tend\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\telseif unit:sub(1,6) == '[-red]' then -- subtract red coalition\n\t\t\tlocal category = ''\n\t\t\tif unit:sub(7) == '[helicopter]' then\n\t\t\t\tcategory = 'helicopter'\n\t\t\telseif unit:sub(7) == '[plane]' then\n\t\t\t\tcategory = 'plane'\n\t\t\telseif unit:sub(7) == '[ship]' then\n\t\t\t\tcategory = 'ship'\n\t\t\telseif unit:sub(7) == '[vehicle]' then\n\t\t\t\tcategory = 'vehicle'\n            elseif unit:sub(7) == '[static]'  then\n\t\t\t\tcategory = 'static'\n\t\t\tend\n\t\t\tfor coa, coa_tbl in pairs(l_munits) do\n\t\t\t\tif coa == 'red' then\n\t\t\t\t\tfor country, country_table in pairs(coa_tbl) do\n\t\t\t\t\t\tfor unit_type, unit_type_tbl in pairs(country_table) do\n\t\t\t\t\t\t\tif type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then\n\t\t\t\t\t\t\t\tfor group_ind, group_tbl in pairs(unit_type_tbl) do\n\t\t\t\t\t\t\t\t\tif type(group_tbl) == 'table' then\n\t\t\t\t\t\t\t\t\t\tfor unit_ind, unit in pairs(group_tbl.units) do\n\t\t\t\t\t\t\t\t\t\t\tif units_by_name[unit.unitName] then\n\t\t\t\t\t\t\t\t\t\t\t\tunits_by_name[unit.unitName] = nil\t--remove\n\t\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\tend\n\t\t\t\t\t\tend\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\telseif unit:sub(1,5) == '[all]' then -- add all of a certain category (or all categories)\n\t\t\tlocal category = ''\n\t\t\tif unit:sub(6) == '[helicopter]' then\n\t\t\t\tcategory = 'helicopter'\n\t\t\telseif unit:sub(6) == '[plane]' then\n\t\t\t\tcategory = 'plane'\n\t\t\telseif unit:sub(6) == '[ship]' then\n\t\t\t\tcategory = 'ship'\n\t\t\telseif unit:sub(6) == '[vehicle]' then\n\t\t\t\tcategory = 'vehicle'\n            elseif unit:sub(6) == '[static]' then\n\t\t\t\tcategory = 'static'\n\t\t\tend\n\t\t\tfor coa, coa_tbl in pairs(l_munits) do\n\t\t\t\tfor country, country_table in pairs(coa_tbl) do\n\t\t\t\t\tfor unit_type, unit_type_tbl in pairs(country_table) do\n\t\t\t\t\t\tif type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then\n\t\t\t\t\t\t\tfor group_ind, group_tbl in pairs(unit_type_tbl) do\n\t\t\t\t\t\t\t\tif type(group_tbl) == 'table' then\n\t\t\t\t\t\t\t\t\tfor unit_ind, unit in pairs(group_tbl.units) do\n\t\t\t\t\t\t\t\t\t\tunits_by_name[unit.unitName] = true\t--add\n\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\tend\n\t\t\t\t\t\tend\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\telseif unit:sub(1,6) == '[-all]' then -- subtract all of a certain category (or all categories)\n\t\t\tlocal category = ''\n\t\t\tif unit:sub(7) == '[helicopter]' then\n\t\t\t\tcategory = 'helicopter'\n\t\t\telseif unit:sub(7) == '[plane]' then\n\t\t\t\tcategory = 'plane'\n\t\t\telseif unit:sub(7) == '[ship]' then\n\t\t\t\tcategory = 'ship'\n\t\t\telseif unit:sub(7) == '[vehicle]' then\n\t\t\t\tcategory = 'vehicle'\n            elseif unit:sub(7) == '[static]'  then\n\t\t\t\tcategory = 'static'\n\t\t\tend\n\t\t\tfor coa, coa_tbl in pairs(l_munits) do\n\t\t\t\tfor country, country_table in pairs(coa_tbl) do\n\t\t\t\t\tfor unit_type, unit_type_tbl in pairs(country_table) do\n\t\t\t\t\t\tif type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then\n\t\t\t\t\t\t\tfor group_ind, group_tbl in pairs(unit_type_tbl) do\n\t\t\t\t\t\t\t\tif type(group_tbl) == 'table' then\n\t\t\t\t\t\t\t\t\tfor unit_ind, unit in pairs(group_tbl.units) do\n\t\t\t\t\t\t\t\t\t\tif units_by_name[unit.unitName] then\n\t\t\t\t\t\t\t\t\t\t\tunits_by_name[unit.unitName] = nil\t--remove\n\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\tend\n\t\t\t\t\t\tend\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\telse -- just a regular unit\n\t\t\tunits_by_name[unit] = true\t--add\n\t\tend\n\tend\n\n\tlocal units_tbl = {}\t-- indexed sequentially\n\tfor unit_name, val in pairs(units_by_name) do\n\t\tif val then\n\t\t\tunits_tbl[#units_tbl + 1] = unit_name\t-- add all the units to the table\n\t\tend\n\tend\n\n\n\tunits_tbl.processed = timer.getTime()\t--add the processed flag\n\treturn units_tbl\nend\n\nfunction mist.getUnitsByAttribute(att, rnum, id)\n    local cEntry = {}\n    cEntry.typeName = att.type or att.typeName or att.typename\n    cEntry.country = att.country\n    cEntry.coalition = att.coalition\n    cEntry.skill = att.skill\n    cEntry.categry = att.category\n    \n    local num = rnum or 1\n    \n    if cEntry.skill == 'human' then\n        cEntry.skill = {'Client', 'Player'}\n    end\n    \n\n    local checkedVal = {}\n    local units = {}\n    for uName, uData in pairs(mist.DBs.unitsByName) do\n        local matched = 0\n        for cName, cVal in pairs(cEntry) do\n            if type(cVal) == 'table' then\n                for sName, sVal in pairs(cVal) do\n                    if (uData[cName] and uData[cName] == sVal) or (uData[cName] and uData[cName] == sName) then\n                         matched = matched + 1\n                    end\n                end\n            else\n                if uData[cName] and uData[cName] == cVal then\n                    matched = matched + 1\n                end\n            end\n        end\n        if matched >= num then\n            if id then \n                units[uData.unitId] = true\n            else\n            \n                units[uName] = true\n            end\n        end\n    end\n    \n    local rtn = {}\n    for name, _ in pairs(units) do\n        table.insert(rtn, name)\n    end\n    return rtn\n    \nend\n\nfunction mist.getGroupsByAttribute(att, rnum, id)\n    local cEntry = {}\n    cEntry.typeName = att.type or att.typeName or att.typename\n    cEntry.country = att.country\n    cEntry.coalition = att.coalition\n    cEntry.skill = att.skill\n    cEntry.categry = att.category\n    \n    local num = rnum or 1\n    \n    if cEntry.skill == 'human' then\n        cEntry.skill = {'Client', 'Player'}\n    end\n    local groups = {}\n    for gName, gData in pairs(mist.DBs.groupsByName) do\n        local matched = 0\n        for cName, cVal in pairs(cEntry) do\n            if type(cVal) == 'table' then\n                for sName, sVal in pairs(cVal) do\n                    if cName == 'skill' or cName == 'typeName' then \n                        local lMatch = 0\n                        for uId, uData in pairs(gData.units) do\n                            if (uData[cName] and uData[cName] == sVal) or (gData[cName] and gData[cName] == sName) then\n                                lMatch = lMatch + 1\n                                break\n                            end\n                        end\n                        if lMatch > 0 then  \n                            matched = matched + 1                    \n                        end\n                    end\n                    if (gData[cName] and gData[cName] == sVal) or (gData[cName] and gData[cName] == sName) then\n                         matched = matched + 1\n                        break\n                    end\n                end\n            else\n                if cName == 'skill' or cName == 'typeName' then\n                    local lMatch = 0\n                    for uId, uData in pairs(gData.units) do\n                        if (uData[cName] and uData[cName] == sVal) then\n                            lMatch = lMatch + 1\n                            break\n                        end\n                    end\n                    if lMatch > 0 then  \n                        matched = matched + 1                    \n                    end\n                end\n                if gData[cName] and gData[cName] == cVal then\n                    matched = matched + 1\n                end\n            end\n        end\n        if matched >= num then\n            if id then \n                groups[gData.groupid] = true\n            else\n                groups[gName] = true\n            end\n        end\n    end\n    local rtn = {}\n    for name, _ in pairs(groups) do\n        table.insert(rtn, name)\n    end\n    return rtn\n    \nend\n\nfunction mist.getDeadMapObjsInZones(zone_names)\n\t-- zone_names: table of zone names\n\t-- returns: table of dead map objects (indexed numerically)\n\tlocal map_objs = {}\n\tlocal zones = {}\n\tfor i = 1, #zone_names do\n\t\tif mist.DBs.zonesByName[zone_names[i]] then\n\t\t\tzones[#zones + 1] = mist.DBs.zonesByName[zone_names[i]]\n\t\tend\n\tend\n\tfor obj_id, obj in pairs(mist.DBs.deadObjects) do\n\t\tif obj.objectType and obj.objectType == 'building' then --dead map object\n\t\t\tfor i = 1, #zones do\n\t\t\t\tif ((zones[i].point.x - obj.objectPos.x)^2 + (zones[i].point.z - obj.objectPos.z)^2)^0.5 <= zones[i].radius then\n\t\t\t\t\tmap_objs[#map_objs + 1] = mist.utils.deepCopy(obj)\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\tend\n\treturn map_objs\nend\n\nfunction mist.getDeadMapObjsInPolygonZone(zone)\n\t-- zone_names: table of zone names\n\t-- returns: table of dead map objects (indexed numerically)\n\tlocal map_objs = {}\n\tfor obj_id, obj in pairs(mist.DBs.deadObjects) do\n\t\tif obj.objectType and obj.objectType == 'building' then --dead map object\n\t\t\tif mist.pointInPolygon(obj.objectPos, zone) then\n\t\t\t\tmap_objs[#map_objs + 1] = mist.utils.deepCopy(obj)\n\t\t\tend\n\t\tend\n\tend\n\treturn map_objs\nend\nmist.shape = {}\nfunction mist.shape.insideShape(shape1, shape2, full)\n    if shape1.radius then -- probably a circle\n        if shape2.radius then\n             return mist.shape.circleInCircle(shape1, shape2, full)\n        elseif shape2[1] then\n             return mist.shape.circleInPoly(shape1, shape2, full)\n        end\n    \n    elseif shape1[1] then -- shape1 is probably a polygon\n        if shape2.radius then\n            return  mist.shape.polyInCircle(shape1, shape2, full)\n        elseif shape2[1] then\n            return  mist.shape.polyInPoly(shape1, shape2, full)\n        end\n    end\n    return false\nend\n\nfunction mist.shape.circleInCircle(c1, c2, full)\n    if not full then -- quick partial check\n        if mist.utils.get2DDist(c1.point, c2.point) <= c2.radius then\n            return true\n        end\n    end\n    local theta = mist.utils.getHeadingPoints(c2.point, c1.point) -- heading from \n    if full then\n        return  mist.utils.get2DDist(mist.projectPoint(c1.point, c1.radius, theta), c2.point) <= c2.radius\n    else\n        return mist.utils.get2DDist(mist.projectPoint(c1.point, c1.radius, theta + math.pi), c2.point) <= c2.radius\n    end\n    return false\nend\n\n\nfunction mist.shape.circleInPoly(circle, poly, full) \n\n    if poly and type(poly) == 'table' and circle and type(circle) == 'table' and circle.radius and circle.point then\n        if not full then \n            for i = 1, #poly do\n                if mist.utils.get2DDist(circle.point, poly[i]) <= circle.radius then\n                    return true\n                end\n            end\n        end\n        -- no point is inside of the zone, now check if any part is\n        local count = 0\n        for i = 1, #poly do\n            local theta -- heading of each set of points\n            if i == #poly then\n                theta = mist.utils.getHeadingPoints(poly[i],poly[1])\n            else\n                theta = mist.utils.getHeadingPoints(poly[i],poly[i+1])\n            end\n            -- offset \n            local pPoint = mist.projectPoint(circle.point, circle.radius, theta - (math.pi/180))\n            local oPoint = mist.projectPoint(circle.point, circle.radius, theta + (math.pi/180))\n\n           \n            if mist.pointInPolygon(pPoint, poly) == true then\n                 if (full and mist.pointInPolygon(oPoint, poly) == true) or not full then\n                    return true\n                \n                end\n               \n            end\n        end      \n        \n    end\n    return false\nend\n\n\nfunction mist.shape.polyInPoly(p1, p2, full)\n    local count = 0\n    for i = 1, #p1 do\n        \n        if mist.pointInPolygon(p1[i], p2) then\n            count = count + 1\n        end\n        if (not full) and count > 0 then\n            return true\n        end\n    end\n    if count == #p1 then\n        return true\n    end\n    \n    return false\nend\n\nfunction mist.shape.polyInCircle(poly, circle, full)\n        local count = 0\n        for i = 1, #poly do\n            if mist.utils.get2DDist(circle.point, poly[i]) <= circle.radius then\n                if full then\n                    count = count + 1\n                else\n                   return true\n                end\n            end\n        end\n        if count == #poly then\n            return true\n        end\n\n    return false\nend\n\nfunction mist.shape.getPointOnSegment(point, seg, isSeg)\n    local p = mist.utils.makeVec2(point)\n    local s1 = mist.utils.makeVec2(seg[1])\n    local s2 = mist.utils.makeVec2(seg[2])\n    \n    \n    local cx, cy = p.x - s1.x, p.y - s1.y\n    local dx, dy = s2.x - s1.x, s2.x - s1.y\n    local d = (dx*dx + dy*dy)\n      \n    if d == 0 then\n       return {x = s1.x, y = s1.y}\n    end\n    local u = (cx*dx + cy*dy)/d\n    if isSeg then \n       if u < 0 then\n            u = 0\n        elseif u > 1 then\n            u = 1\n        end\n    end\n    return {x = s1.x + u*dx, y = s1.y + u*dy}\nend\n\n\nfunction mist.shape.segmentIntersect(segA, segB)\n    local dx1, dy1 = segA[2].x - segA[1].x, segA[2] - segA[1].y\n    local dx2, dy2 = segB[2].x - segB[1].x, segB[2] - segB[1].y\n    local dx3, dy3 = segA[1].x - segB[1].x, segA[1].y - segB[1].y\n    local d = dx1*dy2 - dy1*dx2\n    if d == 0 then\n       return false\n    end\n    local t1 = (dx2*dy3 - dy2*dx3)/d\n    if t1 < 0 or t1 > 1 then\n      return false\n    end\n    local t2 = (dx1*dy3 - dy1*dx3)/d\n    if t2 < 0 or t2 > 1 then\n      return false\n    end\n      -- point of intersection\n      return true, segA[1].x + t1*dx1, segA[1].y + t1*dy1\nend\n\n\nfunction mist.pointInPolygon(point, poly, maxalt) --raycasting point in polygon. Code from http://softsurfer.com/Archive/algorithm_0103/algorithm_0103.htm\n\t--[[local type_tbl = {\n\t\tpoint = {'table'},\n\t\tpoly = {'table'},\n\t\tmaxalt = {'number', 'nil'},\n\t\t}\n\n\tlocal err, errmsg = mist.utils.typeCheck('mist.pointInPolygon', type_tbl, {point, poly, maxalt})\n\tassert(err, errmsg)\n\t]]\n\tpoint = mist.utils.makeVec3(point)\n\tlocal px = point.x\n\tlocal pz = point.z\n\tlocal cn = 0\n\tlocal newpoly = mist.utils.deepCopy(poly)\n\n\tif not maxalt or (point.y <= maxalt) then\n\t\tlocal polysize = #newpoly\n\t\tnewpoly[#newpoly + 1] = newpoly[1]\n\n\t\tnewpoly[1] = mist.utils.makeVec3(newpoly[1])\n\n\t\tfor k = 1, polysize do\n\t\t\tnewpoly[k+1] = mist.utils.makeVec3(newpoly[k+1])\n\t\t\tif ((newpoly[k].z <= pz) and (newpoly[k+1].z > pz)) or ((newpoly[k].z > pz) and (newpoly[k+1].z <= pz)) then\n\t\t\t\tlocal vt = (pz - newpoly[k].z) / (newpoly[k+1].z - newpoly[k].z)\n\t\t\t\tif (px < newpoly[k].x + vt*(newpoly[k+1].x - newpoly[k].x)) then\n\t\t\t\t\tcn = cn + 1\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\n\t\treturn cn%2 == 1\n\telse\n\t\treturn false\n\tend\nend\n\nfunction mist.mapValue(val, inMin, inMax, outMin, outMax)\n     return (val - inMin) * (outMax - outMin) / (inMax - inMin) + outMin\nend\n\nfunction mist.getUnitsInPolygon(unit_names, polyZone, max_alt)\n\tlocal units = {}\n\n\tfor i = 1, #unit_names do\n\t\tunits[#units + 1] = Unit.getByName(unit_names[i]) or StaticObject.getByName(unit_names[i])\n\tend\n\n\tlocal inZoneUnits = {}\n\tfor i =1, #units do\n\t\tlocal lUnit = units[i]\n        local lCat = lUnit:getCategory()\n        if ((lCat == 1 and lUnit:isActive()) or lCat ~= 1) and mist.pointInPolygon(lUnit:getPosition().p, polyZone, max_alt) then\n\t\t\tinZoneUnits[#inZoneUnits + 1] = lUnit\n\t\tend\n\tend\n\n\treturn inZoneUnits\nend\n\nfunction mist.getUnitsInZones(unit_names, zone_names, zone_type)\n    zone_type = zone_type or 'cylinder'\n\tif zone_type == 'c' or zone_type == 'cylindrical' or zone_type == 'C' then\n\t\tzone_type = 'cylinder'\n\tend\n\tif zone_type == 's' or zone_type == 'spherical' or zone_type == 'S' then\n\t\tzone_type = 'sphere'\n\tend\n\n\tassert(zone_type == 'cylinder' or zone_type == 'sphere', 'invalid zone_type: ' .. tostring(zone_type))\n\n\tlocal units = {}\n\tlocal zones = {}\n    \n    if zone_names and type(zone_names) == 'string' then\n        zone_names = {zone_names}\n    end\n\tfor k = 1, #unit_names do\n\t\t\n        local unit = Unit.getByName(unit_names[k]) or StaticObject.getByName(unit_names[k])\n\t\tif unit then\n\t\t\tunits[#units + 1] = unit\n\t\tend\n\tend\n\n\n\tfor k = 1, #zone_names do\n\t\tlocal zone = mist.DBs.zonesByName[zone_names[k]]\n\t\tif zone then\n\t\t\tzones[#zones + 1] = {radius = zone.radius, x = zone.point.x, y = zone.point.y, z = zone.point.z, verts = zone.verticies}\n\t\tend\n\tend\n\n\tlocal in_zone_units = {}\n\tfor units_ind = 1, #units do\n        local lUnit = units[units_ind]\n        local unit_pos = lUnit:getPosition().p\n        local lCat = lUnit:getCategory()\n        for zones_ind = 1, #zones do\n\t\t\tif zone_type == 'sphere' then\t--add land height value for sphere zone type\n\t\t\t\tlocal alt = land.getHeight({x = zones[zones_ind].x, y = zones[zones_ind].z})\n\t\t\t\tif alt then\n\t\t\t\t\tzones[zones_ind].y = alt\n\t\t\t\tend\n\t\t\tend\n\n            if unit_pos and ((lCat == 1 and lUnit:isActive() == true) or lCat ~= 1) then -- it is a unit and is active or it is not a unit\n\t\t\t\tif zones[zones_ind].verts  then\n                    if mist.pointInPolygon(unit_pos, zones[zones_ind].verts) then\n                        in_zone_units[#in_zone_units + 1] = lUnit\n                    end\n\n                else\n                    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\n                        in_zone_units[#in_zone_units + 1] = lUnit\n                        break\n                    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\n                        in_zone_units[#in_zone_units + 1] = lUnit\n                        break\n                    end\n                end\n\t\t\tend\n\t\tend\n\tend\n\treturn in_zone_units\nend\n\nfunction mist.getUnitsInMovingZones(unit_names, zone_unit_names, radius, zone_type)\n\n\tzone_type = zone_type or 'cylinder'\n\tif zone_type == 'c' or zone_type == 'cylindrical' or zone_type == 'C' then\n\t\tzone_type = 'cylinder'\n\tend\n\tif zone_type == 's' or zone_type == 'spherical' or zone_type == 'S' then\n\t\tzone_type = 'sphere'\n\tend\n\n\tassert(zone_type == 'cylinder' or zone_type == 'sphere', 'invalid zone_type: ' .. tostring(zone_type))\n\n\tlocal units = {}\n\tlocal zone_units = {}\n\n\tfor k = 1, #unit_names do\n\t\tlocal unit = Unit.getByName(unit_names[k]) or StaticObject.getByName(unit_names[k])\n\t\tif unit then\n\t\t\tunits[#units + 1] = unit\n\t\tend\n\tend\n\n\tfor k = 1, #zone_unit_names do\n\t\tlocal unit = Unit.getByName(zone_unit_names[k]) or StaticObject.getByName(zone_unit_names[k])\n\t\tif unit then\n\t\t\tzone_units[#zone_units + 1] = unit\n\t\tend\n\tend\n\n\tlocal in_zone_units = {}\n\n\tfor units_ind = 1, #units do\n        local lUnit = units[units_ind]\n        local lCat = lUnit:getCategory()\n        local unit_pos = lUnit:getPosition().p\n\t\tfor zone_units_ind = 1, #zone_units do\n\t\t\t\n\t\t\tlocal zone_unit_pos = zone_units[zone_units_ind]:getPosition().p\n\t\t\tif unit_pos and zone_unit_pos and ((lCat == 1 and lUnit:isActive()) or lCat ~= 1) then\n\t\t\t\tif zone_type == 'cylinder' and (((unit_pos.x - zone_unit_pos.x)^2 + (unit_pos.z - zone_unit_pos.z)^2)^0.5 <= radius) then\n\t\t\t\t\tin_zone_units[#in_zone_units + 1] = lUnit\n\t\t\t\t\tbreak\n\t\t\t\telseif 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\n\t\t\t\t\tin_zone_units[#in_zone_units + 1] = lUnit\n\t\t\t\t\tbreak\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\tend\n\treturn in_zone_units\nend\n\nfunction mist.getUnitsLOS(unitset1, altoffset1, unitset2, altoffset2, radius)\n\tlog:info(\"$1, $2, $3, $4, $5\", unitset1, altoffset1, unitset2, altoffset2, radius)\n\tradius = radius or math.huge\n\tlocal unit_info1 = {}\n\tlocal unit_info2 = {}\n\n\t-- get the positions all in one step, saves execution time.\n\tfor unitset1_ind = 1, #unitset1 do\n\t\tlocal unit1 = Unit.getByName(unitset1[unitset1_ind])\n        local lCat = unit1:getCategory()\n\t\tif unit1 and ((lCat == 1 and unit1:isActive()) or lCat ~= 1) then\n\t\t\tunit_info1[#unit_info1 + 1] = {}\n\t\t\tunit_info1[#unit_info1].unit = unit1\n\t\t\tunit_info1[#unit_info1].pos\t= unit1:getPosition().p\n\t\tend\n\tend\n\n\tfor unitset2_ind = 1, #unitset2 do\n\t\tlocal unit2 = Unit.getByName(unitset2[unitset2_ind])\n        local lCat = unit2:getCategory()\n\t\tif unit2 and ((lCat == 1 and unit2:isActive()) or lCat ~= 1) then\n\t\t\tunit_info2[#unit_info2 + 1] = {}\n\t\t\tunit_info2[#unit_info2].unit = unit2\n\t\t\tunit_info2[#unit_info2].pos\t= unit2:getPosition().p\n\t\tend\n\tend\n\n\tlocal LOS_data = {}\n\t-- now compute los\n\tfor unit1_ind = 1, #unit_info1 do\n\t\tlocal unit_added = false\n\t\tfor unit2_ind = 1, #unit_info2 do\n\t\t\tif radius == math.huge or (mist.vec.mag(mist.vec.sub(unit_info1[unit1_ind].pos, unit_info2[unit2_ind].pos)) < radius) then -- inside radius\n\t\t\t\tlocal point1 = { x = unit_info1[unit1_ind].pos.x, y = unit_info1[unit1_ind].pos.y + altoffset1, z = unit_info1[unit1_ind].pos.z}\n\t\t\t\tlocal point2 = { x = unit_info2[unit2_ind].pos.x, y = unit_info2[unit2_ind].pos.y + altoffset2, z = unit_info2[unit2_ind].pos.z}\n\t\t\t\tif land.isVisible(point1, point2) then\n\t\t\t\t\tif unit_added == false then\n\t\t\t\t\t\tunit_added = true\n\t\t\t\t\t\tLOS_data[#LOS_data + 1] = {}\n\t\t\t\t\t\tLOS_data[#LOS_data].unit = unit_info1[unit1_ind].unit\n\t\t\t\t\t\tLOS_data[#LOS_data].vis = {}\n\t\t\t\t\t\tLOS_data[#LOS_data].vis[#LOS_data[#LOS_data].vis + 1] = unit_info2[unit2_ind].unit\n\t\t\t\t\telse\n\t\t\t\t\t\tLOS_data[#LOS_data].vis[#LOS_data[#LOS_data].vis + 1] = unit_info2[unit2_ind].unit\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\tend\n\n\treturn LOS_data\nend\n\nfunction mist.getAvgPoint(points)\n\tlocal avgX, avgY, avgZ, totNum = 0, 0, 0, 0\n\tfor i = 1, #points do\n        --log:warn(points[i])\n        local nPoint = mist.utils.makeVec3(points[i])\n\t\tif nPoint.z then\n\t\t\tavgX = avgX + nPoint.x\n\t\t\tavgY = avgY + nPoint.y\n\t\t\tavgZ = avgZ + nPoint.z\n\t\t\ttotNum = totNum + 1\n\t\tend\n\tend\n\tif totNum ~= 0 then\n\t\treturn {x = avgX/totNum, y = avgY/totNum, z = avgZ/totNum}\n\tend\nend\n\n--Gets the average position of a group of units (by name)\nfunction mist.getAvgPos(unitNames)\n\tlocal avgX, avgY, avgZ, totNum = 0, 0, 0, 0\n\tfor i = 1, #unitNames do\n\t\tlocal unit\n\t\tif Unit.getByName(unitNames[i]) then\n\t\t\tunit = Unit.getByName(unitNames[i])\n\t\telseif StaticObject.getByName(unitNames[i]) then\n\t\t\tunit = StaticObject.getByName(unitNames[i])\n\t\tend\n\t\tif unit then\n\t\t\tlocal pos = unit:getPosition().p\n\t\t\tif pos then -- you never know O.o\n\t\t\t\tavgX = avgX + pos.x\n\t\t\t\tavgY = avgY + pos.y\n\t\t\t\tavgZ = avgZ + pos.z\n\t\t\t\ttotNum = totNum + 1\n\t\t\tend\n\t\tend\n\tend\n\tif totNum ~= 0 then\n\t\treturn {x = avgX/totNum, y = avgY/totNum, z = avgZ/totNum}\n\tend\nend\n\nfunction mist.getAvgGroupPos(groupName)\n\tif type(groupName) == 'string' and Group.getByName(groupName) and Group.getByName(groupName):isExist() == true then\n\t\tgroupName = Group.getByName(groupName)\n\tend\n\tlocal units = {}\n\tfor i = 1, groupName:getSize() do\n\t\ttable.insert(units, groupName:getUnit(i):getName())\n\tend\n\n\treturn mist.getAvgPos(units)\n\nend\n\n--[[ vars for mist.getMGRSString:\nvars.units - table of unit names (NOT unitNameTable- maybe this should change).\nvars.acc - integer between 0 and 5, inclusive\n]]\nfunction mist.getMGRSString(vars)\n\tlocal units = vars.units\n\tlocal acc = vars.acc or 5\n\tlocal avgPos = mist.getAvgPos(units)\n\tif avgPos then\n\t\treturn mist.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(avgPos)), acc)\n\tend\nend\n\n--[[ vars for mist.getLLString\nvars.units - table of unit names (NOT unitNameTable- maybe this should change).\nvars.acc - integer, number of numbers after decimal place\nvars.DMS - if true, output in degrees, minutes, seconds.\tOtherwise, output in degrees, minutes.\n]]\nfunction mist.getLLString(vars)\n\tlocal units = vars.units\n\tlocal acc = vars.acc or 3\n\tlocal DMS = vars.DMS\n\tlocal avgPos = mist.getAvgPos(units)\n\tif avgPos then\n\t\tlocal lat, lon = coord.LOtoLL(avgPos)\n\t\treturn mist.tostringLL(lat, lon, acc, DMS)\n\tend\nend\n\n--[[\nvars.units- table of unit names (NOT unitNameTable- maybe this should change).\nvars.ref -\tvec3 ref point, maybe overload for vec2 as well?\nvars.alt - boolean, if used, includes altitude in string\nvars.metric - boolean, gives distance in km instead of NM.\n]]\nfunction mist.getBRString(vars)\n\tlocal units = vars.units\n\tlocal ref = mist.utils.makeVec3(vars.ref, 0)\t-- turn it into Vec3 if it is not already.\n\tlocal alt = vars.alt\n\tlocal metric = vars.metric\n\tlocal avgPos = mist.getAvgPos(units)\n\tif avgPos then\n        local vec = {x = avgPos.x - ref.x, y = avgPos.y - ref.y, z = avgPos.z - ref.z}\n        local dir = mist.utils.getDir(vec, ref)\n        local dist = mist.utils.get2DDist(avgPos, ref)\n        if alt then\n            alt = avgPos.y\n        end\n        return mist.tostringBR(dir, dist, alt, metric)\n\tend\nend\n\n-- Returns the Vec3 coordinates of the average position of the concentration of units most in the heading direction.\n--[[ vars for mist.getLeadingPos:\nvars.units - table of unit names\nvars.heading - direction\nvars.radius - number\nvars.headingDegrees - boolean, switches heading to degrees\n]]\nfunction mist.getLeadingPos(vars)\n\tlocal units = vars.units\n\tlocal heading = vars.heading\n\tlocal radius = vars.radius\n\tif vars.headingDegrees then\n\t\theading = mist.utils.toRadian(vars.headingDegrees)\n\tend\n\n\tlocal unitPosTbl = {}\n\tfor i = 1, #units do\n\t\tlocal unit = Unit.getByName(units[i])\n\t\tif unit and unit:isExist() then\n\t\t\tunitPosTbl[#unitPosTbl + 1] = unit:getPosition().p\n\t\tend\n\tend\n    \n\tif #unitPosTbl > 0 then\t-- one more more units found.\n\t\t-- first, find the unit most in the heading direction\n\t\tlocal maxPos = -math.huge\n        heading = heading * -1 -- rotated value appears to be opposite of what was expected\n\t\tlocal maxPosInd\t-- maxPos - the furthest in direction defined by heading; maxPosInd =\n\t\tfor i = 1, #unitPosTbl do\n\t\t\tlocal rotatedVec2 = mist.vec.rotateVec2(mist.utils.makeVec2(unitPosTbl[i]), heading)\n\t\t\tif (not maxPos) or maxPos < rotatedVec2.x then\n\t\t\t\tmaxPos = rotatedVec2.x\n\t\t\t\tmaxPosInd = i\n\t\t\tend\n\t\tend\n\n\t\t--now, get all the units around this unit...\n\t\tlocal avgPos\n\t\tif radius then\n\t\t\tlocal maxUnitPos = unitPosTbl[maxPosInd]\n\t\t\tlocal avgx, avgy, avgz, totNum = 0, 0, 0, 0\n\t\t\tfor i = 1, #unitPosTbl do\n\t\t\t\tif mist.utils.get2DDist(maxUnitPos, unitPosTbl[i]) <= radius then\n\t\t\t\t\tavgx = avgx + unitPosTbl[i].x\n\t\t\t\t\tavgy = avgy + unitPosTbl[i].y\n\t\t\t\t\tavgz = avgz + unitPosTbl[i].z\n\t\t\t\t\ttotNum = totNum + 1\n\t\t\t\tend\n\t\t\tend\n\t\t\tavgPos = { x = avgx/totNum, y = avgy/totNum, z = avgz/totNum}\n\t\telse\n\t\t\tavgPos = unitPosTbl[maxPosInd]\n\t\tend\n\n\t\treturn avgPos\n\tend\nend\n\n--[[ vars for mist.getLeadingMGRSString:\nvars.units - table of unit names\nvars.heading - direction\nvars.radius - number\nvars.headingDegrees - boolean, switches heading to degrees\nvars.acc - number, 0 to 5.\n]]\nfunction mist.getLeadingMGRSString(vars)\n\tlocal pos = mist.getLeadingPos(vars)\n\tif pos then\n\t\tlocal acc = vars.acc or 5\n\t\treturn mist.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(pos)), acc)\n\tend\nend\n\n--[[ vars for mist.getLeadingLLString:\nvars.units - table of unit names\nvars.heading - direction, number\nvars.radius - number\nvars.headingDegrees - boolean, switches heading to degrees\nvars.acc - number of digits after decimal point (can be negative)\nvars.DMS -\tboolean, true if you want DMS.\n]]\nfunction mist.getLeadingLLString(vars)\n\tlocal pos = mist.getLeadingPos(vars)\n\tif pos then\n\t\tlocal acc = vars.acc or 3\n\t\tlocal DMS = vars.DMS\n\t\tlocal lat, lon = coord.LOtoLL(pos)\n\t\treturn mist.tostringLL(lat, lon, acc, DMS)\n\tend\nend\n\n--[[ vars for mist.getLeadingBRString:\nvars.units - table of unit names\nvars.heading - direction, number\nvars.radius - number\nvars.headingDegrees - boolean, switches heading to degrees\nvars.metric - boolean, if true, use km instead of NM.\nvars.alt - boolean, if true, include altitude.\nvars.ref - vec3/vec2 reference point.\n]]\nfunction mist.getLeadingBRString(vars)\n\tlocal pos = mist.getLeadingPos(vars)\n\tif pos then\n\t\tlocal ref = vars.ref\n\t\tlocal alt = vars.alt\n\t\tlocal metric = vars.metric\n\n\t\tlocal vec = {x = pos.x - ref.x, y = pos.y - ref.y, z = pos.z - ref.z}\n\t\tlocal dir = mist.utils.getDir(vec, ref)\n\t\tlocal dist = mist.utils.get2DDist(pos, ref)\n\t\tif alt then\n\t\t\talt = pos.y\n\t\tend\n\t\treturn mist.tostringBR(dir, dist, alt, metric)\n\tend\nend\n\n--[[getPathLength from GSH\n-- Returns the length between the defined set of points. Can also return the point index before the cutoff was achieved\np - table of path points, vec2 or vec3\ncutoff - number distance after which to stop at\ntopo  - boolean for if it should get the topographical distance\n\n]]\n\nfunction mist.getPathLength(p, cutoff, topo)\n    local l = 0\n    local cut = 0 or cutOff\n    local path = {}\n\n    for i = 1, #p do\n        if topo then\n            table.insert(path, mist.utils.makeVec3GL(p[i]))\n        else\n            table.insert(path, mist.utils.makeVec3(p[i]))\n        end\n    end\n    \n    for i = 1, #path do\n        if i + 1 <= #path then \n            if topo then \n                l = mist.utils.get3DDist(path[i], path[i+1]) + l\n            else\n                l = mist.utils.get2DDist(path[i], path[i+1]) + l\n            end\n        end\n        if cut ~= 0 and l > cut  then\n            return l, i\n        end\n    end\n    return l\nend\n\n--[[\nReturn a series of points to simplify the input table. Best used in conjunction with findPathOnRoads to turn the massive table into a list of X points. \np - table of path points, can be vec2 or vec3\nnum - number of segments. \nexact - boolean for whether or not it returns the exact distance or uses the first WP to that distance. \n\n\n]]\n\nfunction mist.getPathInSegments(p, num, exact)\n    local tot = mist.getPathLength(p)\n    local checkDist = tot/num\n    local typeUsed = 'vec2'\n\n    local points = {[1] = p[1]}\n    local curDist = 0\n    for i = 1, #p do\n        if i + 1 <= #p then\n            curDist = mist.utils.get2DDist(p[i], p[i+1]) + curDist\n            if curDist > checkDist then\n                curDist = 0\n                if exact then\n                    -- get avg point between the two\n                    -- insert into point table\n                    -- need to be accurate... maybe reassign the point for the value it is checking?\n                    -- insert into p table?\n                else\n                    table.insert(points, p[i])                \n                end\n            end\n        \n        end\n\n    end\n    return points\n\nend\n\n\nfunction mist.getPointAtDistanceOnPath(p, dist, r, rtn)\n    log:info('find distance: $1', dist)\n    local rType = r or 'roads'\n    local point = {x= 0, y = 0, z = 0}\n    local path = {}\n    local ret = rtn or 'vec2'\n    local l = 0\n    if p[1] and #p == 2 then\n        path = land.findPathOnRoads(rType, p[1].x, p[1].y, p[2].x, p[2].y)\n    else\n        path = p\n    end\n    for i = 1, #path do\n        if i + 1 <= #path then \n            nextPoint = path[i+1]\n            if topo then \n                l = mist.utils.get3DDist(path[i], path[i+1]) + l\n            else\n                l = mist.utils.get2DDist(path[i], path[i+1]) + l\n            end\n        end\n        if l > dist then\n            local diff = dist\n            if i ~= 1 then -- get difference\n                diff = l - dist\n            end\n            local dir = mist.utils.getHeadingPoints(mist.utils.makeVec3(path[i]), mist.utils.makeVec3(path[i+1]))\n            local x, y \n            if r then \n                x, y = land.getClosestPointOnRoads(rType, mist.utils.round((math.cos(dir) * diff) + path[i].x,1),  mist.utils.round((math.sin(dir) * diff) + path[i].y,1))\n            else\n                x, y = mist.utils.round((math.cos(dir) * diff) + path[i].x,1),  mist.utils.round((math.sin(dir) * diff) + path[i].y,1)\n            end\n            \n            if ret == 'vec2' then\n                return {x = x, y = y}, dir\n            elseif ret == 'vec3' then\n                return {x = x, y = 0, z = y}, dir\n            end\n            \n            return {x = x, y = y}, dir\n        end\n    end\n    log:warn('Find point at distance: $1, path distance $2', dist, l)\n    return false\nend\n\n\nfunction mist.projectPoint(point, dist, theta)\n    local newPoint = {}\n    if point.z then\n       newPoint.z = mist.utils.round(math.sin(theta) * dist + point.z, 3)\n       newPoint.y = mist.utils.deepCopy(point.y)\n    else\n       newPoint.y = mist.utils.round(math.sin(theta) * dist + point.y, 3)\n    end\n    newPoint.x = mist.utils.round(math.cos(theta) * dist + point.x, 3)\n\n    return newPoint\nend\n\nend\n\n\n\n\n--- Group functions.\n-- @section groups\ndo -- group functions scope\n\n\t--- Check table used for group creation.\n\t-- @tparam table groupData table to check.\n\t-- @treturn boolean true if a group can be spawned using\n\t-- this table, false otherwise.\n\tfunction mist.groupTableCheck(groupData)\n\t\t-- return false if country, category\n\t\t-- or units are missing\n\t\tif not groupData.country or\n\t\t\tnot groupData.category or\n\t\t\tnot groupData.units then\n\t\t\treturn false\n\t\tend\n\t\t-- return false if unitData misses\n\t\t-- x, y or type\n\t\tfor unitId, unitData in pairs(groupData.units) do\n\t\t\tif not unitData.x or\n\t\t\t\tnot unitData.y or\n\t\t\t\tnot unitData.type then\n\t\t\t\t\treturn false\n\t\t\tend\n\t\tend\n\t\t-- everything we need is here return true\n\t\treturn true\n\tend\n\n\t--- Returns group data table of give group.\n\tfunction mist.getCurrentGroupData(gpName)\n\t\tlocal dbData = mist.getGroupData(gpName)\n\n\t\tif Group.getByName(gpName) and Group.getByName(gpName):isExist() == true then\n\t\t\tlocal newGroup = Group.getByName(gpName)\n\t\t\tlocal newData = {}\n\t\t\tnewData.name = gpName\n\t\t\tnewData.groupId = tonumber(newGroup:getID())\n\t\t\tnewData.category = newGroup:getCategory()\n\t\t\tnewData.groupName = gpName\n\t\t\tnewData.hidden = dbData.hidden\n\n\t\t\tif newData.category == 2 then\n\t\t\t\tnewData.category = 'vehicle'\n\t\t\telseif newData.category == 3 then\n\t\t\t\tnewData.category = 'ship'\n\t\t\tend\n\n\t\t\tnewData.units = {}\n\t\t\tlocal newUnits = newGroup:getUnits()\n            if #newUnits == 0 then\n                log:warn('getCurrentGroupData has returned no units for: $1', gpName)\n            end\n\t\t\tfor unitNum, unitData in pairs(newGroup:getUnits()) do\n\t\t\t\tnewData.units[unitNum] = {}\n                local uName = unitData:getName()\n\n                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\n                    newData.units[unitNum] = mist.utils.deepCopy(mist.DBs.unitsByName[uName])\n                else\n                    newData.units[unitNum].unitId = tonumber(unitData:getID())\n                    newData.units[unitNum].type = unitData:getTypeName()\n                    newData.units[unitNum].skill = mist.getUnitSkill(uName)\n                    newData.country = string.lower(country.name[unitData:getCountry()])\n                    newData.units[unitNum].callsign = unitData:getCallsign()\n                    newData.units[unitNum].unitName = uName\n                end\n\n\t\t\t\tnewData.units[unitNum].x = unitData:getPosition().p.x\n\t\t\t\tnewData.units[unitNum].y = unitData:getPosition().p.z\n                newData.units[unitNum].point = {x = newData.units[unitNum].x, y = newData.units[unitNum].y}\n                newData.units[unitNum].heading = mist.getHeading(unitData, true) -- added to DBs\n\t\t\t\tnewData.units[unitNum].alt = unitData:getPosition().p.y\n                newData.units[unitNum].speed = mist.vec.mag(unitData:getVelocity())\n               \n\t\t\tend\n\n\t\t\treturn newData\n\t\telseif StaticObject.getByName(gpName) and StaticObject.getByName(gpName):isExist() == true then\n\t\t\tlocal staticObj = StaticObject.getByName(gpName)\n\t\t\tdbData.units[1].x = staticObj:getPosition().p.x\n\t\t\tdbData.units[1].y = staticObj:getPosition().p.z\n\t\t\tdbData.units[1].alt = staticObj:getPosition().p.y\n\t\t\tdbData.units[1].heading = mist.getHeading(staticObj, true)\n\n\t\t\treturn dbData\n\t\tend\n\n\tend\n\n\tfunction mist.getGroupData(gpName, route)\n\t\tlocal found = false\n\t\tlocal newData = {}\n\t\tif mist.DBs.groupsByName[gpName] then\n\t\t\tnewData = mist.utils.deepCopy(mist.DBs.groupsByName[gpName])\n\t\t\tfound = true\n\t\tend\n\n\t\tif found == false then\n\t\t\tfor groupName, groupData in pairs(mist.DBs.groupsByName) do\n\t\t\t\tif mist.stringMatch(groupName, gpName) == true then\n\t\t\t\t\tnewData = mist.utils.deepCopy(groupData)\n\t\t\t\t\tnewData.groupName = groupName\n\t\t\t\t\tfound = true\n\t\t\t\t\tbreak\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\n\t\tlocal payloads\n\t\tif newData.category == 'plane' or newData.category == 'helicopter' then\n\t\t\tpayloads = mist.getGroupPayload(newData.groupName)\n\t\tend\n\t\tif found == true then\n\t\t\t--newData.hidden = false -- maybe add this to DBs\n\n\t\t\tfor unitNum, unitData in pairs(newData.units) do\n\t\t\t\tnewData.units[unitNum] = {}\n\n\t\t\t\tnewData.units[unitNum].unitId = unitData.unitId\n\t\t\t\t--newData.units[unitNum].point = unitData.point\n\t\t\t\tnewData.units[unitNum].x = unitData.point.x\n\t\t\t\tnewData.units[unitNum].y = unitData.point.y\n\t\t\t\tnewData.units[unitNum].alt = unitData.alt\n\t\t\t\tnewData.units[unitNum].alt_type = unitData.alt_type\n\t\t\t\tnewData.units[unitNum].speed = unitData.speed\n\t\t\t\tnewData.units[unitNum].type = unitData.type\n\t\t\t\tnewData.units[unitNum].skill = unitData.skill\n\t\t\t\tnewData.units[unitNum].unitName = unitData.unitName\n\t\t\t\tnewData.units[unitNum].heading = unitData.heading -- added to DBs\n\t\t\t\tnewData.units[unitNum].playerCanDrive = unitData.playerCanDrive -- added to DBs\n                newData.units[unitNum].livery_id = unitData.livery_id\n                newData.units[unitNum].AddPropAircraft = unitData.AddPropAircraft\n                newData.units[unitNum].AddPropVehicle = unitData.AddPropVehicle\n                \n\n\t\t\t\tif newData.category == 'plane' or newData.category == 'helicopter' then\n\t\t\t\t\tnewData.units[unitNum].payload = payloads[unitNum]\n\t\t\t\t\t\n\t\t\t\t\tnewData.units[unitNum].onboard_num = unitData.onboard_num\n\t\t\t\t\tnewData.units[unitNum].callsign = unitData.callsign\n\t\t\t\t\t\n\t\t\t\tend\n\t\t\t\tif newData.category == 'static' then\n\t\t\t\t\tnewData.units[unitNum].categoryStatic = unitData.categoryStatic\n\t\t\t\t\tnewData.units[unitNum].mass = unitData.mass\n\t\t\t\t\tnewData.units[unitNum].canCargo = unitData.canCargo\n\t\t\t\t\tnewData.units[unitNum].shape_name = unitData.shape_name\n\t\t\t\tend\n\t\t\tend\n\t\t\t--log:info(newData)\n            if route then\n                newData.route = mist.getGroupRoute(gpName, true)\n            end\n            \n\t\t\treturn newData\n\t\telse\n\t\t\tlog:error('$1 not found in MIST database', gpName)\n\t\t\treturn\n\t\tend\n\tend\n\n\tfunction mist.getPayload(unitIdent)\n\t\t-- refactor to search by groupId and allow groupId and groupName as inputs\n\t\tlocal unitId = unitIdent\n\t\tif type(unitIdent) == 'string' and not tonumber(unitIdent) then\n\t\t\tif mist.DBs.MEunitsByName[unitIdent] then\n\t\t\t\tunitId = mist.DBs.MEunitsByName[unitIdent].unitId\n\t\t\telse\n\t\t\t\tlog:error(\"Unit not found in mist.DBs.MEunitsByName: $1\", unitIdent)\n\t\t\tend\n\t\tend\n\t\tlocal gpId = mist.DBs.MEunitsById[unitId].groupId\n\n\t\tif gpId and unitId then\n\t\t\tfor coa_name, coa_data in pairs(env.mission.coalition) do\n\t\t\t\tif (coa_name == 'red' or coa_name == 'blue') and type(coa_data) == 'table' then\n\t\t\t\t\tif coa_data.country then --there is a country table\n\t\t\t\t\t\tfor cntry_id, cntry_data in pairs(coa_data.country) do\n\t\t\t\t\t\t\tfor obj_cat_name, obj_cat_data in pairs(cntry_data) do\n\t\t\t\t\t\t\t\tif obj_cat_name == \"helicopter\" or obj_cat_name == \"ship\" or obj_cat_name == \"plane\" or obj_cat_name == \"vehicle\" then\t-- only these types have points\n\t\t\t\t\t\t\t\t\tif ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then\t--there's a group!\n\t\t\t\t\t\t\t\t\t\tfor group_num, group_data in pairs(obj_cat_data.group) do\n\t\t\t\t\t\t\t\t\t\t\tif group_data and group_data.groupId == gpId then\n\t\t\t\t\t\t\t\t\t\t\t\tfor unitIndex, unitData in pairs(group_data.units) do --group index\n\t\t\t\t\t\t\t\t\t\t\t\t\tif unitData.unitId == unitId then\n\t\t\t\t\t\t\t\t\t\t\t\t\t\treturn unitData.payload\n\t\t\t\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\tend\n\t\t\t\t\t\tend\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\telse\n\t\t\tlog:error('Need string or number. Got: $1', type(unitIdent))\n\t\t\treturn false\n\t\tend\n\t\tlog:warn(\"Couldn't find payload for unit: $1\", unitIdent)\n\t\treturn\n\tend\n\n\tfunction mist.getGroupPayload(groupIdent)\n\t\tlocal gpId = groupIdent\n\t\tif type(groupIdent) == 'string' and not tonumber(groupIdent) then\n\t\t\tif mist.DBs.MEgroupsByName[groupIdent] then\n\t\t\t\tgpId = mist.DBs.MEgroupsByName[groupIdent].groupId\n\t\t\telse\n\t\t\t\tlog:error('$1 not found in mist.DBs.MEgroupsByName', groupIdent)\n\t\t\tend\n\t\tend\n\n\t\tif gpId then\n\t\t\tfor coa_name, coa_data in pairs(env.mission.coalition) do\n\t\t\t\tif type(coa_data) == 'table' then\n\t\t\t\t\tif coa_data.country then --there is a country table\n\t\t\t\t\t\tfor cntry_id, cntry_data in pairs(coa_data.country) do\n\t\t\t\t\t\t\tfor obj_cat_name, obj_cat_data in pairs(cntry_data) do\n\t\t\t\t\t\t\t\tif obj_cat_name == \"helicopter\" or obj_cat_name == \"ship\" or obj_cat_name == \"plane\" or obj_cat_name == \"vehicle\" then\t-- only these types have points\n\t\t\t\t\t\t\t\t\tif ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then\t--there's a group!\n\t\t\t\t\t\t\t\t\t\tfor group_num, group_data in pairs(obj_cat_data.group) do\n\t\t\t\t\t\t\t\t\t\t\tif group_data and group_data.groupId == gpId then\n\t\t\t\t\t\t\t\t\t\t\t\tlocal payloads = {}\n\t\t\t\t\t\t\t\t\t\t\t\tfor unitIndex, unitData in pairs(group_data.units) do --group index\n\t\t\t\t\t\t\t\t\t\t\t\t\tpayloads[unitIndex] = unitData.payload\n\t\t\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\t\t\t\treturn payloads\n\t\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\tend\n\t\t\t\t\t\tend\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\telse\n\t\t\tlog:error('Need string or number. Got: $1', type(groupIdent))\n\t\t\treturn false\n\t\tend\n\t\tlog:warn(\"Couldn't find payload for group: $1\", groupIdent)\n\t\treturn\n\tend\n    \n    function mist.getGroupTable(groupIdent)\n    \t\tlocal gpId = groupIdent\n\t\tif type(groupIdent) == 'string' and not tonumber(groupIdent) then\n\t\t\tif mist.DBs.MEgroupsByName[groupIdent] then\n\t\t\t\tgpId = mist.DBs.MEgroupsByName[groupIdent].groupId\n\t\t\telse\n\t\t\t\tlog:error('$1 not found in mist.DBs.MEgroupsByName', groupIdent)\n\t\t\tend\n\t\tend\n\n\t\tif gpId then\n\t\t\tfor coa_name, coa_data in pairs(env.mission.coalition) do\n\t\t\t\tif type(coa_data) == 'table' then\n\t\t\t\t\tif coa_data.country then --there is a country table\n\t\t\t\t\t\tfor cntry_id, cntry_data in pairs(coa_data.country) do\n\t\t\t\t\t\t\tfor obj_cat_name, obj_cat_data in pairs(cntry_data) do\n\t\t\t\t\t\t\t\tif obj_cat_name == \"helicopter\" or obj_cat_name == \"ship\" or obj_cat_name == \"plane\" or obj_cat_name == \"vehicle\" then\t-- only these types have points\n\t\t\t\t\t\t\t\t\tif ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then\t--there's a group!\n\t\t\t\t\t\t\t\t\t\tfor group_num, group_data in pairs(obj_cat_data.group) do\n\t\t\t\t\t\t\t\t\t\t\tif group_data and group_data.groupId == gpId then\n\t\t\t\t\t\t\t\t\t\t\t\treturn group_data\n\t\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\tend\n\t\t\t\t\t\tend\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\telse\n\t\t\tlog:error('Need string or number. Got: $1', type(groupIdent))\n\t\t\treturn false\n\t\tend\n\t\tlog:warn(\"Couldn't find table for group: $1\", groupIdent)\n    \n    end\n    \n    function mist.getValidRandomPoint(vars)\n    \n    \n    end\n\n\tfunction mist.teleportToPoint(vars) -- main teleport function that all of teleport/respawn functions call\n\t\t--log:warn(vars)\n        local point = vars.point\n\t\tlocal gpName\n\t\tif vars.gpName then\n\t\t\tgpName = vars.gpName\n\t\telseif vars.groupName then\n\t\t\tgpName = vars.groupName\n\t\telse\n\t\t\tlog:error('Missing field groupName or gpName in variable table')\n\t\tend\n\n        --[[New vars to add, mostly for when called via inZone functions\n        anyTerrain\n        offsetWP1\n        offsetRoute\n        initTasks\n        \n        ]]\n        \n\t\tlocal action = vars.action\n\n\t\tlocal disperse = vars.disperse or false\n\t\tlocal maxDisp = vars.maxDisp or 200\n\t\tlocal radius = vars.radius or 0\n\t\tlocal innerRadius = vars.innerRadius\n\n\t\tlocal dbData = false\n        \n\n\n\t\tlocal newGroupData\n\t\tif gpName and not vars.groupData then\n\t\t\tif string.lower(action) == 'teleport' or string.lower(action) == 'tele' then\n\t\t\t\tnewGroupData = mist.getCurrentGroupData(gpName)\n\t\t\telseif string.lower(action) == 'respawn' then\n\t\t\t\tnewGroupData = mist.getGroupData(gpName)\n\t\t\t\tdbData = true\n\t\t\telseif string.lower(action) == 'clone' then\n\t\t\t\tnewGroupData = mist.getGroupData(gpName)\n\t\t\t\tnewGroupData.clone = 'order66'\n\t\t\t\tdbData = true\n\t\t\telse\n\t\t\t\taction = 'tele'\n\t\t\t\tnewGroupData = mist.getCurrentGroupData(gpName)\n\t\t\tend\n\t\telse\n\t\t\taction = 'tele'\n\t\t\tnewGroupData = vars.groupData\n\t\tend\n        \n        if vars.newGroupName then\n            newGroupData.groupName = vars.newGroupName\n        end\n\t\t\n        if #newGroupData.units == 0 then\n            log:warn('$1 has no units in group table', gpName)\n            return\n        end\n        \n\t\t--log:info('get Randomized Point')\n\t\tlocal diff = {x = 0, y = 0}\n\t\tlocal newCoord, origCoord \n        \n        local validTerrain = {'LAND', 'ROAD', 'SHALLOW_WATER', 'WATER', 'RUNWAY'}\n        if vars.anyTerrain then\n            -- do nothing\n        elseif vars.validTerrain then\n            validTerrain = vars.validTerrain\n        else\n            if string.lower(newGroupData.category) == 'ship' then\n                validTerrain = {'SHALLOW_WATER' , 'WATER'}\n            elseif string.lower(newGroupData.category) == 'vehicle' then\n                validTerrain = {'LAND', 'ROAD'}\n            end\n        end\n\n\t\tif point and radius >= 0 then\n\t\t\tlocal valid = false\n            -- new thoughts\n            --[[ Get AVG position of group and max radius distance to that avg point, otherwise use disperse data to get zone area to check\n            if disperse then\n            \n            else\n                \n            end\n            -- ]]\n            \n            \n            \n\n            \n\n             ---- old\n\t\t\tfor i = 1, 100\tdo\n\t\t\t\tnewCoord = mist.getRandPointInCircle(point, radius, innerRadius)\n\t\t\t\tif vars.anyTerrain or mist.isTerrainValid(newCoord, validTerrain)  then\n\t\t\t\t\torigCoord = mist.utils.deepCopy(newCoord)\n\t\t\t\t\tdiff = {x = (newCoord.x - newGroupData.units[1].x), y = (newCoord.y - newGroupData.units[1].y)}\n\t\t\t\t\tvalid = true\n\t\t\t\t\tbreak\n\t\t\t\tend\n\t\t\tend\n\t\t\tif valid == false then\n\t\t\t\tlog:error('Point supplied in variable table is not a valid coordinate. Valid coords: $1', validTerrain)\n\t\t\t\treturn false\n\t\t\tend\n\t\tend\n\t\tif not newGroupData.country and mist.DBs.groupsByName[newGroupData.groupName].country then\n\t\t\tnewGroupData.country = mist.DBs.groupsByName[newGroupData.groupName].country\n\t\tend\n\t\tif not newGroupData.category and mist.DBs.groupsByName[newGroupData.groupName].category then\n\t\t\tnewGroupData.category = mist.DBs.groupsByName[newGroupData.groupName].category\n\t\tend\n        --log:info(point)\n\t\tfor unitNum, unitData in pairs(newGroupData.units) do\n\t\t\t--log:info(unitNum)\n            if disperse then\n                local unitCoord \n                if maxDisp and type(maxDisp) == 'number' and unitNum ~= 1 then\n\t\t\t\t\tfor i = 1, 100 do \n                        unitCoord = mist.getRandPointInCircle(origCoord, maxDisp)\n                        if mist.isTerrainValid(unitCoord, validTerrain) == true then\n                            --log:warn('Index: $1, Itered: $2. AT: $3', unitNum, i, unitCoord)\n                            break\n                        end                        \n                    end\n                    \n\t\t\t\t\t--else\n\t\t\t\t\t--newCoord = mist.getRandPointInCircle(zone.point, zone.radius)\n\t\t\t\tend\n                if unitNum == 1 then\n                    unitCoord = mist.utils.deepCopy(newCoord)\n                end\n                if unitCoord then \n                    newGroupData.units[unitNum].x = unitCoord.x\n                    newGroupData.units[unitNum].y = unitCoord.y\n                end\n\t\t\telse\n\t\t\t\tnewGroupData.units[unitNum].x = unitData.x + diff.x\n\t\t\t\tnewGroupData.units[unitNum].y = unitData.y + diff.y\n\t\t\tend\n\t\t\tif point then\n\t\t\t\tif (newGroupData.category == 'plane' or newGroupData.category == 'helicopter')\tthen\n                    if point.z and point.y > 0 and point.y > land.getHeight({newGroupData.units[unitNum].x, newGroupData.units[unitNum].y}) + 10 then\n\t\t\t\t\t\tnewGroupData.units[unitNum].alt = point.y\n\t\t\t\t\t\t--log:info('far enough from ground')\n\t\t\t\t\telse\n\t\t\t\t\t\t\n\t\t\t\t\t\tif newGroupData.category == 'plane' then\n\t\t\t\t\t\t\t--log:info('setNewAlt')\n\t\t\t\t\t\t\tnewGroupData.units[unitNum].alt = land.getHeight({newGroupData.units[unitNum].x, newGroupData.units[unitNum].y}) + math.random(300, 9000)\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tnewGroupData.units[unitNum].alt = land.getHeight({newGroupData.units[unitNum].x, newGroupData.units[unitNum].y}) + math.random(200, 3000)\n\t\t\t\t\t\tend\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\n\t\tif newGroupData.start_time then\n\t\t\tnewGroupData.startTime = newGroupData.start_time\n\t\tend\n\n\t\tif newGroupData.startTime and newGroupData.startTime ~= 0 and dbData == true then\n\t\t\tlocal timeDif = timer.getAbsTime() - timer.getTime0()\n\t\t\tif timeDif > newGroupData.startTime then\n\t\t\t\tnewGroupData.startTime = 0\n\t\t\telse\n\t\t\t\tnewGroupData.startTime = newGroupData.startTime - timeDif\n\t\t\tend\n\n\t\tend\n\n\n        local tempRoute\n        \n        if mist.DBs.MEgroupsByName[gpName] and not vars.route then\n           -- log:warn('getRoute')\n            tempRoute = mist.getGroupRoute(gpName, true)\n        elseif vars.route then\n          --  log:warn('routeExist')\n            tempRoute = mist.utils.deepCopy(vars.route)\n        end\n       -- log:warn(tempRoute)\n        if tempRoute then\n            if (vars.offsetRoute or vars.offsetWP1 or vars.initTasks) then\n                for i = 1, #tempRoute do\n                   -- log:warn(i)\n                    if (vars.offsetRoute) or (i == 1 and vars.offsetWP1) or (i == 1 and vars.initTasks) then \n                       -- log:warn('update offset')\n                        tempRoute[i].x = tempRoute[i].x + diff.x\n                        tempRoute[i].y = tempRoute[i].y + diff.y\n                    elseif vars.initTasks and i > 1 then\n                        --log:warn('deleteWP')\n                        tempRoute[i] = nil\n                    end\n                end\n            end\n            newGroupData.route = tempRoute\n        end\n        \n        \n\t\t--log:warn(newGroupData)\n\t\t--mist.debug.writeData(mist.utils.serialize,{'teleportToPoint', newGroupData}, 'newGroupData.lua')\n\t\tif string.lower(newGroupData.category) == 'static' then\n\t\t\t--log:warn(newGroupData)\n\t\t\treturn mist.dynAddStatic(newGroupData)\n\t\tend\n\t\treturn mist.dynAdd(newGroupData)\n\n\tend\n\n\tfunction mist.respawnInZone(gpName, zone, disperse, maxDisp, v)\n\n\t\tif type(gpName) == 'table' and gpName:getName() then\n\t\t\tgpName = gpName:getName()\n\t\telseif type(gpName) == 'table' and gpName[1]:getName() then\n\t\t\tgpName = math.random(#gpName)\n\t\telse\n\t\t\tgpName = tostring(gpName)\n\t\tend\n\n\t\tif type(zone) == 'string' then\n\t\t\tzone = mist.DBs.zonesByName[zone]\n        elseif type(zone) == 'table' and not zone.radius then\n\t\t\tzone = mist.DBs.zonesByName[zone[math.random(1, #zone)]]\n\t\tend\n\t\tlocal vars = {}\n\t\tvars.gpName = gpName\n\t\tvars.action = 'respawn'\n\t\tvars.point = zone.point\n\t\tvars.radius = zone.radius\n\t\tvars.disperse = disperse\n\t\tvars.maxDisp = maxDisp\n        \n        if v and type(v) == 'table' then\n            for index, val in pairs(v) do\n                vars[index] = val\n            end \n        end\n        \n\t\treturn mist.teleportToPoint(vars)\n\tend\n\n\tfunction mist.cloneInZone(gpName, zone, disperse, maxDisp, v)\n\t\t--log:info('cloneInZone')\n\t\tif type(gpName) == 'table' then\n\t\t\tgpName = gpName:getName()\n\t\telse\n\t\t\tgpName = tostring(gpName)\n\t\tend\n\n\t\tif type(zone) == 'string' then\n\t\t\tzone = mist.DBs.zonesByName[zone]\n        elseif type(zone) == 'table' and not zone.radius then\n\t\t\tzone = mist.DBs.zonesByName[zone[math.random(1, #zone)]]\n\t\tend\n\t\tlocal vars = {}\n\t\tvars.gpName = gpName\n\t\tvars.action = 'clone'\n\t\tvars.point = zone.point\n\t\tvars.radius = zone.radius\n\t\tvars.disperse = disperse\n\t\tvars.maxDisp = maxDisp\n\t\t--log:info('do teleport')\n        if v and type(v) == 'table' then\n            for index, val in pairs(v) do\n                vars[index] = val\n            end \n        end\n\t\treturn mist.teleportToPoint(vars)\n\tend\n\n\tfunction mist.teleportInZone(gpName, zone, disperse, maxDisp, v) -- groupName, zoneName or table of Zone Names, keepForm is a boolean\n\t\tif type(gpName) == 'table' and gpName:getName() then\n\t\t\tgpName = gpName:getName()\n\t\telse\n\t\t\tgpName = tostring(gpName)\n\t\tend\n\n\t\tif type(zone) == 'string' then\n\t\t\tzone = mist.DBs.zonesByName[zone]\n        elseif type(zone) == 'table' and not zone.radius then\n\t\t\tzone = mist.DBs.zonesByName[zone[math.random(1, #zone)]]\n\t\tend\n\n\t\tlocal vars = {}\n\t\tvars.gpName = gpName\n\t\tvars.action = 'tele'\n\t\tvars.point = zone.point\n\t\tvars.radius = zone.radius\n\t\tvars.disperse = disperse\n\t\tvars.maxDisp = maxDisp\n        if v and type(v) == 'table' then\n            for index, val in pairs(v) do\n                vars[index] = val\n            end \n        end\n\t\treturn mist.teleportToPoint(vars)\n\tend\n\n\tfunction mist.respawnGroup(gpName, task)\n\t\tlocal vars = {}\n\t\tvars.gpName = gpName\n\t\tvars.action = 'respawn'\n\t\tif task and type(task) ~= 'number' then\n\t\t\tvars.route = mist.getGroupRoute(gpName, 'task')\n\t\tend\n\t\tlocal newGroup = mist.teleportToPoint(vars)\n\t\tif task and type(task) == 'number' then\n\t\t\tlocal newRoute = mist.getGroupRoute(gpName, 'task')\n\t\t\tmist.scheduleFunction(mist.goRoute, {newGroup, newRoute}, timer.getTime() + task)\n\t\tend\n\t\treturn newGroup\n\tend\n\n\tfunction mist.cloneGroup(gpName, task)\n\t\tlocal vars = {}\n\t\tvars.gpName = gpName\n\t\tvars.action = 'clone'\n\t\tif task and type(task) ~= 'number' then\n\t\t\tvars.route = mist.getGroupRoute(gpName, 'task')\n\t\tend\n\t\tlocal newGroup = mist.teleportToPoint(vars)\n\t\tif task and type(task) == 'number' then\n\t\t\tlocal newRoute = mist.getGroupRoute(gpName, 'task')\n\t\t\tmist.scheduleFunction(mist.goRoute, {newGroup, newRoute}, timer.getTime() + task)\n\t\tend\n\t\treturn newGroup\n\tend\n\n\tfunction mist.teleportGroup(gpName, task)\n\t\tlocal vars = {}\n\t\tvars.gpName = gpName\n\t\tvars.action = 'teleport'\n\t\tif task and type(task) ~= 'number' then\n\t\t\tvars.route = mist.getGroupRoute(gpName, 'task')\n\t\tend\n\t\tlocal newGroup = mist.teleportToPoint(vars)\n\t\tif task and type(task) == 'number' then\n\t\t\tlocal newRoute = mist.getGroupRoute(gpName, 'task')\n\t\t\tmist.scheduleFunction(mist.goRoute, {newGroup, newRoute}, timer.getTime() + task)\n\t\tend\n\t\treturn newGroup\n\tend\n\n\tfunction mist.spawnRandomizedGroup(groupName, vars) -- need to debug\n\t\tif Group.getByName(groupName) and Group.getByName(groupName):isExist() == true then\n\t\t\tlocal gpData = mist.getGroupData(groupName)\n\t\t\tgpData.units = mist.randomizeGroupOrder(gpData.units, vars)\n\t\t\tgpData.route = mist.getGroupRoute(groupName, 'task')\n\n\t\t\tmist.dynAdd(gpData)\n\t\tend\n\n\t\treturn true\n\tend\n\n\tfunction mist.randomizeNumTable(vars)\n\t\tlocal newTable = {}\n\n\t\tlocal excludeIndex = {}\n\t\tlocal randomTable = {}\n\n\t\tif vars and vars.exclude and type(vars.exclude) == 'table' then\n\t\t\tfor index, data in pairs(vars.exclude) do\n\t\t\t\texcludeIndex[data] = true\n\t\t\tend\n\t\tend\n\n\t\tlocal low, hi, size\n\n\t\tif vars.size then\n\t\t\tsize = vars.size\n\t\tend\n\n\t\tif vars and vars.lowerLimit and type(vars.lowerLimit) == 'number' then\n\t\t\tlow = mist.utils.round(vars.lowerLimit)\n\t\telse\n\t\t\tlow = 1\n\t\tend\n\n\t\tif vars and vars.upperLimit and type(vars.upperLimit) == 'number' then\n\t\t\thi = mist.utils.round(vars.upperLimit)\n\t\telse\n\t\t\thi = size\n\t\tend\n\n\t\tlocal choices = {}\n\t\t-- add to exclude list and create list of what to randomize\n\t\tfor i = 1, size do\n\t\t\tif not (i >= low and i <= hi) then\n\n\t\t\t\texcludeIndex[i] = true\n\t\t\tend\n\t\t\tif not excludeIndex[i] then\n\t\t\t\ttable.insert(choices, i)\n\t\t\telse\n\t\t\t\tnewTable[i] = i\n\t\t\tend\n\t\tend\n\n\t\tfor ind, num in pairs(choices) do\n\t\t\tlocal found = false\n\t\t\tlocal x = 0\n\t\t\twhile found == false do\n\t\t\t\tx = mist.random(size) -- get random number from list\n\t\t\t\tlocal addNew = true\n\t\t\t\tfor index, _ in pairs(excludeIndex) do\n\t\t\t\t\tif index == x then\n\t\t\t\t\t\taddNew = false\n\t\t\t\t\t\tbreak\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\t\tif addNew == true then\n\t\t\t\t\texcludeIndex[x] = true\n\t\t\t\t\tfound = true\n\t\t\t\tend\n\t\t\t\texcludeIndex[x] = true\n\n\t\t\tend\n\t\t\tnewTable[num] = x\n\t\tend\n\t\t--[[\n\t\tfor i = 1, #newTable do\n\t\t\tlog:info(newTable[i])\n\t\tend\n\t\t]]\n\t\treturn newTable\n\tend\n\n\tfunction mist.randomizeGroupOrder(passedUnits, vars)\n\t\t-- figure out what to exclude, and send data to other func\n\t\tlocal units = passedUnits\n\n\t\tif passedUnits.units then\n\t\t\tunits = passUnits.units\n\t\tend\n\n\t\tlocal exclude = {}\n\t\tlocal excludeNum = {}\n\t\tif vars and vars.excludeType and type(vars.excludeType) == 'table' then\n\t\t\texclude = vars.excludeType\n\t\tend\n\n\t\tif vars and vars.excludeNum and type(vars.excludeNum) == 'table' then\n\t\t\texcludeNum = vars.excludeNum\n\t\tend\n\n\t\tlocal low, hi\n\n\t\tif vars and vars.lowerLimit and type(vars.lowerLimit) == 'number' then\n\t\t\tlow = mist.utils.round(vars.lowerLimit)\n\t\telse\n\t\t\tlow = 1\n\t\tend\n\n\t\tif vars and vars.upperLimit and type(vars.upperLimit) == 'number' then\n\t\t\thi = mist.utils.round(vars.upperLimit)\n\t\telse\n\t\t\thi = #units\n\t\tend\n\n\n\t\tlocal excludeNum = {}\n\t\tfor unitIndex, unitData in pairs(units) do\n\t\t\tif unitIndex >= low and unitIndex\t<= hi then -- if within range\n\t\t\t\tlocal found = false\n\t\t\t\tif #exclude > 0 then\n\t\t\t\t\tfor excludeType, index in pairs(exclude) do -- check if excluded\n\t\t\t\t\t\tif mist.stringMatch(excludeType, unitData.type) then -- if excluded\n\t\t\t\t\t\t\texcludeNum[unitIndex] = unitIndex\n\t\t\t\t\t\t\tfound = true\n\t\t\t\t\t\tend\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\telse -- unitIndex is either to low, or to high: added to exclude list\n\t\t\t\texcludeNum[unitIndex] = unitId\n\t\t\tend\n\t\tend\n\n\t\tlocal newGroup = {}\n\t\tlocal newOrder = mist.randomizeNumTable({exclude = excludeNum, size = #units})\n\n\t\tfor unitIndex, unitData in pairs(units) do\n\t\t\tfor i = 1, #newOrder do\n\t\t\t\tif newOrder[i] == unitIndex then\n\t\t\t\t\tnewGroup[i] = mist.utils.deepCopy(units[i]) -- gets all of the unit data\n\t\t\t\t\tnewGroup[i].type = mist.utils.deepCopy(unitData.type)\n\t\t\t\t\tnewGroup[i].skill = mist.utils.deepCopy(unitData.skill)\n\t\t\t\t\tnewGroup[i].unitName = mist.utils.deepCopy(unitData.unitName)\n\t\t\t\t\tnewGroup[i].unitIndex = mist.utils.deepCopy(unitData.unitIndex) -- replaces the units data with a new type\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\t\treturn newGroup\n\tend\n\n\tfunction mist.random(firstNum, secondNum) -- no support for decimals\n\t\tlocal lowNum, highNum\n\t\tif not secondNum then\n\t\t\thighNum = firstNum\n\t\t\tlowNum = 1\n\t\telse\n\t\t\tlowNum = firstNum\n\t\t\thighNum = secondNum\n\t\tend\n\t\tlocal total = 1\n\t\tif math.abs(highNum - lowNum + 1) < 50 then -- if total values is less than 50\n\t\t\ttotal = math.modf(50/math.abs(highNum - lowNum + 1)) -- make x copies required to be above 50\n\t\tend\n\t\tlocal choices = {}\n\t\tfor i = 1, total do -- iterate required number of times\n\t\t\tfor x = lowNum, highNum do -- iterate between the range\n\t\t\t\tchoices[#choices +1] = x -- add each entry to a table\n\t\t\tend\n\t\tend\n\t\tlocal rtnVal = math.random(#choices) -- will now do a math.random of at least 50 choices\n\t\tfor i = 1, 10 do\n\t\t\trtnVal = math.random(#choices) -- iterate a few times for giggles\n\t\tend\n\t\treturn choices[rtnVal]\n\tend\n    \n    function mist.stringCondense(s)\n        local exclude = {'%-', '%(', '%)', '%_', '%[', '%]', '%.', '%#', '% ', '%{', '%}', '%$', '%%', '%?', '%+', '%^'}\n        for i , str in pairs(exclude) do\n            s = string.gsub(s, str, '')\n        end\n        return s\n    end\n\n\tfunction mist.stringMatch(s1, s2, bool)\n\t\t\n\t\tif type(s1) == 'string' and type(s2) == 'string' then\n            s1 = mist.stringCondense(s1)\n            s2 = mist.stringCondense(s2)\n\t\t\tif not bool then\n\t\t\t\ts1 = string.lower(s1)\n\t\t\t\ts2 = string.lower(s2)\n\t\t\tend\n\t\t\t--log:info('Comparing: $1 and $2', s1, s2)\n\t\t\tif s1 == s2 then\n\t\t\t\treturn true\n\t\t\telse\n\t\t\t\treturn false\n\t\t\tend\n\t\telse\n\t\t\tlog:error('Either the first or second variable were not a string')\n\t\t\treturn false\n\t\tend\n\tend\n\n\tmist.matchString = mist.stringMatch -- both commands work because order out type of I\n\n\t--[[ scope:\n{\n\tunits = {...},\t-- unit names.\n\tcoa = {...}, -- coa names\n\tcountries = {...}, -- country names\n\tCA = {...}, -- looks just like coa.\n\tunitTypes = { red = {}, blue = {}, all = {}, Russia = {},}\n}\n\n\nscope examples:\n\n{\tunits = { 'Hawg11', 'Hawg12' }, CA = {'blue'} }\n\n{ countries = {'Georgia'}, unitTypes = {blue = {'A-10C', 'A-10A'}}}\n\n{ coa = {'all'}}\n\n{unitTypes = { blue = {'A-10C'}}}\n]]\nend\n\n--- Utility functions.\n-- E.g. conversions between units etc.\n-- @section mist.utils\ndo -- mist.util scope\n\tmist.utils = {}\n\n\t--- Converts angle in radians to degrees.\n\t-- @param angle angle in radians\n\t-- @return angle in degrees\n\tfunction mist.utils.toDegree(angle)\n\t\treturn angle*180/math.pi\n\tend\n\n\t--- Converts angle in degrees to radians.\n\t-- @param angle angle in degrees\n\t-- @return angle in degrees\n\tfunction mist.utils.toRadian(angle)\n\t\treturn angle*math.pi/180\n\tend\n\n\t--- Converts meters to nautical miles.\n\t-- @param meters distance in meters\n\t-- @return distance in nautical miles\n\tfunction mist.utils.metersToNM(meters)\n\t\treturn meters/1852\n\tend\n\n\t--- Converts meters to feet.\n\t-- @param meters distance in meters\n\t-- @return distance in feet\n\tfunction mist.utils.metersToFeet(meters)\n\t\treturn meters/0.3048\n\tend\n\n\t--- Converts nautical miles to meters.\n\t-- @param nm distance in nautical miles\n\t-- @return distance in meters\n\tfunction mist.utils.NMToMeters(nm)\n\t\treturn nm*1852\n\tend\n\n\t--- Converts feet to meters.\n\t-- @param feet distance in feet\n\t-- @return distance in meters\n\tfunction mist.utils.feetToMeters(feet)\n\t\treturn feet*0.3048\n\tend\n\n\t--- Converts meters per second to knots.\n\t-- @param mps speed in m/s\n\t-- @return speed in knots\n\tfunction mist.utils.mpsToKnots(mps)\n\t\treturn mps*3600/1852\n\tend\n\n\t--- Converts meters per second to kilometers per hour.\n\t-- @param mps speed in m/s\n\t-- @return speed in km/h\n\tfunction mist.utils.mpsToKmph(mps)\n\t\treturn mps*3.6\n\tend\n\n\t--- Converts knots to meters per second.\n\t-- @param knots speed in knots\n\t-- @return speed in m/s\n\tfunction mist.utils.knotsToMps(knots)\n\t\treturn knots*1852/3600\n\tend\n\n\t--- Converts kilometers per hour to meters per second.\n\t-- @param kmph speed in km/h\n\t-- @return speed in m/s\n\tfunction mist.utils.kmphToMps(kmph)\n\t\treturn kmph/3.6\n\tend\n\t\n\tfunction mist.utils.kelvinToCelsius(t)\n\t\treturn t - 273.15\n\tend\n\t\n\tfunction mist.utils.FahrenheitToCelsius(f)\n\t\treturn (f - 32) * (5/9)\n\tend\n\t\n\tfunction mist.utils.celsiusToFahrenheit(c)\n\t\treturn c*(9/5)+32\n\tend\n    \n    function mist.utils.hexToRGB(hex, l) -- because for some reason the draw tools use hex when everything is rgba 0 - 1\n        local int = 255\n        if l then\n         int = 1\n        end\n        if hex and type(hex) == 'string' then\n            local val = {}\n            hex = string.gsub(hex, '0x', '')\n            if string.len(hex) == 8 then \n                val[1] = tonumber(\"0x\"..hex:sub(1,2)) / int\n                val[2] = tonumber(\"0x\"..hex:sub(3,4)) / int\n                val[3] = tonumber(\"0x\"..hex:sub(5,6)) / int\n                val[4] = tonumber(\"0x\"..hex:sub(7,8)) / int\n                \n                return val\n            end\n        end\n   end\n\t\n\tfunction mist.utils.converter(t1, t2, val)\n\t\tif type(t1) == 'string' then\n\t\t\tt1 = string.lower(t1)\n\t\tend\n\t\tif type(t2) == 'string' then\n\t\t\tt2 = string.lower(t2)\n\t\tend\n\t\tif val and type(val) ~= 'number' then\n\t\t\tif tonumber(val) then\n\t\t\t\tval = tonumber(val)\n\t\t\telse\n\t\t\t\tlog:warn(\"Value given is not a number: $1\", val)\n\t\t\t\treturn 0\n\t\t\tend\n\t\tend\n\t\t\n\t\t-- speed\n\t\tif t1 == 'mps' then\n\t\t\tif t2 == 'kmph' then\n\t\t\t\treturn val * 3.6\n\t\t\telseif t2 == 'knots' or t2 == 'knot' then\n\t\t\t\treturn val * 3600/1852\n\t\t\tend\n\t\telseif t1 == 'kmph' then\n\t\t\tif t2 == 'mps' then\n\t\t\t\treturn val/3.6\n\t\t\telseif t2 == 'knots' or t2 == 'knot' then\n\t\t\t\treturn  val*0.539957\n\t\t\tend\n\t\telseif t1 == 'knot' or t1 == 'knots' then\n\t\t\tif t2 == 'kmph' then\n\t\t\t\treturn val * 1.852\n\t\t\telseif t2 == 'mps' then\n\t\t\t\treturn  val * 0.514444\t\n\t\t\tend\n\t\t\t\n\t\t-- Distance\n\t\telseif t1 == 'feet' or t1 == 'ft' then\n\t\t\tif t2 == 'nm' then\n\t\t\t\treturn val/6076.12\n\t\t\telseif t2 == 'km' then\n\t\t\t\treturn (val*0.3048)/1000\n\t\t\telseif t2 == 'm' then\n\t\t\t\treturn val*0.3048\n\t\t\tend\n\t\telseif t1 == 'nm' then\n\t\t\tif t2 == 'feet' or t2 == 'ft' then\n\t\t\t\treturn val*6076.12\n\t\t\telseif t2 == 'km' then\n\t\t\t\treturn val*1.852\n\t\t\telseif t2 == 'm' then\n\t\t\t\treturn val*1852\n\t\t\tend\n\t\telseif t1 == 'km' then\n\t\t\tif t2 == 'nm' then\n\t\t\t\treturn val/1.852\n\t\t\telseif t2 == 'feet' or t2 == 'ft' then\n\t\t\t\treturn\t(val/0.3048)*1000\n\t\t\telseif t2 == 'm' then\n\t\t\t\treturn val*1000\n\t\t\tend\n\t\telseif t1 == 'm' then\n\t\t\tif t2 == 'nm' then\n\t\t\t\treturn val/1852\n\t\t\telseif t2 == 'km' then\n\t\t\t\treturn val/1000\n\t\t\telseif t2 == 'feet' or t2 == 'ft' then\n\t\t\t\treturn val/0.3048\n\t\t\tend\n\t\t\t\n\t\t-- Temperature\n\t\telseif t1 == 'f' or t1 == 'fahrenheit' then\n\t\t\tif t2 == 'c' or t2 == 'celsius' then\n\t\t\t\treturn (val - 32) * (5/9)\n\t\t\telseif t2 == 'k' or t2 == 'kelvin' then\n\t\t\t\treturn (val + 459.67) * (5/9)\n\t\t\tend\n\t\telseif t1 == 'c' or t1 == 'celsius' then\n\t\t\tif t2 == 'f' or t2 == 'fahrenheit' then\n\t\t\t\treturn val*(9/5)+32\n\t\t\telseif t2 == 'k' or t2 == 'kelvin' then\n\t\t\t\treturn val + 273.15\n\t\t\tend\n\t\telseif t1 == 'k' or t1 == 'kelvin' then\n\t\t\tif t2 == 'c' or t2 == 'celsius' then\n\t\t\t\treturn val - 273.15\n\t\t\telseif t2 == 'f' or t2 == 'fahrenheit' then\n\t\t\t\treturn ((val*(9/5))-459.67)\n\t\t\tend\n\t\t\n\t\t-- Pressure\n\t\telseif t1 == 'p' or t1 == 'pascal' or t1 == 'pascals' then\n\t\t\tif t2 == 'hpa' or t2 == 'hectopascal' then\n\t\t\t\treturn val/100\n\t\t\telseif t2 == 'mmhg' then\n\t\t\t\treturn val * 0.00750061561303\n\t\t\telseif t2 == 'inhg' then\n\t\t\t\treturn val * 0.0002953\n\t\t\tend\n\t\telseif t1 == 'hpa' or t1 == 'hectopascal' then\n\t\t\tif t2 == 'p' or t2 == 'pascal' or t2 == 'pascals' then\n\t\t\t\treturn val*100\n\t\t\telseif t2 == 'mmhg' then\n\t\t\t\treturn val * 0.00750061561303\n\t\t\telseif t2 == 'inhg' then\n\t\t\t\treturn val * 0.02953\n\t\t\tend\n\t\telseif t1 == 'mmhg' then\n\t\t\tif t2 == 'p' or t2 == 'pascal' or t2 == 'pascals' then\n\t\t\t\treturn  val / 0.00750061561303\n\t\t\telseif t2 == 'hpa' or t2 == 'hectopascal' then\n\t\t\t\treturn val * 1.33322\n\t\t\telseif t2 == 'inhg' then\n\t\t\t\treturn val/25.4\n\t\t\tend\n\t\telseif t1 == 'inhg' then\n\t\t\tif t2 == 'p' or t2 == 'pascal' or t2 == 'pascals' then\n\t\t\t\treturn val*3386.39\n\t\t\telseif t2 == 'mmhg' then\n\t\t\t\treturn val*25.4\n\t\t\telseif t2 == 'hpa' or t2 == 'hectopascal' then\n\t\t\t\treturn val * 33.8639\n\t\t\tend\n\t\telse\n\t\t\tlog:warn(\"First value doesn't match with list. Value given: $1\", t1)\n\t\tend\n\t\tlog:warn(\"Match not found. Unable to convert: $1 into $2\", t1, t2)\n\t\n\tend\n\t\n\tmist.converter = mist.utils.converter\n\t\n\tfunction mist.utils.getQFE(point, inchHg)\n\t\t\n\t\tlocal t, p = 0, 0\n\t\tif atmosphere.getTemperatureAndPressure then\n\t\t\tt, p = atmosphere.getTemperatureAndPressure(mist.utils.makeVec3GL(point))\n\t\tend\n\t\tif p == 0 then\n\t\t\tlocal h = land.getHeight(mist.utils.makeVec2(point))/0.3048 -- convert to feet\n\t\t\tif inchHg then\n\t\t\t\treturn (env.mission.weather.qnh - (h/30)) * 0.0295299830714\n\t\t\telse\n\t\t\t\treturn env.mission.weather.qnh - (h/30)\n\t\t\tend\n\t\telse \n\t\t\tif inchHg then\n\t\t\t\treturn mist.converter('p', 'inhg', p)\n\t\t\telse\n\t\t\t\treturn mist.converter('p', 'hpa', p)\n\t\t\tend\n\t\tend\n\n\tend\n\t--- Converts a Vec3 to a Vec2.\n\t-- @tparam Vec3 vec the 3D vector\n\t-- @return vector converted to Vec2\n\tfunction mist.utils.makeVec2(vec)\n\t\tif vec.z then\n\t\t\treturn {x = vec.x, y = vec.z}\n\t\telse\n\t\t\treturn {x = vec.x, y = vec.y}\t-- it was actually already vec2.\n\t\tend\n\tend\n\n\t--- Converts a Vec2 to a Vec3.\n\t-- @tparam Vec2 vec the 2D vector\n\t-- @param y optional new y axis (altitude) value. If omitted it's 0.\n\tfunction mist.utils.makeVec3(vec, y)\n\t\tif not vec.z then\n\t\t\tif vec.alt and not y then\n\t\t\t\ty = vec.alt\n\t\t\telseif not y then\n\t\t\t\ty = 0\n\t\t\tend\n\t\t\treturn {x = vec.x, y = y, z = vec.y}\n\t\telse\n\t\t\treturn {x = vec.x, y = vec.y, z = vec.z}\t-- it was already Vec3, actually.\n\t\tend\n\tend\n\n\t--- Converts a Vec2 to a Vec3 using ground level as altitude.\n\t-- The ground level at the specific point is used as altitude (y-axis)\n\t-- for the new vector. Optionally a offset can be specified.\n\t-- @tparam Vec2 vec the 2D vector\n\t-- @param[opt] offset offset to be applied to the ground level\n\t-- @return new 3D vector\n\tfunction mist.utils.makeVec3GL(vec, offset)\n\t\tlocal adj = offset or 0\n\n\t\tif not vec.z then\n\t\t\treturn {x = vec.x, y = (land.getHeight(vec) + adj), z = vec.y}\n\t\telse\n\t\t\treturn {x = vec.x, y = (land.getHeight({x = vec.x, y = vec.z}) + adj), z = vec.z}\n\t\tend\n\tend\n\n\t--- Returns the center of a zone as Vec3.\n\t-- @tparam string|table zone trigger zone name or table\n\t-- @treturn Vec3 center of the zone\n\tfunction mist.utils.zoneToVec3(zone, gl)\n\t\tlocal new = {}\n\t\tif type(zone) == 'table' then\n\t\t\tif zone.point then\n\t\t\t\tnew.x = zone.point.x\n\t\t\t\tnew.y = zone.point.y\n\t\t\t\tnew.z = zone.point.z\n\t\t\telseif zone.x and zone.y and zone.z then\n                new = mist.utils.deepCopy(zone)\n\t\t\tend\n\t\t\treturn new\n\t\telseif type(zone) == 'string' then\n\t\t\tzone = trigger.misc.getZone(zone)\n\t\t\tif zone then\n\t\t\t\tnew.x = zone.point.x\n\t\t\t\tnew.y = zone.point.y\n\t\t\t\tnew.z = zone.point.z\n\t\t\tend\n\t\tend\n        if new.x and gl then\n            new.y = land.getHeight({x = new.x, y = new.z})\n        end\n        return new\n\tend\n\n    function mist.utils.getHeadingPoints(point1, point2, north) -- sick of writing this out. \n        if north then \n            return mist.utils.getDir(mist.vec.sub(mist.utils.makeVec3(point2), mist.utils.makeVec3(point1)), (mist.utils.makeVec3(point1)))\n        else\n            return mist.utils.getDir(mist.vec.sub(mist.utils.makeVec3(point2), mist.utils.makeVec3(point1))) \n        end\n    end\n\t--- Returns heading-error corrected direction.\n\t-- True-north corrected direction from point along vector vec.\n\t-- @tparam Vec3 vec\n\t-- @tparam Vec2 point\n\t-- @return heading-error corrected direction from point.\n\tfunction mist.utils.getDir(vec, point)\n\t\tlocal dir = math.atan2(vec.z, vec.x)\n\t\tif point then\n\t\t\tdir = dir + mist.getNorthCorrection(point)\n\t\tend\n\t\tif dir < 0 then\n\t\t\tdir = dir + 2 * math.pi\t-- put dir in range of 0 to 2*pi\n\t\tend\n\t\treturn dir\n\tend\n\n\t--- Returns distance in meters between two points.\n\t-- @tparam Vec2|Vec3 point1 first point\n\t-- @tparam Vec2|Vec3 point2 second point\n\t-- @treturn number distance between given points.\n\tfunction mist.utils.get2DDist(point1, point2)\n        if not point1 then\n            log:warn(\"mist.utils.get2DDist  1st input value is nil\") \n        end\n        if not point2 then\n            log:warn(\"mist.utils.get2DDist  2nd input value is nil\") \n        end\n\t\tpoint1 = mist.utils.makeVec3(point1)\n\t\tpoint2 = mist.utils.makeVec3(point2)\n\t\treturn mist.vec.mag({x = point1.x - point2.x, y = 0, z = point1.z - point2.z})\n\tend\n\n\t--- Returns distance in meters between two points in 3D space.\n\t-- @tparam Vec3 point1 first point\n\t-- @tparam Vec3 point2 second point\n\t-- @treturn number distancen between given points in 3D space.\n\tfunction mist.utils.get3DDist(point1, point2)\n        if not point1 then\n            log:warn(\"mist.utils.get2DDist  1st input value is nil\") \n        end\n        if not point2 then\n            log:warn(\"mist.utils.get2DDist  2nd input value is nil\") \n        end\n\t\treturn mist.vec.mag({x = point1.x - point2.x, y = point1.y - point2.y, z = point1.z - point2.z})\n\tend\n\n\t--- Creates a waypoint from a vector.\n\t-- @tparam Vec2|Vec3 vec position of the new waypoint\n\t-- @treturn Waypoint a new waypoint to be used inside paths.\n\tfunction mist.utils.vecToWP(vec)\n\t\tlocal newWP = {}\n\t\tnewWP.x = vec.x\n\t\tnewWP.y = vec.y\n\t\tif vec.z then\n\t\t\tnewWP.alt = vec.y\n\t\t\tnewWP.y = vec.z\n\t\telse\n\t\t\tnewWP.alt = land.getHeight({x = vec.x, y = vec.y})\n\t\tend\n\t\treturn newWP\n\tend\n\n\t--- Creates a waypoint from a unit.\n\t-- This function also considers the units speed.\n\t-- The alt_type of this waypoint is set to \"BARO\".\n\t-- @tparam Unit pUnit Unit whose position and speed will be used.\n\t-- @treturn Waypoint new waypoint.\n\tfunction mist.utils.unitToWP(pUnit)\n\t\tlocal unit = mist.utils.deepCopy(pUnit)\n\t\tif type(unit) == 'string' then\n\t\t\tif Unit.getByName(unit) then\n\t\t\t\tunit = Unit.getByName(unit)\n\t\t\tend\n\t\tend\n\t\tif unit:isExist() == true then\n\t\t\tlocal new = mist.utils.vecToWP(unit:getPosition().p)\n\t\t\tnew.speed = mist.vec.mag(unit:getVelocity())\n\t\t\tnew.alt_type = \"BARO\"\n\n\t\t\treturn new\n\t\tend\n\t\tlog:error(\"$1 not found or doesn't exist\", pUnit)\n\t\treturn false\n\tend\n\n\t--- Creates a deep copy of a object.\n\t-- Usually this object is a table.\n\t-- See also: from http://lua-users.org/wiki/CopyTable\n\t-- @param object object to copy\n\t-- @return copy of object\n\tfunction mist.utils.deepCopy(object)\n\t\tlocal lookup_table = {}\n\t\tlocal function _copy(object)\n\t\t\tif type(object) ~= \"table\" then\n\t\t\t\treturn object\n\t\t\telseif lookup_table[object] then\n\t\t\t\treturn lookup_table[object]\n\t\t\tend\n\t\t\tlocal new_table = {}\n\t\t\tlookup_table[object] = new_table\n\t\t\tfor index, value in pairs(object) do\n\t\t\t\tnew_table[_copy(index)] = _copy(value)\n\t\t\tend\n\t\t\treturn setmetatable(new_table, getmetatable(object))\n\t\tend\n\t\treturn _copy(object)\n\tend\n\n\t--- Simple rounding function.\n\t-- From http://lua-users.org/wiki/SimpleRound\n\t-- use negative idp for rounding ahead of decimal place, positive for rounding after decimal place\n\t-- @tparam number num number to round\n\t-- @param idp\n\tfunction mist.utils.round(num, idp)\n\t\tlocal mult = 10^(idp or 0)\n\t\treturn math.floor(num * mult + 0.5) / mult\n\tend\n\n\t--- Rounds all numbers inside a table.\n\t-- @tparam table tbl table in which to round numbers\n\t-- @param idp\n\tfunction mist.utils.roundTbl(tbl, idp)\n\t\tfor id, val in pairs(tbl) do\n\t\t\tif type(val) == 'number' then\n\t\t\t\ttbl[id] = mist.utils.round(val, idp)\n\t\t\tend\n\t\tend\n\t\treturn tbl\n\tend\n\n\t--- Executes the given string.\n\t-- borrowed from Slmod\n\t-- @tparam string s string containing LUA code.\n\t-- @treturn boolean true if successfully executed, false otherwise\n\tfunction mist.utils.dostring(s)\n\t\tlocal f, err = loadstring(s)\n\t\tif f then\n\t\t\treturn true, f()\n\t\telse\n\t\t\treturn false, err\n\t\tend\n\tend\n\n\t--- Checks a table's types.\n\t-- This function checks a tables types against a specifically forged type table.\n\t-- @param fname\n\t-- @tparam table type_tbl\n\t-- @tparam table var_tbl\n\t-- @usage -- specifically forged type table\n\t-- type_tbl = {\n\t--\t\t\t\t\t\t {'table', 'number'},\n\t--\t\t\t\t\t\t 'string',\n\t--\t\t\t\t\t\t 'number',\n\t--\t\t\t\t\t\t 'number',\n\t--\t\t\t\t\t\t {'string','nil'},\n\t--\t\t\t\t\t\t {'number', 'nil'}\n\t--\t\t\t\t\t }\n\t-- -- my_tbl index 1 must be a table or a number;\n\t-- -- index 2, a string; index 3, a number;\n\t-- -- index 4, a number; index 5, either a string or nil;\n\t-- -- and index 6, either a number or nil.\n\t-- mist.utils.typeCheck(type_tbl, my_tb)\n\t-- @return true if table passes the check, false otherwise.\n\tfunction mist.utils.typeCheck(fname, type_tbl, var_tbl)\n\t\t-- log:info('type check')\n\t\tfor type_key, type_val in pairs(type_tbl) do\n\t\t\t-- log:info('type_key: $1 type_val: $2', type_key, type_val)\n\n\t\t\t--type_key can be a table of accepted keys- so try to find one that is not nil\n\t\t\tlocal type_key_str = ''\n\t\t\tlocal act_key = type_key -- actual key within var_tbl - necessary to use for multiple possible key variables.\tInitialize to type_key\n\t\t\tif type(type_key) == 'table' then\n\n\t\t\t\tfor i = 1, #type_key do\n\t\t\t\t\tif i ~= 1 then\n\t\t\t\t\t\ttype_key_str = type_key_str .. '/'\n\t\t\t\t\tend\n\t\t\t\t\ttype_key_str = type_key_str .. tostring(type_key[i])\n\t\t\t\t\tif var_tbl[type_key[i]] ~= nil then\n\t\t\t\t\t\tact_key = type_key[i]\t-- found a non-nil entry, make act_key now this val.\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\telse\n\t\t\t\ttype_key_str = tostring(type_key)\n\t\t\tend\n\n\t\t\tlocal err_msg = 'Error in function ' .. fname .. ', parameter \"' .. type_key_str .. '\", expected: '\n\t\t\tlocal passed_check = false\n\n\t\t\tif type(type_tbl[type_key]) == 'table' then\n\t\t\t\t-- log:info('err_msg, before: $1', err_msg)\n\t\t\t\tfor j = 1, #type_tbl[type_key] do\n\n\t\t\t\t\tif j == 1 then\n\t\t\t\t\t\terr_msg = err_msg .. type_tbl[type_key][j]\n\t\t\t\t\telse\n\t\t\t\t\t\terr_msg = err_msg .. ' or ' .. type_tbl[type_key][j]\n\t\t\t\t\tend\n\n\t\t\t\t\tif type(var_tbl[act_key]) == type_tbl[type_key][j] then\n\t\t\t\t\t\tpassed_check = true\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\t\t-- log:info('err_msg, after: $1', err_msg)\n\t\t\telse\n\t\t\t\t-- log:info('err_msg, before: $1', err_msg)\n\t\t\t\terr_msg = err_msg .. type_tbl[type_key]\n\t\t\t\t-- log:info('err_msg, after: $1', err_msg)\n\t\t\t\tif type(var_tbl[act_key]) == type_tbl[type_key] then\n\t\t\t\t\tpassed_check = true\n\t\t\t\tend\n\n\t\t\tend\n\n\t\t\tif not passed_check then\n\t\t\t\terr_msg = err_msg .. ', got ' .. type(var_tbl[act_key])\n\t\t\t\treturn false, err_msg\n\t\t\tend\n\t\tend\n\t\treturn true\n\tend\n\n\t--- Serializes the give variable to a string.\n\t-- borrowed from slmod\n\t-- @param var variable to serialize\n\t-- @treturn string variable serialized to string\n\tfunction mist.utils.basicSerialize(var)\n\t\tif var == nil then\n\t\t\treturn \"\\\"\\\"\"\n\t\telse\n\t\t\tif ((type(var) == 'number') or\n\t\t\t\t\t(type(var) == 'boolean') or\n\t\t\t\t\t(type(var) == 'function') or\n\t\t\t\t\t(type(var) == 'table') or\n\t\t\t\t\t(type(var) == 'userdata') ) then\n\t\t\treturn tostring(var)\n\t\telseif type(var) == 'string' then\n\t\t\tvar = string.format('%q', var)\n\t\t\treturn var\n\t\tend\n\tend\nend\n\n--- Serialize value\n-- borrowed from slmod (serialize_slmod)\n-- @param name\n-- @param value value to serialize\n-- @param level\nfunction mist.utils.serialize(name, value, level)\n\t--Based on ED's serialize_simple2\n\tlocal function basicSerialize(o)\n\t\tif type(o) == \"number\" then\n\t\t\treturn tostring(o)\n\t\telseif type(o) == \"boolean\" then\n\t\t\treturn tostring(o)\n\t\telse -- assume it is a string\n\t\t\treturn mist.utils.basicSerialize(o)\n\t\tend\n\tend\n\n\tlocal function serializeToTbl(name, value, level)\n\t\tlocal var_str_tbl = {}\n\t\tif level == nil then\n\t\t\tlevel = \"\"\n\t\tend\n\t\tif level ~= \"\" then \n\t\t\tlevel = level..\"\" \n\t\tend\n\t\ttable.insert(var_str_tbl, level .. name .. \" = \")\n\n\t\tif type(value) == \"number\" or type(value) == \"string\" or type(value) == \"boolean\" then\n\t\t\ttable.insert(var_str_tbl, basicSerialize(value) ..\t\",\\n\")\n\t\telseif type(value) == \"table\" then\n\t\t\ttable.insert(var_str_tbl, \"\\n\"..level..\"{\\n\")\n\n\t\t\tfor k,v in pairs(value) do -- serialize its fields\n\t\t\t\tlocal key\n\t\t\t\tif type(k) == \"number\" then\n\t\t\t\t\tkey = string.format(\"[%s]\", k)\n\t\t\t\telse\n\t\t\t\t\tkey = string.format(\"[%q]\", k)\n\t\t\t\tend\n\t\t\t\ttable.insert(var_str_tbl, mist.utils.serialize(key, v, level..\"\t\"))\n\n\t\t\tend\n\t\t\tif level == \"\" then\n\t\t\t\ttable.insert(var_str_tbl, level..\"} -- end of \"..name..\"\\n\")\n\n\t\t\telse\n\t\t\t\ttable.insert(var_str_tbl, level..\"}, -- end of \"..name..\"\\n\")\n\n\t\t\tend\n\t\telse\n\t\t\tlog:error('Cannot serialize a $1', type(value))\n\t\tend\n\t\treturn var_str_tbl\n\tend\n\n\tlocal t_str = serializeToTbl(name, value, level)\n\n\treturn table.concat(t_str)\nend\n\n--- Serialize value supporting cycles.\n-- borrowed from slmod (serialize_wcycles)\n-- @param name\n-- @param value value to serialize\n-- @param saved\nfunction mist.utils.serializeWithCycles(name, value, saved)\n\t--mostly straight out of Programming in Lua\n\tlocal function basicSerialize(o)\n\t\tif type(o) == \"number\" then\n\t\t\treturn tostring(o)\n\t\telseif type(o) == \"boolean\" then\n\t\t\treturn tostring(o)\n\t\telse -- assume it is a string\n\t\t\treturn mist.utils.basicSerialize(o)\n\t\tend\n\tend\n\n\tlocal t_str = {}\n\tsaved = saved or {}\t\t\t -- initial value\n\tif ((type(value) == 'string') or (type(value) == 'number') or (type(value) == 'table') or (type(value) == 'boolean')) then\n\t\ttable.insert(t_str, name .. \" = \")\n\t\tif type(value) == \"number\" or type(value) == \"string\" or type(value) == \"boolean\" then\n\t\t\ttable.insert(t_str, basicSerialize(value) ..\t\"\\n\")\n\t\telse\n\n\t\t\tif saved[value] then\t\t-- value already saved?\n\t\t\t\ttable.insert(t_str, saved[value] .. \"\\n\")\n\t\t\telse\n\t\t\t\tsaved[value] = name\t -- save name for next time\n\t\t\t\ttable.insert(t_str, \"{}\\n\")\n\t\t\t\tfor k,v in pairs(value) do\t\t\t-- save its fields\n\t\t\t\t\tlocal fieldname = string.format(\"%s[%s]\", name, basicSerialize(k))\n\t\t\t\t\ttable.insert(t_str, mist.utils.serializeWithCycles(fieldname, v, saved))\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\t\treturn table.concat(t_str)\n\telse\n\t\treturn \"\"\n\tend\nend\n\n--- Serialize a table to a single line string.\n-- serialization of a table all on a single line, no comments, made to replace old get_table_string function\n-- borrowed from slmod\n-- @tparam table tbl table to serialize.\n-- @treturn string string containing serialized table\nfunction mist.utils.oneLineSerialize(tbl)\n\tif type(tbl) == 'table' then --function only works for tables!\n\n\t\tlocal tbl_str = {}\n\n\t\ttbl_str[#tbl_str + 1] = '{ '\n\n\t\tfor ind,val in pairs(tbl) do -- serialize its fields\n\t\t\tif type(ind) == \"number\" then\n\t\t\t\ttbl_str[#tbl_str + 1] = '['\n\t\t\t\ttbl_str[#tbl_str + 1] = tostring(ind)\n\t\t\t\ttbl_str[#tbl_str + 1] = '] = '\n\t\t\telse --must be a string\n\t\t\t\ttbl_str[#tbl_str + 1] = '['\n\t\t\t\ttbl_str[#tbl_str + 1] = mist.utils.basicSerialize(ind)\n\t\t\t\ttbl_str[#tbl_str + 1] = '] = '\n\t\t\tend\n\n\t\t\tif ((type(val) == 'number') or (type(val) == 'boolean')) then\n\t\t\t\ttbl_str[#tbl_str + 1] = tostring(val)\n\t\t\t\ttbl_str[#tbl_str + 1] = ', '\n\t\t\telseif type(val) == 'string' then\n\t\t\t\ttbl_str[#tbl_str + 1] = mist.utils.basicSerialize(val)\n\t\t\t\ttbl_str[#tbl_str + 1] = ', '\n\t\t\telseif type(val) == 'nil' then -- won't ever happen, right?\n\t\t\t\ttbl_str[#tbl_str + 1] = 'nil, '\n\t\t\telseif type(val) == 'table' then\n\t\t\t\ttbl_str[#tbl_str + 1] = mist.utils.oneLineSerialize(val)\n\t\t\t\ttbl_str[#tbl_str + 1] = ', '\t --I think this is right, I just added it\n\t\t\telse\n\t\t\t\tlog:warn('Unable to serialize value type $1 at index $2', mist.utils.basicSerialize(type(val)), tostring(ind))\n\t\t\tend\n\n\t\tend\n\t\ttbl_str[#tbl_str + 1] = '}'\n\t\treturn table.concat(tbl_str)\n    else\n        return  mist.utils.basicSerialize(tbl)\n\tend\nend\n\n--- Returns table in a easy readable string representation.\n-- this function is not meant for serialization because it uses\n-- newlines for better readability.\n-- @param tbl table to show\n-- @param loc\n-- @param indent\n-- @param tableshow_tbls\n-- @return human readable string representation of given table\nfunction mist.utils.tableShow(tbl, loc, indent, tableshow_tbls) --based on serialize_slmod, this is a _G serialization\n\ttableshow_tbls = tableshow_tbls or {} --create table of tables\n\tloc = loc or \"\"\n\tindent = indent or \"\"\n\tif type(tbl) == 'table' then --function only works for tables!\n\t\ttableshow_tbls[tbl] = loc\n\n\t\tlocal tbl_str = {}\n\n\t\ttbl_str[#tbl_str + 1] = indent .. '{\\n'\n\n\t\tfor ind,val in pairs(tbl) do -- serialize its fields\n\t\t\tif type(ind) == \"number\" then\n\t\t\t\ttbl_str[#tbl_str + 1] = indent\n\t\t\t\ttbl_str[#tbl_str + 1] = loc .. '['\n\t\t\t\ttbl_str[#tbl_str + 1] = tostring(ind)\n\t\t\t\ttbl_str[#tbl_str + 1] = '] = '\n\t\t\telse\n\t\t\t\ttbl_str[#tbl_str + 1] = indent\n\t\t\t\ttbl_str[#tbl_str + 1] = loc .. '['\n\t\t\t\ttbl_str[#tbl_str + 1] = mist.utils.basicSerialize(ind)\n\t\t\t\ttbl_str[#tbl_str + 1] = '] = '\n\t\t\tend\n\n\t\t\tif ((type(val) == 'number') or (type(val) == 'boolean')) then\n\t\t\t\ttbl_str[#tbl_str + 1] = tostring(val)\n\t\t\t\ttbl_str[#tbl_str + 1] = ',\\n'\n\t\t\telseif type(val) == 'string' then\n\t\t\t\ttbl_str[#tbl_str + 1] = mist.utils.basicSerialize(val)\n\t\t\t\ttbl_str[#tbl_str + 1] = ',\\n'\n\t\t\telseif type(val) == 'nil' then -- won't ever happen, right?\n\t\t\t\ttbl_str[#tbl_str + 1] = 'nil,\\n'\n\t\t\telseif type(val) == 'table' then\n\t\t\t\tif tableshow_tbls[val] then\n\t\t\t\t\ttbl_str[#tbl_str + 1] = tostring(val) .. ' already defined: ' .. tableshow_tbls[val] .. ',\\n'\n\t\t\t\telse\n\t\t\t\t\ttableshow_tbls[val] = loc ..\t'[' .. mist.utils.basicSerialize(ind) .. ']'\n\t\t\t\t\ttbl_str[#tbl_str + 1] = tostring(val) .. ' '\n\t\t\t\t\ttbl_str[#tbl_str + 1] = mist.utils.tableShow(val,\tloc .. '[' .. mist.utils.basicSerialize(ind).. ']', indent .. '    ', tableshow_tbls)\n\t\t\t\t\ttbl_str[#tbl_str + 1] = ',\\n'\n\t\t\t\tend\n\t\t\telseif type(val) == 'function' then\n\t\t\t\tif debug and debug.getinfo then\n\t\t\t\t\tlocal fcnname = tostring(val)\n\t\t\t\t\tlocal info = debug.getinfo(val, \"S\")\n\t\t\t\t\tif info.what == \"C\" then\n\t\t\t\t\t\ttbl_str[#tbl_str + 1] = string.format('%q', fcnname .. ', C function') .. ',\\n'\n\t\t\t\t\telse\n\t\t\t\t\t\tif (string.sub(info.source, 1, 2) == [[./]]) then\n\t\t\t\t\t\t\ttbl_str[#tbl_str + 1] = string.format('%q', fcnname .. ', defined in (' .. info.linedefined .. '-' .. info.lastlinedefined .. ')' .. info.source) ..',\\n'\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\ttbl_str[#tbl_str + 1] = string.format('%q', fcnname .. ', defined in (' .. info.linedefined .. '-' .. info.lastlinedefined .. ')') ..',\\n'\n\t\t\t\t\t\tend\n\t\t\t\t\tend\n\n\t\t\t\telse\n\t\t\t\t\ttbl_str[#tbl_str + 1] = 'a function,\\n'\n\t\t\t\tend\n\t\t\telse\n\t\t\t\ttbl_str[#tbl_str + 1] = 'unable to serialize value type ' .. mist.utils.basicSerialize(type(val)) .. ' at index ' .. tostring(ind)\n\t\t\tend\n\t\tend\n\n\t\ttbl_str[#tbl_str + 1] = indent .. '}'\n\t\treturn table.concat(tbl_str)\n\tend\nend\nend\n\n--- Debug functions\n-- @section mist.debug\ndo -- mist.debug scope\n\tmist.debug = {}\n\n    function mist.debug.changeSetting(s)\n        if type(s) == 'table' then\n            for sName, sVal in pairs(s) do\n                if type(sVal) == 'string' or type(sVal) == 'number' then\n                    if sName == 'log' then\n                        mistSettings[sName] = sVal\n                        mist.log:setLevel(sVal)\n                    elseif sName == 'dbLog' then\n                        mistSettings[sName] = sVal\n                        dblog:setLevel(sVal)\n                    end\n                else\n                    mistSettings[sName] = sVal\n                end\n            end\n        end\n    end\n\t--- Dumps the global table _G.\n\t-- This dumps the global table _G to a file in\n\t-- the DCS\\Logs directory.\n\t-- This function requires you to disable script sanitization\n\t-- in $DCS_ROOT\\Scripts\\MissionScripting.lua to access lfs and io\n\t-- libraries.\n\t-- @param fname\n\tfunction mist.debug.dump_G(fname, simp)\n\t\tif lfs and io then\n\t\t\tlocal fdir = lfs.writedir() .. [[Logs\\]] .. fname\n\t\t\tlocal f = io.open(fdir, 'w')\n            if simp then\n                local g = mist.utils.deepCopy(_G)\n                g.mist = nil\n                g.slmod = nil\n                g.env.mission = nil\n                g.env.warehouses = nil\n                g.country.by_idx = nil\n                g.country.by_country = nil\n                \n                f:write(mist.utils.tableShow(g))\n            else\n            \n                f:write(mist.utils.tableShow(_G))\n            end\n\t\t\tf:close()\n\t\t\tlog:info('Wrote debug data to $1', fdir)\n\t\t\t--trigger.action.outText(errmsg, 10)\n\t\telse\n\t\t\tlog:alert('insufficient libraries to run mist.debug.dump_G, you must disable the sanitization of the io and lfs libraries in ./Scripts/MissionScripting.lua')\n\t\t\t--trigger.action.outText(errmsg, 10)\n\t\tend\n\tend\n\n\t--- Write debug data to file.\n\t-- This function requires you to disable script sanitization\n\t-- in $DCS_ROOT\\Scripts\\MissionScripting.lua to access lfs and io\n\t-- libraries.\n\t-- @param fcn\n\t-- @param fcnVars\n\t-- @param fname\n\tfunction mist.debug.writeData(fcn, fcnVars, fname)\n\t\tif lfs and io then\n\t\t\tlocal fdir = lfs.writedir() .. [[Logs\\]] .. fname\n\t\t\tlocal f = io.open(fdir, 'w')\n\t\t\tf:write(fcn(unpack(fcnVars, 1, table.maxn(fcnVars))))\n\t\t\tf:close()\n\t\t\tlog:info('Wrote debug data to $1', fdir)\n\t\t\tlocal errmsg = 'mist.debug.writeData wrote data to ' .. fdir\n\t\t\ttrigger.action.outText(errmsg, 10)\n\t\telse\n\t\t\tlocal errmsg = 'Error: insufficient libraries to run mist.debug.writeData, you must disable the sanitization of the io and lfs libraries in ./Scripts/MissionScripting.lua'\n\t\t\tlog:alert(errmsg)\n\t\t\ttrigger.action.outText(errmsg, 10)\n\t\tend\n\tend\n\n\t--- Write mist databases to file.\n\t-- This function requires you to disable script sanitization\n\t-- in $DCS_ROOT\\Scripts\\MissionScripting.lua to access lfs and io\n\t-- libraries.\n\tfunction mist.debug.dumpDBs()\n\t\tfor DBname, DB in pairs(mist.DBs) do\n\t\t\tif type(DB) == 'table' and type(DBname) == 'string' then\n\t\t\t\tmist.debug.writeData(mist.utils.serialize, {DBname, DB}, 'mist_DBs_' .. DBname .. '.lua')\n\t\t\tend\n\t\tend\n\tend\n    \n    -- write group table\n    function mist.debug.writeGroup(gName, data)\n        if gName and mist.DBs.groupsByName[gName] then \n            local dat \n            if data then\n                dat = mist.getGroupData(gName)\n            else\n                dat = mist.getGroupTable(gName)\n            end\n            if dat then\n                dat.route = {points = mist.getGroupRoute(gName, true)}\n            end\n            \n            if io and lfs and dat then\n                mist.debug.writeData(mist.utils.serialize, {gName, dat}, gName .. '_table.lua')\n            else\n                if dat then \n                    trigger.action.outText('Error: insufficient libraries to run mist.debug.writeGroup, you must disable the sanitization of the io and lfs libraries in ./Scripts/MissionScripting.lua \\nGroup table written to DCS.log file instead.', 10)\n                    log:warn('$1 dataTable: $2', gName, dat)\n                else\n                    trigger.action.outText('Unable to write group table for: ' .. gName .. '\\n Error: insufficient libraries to run mist.debug.writeGroup, you must disable the sanitization of the io and lfs libraries in ./Scripts/MissionScripting.lua', 10)\n                end\n            end\n         end\n    end\n    \n\n    \n    -- write all object types in mission.\n    function mist.debug.writeTypes(fName)\n        local wt = 'mistDebugWriteTypes.lua'\n        if fName and type(fName) == 'string' and string.find(fName, '.lua') then\n            wt = fName\n        end\n        local output = {units = {}, countries = {}}\n        for coa_name_miz, coa_data in pairs(env.mission.coalition) do\n            if type(coa_data) == 'table' then\n                if coa_data.country then --there is a country table\n                    for cntry_id, cntry_data in pairs(coa_data.country) do\n                        local countryName = string.lower(cntry_data.name)\n                        if cntry_data.id and country.names[cntry_data.id] then\n                            countryName = string.lower(country.names[cntry_data.id])\n                        end\n                        output.countries[countryName] = {}\n                        if type(cntry_data) == 'table' then\t--just making sure\n                            for obj_cat_name, obj_cat_data in pairs(cntry_data) do\n                                if obj_cat_name == \"helicopter\" or obj_cat_name == \"ship\" or obj_cat_name == \"plane\" or obj_cat_name == \"vehicle\" or obj_cat_name == \"static\" then --should be an unncessary check\n                                    local category = obj_cat_name\n                                    if not output.countries[countryName][category] then\n                                        -- log:warn('Create: $1', category)\n                                        output.countries[countryName][category] = {}\n                                    end\n                                    if ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then\t--there's a group!\n                                        for group_num, group_data in pairs(obj_cat_data.group) do\n                                            if group_data and group_data.units and type(group_data.units) == 'table' then\t--making sure again- this is a valid group\n                                                for i = 1, #group_data.units do\n                                                    if group_data.units[i] then\n                                                        local u = group_data.units[i]\n                                                        local liv = u.livery_id or 'default'\n                                                        if not output.units[u.type] then -- create unit table\n                                                           -- log:warn('Create: $1', u.type)\n                                                            output.units[u.type] = {count = 0, livery_id = {}}\n                                                        end\n                                                        \n                                                        if not output.countries[countryName][category][u.type] then\n                                                           -- log:warn('Create country, category, unit: $1', u.type)\n                                                            output.countries[countryName][category][u.type] = 0\n                                                        end\n                                                        -- add to count\n                                                        output.countries[countryName][category][u.type] = output.countries[countryName][category][u.type] + 1\n                                                        output.units[u.type].count =  output.units[u.type].count + 1\n                                                        \n                                                        if liv and not output.units[u.type].livery_id[countryName] then\n                                                           -- log:warn('Create livery country: $1', countryName)\n                                                            output.units[u.type].livery_id[countryName] = {}\n                                                        end\n                                                        if liv and not output.units[u.type].livery_id[countryName][liv] then \n                                                            --log:warn('Create Livery: $1', liv)\n                                                            output.units[u.type].livery_id[countryName][liv] = 0\n                                                        end\n                                                        if liv then \n                                                            output.units[u.type].livery_id[countryName][liv] = output.units[u.type].livery_id[countryName][liv] + 1\n                                                        end\n                                                        if u.payload and u.payload.pylons then\n                                                            if not output.units[u.type].CLSID then\n                                                                output.units[u.type].CLSID = {}\n                                                                output.units[u.type].pylons = {}\n                                                            end\n                                                            \n                                                            for pyIndex, pData in pairs(u.payload.pylons) do\n                                                                if not output.units[u.type].CLSID[pData.CLSID] then\n                                                                   output.units[u.type].CLSID[pData.CLSID] = 0\n                                                                end\n                                                               output.units[u.type].CLSID[pData.CLSID] = output.units[u.type].CLSID[pData.CLSID] + 1\n                                                                \n                                                                if not output.units[u.type].pylons[pyIndex] then\n                                                                    output.units[u.type].pylons[pyIndex] = {}\n                                                                end\n                                                                if not output.units[u.type].pylons[pyIndex][pData.CLSID] then\n                                                                    output.units[u.type].pylons[pyIndex][pData.CLSID] = 0\n                                                                end\n                                                                output.units[u.type].pylons[pyIndex][pData.CLSID] = output.units[u.type].pylons[pyIndex][pData.CLSID] + 1\n                                                            end\n                                                        \n                                                        end\n                                                    end\n                                                end\n                                            end\n                                        end\n                                    end\n                                end\n                            end\n                        end\n                    end\n                end\n            end\n        end\n        if io and lfs then\n             mist.debug.writeData(mist.utils.serialize, {'mistDebugWriteTypes', output}, wt)\n        else\n            trigger.action.outText('Error: insufficient libraries to run mist.debug.writeTypes, you must disable the sanitization of the io and lfs libraries in ./Scripts/MissionScripting.lua \\n writeTypes table written to DCS.log file instead.', 10)\n            log:warn('mist.debug.writeTypes: $1', output)\n        end\n        return output\n    end\n    function mist.debug.writeWeapons(unit)\n    \n    end\n    \n    function mist.debug.mark(msg, coord)\n        \n        mist.marker.add({point = coord, text = msg})\n        log:warn('debug.mark: $1    $2', msg, coord)\n    end\nend\n\n--- 3D Vector functions\n-- @section mist.vec\ndo -- mist.vec scope\n\tmist.vec = {}\n\n\t--- Vector addition.\n\t-- @tparam Vec3 vec1 first vector\n\t-- @tparam Vec3 vec2 second vector\n\t-- @treturn Vec3 new vector, sum of vec1 and vec2.\n\tfunction mist.vec.add(vec1, vec2)\n\t\treturn {x = vec1.x + vec2.x, y = vec1.y + vec2.y, z = vec1.z + vec2.z}\n\tend\n\n\t--- Vector substraction.\n\t-- @tparam Vec3 vec1 first vector\n\t-- @tparam Vec3 vec2 second vector\n\t-- @treturn Vec3 new vector, vec2 substracted from vec1.\n\tfunction mist.vec.sub(vec1, vec2)\n\t\treturn {x = vec1.x - vec2.x, y = vec1.y - vec2.y, z = vec1.z - vec2.z}\n\tend\n\n\t--- Vector scalar multiplication.\n\t-- @tparam Vec3 vec vector to multiply\n\t-- @tparam number mult scalar multiplicator\n\t-- @treturn Vec3 new vector multiplied with the given scalar\n\tfunction mist.vec.scalarMult(vec, mult)\n\t\treturn {x = vec.x*mult, y = vec.y*mult, z = vec.z*mult}\n\tend\n\n\tmist.vec.scalar_mult = mist.vec.scalarMult\n\n\t--- Vector dot product.\n\t-- @tparam Vec3 vec1 first vector\n\t-- @tparam Vec3 vec2 second vector\n\t-- @treturn number dot product of given vectors\n\tfunction mist.vec.dp (vec1, vec2)\n\t\treturn vec1.x*vec2.x + vec1.y*vec2.y + vec1.z*vec2.z\n\tend\n\n\t--- Vector cross product.\n\t-- @tparam Vec3 vec1 first vector\n\t-- @tparam Vec3 vec2 second vector\n\t-- @treturn Vec3 new vector, cross product of vec1 and vec2.\n\tfunction mist.vec.cp(vec1, vec2)\n\t\treturn { 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}\n\tend\n\n\t--- Vector magnitude\n\t-- @tparam Vec3 vec vector\n\t-- @treturn number magnitude of vector vec\n\tfunction mist.vec.mag(vec)\n\t\treturn (vec.x^2 + vec.y^2 + vec.z^2)^0.5\n\tend\n\n\t--- Unit vector\n\t-- @tparam Vec3 vec\n\t-- @treturn Vec3 unit vector of vec\n\tfunction mist.vec.getUnitVec(vec)\n\t\tlocal mag = mist.vec.mag(vec)\n\t\treturn { x = vec.x/mag, y = vec.y/mag, z = vec.z/mag }\n\tend\n\n\t--- Rotate vector.\n\t-- @tparam Vec2 vec2 to rotoate\n\t-- @tparam number theta\n\t-- @return Vec2 rotated vector.\n\tfunction mist.vec.rotateVec2(vec2, theta)\n\t\treturn { x = vec2.x*math.cos(theta) - vec2.y*math.sin(theta), y = vec2.x*math.sin(theta) + vec2.y*math.cos(theta)}\n\tend\n    \n    function mist.vec.normalize(vec3)\n        local mag =  mist.vec.mag(vec3)\n        if mag ~= 0 then \n            return mist.vec.scalar_mult(vec3, 1.0 / mag)\n        end\n    end\nend\n\n--- Flag functions.\n-- The mist \"Flag functions\" are functions that are similar to Slmod functions\n-- that detect a game condition and set a flag when that game condition is met.\n--\n-- They are intended to be used by persons with little or no experience in Lua\n-- programming, but with a good knowledge of the DCS mission editor.\n-- @section mist.flagFunc\ndo -- mist.flagFunc scope\n\tmist.flagFunc = {}\n\n\t--- Sets a flag if map objects are destroyed inside a zone.\n\t-- Once this function is run, it will start a continuously evaluated process\n\t-- that will set a flag true if map objects (such as bridges, buildings in\n\t-- town, etc.) die (or have died) in a mission editor zone (or set of zones).\n\t-- This will only happen once; once the flag is set true, the process ends.\n\t-- @usage\n\t-- -- Example vars table\n\t-- vars = {\n\t--\t zones = { \"zone1\", \"zone2\" }, -- can also be a single string\n\t--\t flag = 3, -- number of the flag\n\t--\t stopflag = 4, -- optional number of the stop flag\n\t--\t req_num = 10, -- optional minimum amount of map objects needed to die\n\t-- }\n\t-- mist.flagFuncs.mapobjs_dead_zones(vars)\n\t-- @tparam table vars table containing parameters.\n\tfunction mist.flagFunc.mapobjs_dead_zones(vars)\n\t\t--[[vars needs to be:\nzones = table or string,\nflag = number,\nstopflag = number or nil,\nreq_num = number or nil\n\nAND used by function,\ninitial_number\n\n]]\n\t\t-- type_tbl\n\t\tlocal type_tbl = {\n\t\t\t[{'zones', 'zone'}] = {'table', 'string'},\n\t\t\tflag = {'number', 'string'},\n\t\t\t[{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'},\n\t\t\t[{'req_num', 'reqnum'}] = {'number', 'nil'},\n\t\t}\n\n\t\tlocal err, errmsg = mist.utils.typeCheck('mist.flagFunc.mapobjs_dead_zones', type_tbl, vars)\n\t\tassert(err, errmsg)\n\t\tlocal zones = vars.zones or vars.zone\n\t\tlocal flag = vars.flag\n\t\tlocal stopflag = vars.stopflag or vars.stopFlag or -1\n\t\tlocal req_num = vars.req_num or vars.reqnum or 1\n\t\tlocal initial_number = vars.initial_number\n\n\t\tif type(zones) == 'string' then\n\t\t\tzones = {zones}\n\t\tend\n\n\t\tif not initial_number then\n\t\t\tinitial_number = #mist.getDeadMapObjsInZones(zones)\n\t\tend\n\n\t\tif 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\n\t\t\tif (#mist.getDeadMapObjsInZones(zones) - initial_number) >= req_num and trigger.misc.getUserFlag(flag) == 0 then\n\t\t\t\ttrigger.action.setUserFlag(flag, true)\n\t\t\t\treturn\n\t\t\telse\n\t\t\t\tmist.scheduleFunction(mist.flagFunc.mapobjs_dead_zones, {{zones = zones, flag = flag, stopflag = stopflag, req_num = req_num, initial_number = initial_number}}, timer.getTime() + 1)\n\t\t\tend\n\t\tend\n\tend\n\n\t--- Sets a flag if map objects are destroyed inside a polygon.\n\t-- Once this function is run, it will start a continuously evaluated process\n\t-- that will set a flag true if map objects (such as bridges, buildings in\n\t-- town, etc.) die (or have died) in a polygon.\n\t-- This will only happen once; once the flag is set true, the process ends.\n\t-- @usage\n\t-- -- Example vars table\n\t-- vars = {\n\t--\t zone = {\n\t--\t\t [1] = mist.DBs.unitsByName['NE corner'].point,\n\t--\t\t [2] = mist.DBs.unitsByName['SE corner'].point,\n\t--\t\t [3] = mist.DBs.unitsByName['SW corner'].point,\n\t--\t\t [4] = mist.DBs.unitsByName['NW corner'].point\n\t--\t }\n\t--\t flag = 3, -- number of the flag\n\t--\t stopflag = 4, -- optional number of the stop flag\n\t--\t req_num = 10, -- optional minimum amount of map objects needed to die\n\t-- }\n\t-- mist.flagFuncs.mapobjs_dead_zones(vars)\n\t-- @tparam table vars table containing parameters.\n\tfunction mist.flagFunc.mapobjs_dead_polygon(vars)\n\t\t--[[vars needs to be:\nzone = table,\nflag = number,\nstopflag = number or nil,\nreq_num = number or nil\n\nAND used by function,\ninitial_number\n\n]]\n\t\t-- type_tbl\n\t\tlocal type_tbl = {\n\t\t\t[{'zone', 'polyzone'}] = 'table',\n\t\t\tflag = {'number', 'string'},\n\t\t\t[{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'},\n\t\t\t[{'req_num', 'reqnum'}] = {'number', 'nil'},\n\t\t}\n\n\t\tlocal err, errmsg = mist.utils.typeCheck('mist.flagFunc.mapobjs_dead_polygon', type_tbl, vars)\n\t\tassert(err, errmsg)\n\t\tlocal zone = vars.zone or vars.polyzone\n\t\tlocal flag = vars.flag\n\t\tlocal stopflag = vars.stopflag or vars.stopFlag or -1\n\t\tlocal req_num = vars.req_num or vars.reqnum or 1\n\t\tlocal initial_number = vars.initial_number\n\n\t\tif not initial_number then\n\t\t\tinitial_number = #mist.getDeadMapObjsInPolygonZone(zone)\n\t\tend\n\n\t\tif 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\n\t\t\tif (#mist.getDeadMapObjsInPolygonZone(zone) - initial_number) >= req_num and trigger.misc.getUserFlag(flag) == 0 then\n\t\t\t\ttrigger.action.setUserFlag(flag, true)\n\t\t\t\treturn\n\t\t\telse\n\t\t\t\tmist.scheduleFunction(mist.flagFunc.mapobjs_dead_polygon, {{zone = zone, flag = flag, stopflag = stopflag, req_num = req_num, initial_number = initial_number}}, timer.getTime() + 1)\n\t\t\tend\n\t\tend\n\tend\n\n\t--- Sets a flag if unit(s) is/are inside a polygon.\n\t-- @tparam table vars @{unitsInPolygonVars}\n\t-- @usage -- set flag 11 to true as soon as any blue vehicles\n\t-- -- are inside the polygon shape created off of the waypoints\n\t-- -- of the group forest1\n\t-- mist.flagFunc.units_in_polygon {\n\t--\t\tunits = {'[blue][vehicle]'},\n\t--\t\tzone = mist.getGroupPoints('forest1'),\n\t--\t\tflag = 11\n\t-- }\n\tfunction mist.flagFunc.units_in_polygon(vars)\n\t\t--[[vars needs to be:\nunits = table,\nzone = table,\nflag = number,\nstopflag = number or nil,\nmaxalt = number or nil,\ninterval\t= number or nil,\nreq_num = number or nil\ntoggle = boolean or nil\nunitTableDef = table or nil\n]]\n\t\t-- type_tbl\n\t\tlocal type_tbl = {\n\t\t\t[{'units', 'unit'}] = 'table',\n\t\t\t[{'zone', 'polyzone'}] = 'table',\n\t\t\tflag = {'number', 'string'},\n\t\t\t[{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'},\n\t\t\t[{'maxalt', 'alt'}] = {'number', 'nil'},\n\t\t\tinterval = {'number', 'nil'},\n\t\t\t[{'req_num', 'reqnum'}] = {'number', 'nil'},\n\t\t\ttoggle = {'boolean', 'nil'},\n\t\t\tunitTableDef = {'table', 'nil'},\n\t\t}\n\n\t\tlocal err, errmsg = mist.utils.typeCheck('mist.flagFunc.units_in_polygon', type_tbl, vars)\n\t\tassert(err, errmsg)\n\t\tlocal units = vars.units or vars.unit\n\t\tlocal zone = vars.zone or vars.polyzone\n\t\tlocal flag = vars.flag\n\t\tlocal stopflag = vars.stopflag or vars.stopFlag or -1\n\t\tlocal interval = vars.interval or 1\n\t\tlocal maxalt = vars.maxalt or vars.alt\n\t\tlocal req_num = vars.req_num or vars.reqnum or 1\n\t\tlocal toggle = vars.toggle or nil\n\t\tlocal unitTableDef = vars.unitTableDef\n\n\t\tif not units.processed then\n\t\t\tunitTableDef = mist.utils.deepCopy(units)\n\t\tend\n\n\t\tif (units.processed and units.processed < mist.getLastDBUpdateTime()) or not units.processed then -- run unit table short cuts\n\t\t\tif unitTableDef then\n\t\t\t\tunits = mist.makeUnitTable(unitTableDef)\n\t\t\tend\n\t\tend\n\n\t\tif 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\n\t\t\tlocal num_in_zone = 0\n\t\t\tfor i = 1, #units do\n\t\t\t\tlocal unit = Unit.getByName(units[i]) or StaticObject.getByName(units[i])\n\t\t\t\tif unit then\n\t\t\t\t\tlocal pos = unit:getPosition().p\n\t\t\t\t\tif mist.pointInPolygon(pos, zone, maxalt) then\n\t\t\t\t\t\tnum_in_zone = num_in_zone + 1\n\t\t\t\t\t\tif num_in_zone >= req_num and trigger.misc.getUserFlag(flag) == 0 then\n\t\t\t\t\t\t\ttrigger.action.setUserFlag(flag, true)\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\tend\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\t\tif toggle and (num_in_zone < req_num) and trigger.misc.getUserFlag(flag) > 0 then\n\t\t\t\ttrigger.action.setUserFlag(flag, false)\n\t\t\tend\n\t\t\t-- do another check in case stopflag was set true by this function\n\t\t\tif (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\n\t\t\t\tmist.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)\n\t\t\tend\n\t\tend\n\n\tend\n\n\t--- Sets a flag if unit(s) is/are inside a trigger zone.\n\t-- @todo document\n\tfunction mist.flagFunc.units_in_zones(vars)\n\t\t--[[vars needs to be:\n\tunits = table,\n\tzones = table,\n\tflag = number,\n\tstopflag = number or nil,\n\tzone_type = string or nil,\n\treq_num = number or nil,\n\tinterval\t= number or nil\n\ttoggle = boolean or nil\n\t]]\n\t\t-- type_tbl\n\t\tlocal type_tbl = {\n\t\t\tunits = 'table',\n\t\t\tzones = 'table',\n\t\t\tflag = {'number', 'string'},\n\t\t\t[{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'},\n\t\t\t[{'zone_type', 'zonetype'}] = {'string', 'nil'},\n\t\t\t[{'req_num', 'reqnum'}] = {'number', 'nil'},\n\t\t\tinterval = {'number', 'nil'},\n\t\t\ttoggle = {'boolean', 'nil'},\n\t\t\tunitTableDef = {'table', 'nil'},\n\t\t}\n\n\t\tlocal err, errmsg = mist.utils.typeCheck('mist.flagFunc.units_in_zones', type_tbl, vars)\n\t\tassert(err, errmsg)\n\t\tlocal units = vars.units\n\t\tlocal zones = vars.zones\n\t\tlocal flag = vars.flag\n\t\tlocal stopflag = vars.stopflag or vars.stopFlag or -1\n\t\tlocal zone_type = vars.zone_type or vars.zonetype or 'cylinder'\n\t\tlocal req_num = vars.req_num or vars.reqnum or 1\n\t\tlocal interval = vars.interval or 1\n\t\tlocal toggle = vars.toggle or nil\n\t\tlocal unitTableDef = vars.unitTableDef\n\n\t\tif not units.processed then\n\t\t\tunitTableDef = mist.utils.deepCopy(units)\n\t\tend\n\t\t\n\t\tif (units.processed and units.processed < mist.getLastDBUpdateTime()) or not units.processed then -- run unit table short cuts\n\t\t\tif unitTableDef then\n\t\t\t\tunits = mist.makeUnitTable(unitTableDef)\n\t\t\tend\n\t\tend\n\n\t\tif 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\n\n\t\t\tlocal in_zone_units = mist.getUnitsInZones(units, zones, zone_type)\n\n\t\t\tif #in_zone_units >= req_num and trigger.misc.getUserFlag(flag) == 0 then\n\t\t\t\ttrigger.action.setUserFlag(flag, true)\n\t\t\telseif #in_zone_units < req_num and toggle then\n\t\t\t\ttrigger.action.setUserFlag(flag, false)\n\t\t\tend\n\t\t\t-- do another check in case stopflag was set true by this function\n\t\t\tif (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\n\t\t\t\tmist.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)\n\t\t\tend\n\t\tend\n\n\tend\n    --[[\n    function mist.flagFunc.weapon_in_zones(vars)\n        -- borrow from suchoi surprise. While running enabled event handler that checks for weapons in zone.\n        -- Choice is weapon category or weapon strings. \n    \n    end\n]]\n\t--- Sets a flag if unit(s) is/are inside a moving zone.\n\t-- @todo document\n\tfunction mist.flagFunc.units_in_moving_zones(vars)\n\t\t--[[vars needs to be:\n\tunits = table,\n\tzone_units = table,\n\tradius = number,\n\tflag = number,\n\tstopflag = number or nil,\n\tzone_type = string or nil,\n\treq_num = number or nil,\n\tinterval\t= number or nil\n\ttoggle = boolean or nil\n\t]]\n\t\t-- type_tbl\n\t\tlocal type_tbl = {\n\t\t\tunits = 'table',\n\t\t\t[{'zone_units', 'zoneunits'}]\t= 'table',\n\t\t\tradius = 'number',\n\t\t\tflag = {'number', 'string'},\n\t\t\t[{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'},\n\t\t\t[{'zone_type', 'zonetype'}] = {'string', 'nil'},\n\t\t\t[{'req_num', 'reqnum'}] = {'number', 'nil'},\n\t\t\tinterval = {'number', 'nil'},\n\t\t\ttoggle = {'boolean', 'nil'},\n\t\t\tunitTableDef = {'table', 'nil'},\n\t\t\tzUnitTableDef = {'table', 'nil'},\n\t\t}\n\n\t\tlocal err, errmsg = mist.utils.typeCheck('mist.flagFunc.units_in_moving_zones', type_tbl, vars)\n\t\tassert(err, errmsg)\n\t\tlocal units = vars.units\n\t\tlocal zone_units = vars.zone_units or vars.zoneunits\n\t\tlocal radius = vars.radius\n\t\tlocal flag = vars.flag\n\t\tlocal stopflag = vars.stopflag or vars.stopFlag or -1\n\t\tlocal zone_type = vars.zone_type or vars.zonetype or 'cylinder'\n\t\tlocal req_num = vars.req_num or vars.reqnum or 1\n\t\tlocal interval = vars.interval or 1\n\t\tlocal toggle = vars.toggle or nil\n\t\tlocal unitTableDef = vars.unitTableDef\n\t\tlocal zUnitTableDef = vars.zUnitTableDef\n\n\t\tif not units.processed then\n\t\t\tunitTableDef = mist.utils.deepCopy(units)\n\t\tend\n\n\t\tif not zone_units.processed then\n\t\t\tzUnitTableDef = mist.utils.deepCopy(zone_units)\n\t\tend\n\n\t\tif (units.processed and units.processed < mist.getLastDBUpdateTime()) or not units.processed then -- run unit table short cuts\n\t\t\tif unitTableDef then\n\t\t\t\tunits = mist.makeUnitTable(unitTableDef)\n\t\t\tend\n\t\tend\n\n\t\tif (zone_units.processed and zone_units.processed < mist.getLastDBUpdateTime()) or not zone_units.processed then -- run unit table short cuts\n\t\t\tif zUnitTableDef then\n\t\t\t\tzone_units = mist.makeUnitTable(zUnitTableDef)\n\t\t\tend\n\t\t\t\n\t\tend\n\n\t\tif 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\n\n\t\t\tlocal in_zone_units = mist.getUnitsInMovingZones(units, zone_units, radius, zone_type)\n\n\t\t\tif #in_zone_units >= req_num and trigger.misc.getUserFlag(flag) == 0 then\n\t\t\t\ttrigger.action.setUserFlag(flag, true)\n\t\t\telseif #in_zone_units < req_num and toggle then\n\t\t\t\ttrigger.action.setUserFlag(flag, false)\n\t\t\tend\n\t\t\t-- do another check in case stopflag was set true by this function\n\t\t\tif (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\n\t\t\t\tmist.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)\n\t\t\tend\n\t\tend\n\n\tend\n\n\t--- Sets a flag if units have line of sight to each other.\n\t-- @todo document\n\tfunction mist.flagFunc.units_LOS(vars)\n\t\t--[[vars needs to be:\nunitset1 = table,\naltoffset1 = number,\nunitset2 = table,\naltoffset2 = number,\nflag = number,\nstopflag = number or nil,\nradius = number or nil,\ninterval\t= number or nil,\nreq_num = number or nil\ntoggle = boolean or nil\n]]\n\t\t-- type_tbl\n\t\tlocal type_tbl = {\n\t\t\t[{'unitset1', 'units1'}] = 'table',\n\t\t\t[{'altoffset1', 'alt1'}] = 'number',\n\t\t\t[{'unitset2', 'units2'}] = 'table',\n\t\t\t[{'altoffset2', 'alt2'}] = 'number',\n\t\t\tflag = {'number', 'string'},\n\t\t\t[{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'},\n\t\t\t[{'req_num', 'reqnum'}] = {'number', 'nil'},\n\t\t\tinterval = {'number', 'nil'},\n\t\t\tradius = {'number', 'nil'},\n\t\t\ttoggle = {'boolean', 'nil'},\n\t\t\tunitTableDef1 = {'table', 'nil'},\n\t\t\tunitTableDef2 = {'table', 'nil'},\n\t\t}\n\n\t\tlocal err, errmsg = mist.utils.typeCheck('mist.flagFunc.units_LOS', type_tbl, vars)\n\t\tassert(err, errmsg)\n\t\tlocal unitset1 = vars.unitset1 or vars.units1\n\t\tlocal altoffset1 = vars.altoffset1 or vars.alt1\n\t\tlocal unitset2 = vars.unitset2 or vars.units2\n\t\tlocal altoffset2 = vars.altoffset2 or vars.alt2\n\t\tlocal flag = vars.flag\n\t\tlocal stopflag = vars.stopflag or vars.stopFlag or -1\n\t\tlocal interval = vars.interval or 1\n\t\tlocal radius = vars.radius or math.huge\n\t\tlocal req_num = vars.req_num or vars.reqnum or 1\n\t\tlocal toggle = vars.toggle or nil\n\t\tlocal unitTableDef1 = vars.unitTableDef1\n\t\tlocal unitTableDef2 = vars.unitTableDef2\n\n\t\tif not unitset1.processed then\n\t\t\tunitTableDef1 = mist.utils.deepCopy(unitset1)\n\t\tend\n\n\t\tif not unitset2.processed then\n\t\t\tunitTableDef2 = mist.utils.deepCopy(unitset2)\n\t\tend\n\n\t\tif (unitset1.processed and unitset1.processed < mist.getLastDBUpdateTime()) or not unitset1.processed then -- run unit table short cuts\n\t\t\tif unitTableDef1 then\n\t\t\t\tunitset1 = mist.makeUnitTable(unitTableDef1)\n\t\t\tend\n\t\tend\n\n\t\tif (unitset2.processed and unitset2.processed < mist.getLastDBUpdateTime()) or not unitset2.processed then -- run unit table short cuts\n\t\t\tif unitTableDef2 then\n\t\t\t\tunitset2 = mist.makeUnitTable(unitTableDef2)\n\t\t\tend\n\t\tend\n\n\n\t\tif 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\n\n\t\t\tlocal unitLOSdata = mist.getUnitsLOS(unitset1, altoffset1, unitset2, altoffset2, radius)\n\n\t\t\tif #unitLOSdata >= req_num and trigger.misc.getUserFlag(flag) == 0 then\n\t\t\t\ttrigger.action.setUserFlag(flag, true)\n\t\t\telseif #unitLOSdata < req_num and toggle then\n\t\t\t\ttrigger.action.setUserFlag(flag, false)\n\t\t\tend\n\t\t\t-- do another check in case stopflag was set true by this function\n\t\t\tif (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\n\t\t\t\tmist.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)\n\t\t\tend\n\t\tend\n\tend\n\n\t--- Sets a flag if group is alive.\n\t-- @todo document\n\tfunction mist.flagFunc.group_alive(vars)\n\t\t--[[vars\ngroupName\nflag\ntoggle\ninterval\nstopFlag\n\n]]\n\t\tlocal type_tbl = {\n\t\t\t[{'group', 'groupname', 'gp', 'groupName'}] = 'string',\n\t\t\tflag = {'number', 'string'},\n\t\t\t[{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'},\n\t\t\tinterval = {'number', 'nil'},\n\t\t\ttoggle = {'boolean', 'nil'},\n\t\t}\n\n\t\tlocal err, errmsg = mist.utils.typeCheck('mist.flagFunc.group_alive', type_tbl, vars)\n\t\tassert(err, errmsg)\n\n\t\tlocal groupName = vars.groupName or vars.group or vars.gp or vars.Groupname\n\t\tlocal flag = vars.flag\n\t\tlocal stopflag = vars.stopflag or vars.stopFlag or -1\n\t\tlocal interval = vars.interval or 1\n\t\tlocal toggle = vars.toggle or nil\n\n\n\t\tif 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\n\t\t\tif Group.getByName(groupName) and Group.getByName(groupName):isExist() == true and #Group.getByName(groupName):getUnits() > 0 then\n\t\t\t\tif trigger.misc.getUserFlag(flag) == 0 then\n\t\t\t\t\ttrigger.action.setUserFlag(flag, true)\n\t\t\t\tend\n\t\t\telse\n\t\t\t\tif toggle then\n\t\t\t\t\ttrigger.action.setUserFlag(flag, false)\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\n\t\tif (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\n\t\t\tmist.scheduleFunction(mist.flagFunc.group_alive, {{groupName = groupName, flag = flag, stopflag = stopflag, interval = interval, toggle = toggle}}, timer.getTime() + interval)\n\t\tend\n\n\tend\n\n\t--- Sets a flag if group is dead.\n\t-- @todo document\n\tfunction mist.flagFunc.group_dead(vars)\n\t\tlocal type_tbl = {\n\t\t\t[{'group', 'groupname', 'gp', 'groupName'}] = 'string',\n\t\t\tflag = {'number', 'string'},\n\t\t\t[{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'},\n\t\t\tinterval = {'number', 'nil'},\n\t\t\ttoggle = {'boolean', 'nil'},\n\t\t}\n\n\t\tlocal err, errmsg = mist.utils.typeCheck('mist.flagFunc.group_dead', type_tbl, vars)\n\t\tassert(err, errmsg)\n\n\t\tlocal groupName = vars.groupName or vars.group or vars.gp or vars.Groupname\n\t\tlocal flag = vars.flag\n\t\tlocal stopflag = vars.stopflag or vars.stopFlag or -1\n\t\tlocal interval = vars.interval or 1\n\t\tlocal toggle = vars.toggle or nil\n\n\n\t\tif 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\n\t\t\tif (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\n\t\t\t\tif trigger.misc.getUserFlag(flag) == 0 then\n\t\t\t\t\ttrigger.action.setUserFlag(flag, true)\n\t\t\t\tend\n\t\t\telse\n\t\t\t\tif toggle then\n\t\t\t\t\ttrigger.action.setUserFlag(flag, false)\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\n\t\tif (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\n\t\t\tmist.scheduleFunction(mist.flagFunc.group_dead, {{groupName = groupName, flag = flag, stopflag = stopflag, interval = interval, toggle = toggle}}, timer.getTime() + interval)\n\t\tend\n\tend\n\n\t--- Sets a flag if less than given percent of group is alive.\n\t-- @todo document\n\tfunction mist.flagFunc.group_alive_less_than(vars)\n\t\tlocal type_tbl = {\n\t\t\t[{'group', 'groupname', 'gp', 'groupName'}] = 'string',\n\t\t\tpercent = 'number',\n\t\t\tflag = {'number', 'string'},\n\t\t\t[{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'},\n\t\t\tinterval = {'number', 'nil'},\n\t\t\ttoggle = {'boolean', 'nil'},\n\t\t}\n\n\t\tlocal err, errmsg = mist.utils.typeCheck('mist.flagFunc.group_alive_less_than', type_tbl, vars)\n\t\tassert(err, errmsg)\n\n\t\tlocal groupName = vars.groupName or vars.group or vars.gp or vars.Groupname\n\t\tlocal flag = vars.flag\n\t\tlocal percent = vars.percent\n\t\tlocal stopflag = vars.stopflag or vars.stopFlag or -1\n\t\tlocal interval = vars.interval or 1\n\t\tlocal toggle = vars.toggle or nil\n\n\n\t\tif 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\n\t\t\tif Group.getByName(groupName) and Group.getByName(groupName):isExist() == true then\n\t\t\t\tif Group.getByName(groupName):getSize()/Group.getByName(groupName):getInitialSize() < percent/100 then\n\t\t\t\t\tif trigger.misc.getUserFlag(flag) == 0 then\n\t\t\t\t\t\ttrigger.action.setUserFlag(flag, true)\n\t\t\t\t\tend\n\t\t\t\telse\n\t\t\t\t\tif toggle then\n\t\t\t\t\t\ttrigger.action.setUserFlag(flag, false)\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\telse\n\t\t\t\tif trigger.misc.getUserFlag(flag) == 0 then\n\t\t\t\t\ttrigger.action.setUserFlag(flag, true)\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\n\t\tif (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\n\t\t\tmist.scheduleFunction(mist.flagFunc.group_alive_less_than, {{groupName = groupName, flag = flag, stopflag = stopflag, interval = interval, toggle = toggle, percent = percent}}, timer.getTime() + interval)\n\t\tend\n\tend\n\n\t--- Sets a flag if more than given percent of group is alive.\n\t-- @todo document\n\tfunction mist.flagFunc.group_alive_more_than(vars)\n\t\tlocal type_tbl = {\n\t\t\t[{'group', 'groupname', 'gp', 'groupName'}] = 'string',\n\t\t\tpercent = 'number',\n\t\t\tflag = {'number', 'string'},\n\t\t\t[{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'},\n\t\t\tinterval = {'number', 'nil'},\n\t\t\ttoggle = {'boolean', 'nil'},\n\t\t}\n\n\t\tlocal err, errmsg = mist.utils.typeCheck('mist.flagFunc.group_alive_more_than', type_tbl, vars)\n\t\tassert(err, errmsg)\n\n\t\tlocal groupName = vars.groupName or vars.group or vars.gp or vars.Groupname\n\t\tlocal flag = vars.flag\n\t\tlocal percent = vars.percent\n\t\tlocal stopflag = vars.stopflag or vars.stopFlag or -1\n\t\tlocal interval = vars.interval or 1\n\t\tlocal toggle = vars.toggle or nil\n\n\n\t\tif 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\n\t\t\tif Group.getByName(groupName) and Group.getByName(groupName):isExist() == true then\n\t\t\t\tif Group.getByName(groupName):getSize()/Group.getByName(groupName):getInitialSize() > percent/100 then\n\t\t\t\t\tif trigger.misc.getUserFlag(flag) == 0 then\n\t\t\t\t\t\ttrigger.action.setUserFlag(flag, true)\n\t\t\t\t\tend\n\t\t\t\telse\n\t\t\t\t\tif toggle and trigger.misc.getUserFlag(flag) == 1 then\n\t\t\t\t\t\ttrigger.action.setUserFlag(flag, false)\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\telse --- just in case\n\t\t\t\tif toggle and trigger.misc.getUserFlag(flag) == 1 then\n\t\t\t\t\ttrigger.action.setUserFlag(flag, false)\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\n\t\tif (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\n\t\t\tmist.scheduleFunction(mist.flagFunc.group_alive_more_than, {{groupName = groupName, flag = flag, stopflag = stopflag, interval = interval, toggle = toggle, percent = percent}}, timer.getTime() + interval)\n\t\tend\n\tend\n\n\tmist.flagFunc.mapobjsDeadPolygon = mist.flagFunc.mapobjs_dead_polygon\n\tmist.flagFunc.mapobjsDeadZones = mist.flagFunc.Mapobjs_dead_zones\n\tmist.flagFunc.unitsInZones = mist.flagFunc.units_in_zones\n\tmist.flagFunc.unitsInMovingZones = mist.flagFunc.units_in_moving_zones\n\tmist.flagFunc.unitsInPolygon = mist.flagFunc.units_in_polygon\n\tmist.flagFunc.unitsLOS = mist.flagFunc.units_LOS\n\tmist.flagFunc.groupAlive = mist.flagFunc.group_alive\n\tmist.flagFunc.groupDead = mist.flagFunc.group_dead\n\tmist.flagFunc.groupAliveMoreThan = mist.flagFunc.group_alive_more_than\n\tmist.flagFunc.groupAliveLessThan = mist.flagFunc.group_alive_less_than\n\nend\n\n--- Message functions.\n-- Messaging system\n-- @section mist.msg\ndo -- mist.msg scope\n\tlocal messageList = {}\n\t-- this defines the max refresh rate of the message box it honestly only needs to\n\t-- go faster than this for precision timing stuff (which could be its own function)\n\tlocal messageDisplayRate = 0.1\n\tlocal messageID = 0\n\tlocal displayActive = false\n\tlocal displayFuncId = 0\n\n\tlocal caSlots = false\n\tlocal caMSGtoGroup = false\n    local anyUpdate = false\n    local lastMessageTime = nil\n\n\tif env.mission.groundControl then -- just to be sure?\n\t\tfor index, value in pairs(env.mission.groundControl) do\n\t\t\tif type(value) == 'table' then\n\t\t\t\tfor roleName, roleVal in pairs(value) do\n\t\t\t\t\tfor rIndex, rVal in pairs(roleVal) do\n                        if type(rVal) == 'number' and rVal > 0 then\n                            caSlots = true\n                            break\n                        end\n\t\t\t\t\t\t\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\telseif type(value) == 'boolean' and value == true then\n\t\t\t\tcaSlots = true\n\t\t\t\tbreak\n\t\t\tend\n\t\tend\n\tend\n\n\tlocal function mistdisplayV5()\n        --log:warn(\"mistdisplayV5: $1\", timer.getTime())\n\n        local clearView = true\n\t\tif #messageList > 0 then\n            --log:warn('Updates: $1', anyUpdate)\n            if anyUpdate == true then\n                local activeClients = {}\n\n                for clientId, clientData in pairs(mist.DBs.humansById) do\n                    if Unit.getByName(clientData.unitName) and Unit.getByName(clientData.unitName):isExist() == true then\n                        activeClients[clientData.groupId] = clientData.groupName\n                    end\n                end\n                anyUpdate = false\n                if displayActive == false then\n                    displayActive = true\n                end\n                --mist.debug.writeData(mist.utils.serialize,{'msg', messageList}, 'messageList.lua')\n                local msgTableText = {}\n                local msgTableSound = {}\n                local curTime = timer.getTime()\n                for mInd, messageData in pairs(messageList) do\n                    --log:warn(messageData)\n                    if messageData.displayTill < curTime then\n                        messageData:remove()\t-- now using the remove/destroy function.\n                    else\n                        if messageData.displayedFor then\n                            messageData.displayedFor = curTime - messageData.addedAt\n                        end\n                        local nextSound = 1000\n                        local soundIndex = 0\n\n                        if messageData.multSound and #messageData.multSound > 0 then\n                            for index, sData in pairs(messageData.multSound) do\n                                if sData.time <= messageData.displayedFor and sData.played == false and sData.time < nextSound then -- find index of the next sound to be played\n                                    nextSound = sData.time\n                                    soundIndex = index\n                                end\n                            end\n                            if soundIndex ~= 0 then\n                                messageData.multSound[soundIndex].played = true\n                            end\n                        end\n\n                        for recIndex, recData in pairs(messageData.msgFor) do -- iterate recipiants\n                            if recData == 'RED' or recData == 'BLUE' or activeClients[recData] then -- rec exists\n                                if messageData.text then -- text\n                                    if not msgTableText[recData] then -- create table entry for text\n                                        msgTableText[recData] = {}\n                                        msgTableText[recData].text = {}\n                                        if recData == 'RED' or recData == 'BLUE' then\n                                            msgTableText[recData].text[1] = '-------Combined Arms Message-------- \\n'\n                                        end\n                                        msgTableText[recData].text[#msgTableText[recData].text + 1] = messageData.text\n                                        msgTableText[recData].displayTime = messageData.displayTime - messageData.displayedFor\n                                    else -- add to table entry and adjust display time if needed\n                                        if recData == 'RED' or recData == 'BLUE' then\n                                            msgTableText[recData].text[#msgTableText[recData].text + 1] = '\\n ---------------- Combined Arms Message: \\n'\n                                        else\n                                            msgTableText[recData].text[#msgTableText[recData].text + 1] = '\\n ---------------- \\n'\n                                        end\n                                        table.insert(msgTableText[recData].text, messageData.text)\n                                        if msgTableText[recData].displayTime < messageData.displayTime - messageData.displayedFor then\n                                            msgTableText[recData].displayTime = messageData.displayTime - messageData.displayedFor\n                                        else\n                                            --msgTableText[recData].displayTime = 10\n                                        end\n                                    end\n                                end\n                                if soundIndex ~= 0 then\n                                    msgTableSound[recData] = messageData.multSound[soundIndex].file\n                                end\n                            end\n                            \n                        end\n                        messageData.update = nil\n\n                    end\n                \n                end\n                ------- new display\n\n                if caSlots == true and caMSGtoGroup == false then\n                    if msgTableText.RED then\n                        trigger.action.outTextForCoalition(coalition.side.RED, table.concat(msgTableText.RED.text), msgTableText.RED.displayTime, clearView)\n\n                    end\n                    if msgTableText.BLUE then\n                        trigger.action.outTextForCoalition(coalition.side.BLUE, table.concat(msgTableText.BLUE.text), msgTableText.BLUE.displayTime, clearView)\n                    end\n                end\n\n                for index, msgData in pairs(msgTableText) do\n                    if type(index) == 'number' then -- its a groupNumber\n                        trigger.action.outTextForGroup(index, table.concat(msgData.text), msgData.displayTime, clearView)\n                    end\n                end\n                --- new audio\n                if msgTableSound.RED then\n                    trigger.action.outSoundForCoalition(coalition.side.RED, msgTableSound.RED)\n                end\n                if msgTableSound.BLUE then\n                    trigger.action.outSoundForCoalition(coalition.side.BLUE, msgTableSound.BLUE)\n                end\n\n\n                for index, file in pairs(msgTableSound) do\n                    if type(index) == 'number' then -- its a groupNumber\n                        trigger.action.outSoundForGroup(index, file)\n                    end\n                end\n                \n            end\n            \n\t\telse\n\t\t\tmist.removeFunction(displayFuncId)\n\t\t\tdisplayActive = false\n\t\tend\n\tend\n\n\tlocal function mistdisplayV4()\n\t\tlocal activeClients = {}\n\n\t\tfor clientId, clientData in pairs(mist.DBs.humansById) do\n\t\t\tif Unit.getByName(clientData.unitName) and Unit.getByName(clientData.unitName):isExist() == true then\n\t\t\t\tactiveClients[clientData.groupId] = clientData.groupName\n\t\t\tend\n\t\tend\n\n\t\t--[[if caSlots == true and caMSGtoGroup == true then\n\n\t\tend]]\n\n\n\t\tif #messageList > 0 then\n\t\t\tif displayActive == false then\n\t\t\t\tdisplayActive = true\n\t\t\tend\n\t\t\t--mist.debug.writeData(mist.utils.serialize,{'msg', messageList}, 'messageList.lua')\n\t\t\tlocal msgTableText = {}\n\t\t\tlocal msgTableSound = {}\n\n\t\t\tfor messageId, messageData in pairs(messageList) do\n\t\t\t\tif messageData.displayedFor > messageData.displayTime then\n\t\t\t\t\tmessageData:remove()\t-- now using the remove/destroy function.\n\t\t\t\telse\n\t\t\t\t\tif messageData.displayedFor then\n\t\t\t\t\t\tmessageData.displayedFor = messageData.displayedFor + messageDisplayRate\n\t\t\t\t\tend\n\t\t\t\t\tlocal nextSound = 1000\n\t\t\t\t\tlocal soundIndex = 0\n\n\t\t\t\t\tif messageData.multSound and #messageData.multSound > 0 then\n\t\t\t\t\t\tfor index, sData in pairs(messageData.multSound) do\n\t\t\t\t\t\t\tif sData.time <= messageData.displayedFor and sData.played == false and sData.time < nextSound then -- find index of the next sound to be played\n\t\t\t\t\t\t\t\tnextSound = sData.time\n\t\t\t\t\t\t\t\tsoundIndex = index\n\t\t\t\t\t\t\tend\n\t\t\t\t\t\tend\n\t\t\t\t\t\tif soundIndex ~= 0 then\n\t\t\t\t\t\t\tmessageData.multSound[soundIndex].played = true\n\t\t\t\t\t\tend\n\t\t\t\t\tend\n\n\t\t\t\t\tfor recIndex, recData in pairs(messageData.msgFor) do -- iterate recipiants\n\t\t\t\t\t\tif recData == 'RED' or recData == 'BLUE' or activeClients[recData] then -- rec exists\n\t\t\t\t\t\t\tif messageData.text then -- text\n\t\t\t\t\t\t\t\tif not msgTableText[recData] then -- create table entry for text\n\t\t\t\t\t\t\t\t\tmsgTableText[recData] = {}\n\t\t\t\t\t\t\t\t\tmsgTableText[recData].text = {}\n\t\t\t\t\t\t\t\t\tif recData == 'RED' or recData == 'BLUE' then\n\t\t\t\t\t\t\t\t\t\tmsgTableText[recData].text[1] = '-------Combined Arms Message-------- \\n'\n\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\tmsgTableText[recData].text[#msgTableText[recData].text + 1] = messageData.text\n\t\t\t\t\t\t\t\t\tmsgTableText[recData].displayTime = messageData.displayTime - messageData.displayedFor\n\t\t\t\t\t\t\t\telse -- add to table entry and adjust display time if needed\n\t\t\t\t\t\t\t\t\tif recData == 'RED' or recData == 'BLUE' then\n\t\t\t\t\t\t\t\t\t\tmsgTableText[recData].text[#msgTableText[recData].text + 1] = '\\n ---------------- Combined Arms Message: \\n'\n\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\tmsgTableText[recData].text[#msgTableText[recData].text + 1] = '\\n ---------------- \\n'\n\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\tmsgTableText[recData].text[#msgTableText[recData].text + 1] = messageData.text\n\t\t\t\t\t\t\t\t\tif msgTableText[recData].displayTime < messageData.displayTime - messageData.displayedFor then\n\t\t\t\t\t\t\t\t\t\tmsgTableText[recData].displayTime = messageData.displayTime - messageData.displayedFor\n\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\tmsgTableText[recData].displayTime = 1\n\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\tif soundIndex ~= 0 then\n\t\t\t\t\t\t\t\tmsgTableSound[recData] = messageData.multSound[soundIndex].file\n\t\t\t\t\t\t\tend\n\t\t\t\t\t\tend\n\t\t\t\t\tend\n\n\n\t\t\t\tend\n\t\t\tend\n\t\t\t------- new display\n\n\t\t\tif caSlots == true and caMSGtoGroup == false then\n\t\t\t\tif msgTableText.RED then\n\t\t\t\t\ttrigger.action.outTextForCoalition(coalition.side.RED, table.concat(msgTableText.RED.text), msgTableText.RED.displayTime, true)\n\n\t\t\t\tend\n\t\t\t\tif msgTableText.BLUE then\n\t\t\t\t\ttrigger.action.outTextForCoalition(coalition.side.BLUE, table.concat(msgTableText.BLUE.text), msgTableText.BLUE.displayTime, true)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tfor index, msgData in pairs(msgTableText) do\n\t\t\t\tif type(index) == 'number' then -- its a groupNumber\n\t\t\t\t\ttrigger.action.outTextForGroup(index, table.concat(msgData.text), msgData.displayTime, true)\n\t\t\t\tend\n\t\t\tend\n\t\t\t--- new audio\n\t\t\tif msgTableSound.RED then\n\t\t\t\ttrigger.action.outSoundForCoalition(coalition.side.RED, msgTableSound.RED)\n\t\t\tend\n\t\t\tif msgTableSound.BLUE then\n\t\t\t\ttrigger.action.outSoundForCoalition(coalition.side.BLUE, msgTableSound.BLUE)\n\t\t\tend\n\n\n\t\t\tfor index, file in pairs(msgTableSound) do\n\t\t\t\tif type(index) == 'number' then -- its a groupNumber\n\t\t\t\t\ttrigger.action.outSoundForGroup(index, file)\n\t\t\t\tend\n\t\t\tend\n\t\telse\n\t\t\tmist.removeFunction(displayFuncId)\n\t\t\tdisplayActive = false\n\t\tend\n\n\tend\n\n\tlocal typeBase = {\n\t\t['Mi-8MT'] = {'Mi-8MTV2', 'Mi-8MTV', 'Mi-8'},\n\t\t['MiG-21Bis'] = {'Mig-21'},\n\t\t['MiG-15bis'] = {'Mig-15'},\n\t\t['FW-190D9'] = {'FW-190'},\n\t\t['Bf-109K-4'] = {'Bf-109'},\n\t}\n\n\t--[[function mist.setCAGroupMSG(val)\n\tif type(val) == 'boolean' then\n\t\tcaMSGtoGroup = val\n\t\treturn true\n\tend\n\treturn false\nend]]\n\n\tmist.message = {\n\n\t\tadd = function(vars)\n\t\t\tlocal function msgSpamFilter(recList, spamBlockOn)\n\t\t\t\tfor id, name in pairs(recList) do\n\t\t\t\t\tif name == spamBlockOn then\n\t\t\t\t\t\t--\tlog:info('already on recList')\n\t\t\t\t\t\treturn recList\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\t\t--log:info('add to recList')\n\t\t\t\ttable.insert(recList, spamBlockOn)\n\t\t\t\treturn recList\n\t\t\tend\n\n\t\t\t--[[\n\t\t\tlocal vars = {}\n\t\t\tvars.text = 'Hello World'\n\t\t\tvars.displayTime = 20\n\t\t\tvars.msgFor = {coa = {'red'}, countries = {'Ukraine', 'Georgia'}, unitTypes = {'A-10C'}}\n\t\t\tmist.message.add(vars)\n\n\t\t\tDisplays the message for all red coalition players. Players belonging to Ukraine and Georgia, and all A-10Cs on the map\n\n\t\t\t]]\n\n        \n\t\t\tlocal new = {}\n\t\t\tnew.text = vars.text -- The actual message\n\t\t\tnew.displayTime = vars.displayTime -- How long will the message appear for\n\t\t\tnew.displayedFor = 0 -- how long the message has been displayed so far\n            new.displayTill = timer.getTime() + vars.displayTime\n\t\t\tnew.name = vars.name\t -- ID to overwrite the older message (if it exists) Basically it replaces a message that is displayed with new text.\n\t\t\tnew.addedAt = timer.getTime()\n            --log:warn('New Message: $1', new.text)\n\n\t\t\tif vars.multSound and vars.multSound[1] then\n\t\t\t\tnew.multSound = vars.multSound\n\t\t\telse\n\t\t\t\tnew.multSound = {}\n\t\t\tend\n\n\t\t\tif vars.sound or vars.fileName then -- converts old sound file system into new multSound format\n\t\t\t\tlocal sound = vars.sound\n\t\t\t\tif vars.fileName then\n\t\t\t\t\tsound = vars.fileName\n\t\t\t\tend\n\t\t\t\tnew.multSound[#new.multSound+1] = {time = 0.1, file = sound}\n\t\t\tend\n\n\t\t\tif #new.multSound > 0 then\n\t\t\t\tfor i, data in pairs(new.multSound) do\n\t\t\t\t\tdata.played = false\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tlocal newMsgFor = {} -- list of all groups message displays for\n\t\t\tfor forIndex, forData in pairs(vars.msgFor) do\n\t\t\t\tfor list, listData in pairs(forData) do\n\t\t\t\t\tfor clientId, clientData in pairs(mist.DBs.humansById) do\n\t\t\t\t\t\tforIndex = string.lower(forIndex)\n\t\t\t\t\t\tif type(listData) == 'string' then\n\t\t\t\t\t\t\tlistData = string.lower(listData)\n\t\t\t\t\t\tend\n\t\t\t\t\t\tif (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 --\n\t\t\t\t\t\t\tnewMsgFor = msgSpamFilter(newMsgFor, clientData.groupId) -- so units dont get the same message twice if complex rules are given\n\t\t\t\t\t\t\t--table.insert(newMsgFor, clientId)\n\t\t\t\t\t\telseif forIndex == 'unittypes' then\n\t\t\t\t\t\t\tfor typeId, typeData in pairs(listData) do\n\t\t\t\t\t\t\t\tlocal found = false\n\t\t\t\t\t\t\t\tfor clientDataEntry, clientDataVal in pairs(clientData) do\n\t\t\t\t\t\t\t\t\tif type(clientDataVal) == 'string' then\n\t\t\t\t\t\t\t\t\t\tif mist.matchString(list, clientDataVal) == true or list == 'all' then\n\t\t\t\t\t\t\t\t\t\t\tlocal sString = typeData\n\t\t\t\t\t\t\t\t\t\t\tfor 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\n\t\t\t\t\t\t\t\t\t\t\t\tfor pIndex, pName in pairs(pTbl) do\n\t\t\t\t\t\t\t\t\t\t\t\t\tif mist.stringMatch(sString, pName) then\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsString = rName\n\t\t\t\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\t\t\tif sString == clientData.type then\n\t\t\t\t\t\t\t\t\t\t\t\tfound = true\n\t\t\t\t\t\t\t\t\t\t\t\tnewMsgFor = msgSpamFilter(newMsgFor, clientData.groupId) -- sends info oto other function to see if client is already recieving the current message.\n\t\t\t\t\t\t\t\t\t\t\t\t--table.insert(newMsgFor, clientId)\n\t\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\tif found == true then\t-- shouldn't this be elsewhere too?\n\t\t\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\tend\n\n\t\t\t\t\t\tend\n\t\t\t\t\tend\n\t\t\t\t\tfor coaData, coaId in pairs(coalition.side) do\n\t\t\t\t\t\tif string.lower(forIndex) == 'coa' or string.lower(forIndex) == 'ca' then\n\t\t\t\t\t\t\tif listData == string.lower(coaData) or listData == 'all' then\n\t\t\t\t\t\t\t\tnewMsgFor = msgSpamFilter(newMsgFor, coaData)\n\t\t\t\t\t\t\tend\n\t\t\t\t\t\tend\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tif #newMsgFor > 0 then\n\t\t\t\tnew.msgFor = newMsgFor -- I swear its not confusing\n\n\t\t\telse\n\t\t\t\treturn false\n\t\t\tend\n\n\n\t\t\tif vars.name and type(vars.name) == 'string' then\n\t\t\t\tfor i = 1, #messageList do\n\t\t\t\t\tif messageList[i].name then\n\t\t\t\t\t\tif messageList[i].name == vars.name then\n\t\t\t\t\t\t\t--log:info('updateMessage')\n                            messageList[i].displayTill = timer.getTime() + messageList[i].displayTime\n\t\t\t\t\t\t\tmessageList[i].displayedFor = 0\n\t\t\t\t\t\t\tmessageList[i].addedAt = timer.getTime()\n\t\t\t\t\t\t\tmessageList[i].sound = new.sound\n\t\t\t\t\t\t\tmessageList[i].text = new.text\n\t\t\t\t\t\t\tmessageList[i].msgFor = new.msgFor\n\t\t\t\t\t\t\tmessageList[i].multSound = new.multSound\n                            anyUpdate = true\n                            --log:warn('Message updated: $1', new.messageID)\n\t\t\t\t\t\t\treturn messageList[i].messageID\n\t\t\t\t\t\tend\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n            anyUpdate = true\n\t\t\tmessageID = messageID + 1\n\t\t\tnew.messageID = messageID\n\n\t\t\t--mist.debug.writeData(mist.utils.serialize,{'msg', new}, 'newMsg.lua')\n\n\n\t\t\tmessageList[#messageList + 1] = new\n\n\t\t\tlocal mt = { __index =\tmist.message}\n\t\t\tsetmetatable(new, mt)\n\n\t\t\tif displayActive == false then\n\t\t\t\tdisplayActive = true\n\t\t\t\tdisplayFuncId = mist.scheduleFunction(mistdisplayV5, {}, timer.getTime() + messageDisplayRate, messageDisplayRate)\n\t\t\tend\n\n\t\t\treturn messageID\n\n\t\tend,\n\n\t\tremove = function(self)\t-- Now a self variable; the former functionality taken up by mist.message.removeById.\n\t\t\tfor i, msgData in pairs(messageList) do\n\t\t\t\tif messageList[i] == self then\n\t\t\t\t\ttable.remove(messageList, i)\n                    anyUpdate = true\n\t\t\t\t\treturn true --removal successful\n\t\t\t\tend\n\t\t\tend\n\t\t\treturn false -- removal not successful this script fails at life!\n\t\tend,\n\n\t\tremoveById = function(id)\t-- This function is NOT passed a self variable; it is the remove by id function.\n\t\t\tfor i, msgData in pairs(messageList) do\n\t\t\t\tif messageList[i].messageID == id then\n\t\t\t\t\ttable.remove(messageList, i)\n                    anyUpdate = true\n\t\t\t\t\treturn true --removal successful\n\t\t\t\tend\n\t\t\tend\n\t\t\treturn false -- removal not successful this script fails at life!\n\t\tend,\n\t}\n\n\t--[[ vars for mist.msgMGRS\nvars.units - table of unit names (NOT unitNameTable- maybe this should change).\nvars.acc - integer between 0 and 5, inclusive\nvars.text - text in the message\nvars.displayTime - self explanatory\nvars.msgFor - scope\n]]\n\tfunction mist.msgMGRS(vars)\n\t\tlocal units = vars.units\n\t\tlocal acc = vars.acc\n\t\tlocal text = vars.text\n\t\tlocal displayTime = vars.displayTime\n\t\tlocal msgFor = vars.msgFor\n\n\t\tlocal s = mist.getMGRSString{units = units, acc = acc}\n\t\tlocal newText\n\t\tif text then\n\t\t\tif string.find(text, '%%s') then\t-- look for %s\n\t\t\t\tnewText = string.format(text, s)\t-- insert the coordinates into the message\n\t\t\telse\n\t\t\t\t-- just append to the end.\n\t\t\t\tnewText = text .. s\n\t\t\tend\n\t\telse\n\t\t\tnewText = s\n\t\tend\n\t\tmist.message.add{\n\t\t\ttext = newText,\n\t\t\tdisplayTime = displayTime,\n\t\t\tmsgFor = msgFor\n\t\t}\n\tend\n\n\t--[[ vars for mist.msgLL\nvars.units - table of unit names (NOT unitNameTable- maybe this should change) (Yes).\nvars.acc - integer, number of numbers after decimal place\nvars.DMS - if true, output in degrees, minutes, seconds.\tOtherwise, output in degrees, minutes.\nvars.text - text in the message\nvars.displayTime - self explanatory\nvars.msgFor - scope\n]]\n\tfunction mist.msgLL(vars)\n\t\tlocal units = vars.units\t-- technically, I don't really need to do this, but it helps readability.\n\t\tlocal acc = vars.acc\n\t\tlocal DMS = vars.DMS\n\t\tlocal text = vars.text\n\t\tlocal displayTime = vars.displayTime\n\t\tlocal msgFor = vars.msgFor\n\n\t\tlocal s = mist.getLLString{units = units, acc = acc, DMS = DMS}\n\t\tlocal newText\n\t\tif text then\n\t\t\tif string.find(text, '%%s') then\t-- look for %s\n\t\t\t\tnewText = string.format(text, s)\t-- insert the coordinates into the message\n\t\t\telse\n\t\t\t\t-- just append to the end.\n\t\t\t\tnewText = text .. s\n\t\t\tend\n\t\telse\n\t\t\tnewText = s\n\t\tend\n\n\t\tmist.message.add{\n\t\t\ttext = newText,\n\t\t\tdisplayTime = displayTime,\n\t\t\tmsgFor = msgFor\n\t\t}\n\n\tend\n\n\t--[[\nvars.units- table of unit names (NOT unitNameTable- maybe this should change).\nvars.ref -\tvec3 ref point, maybe overload for vec2 as well?\nvars.alt - boolean, if used, includes altitude in string\nvars.metric - boolean, gives distance in km instead of NM.\nvars.text - text of the message\nvars.displayTime\nvars.msgFor - scope\n]]\n\tfunction mist.msgBR(vars)\n\t\tlocal units = vars.units\t-- technically, I don't really need to do this, but it helps readability.\n\t\tlocal ref = vars.ref -- vec2/vec3 will be handled in mist.getBRString\n\t\tlocal alt = vars.alt\n\t\tlocal metric = vars.metric\n\t\tlocal text = vars.text\n\t\tlocal displayTime = vars.displayTime\n\t\tlocal msgFor = vars.msgFor\n\n\t\tlocal s = mist.getBRString{units = units, ref = ref, alt = alt, metric = metric}\n\t\tlocal newText\n\t\tif text then\n\t\t\tif string.find(text, '%%s') then\t-- look for %s\n\t\t\t\tnewText = string.format(text, s)\t-- insert the coordinates into the message\n\t\t\telse\n\t\t\t\t-- just append to the end.\n\t\t\t\tnewText = text .. s\n\t\t\tend\n\t\telse\n\t\t\tnewText = s\n\t\tend\n\n\t\tmist.message.add{\n\t\t\ttext = newText,\n\t\t\tdisplayTime = displayTime,\n\t\t\tmsgFor = msgFor\n\t\t}\n\n\tend\n\n\t-- basically, just sub-types of mist.msgBR... saves folks the work of getting the ref point.\n\t--[[\nvars.units- table of unit names (NOT unitNameTable- maybe this should change).\nvars.ref -\tstring red, blue\nvars.alt - boolean, if used, includes altitude in string\nvars.metric - boolean, gives distance in km instead of NM.\nvars.text - text of the message\nvars.displayTime\nvars.msgFor - scope\n]]\n\tfunction mist.msgBullseye(vars)\n\t\tif mist.DBs.missionData.bullseye[string.lower(vars.ref)] then\n\t\t\tvars.ref = mist.DBs.missionData.bullseye[string.lower(vars.ref)]\n\t\t\tmist.msgBR(vars)\n\t\tend\n\tend\n\n\t--[[\nvars.units- table of unit names (NOT unitNameTable- maybe this should change).\nvars.ref -\tunit name of reference point\nvars.alt - boolean, if used, includes altitude in string\nvars.metric - boolean, gives distance in km instead of NM.\nvars.text - text of the message\nvars.displayTime\nvars.msgFor - scope\n]]\n\tfunction mist.msgBRA(vars)\n\t\tif Unit.getByName(vars.ref) and Unit.getByName(vars.ref):isExist() == true then\n\t\t\tvars.ref = Unit.getByName(vars.ref):getPosition().p\n\t\t\tif not vars.alt then\n\t\t\t\tvars.alt = true\n\t\t\tend\n\t\t\tmist.msgBR(vars)\n\t\tend\n\tend\n\n\t--[[ vars for mist.msgLeadingMGRS:\nvars.units - table of unit names\nvars.heading - direction\nvars.radius - number\nvars.headingDegrees - boolean, switches heading to degrees (optional)\nvars.acc - number, 0 to 5.\nvars.text - text of the message\nvars.displayTime\nvars.msgFor - scope\n]]\n\tfunction mist.msgLeadingMGRS(vars)\n\t\tlocal units = vars.units\t-- technically, I don't really need to do this, but it helps readability.\n\t\tlocal heading = vars.heading\n\t\tlocal radius = vars.radius\n\t\tlocal headingDegrees = vars.headingDegrees\n\t\tlocal acc = vars.acc\n\t\tlocal text = vars.text\n\t\tlocal displayTime = vars.displayTime\n\t\tlocal msgFor = vars.msgFor\n\n\t\tlocal s = mist.getLeadingMGRSString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc}\n\t\tlocal newText\n\t\tif text then\n\t\t\tif string.find(text, '%%s') then\t-- look for %s\n\t\t\t\tnewText = string.format(text, s)\t-- insert the coordinates into the message\n\t\t\telse\n\t\t\t\t-- just append to the end.\n\t\t\t\tnewText = text .. s\n\t\t\tend\n\t\telse\n\t\t\tnewText = s\n\t\tend\n\n\t\tmist.message.add{\n\t\t\ttext = newText,\n\t\t\tdisplayTime = displayTime,\n\t\t\tmsgFor = msgFor\n\t\t}\n\n\n\tend\n\n\t--[[ vars for mist.msgLeadingLL:\nvars.units - table of unit names\nvars.heading - direction, number\nvars.radius - number\nvars.headingDegrees - boolean, switches heading to degrees (optional)\nvars.acc - number of digits after decimal point (can be negative)\nvars.DMS -\tboolean, true if you want DMS. (optional)\nvars.text - text of the message\nvars.displayTime\nvars.msgFor - scope\n]]\n\tfunction mist.msgLeadingLL(vars)\n\t\tlocal units = vars.units\t-- technically, I don't really need to do this, but it helps readability.\n\t\tlocal heading = vars.heading\n\t\tlocal radius = vars.radius\n\t\tlocal headingDegrees = vars.headingDegrees\n\t\tlocal acc = vars.acc\n\t\tlocal DMS = vars.DMS\n\t\tlocal text = vars.text\n\t\tlocal displayTime = vars.displayTime\n\t\tlocal msgFor = vars.msgFor\n\n\t\tlocal s = mist.getLeadingLLString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc, DMS = DMS}\n\t\tlocal newText\n\n\t\tif text then\n\t\t\tif string.find(text, '%%s') then\t-- look for %s\n\t\t\t\tnewText = string.format(text, s)\t-- insert the coordinates into the message\n\t\t\telse\n\t\t\t\t-- just append to the end.\n\t\t\t\tnewText = text .. s\n\t\t\tend\n\t\telse\n\t\t\tnewText = s\n\t\tend\n\n\t\tmist.message.add{\n\t\t\ttext = newText,\n\t\t\tdisplayTime = displayTime,\n\t\t\tmsgFor = msgFor\n\t\t}\n\n\tend\n\n\t--[[\nvars.units - table of unit names\nvars.heading - direction, number\nvars.radius - number\nvars.headingDegrees - boolean, switches heading to degrees\t(optional)\nvars.metric - boolean, if true, use km instead of NM. (optional)\nvars.alt - boolean, if true, include altitude. (optional)\nvars.ref - vec3/vec2 reference point.\nvars.text - text of the message\nvars.displayTime\nvars.msgFor - scope\n]]\n\tfunction mist.msgLeadingBR(vars)\n\t\tlocal units = vars.units\t-- technically, I don't really need to do this, but it helps readability.\n\t\tlocal heading = vars.heading\n\t\tlocal radius = vars.radius\n\t\tlocal headingDegrees = vars.headingDegrees\n\t\tlocal metric = vars.metric\n\t\tlocal alt = vars.alt\n\t\tlocal ref = vars.ref -- vec2/vec3 will be handled in mist.getBRString\n\t\tlocal text = vars.text\n\t\tlocal displayTime = vars.displayTime\n\t\tlocal msgFor = vars.msgFor\n\n\t\tlocal s = mist.getLeadingBRString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, metric = metric, alt = alt, ref = ref}\n\t\tlocal newText\n\n\t\tif text then\n\t\t\tif string.find(text, '%%s') then\t-- look for %s\n\t\t\t\tnewText = string.format(text, s)\t-- insert the coordinates into the message\n\t\t\telse\n\t\t\t\t-- just append to the end.\n\t\t\t\tnewText = text .. s\n\t\t\tend\n\t\telse\n\t\t\tnewText = s\n\t\tend\n\n\t\tmist.message.add{\n\t\t\ttext = newText,\n\t\t\tdisplayTime = displayTime,\n\t\t\tmsgFor = msgFor\n\t\t}\n\tend\nend\n\n--- Demo functions.\n-- @section mist.demos\ndo -- mist.demos scope\n\tmist.demos = {}\n\n\tfunction mist.demos.printFlightData(unit)\n\t\tif unit:isExist() then\n\t\t\tlocal function printData(unit, prevVel, prevE, prevTime)\n\t\t\t\tlocal angles = mist.getAttitude(unit)\n\t\t\t\tif angles then\n\t\t\t\t\tlocal Heading = angles.Heading\n\t\t\t\t\tlocal Pitch = angles.Pitch\n\t\t\t\t\tlocal Roll = angles.Roll\n\t\t\t\t\tlocal Yaw = angles.Yaw\n\t\t\t\t\tlocal AoA = angles.AoA\n\t\t\t\t\tlocal ClimbAngle = angles.ClimbAngle\n\n\t\t\t\t\tif not Heading then\n\t\t\t\t\t\tHeading = 'NA'\n\t\t\t\t\telse\n\t\t\t\t\t\tHeading = string.format('%12.2f', mist.utils.toDegree(Heading))\n\t\t\t\t\tend\n\n\t\t\t\t\tif not Pitch then\n\t\t\t\t\t\tPitch = 'NA'\n\t\t\t\t\telse\n\t\t\t\t\t\tPitch = string.format('%12.2f', mist.utils.toDegree(Pitch))\n\t\t\t\t\tend\n\n\t\t\t\t\tif not Roll then\n\t\t\t\t\t\tRoll = 'NA'\n\t\t\t\t\telse\n\t\t\t\t\t\tRoll = string.format('%12.2f', mist.utils.toDegree(Roll))\n\t\t\t\t\tend\n\n\t\t\t\t\tlocal AoAplusYaw = 'NA'\n\t\t\t\t\tif AoA and Yaw then\n\t\t\t\t\t\tAoAplusYaw = string.format('%12.2f', mist.utils.toDegree((AoA^2 + Yaw^2)^0.5))\n\t\t\t\t\tend\n\n\t\t\t\t\tif not Yaw then\n\t\t\t\t\t\tYaw = 'NA'\n\t\t\t\t\telse\n\t\t\t\t\t\tYaw = string.format('%12.2f', mist.utils.toDegree(Yaw))\n\t\t\t\t\tend\n\n\t\t\t\t\tif not AoA then\n\t\t\t\t\t\tAoA = 'NA'\n\t\t\t\t\telse\n\t\t\t\t\t\tAoA = string.format('%12.2f', mist.utils.toDegree(AoA))\n\t\t\t\t\tend\n\n\t\t\t\t\tif not ClimbAngle then\n\t\t\t\t\t\tClimbAngle = 'NA'\n\t\t\t\t\telse\n\t\t\t\t\t\tClimbAngle = string.format('%12.2f', mist.utils.toDegree(ClimbAngle))\n\t\t\t\t\tend\n\t\t\t\t\tlocal unitPos = unit:getPosition()\n\t\t\t\t\tlocal unitVel = unit:getVelocity()\n\t\t\t\t\tlocal curTime = timer.getTime()\n\t\t\t\t\tlocal absVel = string.format('%12.2f', mist.vec.mag(unitVel))\n\n\n\t\t\t\t\tlocal unitAcc = 'NA'\n\t\t\t\t\tlocal Gs = 'NA'\n\t\t\t\t\tlocal axialGs = 'NA'\n\t\t\t\t\tlocal transGs = 'NA'\n\t\t\t\t\tif prevVel and prevTime then\n\t\t\t\t\t\tlocal xAcc = (unitVel.x - prevVel.x)/(curTime - prevTime)\n\t\t\t\t\t\tlocal yAcc = (unitVel.y - prevVel.y)/(curTime - prevTime)\n\t\t\t\t\t\tlocal zAcc = (unitVel.z - prevVel.z)/(curTime - prevTime)\n\n\t\t\t\t\t\tunitAcc = string.format('%12.2f', mist.vec.mag({x = xAcc, y = yAcc, z = zAcc}))\n\t\t\t\t\t\tGs = string.format('%12.2f', mist.vec.mag({x = xAcc, y = yAcc + 9.81, z = zAcc})/9.81)\n\t\t\t\t\t\taxialGs = string.format('%12.2f', mist.vec.dp({x = xAcc, y = yAcc + 9.81, z = zAcc}, unitPos.x)/9.81)\n\t\t\t\t\t\ttransGs = string.format('%12.2f', mist.vec.mag(mist.vec.cp({x = xAcc, y = yAcc + 9.81, z = zAcc}, unitPos.x))/9.81)\n\t\t\t\t\tend\n\n\t\t\t\t\tlocal E = 0.5*mist.vec.mag(unitVel)^2 + 9.81*unitPos.p.y\n\n\t\t\t\t\tlocal energy = string.format('%12.2e', E)\n\n\t\t\t\t\tlocal dEdt = 'NA'\n\t\t\t\t\tif prevE and prevTime then\n\t\t\t\t\t\tdEdt = string.format('%12.2e', (E - prevE)/(curTime - prevTime))\n\t\t\t\t\tend\n\n\t\t\t\t\ttrigger.action.outText(string.format('%-25s', 'Heading: ') .. Heading .. ' degrees\\n' .. string.format('%-25s', 'Roll: ') .. Roll .. ' degrees\\n' .. string.format('%-25s', 'Pitch: ') .. Pitch\n\t\t\t\t\t\t\t.. ' 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: ') ..\n\t\t\t\t\t\t\tClimbAngle .. ' degrees\\n' .. string.format('%-25s', 'Absolute Velocity: ') .. absVel .. ' m/s\\n' .. string.format('%-25s', 'Absolute Acceleration: ') .. unitAcc ..' m/s^2\\n'\n\t\t\t\t\t\t\t.. 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)\n\t\t\t\t\treturn unitVel, E, curTime\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tlocal function frameFinder(unit, prevVel, prevE, prevTime)\n\t\t\t\tif unit:isExist() then\n\t\t\t\t\tlocal currVel = unit:getVelocity()\n\t\t\t\t\tif 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\n\t\t\t\t\t\tprevVel, prevE, prevTime = printData(unit, prevVel, prevE, prevTime)\n\t\t\t\t\tend\n\t\t\t\t\tmist.scheduleFunction(frameFinder, {unit, prevVel, prevE, prevTime}, timer.getTime() + 0.005)\t-- it can't go this fast, limited to the 100 times a sec check right now.\n\t\t\t\tend\n\t\t\tend\n\n\n\t\t\tlocal curVel = unit:getVelocity()\n\t\t\tlocal curTime = timer.getTime()\n\t\t\tlocal curE = 0.5*mist.vec.mag(curVel)^2 + 9.81*unit:getPosition().p.y\n\t\t\tframeFinder(unit, curVel, curE, curTime)\n\n\t\tend\n\n\tend\n\nend\n\n\n\ndo\n\t--[[ stuff for marker panels\n\t\tmarker.add() add marker. Point of these functions is to simplify process and to store all mark panels added. \n\t\t-- generates Id if not specified or if multiple marks created.\n\t\t-- makes marks for countries by creating a mark for each client group in the country\n\t\t-- can create multiple marks if needed for groups and countries.\n\t\t-- adds marks to table for parsing and removing\n\t\t-- Uses similar structure as messages. Big differences is it doesn't only mark to groups.\n\t\t\tIf to All, then mark is for All\n\t\t\tif to coa mark is to coa\n\t\t\tif to specific units, mark is to group\n\t\t\t\n\t\t\t\n\t\t--------\n\t\tSTUFF TO Check\n\t\t--------\n\t\tIf mark added to a group before a client joins slot is synced.\n\t\tMark made for cliet A in Slot A. Client A leaves, Client B joins in slot A. What do they see?\n\t\t\n        \n\t\tMay need to automate process...\n        \n        \n        Could release this. But things I might need to add/change before doing so.\n            - removing marks and re-adding in same sequence doesn't appear to work. May need to schedule adding mark if updating an entry. \n            - I really dont like the old message style code for which groups get the message. Perhaps change to unitsTable and create function for getting humanUnitsTable. \n            = Event Handler, and check it, for marks added via script or user to deconflict Ids.\n            - Full validation of passed values for a specific shape type. \n\n\t]]\n    \n    local usedMarks = {}\n\n    local mDefs = {\n        coa = {\n            ['red'] = {fillColor = {.8, 0 , 0, .5}, color = {.8, 0 , 0, .5}, lineType = 2, fontSize = 16},\n            ['blue'] = {fillColor = {0, 0 , 0.8, .5}, color = {0, 0 , 0.8, .5}, lineType = 2, fontSize = 16},\n            ['all'] = {fillColor = {.1, .1 , .1, .5}, color = {.9, .9 , .9, .5}, lineType = 2, fontSize = 16},\n            ['neutral'] = {fillColor = {.1, .1 , .1, .5}, color = {.2, .2 , .2, .5}, lineType = 2, fontSize = 16},\n        },\n    }\n\n    local userDefs = {['red'] = {},['blue'] = {},['all'] = {},['neutral'] = {}}\n\t\n\tlocal mId = 1000\n    \n    local tNames = {'line', 'circle','rect', 'arrow', 'text', 'quad', 'freeform'}\n    local tLines = {[0] = 'no line', [1] = 'solid', [2] = 'dashed',[3] = 'dotted', [4] = 'dot dash' ,[5] = 'long dash', [6] = 'two dash'}\n    local coas = {[-1] = 'all', [0] = 'neutral', [1] = 'red', [2] = 'blue'}\n    \n    local altNames = {['poly'] = 7, ['lines'] = 1, ['polygon'] = 7 }\n    \n    local function draw(s)\n       --log:warn(s)\n        if type(s) == 'table' then \n            local mType = s.markType\n            if mType == 'panel' then \n                if markScope == 'coa' then\n                    trigger.action.markToCoalition(s.markId, s.text, s.pos, s.markFor, s.readOnly)\n                elseif markScope == 'group' then\n                    trigger.action.markToGroup(s.markId, s.text, s.pos, s.markFor, s.readOnly)\n                else\n                    trigger.action.markToAll(s.markId, s.text, s.pos, s.readOnly)\n                end\n            elseif mType == 'line' then \n                trigger.action.lineToAll(s.coa, s.markId, s.pos[1], s.pos[2], s.color, s.fillColor, s.lineType, s.readOnly, s.message)\n            elseif mType == 'circle' then \n                trigger.action.circleToAll(s.coa, s.markId, s.pos[1], s.radius, s.color, s.fillColor, s.lineType, s.readOnly, s.message)\n            elseif mType == 'rect' then \n                trigger.action.rectToAll(s.coa, s.markId, s.pos[1], s.pos[2], s.color, s.fillColor, s.lineType, s.readOnly, s.message)\n            elseif mType == 'arrow' then \n                trigger.action.arrowToAll(s.coa, s.markId, s.pos[1], s.pos[2], s.color, s.fillColor, s.lineType, s.readOnly, s.message)\n            elseif mType == 'text' then\n                trigger.action.textToAll(s.coa, s.markId, s.pos[1], s.color, s.fillColor, s.fontSize, s.readOnly, s.text)\n            elseif mType == 'quad' then \n                trigger.action.quadToAll(s.coa, s.markId, s.pos[1], s.pos[2], s.pos[3], s.pos[4], s.color, s.fillColor, s.lineType, s.readOnly, s.message)\n            end\n            if s.name and not usedMarks[s.name] then \n                usedMarks[s.name] = s.markId\n            end\n        elseif type(s) == 'string' then\n            --log:warn(s)\n            mist.utils.dostring(s)\n        end\n    end\n    \n\tmist.marker = {}\n\n\tlocal function markSpamFilter(recList, spamBlockOn)\n\t\t\n\t\tfor id, name in pairs(recList) do\n\t\t\tif name == spamBlockOn then\n\t\t\t\t--log:info('already on recList')\n\t\t\t\treturn recList\n\t\t\tend\n\t\tend\n\t\t--log:info('add to recList')\n\t\ttable.insert(recList, spamBlockOn)\n\t\treturn recList\n\tend\n\t\n\tlocal function iterate()\n\t\twhile mId < 10000000 do\n            if usedMarks[mId] then\n                mId = mId + 1\n            else\n                return mist.utils.deepCopy(mId)\n            end\n        end\n\t\treturn  mist.utils.deepCopy(mId)\n\tend\n    \n    local function validateColor(val)\n        if type(val) == 'table' then \n            for i = 1, #val do\n                if type(val[i]) == 'number' and val[i] > 1 then\n                    val[i] = val[i]/255 -- convert RGB values from 0-255 to 0-1 equivilent. \n                end\n            end\n        elseif type(val) == 'string' then\n            val = mist.utils.hexToRGB(val)\n        \n        end\n        return val\n    end\n    \n    local function checkDefs(vName, coa)\n        --log:warn('CheckDefs: $1 $2', vName, coa)\n        local coaName \n        if type(coa) == 'number' then\n            if coas[coa] then\n                coaName = coas[coa]\n            end\n        elseif type(coa) == 'string' then \n            coaName = coa\n        end\n        \n       -- log:warn(coaName)\n        if userDefs[coaName] and userDefs[coaName][vName] then\n            return userDefs[coaName][vName]\n        elseif mDefs.coa[coaName] and mDefs.coa[coaName][vName] then\n            return mDefs.coa[coaName][vName]\n        end\n\n    end\n    \n    function mist.marker.getNextId()\n        return iterate()\n    end\n    \n    local handle = {}\n    function handle:onEvent(e)\n        if world.event.S_EVENT_MARK_ADDED == e.id and e.idx then\n           usedMarks[e.idx] = e.idx\n           if not mist.DBs.markList[e.idx] then\n                --log:info('create maker DB: $1', e.idx)\n               mist.DBs.markList[e.idx] = {time = e.time, pos = e.pos, groupId = e.groupId, mType = 'panel', text = e.text, markId = e.idx, coalition = e.coalition}\n                if e.unit then\n                   mist.DBs.markList[e.idx].unit = e.initiaor:getName()\n                end\n                --log:info(mist.marker.list[e.idx])\n           end\n\n        elseif  world.event.S_EVENT_MARK_CHANGE == e.id and e.idx then\n            if mist.DBs.markList[e.idx] then\n               mist.DBs.markList[e.idx].text = e.text\n            end\n        elseif  world.event.S_EVENT_MARK_REMOVE == e.id and e.idx then\n            if mist.DBs.markList[e.idx] then\n               mist.DBs.markList[e.idx] = nil\n            end\n        end\n        \n    end\n    \n    local function getMarkId(id)\n        if mist.DBs.markList[id] then\n            return id\n        else\n            for mEntry, mData in pairs(mist.DBs.markList) do\n                if id == mData.name or id == mData.id then\n                    return mData.id\n                end\n            end\n        end\n    \n    \n    end\n    \n    \n    local function removeMark(id)\n        --log:info(\"Removing Mark: $1\", id\n        local removed = false\n        if type(id) == 'table' then \n            for ind, val in pairs(id) do\n                local r = getMarkId(val)\n                if r then \n                    trigger.action.removeMark(r)\n                    mist.DBs.markList[r] = nil\n                    removed = true\n                end\n            end\n          \n        else\n            local r = getMarkId(id)\n            trigger.action.removeMark(r)\n            mist.DBs.markList[r] = nil\n            removed = true\n        end\n        return removed\n    end\n    \n    world.addEventHandler(handle)\n    function mist.marker.setDefault(vars)\n        local anyChange = false\n        if vars and type(vars) == 'table' then\n            for l1, l1Data in pairs(vars) do\n                if type(l1Data) == 'table' then\n                    if not userDefs[l1] then\n                        userDefs[l1] = {}\n                    end\n                    \n                    for l2, l2Data in pairs(l1Data) do\n                        userDefs[l1][l2] = l2Data\n                        anyChange = true\n                    end\n                else\n                    userDefs[l1] = l1Data\n                    anyChange = true\n                end\n            end\n        \n        end\n        return anyChange\n    end\n\t\n\tfunction mist.marker.add(vars)\n\t\t--log:warn('markerFunc')\n\t\t--log:warn(vars)\n\t\tlocal pos           = vars.point or vars.points or vars.pos\n        local text          = vars.text or ''\n        local markFor       = vars.markFor\n        local markForCoa    = vars.markForCoa   or vars.coa  -- optional, can be used if you just want to mark to a specific coa/all\n        local id            = vars.id or vars.markId or vars.markid\n        local mType         = vars.mType or vars.markType or vars.type or 0\n        local color         = vars.color \n        local fillColor     = vars.fillColor \n        local lineType      = vars.lineType or 2\n        local readOnly      = vars.readOnly or true\n        local message       = vars.message \n        local fontSize      = vars.fontSize \n        local name          = vars.name\n        local radius        = vars.radius or 500\n        \n        local coa = -1\n        local usedId = 0\n        \n        \n\n        if id then \n            if type(id) ~= 'number' then\n                name = id\n                usedId = iterate()\n            end\n           --log:info('checkIfIdExist: $1', id)\n           --[[\n           Maybe it should treat id or name as the same thing/single value. \n           \n           If passed number it will use that as the first Id used and will delete/update any marks associated with that same value. \n           \n           \n           ]]\n           \n            local lId = id or name\n            if mist.DBs.markList[id] then ----------  NEED A BETTER WAY TO ASSOCIATE THE ID VALUE. CUrrnetly deleting from table and checking if that deleted entry exists which it wont. \n                --log:warn('active mark to be removed: $1', id)\n                name = mist.DBs.markList[id].name or id\n               removeMark(id)\n            elseif usedMarks[id] then\n                --log:info('exists in usedMarks: $1', id)\n               removeMark(usedMarks[id])\n            elseif name and usedMarks[name] then\n                --log:info('exists in usedMarks: $1', name)\n                removeMark(usedMarks[name])\n            end\n            usedId = iterate()\n            usedMarks[id] = usedId -- redefine the value used\n\t\tend\n        if name then\n            usedMarks[name] = usedId\n        end\n        \n        if usedId == 0 then\n            usedId = iterate()\n        end       \n        if mType then\n            if type(mType) == 'string' then\n                for i = 1, #tNames do\n                    --log:warn(tNames[i])\n                    if mist.stringMatch(mType, tNames[i]) then\n                        mType = i\n                        break\n                    end\n                end\n            elseif type(mType) == 'number' and mType > #tNames then\n                mType = 0\n            end\n        end\n        --log:warn(mType)\n\t\tlocal markScope = 'all'\n\t\tlocal markForTable = {}\n\t\t\n        if pos then\n\t\t\tif pos[1] then\n                for i = 1, #pos do\n                    pos[i] = mist.utils.makeVec3(pos[i])\n                end\n            \n            else\n                pos[1] = mist.utils.makeVec3(pos)\n            end\n            \n\t\tend\n\t\tif text and type(text) ~= string then\n\t\t\ttext = tostring(text)\n\t\tend\n        \n        if markForCoa then\n            if type(markForCoa) == 'string' then\n                if tonumber(markForCoa) then \n                    coa = coas[tonumber(markForCoa)]\n                    markScope = 'coa'\n                else\n                    for ind, cName in pairs(coas) do\n                        if mist.stringMatch(cName, markForCoa) then\n                            coa = ind\n                            markScope = 'coa'\n                            break\n                        end\n                    end\n                end\n            elseif type(markForCoa) == 'number' and markForCoa >=-1 and markForCoa <= #coas then\n                coa = markForCoa\n                markScore = 'coa'\n            end\n            \n            \n        \n        elseif markFor then\n\t\t\tif type(markFor) == 'number' then -- groupId\n\t\t\t\tif mist.DBs.groupsById[markFor] then\t\n\t\t\t\t\tmarkScope = 'group'\n\t\t\t\tend\n\t\t\telseif type(markFor) == 'string' then -- groupName\n\t\t\t\tif mist.DBs.groupsByName[markFor] then\t\n\t\t\t\t\tmarkScope = 'group'\n\t\t\t\t\tmarkFor = mist.DBs.groupsByName[markFor].groupId\n\t\t\t\tend\n\t\t\telseif type(markFor) == 'table' then -- multiple groupName, country, coalition, all\n\t\t\t\tmarkScope = 'table'\n\t\t\t\t--log:warn(markFor)\n\t\t\t\tfor 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. \n\t\t\t\t\tfor list, listData in pairs(forData) do\n\t\t\t\t\t\t--log:warn(listData)\n\t\t\t\t\t\tforIndex = string.lower(forIndex)\n\t\t\t\t\t\tif type(listData) == 'string' then\n\t\t\t\t\t\t\tlistData = string.lower(listData)\n\t\t\t\t\t\tend\n\t\t\t\t\t\tif listData == 'all' then\n\t\t\t\t\t\t\tmarkScope = 'all'\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\telseif (forIndex == 'coa' or forIndex == 'ca') then -- mark for coa or CA. \n\t\t\t\t\t\t\tlocal matches = 0\n                            for name, index in pairs (coalition.side) do\n\t\t\t\t\t\t\t\tif listData == string.lower(name) then\n\t\t\t\t\t\t\t\t\tmarkScope = 'coa'\n                                    markFor = index\n                                    coa = index\n                                    matches = matches + 1\n\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\tend\n                            if matches > 1 then\n                                markScope = 'all'\n                            end\n\t\t\t\t\t\telseif forIndex == 'countries' then\n                            for clienId, clientData in pairs(mist.DBs.humansById) do\n                                if (string.lower(clientData.country) == listData) or (forIndex == 'units' and string.lower(clientData.unitName) == listData) then\n                                    markForTable = markSpamFilter(markForTable, clientData.groupId)\n                                end\n                            end\n\t\t\t\t\t\telseif forIndex == 'unittypes' then -- mark to group\n\t\t\t\t\t\t-- iterate play units\n\t\t\t\t\t\t\tfor clientId, clientData in pairs(mist.DBs.humansById) do\n\t\t\t\t\t\t\t\tfor typeId, typeData in pairs(listData) do\n\t\t\t\t\t\t\t\t\t--log:warn(typeData)\n\t\t\t\t\t\t\t\t\tlocal found = false\n\t\t\t\t\t\t\t\t\tif list == 'all' or clientData.coalition and type(clientData.coalition) == 'string' and mist.stringMatch(clientData.coalition, list) then\n\t\t\t\t\t\t\t\t\t\tif mist.matchString(typeData, clientData.type) then\n\t\t\t\t\t\t\t\t\t\t\tfound = true\n\t\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\t\t-- check other known names for aircraft\n\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\tif found == true then\n\t\t\t\t\t\t\t\t\t\tmarkForTable = markSpamFilter(markForTable, clientData.groupId) -- sends info to other function to see if client is already recieving the current message.\n\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\tfor clientDataEntry, clientDataVal in pairs(clientData) do\n\t\t\t\t\t\t\t\t\t\tif type(clientDataVal) == 'string' then\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\tif mist.matchString(list, clientDataVal) == true or list == 'all' then\n\t\t\t\t\t\t\t\t\t\t\t\tlocal sString = typeData\n\t\t\t\t\t\t\t\t\t\t\t\tfor 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\n\t\t\t\t\t\t\t\t\t\t\t\t\tfor pIndex, pName in pairs(pTbl) do\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tif mist.stringMatch(sString, pName) then\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsString = rName\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\t\t\t\tif mist.stringMatch(sString, clientData.type) then\n\t\t\t\t\t\t\t\t\t\t\t\t\tfound = true\n\t\t\t\t\t\t\t\t\t\t\t\t\tmarkForTable = markSpamFilter(markForTable, clientData.groupId) -- sends info oto other function to see if client is already recieving the current message.\n\t\t\t\t\t\t\t\t\t\t\t\t\t--table.insert(newMsgFor, clientId)\n\t\t\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\t\tif found == true then\t-- shouldn't this be elsewhere too?\n\t\t\t\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\tend\n\n\t\t\t\t\t\t\tend\n\t\t\t\t\t\tend\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\telse\n\t\t\tmarkScope = 'all'\n\t\tend\n\t\t\n\t\tif mType == 0  then \n            local data = {markId = usedId, text = text, pos = pos[1], markScope = markScope, markFor = markFor, markType = 'panel', name = name, time = timer.getTime()}\n            if markScope ~= 'table' then\n                -- create marks\n                \n               mist.DBs.markList[usedId] = data-- add to the DB\n                 \n            else\n                if #markForTable > 0 then\n                    --log:info('iterate')\n                    local list = {}\n                    if id and not name then\n                        name = id\n                    end\n                    for i = 1, #markForTable do\n                        local newId = iterate()\n                        local data = {markId = newId, text = text, pos = pos[i], markFor = markForTable[i], markType = 'panel', name = name, readOnly = readOnly, time = timer.getTime()}\n                        mist.DBs.markList[newId] = data\n                        table.insert(list, data)\n\n                        draw(data)\n                        \n                    end\n                    return list\n                end\n            end\n\n            draw(data)\n            \n            return data\n\t\telseif mType > 0 then\n            local newId = iterate()\n            local fCal = {}\n            fCal[#fCal+1] = mType\n            fCal[#fCal+1] = coa\n            fCal[#fCal+1] = usedId\n            \n            local likeARainCoat = false\n            if mType == 7 then \n                local score = 0\n                for i = 1, #pos do\n                    if i < #pos then\n                        local val = ((pos[i+1].x - pos[i].x)*(pos[i+1].z + pos[i].z))\n                        --log:warn(\"$1 index score is: $2\", i, val)\n                        score = score + val\n                    else\n                       score = score + ((pos[1].x - pos[i].x)*(pos[1].z + pos[i].z))\n                    end\n                end\n                --log:warn(score)\n                if score > 0 then -- it is anti-clockwise. Due to DCS bug make it clockwise. \n                    likeARainCoat = true\n                    --log:warn('flip')\n                    \n                    for i = #pos, 1, -1 do\n                       fCal[#fCal+1] = pos[i]\n                    end\n                end\n            end\n            if likeARainCoat == false then \n                for i = 1, #pos do\n                    fCal[#fCal+1] = pos[i]\n                end\n            end\n            if radius and mType == 2 then\n                fCal[#fCal+1] = radius\n            end\n            \n            if not color then\n                color = checkDefs('color', coa)\n            else\n                color = validateColor(color)\n            end\n            fCal[#fCal+1] = color\n            \n            \n            if not fillColor then\n                fillColor = checkDefs('fillColor', coa)\n            else\n                fillColor = validateColor(fillColor)\n            end\n            fCal[#fCal+1] = fillColor\n            \n            if mType == 5 then -- text to all\n                if not fontSize then\n                     fontSize = checkDefs('fontSize', coa) or 16\n                end\n                fCal[#fCal+1] = fontSize\n            else\n                if not lineType then\n                    lineType = checkDefs('lineType', coa) or 2\n                end\n            end\n            fCal[#fCal+1] = lineType\n            if not readOnly then\n                readOnly = true\n            end\n            fCal[#fCal+1] = readOnly\n            if mType == 5 then \n                fCal[#fCal+1] = text\n            else\n            \n                fCal[#fCal+1] = message\n            end\n            local data = {coa = coa, markId = usedId, pos = pos, markFor = markFor, color = color, readOnly = readOnly, message = message, fillColor = fillColor, lineType = lineType, markType = tNames[mType], name = name, radius = radius, text = text, fontSize = fontSize, time = timer.getTime()}\n            mist.DBs.markList[usedId] = data\n            \n            if mType == 7 or  mType == 1 then \n                local s = \"trigger.action.markupToAll(\"\n\n                for i = 1, #fCal do\n                    --log:warn(fCal[i])\n                    if type(fCal[i]) == 'table' or type(fCal[i]) == 'boolean' then\n                        s = s .. mist.utils.oneLineSerialize(fCal[i])\n                    else\n                        s = s .. fCal[i]\n                    end\n                    if i < #fCal then \n                        s = s .. ','\n                    end\n                end\n\n                s = s .. ')'\n                if name then \n                    usedMarks[name] = usedId\n                end\n                draw(s)\n                \n            else\n\n                draw(data)\n                \n            end\n            return data\n        end\n\t\t\n\t\t\n\tend\n\t\n\tfunction mist.marker.remove(id)\n        return removeMark(id)\n\tend\n\t\n\tfunction mist.marker.get(id)\n        if  mist.DBs.markList[id] then\n            return  mist.DBs.markList[id] \n        end\n        local names = {}\n        for markId, data in pairs(mist.DBs.markList) do\n\t\t\tif data.name and data.name == id then\n                table.insert(names, data)\n\t\t\tend\n\t\tend\n        if #names >= 1 then\n            return names\n        end\n\tend\n\t\n   function  mist.marker.drawZone(name, v)\n        if mist.DBs.zonesByName[name] then\n            --log:warn(mist.DBs.zonesByName[name])\n            local vars = v or {}\n            local ref = mist.utils.deepCopy(mist.DBs.zonesByName[name])\n            \n            if ref.type == 2 then -- it is a quad, but use freeform cause it isnt as bugged\n                vars.mType = 6\n                vars.point = ref.verticies\n            else\n                vars.mType = 2\n                vars.radius = ref.radius\n                vars.point = ref.point\n            end\n            \n            \n            if not (vars.ignoreColor and vars.ignoreColor == true) and not vars.fillColor then\n                vars.fillColor = ref.color\n            end\n            \n            --log:warn(vars)\n            return mist.marker.add(vars)\n        end\n    end\n    \n    function mist.marker.drawShape(name, v)\n        if mist.DBs.drawingByName[name] then\n           \n            local d = v or {}\n            local o = mist.utils.deepCopy(mist.DBs.drawingByName[name])\n             --mist.marker.add({point = {x = o.mapX, z = o.mapY}, text = name})\n            --log:warn(o)\n            d.points = o.points or {}\n            if o.primitiveType == \"Polygon\" then\n                d.mType = 7\n                    \n                if o.polygonMode == \"rect\" then\n                    d.mType = 6\n                elseif o.polygonMode == \"circle\" then\n                    d.mType = 2\n                    d.points = {x = o.mapX, y = o.mapY}\n                    d.radius = o.radius\n                end\n            elseif o.primitiveType == \"TextBox\" then\n                d.mType = 5\n                d.points = {x = o.mapX, y = o.mapY}\n                d.text = o.text or d.text\n                d.fontSize = d.fontSize or o.fontSize\n            end\n            -- NOTE TO SELF. FIGURE OUT WHICH SHAPES NEED TO BE OFFSET. OVAL YES.\n            \n            if o.fillColorString and not d.fillColor then\n                d.fillColor = mist.utils.hexToRGB(o.fillColorString)\n            end\n            if o.colorString then\n                d.color = mist.utils.hexToRGB(o.colorString)\n            end\n\n            \n            if o.thickness == 0 then\n                d.lineType = 0\n            elseif o.style == 'solid' then\n                d.lineType = 1\n            elseif o.style == 'dot' then\n                d.lineType = 2\n            elseif o.style == 'dash' then\n                d.lineType = 3\n            else\n                d.lineType = 1\n            end\n            \n            \n            if o.primitiveType == \"Line\" and #d.points >= 2 then\n                d.mType = 1\n                local rtn = {}\n                for i = 1, #d.points -1 do\n                    local var = mist.utils.deepCopy(d)\n                    var.points = {}\n                    var.points[1] = d.points[i]\n                    var.points[2] = d.points[i+1]\n                    table.insert(rtn, mist.marker.add(var))\n                end\n                return rtn\n            else\n                if d.mType then\n                    --log:warn(d)\n                    return mist.marker.add(d)\n                end\n            end\n        end\n    \n    \n    end\n    \n    \n   --[[\n    function mist.marker.circle(v)\n    \n    \n    end\n]]\nend\n--- Time conversion functions.\n-- @section mist.time\ndo -- mist.time scope\n\tmist.time = {}\n\t-- returns a string for specified military time\n\t-- theTime is optional\n\t-- if present current time in mil time is returned\n\t-- if number or table the time is converted into mil tim\n\tfunction mist.time.convertToSec(timeTable)\n\n\t\tlocal timeInSec = 0\n\t\tif timeTable and type(timeTable) == 'number' then\n\t\t\ttimeInSec = timeTable\n\t\telseif timeTable and type(timeTable) == 'table' and (timeTable.d or timeTable.h or timeTable.m or timeTable.s) then\n\t\t\tif timeTable.d and type(timeTable.d) == 'number' then\n\t\t\t\ttimeInSec = timeInSec + (timeTable.d*86400)\n\t\t\tend\n\t\t\tif timeTable.h and type(timeTable.h) == 'number' then\n\t\t\t\ttimeInSec = timeInSec + (timeTable.h*3600)\n\t\t\tend\n\t\t\tif timeTable.m and type(timeTable.m) == 'number' then\n\t\t\t\ttimeInSec = timeInSec + (timeTable.m*60)\n\t\t\tend\n\t\t\tif timeTable.s and type(timeTable.s) == 'number' then\n\t\t\t\ttimeInSec = timeInSec + timeTable.s\n\t\t\tend\n\n\t\tend\n\t\treturn timeInSec\n\tend\n\n\tfunction mist.time.getDHMS(timeInSec)\n\t\tif timeInSec and type(timeInSec) == 'number' then\n\t\t\tlocal tbl = {d = 0, h = 0, m = 0, s = 0}\n\t\t\tif timeInSec > 86400 then\n\t\t\t\twhile timeInSec > 86400 do\n\t\t\t\t\ttbl.d = tbl.d + 1\n\t\t\t\t\ttimeInSec = timeInSec - 86400\n\t\t\t\tend\n\t\t\tend\n\t\t\tif timeInSec > 3600 then\n\t\t\t\twhile timeInSec > 3600 do\n\t\t\t\t\ttbl.h = tbl.h + 1\n\t\t\t\t\ttimeInSec = timeInSec - 3600\n\t\t\t\tend\n\t\t\tend\n\t\t\tif timeInSec > 60 then\n\t\t\t\twhile timeInSec > 60 do\n\t\t\t\t\ttbl.m = tbl.m + 1\n\t\t\t\t\ttimeInSec = timeInSec - 60\n\t\t\t\tend\n\t\t\tend\n\t\t\ttbl.s = timeInSec\n\t\t\treturn tbl\n\t\telse\n\t\t\tlog:error(\"Didn't recieve number\")\n\t\t\treturn\n\t\tend\n\tend\n\n\tfunction mist.getMilString(theTime)\n\t\tlocal timeInSec = 0\n\t\tif theTime then\n\t\t\ttimeInSec = mist.time.convertToSec(theTime)\n\t\telse\n\t\t\ttimeInSec = mist.utils.round(timer.getAbsTime(), 0)\n\t\tend\n\n\t\tlocal DHMS = mist.time.getDHMS(timeInSec)\n\n\t\treturn tostring(string.format('%02d', DHMS.h) .. string.format('%02d',DHMS.m))\n\tend\n\n\tfunction mist.getClockString(theTime, hour)\n\t\tlocal timeInSec = 0\n\t\tif theTime then\n\t\t\ttimeInSec = mist.time.convertToSec(theTime)\n\t\telse\n\t\t\ttimeInSec = mist.utils.round(timer.getAbsTime(), 0)\n\t\tend\n\t\tlocal DHMS = mist.time.getDHMS(timeInSec)\n\t\tif hour then\n\t\t\tif DHMS.h > 12 then\n\t\t\t\tDHMS.h = DHMS.h - 12\n\t\t\t\treturn tostring(string.format('%02d', DHMS.h) .. ':' .. string.format('%02d',DHMS.m)\t.. ':' .. string.format('%02d',DHMS.s) .. ' PM')\n\t\t\telse\n\t\t\t\treturn tostring(string.format('%02d', DHMS.h) .. ':' .. string.format('%02d',DHMS.m)\t.. ':' .. string.format('%02d',DHMS.s) .. ' AM')\n\t\t\tend\n\t\telse\n\t\t\treturn tostring(string.format('%02d', DHMS.h) .. ':' .. string.format('%02d',DHMS.m)\t.. ':' .. string.format('%02d',DHMS.s))\n\t\tend\n\tend\n\n\t-- returns the date in string format\n\t-- both variables optional\n\t-- first val returns with the month as a string\n\t-- 2nd val defins if it should be written the American way or the wrong way.\n\tfunction mist.time.getDate(convert)\n\t\tlocal cal = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} -- \n\t\tlocal date = {}\n\t\t\n\t\tif not env.mission.date then -- Not likely to happen. Resaving mission auto updates this to remove it.\n\t\t\tdate.d = 0\n\t\t\tdate.m = 6\n\t\t\tdate.y = 2011\n\t\telse \n\t\t\tdate.d = env.mission.date.Day\n\t\t\tdate.m = env.mission.date.Month\n\t\t\tdate.y = env.mission.date.Year\n\t\tend\n\t\tlocal start = 86400\n\t\tlocal timeInSec = mist.utils.round(timer.getAbsTime())\n\t\tif convert and type(convert) == 'number' then\n\t\t\ttimeInSec = convert\n\t\tend\n\t\tif timeInSec > 86400 then\n\t\t\twhile start < timeInSec do\n\t\t\t\tif date.d >= cal[date.m] then\n\t\t\t\t\tif date.m == 2 and date.d == 28 then -- HOLY COW we can edit years now. Gotta re-add this!\n\t\t\t\t\t\tif date.y % 4 == 0 and date.y % 100 == 0 and date.y % 400 ~= 0 or date.y % 4 > 0 then\n\t\t\t\t\t\t\tdate.m = date.m + 1\n\t\t\t\t\t\t\tdate.d = 0\n\t\t\t\t\t\tend\n\t\t\t\t\t\t--date.d = 29\n\t\t\t\t\telse\n\t\t\t\t\t\tdate.m = date.m + 1\n\t\t\t\t\t\tdate.d = 0\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\t\tif date.m == 13 then\n\t\t\t\t\tdate.m = 1\n\t\t\t\t\tdate.y = date.y + 1\n\t\t\t\tend\n\t\t\t\tdate.d = date.d + 1\n\t\t\t\tstart = start + 86400\n\t\t\t\t\n\t\t\tend\n\t\tend\n\t\treturn date\n\tend\n\n\tfunction mist.time.relativeToStart(time)\n\t\tif type(time) == 'number' then\n\t\t\treturn time - timer.getTime0()\n\t\tend\n\tend\n\n\tfunction mist.getDateString(rtnType, murica, oTime) -- returns date based on time\n\t\tlocal word = {'January', 'Feburary', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' } -- 'etc\n\t\tlocal curTime = 0\n\t\tif oTime then\n\t\t\tcurTime = oTime\n\t\telse\n\t\t\tcurTime = mist.utils.round(timer.getAbsTime())\n\t\tend\n\t\tlocal tbl = mist.time.getDate(curTime)\n\n\t\tif rtnType then\n\t\t\tif murica then\n\t\t\t\treturn tostring(word[tbl.m] .. ' ' .. tbl.d .. ' ' .. tbl.y)\n\t\t\telse\n\t\t\t\treturn tostring(tbl.d .. ' ' .. word[tbl.m] .. ' ' .. tbl.y)\n\t\t\tend\n\t\telse\n\t\t\tif murica then\n\t\t\t\treturn tostring(tbl.m .. '.' .. tbl.d .. '.' .. tbl.y)\n\t\t\telse\n\t\t\t\treturn tostring(tbl.d .. '.' .. tbl.m .. '.' .. tbl.y)\n\t\t\tend\n\t\tend\n\tend\n\t--WIP\n\tfunction 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.\n\t\tlocal curTime = mist.utils.round(timer.getAbsTime())\n\t\tlocal milTimeInSec = 0\n\n\t\tif milString and type(milString) == 'string' and string.len(milString) >= 4 then\n\t\t\tlocal hr = tonumber(string.sub(milString, 1, 2))\n\t\t\tlocal mi = tonumber(string.sub(milString, 3))\n\t\t\tmilTimeInSec = milTimeInSec + (mi*60) + (hr*3600)\n\t\telseif milString and type(milString) == 'table' and (milString.d or milString.h or milString.m or milString.s) then\n\t\t\tmilTimeInSec = mist.time.convertToSec(milString)\n\t\tend\n\n\t\tlocal startTime = timer.getTime0()\n\t\tlocal daysOffset = 0\n\t\tif startTime > 86400 then\n\t\t\tdaysOffset = mist.utils.round(startTime/86400)\n\t\t\tif daysOffset > 0 then\n\t\t\t\tmilTimeInSec = milTimeInSec *daysOffset\n\t\t\tend\n\t\tend\n\n\t\tif curTime > milTimeInSec then\n\t\t\tmilTimeInSec = milTimeInSec + 86400\n\t\tend\n\t\tif rtnType then\n\t\t\tmilTimeInSec = milTimeInSec - startTime\n\t\tend\n\t\treturn milTimeInSec\n\tend\n\n\nend\n\n--- Group task functions.\n-- @section tasks\ndo -- group tasks scope\n\tmist.ground = {}\n\tmist.fixedWing = {}\n\tmist.heli = {}\n\tmist.air = {}\n\tmist.air.fixedWing = {}\n\tmist.air.heli = {}\n    mist.ship = {}\n\n\t--- Tasks group to follow a route.\n\t-- This sets the mission task for the given group.\n\t-- Any wrapped actions inside the path (like enroute\n\t-- tasks) will be executed.\n\t-- @tparam Group group group to task.\n\t-- @tparam table path containing\n\t-- points defining a route.\n\tfunction mist.goRoute(group, path)\n\t\tlocal misTask = {\n\t\t\tid = 'Mission',\n\t\t\tparams = {\n\t\t\t\troute = {\n\t\t\t\t\tpoints = mist.utils.deepCopy(path),\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t\tif type(group) == 'string' then\n\t\t\tgroup = Group.getByName(group)\n\t\tend\n\t\tif group then\n\t\t\tlocal groupCon = group:getController()\n\t\t\tif groupCon then\n                --log:warn(misTask)\n\t\t\t\tgroupCon:setTask(misTask)\n\t\t\t\treturn true\n\t\t\tend\n\t\tend\n\t\treturn false\n\tend\n\n\t-- same as getGroupPoints but returns speed and formation type along with vec2 of point}\n\tfunction mist.getGroupRoute(groupIdent, task)\n\t\t-- refactor to search by groupId and allow groupId and groupName as inputs\n\t\tlocal gpId = groupIdent\n\t\t\tif mist.DBs.MEgroupsByName[groupIdent] then\n\t\t\t\tgpId = mist.DBs.MEgroupsByName[groupIdent].groupId\n\t\t\telse\n\t\t\t\tlog:error('$1 not found in mist.DBs.MEgroupsByName', groupIdent)\n\t\t\tend\n\n\t\tfor coa_name, coa_data in pairs(env.mission.coalition) do\n\t\t\tif type(coa_data) == 'table' then\n\t\t\t\tif coa_data.country then --there is a country table\n\t\t\t\t\tfor cntry_id, cntry_data in pairs(coa_data.country) do\n\t\t\t\t\t\tfor obj_cat_name, obj_cat_data in pairs(cntry_data) do\n\t\t\t\t\t\t\tif obj_cat_name == \"helicopter\" or obj_cat_name == \"ship\" or obj_cat_name == \"plane\" or obj_cat_name == \"vehicle\" then\t-- only these types have points\n\t\t\t\t\t\t\t\tif ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then\t--there's a group!\n\t\t\t\t\t\t\t\t\tfor group_num, group_data in pairs(obj_cat_data.group) do\n\t\t\t\t\t\t\t\t\t\tif group_data and group_data.groupId == gpId\tthen -- this is the group we are looking for\n\t\t\t\t\t\t\t\t\t\t\tif group_data.route and group_data.route.points and #group_data.route.points > 0 then\n\t\t\t\t\t\t\t\t\t\t\t\tlocal points = {}\n\n\t\t\t\t\t\t\t\t\t\t\t\tfor point_num, point in pairs(group_data.route.points) do\n\t\t\t\t\t\t\t\t\t\t\t\t\tlocal routeData = {}\n\t\t\t\t\t\t\t\t\t\t\t\t\tif env.mission.version > 7 and env.mission.version < 19 then\n\t\t\t\t\t\t\t\t\t\t\t\t\t\trouteData.name = env.getValueDictByKey(point.name)\n\t\t\t\t\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\t\t\t\t\trouteData.name = point.name\n\t\t\t\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\t\t\t\t\tif not point.point then\n\t\t\t\t\t\t\t\t\t\t\t\t\t\trouteData.x = point.x\n\t\t\t\t\t\t\t\t\t\t\t\t\t\trouteData.y = point.y\n\t\t\t\t\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\t\t\t\t\trouteData.point = point.point\t--it's possible that the ME could move to the point = Vec2 notation.\n\t\t\t\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\t\t\t\t\trouteData.form = point.action\n\t\t\t\t\t\t\t\t\t\t\t\t\trouteData.speed = point.speed\n\t\t\t\t\t\t\t\t\t\t\t\t\trouteData.alt = point.alt\n\t\t\t\t\t\t\t\t\t\t\t\t\trouteData.alt_type = point.alt_type\n\t\t\t\t\t\t\t\t\t\t\t\t\trouteData.airdromeId = point.airdromeId\n\t\t\t\t\t\t\t\t\t\t\t\t\trouteData.helipadId = point.helipadId\n\t\t\t\t\t\t\t\t\t\t\t\t\trouteData.type = point.type\n\t\t\t\t\t\t\t\t\t\t\t\t\trouteData.action = point.action\n\t\t\t\t\t\t\t\t\t\t\t\t\tif task then\n\t\t\t\t\t\t\t\t\t\t\t\t\t\trouteData.task = point.task\n\t\t\t\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\t\t\t\t\tpoints[point_num] = routeData\n\t\t\t\t\t\t\t\t\t\t\t\tend\n\n\t\t\t\t\t\t\t\t\t\t\t\treturn points\n\t\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\t\t\tlog:error('Group route not defined in mission editor for groupId: $1', gpId)\n\t\t\t\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t\t\t\tend\t--if group_data and group_data.name and group_data.name == 'groupname'\n\t\t\t\t\t\t\t\t\tend --for group_num, group_data in pairs(obj_cat_data.group) do\n\t\t\t\t\t\t\t\tend --if ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then\n\t\t\t\t\t\t\tend --if obj_cat_name == \"helicopter\" or obj_cat_name == \"ship\" or obj_cat_name == \"plane\" or obj_cat_name == \"vehicle\" or obj_cat_name == \"static\" then\n\t\t\t\t\t\tend --for obj_cat_name, obj_cat_data in pairs(cntry_data) do\n\t\t\t\t\tend --for cntry_id, cntry_data in pairs(coa_data.country) do\n\t\t\t\tend --if coa_data.country then --there is a country table\n\t\t\tend --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then\n\t\tend --for coa_name, coa_data in pairs(mission.coalition) do\n\tend\n\n\t-- function mist.ground.buildPath() end -- ????\n\n\tfunction mist.ground.patrolRoute(vars)\n\t\t--log:info('patrol')\n\t\tlocal tempRoute = {}\n\t\tlocal useRoute = {}\n\t\tlocal gpData = vars.gpData\n\t\tif type(gpData) == 'string' then\n\t\t\tgpData = Group.getByName(gpData)\n\t\tend\n\n\t\tlocal useGroupRoute\n\t\tif not vars.useGroupRoute then\n\t\t\tuseGroupRoute = vars.gpData\n\t\telse\n\t\t\tuseGroupRoute = vars.useGroupRoute\n\t\tend\n\t\tlocal routeProvided = false\n\t\tif not vars.route then\n\t\t\tif useGroupRoute then\n\t\t\t\ttempRoute = mist.getGroupRoute(useGroupRoute)\n\t\t\tend\n\t\telse\n\t\t\tuseRoute = vars.route\n\t\t\tlocal posStart = mist.getLeadPos(gpData)\n\t\t\tuseRoute[1] = mist.ground.buildWP(posStart, useRoute[1].action, useRoute[1].speed)\n\t\t\trouteProvided = true\n\t\tend\n\n\n\t\tlocal overRideSpeed = vars.speed or 'default'\n\t\tlocal pType = vars.pType\n\t\tlocal offRoadForm = vars.offRoadForm or 'default'\n\t\tlocal onRoadForm = vars.onRoadForm or 'default'\n\n\t\tif routeProvided == false and #tempRoute > 0 then\n\t\t\tlocal posStart = mist.getLeadPos(gpData)\n\n\n\t\t\tuseRoute[#useRoute + 1] = mist.ground.buildWP(posStart, offRoadForm, overRideSpeed)\n\t\t\tfor i = 1, #tempRoute do\n\t\t\t\tlocal tempForm = tempRoute[i].action\n\t\t\t\tlocal tempSpeed = tempRoute[i].speed\n\n\t\t\t\tif offRoadForm == 'default' then\n\t\t\t\t\ttempForm = tempRoute[i].action\n\t\t\t\tend\n\t\t\t\tif onRoadForm == 'default' then\n\t\t\t\t\tonRoadForm = 'On Road'\n\t\t\t\tend\n\t\t\t\tif (string.lower(tempRoute[i].action) == 'on road' or\tstring.lower(tempRoute[i].action) == 'onroad' or string.lower(tempRoute[i].action) == 'on_road') then\n\t\t\t\t\ttempForm = onRoadForm\n\t\t\t\telse\n\t\t\t\t\ttempForm = offRoadForm\n\t\t\t\tend\n\n\t\t\t\tif type(overRideSpeed) == 'number' then\n\t\t\t\t\ttempSpeed = overRideSpeed\n\t\t\t\tend\n\n\n\t\t\t\tuseRoute[#useRoute + 1] = mist.ground.buildWP(tempRoute[i], tempForm, tempSpeed)\n\t\t\tend\n\n\t\t\tif pType and string.lower(pType) == 'doubleback' then\n\t\t\t\tlocal curRoute = mist.utils.deepCopy(useRoute)\n\t\t\t\tfor i = #curRoute, 2, -1 do\n\t\t\t\t\tuseRoute[#useRoute + 1] = mist.ground.buildWP(curRoute[i], curRoute[i].action, curRoute[i].speed)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tuseRoute[1].action = useRoute[#useRoute].action -- make it so the first WP matches the last WP\n\t\tend\n\n\t\tlocal cTask3 = {}\n\t\tlocal newPatrol = {}\n\t\tnewPatrol.route = useRoute\n\t\tnewPatrol.gpData = gpData:getName()\n\t\tcTask3[#cTask3 + 1] = 'mist.ground.patrolRoute('\n\t\tcTask3[#cTask3 + 1] = mist.utils.oneLineSerialize(newPatrol)\n\t\tcTask3[#cTask3 + 1] = ')'\n\t\tcTask3 = table.concat(cTask3)\n\t\tlocal tempTask = {\n\t\t\tid = 'WrappedAction',\n\t\t\tparams = {\n\t\t\t\taction = {\n\t\t\t\t\tid = 'Script',\n\t\t\t\t\tparams = {\n\t\t\t\t\t\tcommand = cTask3,\n\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t\t\n\t\tuseRoute[#useRoute].task = tempTask\n\t\tlog:info(useRoute)\n\t\tmist.goRoute(gpData, useRoute)\n\n\t\treturn\n\tend\n\n\tfunction mist.ground.patrol(gpData, pType, form, speed)\n\t\tlocal vars = {}\n\n\t\tif type(gpData) == 'table' and gpData:getName() then\n\t\t\tgpData = gpData:getName()\n\t\tend\n\n\t\tvars.useGroupRoute = gpData\n\t\tvars.gpData = gpData\n\t\tvars.pType = pType\n\t\tvars.offRoadForm = form\n\t\tvars.speed = speed\n\n\t\tmist.ground.patrolRoute(vars)\n\n\t\treturn\n\tend\n\n\t-- No longer accepts path\n\tfunction mist.ground.buildWP(point, overRideForm, overRideSpeed)\n\n\t\tlocal wp = {}\n\t\twp.x = point.x\n\n\t\tif point.z then\n\t\t\twp.y = point.z\n\t\telse\n\t\t\twp.y = point.y\n\t\tend\n\t\tlocal form, speed\n\n\t\tif point.speed and not overRideSpeed then\n\t\t\twp.speed = point.speed\n\t\telseif type(overRideSpeed) == 'number' then\n\t\t\twp.speed = overRideSpeed\n\t\telse\n\t\t\twp.speed = mist.utils.kmphToMps(20)\n\t\tend\n\n\t\tif point.form and not overRideForm then\n\t\t\tform = point.form\n\t\telse\n\t\t\tform = overRideForm\n\t\tend\n\n\t\tif not form then\n\t\t\twp.action = 'Cone'\n\t\telse\n\t\t\tform = string.lower(form)\n\t\t\tif form == 'off_road' or form == 'off road' then\n\t\t\t\twp.action = 'Off Road'\n\t\t\telseif form == 'on_road' or form == 'on road' then\n\t\t\t\twp.action = 'On Road'\n\t\t\telseif form == 'rank' or form == 'line_abrest' or form == 'line abrest' or form == 'lineabrest'then\n\t\t\t\twp.action = 'Rank'\n\t\t\telseif form == 'cone' then\n\t\t\t\twp.action = 'Cone'\n\t\t\telseif form == 'diamond' then\n\t\t\t\twp.action = 'Diamond'\n\t\t\telseif form == 'vee' then\n\t\t\t\twp.action = 'Vee'\n\t\t\telseif form == 'echelon_left' or form == 'echelon left' or form == 'echelonl' then\n\t\t\t\twp.action = 'EchelonL'\n\t\t\telseif form == 'echelon_right' or form == 'echelon right' or form == 'echelonr' then\n\t\t\t\twp.action = 'EchelonR'\n\t\t\telse\n\t\t\t\twp.action = 'Cone' -- if nothing matched\n\t\t\tend\n\t\tend\n\n\t\twp.type = 'Turning Point'\n\n\t\treturn wp\n\n\tend\n\n\tfunction mist.fixedWing.buildWP(point, WPtype, speed, alt, altType)\n\n\t\tlocal wp = {}\n\t\twp.x = point.x\n\n\t\tif point.z then\n\t\t\twp.y = point.z\n\t\telse\n\t\t\twp.y = point.y\n\t\tend\n\n\t\tif alt and type(alt) == 'number' then\n\t\t\twp.alt = alt\n\t\telse\n\t\t\twp.alt = 2000\n\t\tend\n\n\t\tif altType then\n\t\t\taltType = string.lower(altType)\n\t\t\tif altType == 'radio' or altType == 'agl' then\n\t\t\t\twp.alt_type = 'RADIO'\n\t\t\telseif altType == 'baro' or altType == 'asl' then\n\t\t\t\twp.alt_type = 'BARO'\n\t\t\tend\n\t\telse\n\t\t\twp.alt_type = 'RADIO'\n\t\tend\n\n\t\tif point.speed then\n\t\t\tspeed = point.speed\n\t\tend\n\n\t\tif point.type then\n\t\t\tWPtype = point.type\n\t\tend\n\n\t\tif not speed then\n\t\t\twp.speed = mist.utils.kmphToMps(500)\n\t\telse\n\t\t\twp.speed = speed\n\t\tend\n\n\t\tif not WPtype then\n\t\t\twp.action =\t'Turning Point'\n\t\telse\n\t\t\tWPtype = string.lower(WPtype)\n\t\t\tif WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then\n\t\t\t\twp.action =\t'Fly Over Point'\n\t\t\telseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then\n\t\t\t\twp.action =\t'Turning Point'\n\t\t\telse\n\t\t\t\twp.action = 'Turning Point'\n\t\t\tend\n\t\tend\n\n\t\twp.type = 'Turning Point'\n\t\treturn wp\n\tend\n\n\tfunction mist.heli.buildWP(point, WPtype, speed, alt, altType)\n\n\t\tlocal wp = {}\n\t\twp.x = point.x\n\n\t\tif point.z then\n\t\t\twp.y = point.z\n\t\telse\n\t\t\twp.y = point.y\n\t\tend\n\n\t\tif alt and type(alt) == 'number' then\n\t\t\twp.alt = alt\n\t\telse\n\t\t\twp.alt = 500\n\t\tend\n\n\t\tif altType then\n\t\t\taltType = string.lower(altType)\n\t\t\tif altType == 'radio' or altType == 'agl' then\n\t\t\t\twp.alt_type = 'RADIO'\n\t\t\telseif altType == 'baro' or altType == 'asl' then\n\t\t\t\twp.alt_type = 'BARO'\n\t\t\tend\n\t\telse\n\t\t\twp.alt_type = 'RADIO'\n\t\tend\n\n\t\tif point.speed then\n\t\t\tspeed = point.speed\n\t\tend\n\n\t\tif point.type then\n\t\t\tWPtype = point.type\n\t\tend\n\n\t\tif not speed then\n\t\t\twp.speed = mist.utils.kmphToMps(200)\n\t\telse\n\t\t\twp.speed = speed\n\t\tend\n\n\t\tif not WPtype then\n\t\t\twp.action =\t'Turning Point'\n\t\telse\n\t\t\tWPtype = string.lower(WPtype)\n\t\t\tif WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then\n\t\t\t\twp.action =\t'Fly Over Point'\n\t\t\telseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then\n\t\t\t\twp.action = 'Turning Point'\n\t\t\telse\n\t\t\t\twp.action =\t'Turning Point'\n\t\t\tend\n\t\tend\n\n\t\twp.type = 'Turning Point'\n\t\treturn wp\n\tend\n\n\t-- need to return a Vec3 or Vec2?\n\tfunction mist.getRandPointInCircle(p, r, innerRadius, maxA, minA)\n\t\tlocal point = mist.utils.makeVec3(p)\n        local theta = 2*math.pi*math.random()\n        local radius = r or 1000\n\t\tlocal minR = innerRadius or 0\n\t\tif maxA and not minA then\n\t\t\ttheta = math.rad(math.random(0, maxA - math.random()))\n\t\telseif maxA and minA then\n            if minA < maxA then\n                theta = math.rad(math.random(minA, maxA) - math.random())\n            else\n                theta = math.rad(math.random(maxA, minA) - math.random())\n            end\n\t\tend\n\t\tlocal rad = math.random() + math.random()\n\t\tif rad > 1 then\n\t\t\trad = 2 - rad\n\t\tend\n\n\t\tlocal radMult\n\t\tif minR and minR <= radius then\n\t\t\t--radMult = (radius - innerRadius)*rad + innerRadius\n\t\t\tradMult = radius * math.sqrt((minR^2 + (radius^2 - minR^2) * math.random()) / radius^2)\n\t\telse\n\t\t\tradMult = radius*rad\n\t\tend\n\n\t\tlocal rndCoord\n\t\tif radius > 0 then\n\t\t\trndCoord = {x = math.cos(theta)*radMult + point.x, y = math.sin(theta)*radMult + point.z}\n\t\telse\n\t\t\trndCoord = {x = point.x, y = point.z}\n\t\tend\n\t\treturn rndCoord\n\tend\n\n\tfunction mist.getRandomPointInZone(zoneName, innerRadius, maxA, minA)\n\t\tif type(zoneName) == 'string'  then \n            local zone = mist.DBs.zonesByName[zoneName]\n            if zone.type and zone.type == 2 then\n                return mist.getRandomPointInPoly(zone.verticies)\n            else\n                return mist.getRandPointInCircle(zone.point, zone.radius, innerRadius, maxA, minA)\n            end\n        end\n\t\treturn false\n\tend\n\t\n\tfunction mist.getRandomPointInPoly(zone)\n\t\t--env.info('Zone Size: '.. #zone)\n        local avg = mist.getAvgPoint(zone)\n        --log:warn(avg)\n\t\tlocal radius = 0\n\t\tlocal minR = math.huge\n\t\tlocal newCoord = {}\n\t\tfor i = 1, #zone do\n\t\t\tif mist.utils.get2DDist(avg, zone[i]) > radius then\n\t\t\t\tradius = mist.utils.get2DDist(avg, zone[i])\n\t\t\tend\n\t\t\tif mist.utils.get2DDist(avg, zone[i]) < minR then\n\t\t\t\tminR = mist.utils.get2DDist(avg, zone[i])\n\t\t\tend\n\t\tend\n        --log:warn('Radius: $1', radius)\n        --log:warn('minR: $1', minR)\n\t\tlocal lSpawnPos = {}\n\t\tfor j = 1, 100 do\n\t\t\tnewCoord = mist.getRandPointInCircle(avg, radius)\n\t\t\tif mist.pointInPolygon(newCoord, zone) then\n\t\t\t\tbreak\n\t\t\tend\n\t\t\tif j == 100 then\n\t\t\t\tnewCoord = mist.getRandPointInCircle(avg, 50000)\n\t\t\t\tlog:warn(\"Failed to find point in poly; Giving random point from center of the poly\")\n\t\t\tend\n\t\tend\n\t\treturn newCoord\n\tend\n    \n    function mist.getWindBearingAndVel(p)\n        local point = mist.utils.makeVec3(o)\n        local gLevel = land.getHeight({x = point.x, y = point.z})\n        if point.y <= gLevel then\n            point.y = gLevel + 10\n        end\n        local t = atmosphere.getWind(point)\n        local bearing = math.tan(t.z/t.x)\n        local vel = math.sqrt(t.x^2 + t.z^2)\n        return bearing, vel\n    \n    end\n\n\tfunction mist.groupToRandomPoint(vars)\n\t\tlocal group = vars.group --Required\n\t\tlocal point = vars.point --required\n\t\tlocal radius = vars.radius or 0\n\t\tlocal innerRadius = vars.innerRadius\n\t\tlocal form = vars.form or 'Cone'\n\t\tlocal heading = vars.heading or math.random()*2*math.pi\n\t\tlocal headingDegrees = vars.headingDegrees\n\t\tlocal speed = vars.speed or mist.utils.kmphToMps(20)\n\n\n\t\tlocal useRoads\n\t\tif not vars.disableRoads then\n\t\t\tuseRoads = true\n\t\telse\n\t\t\tuseRoads = false\n\t\tend\n\n\t\tlocal path = {}\n\n\t\tif headingDegrees then\n\t\t\theading = headingDegrees*math.pi/180\n\t\tend\n\n\t\tif heading >= 2*math.pi then\n\t\t\theading = heading - 2*math.pi\n\t\tend\n\n\t\tlocal rndCoord = mist.getRandPointInCircle(point, radius, innerRadius)\n\n\t\tlocal offset = {}\n\t\tlocal posStart = mist.getLeadPos(group)\n\t\tif posStart then\n\t\t\toffset.x = mist.utils.round(math.sin(heading - (math.pi/2)) * 50 + rndCoord.x, 3)\n\t\t\toffset.z = mist.utils.round(math.cos(heading + (math.pi/2)) * 50 + rndCoord.y, 3)\n\t\t\tpath[#path + 1] = mist.ground.buildWP(posStart, form, speed)\n\n\n\t\t\tif useRoads == true and ((point.x - posStart.x)^2 + (point.z - posStart.z)^2)^0.5 > radius * 1.3 then\n\t\t\t\tpath[#path + 1] = mist.ground.buildWP({x = posStart.x + 11, z = posStart.z + 11}, 'off_road', speed)\n\t\t\t\tpath[#path + 1] = mist.ground.buildWP(posStart, 'on_road', speed)\n\t\t\t\tpath[#path + 1] = mist.ground.buildWP(offset, 'on_road', speed)\n\t\t\telse\n\t\t\t\tpath[#path + 1] = mist.ground.buildWP({x = posStart.x + 25, z = posStart.z + 25}, form, speed)\n\t\t\tend\n\t\tend\n\t\tpath[#path + 1] = mist.ground.buildWP(offset, form, speed)\n\t\tpath[#path + 1] = mist.ground.buildWP(rndCoord, form, speed)\n\n\t\tmist.goRoute(group, path)\n\n\t\treturn\n\tend\n\n\tfunction mist.groupRandomDistSelf(gpData, dist, form, heading, speed, disableRoads)\n\t\tlocal pos = mist.getLeadPos(gpData)\n\t\tlocal fakeZone = {}\n\t\tfakeZone.radius = dist or math.random(300, 1000)\n\t\tfakeZone.point = {x = pos.x, y = pos.y, z = pos.z}\n\t\tmist.groupToRandomZone(gpData, fakeZone, form, heading, speed, disableRoads)\n\n\t\treturn\n\tend\n\n\tfunction mist.groupToRandomZone(gpData, zone, form, heading, speed, disableRoads)\n\t\tif type(gpData) == 'string' then\n\t\t\tgpData = Group.getByName(gpData)\n\t\tend\n\n\t\tif type(zone) == 'string' then\n\t\t\tzone = mist.DBs.zonesByName[zone]\n\t\telseif type(zone) == 'table' and not zone.radius then\n\t\t\tzone =  mist.DBs.zonesByName[zone[math.random(1, #zone)]]\n\t\tend\n\n\t\tif speed then\n\t\t\tspeed = mist.utils.kmphToMps(speed)\n\t\tend\n\n\t\tlocal vars = {}\n\t\tvars.group = gpData\n\t\tvars.radius = zone.radius\n\t\tvars.form = form\n\t\tvars.headingDegrees = heading\n\t\tvars.speed = speed\n\t\tvars.point = mist.utils.zoneToVec3(zone)\n        vars.disableRoads = disableRoads\n\t\tmist.groupToRandomPoint(vars)\n\n\t\treturn\n\tend\n\n\tfunction mist.isTerrainValid(coord, terrainTypes) -- vec2/3 and enum or table of acceptable terrain types\n\t\tif coord.z then\n\t\t\tcoord.y = coord.z\n\t\tend\n\t\tlocal typeConverted = {}\n\n\t\tif type(terrainTypes) == 'string' then -- if its a string it does this check\n\t\t\tfor constId, constData in pairs(land.SurfaceType) do\n\t\t\t\tif string.lower(constId) == string.lower(terrainTypes) or string.lower(constData) == string.lower(terrainTypes) then\n\t\t\t\t\ttable.insert(typeConverted, constId)\n\t\t\t\tend\n\t\t\tend\n\t\telseif type(terrainTypes) == 'table' then -- if its a table it does this check\n\t\t\tfor typeId, typeData in pairs(terrainTypes) do\n\t\t\t\tfor constId, constData in pairs(land.SurfaceType) do\n\t\t\t\t\tif string.lower(constId) == string.lower(typeData) or string.lower(constData) == string.lower(typeData) then\n\t\t\t\t\t\ttable.insert(typeConverted, constId)\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\t\tfor validIndex, validData in pairs(typeConverted) do\n\t\t\tif land.getSurfaceType(coord) == land.SurfaceType[validData] then\n\t\t\t\tlog:info('Surface is : $1', validData)\n                return true\n\t\t\tend\n\t\tend\n\t\treturn false\n\tend\n\n\tfunction mist.terrainHeightDiff(coord, searchSize)\n\t\tlocal samples = {}\n\t\tlocal searchRadius = 5\n\t\tif searchSize then\n\t\t\tsearchRadius = searchSize\n\t\tend\n\t\tif type(coord) == 'string' then\n\t\t\tcoord = mist.utils.zoneToVec3(coord)\n\t\tend\n\n\t\tcoord = mist.utils.makeVec2(coord)\n\n\t\tsamples[#samples + 1] = land.getHeight(coord)\n\t\tfor i = 0, 360, 30 do\n\t\t\tsamples[#samples + 1] = land.getHeight({x = (coord.x + (math.sin(math.rad(i))*searchRadius)), y = (coord.y + (math.cos(math.rad(i))*searchRadius))})\n\t\t\tif searchRadius >= 20 then -- if search radius is sorta large, take a sample halfway between center and outer edge\n\t\t\t\tsamples[#samples + 1] = land.getHeight({x = (coord.x + (math.sin(math.rad(i))*(searchRadius/2))), y = (coord.y + (math.cos(math.rad(i))*(searchRadius/2)))})\n\t\t\tend\n\t\tend\n\t\tlocal tMax, tMin = 0, 1000000\n\t\tfor index, height in pairs(samples) do\n\t\t\tif height > tMax then\n\t\t\t\ttMax = height\n\t\t\tend\n\t\t\tif height < tMin then\n\t\t\t\ttMin = height\n\t\t\tend\n\t\tend\n\t\treturn mist.utils.round(tMax - tMin, 2)\n\tend\n\n\tfunction mist.groupToPoint(gpData, point, form, heading, speed, useRoads)\n\t\tif type(point) == 'string' then\n\t\t\tpoint = mist.DBs.zonesByName[point]\n\t\tend\n\t\tif speed then\n\t\t\tspeed = mist.utils.kmphToMps(speed)\n\t\tend\n\n\t\tlocal vars = {}\n\t\tvars.group = gpData\n\t\tvars.form = form\n\t\tvars.headingDegrees = heading\n\t\tvars.speed = speed\n\t\tvars.disableRoads = useRoads\n\t\tvars.point = mist.utils.zoneToVec3(point)\n\t\tmist.groupToRandomPoint(vars)\n\n\t\treturn\n\tend\n\n\tfunction mist.getLeadPos(group)\n\t\tif type(group) == 'string' then -- group name\n\t\t\tgroup = Group.getByName(group)\n\t\tend\n\t\t\n\t\tlocal units = group:getUnits()\n\n\t\tlocal leader = units[1]\n\t\tif Unit.getLife(leader) == 0 or not Unit.isExist(leader) then\t-- SHOULD be good, but if there is a bug, this code future-proofs it then.\n\t\t\tlocal lowestInd = math.huge\n\t\t\tfor ind, unit in pairs(units) do\n\t\t\t\tif Unit.isExist(unit) and ind < lowestInd then\n\t\t\t\t\tlowestInd = ind\n\t\t\t\t\treturn unit:getPosition().p\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\t\tif leader and Unit.isExist(leader) then\t-- maybe a little too paranoid now...\n\t\t\treturn leader:getPosition().p\n\t\tend\n\tend\n    \n    function mist.groupIsDead(groupName) -- copy more or less from on station\n\t\tif Group.getByName(groupName) then \n            local gp = Group.getByName(groupName)\n            if  #gp:getUnits() > 0 or gp:isExist() == true  then\n                return false\n            end\n\t\tend\n\t\treturn true\n\tend\n\nend\n\n--- Database tables.\n-- @section mist.DBs\n\n--- Mission data\n-- @table mist.DBs.missionData\n-- @field startTime mission start time\n-- @field theatre mission theatre/map e.g. Caucasus\n-- @field version mission version\n-- @field files mission resources\n\n--- Tables used as parameters.\n-- @section varTables\n\n--- mist.flagFunc.units_in_polygon parameter table.\n-- @table unitsInPolygonVars\n-- @tfield table unit name table @{UnitNameTable}.\n-- @tfield table zone table defining a polygon.\n-- @tfield number|string flag flag to set to true.\n-- @tfield[opt] number|string stopflag if set to true the function\n-- will stop evaluating.\n-- @tfield[opt] number maxalt maximum altitude (MSL) for the\n-- polygon.\n-- @tfield[opt] number req_num minimum number of units that have\n-- to be in the polygon.\n-- @tfield[opt] number interval sets the interval for\n-- checking if units are inside of the polygon in seconds. Default: 1.\n-- @tfield[opt] boolean toggle switch the flag to false if required\n-- conditions are not met. Default: false.\n-- @tfield[opt] table unitTableDef\n--- Logger class.\n-- @type mist.Logger\ndo -- mist.Logger scope\n\tmist.Logger = {}\n\n\t--- parses text and substitutes keywords with values from given array.\n\t-- @param text string containing keywords to substitute with values\n\t-- or a variable.\n\t-- @param ... variables to use for substitution in string.\n\t-- @treturn string new string with keywords substituted or\n\t-- value of variable as string.\n\tlocal function formatText(text, ...)\n\t\tif type(text) ~= 'string' then\n\t\t\tif type(text) == 'table' then\n\t\t\t\ttext = mist.utils.oneLineSerialize(text)\n\t\t\telse\n\t\t\t\ttext = tostring(text)\n\t\t\tend\n\t\telse\n\t\t\tfor index,value in ipairs(arg) do\n\t\t\t\t-- TODO: check for getmetatabel(value).__tostring\n\t\t\t\tif type(value) == 'table' then\n\t\t\t\t\tvalue = mist.utils.oneLineSerialize(value)\n\t\t\t\telse\n\t\t\t\t\tvalue = tostring(value)\n\t\t\t\tend\n\t\t\t\ttext = text:gsub('$' .. index, value)\n\t\t\tend\n\t\tend\n    local fName = nil\n    local cLine = nil\n\t\tif debug then\n\t\t\tlocal dInfo = debug.getinfo(3)\n\t\t\tfName = dInfo.name\n\t\t\tcLine = dInfo.currentline\n\t\t\t-- local fsrc = dinfo.short_src\n\t\t\t--local fLine = dInfo.linedefined\n\t\tend\n\t\tif fName and cLine then\n\t\t\treturn fName .. '|' .. cLine .. ': ' .. text\n\t\telseif cLine then\n\t\t\treturn cLine .. ': ' .. text\n\t\telse\n\t\t\treturn ' ' .. text\n\t\tend\n\tend\n\n\tlocal function splitText(text)\n\t\tlocal tbl = {}\n\t\twhile text:len() > 4000 do\n\t\t\tlocal sub = text:sub(1, 4000)\n\t\t\ttext = text:sub(4001)\n\t\t\ttable.insert(tbl, sub)\n\t\tend\n\t\ttable.insert(tbl, text)\n\t\treturn tbl\n\tend\n\n\t--- Creates a new logger.\n\t-- Each logger has it's own tag and log level.\n\t-- @tparam string tag tag which appears at the start of\n\t-- every log line produced by this logger.\n\t-- @tparam[opt] number|string level the log level defines which messages\n\t-- will be logged and which will be omitted. Log level 3 beeing the most verbose\n\t-- and 0 disabling all output. This can also be a string. Allowed strings are:\n\t-- \"none\" (0), \"error\" (1), \"warning\" (2) and \"info\" (3).\n\t-- @usage myLogger = mist.Logger:new(\"MyScript\")\n\t-- @usage myLogger = mist.Logger:new(\"MyScript\", 2)\n\t-- @usage myLogger = mist.Logger:new(\"MyScript\", \"info\")\n\t-- @treturn mist.Logger\n\tfunction mist.Logger:new(tag, level)\n\t\tlocal l = {tag = tag}\n\t\tsetmetatable(l, self)\n\t\tself.__index = self\n\t\tl:setLevel(level)\n\t\treturn l\n\tend\n\n\t--- Sets the level of verbosity for this logger.\n\t-- @tparam[opt] number|string level the log level defines which messages\n\t-- will be logged and which will be omitted. Log level 3 beeing the most verbose\n\t-- and 0 disabling all output. This can also[ be a string. Allowed strings are:\n\t-- \"none\" (0), \"error\" (1), \"warning\" (2) and \"info\" (3).\n\t-- @usage myLogger:setLevel(\"info\")\n\t-- @usage -- log everything\n\t--myLogger:setLevel(3)\n\tfunction mist.Logger:setLevel(level)\n        if not level then\n\t\t\tself.level = 2\n\t\telse\n\t\t\tif type(level) == 'string' then\n\t\t\t\tif level == 'none' or level == 'off' then\n\t\t\t\t\tself.level = 0\n\t\t\t\telseif level == 'error' then\n\t\t\t\t\tself.level = 1\n\t\t\t\telseif level == 'warning' or level == 'warn' then\n\t\t\t\t\tself.level = 2\n\t\t\t\telseif level == 'info' then\n\t\t\t\t\tself.level = 3\n\t\t\t\tend\n\t\t\telseif type(level) == 'number' then\n\t\t\t\tself.level = level\n\t\t\telse\n\t\t\t\tself.level = 2\n\t\t\tend\n\t\tend\n\tend\n\n\t--- Logs error and shows alert window.\n\t-- This logs an error to the dcs.log and shows a popup window,\n\t-- pausing the simulation. This works always even if logging is\n\t-- disabled by setting a log level of \"none\" or 0.\n\t-- @tparam string text the text with keywords to substitute.\n\t-- @param ... variables to be used for substitution.\n\t-- @usage myLogger:alert(\"Shit just hit the fan! WEEEE!!!11\")\n\tfunction mist.Logger:alert(text, ...)\n\t\ttext = formatText(text, unpack(arg))\n\t\tif text:len() > 4000 then\n\t\t\tlocal texts = splitText(text)\n\t\t\tfor i = 1, #texts do\n\t\t\t\tif i == 1 then\n\t\t\t\t\tenv.error(self.tag .. '|' .. texts[i], true)\n\t\t\t\telse\n\t\t\t\t\tenv.error(texts[i])\n\t\t\t\tend\n\t\t\tend\n\t\telse\n\t\t\tenv.error(self.tag .. '|' .. text, true)\n\t\tend\n\tend\n\n\t--- Logs a message, disregarding the log level.\n\t-- @tparam string text the text with keywords to substitute.\n\t-- @param ... variables to be used for substitution.\n\t-- @usage myLogger:msg(\"Always logged!\")\n\tfunction mist.Logger:msg(text, ...)\n\t\ttext = formatText(text, unpack(arg))\n\t\tif text:len() > 4000 then\n\t\t\tlocal texts = splitText(text)\n\t\t\tfor i = 1, #texts do\n\t\t\t\tif i == 1 then\n\t\t\t\t\tenv.info(self.tag .. '|' .. texts[i])\n\t\t\t\telse\n\t\t\t\t\tenv.info(texts[i])\n\t\t\t\tend\n\t\t\tend\n\t\telse\n\t\t\tenv.info(self.tag .. '|' .. text)\n\t\tend\n\tend\n\n\t--- Logs an error.\n\t-- logs a message prefixed with this loggers tag to dcs.log as\n\t-- long as at least the \"error\" log level (1) is set.\n\t-- @tparam string text the text with keywords to substitute.\n\t-- @param ... variables to be used for substitution.\n\t-- @usage myLogger:error(\"Just an error!\")\n\t-- @usage myLogger:error(\"Foo is $1 instead of $2\", foo, \"bar\")\n\tfunction mist.Logger:error(text, ...)\n\t\tif self.level >= 1 then\n\t\t\ttext = formatText(text, unpack(arg))\n\t\t\tif text:len() > 4000 then\n\t\t\t\tlocal texts = splitText(text)\n\t\t\t\tfor i = 1, #texts do\n\t\t\t\t\tif i == 1 then\n\t\t\t\t\t\tenv.error(self.tag .. '|' .. texts[i])\n\t\t\t\t\telse\n\t\t\t\t\t\tenv.error(texts[i])\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\telse\n\t\t\t\tenv.error(self.tag .. '|' .. text, mistSettings.errorPopup)\n\t\t\tend\n\t\tend\n\tend\n\n\t--- Logs a warning.\n\t-- logs a message prefixed with this loggers tag to dcs.log as\n\t-- long as at least the \"warning\" log level (2) is set.\n\t-- @tparam string text the text with keywords to substitute.\n\t-- @param ... variables to be used for substitution.\n\t-- @usage myLogger:warn(\"Mother warned you! Those $1 from the interwebs are $2\", {\"geeks\", 1337})\n\tfunction mist.Logger:warn(text, ...)\n\t\tif self.level >= 2 then\n\t\t\ttext = formatText(text, unpack(arg))\n\t\t\tif text:len() > 4000 then\n\t\t\t\tlocal texts = splitText(text)\n\t\t\t\tfor i = 1, #texts do\n\t\t\t\t\tif i == 1 then\n\t\t\t\t\t\tenv.warning(self.tag .. '|' .. texts[i])\n\t\t\t\t\telse\n\t\t\t\t\t\tenv.warning(texts[i])\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\telse\n\t\t\t\tenv.warning(self.tag .. '|' .. text, mistSettings.warnPopup)\n\t\t\tend\n\t\tend\n\tend\n\n\t--- Logs a info.\n\t-- logs a message prefixed with this loggers tag to dcs.log as\n\t-- long as the highest log level (3) \"info\" is set.\n\t-- @tparam string text the text with keywords to substitute.\n\t-- @param ... variables to be used for substitution.\n\t-- @see warn\n\tfunction mist.Logger:info(text, ...)\n\t\tif self.level >= 3 then\n\t\t\ttext = formatText(text, unpack(arg))\n\t\t\tif text:len() > 4000 then\n\t\t\t\tlocal texts = splitText(text)\n\t\t\t\tfor i = 1, #texts do\n\t\t\t\t\tif i == 1 then\n\t\t\t\t\t\tenv.info(self.tag .. '|' .. texts[i])\n\t\t\t\t\telse\n\t\t\t\t\t\tenv.info(texts[i])\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\telse\n\t\t\t\tenv.info(self.tag .. '|' .. text, mistSettings.infoPopup)\n\t\t\tend\n\t\tend\n\tend\n\nend\n\n\n-- initialize mist\nmist.init()\nenv.info(('Mist version ' .. mist.majorVersion .. '.' .. mist.minorVersion .. '.' .. mist.build .. ' loaded.'))\n\n-- vim: noet:ts=2:sw=2\n"
  },
  {
    "path": "demo-missions/moose_a2a_connector/skynet-and-moose-a2a-dispatcher-setup.lua",
    "content": "do\n\n\n--Setup Syknet IADS:\nredIADS = SkynetIADS:create('Enemy IADS')\n\n\nlocal iadsDebug = redIADS:getDebugSettings()  \niadsDebug.IADSStatus = true\niadsDebug.contacts = true\n\n--[[\niadsDebug.radarWentDark = true\niadsDebug.radarWentLive = true\niadsDebug.ewRadarNoConnection = true\niadsDebug.samNoConnection = true\niadsDebug.jammerProbability = true\niadsDebug.addedEWRadar = true\niadsDebug.hasNoPower = true\niadsDebug.addedSAMSite = true\niadsDebug.warnings = true\niadsDebug.harmDefence = true\niadsDebug.samSiteStatusEnvOutput = true\niadsDebug.earlyWarningRadarStatusEnvOutput = true\n--]]\n\nredIADS:addSAMSitesByPrefix('SAM')\n\nlocal power = StaticObject.getByName('power-source')\nredIADS:addEarlyWarningRadarsByPrefix('EW')\nredIADS:getEarlyWarningRadarByUnitName('EW-1'):addConnectionNode(power)\n\nredIADS:activate()\n\n\n-- Define a SET_GROUP object that builds a collection of groups that define the EWR network.\nDetectionSetGroup = SET_GROUP:New()\n\n-- add the MOOSE SET_GROUP to the Skynet IADS, from now on Skynet will update active radars that the MOOSE SET_GROUP can use for EW detection.\nredIADS:addMooseSetGroup(DetectionSetGroup)\n\n-- Setup the detection and group targets to a 30km range!\nDetection = DETECTION_AREAS:New( DetectionSetGroup, 30000 )\n\n-- Setup the A2A dispatcher, and initialize it.\nA2ADispatcher = AI_A2A_DISPATCHER:New( Detection )\n\n-- Set 100km as the radius to engage any target by airborne friendlies.\nA2ADispatcher:SetEngageRadius() -- 100000 is the default value.\n\n-- Set 200km as the radius to ground control intercept.\nA2ADispatcher:SetGciRadius() -- 200000 is the default value.\n\nCCCPBorderZone = ZONE_POLYGON:New( \"RED-BORDER\", GROUP:FindByName( \"RED-BORDER\" ) )\nA2ADispatcher:SetBorderZone( CCCPBorderZone )\n\nA2ADispatcher:SetSquadron( \"Kutaisi\", AIRBASE.Caucasus.Kutaisi, { \"Squadron red SU-27\" }, 2 )\nA2ADispatcher:SetSquadronGrouping( \"Kutaisi\", 2 )\nA2ADispatcher:SetSquadronGci( \"Kutaisi\", 900, 1200 )\nA2ADispatcher:SetTacticalDisplay(true)\nA2ADispatcher:Start()\n\n--test to see which groups are added and removed to the SET_GROUP at runtime by Skynet:\nfunction outputNames()\n\tenv.info(\"IADS Radar Groups added by Skynet:\")\n\tenv.info(DetectionSetGroup:GetObjectNames())\nend\n\nmist.scheduleFunction(outputNames, self, 1, 2)\n--end test\nend\n"
  },
  {
    "path": "demo-missions/skynet-iads-compiled.lua",
    "content": "env.info(\"--- SKYNET VERSION: 3.3.0 | BUILD TIME: 29.12.2023 2311Z ---\")\ndo\n--this file contains the required units per sam type\nsamTypesDB = {\t\n\t['S-200'] = {\n        ['type'] = 'complex',\n        ['searchRadar'] = {\n            ['RLS_19J6'] = {\n                ['name'] = {\n                    ['NATO'] = 'Tin Shield',\n                },\n\t\t\t}, \n\t\t\t['p-19 s-125 sr'] = {\n\t\t\t\t['name'] = {\n\t\t\t\t\t['NATO'] = 'Flat Face',\n\t\t\t\t},\n\t\t\t},\t\n\t\t},\n        ['EWR P-37 BAR LOCK'] = {\n            ['Name'] = {\n              ['NATO'] = \"Bar lock\",\n            },   \n        },\n        ['trackingRadar'] = {\n            ['RPC_5N62V'] = {\n            },\n        },\n        ['launchers'] = {\n            ['S-200_Launcher'] = {\n            },\n        },\n        ['name'] = {\n            ['NATO'] = 'SA-5 Gammon',\n        },\n        ['harm_detection_chance'] = 60\n    },\n\t['S-300'] = {\n\t\t['type'] = 'complex',\n\t\t['searchRadar'] = {\n\t\t\t['S-300PS 40B6MD sr'] = {\n\t\t\t\t['name'] = {\n\t\t\t\t\t['NATO'] = 'Clam Shell',\n\t\t\t\t},\n\t\t\t},\n\t\t\t['S-300PS 64H6E sr'] = {\n\t\t\t\t['name'] = {\n\t\t\t\t\t['NATO'] = 'Big Bird',\n\t\t\t\t},\n\t\t\t},\n\t\t\t['S-300PS 40B6MD sr_19J6'] = {\n\t\t\t\t['name'] = {\n\t\t\t\t\t['NATO'] = 'Tin Shield',\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t['trackingRadar'] = {\n\t\t\t['S-300PS 40B6M tr'] = {\n\t\t\t},\t\n\t\t\t['S-300PS 5H63C 30H6_tr'] = {\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['S-300PS 5P85D ln'] = {\n\t\t\t},\n\t\t\t['S-300PS 5P85C ln'] = {\n\t\t\t},\n\t\t},\n\t\t['misc'] = {\n\t\t\t['S-300PS 54K6 cp'] = {\n\t\t\t\t['required'] = true,\n\t\t\t},\n\t\t},\n\t\t['name'] = {\n\t\t\t['NATO'] = 'SA-10 Grumble',\n\t\t},\n\t\t['harm_detection_chance'] = 90,\n\t\t['can_engage_harm'] = true\n\t},\n\t['Buk'] = {\n\t\t['type'] = 'complex',\n\t\t['searchRadar'] = {\n\t\t\t['SA-11 Buk SR 9S18M1'] = {\n\t\t\t\t['name'] = {\n\t\t\t\t\t['NATO'] = 'Snow Drift',\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['SA-11 Buk LN 9A310M1'] = {\n\t\t\t},\n\t\t},\n\t\t['misc'] = {\n\t\t\t['SA-11 Buk CC 9S470M1'] = {\n\t\t\t\t['required'] = true,\n\t\t\t},\n\t\t},\n\t\t['name'] = {\n\t\t\t['NATO'] = 'SA-11 Gadfly',\n\t\t},\n\t\t['harm_detection_chance'] = 70\n\t},\n\t['S-125'] = {\n\t\t['type'] = 'complex',\n\t\t['searchRadar'] = {\n\t\t\t['p-19 s-125 sr'] = {\n\t\t\t\t['name'] = {\n\t\t\t\t\t['NATO'] = 'Flat Face',\n\t\t\t\t},\n\t\t\t},\t\n\t\t},\n\t\t['trackingRadar'] = {\n\t\t\t['snr s-125 tr'] = {\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['5p73 s-125 ln'] = {\n\t\t\t},\n\t\t},\n\t\t['name'] = {\n\t\t\t['NATO'] = 'SA-3 Goa',\n\t\t},\n\t\t['harm_detection_chance'] = 30\n\t},\n    ['S-75'] = {\n\t\t['type'] = 'complex',\n\t\t['searchRadar'] = {\n\t\t\t['p-19 s-125 sr'] = {\n\t\t\t\t['name'] = {\n\t\t\t\t\t['NATO'] = 'Flat Face',\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t['trackingRadar'] = {\n\t\t\t['SNR_75V'] = {\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['S_75M_Volhov'] = {\n\t\t\t},\n\t\t},\n\t\t['name'] = {\n\t\t\t['NATO'] = 'SA-2 Guideline',\n\t\t},\n\t\t['harm_detection_chance'] = 30\n\t},\n\t['Kub'] = {\n\t\t['type'] = 'complex',\n\t\t['searchRadar'] = {\n\t\t\t['Kub 1S91 str'] = {\n\t\t\t\t['name'] = {\n\t\t\t\t\t['NATO'] = 'Straight Flush',\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['Kub 2P25 ln'] = {\n\t\t\t},\n\t\t},\n\t\t['name'] = {\n\t\t\t['NATO'] = 'SA-6 Gainful',\n\t\t},\n\t\t['harm_detection_chance'] = 40\n\t},\n\t['Patriot'] = {\n\t\t['type'] = 'complex',\n\t\t['searchRadar'] = {\n\t\t\t['Patriot str'] = {\n\t\t\t\t['name'] = {\n\t\t\t\t\t['NATO'] = 'Patriot str',\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['Patriot ln'] = {\n\t\t\t},\n\t\t},\n\t\t['misc'] = {\n\t\t\t['Patriot cp'] = {\n\t\t\t\t['required'] = false,\n\t\t\t},\n\t\t\t['Patriot EPP']  = {\n\t\t\t\t['required'] = false,\n\t\t\t},\n\t\t\t['Patriot ECS']  = {\n\t\t\t\t['required'] = true,\n\t\t\t},\n\t\t\t['Patriot AMG']  = {\n\t\t\t\t['required'] = false,\n\t\t\t},\n\t\t},\n\t\t['name'] = {\n\t\t\t['NATO'] = 'Patriot',\n\t\t},\n\t\t['harm_detection_chance'] = 90,\n\t\t['can_engage_harm'] = true\n\t},\n\t['Hawk'] = {\n\t\t['type'] = 'complex',\n\t\t['searchRadar'] = {\n\t\t\t['Hawk sr'] = {\n\t\t\t\t['name'] = {\n\t\t\t\t\t['NATO'] = 'Hawk str',\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t['trackingRadar'] = {\n\t\t\t['Hawk tr'] = {\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['Hawk ln'] = {\n\t\t\t},\n\t\t},\n\n\t\t['name'] = {\n\t\t\t['NATO'] = 'Hawk',\n\t\t},\n\t\t['harm_detection_chance'] = 40\n\n\t},\t\n\t['Roland ADS'] = {\n\t\t['type'] = 'complex',\n\t\t['searchRadar'] = {\n\t\t\t['Roland Radar'] = {\n\t\t\t\t['name'] = {\n\t\t\t\t\t['NATO'] = 'Roland EWR',\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['Roland ADS'] = {\n\t\t\t},\n\t\t},\n\t\t['name'] = {\n\t\t\t['NATO'] = 'Roland ADS',\n\t\t},\n\t\t['harm_detection_chance'] = 60\n\t},\t\n\t['NASAMS'] = {\n\t\t['type'] = 'complex',\n\t\t['searchRadar'] = {\n\t\t\t['NASAMS_Radar_MPQ64F1'] = {\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['NASAMS_LN_B'] = {\t\t\n\t\t\t},\n\t\t\t['NASAMS_LN_C'] = {\t\t\n\t\t\t},\n\t\t},\n\t\t\n\t\t['name'] = {\n\t\t\t['NATO'] = 'NASAMS',\n\t\t},\n\t\t['misc'] = {\n\t\t\t['NASAMS_Command_Post'] = {\n\t\t\t\t['required'] = false,\n\t\t\t},\n\t\t},\n\t\t['can_engage_harm'] = true,\n\t\t['harm_detection_chance'] = 90\n\t},\t\n\t['2S6 Tunguska'] = {\n\t\t['type'] = 'single',\n\t\t['searchRadar'] = {\n\t\t\t['2S6 Tunguska'] = {\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['2S6 Tunguska'] = {\n\t\t\t},\n\t\t},\n\t\t['name'] = {\n\t\t\t['NATO'] = 'SA-19 Grison',\n\t\t},\n\t},\t\t\n\t['Osa'] = {\n\t\t['type'] = 'single',\n\t\t['searchRadar'] = {\n\t\t\t['Osa 9A33 ln'] = {\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['Osa 9A33 ln'] = {\n\t\t\t\n\t\t\t},\n\t\t},\n\t\t['name'] = {\n\t\t\t['NATO'] = 'SA-8 Gecko',\n\t\t},\n\t\t['harm_detection_chance'] = 20\n\t},\t\n\t['Strela-10M3'] = {\n\t\t['type'] = 'single',\n\t\t['searchRadar'] = {\n\t\t\t['Strela-10M3'] = {\n\t\t\t\t['trackingRadar'] = true,\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['Strela-10M3'] = {\n\t\t\t},\n\t\t},\n\t\t['name'] = {\n\t\t\t['NATO'] = 'SA-13 Gopher',\n\t\t},\n\t},\t\n\t['Strela-1 9P31'] = {\n\t\t['type'] = 'single',\n\t\t['searchRadar'] = {\n\t\t\t['Strela-1 9P31'] = {\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['Strela-1 9P31'] = {\n\t\t\t},\n\t\t},\n\t\t['name'] = {\n\t\t\t['NATO'] = 'SA-9 Gaskin',\n\t\t},\n\t\t['harm_detection_chance'] = 20\n\t},\n\t['Tor'] = {\n\t\t['type'] = 'single',\n\t\t['searchRadar'] = {\n\t\t\t['Tor 9A331'] = {\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['Tor 9A331'] = {\n\t\t\t},\n\t\t},\n\t\t['name'] = {\n\t\t\t['NATO'] = 'SA-15 Gauntlet',\n\t\t},\n\t\t['harm_detection_chance'] = 90,\n\t\t['can_engage_harm'] = true\n\t\t\n\t},\n\t['Gepard'] = {\n\t\t['type'] = 'single',\n\t\t['searchRadar'] = {\n\t\t\t['Gepard'] = {\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['Gepard'] = {\n\t\t\t},\n\t\t},\n\t\t['name'] = {\n\t\t\t['NATO'] = 'Gepard',\n\t\t},\n\t\t['harm_detection_chance'] = 10\n\t},\t\t\n    ['Rapier'] = {\n        ['searchRadar'] = {\n            ['rapier_fsa_blindfire_radar'] = {\n            },\n        },\n        ['launchers'] = {\n        \t['rapier_fsa_launcher'] = {\n\t\t\t\t['trackingRadar'] = true,\n\t\t\t},\n        },\n        ['misc'] = {\n            ['rapier_fsa_optical_tracker_unit'] = {\n                ['required'] = true,\n            },\n        },\n        ['name'] = {\n\t\t\t['NATO'] = 'Rapier',\n\t\t},\n\t\t['harm_detection_chance'] = 10\n    },\t\n\t['ZSU-23-4 Shilka'] = {\n\t\t['type'] = 'single',\n\t\t['searchRadar'] = {\n\t\t\t['ZSU-23-4 Shilka'] = {\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['ZSU-23-4 Shilka'] = {\n\t\t\t},\n\t\t},\n\t\t['name'] = {\n\t\t\t['NATO'] = 'Zues',\n\t\t},\n\t\t['harm_detection_chance'] = 10\n\t},\n\t['HQ-7'] = {\n\t\t['searchRadar'] = {\n\t\t\t['HQ-7_STR_SP'] = {\n\t\t\t\t['name'] = {\n\t\t\t\t\t['NATO'] = 'CSA-4',\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['HQ-7_LN_SP'] = {\n\t\t\t},\n\t\t},\n\t\t['name'] = {\n\t\t\t['NATO'] = 'CSA-4',\n\t\t},\n\t\t['harm_detection_chance'] = 30\n\t},\t\n\t['Phalanx'] = {\n\t\t['type'] = 'single',\n\t\t['searchRadar'] = {\n\t\t\t['HEMTT_C-RAM_Phalanx'] = {\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['HEMTT_C-RAM_Phalanx'] = {\n\t\t\t},\n\t\t},\n\t\t['name'] = {\n\t\t\t['NATO'] = 'Phalanx',\n\t\t},\n\t\t['harm_detection_chance'] = 10\n\t},\t\n-- Start of RED EW radars:\t\n\t['1L13 EWR'] = {\n\t\t['type'] = 'ewr',\n\t\t['searchRadar'] = {\n\t\t\t['1L13 EWR'] = {\n\t\t\t\t['name'] = {\n\t\t\t\t\t['NATO'] = 'Box Spring',\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t['harm_detection_chance'] = 60\n\t},\n\t['55G6 EWR'] = {\n\t\t['type'] = 'ewr',\n\t\t['searchRadar'] = {\n\t\t\t['55G6 EWR'] = {\n\t\t\t\t['name'] = {\n\t\t\t\t\t['NATO'] = 'Tall Rack',\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t['harm_detection_chance'] = 60\n\t},\n\t['Dog Ear'] = {\n\t\t['type'] = 'ewr',\n\t\t['searchRadar'] = {\n\t\t\t['Dog Ear radar'] = {\n\t\t\t\t['name'] = {\n\t\t\t\t\t['NATO'] = 'Dog Ear',\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t['harm_detection_chance'] = 20\n\t},\n-- Start of BLUE EW radars:\n\t['FPS-117 Dome'] = {\n\t\t['type'] = 'ewr',\n\t\t['searchRadar'] = {\n\t\t\t['FPS-117 Dome'] = {\n\t\t\t\t['name'] = {\n\t\t\t\t\t['NATO'] = 'FPS-117 Dome',\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t['harm_detection_chance'] = 80\n\t},\n\t['FPS-117'] = {\n\t\t['type'] = 'ewr',\n\t\t['searchRadar'] = {\n\t\t\t['FPS-117'] = {\n\t\t\t\t['name'] = {\n\t\t\t\t\t['NATO'] = 'FPS-117',\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t['harm_detection_chance'] = 80\n\t}\n}\nend\ndo\n-- this file contains the definitions for the HightDigitSAMSs: https://github.com/Auranis/HighDigitSAMs\n\n--EW radars used in multiple SAM systems:\n\ns300PMU164N6Esr = {\n\t['name'] = {\n\t\t['NATO'] = 'Big Bird',\n\t},\n}\n\ns300PMU140B6MDsr = {\n\t['name'] = {\n\t\t['NATO'] = 'Clam Shell',\n\t},\n}\n\n--[[ units in SA-10 group Gargoyle:\n2020-12-10 18:27:27.050 INFO    SCRIPTING: S-300PMU1 54K6 cp\n2020-12-10 18:27:27.050 INFO    SCRIPTING: S-300PMU1 5P85CE ln\n2020-12-10 18:27:27.050 INFO    SCRIPTING: S-300PMU1 5P85DE ln\n2020-12-10 18:27:27.050 INFO    SCRIPTING: S-300PMU1 40B6MD sr\n2020-12-10 18:27:27.050 INFO    SCRIPTING: S-300PMU1 64N6E sr\n2020-12-10 18:27:27.050 INFO    SCRIPTING: S-300PMU1 40B6M tr\n2020-12-10 18:27:27.050 INFO    SCRIPTING: S-300PMU1 30N6E tr\n--]]\nsamTypesDB['S-300PMU1'] = {\n\t['type'] = 'complex',\n\t['searchRadar'] = {\n\t\t['S-300PMU1 40B6MD sr'] = s300PMU140B6MDsr,\n\t\t['S-300PMU1 64N6E sr'] = s300PMU164N6Esr,\n\t\t\n\t\t['S-300PS 40B6MD sr'] = {\n\t\t\t['name'] = {\n\t\t\t\t['NATO'] = '',\n\t\t\t},\n\t\t},\n\t\t['S-300PS 64H6E sr'] = {\n\t\t\t['name'] = {\n\t\t\t\t['NATO'] = '',\n\t\t\t},\n\t\t},\n\t},\n\t['trackingRadar'] = {\n\t\t['S-300PMU1 40B6M tr'] = {\n\t\t\t['name'] = {\n\t\t\t\t['NATO'] = 'Grave Stone',\n\t\t\t},\n\t\t},\n\t\t['S-300PMU1 30N6E tr'] = {\n\t\t\t['name'] = {\n\t\t\t\t['NATO'] = 'Flap Lid',\n\t\t\t},\n\n\t\t},\n\t\t['S-300PS 40B6M tr'] = {\n\t\t\t['name'] = {\n\t\t\t\t['NATO'] = '',\n\t\t\t},\n\t\t},\n\t},\n\t['misc'] = {\n\t\t['S-300PMU1 54K6 cp'] = {\n\t\t\t['required'] = true,\n\t\t},\n\t},\n\t['launchers'] = {\n\t\t['S-300PMU1 5P85CE ln'] = {\n\t\t},\n\t\t['S-300PMU1 5P85DE ln'] = {\n\t\t},\n\t},\n\t['name']  = {\n\t\t['NATO'] = 'SA-20A Gargoyle'\n\t},\n\t['harm_detection_chance'] = 90,\n\t['can_engage_harm'] = true\n}\t\n\n--[[ Units in the SA-23 Group:\n2020-12-11 16:40:52.072 INFO    SCRIPTING: S-300VM 9A82ME ln\n2020-12-11 16:40:52.072 INFO    SCRIPTING: S-300VM 9A83ME ln\n2020-12-11 16:40:52.072 INFO    SCRIPTING: S-300VM 9S15M2 sr\n2020-12-11 16:40:52.072 INFO    SCRIPTING: S-300VM 9S19M2 sr\n2020-12-11 16:40:52.072 INFO    SCRIPTING: S-300VM 9S32ME tr\n2020-12-11 16:40:52.072 INFO    SCRIPTING: S-300VM 9S457ME cp\n\n]]--\nsamTypesDB['S-300VM'] = {\n\t['type'] = 'complex',\n\t['searchRadar'] = {\n\t\t['S-300VM 9S15M2 sr'] = {\n\t\t\t['name'] = {\n\t\t\t\t['NATO'] = 'Bill Board-C',\n\t\t\t},\n\t\t},\n\t\t['S-300VM 9S19M2 sr'] = {\n\t\t\t['name'] = {\n\t\t\t\t['NATO'] = 'High Screen-B',\n\t\t\t},\n\t\t},\n\t},\n\t['trackingRadar'] = {\n\t\t['S-300VM 9S32ME tr'] = {\n\t\t},\n\t},\n\t['misc'] = {\n\t\t['S-300VM 9S457ME cp'] = {\n\t\t\t['required'] = true,\n\t\t},\n\t},\n\t['launchers'] = {\n\t\t['S-300VM 9A82ME ln'] = {\n\t\t},\n\t\t['S-300VM 9A83ME ln'] = {\n\t\t},\n\t},\n\t['name']  = {\n\t\t['NATO'] = 'SA-23 Antey-2500'\n\t},\n\t['harm_detection_chance'] = 90,\n\t['can_engage_harm'] = true\n}\t\n\n--[[ Units in the SA-10B Group:\n2021-01-01 20:39:14.413 INFO    SCRIPTING: S-300PS SA-10B 40B6MD MAST sr\n2021-01-01 20:39:14.413 INFO    SCRIPTING: S-300PS SA-10B 54K6 cp\n2021-01-01 20:39:14.413 INFO    SCRIPTING: S-300PS 5P85SE_mod ln\n2021-01-01 20:39:14.413 INFO    SCRIPTING: S-300PS 5P85SU_mod ln\n2021-01-01 20:39:14.413 INFO    SCRIPTING: S-300PS 64H6E TRAILER sr\n2021-01-01 20:39:14.413 INFO    SCRIPTING: S-300PS 30N6 TRAILER tr\n2021-01-01 20:39:14.413 INFO    SCRIPTING: S-300PS SA-10B 40B6M MAST tr\n--]]\nsamTypesDB['S-300PS'] = {\n\t['type'] = 'complex',\n\t['searchRadar'] = {\n\t\t['S-300PS SA-10B 40B6MD MAST sr'] = {\n\t\t\t['name'] = {\n\t\t\t\t['NATO'] = 'Clam Shell',\n\t\t\t},\n\t\t},\n\t\t['S-300PS 64H6E TRAILER sr'] = {\n\t\t},\n\t},\n\t['trackingRadar'] = {\n\t\t['S-300PS 30N6 TRAILER tr'] = {\n\t\t},\n\t\t['S-300PS SA-10B 40B6M MAST tr'] = {\n\t\t},\n\t\t['S-300PS 40B6M tr'] = {\n\t\t},\n\t\t['S-300PMU1 40B6M tr'] = {\n\t\t},\t\n\t\t['S-300PMU1 30N6E tr'] = {\n\t\t},\t\t\n\t},\n\t['misc'] = {\n\t\t['S-300PS SA-10B 54K6 cp'] = {\n\t\t\t['required'] = true,\n\t\t},\n\t},\n\t['launchers'] = {\n\t\t['S-300PS 5P85SE_mod ln'] = {\n\t\t},\n\t\t['S-300PS 5P85SU_mod ln'] = {\n\t\t},\n\t},\n\t['name']  = {\n\t\t['NATO'] = 'SA-10B Grumble'\n\t},\n\t['harm_detection_chance'] = 90,\n\t['can_engage_harm'] = true\n}\n\n--[[ Extra launchers for the in game SA-10C and HighDigitSAMs SA-10B, SA-20B\n2021-01-01 21:04:19.908 INFO    SCRIPTING: S-300PS 5P85DE ln\n2021-01-01 21:04:19.908 INFO    SCRIPTING: S-300PS 5P85CE ln\n--]]\n\nlocal s300launchers = samTypesDB['S-300']['launchers']\ns300launchers['S-300PS 5P85DE ln'] = {}\ns300launchers['S-300PS 5P85CE ln'] = {}\n\nlocal s300launchers = samTypesDB['S-300PS']['launchers']\ns300launchers['S-300PS 5P85DE ln'] = {}\ns300launchers['S-300PS 5P85CE ln'] = {}\n\nlocal s300launchers = samTypesDB['S-300PMU1']['launchers']\ns300launchers['S-300PS 5P85DE ln'] = {}\ns300launchers['S-300PS 5P85CE ln'] = {}\n\n--[[\nNew launcher for the SA-11 complex, will identify as SA-17\nSA-17 Buk M1-2 LN 9A310M1-2\n --]]\nsamTypesDB['Buk-M2'] = {\n\t['type'] = 'complex',\n\t['searchRadar'] = {\n\t\t['SA-11 Buk SR 9S18M1'] = {\n\t\t\t['name'] = {\n\t\t\t\t['NATO'] = 'Snow Drift',\n\t\t\t},\n\t\t},\n\t},\n\t['launchers'] = {\n\t\t['SA-17 Buk M1-2 LN 9A310M1-2'] = {\n\t\t},\n\t},\n\t['misc'] = {\n\t\t['SA-11 Buk CC 9S470M1'] = {\n\t\t\t['required'] = true,\n\t\t},\n\t},\n\t['name'] = {\n\t\t['NATO'] = 'SA-17 Grizzly',\n\t},\n\t['harm_detection_chance'] = 90\n}\n\n--[[\nNew launcher for the SA-2 complex: S_75M_Volhov_V759\n--]]\nlocal s75launchers = samTypesDB['S-75']['launchers']\ns75launchers['S_75M_Volhov_V759'] = {}\n\n--[[\nNew launcher for the SA-3 complex:\n--]]\nlocal s125launchers = samTypesDB['S-125']['launchers']\ns125launchers['5p73 V-601P ln'] = {}\n\n--[[\nNew launcher for the SA-2 complex: HQ_2_Guideline_LN\n--]]\nlocal s125launchers = samTypesDB['S-75']['launchers']\ns125launchers['HQ_2_Guideline_LN'] = {}\n\n--[[\nSA-12 Gladiator / Giant:\n2021-03-19 21:24:22.620 INFO    SCRIPTING: S-300V 9S15 sr\n2021-03-19 21:24:22.620 INFO    SCRIPTING: S-300V 9S19 sr\n2021-03-19 21:24:22.620 INFO    SCRIPTING: S-300V 9S32 tr\n2021-03-19 21:24:22.620 INFO    SCRIPTING: S-300V 9S457 cp\n2021-03-19 21:24:22.620 INFO    SCRIPTING: S-300V 9A83 ln\n2021-03-19 21:24:22.620 INFO    SCRIPTING: S-300V 9A82 ln\n--]]\nsamTypesDB['S-300V'] = {\n\t['type'] = 'complex',\n\t['searchRadar'] = {\n\t\t['S-300V 9S15 sr'] = {\n\t\t\t['name'] = {\n\t\t\t\t['NATO'] = 'Bill Board',\n\t\t\t},\n\t\t},\n\t\t['S-300V 9S19 sr'] = {\n\t\t\t['name'] = {\n\t\t\t\t['NATO'] = 'High Screen',\n\t\t\t},\n\t\t},\n\t},\n\t['trackingRadar'] = {\n\t\t['S-300V 9S32 tr'] = {\n\t\t\t['NATO'] = 'Grill Pan',\n\t\t\t},\n\t},\n\t['misc'] = {\n\t\t['S-300V 9S457 cp'] = {\n\t\t\t['required'] = true,\n\t\t},\n\t},\n\t['launchers'] = {\n\t\t['S-300V 9A83 ln'] = {\n\t\t},\n\t\t['S-300V 9A82 ln'] = {\n\t\t},\n\t},\n\t['name']  = {\n\t\t['NATO'] = 'SA-12 Gladiator/Giant'\n\t},\n\t['harm_detection_chance'] = 90,\n\t['can_engage_harm'] = true\n}\n\n--[[\nSA-20B Gargoyle B:\n\n2021-03-25 19:15:02.135 INFO    SCRIPTING: S-300PMU2 64H6E2 sr\n2021-03-25 19:15:02.135 INFO    SCRIPTING: S-300PMU2 92H6E tr\n2021-03-25 19:15:02.135 INFO    SCRIPTING: S-300PMU2 5P85SE2 ln\n2021-03-25 19:15:02.135 INFO    SCRIPTING: S-300PMU2 54K6E2 cp\n--]]\n\nsamTypesDB['S-300PMU2'] = {\n\t['type'] = 'complex',\n\t['searchRadar'] = {\n\t\t['S-300PMU2 64H6E2 sr'] = {\n\t\t\t['name'] = {\n\t\t\t\t['NATO'] = '',\n\t\t\t},\n\t\t},\n\t\t['S-300PMU1 40B6MD sr'] = s300PMU140B6MDsr,\n\t\t['S-300PMU1 64N6E sr'] = s300PMU164N6Esr,\n\t},\n\t['trackingRadar'] = {\n\t\t['S-300PMU2 92H6E tr'] = {\n\t\t},\n\t\t['S-300PS 40B6M tr'] = {\n\t\t},\n\t\t['S-300PMU1 40B6M tr'] = {\n\t\t},\n\t\t['S-300PMU1 30N6E tr'] = {\n\t\t},\n\t},\n\t['misc'] = {\n\t\t['S-300PMU2 54K6E2 cp'] = {\n\t\t\t['required'] = true,\n\t\t},\n\t},\n\t['launchers'] = {\n\t\t['S-300PMU2 5P85SE2 ln'] = {\n\t\t},\n\t},\n\t['name']  = {\n\t\t['NATO'] = 'SA-20B Gargoyle B'\n\t},\n\t['harm_detection_chance'] = 90,\n\t['can_engage_harm'] = true\n}\n\n--[[\n\n--]]\nend\n\n\n\ndo\n\nSkynetIADSLogger = {}\nSkynetIADSLogger.__index = SkynetIADSLogger\n\nfunction SkynetIADSLogger:create(iads)\n\tlocal logger = {}\n\tsetmetatable(logger, SkynetIADSLogger)\n\tlogger.debugOutput = {}\n\tlogger.debugOutput.IADSStatus = false\n\tlogger.debugOutput.samWentDark = false\n\tlogger.debugOutput.contacts = false\n\tlogger.debugOutput.radarWentLive = false\n\tlogger.debugOutput.jammerProbability = false\n\tlogger.debugOutput.addedEWRadar = false\n\tlogger.debugOutput.addedSAMSite = false\n\tlogger.debugOutput.warnings = true\n\tlogger.debugOutput.harmDefence = false\n\tlogger.debugOutput.samSiteStatusEnvOutput = false\n\tlogger.debugOutput.earlyWarningRadarStatusEnvOutput = false\n\tlogger.debugOutput.commandCenterStatusEnvOutput = false\n\tlogger.iads = iads\n\treturn logger\nend\n\nfunction SkynetIADSLogger:getDebugSettings()\n\treturn self.debugOutput\nend\n\nfunction SkynetIADSLogger:printOutput(output, typeWarning)\n\tif typeWarning == true and self:getDebugSettings().warnings or typeWarning == nil then\n\t\tif typeWarning == true then\n\t\t\toutput = \"WARNING: \"..output\n\t\tend\n\t\ttrigger.action.outText(output, 4)\n\tend\nend\n\nfunction SkynetIADSLogger:printOutputToLog(output)\n\tenv.info(\"SKYNET: \"..output, 4)\nend\n\nfunction SkynetIADSLogger:printEarlyWarningRadarStatus()\n\tlocal ewRadars = self.iads:getEarlyWarningRadars()\n\tself:printOutputToLog(\"------------------------------------------ EW RADAR STATUS: \"..self.iads:getCoalitionString()..\" -------------------------------\")\n\tfor i = 1, #ewRadars do\n\t\tlocal ewRadar = ewRadars[i]\n\t\tlocal numConnectionNodes = #ewRadar:getConnectionNodes()\n\t\tlocal numPowerSources = #ewRadar:getPowerSources()\n\t\tlocal isActive = ewRadar:isActive()\n\t\tlocal connectionNodes = ewRadar:getConnectionNodes()\n\t\tlocal firstRadar = nil\n\t\tlocal radars = ewRadar:getRadars()\n\t\t\n\t\t--get the first existing radar to prevent issues in calculating the distance later on:\n\t\tfor i = 1, #radars do\n\t\t\tif radars[i]:isExist() then\n\t\t\t\tfirstRadar = radars[i]\n\t\t\t\tbreak\n\t\t\tend\n\t\t\n\t\tend\n\t\tlocal numDamagedConnectionNodes = 0\n\t\t\n\t\t\n\t\tfor j = 1, #connectionNodes do\n\t\t\tlocal connectionNode = connectionNodes[j]\n\t\t\tif connectionNode:isExist() == false then\n\t\t\t\tnumDamagedConnectionNodes = numDamagedConnectionNodes + 1\n\t\t\tend\n\t\tend\n\t\tlocal intactConnectionNodes = numConnectionNodes - numDamagedConnectionNodes\n\t\t\n\t\tlocal powerSources = ewRadar:getPowerSources()\n\t\tlocal numDamagedPowerSources = 0\n\t\tfor j = 1, #powerSources do\n\t\t\tlocal powerSource = powerSources[j]\n\t\t\tif powerSource:isExist() == false then\n\t\t\t\tnumDamagedPowerSources = numDamagedPowerSources + 1\n\t\t\tend\n\t\tend\n\t\tlocal intactPowerSources = numPowerSources - numDamagedPowerSources \n\t\t\n\t\tlocal detectedTargets = ewRadar:getDetectedTargets()\n\t\tlocal samSitesInCoveredArea = ewRadar:getChildRadars()\n\t\t\n\t\tlocal unitName = \"DESTROYED\"\n\t\t\n\t\tif ewRadar:getDCSRepresentation():isExist() then\n\t\t\tunitName = ewRadar:getDCSName()\n\t\tend\n\t\t\n\t\tself:printOutputToLog(\"UNIT: \"..unitName..\" | TYPE: \"..ewRadar:getNatoName())\n\t\tself:printOutputToLog(\"ACTIVE: \"..tostring(isActive)..\"| DETECTED TARGETS: \"..#detectedTargets..\" | DEFENDING HARM: \"..tostring(ewRadar:isDefendingHARM()))\n\t\tif numConnectionNodes > 0 then\n\t\t\tself:printOutputToLog(\"CONNECTION NODES: \"..numConnectionNodes..\" | DAMAGED: \"..numDamagedConnectionNodes..\" | INTACT: \"..intactConnectionNodes)\n\t\telse\n\t\t\tself:printOutputToLog(\"NO CONNECTION NODES SET\")\n\t\tend\n\t\tif numPowerSources > 0 then\n\t\t\tself:printOutputToLog(\"POWER SOURCES : \"..numPowerSources..\" | DAMAGED:\"..numDamagedPowerSources..\" | INTACT: \"..intactPowerSources)\n\t\telse\n\t\t\tself:printOutputToLog(\"NO POWER SOURCES SET\")\n\t\tend\n\t\t\n\t\tself:printOutputToLog(\"SAM SITES IN COVERED AREA: \"..#samSitesInCoveredArea)\n\t\tfor j = 1, #samSitesInCoveredArea do\n\t\t\tlocal samSiteCovered = samSitesInCoveredArea[j]\n\t\t\tself:printOutputToLog(samSiteCovered:getDCSName())\n\t\tend\n\t\t\n\t\tfor j = 1, #detectedTargets do\n\t\t\tlocal contact = detectedTargets[j]\n\t\t\tif firstRadar ~= nil and firstRadar:isExist() then\n\t\t\t\tlocal distance = mist.utils.round(mist.utils.metersToNM(ewRadar:getDistanceInMetersToContact(firstRadar:getDCSRepresentation(), contact:getPosition().p)), 2)\n\t\t\t\tself:printOutputToLog(\"CONTACT: \"..contact:getName()..\" | TYPE: \"..contact:getTypeName()..\" | DISTANCE NM: \"..distance)\n\t\t\tend\n\t\tend\n\t\t\n\t\tself:printOutputToLog(\"---------------------------------------------------\")\n\t\t\n\tend\n\nend\n\nfunction SkynetIADSLogger:getMetaInfo(abstractElementSupport)\n\tlocal info = {}\n\tinfo.numSources = #abstractElementSupport\n\tinfo.numDamagedSources = 0\n\tinfo.numIntactSources = 0\n\tfor j = 1, #abstractElementSupport do\n\t\tlocal source = abstractElementSupport[j]\n\t\tif source:isExist() == false then\n\t\t\tinfo.numDamagedSources = info.numDamagedSources + 1\n\t\tend\n\tend\n\tinfo.numIntactSources = info.numSources - info.numDamagedSources\n\treturn info\nend\n\nfunction SkynetIADSLogger:printSAMSiteStatus()\n\tlocal samSites = self.iads:getSAMSites()\n\t\n\tself:printOutputToLog(\"------------------------------------------ SAM STATUS: \"..self.iads:getCoalitionString()..\" -------------------------------\")\n\tfor i = 1, #samSites do\n\t\tlocal samSite = samSites[i]\n\t\tlocal numConnectionNodes = #samSite:getConnectionNodes()\n\t\tlocal numPowerSources = #samSite:getPowerSources()\n\t\tlocal isAutonomous = samSite:getAutonomousState()\n\t\tlocal isActive = samSite:isActive()\n\t\t\n\t\tlocal connectionNodes = samSite:getConnectionNodes()\n\t\tlocal firstRadar = samSite:getRadars()[1]\n\t\tlocal numDamagedConnectionNodes = 0\n\t\tfor j = 1, #connectionNodes do\n\t\t\tlocal connectionNode = connectionNodes[j]\n\t\t\tif connectionNode:isExist() == false then\n\t\t\t\tnumDamagedConnectionNodes = numDamagedConnectionNodes + 1\n\t\t\tend\n\t\tend\n\t\tlocal intactConnectionNodes = numConnectionNodes - numDamagedConnectionNodes\n\t\t\n\t\tlocal powerSources = samSite:getPowerSources()\n\t\tlocal numDamagedPowerSources = 0\n\t\tfor j = 1, #powerSources do\n\t\t\tlocal powerSource = powerSources[j]\n\t\t\tif powerSource:isExist() == false then\n\t\t\t\tnumDamagedPowerSources = numDamagedPowerSources + 1\n\t\t\tend\n\t\tend\n\t\tlocal intactPowerSources = numPowerSources - numDamagedPowerSources \n\t\t\n\t\tlocal detectedTargets = samSite:getDetectedTargets()\n\t\t\n\t\tlocal samSitesInCoveredArea = samSite:getChildRadars()\n\t\t\n\t\tlocal engageAirWeapons = samSite:getCanEngageAirWeapons()\n\t\t\n\t\tlocal engageHARMS = samSite:getCanEngageHARM()\n\t\t\n\t\tlocal hasAmmo = samSite:hasRemainingAmmo()\n\t\t\n\t\tself:printOutputToLog(\"GROUP: \"..samSite:getDCSName()..\" | TYPE: \"..samSite:getNatoName())\n\t\tself:printOutputToLog(\"ACTIVE: \"..tostring(isActive)..\" | AUTONOMOUS: \"..tostring(isAutonomous)..\" | IS ACTING AS EW: \"..tostring(samSite:getActAsEW())..\" | CAN ENGAGE AIR WEAPONS : \"..tostring(engageAirWeapons)..\" | CAN ENGAGE HARMS : \"..tostring(engageHARMS)..\" | HAS AMMO: \"..tostring(hasAmmo)..\" | DETECTED TARGETS: \"..#detectedTargets..\" | DEFENDING HARM: \"..tostring(samSite:isDefendingHARM())..\" | MISSILES IN FLIGHT: \"..tostring(samSite:getNumberOfMissilesInFlight()))\n\t\t\n\t\tif numConnectionNodes > 0 then\n\t\t\tself:printOutputToLog(\"CONNECTION NODES: \"..numConnectionNodes..\" | DAMAGED: \"..numDamagedConnectionNodes..\" | INTACT: \"..intactConnectionNodes)\n\t\telse\n\t\t\tself:printOutputToLog(\"NO CONNECTION NODES SET\")\n\t\tend\n\t\tif numPowerSources > 0 then\n\t\t\tself:printOutputToLog(\"POWER SOURCES : \"..numPowerSources..\" | DAMAGED:\"..numDamagedPowerSources..\" | INTACT: \"..intactPowerSources)\n\t\telse\n\t\t\tself:printOutputToLog(\"NO POWER SOURCES SET\")\n\t\tend\n\t\t\n\t\tself:printOutputToLog(\"SAM SITES IN COVERED AREA: \"..#samSitesInCoveredArea)\n\t\tfor j = 1, #samSitesInCoveredArea do\n\t\t\tlocal samSiteCovered = samSitesInCoveredArea[j]\n\t\t\tself:printOutputToLog(samSiteCovered:getDCSName())\n\t\tend\n\t\t\n\t\tfor j = 1, #detectedTargets do\n\t\t\tlocal contact = detectedTargets[j]\n\t\t\tif firstRadar ~= nil and firstRadar:isExist() then\n\t\t\t\tlocal distance = mist.utils.round(mist.utils.metersToNM(samSite:getDistanceInMetersToContact(firstRadar:getDCSRepresentation(), contact:getPosition().p)), 2)\n\t\t\t\tself:printOutputToLog(\"CONTACT: \"..contact:getName()..\" | TYPE: \"..contact:getTypeName()..\" | DISTANCE NM: \"..distance)\n\t\t\tend\n\t\tend\n\t\t\n\t\tself:printOutputToLog(\"---------------------------------------------------\")\n\tend\nend\n\nfunction SkynetIADSLogger:printCommandCenterStatus()\n\tlocal commandCenters = self.iads:getCommandCenters()\n\tself:printOutputToLog(\"------------------------------------------ COMMAND CENTER STATUS: \"..self.iads:getCoalitionString()..\" -------------------------------\")\n\t\n\tfor i = 1, #commandCenters do\n\t\tlocal commandCenter = commandCenters[i]\n\t\tlocal numConnectionNodes = #commandCenter:getConnectionNodes()\n\t\tlocal powerSourceInfo = self:getMetaInfo(commandCenter:getPowerSources())\n\t\tlocal connectionNodeInfo = self:getMetaInfo(commandCenter:getConnectionNodes())\n\t\tself:printOutputToLog(\"GROUP: \"..commandCenter:getDCSName()..\" | TYPE: \"..commandCenter:getNatoName())\n\t\tif connectionNodeInfo.numSources > 0 then\n\t\t\tself:printOutputToLog(\"CONNECTION NODES: \"..connectionNodeInfo.numSources..\" | DAMAGED: \"..connectionNodeInfo.numDamagedSources..\" | INTACT: \"..connectionNodeInfo.numIntactSources)\n\t\telse\n\t\t\tself:printOutputToLog(\"NO CONNECTION NODES SET\")\n\t\tend\n\t\tif powerSourceInfo.numSources > 0 then\n\t\t\tself:printOutputToLog(\"POWER SOURCES : \"..powerSourceInfo.numSources..\" | DAMAGED: \"..powerSourceInfo.numDamagedSources..\" | INTACT: \"..powerSourceInfo.numIntactSources)\n\t\telse\n\t\t\tself:printOutputToLog(\"NO POWER SOURCES SET\")\n\t\tend\n\t\tself:printOutputToLog(\"---------------------------------------------------\")\n\tend\nend\n\nfunction SkynetIADSLogger:printSystemStatus()\t\n\n\tif self:getDebugSettings().IADSStatus or self:getDebugSettings().contacts then\n\t\tlocal coalitionStr = self.iads:getCoalitionString()\n\t\tself:printOutput(\"---- IADS: \"..coalitionStr..\" ------\")\n\tend\n\t\n\tif self:getDebugSettings().IADSStatus then\n\n\t\tlocal commandCenters = self.iads:getCommandCenters()\n\t\tlocal numComCenters = #commandCenters\n\t\tlocal numDestroyedComCenters = 0\n\t\tlocal numComCentersNoPower = 0\n\t\tlocal numComCentersNoConnectionNode = 0\n\t\tlocal numIntactComCenters = 0\n\t\tfor i = 1, #commandCenters do\n\t\t\tlocal commandCenter = commandCenters[i]\n\t\t\tif commandCenter:hasWorkingPowerSource() == false then\n\t\t\t\tnumComCentersNoPower = numComCentersNoPower + 1\n\t\t\tend\n\t\t\tif commandCenter:hasActiveConnectionNode() == false then\n\t\t\t\tnumComCentersNoConnectionNode = numComCentersNoConnectionNode + 1\n\t\t\tend\n\t\t\tif commandCenter:isDestroyed() == false then\n\t\t\t\tnumIntactComCenters = numIntactComCenters + 1\n\t\t\tend\n\t\tend\n\t\t\n\t\tnumDestroyedComCenters = numComCenters - numIntactComCenters\n\t\t\n\t\t\n\t\tself:printOutput(\"COMMAND CENTERS: \"..numComCenters..\" | Destroyed: \"..numDestroyedComCenters..\" | NoPowr: \"..numComCentersNoPower..\" | NoCon: \"..numComCentersNoConnectionNode)\n\t\n\t\tlocal ewNoPower = 0\n\t\tlocal earlyWarningRadars = self.iads:getEarlyWarningRadars()\n\t\tlocal ewTotal = #earlyWarningRadars\n\t\tlocal ewNoConnectionNode = 0\n\t\tlocal ewActive = 0\n\t\tlocal ewRadarsInactive = 0\n\n\t\tfor i = 1, #earlyWarningRadars do\n\t\t\tlocal ewRadar = earlyWarningRadars[i]\n\t\t\tif ewRadar:hasWorkingPowerSource() == false then\n\t\t\t\tewNoPower = ewNoPower + 1\n\t\t\tend\n\t\t\tif ewRadar:hasActiveConnectionNode() == false then\n\t\t\t\tewNoConnectionNode = ewNoConnectionNode + 1\n\t\t\tend\n\t\t\tif ewRadar:isActive() then\n\t\t\t\tewActive = ewActive + 1\n\t\t\tend\n\t\tend\n\t\t\n\t\tewRadarsInactive = ewTotal - ewActive\t\n\t\tlocal numEWRadarsDestroyed = #self.iads:getDestroyedEarlyWarningRadars()\n\t\tself:printOutput(\"EW: \"..ewTotal..\" | On: \"..ewActive..\" | Off: \"..ewRadarsInactive..\" | Destroyed: \"..numEWRadarsDestroyed..\" | NoPowr: \"..ewNoPower..\" | NoCon: \"..ewNoConnectionNode)\n\t\t\n\t\tlocal samSitesInactive = 0\n\t\tlocal samSitesActive = 0\n\t\tlocal samSites = self.iads:getSAMSites()\n\t\tlocal samSitesTotal = #samSites\n\t\tlocal samSitesNoPower = 0\n\t\tlocal samSitesNoConnectionNode = 0\n\t\tlocal samSitesOutOfAmmo = 0\n\t\tlocal samSiteAutonomous = 0\n\t\tlocal samSiteRadarDestroyed = 0\n\t\tfor i = 1, #samSites do\n\t\t\tlocal samSite = samSites[i]\n\t\t\tif samSite:hasWorkingPowerSource() == false then\n\t\t\t\tsamSitesNoPower = samSitesNoPower + 1\n\t\t\tend\n\t\t\tif samSite:hasActiveConnectionNode() == false then\n\t\t\t\tsamSitesNoConnectionNode = samSitesNoConnectionNode + 1\n\t\t\tend\n\t\t\tif samSite:isActive() then\n\t\t\t\tsamSitesActive = samSitesActive + 1\n\t\t\tend\n\t\t\tif samSite:hasRemainingAmmo() == false then\n\t\t\t\tsamSitesOutOfAmmo = samSitesOutOfAmmo + 1\n\t\t\tend\n\t\t\tif samSite:getAutonomousState() == true then\n\t\t\t\tsamSiteAutonomous = samSiteAutonomous + 1\n\t\t\tend\n\t\t\tif samSite:hasWorkingRadar() == false then\n\t\t\t\tsamSiteRadarDestroyed = samSiteRadarDestroyed + 1\n\t\t\tend\n\t\tend\n\t\t\n\t\tsamSitesInactive = samSitesTotal - samSitesActive\n\t\tself:printOutput(\"SAM: \"..samSitesTotal..\" | On: \"..samSitesActive..\" | Off: \"..samSitesInactive..\" | Autonm: \"..samSiteAutonomous..\" | Raddest: \"..samSiteRadarDestroyed..\" | NoPowr: \"..samSitesNoPower..\" | NoCon: \"..samSitesNoConnectionNode..\" | NoAmmo: \"..samSitesOutOfAmmo)\n\tend\n\t\n\tif self:getDebugSettings().contacts then\n\t\tlocal contacts = self.iads:getContacts()\n\t\tif contacts then\n\t\t\tfor i = 1, #contacts do\n\t\t\t\tlocal contact = contacts[i]\n\t\t\t\t\tself:printOutput(\"CONTACT: \"..contact:getName()..\" | TYPE: \"..contact:getTypeName()..\" | GS: \"..tostring(contact:getGroundSpeedInKnots())..\" | LAST SEEN: \"..contact:getAge())\n\t\t\tend\n\t\tend\n\tend\n\t\n\tif self:getDebugSettings().commandCenterStatusEnvOutput then\n\t\tself:printCommandCenterStatus()\n\tend\n\n\tif self:getDebugSettings().earlyWarningRadarStatusEnvOutput then\n\t\tself:printEarlyWarningRadarStatus()\n\tend\n\t\n\tif self:getDebugSettings().samSiteStatusEnvOutput then\n\t\tself:printSAMSiteStatus()\n\tend\n\nend\n\nend\ndo\n\nSkynetIADS = {}\nSkynetIADS.__index = SkynetIADS\n\nSkynetIADS.database = samTypesDB\n\nfunction SkynetIADS:create(name)\n\tlocal iads = {}\n\tsetmetatable(iads, SkynetIADS)\n\tiads.radioMenu = nil\n\tiads.earlyWarningRadars = {}\n\tiads.samSites = {}\n\tiads.commandCenters = {}\n\tiads.ewRadarScanMistTaskID = nil\n\tiads.coalition = nil\n\tiads.contacts = {}\n\tiads.maxTargetAge = 32\n\tiads.name = name\n\tiads.harmDetection = SkynetIADSHARMDetection:create(iads)\n\tiads.logger = SkynetIADSLogger:create(iads)\n\tif iads.name == nil then\n\t\tiads.name = \"\"\n\tend\n\tiads.contactUpdateInterval = 5\n\tworld.addEventHandler(iads)\n\treturn iads\nend\n\nfunction SkynetIADS:onEvent(event)\n\tif (event.id == world.event.S_EVENT_BIRTH ) then\n\t\tenv.info(\"New Object Spawned\")\n\t--\tself:addSAMSite(event.initiator:getGroup():getName());\n\tend\nend\n\nfunction SkynetIADS:setUpdateInterval(interval)\n\tself.contactUpdateInterval = interval\nend\n\nfunction SkynetIADS:setCoalition(item)\n\tif item then\n\t\tlocal coalitionID = item:getCoalition()\n\t\tif self.coalitionID == nil then\n\t\t\tself.coalitionID = coalitionID\n\t\tend\n\t\tif self.coalitionID ~= coalitionID then\n\t\t\tself:printOutputToLog(\"element: \"..item:getName()..\" has a different coalition than the IADS\", true)\n\t\tend\n\tend\nend\n\nfunction SkynetIADS:addJammer(jammer)\n\ttable.insert(self.jammers, jammer)\nend\n\nfunction SkynetIADS:getCoalition()\n\treturn self.coalitionID\nend\n\nfunction SkynetIADS:getDestroyedEarlyWarningRadars()\n\tlocal destroyedSites = {}\n\tfor i = 1, #self.earlyWarningRadars do\n\t\tlocal ewSite = self.earlyWarningRadars[i]\n\t\tif ewSite:isDestroyed() then\n\t\t\ttable.insert(destroyedSites, ewSite)\n\t\tend\n\tend\n\treturn destroyedSites\nend\n\nfunction SkynetIADS:getUsableAbstractRadarElemtentsOfTable(abstractRadarTable)\n\tlocal usable = {}\n\tfor i = 1, #abstractRadarTable do\n\t\tlocal abstractRadarElement = abstractRadarTable[i]\n\t\tif abstractRadarElement:hasActiveConnectionNode() and abstractRadarElement:hasWorkingPowerSource() and abstractRadarElement:isDestroyed() == false then\n\t\t\ttable.insert(usable, abstractRadarElement)\n\t\tend\n\tend\n\treturn usable\nend\n\nfunction SkynetIADS:getUsableEarlyWarningRadars()\n\treturn self:getUsableAbstractRadarElemtentsOfTable(self.earlyWarningRadars)\nend\n\nfunction SkynetIADS:createTableDelegator(units) \n\tlocal sites = SkynetIADSTableDelegator:create()\n\tfor i = 1, #units do\n\t\tlocal site = units[i]\n\t\ttable.insert(sites, site)\n\tend\n\treturn sites\nend\n\nfunction SkynetIADS:addEarlyWarningRadarsByPrefix(prefix)\n\tself:deactivateEarlyWarningRadars()\n\tself.earlyWarningRadars = {}\n\tfor unitName, unit in pairs(mist.DBs.unitsByName) do\n\t\tlocal pos = self:findSubString(unitName, prefix)\n\t\t--somehow the MIST unit db contains StaticObject, we check to see we only add Units\n\t\tlocal unit = Unit.getByName(unitName)\n\t\tif pos and pos == 1 and unit then\n\t\t\tself:addEarlyWarningRadar(unitName)\n\t\tend\n\tend\n\treturn self:createTableDelegator(self.earlyWarningRadars)\nend\n\nfunction SkynetIADS:addEarlyWarningRadar(earlyWarningRadarUnitName)\n\tlocal earlyWarningRadarUnit = Unit.getByName(earlyWarningRadarUnitName)\n\tif earlyWarningRadarUnit == nil then\n\t\tself:printOutputToLog(\"you have added an EW Radar that does not exist, check name of Unit in Setup and Mission editor: \"..earlyWarningRadarUnitName, true)\n\t\treturn\n\tend\n\tself:setCoalition(earlyWarningRadarUnit)\n\tlocal ewRadar = nil\n\tlocal category = earlyWarningRadarUnit:getDesc().category\n\tif category == Unit.Category.AIRPLANE or category == Unit.Category.SHIP then\n\t\tewRadar = SkynetIADSAWACSRadar:create(earlyWarningRadarUnit, self)\n\telse\n\t\tewRadar = SkynetIADSEWRadar:create(earlyWarningRadarUnit, self)\n\tend\n\tewRadar:setupElements()\n\tewRadar:setCachedTargetsMaxAge(self:getCachedTargetsMaxAge())\t\n\t-- for performance improvement, if iads is not scanning no update coverage update needs to be done, will be executed once when iads activates\n\tif self.ewRadarScanMistTaskID ~= nil then\n\t\tself:buildRadarCoverageForEarlyWarningRadar(ewRadar)\n\tend\n\tewRadar:setActAsEW(true)\n\tewRadar:setToCorrectAutonomousState()\n\tewRadar:goLive()\n\ttable.insert(self.earlyWarningRadars, ewRadar)\n\tif self:getDebugSettings().addedEWRadar then\n\t\t\tself:printOutputToLog(\"ADDED: \"..ewRadar:getDescription())\n\tend\n\treturn ewRadar\nend\n\nfunction SkynetIADS:getCachedTargetsMaxAge()\n\treturn self.contactUpdateInterval\nend\n\nfunction SkynetIADS:getEarlyWarningRadars()\n\treturn self:createTableDelegator(self.earlyWarningRadars)\nend\n\nfunction SkynetIADS:getEarlyWarningRadarByUnitName(unitName)\n\tfor i = 1, #self.earlyWarningRadars do\n\t\tlocal ewRadar = self.earlyWarningRadars[i]\n\t\tif ewRadar:getDCSName() == unitName then\n\t\t\treturn ewRadar\n\t\tend\n\tend\nend\n\nfunction SkynetIADS:findSubString(haystack, needle)\n\treturn string.find(haystack, needle, 1, true)\nend\n\nfunction SkynetIADS:addSAMSitesByPrefix(prefix)\n\tself:deativateSAMSites()\n\tself.samSites = {}\n\tfor groupName, groupData in pairs(mist.DBs.groupsByName) do\n\t\tlocal pos = self:findSubString(groupName, prefix)\n\t\tif pos and pos == 1 then\n\t\t\t--mist returns groups, units and, StaticObjects\n\t\t\tlocal dcsObject = Group.getByName(groupName)\n\t\t\tif dcsObject and dcsObject:getUnits()[1]:isActive() then\n\t\t\t\tself:addSAMSite(groupName)\n\t\t\tend\n\t\tend\n\tend\n\treturn self:createTableDelegator(self.samSites)\nend\n\nfunction SkynetIADS:getSAMSitesByPrefix(prefix)\n\tlocal returnSams = {}\n\tfor i = 1, #self.samSites do\n\t\tlocal samSite = self.samSites[i]\n\t\tlocal groupName = samSite:getDCSName()\n\t\tlocal pos = self:findSubString(groupName, prefix)\n\t\tif pos and pos == 1 then\n\t\t\ttable.insert(returnSams, samSite)\n\t\tend\n\tend\n\treturn self:createTableDelegator(returnSams)\nend\n\nfunction SkynetIADS:addSAMSite(samSiteName)\n\tlocal samSiteDCS = Group.getByName(samSiteName)\n\tif samSiteDCS == nil then\n\t\tself:printOutputToLog(\"you have added an SAM Site that does not exist, check name of Group in Setup and Mission editor: \"..tostring(samSiteName), true)\n\t\treturn\n\tend\n\tself:setCoalition(samSiteDCS)\n\tlocal samSite = SkynetIADSSamSite:create(samSiteDCS, self)\n\tsamSite:setupElements()\n\tsamSite:setCanEngageAirWeapons(true)\n\tsamSite:goLive()\n\tsamSite:setCachedTargetsMaxAge(self:getCachedTargetsMaxAge())\n\tif samSite:getNatoName() == \"UNKNOWN\" then\n\t\tself:printOutputToLog(\"you have added an SAM site that Skynet IADS can not handle: \"..samSite:getDCSName(), true)\n\t\tsamSite:cleanUp()\n\telse\n\t\tsamSite:goDark()\n\t\ttable.insert(self.samSites, samSite)\n\t\tif self:getDebugSettings().addedSAMSite then\n\t\t\tself:printOutputToLog(\"ADDED: \"..samSite:getDescription())\n\t\tend\n\t\t-- for performance improvement, if iads is not scanning no update coverage update needs to be done, will be executed once when iads activates\n\t\tif self.ewRadarScanMistTaskID ~= nil then\n\t\t\tself:buildRadarCoverageForSAMSite(samSite)\n\t\tend\n\t\treturn samSite\n\tend \nend\n\nfunction SkynetIADS:getUsableSAMSites()\n\treturn self:getUsableAbstractRadarElemtentsOfTable(self.samSites)\nend\n\nfunction SkynetIADS:getDestroyedSAMSites()\n\tlocal destroyedSites = {}\n\tfor i = 1, #self.samSites do\n\t\tlocal samSite = self.samSites[i]\n\t\tif samSite:isDestroyed() then\n\t\t\ttable.insert(destroyedSites, samSite)\n\t\tend\n\tend\n\treturn destroyedSites\nend\n\nfunction SkynetIADS:getSAMSites()\n\treturn self:createTableDelegator(self.samSites)\nend\n\nfunction SkynetIADS:getActiveSAMSites()\n\tlocal activeSAMSites = {}\n\tfor i = 1, #self.samSites do\n\t\tif self.samSites[i]:isActive() then\n\t\t\ttable.insert(activeSAMSites, self.samSites[i])\n\t\tend\n\tend\n\treturn activeSAMSites\nend\n\nfunction SkynetIADS:getSAMSiteByGroupName(groupName)\n\tfor i = 1, #self.samSites do\n\t\tlocal samSite = self.samSites[i]\n\t\tif samSite:getDCSName() == groupName then\n\t\t\treturn samSite\n\t\tend\n\tend\nend\n\nfunction SkynetIADS:getSAMSitesByNatoName(natoName)\n\tlocal selectedSAMSites = SkynetIADSTableDelegator:create()\n\tfor i = 1, #self.samSites do\n\t\tlocal samSite = self.samSites[i]\n\t\tif samSite:getNatoName() == natoName then\n\t\t\ttable.insert(selectedSAMSites, samSite)\n\t\tend\n\tend\n\treturn selectedSAMSites\nend\n\nfunction SkynetIADS:addCommandCenter(commandCenter)\n\tself:setCoalition(commandCenter)\n\tlocal comCenter = SkynetIADSCommandCenter:create(commandCenter, self)\n\ttable.insert(self.commandCenters, comCenter)\n\t-- when IADS is active the radars will be added to the new command center. If it not active this will happen when radar coverage is built\n\tif self.ewRadarScanMistTaskID ~= nil then\n\t\tself:addRadarsToCommandCenters()\n\tend\n\treturn comCenter\nend\n\nfunction SkynetIADS:isCommandCenterUsable()\n\tif #self:getCommandCenters() == 0 then\n\t\treturn true\n\tend\n\tlocal usableComCenters = self:getUsableAbstractRadarElemtentsOfTable(self:getCommandCenters())\n\treturn (#usableComCenters > 0)\nend\n\nfunction SkynetIADS:getCommandCenters()\n\treturn self.commandCenters\nend\n\n\nfunction SkynetIADS.evaluateContacts(self)\n\n\tlocal ewRadars = self:getUsableEarlyWarningRadars()\n\tlocal samSites = self:getUsableSAMSites()\n\t\n\t--will add SAM Sites acting as EW Rardars to the ewRadars array:\n\tfor i = 1, #samSites do\n\t\tlocal samSite = samSites[i]\n\t\t--We inform SAM sites that a target update is about to happen. If they have no targets in range after the cycle they go dark\n\t\tsamSite:targetCycleUpdateStart()\n\t\tif samSite:getActAsEW() then\n\t\t\ttable.insert(ewRadars, samSite)\n\t\tend\n\t\t--if the sam site is not in ew mode and active we grab the detected targets right here\n\t\tif samSite:isActive() and samSite:getActAsEW() == false then\n\t\t\tlocal contacts = samSite:getDetectedTargets()\n\t\t\tfor j = 1, #contacts do\n\t\t\t\tlocal contact = contacts[j]\n\t\t\t\tself:mergeContact(contact)\n\t\t\tend\n\t\tend\n\tend\n\n\tlocal samSitesToTrigger = {}\n\t\n\tfor i = 1, #ewRadars do\n\t\tlocal ewRadar = ewRadars[i]\n\t\t--call go live in case ewRadar had to shut down (HARM attack)\n\t\tewRadar:goLive()\n\t\t-- if an awacs has traveled more than a predeterminded distance we update the autonomous state of the SAMs\n\t\tif getmetatable(ewRadar) == SkynetIADSAWACSRadar and ewRadar:isUpdateOfAutonomousStateOfSAMSitesRequired() then\n\t\t\tself:buildRadarCoverageForEarlyWarningRadar(ewRadar)\n\t\tend\n\t\tlocal ewContacts = ewRadar:getDetectedTargets()\n\t\tif #ewContacts > 0 then\n\t\t\tlocal samSitesUnderCoverage = ewRadar:getUsableChildRadars()\n\t\t\tfor j = 1, #samSitesUnderCoverage do\n\t\t\t\tlocal samSiteUnterCoverage = samSitesUnderCoverage[j]\n\t\t\t\t-- only if a SAM site is not active we add it to the hash of SAM sites to be iterated later on\n\t\t\t\tif samSiteUnterCoverage:isActive() == false then\n\t\t\t\t\t--we add them to a hash to make sure each SAM site is in the collection only once, reducing the number of loops we conduct later on\n\t\t\t\t\tsamSitesToTrigger[samSiteUnterCoverage:getDCSName()] = samSiteUnterCoverage\n\t\t\t\tend\n\t\t\tend\n\t\t\tfor j = 1, #ewContacts do\n\t\t\t\tlocal contact = ewContacts[j]\n\t\t\t\tself:mergeContact(contact)\n\t\t\tend\n\t\tend\n\tend\n\n\tself:cleanAgedTargets()\n\t\n\tfor samName, samToTrigger in pairs(samSitesToTrigger) do\n\t\tfor j = 1, #self.contacts do\n\t\t\tlocal contact = self.contacts[j]\n\t\t\t-- the DCS Radar only returns enemy aircraft, if that should change a coalition check will be required\n\t\t\t-- currently every type of object in the air is handed of to the SAM site, including missiles\n\t\t\tlocal description = contact:getDesc()\n\t\t\tlocal category = description.category\n\t\t\tif category and category ~= Unit.Category.GROUND_UNIT and category ~= Unit.Category.SHIP and category ~= Unit.Category.STRUCTURE then\n\t\t\t\tsamToTrigger:informOfContact(contact)\n\t\t\tend\n\t\tend\n\tend\n\t\n\tfor i = 1, #samSites do\n\t\tlocal samSite = samSites[i]\n\t\tsamSite:targetCycleUpdateEnd()\n\tend\n\t\n\tself.harmDetection:setContacts(self:getContacts())\n\tself.harmDetection:evaluateContacts()\n\t\n\tself.logger:printSystemStatus()\nend\n\nfunction SkynetIADS:cleanAgedTargets()\n\tlocal contactsToKeep = {}\n\tfor i = 1, #self.contacts do\n\t\tlocal contact = self.contacts[i]\n\t\tif contact:getAge() < self.maxTargetAge then\n\t\t\ttable.insert(contactsToKeep, contact)\n\t\tend\n\tend\n\tself.contacts = contactsToKeep\nend\n\n--TODO unit test this method:\nfunction SkynetIADS:getAbstracRadarElements()\n\tlocal abstractRadarElements = {}\n\tlocal ewRadars = self:getEarlyWarningRadars()\n\tlocal samSites = self:getSAMSites()\n\t\n\tfor i = 1, #ewRadars do\n\t\tlocal ewRadar = ewRadars[i]\n\t\ttable.insert(abstractRadarElements, ewRadar)\n\tend\n\t\n\tfor i = 1, #samSites do\n\t\tlocal samSite = samSites[i]\n\t\ttable.insert(abstractRadarElements, samSite)\n\tend\n\treturn abstractRadarElements\nend\n\n\nfunction SkynetIADS:addRadarsToCommandCenters()\n\n\t--we clear any existing radars that may have been added earlier\n\tlocal comCenters = self:getCommandCenters()\n\tfor i = 1, #comCenters do\n\t\tlocal comCenter = comCenters[i]\n\t\tcomCenter:clearChildRadars()\n\tend\t\n\t\n\t-- then we add child radars to the command centers\n\tlocal abstractRadarElements = self:getAbstracRadarElements()\n\t\tfor i = 1, #abstractRadarElements do\n\t\t\tlocal abstractRadar = abstractRadarElements[i]\n\t\t\tself:addSingleRadarToCommandCenters(abstractRadar)\n\t\tend\nend\n\nfunction SkynetIADS:addSingleRadarToCommandCenters(abstractRadarElement)\n\tlocal comCenters = self:getCommandCenters()\n\tfor i = 1, #comCenters do\n\t\tlocal comCenter = comCenters[i]\n\t\tcomCenter:addChildRadar(abstractRadarElement)\n\tend\t\nend\n\n-- this method rebuilds the radar coverage of the IADS, a complete rebuild is only required the first time the IADS is activated\n-- during runtime it is sufficient to call buildRadarCoverageForSAMSite or buildRadarCoverageForEarlyWarningRadar method that just updates the IADS for one unit, this saves script execution time\nfunction SkynetIADS:buildRadarCoverage()\t\n\t\n\t--to build the basic radar coverage we use all SAM sites. Checks if SAM site has power or a connection node is done when using the SAM site later on\n\tlocal samSites = self:getSAMSites()\n\t\n\t--first we clear all child and parent radars that may have been added previously\n\tfor i = 1, #samSites do\n\t\tlocal samSite = samSites[i]\n\t\tsamSite:clearChildRadars()\n\t\tsamSite:clearParentRadars()\n\tend\n\t\n\tlocal ewRadars = self:getEarlyWarningRadars()\n\t\n\tfor i = 1, #ewRadars do\n\t\tlocal ewRadar = ewRadars[i]\n\t\tewRadar:clearChildRadars()\n\tend\t\n\t\n\t--then we rebuild the radar coverage\n\tlocal abstractRadarElements = self:getAbstracRadarElements()\n\tfor i = 1, #abstractRadarElements do\n\t\tlocal abstract = abstractRadarElements[i]\n\t\tself:buildRadarCoverageForAbstractRadarElement(abstract)\n\tend\n\t\n\tself:addRadarsToCommandCenters()\n\t\n\t--we call this once on all sam sites, to make sure autonomous sites go live when IADS activates\n\tfor i = 1, #samSites do\n\t\tlocal samSite = samSites[i]\n\t\tsamSite:informChildrenOfStateChange()\n\tend\n\nend\n\nfunction SkynetIADS:buildRadarCoverageForAbstractRadarElement(abstractRadarElement)\n\tlocal abstractRadarElements = self:getAbstracRadarElements()\n\tfor i = 1, #abstractRadarElements do\n\t\tlocal aElementToCompare = abstractRadarElements[i]\n\t\tif aElementToCompare ~= abstractRadarElement then\n\t\t\tif abstractRadarElement:isInRadarDetectionRangeOf(aElementToCompare) then\n\t\t\t\tself:buildRadarAssociation(aElementToCompare, abstractRadarElement)\n\t\t\tend\n\t\t\tif aElementToCompare:isInRadarDetectionRangeOf(abstractRadarElement) then\n\t\t\t\tself:buildRadarAssociation(abstractRadarElement, aElementToCompare)\n\t\t\tend\n\t\tend\n\tend\nend\n\nfunction SkynetIADS:buildRadarAssociation(parent, child)\n\t--chilren should only be SAM sites not EW radars\n\tif ( getmetatable(child) == SkynetIADSSamSite ) then\n\t\tparent:addChildRadar(child)\n\tend\n\t--Only SAM Sites should have parent Radars, not EW Radars\n\tif ( getmetatable(child) == SkynetIADSSamSite ) then\n\t\tchild:addParentRadar(parent)\n\tend\nend\n\nfunction SkynetIADS:buildRadarCoverageForSAMSite(samSite)\n\tself:buildRadarCoverageForAbstractRadarElement(samSite)\n\tself:addSingleRadarToCommandCenters(samSite)\nend\n\nfunction SkynetIADS:buildRadarCoverageForEarlyWarningRadar(ewRadar)\n\tself:buildRadarCoverageForAbstractRadarElement(ewRadar)\n\tself:addSingleRadarToCommandCenters(ewRadar)\nend\n\nfunction SkynetIADS:mergeContact(contact)\n\tlocal existingContact = false\n\tfor i = 1, #self.contacts do\n\t\tlocal iadsContact = self.contacts[i]\n\t\tif iadsContact:getName() == contact:getName() then\n\t\t\tiadsContact:refresh()\n\t\t\t--these contacts are used in the logger we set a kown harm state of a contact coming from a SAM site. So the logger will show them als HARMs\n\t\t\tcontact:setHARMState(iadsContact:getHARMState())\n\t\t\tlocal radars = contact:getAbstractRadarElementsDetected()\n\t\t\tfor j = 1, #radars do\n\t\t\t\tlocal radar = radars[j]\n\t\t\t\tiadsContact:addAbstractRadarElementDetected(radar)\n\t\t\tend\n\t\t\texistingContact = true\n\t\tend\n\tend\n\tif existingContact == false then\n\t\ttable.insert(self.contacts, contact)\n\tend\nend\n\n\nfunction SkynetIADS:getContacts()\n\treturn self.contacts\nend\n\nfunction SkynetIADS:getDebugSettings()\n\treturn self.logger.debugOutput\nend\n\nfunction SkynetIADS:printOutput(output, typeWarning)\n\tself.logger:printOutput(output, typeWarning)\nend\n\nfunction SkynetIADS:printOutputToLog(output)\n\tself.logger:printOutputToLog(output)\nend\n\n-- will start going through the Early Warning Radars and SAM sites to check what targets they have detected\nfunction SkynetIADS.activate(self)\n\tmist.removeFunction(self.ewRadarScanMistTaskID)\n\tself.ewRadarScanMistTaskID = mist.scheduleFunction(SkynetIADS.evaluateContacts, {self}, 1, self.contactUpdateInterval)\n\tself:buildRadarCoverage()\nend\n\nfunction SkynetIADS:setupSAMSitesAndThenActivate(setupTime)\n\tself:activate()\n\tself.logger:printOutputToLog(\"DEPRECATED: setupSAMSitesAndThenActivate, no longer needed since using enableEmission instead of AI on / off allows for the Ground units to setup with their radars turned off\")\nend\n\nfunction SkynetIADS:deactivate()\n\tmist.removeFunction(self.ewRadarScanMistTaskID)\n\tmist.removeFunction(self.samSetupMistTaskID)\n\tself:deativateSAMSites()\n\tself:deactivateEarlyWarningRadars()\n\tself:deactivateCommandCenters()\nend\n\nfunction SkynetIADS:deactivateCommandCenters()\n\tfor i = 1, #self.commandCenters do\n\t\tlocal comCenter = self.commandCenters[i]\n\t\tcomCenter:cleanUp()\n\tend\nend\n\nfunction SkynetIADS:deativateSAMSites()\n\tfor i = 1, #self.samSites do\n\t\tlocal samSite = self.samSites[i]\n\t\tsamSite:cleanUp()\n\tend\nend\n\nfunction SkynetIADS:deactivateEarlyWarningRadars()\n\tfor i = 1, #self.earlyWarningRadars do\n\t\tlocal ewRadar = self.earlyWarningRadars[i]\n\t\tewRadar:cleanUp()\n\tend\nend\t\n\nfunction SkynetIADS:addRadioMenu()\n\tself.radioMenu = missionCommands.addSubMenu('SKYNET IADS '..self:getCoalitionString())\n\tlocal displayIADSStatus = missionCommands.addCommand('show IADS Status', self.radioMenu, SkynetIADS.updateDisplay, {self = self, value = true, option = 'IADSStatus'})\n\tlocal displayIADSStatus = missionCommands.addCommand('hide IADS Status', self.radioMenu, SkynetIADS.updateDisplay, {self = self, value = false, option = 'IADSStatus'})\n\tlocal displayIADSStatus = missionCommands.addCommand('show contacts', self.radioMenu, SkynetIADS.updateDisplay, {self = self, value = true, option = 'contacts'})\n\tlocal displayIADSStatus = missionCommands.addCommand('hide contacts', self.radioMenu, SkynetIADS.updateDisplay, {self = self, value = false, option = 'contacts'})\nend\n\nfunction SkynetIADS:removeRadioMenu()\n\tmissionCommands.removeItem(self.radioMenu)\nend\n\nfunction SkynetIADS.updateDisplay(params)\n\tlocal option = params.option\n\tlocal self = params.self\n\tlocal value = params.value\n\tif option == 'IADSStatus' then\n\t\tself:getDebugSettings()[option] = value\n\telseif option == 'contacts' then\n\t\tself:getDebugSettings()[option] = value\n\tend\nend\n\nfunction SkynetIADS:getCoalitionString()\n\tlocal coalitionStr = \"RED\"\n\tif self.coalitionID == coalition.side.BLUE then\n\t\tcoalitionStr = \"BLUE\"\n\telseif self.coalitionID == coalition.side.NEUTRAL then\n\t\tcoalitionStr = \"NEUTRAL\"\n\tend\n\t\t\n\tif self.name then\n\t\tcoalitionStr = \"COALITION: \"..coalitionStr..\" | NAME: \"..self.name\n\tend\n\t\n\treturn coalitionStr\nend\n\nfunction SkynetIADS:getMooseConnector()\n\tif self.mooseConnector == nil then\n\t\tself.mooseConnector = SkynetMooseA2ADispatcherConnector:create(self)\n\tend\n\treturn self.mooseConnector\nend\n\nfunction SkynetIADS:addMooseSetGroup(mooseSetGroup)\n\tself:getMooseConnector():addMooseSetGroup(mooseSetGroup)\nend\n\nend\ndo\n\nSkynetMooseA2ADispatcherConnector = {}\n\nfunction SkynetMooseA2ADispatcherConnector:create(iads)\n\tlocal instance = {}\n\tsetmetatable(instance, self)\n\tself.__index = self\n\tinstance.iadsCollection = {}\n\tinstance.mooseGroups = {}\n\tinstance.ewRadarGroupNames = {}\n\tinstance.samSiteGroupNames = {}\n\ttable.insert(instance.iadsCollection, iads)\n\treturn instance\nend\n\nfunction SkynetMooseA2ADispatcherConnector:addIADS(iads)\n\ttable.insert(self.iadsCollection, iads)\nend\n\nfunction SkynetMooseA2ADispatcherConnector:addMooseSetGroup(mooseSetGroup)\n\ttable.insert(self.mooseGroups, mooseSetGroup)\n\tself:update()\nend\n\nfunction SkynetMooseA2ADispatcherConnector:getEarlyWarningRadarGroupNames()\n\tself.ewRadarGroupNames = {}\n\tfor i = 1, #self.iadsCollection do\n\t\tlocal ewRadars = self.iadsCollection[i]:getUsableEarlyWarningRadars()\n\t\tfor j = 1, #ewRadars do\n\t\t\tlocal ewRadar = ewRadars[j]\n\t\t\ttable.insert(self.ewRadarGroupNames, ewRadar:getDCSRepresentation():getGroup():getName())\n\t\tend\n\tend\n\treturn self.ewRadarGroupNames\nend\n\nfunction SkynetMooseA2ADispatcherConnector:getSAMSiteGroupNames()\n\tself.samSiteGroupNames = {}\n\tfor i = 1, #self.iadsCollection do\n\t\tlocal samSites = self.iadsCollection[i]:getUsableSAMSites()\n\t\tfor j = 1, #samSites do\n\t\t\tlocal samSite = samSites[j]\n\t\t\ttable.insert(self.samSiteGroupNames, samSite:getDCSName())\n\t\tend\n\tend\n\treturn self.samSiteGroupNames\nend\n\nfunction SkynetMooseA2ADispatcherConnector:update()\n\t\n\t--mooseGroup elements are type of:\n\t--https://flightcontrol-master.github.io/MOOSE_DOCS_DEVELOP/Documentation/Core.Set.html##(SET_GROUP)\n\t\n\t--remove previously set group names:\n\tfor i = 1, #self.mooseGroups do\n\t\tlocal mooseGroup = self.mooseGroups[i]\n\t\tmooseGroup:RemoveGroupsByName(self.ewRadarGroupNames)\n\t\tmooseGroup:RemoveGroupsByName(self.samSiteGroupNames)\n\tend\n\t\n\t--add group names of IADS radars that are currently usable by the IADS:\n\tfor i = 1, #self.mooseGroups do\n\t\tlocal mooseGroup = self.mooseGroups[i]\n\t\tmooseGroup:AddGroupsByName(self:getEarlyWarningRadarGroupNames())\n\t\tmooseGroup:AddGroupsByName(self:getSAMSiteGroupNames())\n\tend\nend\n\nend\ndo\n\n\nSkynetIADSTableDelegator = {}\n\nfunction SkynetIADSTableDelegator:create()\n\tlocal instance = {}\n\tlocal forwarder = {}\n\tforwarder.__index = function(tbl, name)\n\t\ttbl[name] = function(self, ...)\n\t\t\t\tfor i = 1, #self do\n\t\t\t\t\tself[i][name](self[i], ...)\n\t\t\t\tend\n\t\t\t\treturn self\n\t\t\tend\n\t\treturn tbl[name]\n\tend\n\tsetmetatable(instance, forwarder)\n\tinstance.__index = forwarder\n\treturn instance\nend\n\nend\ndo\n\nSkynetIADSAbstractDCSObjectWrapper = {}\n\nfunction SkynetIADSAbstractDCSObjectWrapper:create(dcsRepresentation)\n\tlocal instance = {}\n\tsetmetatable(instance, self)\n\tself.__index = self\n\tinstance.dcsName = \"\"\n\tinstance.typeName = \"\"\n\tinstance:setDCSRepresentation(dcsRepresentation)\n\tif getmetatable(dcsRepresentation) ~= Group then\n\t\tinstance.typeName = dcsRepresentation:getTypeName()\n\tend\n\treturn instance\nend\n\nfunction SkynetIADSAbstractDCSObjectWrapper:setDCSRepresentation(representation)\n\tself.dcsRepresentation = representation\n\tif self.dcsRepresentation then\n\t\tself.dcsName = self.dcsRepresentation:getName()\n\t\tif (self.dcsName == nil or string.len(self.dcsName) == 0) and self.dcsRepresentation.id_ then\n\t\t\tself.dcsName = self.dcsRepresentation.id_\n\t\tend\n\tend\nend\n\nfunction SkynetIADSAbstractDCSObjectWrapper:getDCSRepresentation()\n\treturn self.dcsRepresentation\nend\n\nfunction SkynetIADSAbstractDCSObjectWrapper:getName()\n\treturn self.dcsName\nend\n\nfunction SkynetIADSAbstractDCSObjectWrapper:getTypeName()\n\treturn self.typeName\nend\n\nfunction SkynetIADSAbstractDCSObjectWrapper:getPosition()\n\treturn self.dcsRepresentation:getPosition()\nend\n\nfunction SkynetIADSAbstractDCSObjectWrapper:isExist()\n\tif self.dcsRepresentation then\n\t\treturn self.dcsRepresentation:isExist()\n\telse\n\t\treturn false\n\tend\nend\n\nfunction SkynetIADSAbstractDCSObjectWrapper:insertToTableIfNotAlreadyAdded(tbl, object)\n\tlocal isAdded = false\n\tfor i = 1, #tbl do\n\t\tlocal child = tbl[i]\n\t\tif child == object then\n\t\t\tisAdded = true\n\t\tend\n\tend\n\tif isAdded == false then\n\t\ttable.insert(tbl, object)\n\tend\n\treturn not isAdded\nend\n\n-- helper code for class inheritance\nfunction inheritsFrom( baseClass )\n\n    local new_class = {}\n    local class_mt = { __index = new_class }\n\n    function new_class:create()\n        local newinst = {}\n        setmetatable( newinst, class_mt )\n        return newinst\n    end\n\n    if nil ~= baseClass then\n        setmetatable( new_class, { __index = baseClass } )\n    end\n\n    -- Implementation of additional OO properties starts here --\n\n    -- Return the class object of the instance\n    function new_class:class()\n        return new_class\n    end\n\n    -- Return the super class object of the instance\n    function new_class:superClass()\n        return baseClass\n    end\n\n    -- Return true if the caller is an instance of theClass\n    function new_class:isa( theClass )\n        local b_isa = false\n\n        local cur_class = new_class\n\n        while ( nil ~= cur_class ) and ( false == b_isa ) do\n            if cur_class == theClass then\n                b_isa = true\n            else\n                cur_class = cur_class:superClass()\n            end\n        end\n\n        return b_isa\n    end\n\n    return new_class\nend\n\n\nend\n\ndo\n\nSkynetIADSAbstractElement = {}\nSkynetIADSAbstractElement = inheritsFrom(SkynetIADSAbstractDCSObjectWrapper)\n\nfunction SkynetIADSAbstractElement:create(dcsRepresentation, iads)\n\tlocal instance = self:superClass():create(dcsRepresentation)\n\tsetmetatable(instance, self)\n\tself.__index = self\n\tinstance.connectionNodes = {}\n\tinstance.powerSources = {}\n\tinstance.iads = iads\n\tinstance.natoName = \"UNKNOWN\"\n\tworld.addEventHandler(instance)\n\treturn instance\nend\n\nfunction SkynetIADSAbstractElement:removeEventHandlers()\n\tworld.removeEventHandler(self)\nend\n\nfunction SkynetIADSAbstractElement:cleanUp()\n\tself:removeEventHandlers()\nend\n\nfunction SkynetIADSAbstractElement:isDestroyed()\n\treturn self:getDCSRepresentation():isExist() == false\nend\n\nfunction SkynetIADSAbstractElement:addPowerSource(powerSource)\n\ttable.insert(self.powerSources, powerSource)\n\tself:informChildrenOfStateChange()\n\treturn self\nend\n\nfunction SkynetIADSAbstractElement:getPowerSources()\n\treturn self.powerSources\nend\n\nfunction SkynetIADSAbstractElement:addConnectionNode(connectionNode)\n\ttable.insert(self.connectionNodes, connectionNode)\n\tself:informChildrenOfStateChange()\n\treturn self\nend\n\nfunction SkynetIADSAbstractElement:getConnectionNodes()\n\treturn self.connectionNodes\nend\n\nfunction SkynetIADSAbstractElement:hasActiveConnectionNode()\n\tlocal connectionNode = self:genericCheckOneObjectIsAlive(self.connectionNodes)\n\tif connectionNode == false and self.iads:getDebugSettings().samNoConnection then\n\t\tself.iads:printOutput(self:getDescription()..\" no connection to Command Center\")\n\tend\n\treturn connectionNode\nend\n\nfunction SkynetIADSAbstractElement:hasWorkingPowerSource()\n\tlocal power = self:genericCheckOneObjectIsAlive(self.powerSources)\n\tif power == false and self.iads:getDebugSettings().hasNoPower then\n\t\tself.iads:printOutput(self:getDescription()..\" has no power\")\n\tend\n\treturn power\nend\n\nfunction SkynetIADSAbstractElement:getDCSName()\n\treturn self.dcsName\nend\n\n-- generic function to theck if power plants, command centers, connection nodes are still alive\nfunction SkynetIADSAbstractElement:genericCheckOneObjectIsAlive(objects)\n\tlocal isAlive = (#objects == 0)\n\tfor i = 1, #objects do\n\t\tlocal object = objects[i]\n\t\t--if we find one object that is not fully destroyed we assume the IADS is still working\n\t\tif object:isExist() then\n\t\t\tisAlive = true\n\t\t\tbreak\n\t\tend\n\tend\n\treturn isAlive\nend\n\nfunction SkynetIADSAbstractElement:getNatoName()\n\treturn self.natoName\nend\n\nfunction SkynetIADSAbstractElement:getDescription()\n\treturn \"IADS ELEMENT: \"..self:getDCSName()..\" | Type: \"..tostring(self:getNatoName())\nend\n\nfunction SkynetIADSAbstractElement:onEvent(event)\n\t--if a unit is destroyed we check to see if its a power plant powering the unit or a connection node\n\tif event.id == world.event.S_EVENT_DEAD then\n\t\tif self:hasWorkingPowerSource() == false or self:isDestroyed() then\n\t\t\tself:goDark()\n\t\t\tself:informChildrenOfStateChange()\n\t\tend\n\t\tif self:hasActiveConnectionNode() == false then\n\t\t\tself:informChildrenOfStateChange()\n\t\tend\n\tend\n\tif event.id == world.event.S_EVENT_SHOT then\n\t\tself:weaponFired(event)\n\tend\nend\n\n--placeholder method, can be implemented by subclasses\nfunction SkynetIADSAbstractElement:weaponFired(event)\n\t\nend\n\n--placeholder method, can be implemented by subclasses\nfunction SkynetIADSAbstractElement:goDark()\n\t\nend\n\n--placeholder method, can be implemented by subclasses\nfunction SkynetIADSAbstractElement:goAutonomous()\n\nend\n\n--placeholder method, can be implemented by subclasses\nfunction SkynetIADSAbstractElement:setToCorrectAutonomousState()\n\nend\n\n--placeholder method, can be implemented by subclasses\nfunction SkynetIADSAbstractElement:informChildrenOfStateChange()\n\t\nend\n\nend\ndo\n\nSkynetIADSAbstractRadarElement = {}\nSkynetIADSAbstractRadarElement = inheritsFrom(SkynetIADSAbstractElement)\n\nSkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DCS_AI = 1\nSkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK = 2\n\nSkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE = 1\nSkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE = 2\n\nSkynetIADSAbstractRadarElement.HARM_TO_SAM_ASPECT = 15\nSkynetIADSAbstractRadarElement.HARM_LOOKAHEAD_NM = 20\n\nfunction SkynetIADSAbstractRadarElement:create(dcsElementWithRadar, iads)\n\tlocal instance = self:superClass():create(dcsElementWithRadar, iads)\n\tsetmetatable(instance, self)\n\tself.__index = self\n\tinstance.aiState = false\n\tinstance.harmScanID = nil\n\tinstance.harmSilenceID = nil\n\tinstance.lastJammerUpdate = 0\n\tinstance.objectsIdentifiedAsHarms = {}\n\tinstance.objectsIdentifiedAsHarmsMaxTargetAge = 60\n\tinstance.launchers = {}\n\tinstance.trackingRadars = {}\n\tinstance.searchRadars = {}\n\tinstance.parentRadars = {}\n\tinstance.childRadars = {}\n\tinstance.missilesInFlight = {}\n\tinstance.pointDefences = {}\n\tinstance.harmDecoys = {}\n\tinstance.autonomousBehaviour = SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DCS_AI\n\tinstance.goLiveRange = SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE\n\tinstance.isAutonomous = true\n\tinstance.harmDetectionChance = 0\n\tinstance.minHarmShutdownTime = 0\n\tinstance.maxHarmShutDownTime = 0\n\tinstance.minHarmPresetShutdownTime = 30\n\tinstance.maxHarmPresetShutdownTime = 180\n\tinstance.harmShutdownTime = 0\n\tinstance.firingRangePercent = 100\n\tinstance.actAsEW = false\n\tinstance.cachedTargets = {}\n\tinstance.cachedTargetsMaxAge = 1\n\tinstance.cachedTargetsCurrentAge = 0\n\tinstance.goLiveTime = 0\n\tinstance.engageAirWeapons = false\n\tinstance.isAPointDefence = false\n\tinstance.canEngageHARM = false\n\tinstance.dataBaseSupportedTypesCanEngageHARM = false\n\t-- 5 seconds seems to be a good value for the sam site to find the target with its organic radar\n\tinstance.noCacheActiveForSecondsAfterGoLive = 5\n\treturn instance\nend\n\n--TODO: this method could be updated to only return Radar weapons fired, this way a SAM firing an IR weapon could go dark faster in the goDark() method\nfunction SkynetIADSAbstractRadarElement:weaponFired(event)\n\tif event.id == world.event.S_EVENT_SHOT then\n\t\tlocal weapon = event.weapon\n\t\tlocal launcherFired = event.initiator\n\t\tfor i = 1, #self.launchers do\n\t\t\tlocal launcher = self.launchers[i]\n\t\t\tif launcher:getDCSRepresentation() == launcherFired then\n\t\t\t\ttable.insert(self.missilesInFlight, weapon)\n\t\t\tend\n\t\tend\n\tend\nend\n\nfunction SkynetIADSAbstractRadarElement:setCachedTargetsMaxAge(maxAge)\n\tself.cachedTargetsMaxAge = maxAge\nend\n\nfunction SkynetIADSAbstractRadarElement:cleanUp()\n\tfor i = 1, #self.pointDefences do\n\t\tlocal pointDefence = self.pointDefences[i]\n\t\tpointDefence:cleanUp()\n\tend\n\tmist.removeFunction(self.harmScanID)\n\tmist.removeFunction(self.harmSilenceID)\n\t--call method from super class\n\tself:removeEventHandlers()\nend\n\nfunction SkynetIADSAbstractRadarElement:setIsAPointDefence(state)\n\tif (state == true or state == false) then\n\t\tself.isAPointDefence = state\n\tend\nend\n\nfunction SkynetIADSAbstractRadarElement:getIsAPointDefence()\n\treturn self.isAPointDefence\nend\n\nfunction SkynetIADSAbstractRadarElement:addPointDefence(pointDefence)\n\ttable.insert(self.pointDefences, pointDefence)\n\tpointDefence:setIsAPointDefence(true)\n\treturn self\nend\n\nfunction SkynetIADSAbstractRadarElement:getPointDefences()\n\treturn self.pointDefences\nend\n\nfunction SkynetIADSAbstractRadarElement:addHARMDecoy(harmDecoy)\n\ttable.insert(self.harmDecoys, harmDecoy)\nend\n\nfunction SkynetIADSAbstractRadarElement:addParentRadar(parentRadar)\n\tself:insertToTableIfNotAlreadyAdded(self.parentRadars, parentRadar)\n\tself:informChildrenOfStateChange()\nend\n\nfunction SkynetIADSAbstractRadarElement:getParentRadars()\n\treturn self.parentRadars\nend\n\nfunction SkynetIADSAbstractRadarElement:clearParentRadars()\n\tself.parentRadars = {}\nend\n\nfunction SkynetIADSAbstractRadarElement:addChildRadar(childRadar)\n\tself:insertToTableIfNotAlreadyAdded(self.childRadars, childRadar)\nend\n\nfunction SkynetIADSAbstractRadarElement:getChildRadars()\n\treturn self.childRadars\nend\n\nfunction SkynetIADSAbstractRadarElement:clearChildRadars()\n\tself.childRadars = {}\nend\n\n--TODO: unit test this method\nfunction SkynetIADSAbstractRadarElement:getUsableChildRadars()\n\tlocal usableRadars = {}\n\tfor i = 1, #self.childRadars do\n\t\tlocal childRadar = self.childRadars[i]\n\t\tif childRadar:hasWorkingPowerSource() and childRadar:hasActiveConnectionNode() then\n\t\t\ttable.insert(usableRadars, childRadar)\n\t\tend\n\tend\t\n\treturn usableRadars\nend\n\nfunction SkynetIADSAbstractRadarElement:informChildrenOfStateChange()\n\tself:setToCorrectAutonomousState()\n\tlocal children = self:getChildRadars()\n\tfor i = 1, #children do\n\t\tlocal childRadar = children[i]\n\t\tchildRadar:setToCorrectAutonomousState()\n\tend\n\tself.iads:getMooseConnector():update()\nend\n\nfunction SkynetIADSAbstractRadarElement:setToCorrectAutonomousState()\n\tlocal parents = self:getParentRadars()\n\tfor i = 1, #parents do\n\t\tlocal parent = parents[i]\n\t\t--of one parent exists that still is connected to the IADS, the SAM site does not have to go autonomous\n\t\t--instead of isDestroyed() write method, hasWorkingSearchRadars()\n\t\tif self:hasActiveConnectionNode() and self.iads:isCommandCenterUsable() and parent:hasWorkingPowerSource() and parent:hasActiveConnectionNode() and parent:getActAsEW() == true and parent:isDestroyed() == false then\n\t\t\tself:resetAutonomousState()\n\t\t\treturn\n\t\tend\n\tend\n\tself:goAutonomous()\nend\n\n\nfunction SkynetIADSAbstractRadarElement:setAutonomousBehaviour(mode)\n\tif mode ~= nil then\n\t\tself.autonomousBehaviour = mode\n\tend\n\treturn self\nend\n\nfunction SkynetIADSAbstractRadarElement:getAutonomousBehaviour()\n\treturn self.autonomousBehaviour\nend\n\nfunction SkynetIADSAbstractRadarElement:resetAutonomousState()\n\tself.isAutonomous = false\n\tself:goDark()\nend\n\nfunction SkynetIADSAbstractRadarElement:goAutonomous()\n\tself.isAutonomous = true\n\tif self.autonomousBehaviour == SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK then\n\t\tself:goDark()\n\telse\n\t\tself:goLive()\n\tend\nend\n\nfunction SkynetIADSAbstractRadarElement:getAutonomousState()\n\treturn self.isAutonomous\nend\n\nfunction SkynetIADSAbstractRadarElement:pointDefencesHaveRemainingAmmo(minNumberOfMissiles)\n\tlocal remainingMissiles = 0\n\tfor i = 1, #self.pointDefences do\n\t\tlocal pointDefence = self.pointDefences[i]\n\t\tremainingMissiles = remainingMissiles + pointDefence:getRemainingNumberOfMissiles()\n\tend\n\treturn self:hasRequiredNumberOfMissiles(minNumberOfMissiles, remainingMissiles)\nend\n\nfunction SkynetIADSAbstractRadarElement:hasRequiredNumberOfMissiles(minNumberOfMissiles, remainingMissiles)\n\tlocal returnValue = false\n\tif ( remainingMissiles > 0 and remainingMissiles >= minNumberOfMissiles ) then\n\t\treturnValue = true\n\tend\n\treturn returnValue\nend\n\nfunction SkynetIADSAbstractRadarElement:hasRemainingAmmoToEngageMissiles(minNumberOfMissiles)\n\tlocal remainingMissiles = self:getRemainingNumberOfMissiles()\n\treturn self:hasRequiredNumberOfMissiles(minNumberOfMissiles, remainingMissiles)\nend\n\n-- this method needs to be refactored so that it works for ew radars that don't have launchers, or that it is only called by sam sites\nfunction SkynetIADSAbstractRadarElement:hasEnoughLaunchersToEngageMissiles(minNumberOfLaunchers)\n\tlocal launchers = self:getLaunchers()\n\tif(launchers ~= nil) then\n\t launchers = #self:getLaunchers()\n\telse \n\t\tlaunchers = 0\n\tend\n\treturn self:hasRequiredNumberOfMissiles(minNumberOfLaunchers, launchers)\nend\n\nfunction SkynetIADSAbstractRadarElement:pointDefencesHaveEnoughLaunchers(minNumberOfLaunchers)\n\tlocal numOfLaunchers = 0\n\tfor i = 1, #self.pointDefences do\n\t\tlocal pointDefence = self.pointDefences[i]\n\t\tnumOfLaunchers = numOfLaunchers + #pointDefence:getLaunchers()\t\n\tend\n\treturn self:hasRequiredNumberOfMissiles(minNumberOfLaunchers, numOfLaunchers)\nend\n\nfunction SkynetIADSAbstractRadarElement:setIgnoreHARMSWhilePointDefencesHaveAmmo(state)\n\tself.iads:printOutputToLog(\"DEPRECATED: setIgnoreHARMSWhilePointDefencesHaveAmmo SAM Site will stay live automaticall as long as itself or it's point defences can defend against a HARM\")\n\treturn self\nend\n\nfunction SkynetIADSAbstractRadarElement:hasMissilesInFlight()\n\treturn #self.missilesInFlight > 0\nend\n\nfunction SkynetIADSAbstractRadarElement:getNumberOfMissilesInFlight()\n\treturn #self.missilesInFlight\nend\n\n-- DCS does not send an event, when a missile is destroyed, so this method needs to be polled so that the missiles in flight are current, polling is done in the HARM Search call: evaluateIfTargetsContainHARMs\nfunction SkynetIADSAbstractRadarElement:updateMissilesInFlight()\n\tlocal missilesInFlight = {}\n\tfor i = 1, #self.missilesInFlight do\n\t\tlocal missile = self.missilesInFlight[i]\n\t\tif missile:isExist() then\n\t\t\ttable.insert(missilesInFlight, missile)\n\t\tend\n\tend\n\tself.missilesInFlight = missilesInFlight\n\tself:goDarkIfOutOfAmmo()\nend\n\nfunction SkynetIADSAbstractRadarElement:goDarkIfOutOfAmmo()\n\tif self:hasRemainingAmmo() == false and self:getActAsEW() == false then\n\t\tself:goDark()\n\tend\nend\n\nfunction SkynetIADSAbstractRadarElement:getActAsEW()\n\treturn self.actAsEW\nend\t\n\nfunction SkynetIADSAbstractRadarElement:setActAsEW(ewState)\n\tif ewState == true or ewState == false then\n\t\tlocal stateChange = false\n\t\tif ewState ~= self.actAsEW then\n\t\t\tstateChange = true\n\t\tend\n\t\tself.actAsEW = ewState\n\t\tif stateChange then\n\t\t\tself:informChildrenOfStateChange()\n\t\tend\n\tend\n\tif self.actAsEW == true then\n\t\tself:goLive()\n\telse\n\t\tself:goDark()\n\tend\n\treturn self\nend\n\nfunction SkynetIADSAbstractRadarElement:getUnitsToAnalyse()\n\tlocal units = {}\n\ttable.insert(units, self:getDCSRepresentation())\n\tif getmetatable(self:getDCSRepresentation()) == Group then\n\t\tunits = self:getDCSRepresentation():getUnits()\n\tend\n\treturn units\nend\n\nfunction SkynetIADSAbstractRadarElement:getRemainingNumberOfMissiles()\n\tlocal remainingNumberOfMissiles = 0\n\tfor i = 1, #self.launchers do\n\t\tlocal launcher = self.launchers[i]\n\t\tremainingNumberOfMissiles = remainingNumberOfMissiles + launcher:getRemainingNumberOfMissiles()\n\tend\n\treturn remainingNumberOfMissiles\nend\n\nfunction SkynetIADSAbstractRadarElement:getInitialNumberOfMissiles()\n\tlocal initalNumberOfMissiles = 0\n\tfor i = 1, #self.launchers do\n\t\tlocal launcher = self.launchers[i]\n\t\tinitalNumberOfMissiles = launcher:getInitialNumberOfMissiles() + initalNumberOfMissiles\n\tend\n\treturn initalNumberOfMissiles\nend\n\nfunction SkynetIADSAbstractRadarElement:getRemainingNumberOfShells()\n\tlocal remainingNumberOfShells = 0\n\tfor i = 1, #self.launchers do\n\t\tlocal launcher = self.launchers[i]\n\t\tremainingNumberOfShells = remainingNumberOfShells + launcher:getRemainingNumberOfShells()\n\tend\n\treturn remainingNumberOfShells\nend\n\nfunction SkynetIADSAbstractRadarElement:getInitialNumberOfShells()\n\tlocal initialNumberOfShells = 0\n\tfor i = 1, #self.launchers do\n\t\tlocal launcher = self.launchers[i]\n\t\tinitialNumberOfShells = initialNumberOfShells + launcher:getInitialNumberOfShells()\n\tend\n\treturn initialNumberOfShells\nend\n\nfunction SkynetIADSAbstractRadarElement:hasRemainingAmmo()\n\t--the launcher check is due to ew radars they have no launcher and no ammo and therefore are never out of ammo\n\treturn ( #self.launchers == 0 ) or ((self:getRemainingNumberOfMissiles() > 0 ) or ( self:getRemainingNumberOfShells() > 0 ) )\nend\n\nfunction SkynetIADSAbstractRadarElement:getHARMDetectionChance()\n\treturn self.harmDetectionChance\nend\n\nfunction SkynetIADSAbstractRadarElement:setHARMDetectionChance(chance)\n\tif chance and chance >= 0 and chance <= 100 then\n\t\tself.harmDetectionChance = chance\n\tend\n\treturn self\nend\n\nfunction SkynetIADSAbstractRadarElement:setupElements()\n\tlocal numUnits = #self:getUnitsToAnalyse()\n\tfor typeName, dataType in pairs(SkynetIADS.database) do\n\t\tlocal hasSearchRadar = false\n\t\tlocal hasTrackingRadar = false\n\t\tlocal hasLauncher = false\n\t\tself.searchRadars = {}\n\t\tself.trackingRadars = {}\n\t\tself.launchers = {}\n\t\tfor entry, unitData in pairs(dataType) do\n\t\t\tif entry == 'searchRadar' then\n\t\t\t\tself:analyseAndAddUnit(SkynetIADSSAMSearchRadar, self.searchRadars, unitData)\n\t\t\t\thasSearchRadar = true\n\t\t\tend\n\t\t\tif entry == 'launchers' then\n\t\t\t\tself:analyseAndAddUnit(SkynetIADSSAMLauncher, self.launchers, unitData)\n\t\t\t\thasLauncher = true\n\t\t\tend\n\t\t\tif entry == 'trackingRadar' then\n\t\t\t\tself:analyseAndAddUnit(SkynetIADSSAMTrackingRadar, self.trackingRadars, unitData)\n\t\t\t\thasTrackingRadar = true\n\t\t\tend\n\t\tend\n\t\t\n\t\t--this check ensures a unit or group has all required elements for the specific sam or ew type:\n\t\tif (hasLauncher and hasSearchRadar and hasTrackingRadar and #self.launchers > 0 and #self.searchRadars > 0  and #self.trackingRadars > 0 ) \n\t\t\tor (hasSearchRadar and hasLauncher and #self.searchRadars > 0 and #self.launchers > 0) then\n\t\t\tself:setHARMDetectionChance(dataType['harm_detection_chance'])\n\t\t\tself.dataBaseSupportedTypesCanEngageHARM = dataType['can_engage_harm'] \n\t\t\tself:setCanEngageHARM(self.dataBaseSupportedTypesCanEngageHARM)\n\t\t\tlocal natoName = dataType['name']['NATO']\n\t\t\tself:buildNatoName(natoName)\n\t\t\tbreak\n\t\tend\t\n\tend\nend\n\nfunction SkynetIADSAbstractRadarElement:setCanEngageHARM(canEngage)\n\tif canEngage == true or canEngage == false then\n\t\tself.canEngageHARM = canEngage\n\t\tif ( canEngage == true and self:getCanEngageAirWeapons() == false ) then\n\t\t\tself:setCanEngageAirWeapons(true)\n\t\tend\n\tend\n\treturn self\nend\n\nfunction SkynetIADSAbstractRadarElement:getCanEngageHARM()\n\treturn self.canEngageHARM\nend\n\nfunction SkynetIADSAbstractRadarElement:setCanEngageAirWeapons(engageAirWeapons)\n\tif self:isDestroyed() == false then\n\t\tlocal controller = self:getDCSRepresentation():getController()\n\t\tif ( engageAirWeapons == true ) then\n\t\t\tcontroller:setOption(AI.Option.Ground.id.ENGAGE_AIR_WEAPONS, true)\n\t\t\t--its important that we set var to true here, to prevent recursion in setCanEngageHARM\n\t\t\tself.engageAirWeapons = true\n\t\t\t--we set the original value we got when loading info about the SAM site\n\t\t\tself:setCanEngageHARM(self.dataBaseSupportedTypesCanEngageHARM)\n\t\telse\n\t\t\tcontroller:setOption(AI.Option.Ground.id.ENGAGE_AIR_WEAPONS, false)\n\t\t\tself:setCanEngageHARM(false)\n\t\t\tself.engageAirWeapons = false\n\t\tend\n\tend\n\treturn self\nend\n\nfunction SkynetIADSAbstractRadarElement:getCanEngageAirWeapons()\n\treturn self.engageAirWeapons\nend\n\nfunction SkynetIADSAbstractRadarElement:buildNatoName(natoName)\n\t--we shorten the SA-XX names and don't return their code names eg goa, gainful..\n\tlocal pos = natoName:find(\" \")\n\tlocal prefix = natoName:sub(1, 2)\n\tif string.lower(prefix) == 'sa' and pos ~= nil then\n\t\tself.natoName = natoName:sub(1, (pos-1))\n\telse\n\t\tself.natoName = natoName\n\tend\nend\n\nfunction SkynetIADSAbstractRadarElement:analyseAndAddUnit(class, tableToAdd, unitData)\n\tlocal units = self:getUnitsToAnalyse()\n\tfor i = 1, #units do\n\t\tlocal unit = units[i]\n\t\tself:buildSingleUnit(unit, class, tableToAdd, unitData)\n\tend\nend\n\nfunction SkynetIADSAbstractRadarElement:buildSingleUnit(unit, class, tableToAdd, unitData)\n\tlocal unitTypeName = unit:getTypeName()\n\tfor unitName, unitPerformanceData in pairs(unitData) do\n\t\tif unitName == unitTypeName then\n\t\t\tsamElement = class:create(unit)\n\t\t\tsamElement:setupRangeData()\n\t\t\ttable.insert(tableToAdd, samElement)\n\t\tend\n\tend\nend\n\nfunction SkynetIADSAbstractRadarElement:getController()\n\tlocal dcsRepresentation = self:getDCSRepresentation()\n\tif dcsRepresentation:isExist() then\n\t\treturn dcsRepresentation:getController()\n\telse\n\t\treturn nil\n\tend\nend\n\nfunction SkynetIADSAbstractRadarElement:getLaunchers()\n\treturn self.launchers\nend\n\nfunction SkynetIADSAbstractRadarElement:getSearchRadars()\n\treturn self.searchRadars\nend\n\nfunction SkynetIADSAbstractRadarElement:getTrackingRadars()\n\treturn self.trackingRadars\nend\n\nfunction SkynetIADSAbstractRadarElement:getRadars()\n\tlocal radarUnits = {}\t\n\tfor i = 1, #self.searchRadars do\n\t\ttable.insert(radarUnits, self.searchRadars[i])\n\tend\t\n\tfor i = 1, #self.trackingRadars do\n\t\ttable.insert(radarUnits, self.trackingRadars[i])\n\tend\n\treturn radarUnits\nend\n\nfunction SkynetIADSAbstractRadarElement:setGoLiveRangeInPercent(percent)\n\tif percent ~= nil then\n\t\tself.firingRangePercent = percent\t\n\t\tfor i = 1, #self.launchers do\n\t\t\tlocal launcher = self.launchers[i]\n\t\t\tlauncher:setFiringRangePercent(self.firingRangePercent)\n\t\tend\n\t\tfor i = 1, #self.searchRadars do\n\t\t\tlocal radar = self.searchRadars[i]\n\t\t\tradar:setFiringRangePercent(self.firingRangePercent)\n\t\tend\n\tend\n\treturn self\nend\n\nfunction SkynetIADSAbstractRadarElement:getGoLiveRangeInPercent()\n\treturn self.firingRangePercent\nend\n\nfunction SkynetIADSAbstractRadarElement:setEngagementZone(engagementZone)\n\tif engagementZone == SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE then\n\t\tself.goLiveRange = engagementZone\n\telseif engagementZone == SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE then\n\t\tself.goLiveRange = engagementZone\n\tend\n\treturn self\nend\n\nfunction SkynetIADSAbstractRadarElement:getEngagementZone()\n\treturn self.goLiveRange\nend\n\nfunction SkynetIADSAbstractRadarElement:goLive()\n\tif ( self.aiState == false and self:hasWorkingPowerSource() and self.harmSilenceID == nil) \n\tand (self:hasRemainingAmmo() == true  )\n\tthen\n\t\tif self:isDestroyed() == false then\n\t\t\tlocal  cont = self:getController()\n\t\t\tcont:setOnOff(true)\n\t\t\tcont:setOption(AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.RED)\t\n\t\t\tcont:setOption(AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_FREE)\n\t\t\tself:getDCSRepresentation():enableEmission(true)\n\t\t\tself.goLiveTime = timer.getTime()\n\t\t\tself.aiState = true\n\t\tend\n\t\tself:pointDefencesStopActingAsEW()\n\t\tif  self.iads:getDebugSettings().radarWentLive then\n\t\t\tself.iads:printOutputToLog(\"GOING LIVE: \"..self:getDescription())\n\t\tend\n\t\tself:scanForHarms()\n\tend\nend\n\nfunction SkynetIADSAbstractRadarElement:pointDefencesStopActingAsEW()\n\tfor i = 1, #self.pointDefences do\n\t\tlocal pointDefence = self.pointDefences[i]\n\t\tpointDefence:setActAsEW(false)\n\tend\nend\n\n\nfunction SkynetIADSAbstractRadarElement:goDark()\n\tif (self:hasWorkingPowerSource() == false) or ( self.aiState == true ) \n\tand (self.harmSilenceID ~= nil or ( self.harmSilenceID == nil and #self:getDetectedTargets() == 0 and self:hasMissilesInFlight() == false) or ( self.harmSilenceID == nil and #self:getDetectedTargets() > 0 and self:hasMissilesInFlight() == false and self:hasRemainingAmmo() == false ) )\t\n\tthen\n\t\tif self:isDestroyed() == false then\n\t\t\tself:getDCSRepresentation():enableEmission(false)\n\t\tend\n\t\t-- point defence will only go live if the Radar Emitting site it is protecting goes dark and this is due to a it defending against a HARM\n\t\tif (self.harmSilenceID ~= nil) then\n\t\t\tself:pointDefencesGoLive()\n\t\t\tif self:isDestroyed() == false then\n\t\t\t\t--if site goes dark due to HARM we turn off AI, this is due to a bug in DCS multiplayer where the harm will find its way to the radar emitter if just setEmissions is set to false\n\t\t\t\tlocal controller = self:getController()\n\t\t\t\tcontroller:setOnOff(false)\n\t\t\tend\n\t\tend\n\t\tself.aiState = false\n\t\tself:stopScanningForHARMs()\n\t\tself.cachedTargets = {}\n\t\tif self.iads:getDebugSettings().radarWentDark then\n\t\t\tself.iads:printOutputToLog(\"GOING DARK: \"..self:getDescription())\n\t\tend\n\tend\nend\n\nfunction SkynetIADSAbstractRadarElement:pointDefencesGoLive()\n\tlocal setActive = false\n\tfor i = 1, #self.pointDefences do\n\t\tlocal pointDefence = self.pointDefences[i]\n\t\tif ( pointDefence:getActAsEW() == false ) then\n\t\t\tsetActive = true\n\t\t\tpointDefence:setActAsEW(true)\n\t\tend\n\tend\n\treturn setActive\nend\n\nfunction SkynetIADSAbstractRadarElement:isActive()\n\treturn self.aiState\nend\n\nfunction SkynetIADSAbstractRadarElement:isTargetInRange(target)\n\n\tlocal isSearchRadarInRange = false\n\tlocal isTrackingRadarInRange = false\n\tlocal isLauncherInRange = false\n\t\n\tlocal isSearchRadarInRange = ( #self.searchRadars == 0 )\n\tfor i = 1, #self.searchRadars do\n\t\tlocal searchRadar = self.searchRadars[i]\n\t\tif searchRadar:isInRange(target) then\n\t\t\tisSearchRadarInRange = true\n\t\t\tbreak\n\t\tend\n\tend\n\t\n\tif self.goLiveRange == SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE then\n\t\t\n\t\tisLauncherInRange = ( #self.launchers == 0 )\n\t\tfor i = 1, #self.launchers do\n\t\t\tlocal launcher = self.launchers[i]\n\t\t\tif launcher:isInRange(target) then\n\t\t\t\tisLauncherInRange = true\n\t\t\t\tbreak\n\t\t\tend\n\t\tend\n\t\t\n\t\tisTrackingRadarInRange = ( #self.trackingRadars == 0 )\n\t\tfor i = 1, #self.trackingRadars do\n\t\t\tlocal trackingRadar = self.trackingRadars[i]\n\t\t\tif trackingRadar:isInRange(target) then\n\t\t\t\tisTrackingRadarInRange = true\n\t\t\t\tbreak\n\t\t\tend\n\t\tend\n\telse\n\t\tisLauncherInRange = true\n\t\tisTrackingRadarInRange = true\n\tend\n\treturn  (isSearchRadarInRange and isTrackingRadarInRange and isLauncherInRange )\nend\n\nfunction SkynetIADSAbstractRadarElement:isInRadarDetectionRangeOf(abstractRadarElement)\n\tlocal radars = self:getRadars()\n\tlocal abstractRadarElementRadars = abstractRadarElement:getRadars()\n\tfor i = 1, #radars do\n\t\tlocal radar = radars[i]\n\t\tfor j = 1, #abstractRadarElementRadars do\n\t\t\tlocal abstractRadarElementRadar = abstractRadarElementRadars[j]\n\t\t\tif  abstractRadarElementRadar:isExist() and radar:isExist() then\n\t\t\t\tlocal distance = self:getDistanceToUnit(radar:getDCSRepresentation():getPosition().p, abstractRadarElementRadar:getDCSRepresentation():getPosition().p)\t\n\t\t\t\tif abstractRadarElementRadar:getMaxRangeFindingTarget() >= distance then\n\t\t\t\t\treturn true\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\tend\n\treturn false\nend\n\nfunction SkynetIADSAbstractRadarElement:getDistanceToUnit(unitPosA, unitPosB)\n\treturn mist.utils.round(mist.utils.get2DDist(unitPosA, unitPosB, 0))\nend\n\nfunction SkynetIADSAbstractRadarElement:hasWorkingRadar()\n\tlocal radars = self:getRadars()\n\tfor i = 1, #radars do\n\t\tlocal radar = radars[i]\n\t\tif radar:isRadarWorking() == true then\n\t\t\treturn true\n\t\tend\n\tend\n\treturn false\nend\n\nfunction SkynetIADSAbstractRadarElement:jam(successProbability)\n\t\tif self:isDestroyed() == false then\n\t\t\tlocal controller = self:getController()\n\t\t\tlocal probability = math.random(1, 100)\n\t\t\tif self.iads:getDebugSettings().jammerProbability then\n\t\t\t\tself.iads:printOutputToLog(\"JAMMER: \"..self:getDescription()..\": Probability: \"..successProbability)\n\t\t\tend\n\t\t\tif successProbability > probability then\n\t\t\t\tcontroller:setOption(AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD)\n\t\t\t\tif self.iads:getDebugSettings().jammerProbability then\n\t\t\t\t\tself.iads:printOutputToLog(\"JAMMER: \"..self:getDescription()..\": jammed, setting to weapon hold\")\n\t\t\t\tend\n\t\t\telse\n\t\t\t\tcontroller:setOption(AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_FREE)\n\t\t\t\tif self.iads:getDebugSettings().jammerProbability then\n\t\t\t\t\tself.iads:printOutputToLog(\"JAMMER: \"..self:getDescription()..\": jammed, setting to weapon free\")\n\t\t\t\tend\n\t\t\tend\n\t\t\tself.lastJammerUpdate = timer:getTime()\n\t\tend\nend\n\nfunction SkynetIADSAbstractRadarElement:scanForHarms()\n\tself:stopScanningForHARMs()\n\tself.harmScanID = mist.scheduleFunction(SkynetIADSAbstractRadarElement.evaluateIfTargetsContainHARMs, {self}, 1, 2)\nend\n\nfunction SkynetIADSAbstractRadarElement:isScanningForHARMs()\n\treturn self.harmScanID ~= nil\nend\n\nfunction SkynetIADSAbstractRadarElement:isDefendingHARM()\n\treturn self.harmSilenceID ~= nil\nend\n\nfunction SkynetIADSAbstractRadarElement:stopScanningForHARMs()\n\tmist.removeFunction(self.harmScanID)\n\tself.harmScanID = nil\nend\n\nfunction SkynetIADSAbstractRadarElement:goSilentToEvadeHARM(timeToImpact)\n\tself:finishHarmDefence(self)\n\tif ( timeToImpact == nil ) then\n\t\ttimeToImpact = 0\n\tend\n\t\n\tself.minHarmShutdownTime = self:calculateMinimalShutdownTimeInSeconds(timeToImpact)\n\tself.maxHarmShutDownTime = self:calculateMaximalShutdownTimeInSeconds(self.minHarmShutdownTime)\n\t\n\tself.harmShutdownTime = self:calculateHARMShutdownTime()\n\tif self.iads:getDebugSettings().harmDefence then\n\t\tself.iads:printOutputToLog(\"HARM DEFENCE SHUTTING DOWN: \"..self:getDCSName()..\" | FOR: \"..self.harmShutdownTime..\" seconds | TTI: \"..timeToImpact)\n\tend\n\tself.harmSilenceID = mist.scheduleFunction(SkynetIADSAbstractRadarElement.finishHarmDefence, {self}, timer.getTime() + self.harmShutdownTime, 1)\n\tself:goDark()\nend\n\nfunction SkynetIADSAbstractRadarElement:getHARMShutdownTime()\n\treturn self.harmShutdownTime\nend\n\nfunction SkynetIADSAbstractRadarElement:calculateHARMShutdownTime()\n\tlocal shutDownTime = math.random(self.minHarmShutdownTime, self.maxHarmShutDownTime)\n\treturn shutDownTime\nend\n\nfunction SkynetIADSAbstractRadarElement.finishHarmDefence(self)\n\tmist.removeFunction(self.harmSilenceID)\n\tself.harmSilenceID = nil\n\tself.harmShutdownTime = 0\n\t\n\tif ( self:getAutonomousState() == true ) then\n\t\tself:goAutonomous()\n\tend\t\nend\n\nfunction SkynetIADSAbstractRadarElement:getDetectedTargets()\n\tif ( timer.getTime() - self.cachedTargetsCurrentAge > self.cachedTargetsMaxAge ) or ( timer.getTime() - self.goLiveTime < self.noCacheActiveForSecondsAfterGoLive ) then\n\t\tself.cachedTargets = {}\n\t\tself.cachedTargetsCurrentAge = timer.getTime()\n\t\tif self:hasWorkingPowerSource() and self:isDestroyed() == false then\n\t\t\tlocal targets = self:getController():getDetectedTargets(Controller.Detection.RADAR)\n\t\t\tfor i = 1, #targets do\n\t\t\t\tlocal target = targets[i]\n\t\t\t\t-- there are cases when a destroyed object is still visible as a target to the radar, don't add it, will cause errors everywhere the dcs object is accessed\n\t\t\t\tif target.object then\n\t\t\t\t\tlocal iadsTarget = SkynetIADSContact:create(target, self)\n\t\t\t\t\tiadsTarget:refresh()\n\t\t\t\t\tif self:isTargetInRange(iadsTarget) then\n\t\t\t\t\t\ttable.insert(self.cachedTargets, iadsTarget)\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\tend\n\treturn self.cachedTargets\nend\n\nfunction SkynetIADSAbstractRadarElement:getSecondsToImpact(distanceNM, speedKT)\n\tlocal tti = 0\n\tif speedKT > 0 then\n\t\ttti = mist.utils.round((distanceNM / speedKT) * 3600, 0)\n\t\tif tti < 0 then\n\t\t\ttti = 0\n\t\tend\n\tend\n\treturn tti\nend\n\nfunction SkynetIADSAbstractRadarElement:getDistanceInMetersToContact(radarUnit, point)\n\treturn mist.utils.round(mist.utils.get3DDist(radarUnit:getPosition().p, point), 0)\nend\n\nfunction SkynetIADSAbstractRadarElement:calculateMinimalShutdownTimeInSeconds(timeToImpact)\n\treturn timeToImpact + self.minHarmPresetShutdownTime\nend\n\nfunction SkynetIADSAbstractRadarElement:calculateMaximalShutdownTimeInSeconds(minShutdownTime)\t\n\treturn minShutdownTime + mist.random(1, self.maxHarmPresetShutdownTime)\nend\n\nfunction SkynetIADSAbstractRadarElement:calculateImpactPoint(target, distanceInMeters)\n\t-- distance needs to be incremented by a certain value for ip calculation to work, check why presumably due to rounding errors in the previous distance calculation\n\treturn land.getIP(target:getPosition().p, target:getPosition().x, distanceInMeters + 50)\nend\n\nfunction SkynetIADSAbstractRadarElement:shallReactToHARM()\n\treturn self.harmDetectionChance >=  math.random(1, 100)\nend\n\n-- will only check for missiles, if DCS ads AAA than can engage HARMs then this code must be updated:\nfunction SkynetIADSAbstractRadarElement:shallIgnoreHARMShutdown()\n\tlocal numOfHarms = self:getNumberOfObjectsItentifiedAsHARMS()\n\t--[[\n\tself.iads:printOutputToLog(\"Self enough launchers: \"..tostring(self:hasEnoughLaunchersToEngageMissiles(numOfHarms)))\n\tself.iads:printOutputToLog(\"Self enough missiles: \"..tostring(self:hasRemainingAmmoToEngageMissiles(numOfHarms)))\n\tself.iads:printOutputToLog(\"PD enough missiles: \"..tostring(self:pointDefencesHaveRemainingAmmo(numOfHarms)))\n\tself.iads:printOutputToLog(\"PD enough launchers: \"..tostring(self:pointDefencesHaveEnoughLaunchers(numOfHarms)))\n\t--]]\n\treturn ( ((self:hasEnoughLaunchersToEngageMissiles(numOfHarms) and self:hasRemainingAmmoToEngageMissiles(numOfHarms) and self:getCanEngageHARM()) or (self:pointDefencesHaveRemainingAmmo(numOfHarms) and self:pointDefencesHaveEnoughLaunchers(numOfHarms))))\nend\n\nfunction SkynetIADSAbstractRadarElement:informOfHARM(harmContact)\n\tlocal radars = self:getRadars()\n\t\tfor j = 1, #radars do\n\t\t\tlocal radar = radars[j]\n\t\t\tif radar:isExist() then\n\t\t\t\tlocal distanceNM =  mist.utils.metersToNM(self:getDistanceInMetersToContact(radar, harmContact:getPosition().p))\n\t\t\t\tlocal harmToSAMHeading = mist.utils.toDegree(mist.utils.getHeadingPoints(harmContact:getPosition().p, radar:getPosition().p))\n\t\t\t\tlocal harmToSAMAspect = self:calculateAspectInDegrees(harmContact:getMagneticHeading(), harmToSAMHeading)\n\t\t\t\tlocal speedKT = harmContact:getGroundSpeedInKnots(0)\n\t\t\t\tlocal secondsToImpact = self:getSecondsToImpact(distanceNM, speedKT)\n\t\t\t\t--TODO: use tti instead of distanceNM?\n\t\t\t\t-- when iterating through the radars, store shortest tti and work with that value??\n\t\t\t\tif ( harmToSAMAspect < SkynetIADSAbstractRadarElement.HARM_TO_SAM_ASPECT and distanceNM < SkynetIADSAbstractRadarElement.HARM_LOOKAHEAD_NM ) then\n\t\t\t\t\tself:addObjectIdentifiedAsHARM(harmContact)\n\t\t\t\t\tif ( #self:getPointDefences() > 0 and self:pointDefencesGoLive() == true and self.iads:getDebugSettings().harmDefence ) then\n\t\t\t\t\t\t\tself.iads:printOutputToLog(\"POINT DEFENCES GOING LIVE FOR: \"..self:getDCSName()..\" | TTI: \"..secondsToImpact)\n\t\t\t\t\tend\n\t\t\t\t\t--self.iads:printOutputToLog(\"Ignore HARM shutdown: \"..tostring(self:shallIgnoreHARMShutdown()))\n\t\t\t\t\tif ( self:getIsAPointDefence() == false and ( self:isDefendingHARM() == false or ( self:getHARMShutdownTime() < secondsToImpact ) ) and self:shallIgnoreHARMShutdown() == false) then\n\t\t\t\t\t\tself:goSilentToEvadeHARM(secondsToImpact)\n\t\t\t\t\t\tbreak\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\tend\nend\n\nfunction SkynetIADSAbstractElement:addObjectIdentifiedAsHARM(harmContact)\n\tself:insertToTableIfNotAlreadyAdded(self.objectsIdentifiedAsHarms, harmContact)\nend\n\nfunction SkynetIADSAbstractRadarElement:calculateAspectInDegrees(harmHeading, harmToSAMHeading)\n\t\tlocal aspect = harmHeading - harmToSAMHeading\n\t\tif ( aspect < 0 ) then\n\t\t\taspect = -1 * aspect\n\t\tend\n\t\tif aspect > 180 then\n\t\t\taspect = 360 - aspect\n\t\tend\n\t\treturn mist.utils.round(aspect)\nend\n\nfunction SkynetIADSAbstractRadarElement:getNumberOfObjectsItentifiedAsHARMS()\n\treturn #self.objectsIdentifiedAsHarms\nend\n\nfunction SkynetIADSAbstractRadarElement:cleanUpOldObjectsIdentifiedAsHARMS()\n\tlocal newHARMS = {}\n\tfor i = 1, #self.objectsIdentifiedAsHarms do\n\t\tlocal harmContact = self.objectsIdentifiedAsHarms[i]\n\t\tif harmContact:getAge() < self.objectsIdentifiedAsHarmsMaxTargetAge then\n\t\t\ttable.insert(newHARMS, harmContact)\n\t\tend\n\tend\n\t--stop point defences acting as ew (always on), will occur if activated via evaluateIfTargetsContainHARMs()\n\t--if in this iteration all harms where cleared we turn of the point defence. But in any other cases we dont turn of point defences, that interferes with other parts of the iads\n\t-- when setting up the iads (letting pds go to read state)\n\tif (#newHARMS == 0 and self:getNumberOfObjectsItentifiedAsHARMS() > 0 ) then\n\t\tself:pointDefencesStopActingAsEW()\n\tend\n\tself.objectsIdentifiedAsHarms = newHARMS\nend\n\n\nfunction SkynetIADSAbstractRadarElement.evaluateIfTargetsContainHARMs(self)\n\n\t--if an emitter dies the SAM site being jammed will revert back to normal operation:\n\tif self.lastJammerUpdate > 0 and ( timer:getTime() - self.lastJammerUpdate ) > 10 then\n\t\tself:jam(0)\n\t\tself.lastJammerUpdate = 0\n\tend\n\t\n\t--we use the regular interval of this method to update to other states: \n\tself:updateMissilesInFlight()\t\n\tself:cleanUpOldObjectsIdentifiedAsHARMS()\nend\n\nend\ndo\n--this class is currently used for AWACS and Ships, at a latter date a separate class for ships could be created, currently not needed\nSkynetIADSAWACSRadar = {}\nSkynetIADSAWACSRadar = inheritsFrom(SkynetIADSAbstractRadarElement)\n\nfunction SkynetIADSAWACSRadar:create(radarUnit, iads)\n\tlocal instance = self:superClass():create(radarUnit, iads)\n\tsetmetatable(instance, self)\n\tself.__index = self\n\tinstance.lastUpdatePosition = nil\n\tinstance.natoName = radarUnit:getTypeName()\n\treturn instance\nend\n\nfunction SkynetIADSAWACSRadar:setupElements()\n\tlocal unit = self:getDCSRepresentation()\n\tlocal radar = SkynetIADSSAMSearchRadar:create(unit)\n\tradar:setupRangeData()\n\ttable.insert(self.searchRadars, radar)\nend\n\n\n-- AWACs will not scan for HARMS\nfunction SkynetIADSAWACSRadar:scanForHarms()\n\t\nend\n\nfunction SkynetIADSAWACSRadar:getMaxAllowedMovementForAutonomousUpdateInNM()\n\t--local radarRange = mist.utils.metersToNM(self.searchRadars[1]:getMaxRangeFindingTarget())\n\t--return mist.utils.round(radarRange / 10)\n\t--fixed to 10 nm miles to better fit small SAM sites\n\treturn 10\nend\n\nfunction SkynetIADSAWACSRadar:isUpdateOfAutonomousStateOfSAMSitesRequired()\n\tlocal isUpdateRequired = self:getDistanceTraveledSinceLastUpdate() > self:getMaxAllowedMovementForAutonomousUpdateInNM()\n\tif isUpdateRequired then\n\t\tself.lastUpdatePosition = nil\n\tend\n\treturn isUpdateRequired\nend\n\nfunction SkynetIADSAWACSRadar:getDistanceTraveledSinceLastUpdate()\n\tlocal currentPosition = nil\n\tif self.lastUpdatePosition == nil and self:getDCSRepresentation():isExist() then\n\t\tself.lastUpdatePosition = self:getDCSRepresentation():getPosition().p\n\tend\n\tif self:getDCSRepresentation():isExist() then\n\t\tcurrentPosition = self:getDCSRepresentation():getPosition().p\n\tend\n\treturn mist.utils.round(mist.utils.metersToNM(self:getDistanceToUnit(self.lastUpdatePosition, currentPosition)))\nend\n\nend\n\ndo\nSkynetIADSCommandCenter = {}\nSkynetIADSCommandCenter = inheritsFrom(SkynetIADSAbstractRadarElement)\n\nfunction SkynetIADSCommandCenter:create(commandCenter, iads)\n\tlocal instance = self:superClass():create(commandCenter, iads)\n\tsetmetatable(instance, self)\n\tself.__index = self\n\tinstance.natoName = \"COMMAND CENTER\"\n\treturn instance\nend\n\nfunction SkynetIADSCommandCenter:goDark()\n\nend\n\nfunction SkynetIADSCommandCenter:goLive()\n\nend\n\nend\ndo\n\nSkynetIADSContact = {}\nSkynetIADSContact = inheritsFrom(SkynetIADSAbstractDCSObjectWrapper)\n\nSkynetIADSContact.CLIMB = \"CLIMB\"\nSkynetIADSContact.DESCEND = \"DESCEND\"\n\nSkynetIADSContact.HARM = \"HARM\"\nSkynetIADSContact.NOT_HARM = \"NOT_HARM\"\nSkynetIADSContact.HARM_UNKNOWN = \"HARM_UNKNOWN\"\n\nfunction SkynetIADSContact:create(dcsRadarTarget, abstractRadarElementDetected)\n\tlocal instance = self:superClass():create(dcsRadarTarget.object)\n\tsetmetatable(instance, self)\n\tself.__index = self\n\tinstance.abstractRadarElementsDetected = {}\n\ttable.insert(instance.abstractRadarElementsDetected, abstractRadarElementDetected)\n\tinstance.firstContactTime = timer.getAbsTime()\n\tinstance.lastTimeSeen = 0\n\tinstance.dcsRadarTarget = dcsRadarTarget\n\tinstance.position = instance:getDCSRepresentation():getPosition()\n\tinstance.numOfTimesRefreshed = 0\n\tinstance.speed = 0\n\tinstance.harmState = SkynetIADSContact.HARM_UNKNOWN\n\tinstance.simpleAltitudeProfile = {}\n\treturn instance\nend\n\nfunction SkynetIADSContact:setHARMState(state)\n\tself.harmState = state\nend\n\nfunction SkynetIADSContact:getHARMState()\n\treturn self.harmState\nend\n\nfunction SkynetIADSContact:isIdentifiedAsHARM()\n\treturn self.harmState == SkynetIADSContact.HARM\nend\n\nfunction SkynetIADSContact:isHARMStateUnknown()\n\treturn self.harmState == SkynetIADSContact.HARM_UNKNOWN\nend\n\nfunction SkynetIADSContact:getMagneticHeading()\n\tif ( self:isExist() ) then\n\t\treturn mist.utils.round(mist.utils.toDegree(mist.getHeading(self:getDCSRepresentation())))\n\telse\n\t\treturn -1\n\tend\nend\n\nfunction SkynetIADSContact:getAbstractRadarElementsDetected()\n\treturn self.abstractRadarElementsDetected\nend\n\nfunction SkynetIADSContact:addAbstractRadarElementDetected(radar)\n\tself:insertToTableIfNotAlreadyAdded(self.abstractRadarElementsDetected, radar)\nend\n\nfunction SkynetIADSContact:isTypeKnown()\n\treturn self.dcsRadarTarget.type\nend\n\nfunction SkynetIADSContact:isDistanceKnown()\n\treturn self.dcsRadarTarget.distance\nend\n\nfunction SkynetIADSContact:getTypeName()\n\tif self:isIdentifiedAsHARM() then\n\t\treturn SkynetIADSContact.HARM\n\tend\n\tif self:getDCSRepresentation() ~= nil then\n\t\tlocal category = self:getDCSRepresentation():getCategory()\n\t\tif category == Object.Category.UNIT then\n\t\t\treturn self.typeName\n\t\tend\n\tend\n\treturn \"UNKNOWN\"\nend\n\nfunction SkynetIADSContact:getPosition()\n\treturn self.position\nend\n\nfunction SkynetIADSContact:getGroundSpeedInKnots(decimals)\n\tif decimals == nil then\n\t\tdecimals = 2\n\tend\n\treturn mist.utils.round(self.speed, decimals)\nend\n\nfunction SkynetIADSContact:getHeightInFeetMSL()\n\tif self:isExist() then\n\t\treturn mist.utils.round(mist.utils.metersToFeet(self:getDCSRepresentation():getPosition().p.y), 0)\n\telse\n\t\treturn 0\n\tend\nend\n\nfunction SkynetIADSContact:getDesc()\n\tif self:isExist() then\n\t\treturn self:getDCSRepresentation():getDesc()\n\telse\n\t\treturn {}\n\tend\nend\n\nfunction SkynetIADSContact:getNumberOfTimesHitByRadar()\n\treturn self.numOfTimesRefreshed\nend\n\nfunction SkynetIADSContact:refresh()\n\tif self:isExist() then\n\t\tlocal timeDelta = (timer.getAbsTime() - self.lastTimeSeen)\n\t\tif timeDelta > 0 then\n\t\t\tself.numOfTimesRefreshed = self.numOfTimesRefreshed + 1\n\t\t\tlocal distance = mist.utils.metersToNM(mist.utils.get2DDist(self.position.p, self:getDCSRepresentation():getPosition().p))\n\t\t\tlocal hours = timeDelta / 3600\n\t\t\tself.speed = (distance / hours)\n\t\t\tself:updateSimpleAltitudeProfile()\n\t\t\tself.position = self:getDCSRepresentation():getPosition()\n\t\tend \n\tend\n\tself.lastTimeSeen = timer.getAbsTime()\nend\n\nfunction SkynetIADSContact:updateSimpleAltitudeProfile()\n\tlocal currentAltitude = self:getDCSRepresentation():getPosition().p.y\n\t\n\tlocal previousPath = \"\"\n\tif #self.simpleAltitudeProfile > 0 then\n\t\tpreviousPath = self.simpleAltitudeProfile[#self.simpleAltitudeProfile]\n\tend\n\t\n\tif self.position.p.y > currentAltitude and previousPath ~= SkynetIADSContact.DESCEND then\n\t\ttable.insert(self.simpleAltitudeProfile, SkynetIADSContact.DESCEND)\n\telseif self.position.p.y < currentAltitude and previousPath ~= SkynetIADSContact.CLIMB then\n\t\ttable.insert(self.simpleAltitudeProfile, SkynetIADSContact.CLIMB)\n\tend\nend\n\nfunction SkynetIADSContact:getSimpleAltitudeProfile()\n\treturn self.simpleAltitudeProfile\nend\n\nfunction SkynetIADSContact:getAge()\n\treturn mist.utils.round(timer.getAbsTime() - self.lastTimeSeen)\nend\n\nend\n\ndo\n\nSkynetIADSEWRadar = {}\nSkynetIADSEWRadar = inheritsFrom(SkynetIADSAbstractRadarElement)\n\nfunction SkynetIADSEWRadar:create(radarUnit, iads)\n\tlocal instance = self:superClass():create(radarUnit, iads)\n\tsetmetatable(instance, self)\n\tself.__index = self\n\tinstance.autonomousBehaviour = SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK\n\treturn instance\nend\n\nfunction SkynetIADSEWRadar:setupElements()\n\tlocal unit = self:getDCSRepresentation()\n\tlocal unitType = unit:getTypeName()\n\tfor typeName, dataType in pairs(SkynetIADS.database) do\n\t\tfor entry, unitData in pairs(dataType) do\n\t\t\tif entry == 'searchRadar' then\n\t\t\t\t--buildSingleUnit checks to make sure the EW radar is defined in the Skynet database. If it is not, self.searchRadars will be 0 so no ew radar will be added\n\t\t\t\tself:buildSingleUnit(unit, SkynetIADSSAMSearchRadar, self.searchRadars, unitData)\n\t\t\t\tif #self.searchRadars > 0 then\n\t\t\t\t\tlocal harmDetection = dataType['harm_detection_chance']\n\t\t\t\t\tself:setHARMDetectionChance(harmDetection)\n\t\t\t\t\tif unitData[unitType]['name'] then\n\t\t\t\t\t\tlocal natoName = unitData[unitType]['name']['NATO']\n\t\t\t\t\t\tself:buildNatoName(natoName)\n\t\t\t\t\tend\n\t\t\t\t\treturn\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\tend\nend\n\n--an Early Warning Radar has simplified check to determine if its autonomous or not\nfunction SkynetIADSEWRadar:setToCorrectAutonomousState()\n\tif self:hasActiveConnectionNode() and self:hasWorkingPowerSource() and self.iads:isCommandCenterUsable() then\n\t\tself:resetAutonomousState()\n\t\tself:goLive()\n\tend\n\tif self:hasActiveConnectionNode() == false or self.iads:isCommandCenterUsable() == false then\n\t\tself:goAutonomous()\n\tend\nend\n\nend\ndo\n\nSkynetIADSJammer = {}\nSkynetIADSJammer.__index = SkynetIADSJammer\n\nfunction SkynetIADSJammer:create(emitter, iads)\n\tlocal jammer = {}\n\tsetmetatable(jammer, SkynetIADSJammer)\n\tjammer.radioMenu = nil\n\tjammer.emitter = emitter\n\tjammer.jammerTaskID = nil\n\tjammer.iads = {iads}\n\tjammer.maximumEffectiveDistanceNM = 200\n\t--jammer probability settings are stored here, visualisation, see: https://docs.google.com/spreadsheets/d/16rnaU49ZpOczPEsdGJ6nfD0SLPxYLEYKmmo4i2Vfoe0/edit#gid=0\n\tjammer.jammerTable = {\n\t\t['SA-2'] = {\n\t\t\t['function'] = function(distanceNauticalMiles) return ( 1.4 ^ distanceNauticalMiles ) + 90 end,\n\t\t\t['canjam'] = true,\n\t\t},\n\t\t['SA-3'] = {\n\t\t\t['function'] = function(distanceNauticalMiles) return ( 1.4 ^ distanceNauticalMiles ) + 80 end,\n\t\t\t['canjam'] = true,\n\t\t},\n\t\t['SA-6'] = {\n\t\t\t['function'] = function(distanceNauticalMiles) return ( 1.4 ^ distanceNauticalMiles ) + 23 end,\n\t\t\t['canjam'] = true,\n\t\t},\n\t\t['SA-8'] = {\n\t\t\t['function'] = function(distanceNauticalMiles) return ( 1.35 ^ distanceNauticalMiles ) + 30 end,\n\t\t\t['canjam'] = true,\n\t\t},\n\t\t['SA-10'] = {\n\t\t\t['function'] = function(distanceNauticalMiles) return ( 1.07 ^ (distanceNauticalMiles / 1.13) ) + 5 end,\n\t\t\t['canjam'] = true,\n\t\t},\n\t\t['SA-11'] = {\n\t\t\t['function'] = function(distanceNauticalMiles) return ( 1.25 ^ distanceNauticalMiles ) + 15 end,\n\t\t\t['canjam'] = true,\n\t\t},\n\t\t['SA-15'] = {\n\t\t\t['function'] = function(distanceNauticalMiles) return ( 1.15 ^ distanceNauticalMiles ) + 5 end,\n\t\t\t['canjam'] = true,\n\t\t},\n\t}\n\treturn jammer\nend\n\nfunction SkynetIADSJammer:masterArmOn()\n\tself:masterArmSafe()\n\tself.jammerTaskID = mist.scheduleFunction(SkynetIADSJammer.runCycle, {self}, 1, 10)\nend\n\nfunction SkynetIADSJammer:addFunction(natoName, jammerFunction)\n\tself.jammerTable[natoName] = {\n\t\t['function'] = jammerFunction,\n\t\t['canjam'] = true\n\t}\nend\n\nfunction SkynetIADSJammer:setMaximumEffectiveDistance(distance)\n\tself.maximumEffectiveDistanceNM = distance\nend\n\nfunction SkynetIADSJammer:disableFor(natoName)\n\tself.jammerTable[natoName]['canjam'] = false\nend\n\nfunction SkynetIADSJammer:isKnownRadarEmitter(natoName)\n\tlocal isActive = false\n\tfor unitName, unit in pairs(self.jammerTable) do\n\t\tif unitName == natoName and unit['canjam'] == true then\n\t\t\tisActive = true\n\t\tend\n\tend\n\treturn isActive\nend\n\nfunction SkynetIADSJammer:addIADS(iads)\n\ttable.insert(self.iads, iads)\nend\n\nfunction SkynetIADSJammer:getSuccessProbability(distanceNauticalMiles, natoName)\n\tlocal probability = 0\n\tlocal jammerSettings = self.jammerTable[natoName]\n\tif jammerSettings ~= nil then\n\t\tprobability = jammerSettings['function'](distanceNauticalMiles)\n\tend\n\treturn probability\nend\n\nfunction SkynetIADSJammer:getDistanceNMToRadarUnit(radarUnit)\n\treturn mist.utils.metersToNM(mist.utils.get3DDist(self.emitter:getPosition().p, radarUnit:getPosition().p))\nend\n\nfunction SkynetIADSJammer.runCycle(self)\n\n\tif self.emitter:isExist() == false then\n\t\tself:masterArmSafe()\n\t\treturn\n\tend\n\n\tfor i = 1, #self.iads do\n\t\tlocal iads = self.iads[i]\n\t\tlocal samSites = iads:getActiveSAMSites()\t\n\t\tfor j = 1, #samSites do\n\t\t\tlocal samSite = samSites[j]\n\t\t\tlocal radars = samSite:getRadars()\n\t\t\tlocal hasLOS = false\n\t\t\tlocal distance = 0\n\t\t\tlocal natoName = samSite:getNatoName()\n\t\t\tfor l = 1, #radars do\n\t\t\t\tlocal radar = radars[l]\n\t\t\t\tdistance = self:getDistanceNMToRadarUnit(radar)\n\t\t\t\t-- I try to emulate the system as it would work in real life, so a jammer can only jam a SAM site if has line of sight to at least one radar in the group\n\t\t\t\tif self:isKnownRadarEmitter(natoName) and self:hasLineOfSightToRadar(radar) and distance <= self.maximumEffectiveDistanceNM then\n\t\t\t\t\tif iads:getDebugSettings().jammerProbability then\n\t\t\t\t\t\tiads:printOutput(\"JAMMER: Distance: \"..distance)\n\t\t\t\t\tend\n\t\t\t\t\tsamSite:jam(self:getSuccessProbability(distance, natoName))\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\tend\nend\n\nfunction SkynetIADSJammer:hasLineOfSightToRadar(radar)\n\tlocal radarPos = radar:getPosition().p\n\t--lift the radar 30 meters off the ground, some 3d models are dug in to the ground, creating issues in calculating LOS\n\tradarPos.y = radarPos.y + 30\n\treturn land.isVisible(radarPos, self.emitter:getPosition().p) \nend\n\nfunction SkynetIADSJammer:masterArmSafe()\n\tmist.removeFunction(self.jammerTaskID)\nend\n\n--TODO: Remove Menu when emitter dies:\nfunction SkynetIADSJammer:addRadioMenu()\n\tself.radioMenu = missionCommands.addSubMenu('Jammer: '..self.emitter:getName())\n\tmissionCommands.addCommand('Master Arm On', self.radioMenu, SkynetIADSJammer.updateMasterArm, {self = self, option = 'masterArmOn'})\n\tmissionCommands.addCommand('Master Arm Safe', self.radioMenu, SkynetIADSJammer.updateMasterArm, {self = self, option = 'masterArmSafe'})\nend\n\nfunction SkynetIADSJammer.updateMasterArm(params)\n\tlocal option = params.option\n\tlocal self = params.self\n\tif option == 'masterArmOn' then\n\t\tself:masterArmOn()\n\telseif option == 'masterArmSafe' then\n\t\tself:masterArmSafe()\n\tend\nend\n\nfunction SkynetIADSJammer:removeRadioMenu()\n\tmissionCommands.removeItem(self.radioMenu)\nend\n\nend\ndo\n\nSkynetIADSSAMSearchRadar = {}\nSkynetIADSSAMSearchRadar = inheritsFrom(SkynetIADSAbstractDCSObjectWrapper)\n\nfunction SkynetIADSSAMSearchRadar:create(unit)\n\tlocal instance = self:superClass():create(unit)\n\tsetmetatable(instance, self)\n\tself.__index = self\n\tinstance.firingRangePercent = 100\n\tinstance.maximumRange = 0\n\tinstance.initialNumberOfMissiles = 0\n\tinstance.remainingNumberOfMissiles = 0\n\tinstance.initialNumberOfShells = 0\n\tinstance.remainingNumberOfShells = 0\n\tinstance.triedSensors = 0\n\treturn instance\nend\n\n--override in subclasses to match different datastructure of getSensors()\nfunction SkynetIADSSAMSearchRadar:setupRangeData()\n\tif self:isExist() then\n\t\tlocal data = self:getDCSRepresentation():getSensors()\n\t\tif data == nil then\n\t\t\t--this is to prevent infinite calls between launcher and search radar\n\t\t\tself.triedSensors = self.triedSensors + 1\n\t\t\t--the SA-13 does not have any sensor data, but is has launcher data, so we use the stuff from the launcher for the radar range.\n\t\t\tSkynetIADSSAMLauncher.setupRangeData(self)\n\t\t\treturn\n\t\tend\n\t\tfor i = 1, #data do\n\t\t\tlocal subEntries = data[i]\n\t\t\tfor j = 1, #subEntries do\n\t\t\t\tlocal sensorInformation = subEntries[j]\n\t\t\t\t-- some sam sites have  IR and passive EWR detection, we are just interested in the radar data\n\t\t\t\t-- investigate if upperHemisphere and headOn is ok, I guess it will work for most detection cases\n\t\t\t\tif sensorInformation.type == Unit.SensorType.RADAR and sensorInformation['detectionDistanceAir'] then\n\t\t\t\t\tlocal upperHemisphere = sensorInformation['detectionDistanceAir']['upperHemisphere']['headOn']\n\t\t\t\t\tlocal lowerHemisphere = sensorInformation['detectionDistanceAir']['lowerHemisphere']['headOn']\n\t\t\t\t\tself.maximumRange = upperHemisphere\n\t\t\t\t\tif lowerHemisphere > upperHemisphere then\n\t\t\t\t\t\tself.maximumRange = lowerHemisphere\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\tend\nend\n\nfunction SkynetIADSSAMSearchRadar:getMaxRangeFindingTarget()\n\treturn self.maximumRange\nend\n\nfunction SkynetIADSSAMSearchRadar:isRadarWorking()\n\t-- the ammo check is for the SA-13 which does not return any sensor data:\n\treturn (self:isExist() == true and ( self:getDCSRepresentation():getSensors() ~= nil or self:getDCSRepresentation():getAmmo() ~= nil ) )\nend\n\nfunction SkynetIADSSAMSearchRadar:setFiringRangePercent(percent)\n\tself.firingRangePercent = percent\nend\n\nfunction SkynetIADSSAMSearchRadar:getDistance(target)\n\treturn mist.utils.get2DDist(target:getPosition().p, self:getDCSRepresentation():getPosition().p)\nend\n\nfunction SkynetIADSSAMSearchRadar:getHeight(target)\n\tlocal radarElevation = self:getDCSRepresentation():getPosition().p.y\n\tlocal targetElevation = target:getPosition().p.y\n\treturn math.abs(targetElevation - radarElevation)\nend\n\nfunction SkynetIADSSAMSearchRadar:isInHorizontalRange(target)\n\treturn (self:getMaxRangeFindingTarget() / 100 * self.firingRangePercent) >= self:getDistance(target)\nend\n\nfunction SkynetIADSSAMSearchRadar:isInRange(target)\n\tif self:isExist() == false then\n\t\treturn false\n\tend\n\treturn self:isInHorizontalRange(target)\nend\n\nend\n\ndo\n\nSkynetIADSSamSite = {}\nSkynetIADSSamSite = inheritsFrom(SkynetIADSAbstractRadarElement)\n\nfunction SkynetIADSSamSite:create(samGroup, iads)\n\tlocal sam = self:superClass():create(samGroup, iads)\n\tsetmetatable(sam, self)\n\tself.__index = self\n\tsam.targetsInRange = false\n\tsam.goLiveConstraints = {}\n\treturn sam\nend\n\nfunction SkynetIADSSamSite:addGoLiveConstraint(constraintName, constraint)\n\tself.goLiveConstraints[constraintName] = constraint\nend\n\nfunction SkynetIADSAbstractRadarElement:areGoLiveConstraintsSatisfied(contact)\n\tfor constraintName, constraint in pairs(self.goLiveConstraints) do\n\t\tif ( constraint(contact) ~= true ) then\n\t\t\treturn false\n\t\tend\n\tend\n\treturn true\nend\n\nfunction SkynetIADSAbstractRadarElement:removeGoLiveConstraint(constraintName)\n\tlocal constraints = {}\n\tfor cName, constraint in pairs(self.goLiveConstraints) do\n\t\tif cName ~= constraintName then\n\t\t\tconstraints[cName] = constraint\n\t\tend\n\tend\n\tself.goLiveConstraints = constraints\nend\n\nfunction SkynetIADSAbstractRadarElement:getGoLiveConstraints()\n\treturn self.goLiveConstraints\nend\n\nfunction SkynetIADSSamSite:isDestroyed()\n\tlocal isDestroyed = true\n\tfor i = 1, #self.launchers do\n\t\tlocal launcher = self.launchers[i]\n\t\tif launcher:isExist() == true then\n\t\t\tisDestroyed = false\n\t\tend\n\tend\n\tlocal radars = self:getRadars()\n\tfor i = 1, #radars do\n\t\tlocal radar = radars[i]\n\t\tif radar:isExist() == true then\n\t\t\tisDestroyed = false\n\t\tend\n\tend\t\n\treturn isDestroyed\nend\n\nfunction SkynetIADSSamSite:targetCycleUpdateStart()\n\tself.targetsInRange = false\nend\n\nfunction SkynetIADSSamSite:targetCycleUpdateEnd()\n\tif self.targetsInRange == false and self.actAsEW == false and self:getAutonomousState() == false and self:getAutonomousBehaviour() == SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DCS_AI then\n\t\tself:goDark()\n\tend\nend\n\nfunction SkynetIADSSamSite:informOfContact(contact)\n\t-- we make sure isTargetInRange (expensive call) is only triggered if no previous calls to this method resulted in targets in range\n\tif ( self.targetsInRange == false and self:areGoLiveConstraintsSatisfied(contact) == true and self:isTargetInRange(contact) and ( contact:isIdentifiedAsHARM() == false or ( contact:isIdentifiedAsHARM() == true and self:getCanEngageHARM() == true ) ) ) then\n\t\tself:goLive()\n\t\tself.targetsInRange = true\n\tend\nend\n\nend\ndo\n\nSkynetIADSSAMTrackingRadar = {}\nSkynetIADSSAMTrackingRadar = inheritsFrom(SkynetIADSSAMSearchRadar)\n\nfunction SkynetIADSSAMTrackingRadar:create(unit)\n\tlocal instance = self:superClass():create(unit)\n\tsetmetatable(instance, self)\n\tself.__index = self\n\treturn instance\nend\n\nend\ndo\n\nSkynetIADSSAMLauncher = {}\nSkynetIADSSAMLauncher = inheritsFrom(SkynetIADSSAMSearchRadar)\n\nfunction SkynetIADSSAMLauncher:create(unit)\n\tlocal instance = self:superClass():create(unit)\n\tsetmetatable(instance, self)\n\tself.__index = self\n\tinstance.maximumFiringAltitude = 0\n\treturn instance\nend\n\nfunction SkynetIADSSAMLauncher:setupRangeData()\n\tself.remainingNumberOfMissiles = 0\n\tself.remainingNumberOfShells = 0\n\tif self:isExist() then\n\t\tlocal data = self:getDCSRepresentation():getAmmo()\n\t\tlocal initialNumberOfMissiles = 0\n\t\tlocal initialNumberOfShells = 0\n\t\t--data becomes nil, when all missiles are fired\n\t\tif data then\n\t\t\tfor i = 1, #data do\n\t\t\t\tlocal ammo = data[i]\t\t\n\t\t\t\t--we ignore checks on radar guidance types, since we are not interested in how exactly the missile is guided by the SAM site.\n\t\t\t\tif ammo.desc.category == Weapon.Category.MISSILE then\n\t\t\t\t\t--TODO: see what the difference is between Max and Min values, SA-3 has higher Min value than Max?, most likely it has to do with the box parameters supplied by launcher\n\t\t\t\t\t--to simplyfy we just use the larger value, sam sites need a few seconds of tracking time to fire, by that time contact has most likely closed in on the SAM site.\n\t\t\t\t\tlocal altMin = ammo.desc.rangeMaxAltMin\n\t\t\t\t\tlocal altMax = ammo.desc.rangeMaxAltMax\n\t\t\t\t\tself.maximumRange = altMin\n\t\t\t\t\tif altMin < altMax then\n\t\t\t\t\t\tself.maximumRange = altMax\n\t\t\t\t\tend\n\t\t\t\t\tself.maximumFiringAltitude = ammo.desc.altMax\n\t\t\t\t\tself.remainingNumberOfMissiles = self.remainingNumberOfMissiles + ammo.count\n\t\t\t\t\tinitialNumberOfMissiles = self.remainingNumberOfMissiles\n\t\t\t\tend\n\t\t\t\tif ammo.desc.category == Weapon.Category.SHELL then\n\t\t\t\t\tself.remainingNumberOfShells = self.remainingNumberOfShells + ammo.count\n\t\t\t\t\tinitialNumberOfShells = self.remainingNumberOfShells\n\t\t\t\tend\n\t\t\t\t--if no distance was detected we run the code for the search radar. This happens when all in one units are passed like the shilka\n\t\t\t\tif self.maximumRange == 0 then\n\t\t\t\t\t--this is to prevent infinite calls between launcher and search radar\n\t\t\t\t\tif self.triedSensors <= 2 then\n\t\t\t\t\t\tSkynetIADSSAMSearchRadar.setupRangeData(self)\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\t\t-- conditions here are because setupRangeData() is called multiple times in the code to update ammo status, we set initial values only the first time the method is called\n\t\t\tif self.initialNumberOfMissiles == 0 then\n\t\t\t\tself.initialNumberOfMissiles = initialNumberOfMissiles\n\t\t\tend\n\t\t\tif self.initialNumberOfShells == 0 then\n\t\t\t\tself.initialNumberOfShells = initialNumberOfShells\n\t\t\tend\n\t\tend\n\tend\nend\n\nfunction SkynetIADSSAMLauncher:getInitialNumberOfShells()\n\treturn self.initialNumberOfShells\nend\n\nfunction SkynetIADSSAMLauncher:getRemainingNumberOfShells()\n\tself:setupRangeData()\n\treturn self.remainingNumberOfShells\nend\n\nfunction SkynetIADSSAMLauncher:getInitialNumberOfMissiles()\n\treturn self.initialNumberOfMissiles\nend\n\nfunction SkynetIADSSAMLauncher:getRemainingNumberOfMissiles()\n\tself:setupRangeData()\n\treturn self.remainingNumberOfMissiles\nend\n\nfunction SkynetIADSSAMLauncher:getRange()\n\treturn self.maximumRange\nend\n\nfunction SkynetIADSSAMLauncher:getMaximumFiringAltitude()\n\treturn self.maximumFiringAltitude\nend\n\nfunction SkynetIADSSAMLauncher:isWithinFiringHeight(target)\n\t-- if no max firing height is set (radar quided AAA) then we use the vertical range, bit of a hack but probably ok for AAA\n\tif self:getMaximumFiringAltitude() > 0 then\n\t\treturn self:getMaximumFiringAltitude() >= self:getHeight(target) \n\telse\n\t\treturn self:getRange() >= self:getHeight(target)\n\tend\nend\n\nfunction SkynetIADSSAMLauncher:isInRange(target)\n\tif self:isExist() == false then\n\t\treturn false\n\tend\n\treturn self:isWithinFiringHeight(target) and self:isInHorizontalRange(target)\nend\n\nend\n\n--[[\nSA-2 Launcher:\n    {\n        count=1,\n        desc={\n            Nmax=17,\n            RCS=0.39669999480247,\n            _origin=\"\",\n            altMax=25000,\n            altMin=100,\n            box={\n                max={x=4.7303376197815, y=0.84564626216888, z=0.84564626216888},\n                min={x=-5.8387970924377, y=-0.84564626216888, z=-0.84564626216888}\n            },\n            category=1,\n            displayName=\"SA2V755\",\n            fuseDist=20,\n            guidance=4,\n            life=2,\n            missileCategory=2,\n            rangeMaxAltMax=30000,\n            rangeMaxAltMin=40000,\n            rangeMin=7000,\n            typeName=\"SA2V755\",\n            warhead={caliber=500, explosiveMass=196, mass=196, type=1}\n        }\n    }\n}\n--]]\ndo\n\nSkynetIADSHARMDetection = {}\nSkynetIADSHARMDetection.__index = SkynetIADSHARMDetection\n\nSkynetIADSHARMDetection.HARM_THRESHOLD_SPEED_KTS = 800\n\nfunction SkynetIADSHARMDetection:create(iads)\n\tlocal harmDetection = {}\n\tsetmetatable(harmDetection, self)\n\tharmDetection.contacts = {}\n\tharmDetection.iads = iads\n\tharmDetection.contactRadarsEvaluated = {}\n\treturn harmDetection\nend\n\nfunction SkynetIADSHARMDetection:setContacts(contacts)\n\tself.contacts = contacts\nend\n\nfunction SkynetIADSHARMDetection:evaluateContacts()\n\tself:cleanAgedContacts()\n\tfor i = 1, #self.contacts do\n\t\tlocal contact = self.contacts[i]\t\n\t\tlocal groundSpeed  = contact:getGroundSpeedInKnots(0)\n\t\t--if a contact has only been hit by a radar once it's speed is 0\n\t\tif groundSpeed == 0 then\n\t\t\treturn\n\t\tend\n\t\tlocal simpleAltitudeProfile = contact:getSimpleAltitudeProfile()\n\t\tlocal newRadarsToEvaluate = self:getNewRadarsThatHaveDetectedContact(contact)\n\t\t--self.iads:printOutputToLog(contact:getName()..\" new Radars to evaluate: \"..#newRadarsToEvaluate)\n\t\t--self.iads:printOutputToLog(contact:getName()..\" ground speed: \"..groundSpeed)\n\t\tif ( #newRadarsToEvaluate > 0 and contact:isIdentifiedAsHARM() == false and ( groundSpeed > SkynetIADSHARMDetection.HARM_THRESHOLD_SPEED_KTS and #simpleAltitudeProfile <= 2 ) ) then\n\t\t\tlocal detectionProbability = self:getDetectionProbability(newRadarsToEvaluate)\n\t\t\t--self.iads:printOutputToLog(\"DETECTION PROB: \"..detectionProbability)\n\t\t\tif ( self:shallReactToHARM(detectionProbability) ) then\n\t\t\t\tcontact:setHARMState(SkynetIADSContact.HARM)\n\t\t\t\tif (self.iads:getDebugSettings().harmDefence ) then\n\t\t\t\t\tself.iads:printOutputToLog(\"HARM IDENTIFIED: \"..contact:getTypeName()..\" | DETECTION PROBABILITY WAS: \"..detectionProbability..\"%\")\n\t\t\t\tend\n\t\t\telse\n\t\t\t\tcontact:setHARMState(SkynetIADSContact.NOT_HARM)\n\t\t\t\tif (self.iads:getDebugSettings().harmDefence ) then\n\t\t\t\t\tself.iads:printOutputToLog(\"HARM NOT IDENTIFIED: \"..contact:getTypeName()..\" | DETECTION PROBABILITY WAS: \"..detectionProbability..\"%\")\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\t\t\n\t\tif ( #simpleAltitudeProfile > 2 and contact:isIdentifiedAsHARM() ) then\n\t\t\tcontact:setHARMState(SkynetIADSContact.HARM_UNKNOWN)\n\t\t\tif (self.iads:getDebugSettings().harmDefence ) then\n\t\t\t\tself.iads:printOutputToLog(\"CORRECTING HARM STATE: CONTACT IS NOT A HARM: \"..contact:getName())\n\t\t\tend\n\t\tend\n\t\t\n\t\tif ( contact:isIdentifiedAsHARM() ) then\n\t\t\tself:informRadarsOfHARM(contact)\n\t\tend\n\tend\nend\n\nfunction SkynetIADSHARMDetection:cleanAgedContacts()\n\tlocal activeContactRadars = {}\n\tfor contact, radars in pairs (self.contactRadarsEvaluated) do\n\t\tif contact:getAge() < 32 then\n\t\t\tactiveContactRadars[contact] = radars\n\t\tend\n\tend\n\tself.contactRadarsEvaluated = activeContactRadars\nend\n\nfunction SkynetIADSHARMDetection:getNewRadarsThatHaveDetectedContact(contact)\n\tlocal radarsFromContact = contact:getAbstractRadarElementsDetected()\n\tlocal evaluatedRadars = self.contactRadarsEvaluated[contact]\n\tlocal newRadars = {}\n\tif evaluatedRadars == nil then\n\t\tevaluatedRadars = {}\n\t\tself.contactRadarsEvaluated[contact] = evaluatedRadars\n\tend\n\tfor i = 1, #radarsFromContact do\n\t\tlocal contactRadar = radarsFromContact[i]\n\t\tif self:isElementInTable(evaluatedRadars, contactRadar) == false then\n\t\t\ttable.insert(evaluatedRadars, contactRadar)\n\t\t\ttable.insert(newRadars, contactRadar)\n\t\tend\n\tend\n\treturn newRadars\nend\n\nfunction SkynetIADSHARMDetection:isElementInTable(tbl, element)\n\tfor i = 1, #tbl do\n\t\tlocal tblElement = tbl[i]\n\t\tif tblElement == element then\n\t\t\treturn true\n\t\tend\n\tend\n\treturn false\nend\n\nfunction SkynetIADSHARMDetection:informRadarsOfHARM(contact)\n\tlocal samSites = self.iads:getUsableSAMSites()\n\tself:updateRadarsOfSites(samSites, contact)\n\t\n\tlocal ewRadars = self.iads:getUsableEarlyWarningRadars()\n\tself:updateRadarsOfSites(ewRadars, contact)\nend\n\nfunction SkynetIADSHARMDetection:updateRadarsOfSites(sites, contact)\n\tfor i = 1, #sites do\n\t\tlocal site = sites[i]\n\t\tsite:informOfHARM(contact)\n\tend\nend\n\nfunction SkynetIADSHARMDetection:shallReactToHARM(chance)\n\treturn chance >=  math.random(1, 100)\nend\n\nfunction SkynetIADSHARMDetection:getDetectionProbability(radars)\n\tlocal detectionChance = 0\n\tlocal missChance = 100\n\tlocal detection = 0\n\tfor i = 1, #radars do\n\t\tdetection = radars[i]:getHARMDetectionChance()\n\t\tif ( detectionChance == 0 ) then\n\t\t\tdetectionChance = detection\n\t\telse\n\t\t\tdetectionChance = detectionChance + (detection * (missChance / 100))\n\t\tend\t\n\t\tmissChance = 100 - detection\n\tend\n\treturn detectionChance\nend\n\nend\n\n\n"
  },
  {
    "path": "demo-missions/skynet-iads-setup-persian-gulf.lua",
    "content": "do\n--create an instance of the IADS\nredIADS = SkynetIADS:create('IRAN')\n\n---debug settings remove from here on if you do not wan't any output on what the IADS is doing by default\nlocal iadsDebug = redIADS:getDebugSettings()\niadsDebug.IADSStatus = true\niadsDebug.radarWentDark = true\niadsDebug.contacts = true\niadsDebug.radarWentLive = true\niadsDebug.noWorkingCommmandCenter = false\niadsDebug.ewRadarNoConnection = false\niadsDebug.samNoConnection = false\niadsDebug.jammerProbability = true\niadsDebug.addedEWRadar = false\niadsDebug.hasNoPower = false\niadsDebug.harmDefence = true\niadsDebug.samSiteStatusEnvOutput = true\niadsDebug.earlyWarningRadarStatusEnvOutput = true\niadsDebug.commandCenterStatusEnvOutput = true\n---end remove debug ---\n\n--add all units with unit name beginning with 'EW' to the IADS:\nredIADS:addEarlyWarningRadarsByPrefix('EW')\n\n--add all groups begining with group name 'SAM' to the IADS:\nredIADS:addSAMSitesByPrefix('SAM')\n\n--add a command center:\ncommandCenter = StaticObject.getByName('Command-Center')\nredIADS:addCommandCenter(commandCenter)\n\n---we add a K-50 AWACs, manually. This could just as well be automated by adding an 'EW' prefix to the unit name:\nredIADS:addEarlyWarningRadar('AWACS-K-50')\n\n--add a power source and a connection node for this EW radar:\nlocal powerSource = StaticObject.getByName('Power-Source-EW-Center3')\nlocal connectionNodeEW = StaticObject.getByName('Connection-Node-EW-Center3')\nredIADS:getEarlyWarningRadarByUnitName('EW-Center3'):addPowerSource(powerSource):addConnectionNode(connectionNodeEW)\n\n--add a connection node to this SA-2 site, and set the option for it to go dark, if it looses connection to the IADS:\nlocal connectionNode = Unit.getByName('Mobile-Command-Post-SAM-SA-2')\nredIADS:getSAMSiteByGroupName('SAM-SA-2'):addConnectionNode(connectionNode):setAutonomousBehaviour(SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK)\n\n--this SA-2 site will go live at 70% of its max search range:\nredIADS:getSAMSiteByGroupName('SAM-SA-2'):setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE):setGoLiveRangeInPercent(70)\n\n--all SA-10 sites shall act as EW sites, meaning their radars will be on all the time:\nredIADS:getSAMSitesByNatoName('SA-10'):setActAsEW(true)\n\n--set the sa15 as point defence for the SA-10 site, we set it to always react to a HARM so we can demonstrate the point defence mechanism in Skynet\nlocal sa15 = redIADS:getSAMSiteByGroupName('SAM-SA-15-point-defence-SA-10')\nredIADS:getSAMSiteByGroupName('SAM-SA-10'):addPointDefence(sa15):setHARMDetectionChance(100)\n\n\n--set this SA-11 site to go live 70% of max range of its missiles (default value: 100%), its HARM detection probability is set to 50% (default value: 70%)\nredIADS:getSAMSiteByGroupName('SAM-SA-11'):setGoLiveRangeInPercent(70):setHARMDetectionChance(50)\n\n--this SA-6 site will always react to a HARM being fired at it:\nredIADS:getSAMSiteByGroupName('SAM-SA-6'):setHARMDetectionChance(100)\n\n--set this SA-11 site to go live at maximunm search range (default is at maximung firing range):\nredIADS:getSAMSiteByGroupName('SAM-SA-11-2'):setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE)\n\n--activate the radio menu to toggle IADS Status output\nredIADS:addRadioMenu()\n\n-- activate the IADS\nredIADS:activate()\t\n\n--add the jammer\nlocal jammer = SkynetIADSJammer:create(Unit.getByName('jammer-emitter'), redIADS)\njammer:masterArmOn()\njammer:addRadioMenu()\n\n---some special code to remove the jammer aircraft if player is not flying with it in formation, has nothing to do with the IADS:\nlocal hornet = Unit.getByName('Hornet SA-11-2 Attack')\nif hornet == nil then\n\tUnit.getByName('jammer-emitter'):destroy()\n\tjammer:removeRadioMenu()\nend\n--end special code\n\n------setup blue IADS:\nblueIADS = SkynetIADS:create('UAE')\nblueIADS:addSAMSitesByPrefix('BLUE-SAM')\nblueIADS:addEarlyWarningRadarsByPrefix('BLUE-EW')\nblueIADS:activate()\nblueIADS:addRadioMenu()\n\nlocal iadsDebug = blueIADS:getDebugSettings()\niadsDebug.IADSStatus = true\niadsDebug.contacts = true\n\nend"
  },
  {
    "path": "skynet-iads-source/README_source.md",
    "content": "# Skynet-IADS\n![logo](/images/SA3_2.jpg)\n\nAn IADS (Integrated Air Defence System) script for DCS (Digital Combat Simulator).\n\n# Abstract\nThis script simulates an IADS within the scripting possibilities of DCS. Early Warning Radar Stations (EW Radar) scan the sky for contacts. These contacts are correlated with SAM (Surface to Air Missile) sites. If a contact is within firing range of the SAM site it will become active.\n\nA modern IADS also depends on command centers and datalinks to the SAM sites. The IADS can be set up with this infrastructure. Destroying it will degrade the capability of the IADS.\n\nThis all sounds gibberish to you? Watch [this video by Covert Cabal on modern IADS](https://www.youtube.com/watch?v=9J9kntzkSQY).\n\nVisit [this DCS forum thread](https://forums.eagle.ru/topic/226173-skynet-an-iads-for-mission-builders) for development updates.\n\nJoin the [Skynet discord group](https://discord.gg/pz8wcQs) and get support setting up your mission.\n\nSkynet supports the [HighDigitSAMs Mod](https://github.com/Auranis/HighDigitSAMs).\n\nYou can also connect [Skynet with the AI_A2A_DISPATCHER](#how-do-i-connect-skynet-with-the-moose-ai_a2a_dispatcher-and-what-are-the-benefits-of-that) by MOOSE to add interceptors to the IADS.\n\n**So far over 200 hours of work went in to the development of Skynet.  \nIf you like using it, please consider a donation:**\n\n[![Skynet IADS donation](/images/btn_donateCC_LG.gif.png)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=7GSVFH448BWFQ&source=url)\n\n\n\n\n{TOC_PLACEHOLDER}\n\n# Quick start\nTired of reading already? Download the [demo mission](/demo-missions/skynet-test-persian-gulf.miz) in the persian gulf map and see Skynet in action. More complex demo missions will follow soon.\n\n# Skynet IADS Elements\n![Skynet IADS overview](/images/skynet-overview.jpg)\n\n## IADS\nA Skynet IADS is a complete operational network. You can have multiple Skynet IADS instances per coalition in a DCS mission. A simple setup would be one IADS for the blue side and one IADS for the red side.\n\n## Track files\nSkynet keeps a global track file of all detected targets. It queries all its units with radars and deduplicates contacts. By default lost contacts are stored up to 32 seconds in memory. \n\n## Comand Centers\nYou can add multiple command centers to a Skynet IADS. Once all command centers are destroyed the IADS will go in to autonomous mode.\n\n## SAM Sites\nSkynet can handle multiple SAM sites, it will try and keep emissions to a minimum, therefore by default SAM sites will be turned on only if a target is in range. \nEvery single launcher and radar unit's distance of a SAM site is analysed individually. \nIf at least one launcher and radar is within range, the SAM Site will become active. \nThis allows for a scattered placement of radar and launcher units as in real life.\n\nIf SAM sites or radar guided AAA run out of ammo they will go dark. In the case of a SAM site it will wait with going dark as long as the last fired missile is still in the air.\n\nIf an EW radar or a SAM site acting as EW radar is destoyed surrounding SAM sites can be left withouth EW radar coverage. This can also happen if a SAM site is outside of AWACS coverage.\nSAM sites will go autonomous in such a case meaning they will use their organic radars or just stay dark depending on setup.\nOnce a SAM site is within EW radar coverage again it will be updated by the IADS.\n\n## Early Warning Radars\nSkynet can handle 0-n EW radars. For detection of a target the DCS radar detection logic is used. You can use any type of radar listed in [skynet-iads-supported-types.lua](/skynet-iads-source/skynet-iads-supported-types.lua) in an EW role in Skynet. \nSome modern SAM radars have a greater detection range than older EW radars, e.g. the S-300PS 64H6E (160 km) vs EWR 55G6 (120 km).\n\nYou can also designate SAM sites to act as EW radars, in this case a SAM site will constantly have their radar on. Long range systems like the S-300 are used as EW radars in real life.\nSAM sites that are out of ammo will stay live if they are set to act as EW radars.\n\nNice to know:\nTerrain elevation around an EW radar will create blinds spots, allowing low and fast movers to penetrate radar networks through valleys.\n\n##  Power Sources\nBy default Skynet IADS will run without having to add power sources. You can add multiple power sources to SAM sites, EW radars and command centers.\nOnce a power source is fully damaged the Skynet IADS unit will stop working.\n\nNice to know:\nTaking out the power source of a command center is a real life tactic used in SEAD (Suppression of Enemy Air Defence).\n\n## Connection Nodes\nBy default Skynet IADS will run without having to add connection nodes. You can add multiple connection nodes to SAM sites, EW radars and command centers.\n\nWhen all the unit's connection nodes are fully damaged an EW radar or SAM site will go in to autonomous mode. For a SAM site this means it will behave in its autonomous mode setting. \nIf an EW Radar looses its node it will no longer contribute information to the IADS but otherwise the IADS will still work. Command centers do not have an autonomous mode.\n\nNice to know:\nA single node can be used to connect an arbitrary number of Skynet IADS units. This way you can add a single point of failure in to an IADS.\n\n## AWACS (Airborne Early Warning and Control System)\nAny aircraft with an air to air radar can be added as AWACS. Contacts detected will be added to the IADS. The AWACS will also detect ground units like ships.\nThese will however not be passed to the SAM sites.\n\nYou can add a connection node for the AWACS like an antenna, if it is destroyed, the AWACS will no longer be able to contribute contacts to the IADS.\nTechnically you can also add a power source. In this context it would represent the power source for the connection node, since an aircraft provides its own power.\n\n## Ships\nShips will contribute to the IADS the same way AWACS units do. Add them as a regular EW radar. \n\n# Tactics\n\n## HARM defence\nSAM sites and EW radars will shut down their radars if they believe a HARM (High speed anti radiation missile) is heading for them. For this to happen, the IADS will evaluate contacts and determine if they are likely to be HARMs.\nEach SAM site or EW radar has HARM detection chance set. If a HARM is detected by more than one radar, the chance of it being identified as a HARM is increased.  \nSee [skynet-iads-supported-types.lua](/skynet-iads-source/skynet-iads-supported-types.lua) field ```['harm_detection_chance']``` for the probability per radar system.\n\n### HARM detection\nlet's say SAM site A has a 60% HARM detection chance and SAM site B has a 50% HARM detection cance. If a HARM is picked up by both radars the chance the IADS will identify the HARM will be 80%.  \n\nWith the radar cross section updates of HARMs in DCS 2.7 older radars like the ones used in the SA-2 and SA-6 can only identifiy a HARM at very close range usualy less than 10 seconds before impact. These systems will not have a very good HARM defence with Skynet.\n\n![Skynet IADS overview](/images/skynet-harm-detection.jpg)\n\n### HARM flight path analysis\nThe contact needs to be traveling faster than 800 kt and it may not have changed its flight path more than 2 times (eg ```climb-descend```, ```climb``` or ```descend```).This is to minimise false positives, for example a fighter flying very fast.\n\n![Skynet IADS overview](/images/skynet-harm-flightpath.jpg)\n\nThis implementation is closer to real life. SAM sites like the patriot and most likely modern Russian systems calculate the flight path and analyse the radar cross section to determine if a contact heading inbound is a HARM.\n\nIf identified as a HARM the IADS will shut down radars 15 degrees left and right of the HARM's fight path up to a distance of 20 nautical miles in front of the HARM.\nThe IADS will calculate time to impact and shut down radar emitters up to a maximum of 180 seconds after time to impact. \n\n## HARM radar shutdown\nOnce a HARM has been identified by Skynet, radars up to 20 nm ahead and 15 degrees left or right of the HARM will be notified. Depending on their settins radar emitters will shut down or start defending against the HARM.\n\n![Skynet IADS overview](/images/skynet-harm-radar-shutdown.jpg)\n\n## Point defence\nWhen a radar emitter (EW radar or SAM site) is attacked by a HARM there is a chance it may detect the HARM and go dark. If this radar emitter is acting as the sole EW radar in the area, surrounding SAM sites will not be able to go live since they rely on the EW radar for target information. This is an issue if you have SA-15 Tors next to the EW radar for point defence protection. They will stay dark and not engange the HARM.\n\nUse this feature if you don't want the IADS to loose situational awareness just because a HARM is inbound. The radar emitter will shut down, if it believes its point defences won't be able to handle the number of HARMs inbound. As long as there is one point defence launcher and missile per HARM inbound the radar emitter will keep emitting. If the HARMs exeed the number of point defence launchers and missiles the protected asset will shut down. Tests in DCS have shown that this is roughly the saturation point. If the SAM site reling on point defence can engagen HARMs its launchers an missiles will also count to the saturation point.\n\nSee FAQ [Which SAM systems can engage HARMS?](#which-sam-systems-can-engage-harms)\n\n[Point defence setup example](#point-defence-1)\n\n## Electronic Warfare\nA simple form of jamming is part of the Skynet IADS package. It's off by default. The jamming works by setting the ROE state of a SAM Site. \nThe closer the jamming emitter gets to a SAM site the less effective jamming will become (burn through). For the jammer to work it will need LOS (line of sight) to a radar unit. \nOlder SAM sites are more susceptible to jamming. EW radars are currently not jammable.\n\nI recommend you add an AI unit that follows the strike package you're flying in to act as a jammer aircraft. This will give you the most realistic experience. \nThe jammer emitter will toggle the ROE state of a SAM site which affects how the SAM site reacts to all threats near or far.\n\nI presume an aircraft very close to a SAM site beeing jammed by a emitter very far away would most likely be detected.\nSo the farther away you are from the jammer source the more unrealistic your experience will be.\n\nHere is a [list of SAM sites currently supported by the jammer](https://docs.google.com/spreadsheets/d/16rnaU49ZpOczPEsdGJ6nfD0SLPxYLEYKmmo4i2Vfoe0/edit#gid=0) and the jammer's effectiveness on them. \nWhen setting up a jammer you can decide which SAM sites it is able to jam. For example you could design a mission in which the jammer is not able to jam a SA-6 but is able to jam a SA-2. \nThe jammer effectiveness is not based on any real world data I just read about the different types and made my own conclusions.\n\nHere is an old school documentary [showing the Prowler in action](https://www.youtube.com/watch?v=su44ZU7NcQU). They brief to turn on their jamming equipement at 60 nm from the target.\nI suppose that must have been the effective range of 70's jamming tech.\n\n# Using Skynet in the mission editor\nIt's quite simple to setup an IADS have a look at the demo missions in the [/demo-missions/](/demo-missions) folder.\n\n## Placing units\nThis tutorial assumes you are familiar on how to set up a SAM site in DCS. If not I suggest you watch [this video](https://www.youtube.com/watch?v=YZPh-JNf6Ww) by the Grim Reapers.\nPlace the IADS elements you wish to add on the map.\n\n![Mission Editor IADS Setup](/images/iads-setup.png)  \n\n## Preparing a SAM site\nThere may be only be **one type of SAM site per group**. More than one type of SAM site per group will result in Skynet no being able to properly controll the group. Also please refrain from from adding units to the SAM group that are not required for the SAM like trucks, tanks and soldiers.\nThe skill level you set on a SAM group is retained by Skynet. Make sure you name the **SAM site group** in a consistent manner with a prefix e.g. 'SAM-SA-2'.\n\n![Mission Editor add SAM site](/images/add-sam-site.png)  \n\n## Preparing an EW radar\nYou can use any type of radar as an EW radar. Make sure you **name the unit** in a consistent manner with a prefix, e.g. 'EW-center3'. Make sure you have only **one EW radar in a group** otherwise Skynet will not be able to control single EW radars.\n\n![Mission Editor EW radar](/images/ew-setup.png)  \n\n## Adding the Skynet code\nSkynet requires MIST. A version is provided in this repository or you can download the most current version [here](https://github.com/mrSkortch/MissionScriptingTools).\nMake sure you load MIST and the compiled skynet code in to a mission. The [skynet-iads-compiled.lua](/demo-missions/skynet-iads-compiled.lua) and [mist_4_5_107.lua](/demo-missions/mist_4_5_107.lua) files are located in the [/demo-missions/](/demo-missions) folder. \n\nI recommend you create a text file e.g. 'my-iads-setup.lua' and then add the code needed to get the IADS runing. When updating the setup remember to reload the file in the mission editor. Otherwise changes will not become effective.\nYou can also add the code directly in the mission editor, however that input field is quite small if you write more than a few lines of code.\n\n![Mission Editor IADS Setup](/images/load-scripts.png)  \n\n## Adding the Skynet IADS\nFor the IADS to work you need four lines of code.\n\ncreate an instance of the IADS, the name string is optional and will be displayed in status output:\n```lua\nredIADS = SkynetIADS:create('name')\n``` \n\n\nGive all SAM groups you want to add a common prefix in the mission editor eg: 'SAM-SA-10 west', then add this line of code:  \n```lua\nredIADS:addSAMSitesByPrefix('SAM')\n``` \n\n\nSame for the EW radars, name all units with a common prefix in the mission editor eg: 'EW-radar-south':  \n```lua\nredIADS:addEarlyWarningRadarsByPrefix('EW')\n``` \n\n\nActivate the IADS:  \n```lua\nredIADS:activate()\n```\n\n# Advanced setup\nThis is the danger zone. Call Kenny Loggins. Some experience with scripting is recommended.\nYou can handcraft your IADS with the following functions. If you refrence units that don't exist a message will be displayed when the mission loads.\nThe following examples use static objects for command centers, connection nodes and power sources, you can also use units instead.\n\n## IADS configuration\nCall this method to add or remove a radio menu to toggle the status output of the IADS. By default the radio menu option is not visible:\n```lua\nredIADS:addRadioMenu()  \n```\n```lua\nredIADS:removeRadioMenu()\n```\n\nIf you dereference the IADS remember to call ```deactivate()``` otherwise background tasks of the IADS will continue running, resulting in unexpected behaviour:\n```lua\nredIADS:deactivate()\n```\n\nSet the update interval in seconds of the IADS. This determines in what interval the IADS wil turn SAM sites of or on according to targets it has detected:\n```lua\nredIADS:setUpdateInterval(5)\n```\n\n## Adding a command center\nThe command center represents the place where information is collected and analysed. It if is destroyed the IADS disintegrates.\n\nAdd a command center like this:\n```lua\nlocal commandCenter = StaticObject.getByName(\"Command Center\")\nredIADS:addCommandCenter(commandCenter)\n```\n\n## Power sources and connection nodes\nYou can use units or static objects. Call the function multiple times to add more than one power source or connection node:\n\n```unit``` refers to a SAM site, or EW Radar you retrieved from the IADS, see [setting an option for Radar units](#setting-an-option).\n```lua\nlocal powerSource = StaticObject.getByName(\"EW Power Source\")  \nunit:addPowerSource(powerSource)\n```\n\n```lua\nlocal connectionNode = Unit.getByName(\"EW connection node\") \nunit:addConnectionNode(connectionNode)\n```\n\nFor command centers use:\n```lua\nlocal commandCenter = StaticObject.getByName(\"Command Center2\")\nlocal comPowerSource = StaticObject.getByName(\"Command Center2 Power Source\")\nredIADS:addCommandCenter(commandCenter):addPowerSource(comPowerSource)\n```\n\n## Warm up the SAM sites of an IADS\nThis function is deprecated and will be removed in a future release.\n\n```lua\nredIADS:setupSAMSitesAndThenActivate()\n```\n\n\n## Connecting Skynet to the MOOSE AI_A2A_DISPATCHER\nYou can connect Skynet with MOOSE's [AI_A2A_DISPATCHER](https://flightcontrol-master.github.io/MOOSE_DOCS/Documentation/AI.AI_A2A_Dispatcher.html). This allows the IADS not only to direct SAM sites but also to scramble fighters.\nSkynet will set the radars it can use on the SET_GROUP object of a dispatcher. Meaning that if a radar is lost in Skynet it will no longer be availabe to detect and scramble interceptors.\n\nAdd the object of type SET_GROUP to the iads like this (in this example ```DectionSetGroup```):\n```lua\nredIADS:addMooseSetGroup(DetectionSetGroup)\n```\n\n## SAM site configuration\n\n### Adding SAM sites\n\n#### Add multiple SAM sites\nAdds SAM sites with prefix in group name to the IADS. Previously added SAM sites are cleared:\n```lua\nredIADS:addSAMSitesByPrefix('SAM')\n```\n\n#### Add a SAM site manually\nYou can manually add a SAM site, must be a valid group name:\n```lua\nredIADS:addSAMSite('SA-6 Group2')\n```\n\n### Accessing SAM sites in the IADS\nThe following functions exist to access SAM sites added to the IADS. They all support daisy chaining options:\n\nReturns all SAM sites with the corresponding Nato name, see [skynet-iads-supported-types.lua](/skynet-iads-source/skynet-iads-supported-types.lua). For all units beginning with 'SA-': Don't add Nato code names (Guideline, Gainful), just write 'SA-2', 'SA-6':\n```lua\nredIADS:getSAMSitesByNatoName('SA-6')\n```\n\nReturns all SAM sites in the IADS:\n```lua\nredIADS:getSAMSites()\n```\n\nReturns a SAM site with the specified group name:\n```lua\nredIADS:getSAMSiteByGroupName('SAM-SA-6')\n```\n\nReturns a SAM site with the specified group name prefix. Let's say you have a bunch of SAM sites that all will share the same power source. \nGive these sites a special prefix in the group name, e.g.: ```'SAM-SECTOR-A'```. Once you have added the SAM sites you can access them via the prefix to set whatever options you want:\n\n```lua\nredIADS:getSAMSitesByPrefix('SAM-SECTOR-A')\n```\n\n### Act as EW radar\nWill set the SAM site to act as an EW radar. This will result in the SAM site always having its radar on. Contacts the SAM site sees are reported to the IADS. This option is recomended for long range systems like the S-300: \n```lua\nsamSite:setActAsEW(true)\n```\n\n### Engagement zone\nSet the distance at which a SAM site will switch on its radar:\n```lua\nsamSite:setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE)\n```\n\n#### Engagement zone options  \n\nSAM site will go live when target is within the red circle in the mission editor (default Skynet behaviour): \n```lua\nSkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE\n```\n\nSAM site will go live when target is within the yelow circle in the mission editor: \n```lua\nSkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE\n```\n\nThis option sets the range in relation to the zone you set in ``setEngagementZone`` for a SAM site to go live. Be careful not to set the value too low. Some SAM sites need up to 30 seconds until they can fire. \nDuring this time a target might have already left the engagement zone of SAM site. This option is intended for long range systems like the S-300. You can also set the range above 100 this will have the effect that the SAM site goes live earlier:\n\n```lua\nsamSite:setGoLiveRangeInPercent(90)\n```\n\n### Engage air weapons\nWill set the SAM site to engage air weapons, if it is able to do so in DCS. It is a wrapper for the [ENGAGE_AIR_WEAPONS](https://wiki.hoggitworld.com/view/DCS_option_engage_air_weapons) setting.\n\n```lua\nsamSite:setCanEngageAirWeapons(true)\n```\n\n### Engage HARM\nWill set the SAM site to engage HARMs, if it is able to do so in DCS. If set to false the SAM site will shut down if a HARM that has been identified by the IADS is inbound. SAM sites that can engage HARMS are set to true by default.\n\n```lua\nsamSite:setCanEngageHARM(true)\n```\n\n## Add go live constraints\nYou can include constraints wich must be satisfied for the SAM site to go live. Please note this only controls activation of the SAM site. \nThere is currently no way to tell a SAM site to only target a certain contact via the lua scripting engine in DCS. \n\nThe constraint must evaluate to true and the contact must be in range of the SAM site (handled by Skynet). \n\n### Use cases\nPlace a SAM site on an flight path that you suspect strike strike fighters will pass. Add a heading constraint to ensure that the SAM site will only go ive when fighters are on their way back from the target.  \n\nSet a SAM site to only go live if aircraft are in a certain altitude band.\n\nSAM site shall only go live once a strike package has destroyed a certain building or unit.  \n\nYou do not have to use the contact provided in the function to evaluate the constraint. You can make any assertion you want.\n\nCreate a function that will evaluate if the constraint is satisfied. The function will have access to the [contact](#contact) the SAM site is evaluating:\n```lua\n--SAM site will only go live if the contact is below 1000 feet.\nlocal function goLiveConstraint(contact)\n\treturn ( contact:getHeightInFeetMSL() < 1000 )\nend\n```\n\nAdd the function to the SAM site and give it a name. You can add as many constraints as you wish:\n```lua\nself.samSite:addGoLiveConstraint('ignore-low-flying-contacts', goLiveConstraint)\n```\n\nRemove constraint you no longer wish to use:\n```lua\nself.samSite:removeGoLiveConstraint('ignore-low-flying-contacts')\n```\n\nGet a table of all constraints:\n```lua\nself.samSite:getGoLiveConstraints()\n```\n\n## Contact\nYou can use the following methods to get information about a contact.\n\nWill return true if contact has been identified as a HARM by Skynet:\n```lua\ncontact:isIdentifiedAsHARM()\n```  \n\nWill return the height of a contact:\n```lua\ncontact:getHeightInFeetMSL()\n```  \n\nWill return the current magnetic heading of a contact. Note the heading is availble only after a contact has been tracked in more than one cycle by the IADS. Until that has happened heading will be 0:\n```lua\ncontact:getMagneticHeading()\n```  \n\nWill return the current ground speed of a contact. Note the speed is availble only after a contact has been tracked in more than one cycle by the IADS. Until that has happend speed will be 0:\n```lua\ncontact:getMagneticHeading()\n```  \n\nWill return the time in seconds a contact has been known to the IADS:\n```lua\ncontact:getAge()\n```  \n\nWill return the type as a ```Object.Category```:\n```lua\ncontact:getTypeName()\n```  \n\nWill return the unit name:\n```lua\ncontact:getName()\n```  \n\n\n## EW radar configuration\n\n### Adding EW radars\n\n#### Add multiple EW radars\nAdds EW radars with prefix in unit name to the IADS. Previously added EW sites are cleared:\n```lua\nredIADS:addEarlyWarningRadarsByPrefix('EW')\n``` \n\n#### Add an EW radar manually\nYou can add EW radars manually, must be a valid unit name: \n```lua\nredIADS:addEarlyWarningRadar('EWR West')\n```\n\n### Accessing EW radars in the IADS\nThe following functions exist to access EW radars added to the IADS. They all support daisy chaining options. \n\n\nReturns all EW radars in the IADS:\n```lua\nredIADS:getEarlyWarningRadars()\n```\n\nReturns the EW radar with the specified unit name:\n```lua\nredIADS:getEarlyWarningRadarByUnitName('EW-west')\n```\n\n## Options for SAM sites and EW radars\n\n### Setting an option\nIn the following examples ```ewRadarOrSamSite``` refers to an single EW radar or SAM site or a table of EW radars and SAM sites you got from the Skynet IADS, by calling one of the functions named in [accessing EW radars](#accessing-ew-radars-in-the-iads) or [accessing SAM sites](#accessing-sam-sites-in-the-iads).\n\n### Daisy chaining options\n You can daisy chain options on a single SAM site / EW Radar or a table of SAM sites / EW radars like this:\n ```lua\n redIADS:getSAMSites():setActAsEW(true):addPowerSource(powerSource):addConnectionNode(connectionNode):setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE):setGoLiveRangeInPercent(90):setAutonomousBehaviour(SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK)\n ```  \n\n### HARM Defence\nYou can set the reaction probability (between 0 and 100 percent). See [skynet-iads-supported-types.lua](/skynet-iads-source/skynet-iads-supported-types.lua) field ```['harm_detection_chance']``` for default detection probabilities:\n```lua\newRadarOrSamSite:setHARMDetectionChance(50)\n```\n\n### Point defence\nYou must use a point defence SAM that can engage HARM missiles. Can be used to protect SAM sites or EW radars. See [point defence](#point-defence) for information what this does:\n\nIf you want the point defences to coordinate their HARM defence then you can add multiple point defence SAM sites in to one group. **This is the only place where you should add multiple SAM sites in to one group in Skynet**.\nLet's assume you have two SA-15 units defending a radar. If the SA-15 units are in separate groups they will both fire at the same HARM inbound. However if they are in the same group and multiple HARMS are inbound they will each pick a separate HARM to engage.\n\n```lua\n--first get the SAM site you want to use as point defence from the IADS:\nlocal sa15 = redIADS:getSAMSiteByGroupName('SAM-SA-15')\n--then add it to the SAM site it should protect:\nredIADS:getSAMSiteByGroupName('SAM-SA-10'):addPointDefence(sa15)\n```\n\nThis function is deprecated and will be removed in a future release.\n```lua\newRadarOrSamSite:setIgnoreHARMSWhilePointDefencesHaveAmmo(true)\n```\n\n### Autonomous mode behaviour\nSet how the SAM site or EW radar will behave if it looses connection to the IADS:\n```lua\newRadarOrSamSite:setAutonomousBehaviour(SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK)\n```\n\n#### Autonomous mode options \nSAM site or EW radar will behave in the default DCS AI. Alarm State will be red and ROE weapons free (default Skynet behaviour for SAM sites):\n```lua\nSkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DCS_AI\n```\n\nSAM Site or EW radar will go dark if it looses connection to IADS (default behaviour for EW radars):\n```lua\nSkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK\n```\n\n## Adding a jammer\nThe jammer is quite easy to set up. You need a unit that acts as a jammer source, preferably it will be an aircraft in the strike package.\nOnce the jammer detects an emitter it starts jamming the radar. Set the [coresponding debug variable jammerProbability](#setting-debug-information) to see what the jammer is doing.\nCheck [skynet-iads-jammer.lua](/skynet-iads-source/skynet-iads-jammer.lua) to see which SAM sites are supported.\n\nRemember to set the AI aircraft acting as jammer in the Mission editor to ```Reaction to Threat = EVADE FIRE``` otherwise the AI will try and actively attack the SAM site.\nThis way it will stick to the preset flight plan.\n\nCreate a jammer and assign it to an unit. Also make sure you add the IADS you wan't the jammer to work for:\n```lua\nlocal jammerSource = Unit.getByName(\"F-4 AI\")\njammer = SkynetIADSJammer:create(jammerSource, iads)\n```\n\nThe jammer will start listening for emitters and if it finds one of the emitters it is able to jam it will start jamming it:\n```lua\njammer:masterArmOn()\n```\n\nWill disable jamming for the specified SAM type, pass the Nato name:\n```lua\njammer:disableFor('SA-2')\n```\n\nWill turn off the jammer. Make sure you call this function before you dereference a jammer in the code, otherwise a background task will keep on jamming:\n```lua\njammer:masterArmSafe()\n```\n\nWill add jammer on / off to the radio menu:\n```lua\njammer:addRadioMenu()\n```\n\nWill remove jammer on / off from the radio menu:\n```lua\njammer:removeRadioMenu()\n```\n\n### Advanced functions\n\nAdd a second IADS the jammer should be able to jam, for example if you have two separate IADS running:\n```lua\njammer:addIADS(iads2)\n```\n\nAdd a new jammer function:\n\n```lua\n-- write a lambda function that expects one parameter:\n-- given public available data on jammers their effeciveness drastically decreases the closer you get, so a non-linear function would make sense:\nlocal function f(distanceNM)\n\treturn ( 1.4 ^ distanceNM ) + 80\nend\n\n-- add the function: specify which SAM type it should apply for:\nself.jammer:addFunction('SA-10', f)\n```\n\nSet the maximum range the jammer will work, the default value is set to 200 nautical miles:\n```lua\njammer:setMaximumEffectiveDistance(100)\n```\n\n## Setting debug information\nWhen developing a mission I suggest you add debug output to check how the IADS reacts to threats. Debug output may slow down DCS, so it's recommended to turn it off in a live environment:\n\nAccess the debug settings:\n```lua\nlocal iadsDebug = redIADS:getDebugSettings()  \n```\n\nOutput in game:\n```lua\niadsDebug.IADSStatus = true\niadsDebug.contacts = true\niadsDebug.jammerProbability = true\n```\n\nOutput to dcs.log:\n```lua\niadsDebug.addedEWRadar = true\niadsDebug.addedSAMSite = true\niadsDebug.warnings = true\niadsDebug.radarWentLive = true\niadsDebug.radarWentDark = true\niadsDebug.harmDefence = true\n```\n\nThese three options will output detailed information on every radar in the IADS to the dcs.log file. Enabling these may have an impact on performance:\n```lua\niadsDebug.samSiteStatusEnvOutput = true\niadsDebug.earlyWarningRadarStatusEnvOutput = true\niadsDebug.commandCenterStatusEnvOutput = true\n```\n![Mission Editor IADS Setup](/images/skynet-debug.png)  \n\n# Example Setup\nThis is an example of how you can set up your IADS used in the [demo mission](/demo-missions/skynet-test-persian-gulf.miz):\n```lua\ndo\n\n--create an instance of the IADS\nredIADS = SkynetIADS:create('RED IADS')\n\n---debug settings remove from here on if you do not wan't any output on what the IADS is doing by default\nlocal iadsDebug = redIADS:getDebugSettings()\niadsDebug.IADSStatus = true\niadsDebug.radarWentDark = true\niadsDebug.contacts = true\niadsDebug.radarWentLive = true\niadsDebug.noWorkingCommmandCenter = true\niadsDebug.samNoConnection = true\niadsDebug.jammerProbability = true\niadsDebug.addedEWRadar = true\niadsDebug.harmDefence = true\n---end remove debug ---\n\n--add all units with unit name beginning with 'EW' to the IADS:\nredIADS:addEarlyWarningRadarsByPrefix('EW')\n\n--add all groups begining with group name 'SAM' to the IADS:\nredIADS:addSAMSitesByPrefix('SAM')\n\n--add a command center:\ncommandCenter = StaticObject.getByName('Command-Center')\nredIADS:addCommandCenter(commandCenter)\n\n---we add a K-50 AWACs, manually. This could just as well be automated by adding an 'EW' prefix to the unit name:\nredIADS:addEarlyWarningRadar('AWACS-K-50')\n\n--add a power source and a connection node for this EW radar:\nlocal powerSource = StaticObject.getByName('Power-Source-EW-Center3')\nlocal connectionNodeEW = StaticObject.getByName('Connection-Node-EW-Center3')\nredIADS:getEarlyWarningRadarByUnitName('EW-Center3'):addPowerSource(powerSource):addConnectionNode(connectionNodeEW)\n\n--add a connection node to this SA-2 site, and set the option for it to go dark, if it looses connection to the IADS:\nlocal connectionNode = Unit.getByName('Mobile-Command-Post-SAM-SA-2')\nredIADS:getSAMSiteByGroupName('SAM-SA-2'):addConnectionNode(connectionNode):setAutonomousBehaviour(SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK)\n\n--this SA-2 site will go live at 70% of its max search range:\nredIADS:getSAMSiteByGroupName('SAM-SA-2'):setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE):setGoLiveRangeInPercent(70)\n\n--all SA-10 sites shall act as EW sites, meaning their radars will be on all the time:\nredIADS:getSAMSitesByNatoName('SA-10'):setActAsEW(true)\n\n--set the SA-15's as point defence for the SA-10 site. We set the SA-10 to always identify HARMs so we can demonstrate the point defence mechanism in Skynet.\n--the SA-10 will stay online when shot at by HARMS as long as the point defences and SAM site have ammo and the saturation point is not reached.\nlocal sa15 = redIADS:getSAMSiteByGroupName('SAM-SA-15-point-defence-SA-10')\nredIADS:getSAMSiteByGroupName('SAM-SA-10'):addPointDefence(sa15):setHARMDetectionChance(100)\n\n--set this SA-11 site to go live 70% of max range of its missiles (default value: 100%), its HARM detection probability is set to 50% (default value: 70%)\nredIADS:getSAMSiteByGroupName('SAM-SA-11'):setGoLiveRangeInPercent(70):setHARMDetectionChance(50)\n\n--this SA-6 site will always react to a HARM being fired at it:\nredIADS:getSAMSiteByGroupName('SAM-SA-6'):setHARMDetectionChance(100)\n\n--set this SA-11 site to go live at maximunm search range (default is at maximung firing range):\nredIADS:getSAMSiteByGroupName('SAM-SA-11-2'):setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE)\n\n--activate the radio menu to toggle IADS Status output\nredIADS:addRadioMenu()\n\n--activate the IADS\nredIADS:activate()\t\n\n--add the jammer\nlocal jammer = SkynetIADSJammer:create(Unit.getByName('jammer-emitter'), redIADS)\njammer:masterArmOn()\n\n--setup blue IADS:\nblueIADS = SkynetIADS:create('BLUE IADS')\nblueIADS:addSAMSitesByPrefix('BLUE-SAM')\nblueIADS:addEarlyWarningRadarsByPrefix('BLUE-EW')\nblueIADS:activate()\nblueIADS:addRadioMenu()\n\nlocal iadsDebug = blueIADS:getDebugSettings()\niadsDebug.IADSStatus = true\niadsDebug.contacts = true\n\nend\n```\n\n# FAQ\n\n## Does Skynet IADS have an impact on game performance?\nSkynet may actually improve game performance when using a lot of SAM AI units. This is because Skynet will turn off radar emissions of all SAM groups currently not in range of a target. By default these SAM groups would otherwise have their radars on. Skynet caches target information for a few seconds to reduce expensive calls on DCS radar detection.\n\n## What air defence units shall I add to the Skynet IADS?\nIn theory you can add all the types that are listed in the [skynet-iads-supported-types.lua](skynet-iads-source/skynet-iads-supported-types.lua) file. \nVery short range units (like the Shilka AAA, Rapier) won't really benefit from the IADS apart from reacting to HARMs. These are better just placed in a mission and handeled by the default AI of DCS.\nThis is due to the short range of their radars. By the time the IADS wakes them up, the contact has likely passed their engagement range.\nThe strength of the Skynet IADS lies with handling long range systems that operate by radar.\n\n## Which SAM systems can engage HARMS?\nAs of July 2022 I have only been able to get the SA-15, SA-10, NASAMS and Patriot to engage HARMS. The best option for a solid HARM defence is to add SA-15's around EW radars or high value SAM sites.\n\n## What exactly does Skynet do with the SAMS?\nVia the scripting engine one can toggle the radar emitters on and off. Further options are the alarm state and the rules of engagement. In a nutshell that's all that Skynet does. Skynet does also read the radar and firing range properties of a SAM site. Based on that data and the setup options a mission designer provides Skynet will turn a SAM site on or off. \n\nNo god like intervention is used (like magically exploding HARMS via the scripting engine).\nIf a SAM site or EW radar detects an inbound HARM it just turns off its radar as in real life. The HARM as it is programmed in DCS will try and glide in to the last known position mostly resulting in misses by 50-100 meters.\n\n## Are there known bugs?\nYes, when placing multi unit SAM sites (e.g. SA-3, Patriot..) make sure the first unit you place is the search radar. If you add any other element as the first unit, Skynet will not be able to read radar data.\nThe result will be that the SAM site won't go live. This bug was observed in DCS 2.5.5. The SAM site will work fine when used as a standalone unit outside of Skynet.\n\n## How do I know if a SAM site is in range of an EW site or a SAM site in EW mode?\nTo get a rough idea you can look at the range circles in the mission editor. However these ranges are greater than the actual in game detection ranges of an EW radar or SAM site.\nThe following screenshot shows the range of the 1L13 EWR. The mission editor shows a range of 64 NM (nautical miles) where as the in game range is 43 NM.\n\nIn this example the SAM site to the north east would not be in range of the EW radar, therefore it would go in to autonomous mode once the mission starts. \n\n\n![1L13 EWR range differences](/images/ew-detection-distance-example.png)  \n\nSet the debug options ```samSiteStatusEnvOutput``` and ```earlyWarningRadarStatusEnvOutput``` to get detailed information on every SAM site and EW radar.\nThe text marked in the red box will show you which SAM sites are in the covered area of a SAM site or EW radar.\n\n\n![SAM sites in covered area](/images/radar-emitter-status-dcs-log.png) \n\n## How do I connect Skynet with the MOOSE AI_A2A_DISPATCHER and what are the benefits of that?\nIRL an IADS would most likely not only handle SAM sites but also pass information to interceptor aircraft. By connecting Skynet to the [AI_A2A_DISPATCHER](https://flightcontrol-master.github.io/MOOSE_DOCS/Documentation/AI.AI_A2A_Dispatcher.html) by MOOSE you are able\nto add interceptors to the IADS. See [Skynet Moose AI_A2A_DISPATCHER](#connecting-skynet-to-the-moose-ai_a2a_dispatcher) and the [moose_a2a_connector demo mission](demo-missions/moose_a2a_connector) for more information.\n\nAn example setup of Skynet and the [AI_A2A_DISPATCHER](https://flightcontrol-master.github.io/MOOSE_DOCS/Documentation/AI.AI_A2A_Dispatcher.html) :\n```lua\n\n--Setup Syknet IADS:\nredIADS = SkynetIADS:create('Enemy IADS')\nredIADS:addSAMSitesByPrefix('SAM')\nredIADS:addEarlyWarningRadarsByPrefix('EW')\nredIADS:activate()\n\n-- START MOOSE CODE:\n-- Define a SET_GROUP object that builds a collection of groups that define the EWR network.\nDetectionSetGroup = SET_GROUP:New()\n\n-- Setup the detection and group targets to a 30km range!\nDetection = DETECTION_AREAS:New( DetectionSetGroup, 30000 )\n\n-- Setup the A2A dispatcher, and initialize it.\nA2ADispatcher = AI_A2A_DISPATCHER:New( Detection )\n\n-- Set 100km as the radius to engage any target by airborne friendlies.\nA2ADispatcher:SetEngageRadius() -- 100000 is the default value.\n\n-- Set 200km as the radius to ground control intercept.\nA2ADispatcher:SetGciRadius() -- 200000 is the default value.\n\nCCCPBorderZone = ZONE_POLYGON:New( \"RED-BORDER\", GROUP:FindByName( \"RED-BORDER\" ) )\nA2ADispatcher:SetBorderZone( CCCPBorderZone )\nA2ADispatcher:SetSquadron( \"Kutaisi\", AIRBASE.Caucasus.Kutaisi, { \"Squadron red SU-27\" }, 2 )\nA2ADispatcher:SetSquadronGrouping( \"Kutaisi\", 2 )\nA2ADispatcher:SetSquadronGci( \"Kutaisi\", 900, 1200 )\nA2ADispatcher:SetTacticalDisplay(true)\nA2ADispatcher:Start()\n--END MOOSE CODE\n\n-- add the MOOSE SET_GROUP to the IADS, from now on Skynet will update active radars that the MOOSE SET_GROUP can use for EW detection.\nredIADS:addMooseSetGroup(DetectionSetGroup)\n```\n\n# Thanks\nSpecial thaks to Spearzone and Coranthia for researching public available information on IADS networks and getting me up to speed on how such a system works.\nI based the SAM site setup on [Grimes SAM DB](https://forums.eagle.ru/showthread.php?t=118175) from his IADS script, however I removed range data since Skynet loads that from DCS.\n\n\n"
  },
  {
    "path": "skynet-iads-source/highdigitsams/skynet-iads-high-digit-sams-suported-types.lua",
    "content": "do\n-- this file contains the definitions for the HightDigitSAMSs: https://github.com/Auranis/HighDigitSAMs\n\n--EW radars used in multiple SAM systems:\n\ns300PMU164N6Esr = {\n\t['name'] = {\n\t\t['NATO'] = 'Big Bird',\n\t},\n}\n\ns300PMU140B6MDsr = {\n\t['name'] = {\n\t\t['NATO'] = 'Clam Shell',\n\t},\n}\n\n--[[ units in SA-10 group Gargoyle:\n2020-12-10 18:27:27.050 INFO    SCRIPTING: S-300PMU1 54K6 cp\n2020-12-10 18:27:27.050 INFO    SCRIPTING: S-300PMU1 5P85CE ln\n2020-12-10 18:27:27.050 INFO    SCRIPTING: S-300PMU1 5P85DE ln\n2020-12-10 18:27:27.050 INFO    SCRIPTING: S-300PMU1 40B6MD sr\n2020-12-10 18:27:27.050 INFO    SCRIPTING: S-300PMU1 64N6E sr\n2020-12-10 18:27:27.050 INFO    SCRIPTING: S-300PMU1 40B6M tr\n2020-12-10 18:27:27.050 INFO    SCRIPTING: S-300PMU1 30N6E tr\n--]]\nsamTypesDB['S-300PMU1'] = {\n\t['type'] = 'complex',\n\t['searchRadar'] = {\n\t\t['S-300PMU1 40B6MD sr'] = s300PMU140B6MDsr,\n\t\t['S-300PMU1 64N6E sr'] = s300PMU164N6Esr,\n\t\t\n\t\t['S-300PS 40B6MD sr'] = {\n\t\t\t['name'] = {\n\t\t\t\t['NATO'] = '',\n\t\t\t},\n\t\t},\n\t\t['S-300PS 64H6E sr'] = {\n\t\t\t['name'] = {\n\t\t\t\t['NATO'] = '',\n\t\t\t},\n\t\t},\n\t},\n\t['trackingRadar'] = {\n\t\t['S-300PMU1 40B6M tr'] = {\n\t\t\t['name'] = {\n\t\t\t\t['NATO'] = 'Grave Stone',\n\t\t\t},\n\t\t},\n\t\t['S-300PMU1 30N6E tr'] = {\n\t\t\t['name'] = {\n\t\t\t\t['NATO'] = 'Flap Lid',\n\t\t\t},\n\n\t\t},\n\t\t['S-300PS 40B6M tr'] = {\n\t\t\t['name'] = {\n\t\t\t\t['NATO'] = '',\n\t\t\t},\n\t\t},\n\t},\n\t['misc'] = {\n\t\t['S-300PMU1 54K6 cp'] = {\n\t\t\t['required'] = true,\n\t\t},\n\t},\n\t['launchers'] = {\n\t\t['S-300PMU1 5P85CE ln'] = {\n\t\t},\n\t\t['S-300PMU1 5P85DE ln'] = {\n\t\t},\n\t},\n\t['name']  = {\n\t\t['NATO'] = 'SA-20A Gargoyle'\n\t},\n\t['harm_detection_chance'] = 90,\n\t['can_engage_harm'] = true\n}\t\n\n--[[ Units in the SA-23 Group:\n2020-12-11 16:40:52.072 INFO    SCRIPTING: S-300VM 9A82ME ln\n2020-12-11 16:40:52.072 INFO    SCRIPTING: S-300VM 9A83ME ln\n2020-12-11 16:40:52.072 INFO    SCRIPTING: S-300VM 9S15M2 sr\n2020-12-11 16:40:52.072 INFO    SCRIPTING: S-300VM 9S19M2 sr\n2020-12-11 16:40:52.072 INFO    SCRIPTING: S-300VM 9S32ME tr\n2020-12-11 16:40:52.072 INFO    SCRIPTING: S-300VM 9S457ME cp\n\n]]--\nsamTypesDB['S-300VM'] = {\n\t['type'] = 'complex',\n\t['searchRadar'] = {\n\t\t['S-300VM 9S15M2 sr'] = {\n\t\t\t['name'] = {\n\t\t\t\t['NATO'] = 'Bill Board-C',\n\t\t\t},\n\t\t},\n\t\t['S-300VM 9S19M2 sr'] = {\n\t\t\t['name'] = {\n\t\t\t\t['NATO'] = 'High Screen-B',\n\t\t\t},\n\t\t},\n\t},\n\t['trackingRadar'] = {\n\t\t['S-300VM 9S32ME tr'] = {\n\t\t},\n\t},\n\t['misc'] = {\n\t\t['S-300VM 9S457ME cp'] = {\n\t\t\t['required'] = true,\n\t\t},\n\t},\n\t['launchers'] = {\n\t\t['S-300VM 9A82ME ln'] = {\n\t\t},\n\t\t['S-300VM 9A83ME ln'] = {\n\t\t},\n\t},\n\t['name']  = {\n\t\t['NATO'] = 'SA-23 Antey-2500'\n\t},\n\t['harm_detection_chance'] = 90,\n\t['can_engage_harm'] = true\n}\t\n\n--[[ Units in the SA-10B Group:\n2021-01-01 20:39:14.413 INFO    SCRIPTING: S-300PS SA-10B 40B6MD MAST sr\n2021-01-01 20:39:14.413 INFO    SCRIPTING: S-300PS SA-10B 54K6 cp\n2021-01-01 20:39:14.413 INFO    SCRIPTING: S-300PS 5P85SE_mod ln\n2021-01-01 20:39:14.413 INFO    SCRIPTING: S-300PS 5P85SU_mod ln\n2021-01-01 20:39:14.413 INFO    SCRIPTING: S-300PS 64H6E TRAILER sr\n2021-01-01 20:39:14.413 INFO    SCRIPTING: S-300PS 30N6 TRAILER tr\n2021-01-01 20:39:14.413 INFO    SCRIPTING: S-300PS SA-10B 40B6M MAST tr\n--]]\nsamTypesDB['S-300PS'] = {\n\t['type'] = 'complex',\n\t['searchRadar'] = {\n\t\t['S-300PS SA-10B 40B6MD MAST sr'] = {\n\t\t\t['name'] = {\n\t\t\t\t['NATO'] = 'Clam Shell',\n\t\t\t},\n\t\t},\n\t\t['S-300PS 64H6E TRAILER sr'] = {\n\t\t},\n\t},\n\t['trackingRadar'] = {\n\t\t['S-300PS 30N6 TRAILER tr'] = {\n\t\t},\n\t\t['S-300PS SA-10B 40B6M MAST tr'] = {\n\t\t},\n\t\t['S-300PS 40B6M tr'] = {\n\t\t},\n\t\t['S-300PMU1 40B6M tr'] = {\n\t\t},\t\n\t\t['S-300PMU1 30N6E tr'] = {\n\t\t},\t\t\n\t},\n\t['misc'] = {\n\t\t['S-300PS SA-10B 54K6 cp'] = {\n\t\t\t['required'] = true,\n\t\t},\n\t},\n\t['launchers'] = {\n\t\t['S-300PS 5P85SE_mod ln'] = {\n\t\t},\n\t\t['S-300PS 5P85SU_mod ln'] = {\n\t\t},\n\t},\n\t['name']  = {\n\t\t['NATO'] = 'SA-10B Grumble'\n\t},\n\t['harm_detection_chance'] = 90,\n\t['can_engage_harm'] = true\n}\n\n--[[ Extra launchers for the in game SA-10C and HighDigitSAMs SA-10B, SA-20B\n2021-01-01 21:04:19.908 INFO    SCRIPTING: S-300PS 5P85DE ln\n2021-01-01 21:04:19.908 INFO    SCRIPTING: S-300PS 5P85CE ln\n--]]\n\nlocal s300launchers = samTypesDB['S-300']['launchers']\ns300launchers['S-300PS 5P85DE ln'] = {}\ns300launchers['S-300PS 5P85CE ln'] = {}\n\nlocal s300launchers = samTypesDB['S-300PS']['launchers']\ns300launchers['S-300PS 5P85DE ln'] = {}\ns300launchers['S-300PS 5P85CE ln'] = {}\n\nlocal s300launchers = samTypesDB['S-300PMU1']['launchers']\ns300launchers['S-300PS 5P85DE ln'] = {}\ns300launchers['S-300PS 5P85CE ln'] = {}\n\n--[[\nNew launcher for the SA-11 complex, will identify as SA-17\nSA-17 Buk M1-2 LN 9A310M1-2\n --]]\nsamTypesDB['Buk-M2'] = {\n\t['type'] = 'complex',\n\t['searchRadar'] = {\n\t\t['SA-11 Buk SR 9S18M1'] = {\n\t\t\t['name'] = {\n\t\t\t\t['NATO'] = 'Snow Drift',\n\t\t\t},\n\t\t},\n\t},\n\t['launchers'] = {\n\t\t['SA-17 Buk M1-2 LN 9A310M1-2'] = {\n\t\t},\n\t},\n\t['misc'] = {\n\t\t['SA-11 Buk CC 9S470M1'] = {\n\t\t\t['required'] = true,\n\t\t},\n\t},\n\t['name'] = {\n\t\t['NATO'] = 'SA-17 Grizzly',\n\t},\n\t['harm_detection_chance'] = 90\n}\n\n--[[\nNew launcher for the SA-2 complex: S_75M_Volhov_V759\n--]]\nlocal s75launchers = samTypesDB['S-75']['launchers']\ns75launchers['S_75M_Volhov_V759'] = {}\n\n--[[\nNew launcher for the SA-3 complex:\n--]]\nlocal s125launchers = samTypesDB['S-125']['launchers']\ns125launchers['5p73 V-601P ln'] = {}\n\n--[[\nNew launcher for the SA-2 complex: HQ_2_Guideline_LN\n--]]\nlocal s125launchers = samTypesDB['S-75']['launchers']\ns125launchers['HQ_2_Guideline_LN'] = {}\n\n--[[\nSA-12 Gladiator / Giant:\n2021-03-19 21:24:22.620 INFO    SCRIPTING: S-300V 9S15 sr\n2021-03-19 21:24:22.620 INFO    SCRIPTING: S-300V 9S19 sr\n2021-03-19 21:24:22.620 INFO    SCRIPTING: S-300V 9S32 tr\n2021-03-19 21:24:22.620 INFO    SCRIPTING: S-300V 9S457 cp\n2021-03-19 21:24:22.620 INFO    SCRIPTING: S-300V 9A83 ln\n2021-03-19 21:24:22.620 INFO    SCRIPTING: S-300V 9A82 ln\n--]]\nsamTypesDB['S-300V'] = {\n\t['type'] = 'complex',\n\t['searchRadar'] = {\n\t\t['S-300V 9S15 sr'] = {\n\t\t\t['name'] = {\n\t\t\t\t['NATO'] = 'Bill Board',\n\t\t\t},\n\t\t},\n\t\t['S-300V 9S19 sr'] = {\n\t\t\t['name'] = {\n\t\t\t\t['NATO'] = 'High Screen',\n\t\t\t},\n\t\t},\n\t},\n\t['trackingRadar'] = {\n\t\t['S-300V 9S32 tr'] = {\n\t\t\t['NATO'] = 'Grill Pan',\n\t\t\t},\n\t},\n\t['misc'] = {\n\t\t['S-300V 9S457 cp'] = {\n\t\t\t['required'] = true,\n\t\t},\n\t},\n\t['launchers'] = {\n\t\t['S-300V 9A83 ln'] = {\n\t\t},\n\t\t['S-300V 9A82 ln'] = {\n\t\t},\n\t},\n\t['name']  = {\n\t\t['NATO'] = 'SA-12 Gladiator/Giant'\n\t},\n\t['harm_detection_chance'] = 90,\n\t['can_engage_harm'] = true\n}\n\n--[[\nSA-20B Gargoyle B:\n\n2021-03-25 19:15:02.135 INFO    SCRIPTING: S-300PMU2 64H6E2 sr\n2021-03-25 19:15:02.135 INFO    SCRIPTING: S-300PMU2 92H6E tr\n2021-03-25 19:15:02.135 INFO    SCRIPTING: S-300PMU2 5P85SE2 ln\n2021-03-25 19:15:02.135 INFO    SCRIPTING: S-300PMU2 54K6E2 cp\n--]]\n\nsamTypesDB['S-300PMU2'] = {\n\t['type'] = 'complex',\n\t['searchRadar'] = {\n\t\t['S-300PMU2 64H6E2 sr'] = {\n\t\t\t['name'] = {\n\t\t\t\t['NATO'] = '',\n\t\t\t},\n\t\t},\n\t\t['S-300PMU1 40B6MD sr'] = s300PMU140B6MDsr,\n\t\t['S-300PMU1 64N6E sr'] = s300PMU164N6Esr,\n\t},\n\t['trackingRadar'] = {\n\t\t['S-300PMU2 92H6E tr'] = {\n\t\t},\n\t\t['S-300PS 40B6M tr'] = {\n\t\t},\n\t\t['S-300PMU1 40B6M tr'] = {\n\t\t},\n\t\t['S-300PMU1 30N6E tr'] = {\n\t\t},\n\t},\n\t['misc'] = {\n\t\t['S-300PMU2 54K6E2 cp'] = {\n\t\t\t['required'] = true,\n\t\t},\n\t},\n\t['launchers'] = {\n\t\t['S-300PMU2 5P85SE2 ln'] = {\n\t\t},\n\t},\n\t['name']  = {\n\t\t['NATO'] = 'SA-20B Gargoyle B'\n\t},\n\t['harm_detection_chance'] = 90,\n\t['can_engage_harm'] = true\n}\n\n--[[\n\n--]]\nend\n\n\n\n"
  },
  {
    "path": "skynet-iads-source/skynet-iads-abstract-dcs-object-wrapper.lua",
    "content": "do\n\nSkynetIADSAbstractDCSObjectWrapper = {}\n\nfunction SkynetIADSAbstractDCSObjectWrapper:create(dcsRepresentation)\n\tlocal instance = {}\n\tsetmetatable(instance, self)\n\tself.__index = self\n\tinstance.dcsName = \"\"\n\tinstance.typeName = \"\"\n\tinstance:setDCSRepresentation(dcsRepresentation)\n\tif getmetatable(dcsRepresentation) ~= Group then\n\t\tinstance.typeName = dcsRepresentation:getTypeName()\n\tend\n\treturn instance\nend\n\nfunction SkynetIADSAbstractDCSObjectWrapper:setDCSRepresentation(representation)\n\tself.dcsRepresentation = representation\n\tif self.dcsRepresentation then\n\t\tself.dcsName = self.dcsRepresentation:getName()\n\t\tif (self.dcsName == nil or string.len(self.dcsName) == 0) and self.dcsRepresentation.id_ then\n\t\t\tself.dcsName = self.dcsRepresentation.id_\n\t\tend\n\tend\nend\n\nfunction SkynetIADSAbstractDCSObjectWrapper:getDCSRepresentation()\n\treturn self.dcsRepresentation\nend\n\nfunction SkynetIADSAbstractDCSObjectWrapper:getName()\n\treturn self.dcsName\nend\n\nfunction SkynetIADSAbstractDCSObjectWrapper:getTypeName()\n\treturn self.typeName\nend\n\nfunction SkynetIADSAbstractDCSObjectWrapper:getPosition()\n\treturn self.dcsRepresentation:getPosition()\nend\n\nfunction SkynetIADSAbstractDCSObjectWrapper:isExist()\n\tif self.dcsRepresentation then\n\t\treturn self.dcsRepresentation:isExist()\n\telse\n\t\treturn false\n\tend\nend\n\nfunction SkynetIADSAbstractDCSObjectWrapper:insertToTableIfNotAlreadyAdded(tbl, object)\n\tlocal isAdded = false\n\tfor i = 1, #tbl do\n\t\tlocal child = tbl[i]\n\t\tif child == object then\n\t\t\tisAdded = true\n\t\tend\n\tend\n\tif isAdded == false then\n\t\ttable.insert(tbl, object)\n\tend\n\treturn not isAdded\nend\n\n-- helper code for class inheritance\nfunction inheritsFrom( baseClass )\n\n    local new_class = {}\n    local class_mt = { __index = new_class }\n\n    function new_class:create()\n        local newinst = {}\n        setmetatable( newinst, class_mt )\n        return newinst\n    end\n\n    if nil ~= baseClass then\n        setmetatable( new_class, { __index = baseClass } )\n    end\n\n    -- Implementation of additional OO properties starts here --\n\n    -- Return the class object of the instance\n    function new_class:class()\n        return new_class\n    end\n\n    -- Return the super class object of the instance\n    function new_class:superClass()\n        return baseClass\n    end\n\n    -- Return true if the caller is an instance of theClass\n    function new_class:isa( theClass )\n        local b_isa = false\n\n        local cur_class = new_class\n\n        while ( nil ~= cur_class ) and ( false == b_isa ) do\n            if cur_class == theClass then\n                b_isa = true\n            else\n                cur_class = cur_class:superClass()\n            end\n        end\n\n        return b_isa\n    end\n\n    return new_class\nend\n\n\nend\n\n"
  },
  {
    "path": "skynet-iads-source/skynet-iads-abstract-element.lua",
    "content": "do\n\nSkynetIADSAbstractElement = {}\nSkynetIADSAbstractElement = inheritsFrom(SkynetIADSAbstractDCSObjectWrapper)\n\nfunction SkynetIADSAbstractElement:create(dcsRepresentation, iads)\n\tlocal instance = self:superClass():create(dcsRepresentation)\n\tsetmetatable(instance, self)\n\tself.__index = self\n\tinstance.connectionNodes = {}\n\tinstance.powerSources = {}\n\tinstance.iads = iads\n\tinstance.natoName = \"UNKNOWN\"\n\tworld.addEventHandler(instance)\n\treturn instance\nend\n\nfunction SkynetIADSAbstractElement:removeEventHandlers()\n\tworld.removeEventHandler(self)\nend\n\nfunction SkynetIADSAbstractElement:cleanUp()\n\tself:removeEventHandlers()\nend\n\nfunction SkynetIADSAbstractElement:isDestroyed()\n\treturn self:getDCSRepresentation():isExist() == false\nend\n\nfunction SkynetIADSAbstractElement:addPowerSource(powerSource)\n\ttable.insert(self.powerSources, powerSource)\n\tself:informChildrenOfStateChange()\n\treturn self\nend\n\nfunction SkynetIADSAbstractElement:getPowerSources()\n\treturn self.powerSources\nend\n\nfunction SkynetIADSAbstractElement:addConnectionNode(connectionNode)\n\ttable.insert(self.connectionNodes, connectionNode)\n\tself:informChildrenOfStateChange()\n\treturn self\nend\n\nfunction SkynetIADSAbstractElement:getConnectionNodes()\n\treturn self.connectionNodes\nend\n\nfunction SkynetIADSAbstractElement:hasActiveConnectionNode()\n\tlocal connectionNode = self:genericCheckOneObjectIsAlive(self.connectionNodes)\n\tif connectionNode == false and self.iads:getDebugSettings().samNoConnection then\n\t\tself.iads:printOutput(self:getDescription()..\" no connection to Command Center\")\n\tend\n\treturn connectionNode\nend\n\nfunction SkynetIADSAbstractElement:hasWorkingPowerSource()\n\tlocal power = self:genericCheckOneObjectIsAlive(self.powerSources)\n\tif power == false and self.iads:getDebugSettings().hasNoPower then\n\t\tself.iads:printOutput(self:getDescription()..\" has no power\")\n\tend\n\treturn power\nend\n\nfunction SkynetIADSAbstractElement:getDCSName()\n\treturn self.dcsName\nend\n\n-- generic function to theck if power plants, command centers, connection nodes are still alive\nfunction SkynetIADSAbstractElement:genericCheckOneObjectIsAlive(objects)\n\tlocal isAlive = (#objects == 0)\n\tfor i = 1, #objects do\n\t\tlocal object = objects[i]\n\t\t--if we find one object that is not fully destroyed we assume the IADS is still working\n\t\tif object:isExist() then\n\t\t\tisAlive = true\n\t\t\tbreak\n\t\tend\n\tend\n\treturn isAlive\nend\n\nfunction SkynetIADSAbstractElement:getNatoName()\n\treturn self.natoName\nend\n\nfunction SkynetIADSAbstractElement:getDescription()\n\treturn \"IADS ELEMENT: \"..self:getDCSName()..\" | Type: \"..tostring(self:getNatoName())\nend\n\nfunction SkynetIADSAbstractElement:onEvent(event)\n\t--if a unit is destroyed we check to see if its a power plant powering the unit or a connection node\n\tif event.id == world.event.S_EVENT_DEAD then\n\t\tif self:hasWorkingPowerSource() == false or self:isDestroyed() then\n\t\t\tself:goDark()\n\t\t\tself:informChildrenOfStateChange()\n\t\tend\n\t\tif self:hasActiveConnectionNode() == false then\n\t\t\tself:informChildrenOfStateChange()\n\t\tend\n\tend\n\tif event.id == world.event.S_EVENT_SHOT then\n\t\tself:weaponFired(event)\n\tend\nend\n\n--placeholder method, can be implemented by subclasses\nfunction SkynetIADSAbstractElement:weaponFired(event)\n\t\nend\n\n--placeholder method, can be implemented by subclasses\nfunction SkynetIADSAbstractElement:goDark()\n\t\nend\n\n--placeholder method, can be implemented by subclasses\nfunction SkynetIADSAbstractElement:goAutonomous()\n\nend\n\n--placeholder method, can be implemented by subclasses\nfunction SkynetIADSAbstractElement:setToCorrectAutonomousState()\n\nend\n\n--placeholder method, can be implemented by subclasses\nfunction SkynetIADSAbstractElement:informChildrenOfStateChange()\n\t\nend\n\nend\n"
  },
  {
    "path": "skynet-iads-source/skynet-iads-abstract-radar-element.lua",
    "content": "do\n\nSkynetIADSAbstractRadarElement = {}\nSkynetIADSAbstractRadarElement = inheritsFrom(SkynetIADSAbstractElement)\n\nSkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DCS_AI = 1\nSkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK = 2\n\nSkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE = 1\nSkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE = 2\n\nSkynetIADSAbstractRadarElement.HARM_TO_SAM_ASPECT = 15\nSkynetIADSAbstractRadarElement.HARM_LOOKAHEAD_NM = 20\n\nfunction SkynetIADSAbstractRadarElement:create(dcsElementWithRadar, iads)\n\tlocal instance = self:superClass():create(dcsElementWithRadar, iads)\n\tsetmetatable(instance, self)\n\tself.__index = self\n\tinstance.aiState = false\n\tinstance.harmScanID = nil\n\tinstance.harmSilenceID = nil\n\tinstance.lastJammerUpdate = 0\n\tinstance.objectsIdentifiedAsHarms = {}\n\tinstance.objectsIdentifiedAsHarmsMaxTargetAge = 60\n\tinstance.launchers = {}\n\tinstance.trackingRadars = {}\n\tinstance.searchRadars = {}\n\tinstance.parentRadars = {}\n\tinstance.childRadars = {}\n\tinstance.missilesInFlight = {}\n\tinstance.pointDefences = {}\n\tinstance.harmDecoys = {}\n\tinstance.autonomousBehaviour = SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DCS_AI\n\tinstance.goLiveRange = SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE\n\tinstance.isAutonomous = true\n\tinstance.harmDetectionChance = 0\n\tinstance.minHarmShutdownTime = 0\n\tinstance.maxHarmShutDownTime = 0\n\tinstance.minHarmPresetShutdownTime = 30\n\tinstance.maxHarmPresetShutdownTime = 180\n\tinstance.harmShutdownTime = 0\n\tinstance.firingRangePercent = 100\n\tinstance.actAsEW = false\n\tinstance.cachedTargets = {}\n\tinstance.cachedTargetsMaxAge = 1\n\tinstance.cachedTargetsCurrentAge = 0\n\tinstance.goLiveTime = 0\n\tinstance.engageAirWeapons = false\n\tinstance.isAPointDefence = false\n\tinstance.canEngageHARM = false\n\tinstance.dataBaseSupportedTypesCanEngageHARM = false\n\t-- 5 seconds seems to be a good value for the sam site to find the target with its organic radar\n\tinstance.noCacheActiveForSecondsAfterGoLive = 5\n\treturn instance\nend\n\n--TODO: this method could be updated to only return Radar weapons fired, this way a SAM firing an IR weapon could go dark faster in the goDark() method\nfunction SkynetIADSAbstractRadarElement:weaponFired(event)\n\tif event.id == world.event.S_EVENT_SHOT then\n\t\tlocal weapon = event.weapon\n\t\tlocal launcherFired = event.initiator\n\t\tfor i = 1, #self.launchers do\n\t\t\tlocal launcher = self.launchers[i]\n\t\t\tif launcher:getDCSRepresentation() == launcherFired then\n\t\t\t\ttable.insert(self.missilesInFlight, weapon)\n\t\t\tend\n\t\tend\n\tend\nend\n\nfunction SkynetIADSAbstractRadarElement:setCachedTargetsMaxAge(maxAge)\n\tself.cachedTargetsMaxAge = maxAge\nend\n\nfunction SkynetIADSAbstractRadarElement:cleanUp()\n\tfor i = 1, #self.pointDefences do\n\t\tlocal pointDefence = self.pointDefences[i]\n\t\tpointDefence:cleanUp()\n\tend\n\tmist.removeFunction(self.harmScanID)\n\tmist.removeFunction(self.harmSilenceID)\n\t--call method from super class\n\tself:removeEventHandlers()\nend\n\nfunction SkynetIADSAbstractRadarElement:setIsAPointDefence(state)\n\tif (state == true or state == false) then\n\t\tself.isAPointDefence = state\n\tend\nend\n\nfunction SkynetIADSAbstractRadarElement:getIsAPointDefence()\n\treturn self.isAPointDefence\nend\n\nfunction SkynetIADSAbstractRadarElement:addPointDefence(pointDefence)\n\ttable.insert(self.pointDefences, pointDefence)\n\tpointDefence:setIsAPointDefence(true)\n\treturn self\nend\n\nfunction SkynetIADSAbstractRadarElement:getPointDefences()\n\treturn self.pointDefences\nend\n\nfunction SkynetIADSAbstractRadarElement:addHARMDecoy(harmDecoy)\n\ttable.insert(self.harmDecoys, harmDecoy)\nend\n\nfunction SkynetIADSAbstractRadarElement:addParentRadar(parentRadar)\n\tself:insertToTableIfNotAlreadyAdded(self.parentRadars, parentRadar)\n\tself:informChildrenOfStateChange()\nend\n\nfunction SkynetIADSAbstractRadarElement:getParentRadars()\n\treturn self.parentRadars\nend\n\nfunction SkynetIADSAbstractRadarElement:clearParentRadars()\n\tself.parentRadars = {}\nend\n\nfunction SkynetIADSAbstractRadarElement:addChildRadar(childRadar)\n\tself:insertToTableIfNotAlreadyAdded(self.childRadars, childRadar)\nend\n\nfunction SkynetIADSAbstractRadarElement:getChildRadars()\n\treturn self.childRadars\nend\n\nfunction SkynetIADSAbstractRadarElement:clearChildRadars()\n\tself.childRadars = {}\nend\n\n--TODO: unit test this method\nfunction SkynetIADSAbstractRadarElement:getUsableChildRadars()\n\tlocal usableRadars = {}\n\tfor i = 1, #self.childRadars do\n\t\tlocal childRadar = self.childRadars[i]\n\t\tif childRadar:hasWorkingPowerSource() and childRadar:hasActiveConnectionNode() then\n\t\t\ttable.insert(usableRadars, childRadar)\n\t\tend\n\tend\t\n\treturn usableRadars\nend\n\nfunction SkynetIADSAbstractRadarElement:informChildrenOfStateChange()\n\tself:setToCorrectAutonomousState()\n\tlocal children = self:getChildRadars()\n\tfor i = 1, #children do\n\t\tlocal childRadar = children[i]\n\t\tchildRadar:setToCorrectAutonomousState()\n\tend\n\tself.iads:getMooseConnector():update()\nend\n\nfunction SkynetIADSAbstractRadarElement:setToCorrectAutonomousState()\n\tlocal parents = self:getParentRadars()\n\tfor i = 1, #parents do\n\t\tlocal parent = parents[i]\n\t\t--of one parent exists that still is connected to the IADS, the SAM site does not have to go autonomous\n\t\t--instead of isDestroyed() write method, hasWorkingSearchRadars()\n\t\tif self:hasActiveConnectionNode() and self.iads:isCommandCenterUsable() and parent:hasWorkingPowerSource() and parent:hasActiveConnectionNode() and parent:getActAsEW() == true and parent:isDestroyed() == false then\n\t\t\tself:resetAutonomousState()\n\t\t\treturn\n\t\tend\n\tend\n\tself:goAutonomous()\nend\n\n\nfunction SkynetIADSAbstractRadarElement:setAutonomousBehaviour(mode)\n\tif mode ~= nil then\n\t\tself.autonomousBehaviour = mode\n\tend\n\treturn self\nend\n\nfunction SkynetIADSAbstractRadarElement:getAutonomousBehaviour()\n\treturn self.autonomousBehaviour\nend\n\nfunction SkynetIADSAbstractRadarElement:resetAutonomousState()\n\tself.isAutonomous = false\n\tself:goDark()\nend\n\nfunction SkynetIADSAbstractRadarElement:goAutonomous()\n\tself.isAutonomous = true\n\tif self.autonomousBehaviour == SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK then\n\t\tself:goDark()\n\telse\n\t\tself:goLive()\n\tend\nend\n\nfunction SkynetIADSAbstractRadarElement:getAutonomousState()\n\treturn self.isAutonomous\nend\n\nfunction SkynetIADSAbstractRadarElement:pointDefencesHaveRemainingAmmo(minNumberOfMissiles)\n\tlocal remainingMissiles = 0\n\tfor i = 1, #self.pointDefences do\n\t\tlocal pointDefence = self.pointDefences[i]\n\t\tremainingMissiles = remainingMissiles + pointDefence:getRemainingNumberOfMissiles()\n\tend\n\treturn self:hasRequiredNumberOfMissiles(minNumberOfMissiles, remainingMissiles)\nend\n\nfunction SkynetIADSAbstractRadarElement:hasRequiredNumberOfMissiles(minNumberOfMissiles, remainingMissiles)\n\tlocal returnValue = false\n\tif ( remainingMissiles > 0 and remainingMissiles >= minNumberOfMissiles ) then\n\t\treturnValue = true\n\tend\n\treturn returnValue\nend\n\nfunction SkynetIADSAbstractRadarElement:hasRemainingAmmoToEngageMissiles(minNumberOfMissiles)\n\tlocal remainingMissiles = self:getRemainingNumberOfMissiles()\n\treturn self:hasRequiredNumberOfMissiles(minNumberOfMissiles, remainingMissiles)\nend\n\n-- this method needs to be refactored so that it works for ew radars that don't have launchers, or that it is only called by sam sites\nfunction SkynetIADSAbstractRadarElement:hasEnoughLaunchersToEngageMissiles(minNumberOfLaunchers)\n\tlocal launchers = self:getLaunchers()\n\tif(launchers ~= nil) then\n\t launchers = #self:getLaunchers()\n\telse \n\t\tlaunchers = 0\n\tend\n\treturn self:hasRequiredNumberOfMissiles(minNumberOfLaunchers, launchers)\nend\n\nfunction SkynetIADSAbstractRadarElement:pointDefencesHaveEnoughLaunchers(minNumberOfLaunchers)\n\tlocal numOfLaunchers = 0\n\tfor i = 1, #self.pointDefences do\n\t\tlocal pointDefence = self.pointDefences[i]\n\t\tnumOfLaunchers = numOfLaunchers + #pointDefence:getLaunchers()\t\n\tend\n\treturn self:hasRequiredNumberOfMissiles(minNumberOfLaunchers, numOfLaunchers)\nend\n\nfunction SkynetIADSAbstractRadarElement:setIgnoreHARMSWhilePointDefencesHaveAmmo(state)\n\tself.iads:printOutputToLog(\"DEPRECATED: setIgnoreHARMSWhilePointDefencesHaveAmmo SAM Site will stay live automaticall as long as itself or it's point defences can defend against a HARM\")\n\treturn self\nend\n\nfunction SkynetIADSAbstractRadarElement:hasMissilesInFlight()\n\treturn #self.missilesInFlight > 0\nend\n\nfunction SkynetIADSAbstractRadarElement:getNumberOfMissilesInFlight()\n\treturn #self.missilesInFlight\nend\n\n-- DCS does not send an event, when a missile is destroyed, so this method needs to be polled so that the missiles in flight are current, polling is done in the HARM Search call: evaluateIfTargetsContainHARMs\nfunction SkynetIADSAbstractRadarElement:updateMissilesInFlight()\n\tlocal missilesInFlight = {}\n\tfor i = 1, #self.missilesInFlight do\n\t\tlocal missile = self.missilesInFlight[i]\n\t\tif missile:isExist() then\n\t\t\ttable.insert(missilesInFlight, missile)\n\t\tend\n\tend\n\tself.missilesInFlight = missilesInFlight\n\tself:goDarkIfOutOfAmmo()\nend\n\nfunction SkynetIADSAbstractRadarElement:goDarkIfOutOfAmmo()\n\tif self:hasRemainingAmmo() == false and self:getActAsEW() == false then\n\t\tself:goDark()\n\tend\nend\n\nfunction SkynetIADSAbstractRadarElement:getActAsEW()\n\treturn self.actAsEW\nend\t\n\nfunction SkynetIADSAbstractRadarElement:setActAsEW(ewState)\n\tif ewState == true or ewState == false then\n\t\tlocal stateChange = false\n\t\tif ewState ~= self.actAsEW then\n\t\t\tstateChange = true\n\t\tend\n\t\tself.actAsEW = ewState\n\t\tif stateChange then\n\t\t\tself:informChildrenOfStateChange()\n\t\tend\n\tend\n\tif self.actAsEW == true then\n\t\tself:goLive()\n\telse\n\t\tself:goDark()\n\tend\n\treturn self\nend\n\nfunction SkynetIADSAbstractRadarElement:getUnitsToAnalyse()\n\tlocal units = {}\n\ttable.insert(units, self:getDCSRepresentation())\n\tif getmetatable(self:getDCSRepresentation()) == Group then\n\t\tunits = self:getDCSRepresentation():getUnits()\n\tend\n\treturn units\nend\n\nfunction SkynetIADSAbstractRadarElement:getRemainingNumberOfMissiles()\n\tlocal remainingNumberOfMissiles = 0\n\tfor i = 1, #self.launchers do\n\t\tlocal launcher = self.launchers[i]\n\t\tremainingNumberOfMissiles = remainingNumberOfMissiles + launcher:getRemainingNumberOfMissiles()\n\tend\n\treturn remainingNumberOfMissiles\nend\n\nfunction SkynetIADSAbstractRadarElement:getInitialNumberOfMissiles()\n\tlocal initalNumberOfMissiles = 0\n\tfor i = 1, #self.launchers do\n\t\tlocal launcher = self.launchers[i]\n\t\tinitalNumberOfMissiles = launcher:getInitialNumberOfMissiles() + initalNumberOfMissiles\n\tend\n\treturn initalNumberOfMissiles\nend\n\nfunction SkynetIADSAbstractRadarElement:getRemainingNumberOfShells()\n\tlocal remainingNumberOfShells = 0\n\tfor i = 1, #self.launchers do\n\t\tlocal launcher = self.launchers[i]\n\t\tremainingNumberOfShells = remainingNumberOfShells + launcher:getRemainingNumberOfShells()\n\tend\n\treturn remainingNumberOfShells\nend\n\nfunction SkynetIADSAbstractRadarElement:getInitialNumberOfShells()\n\tlocal initialNumberOfShells = 0\n\tfor i = 1, #self.launchers do\n\t\tlocal launcher = self.launchers[i]\n\t\tinitialNumberOfShells = initialNumberOfShells + launcher:getInitialNumberOfShells()\n\tend\n\treturn initialNumberOfShells\nend\n\nfunction SkynetIADSAbstractRadarElement:hasRemainingAmmo()\n\t--the launcher check is due to ew radars they have no launcher and no ammo and therefore are never out of ammo\n\treturn ( #self.launchers == 0 ) or ((self:getRemainingNumberOfMissiles() > 0 ) or ( self:getRemainingNumberOfShells() > 0 ) )\nend\n\nfunction SkynetIADSAbstractRadarElement:getHARMDetectionChance()\n\treturn self.harmDetectionChance\nend\n\nfunction SkynetIADSAbstractRadarElement:setHARMDetectionChance(chance)\n\tif chance and chance >= 0 and chance <= 100 then\n\t\tself.harmDetectionChance = chance\n\tend\n\treturn self\nend\n\nfunction SkynetIADSAbstractRadarElement:setupElements()\n\tlocal numUnits = #self:getUnitsToAnalyse()\n\tfor typeName, dataType in pairs(SkynetIADS.database) do\n\t\tlocal hasSearchRadar = false\n\t\tlocal hasTrackingRadar = false\n\t\tlocal hasLauncher = false\n\t\tself.searchRadars = {}\n\t\tself.trackingRadars = {}\n\t\tself.launchers = {}\n\t\tfor entry, unitData in pairs(dataType) do\n\t\t\tif entry == 'searchRadar' then\n\t\t\t\tself:analyseAndAddUnit(SkynetIADSSAMSearchRadar, self.searchRadars, unitData)\n\t\t\t\thasSearchRadar = true\n\t\t\tend\n\t\t\tif entry == 'launchers' then\n\t\t\t\tself:analyseAndAddUnit(SkynetIADSSAMLauncher, self.launchers, unitData)\n\t\t\t\thasLauncher = true\n\t\t\tend\n\t\t\tif entry == 'trackingRadar' then\n\t\t\t\tself:analyseAndAddUnit(SkynetIADSSAMTrackingRadar, self.trackingRadars, unitData)\n\t\t\t\thasTrackingRadar = true\n\t\t\tend\n\t\tend\n\t\t\n\t\t--this check ensures a unit or group has all required elements for the specific sam or ew type:\n\t\tif (hasLauncher and hasSearchRadar and hasTrackingRadar and #self.launchers > 0 and #self.searchRadars > 0  and #self.trackingRadars > 0 ) \n\t\t\tor (hasSearchRadar and hasLauncher and #self.searchRadars > 0 and #self.launchers > 0) then\n\t\t\tself:setHARMDetectionChance(dataType['harm_detection_chance'])\n\t\t\tself.dataBaseSupportedTypesCanEngageHARM = dataType['can_engage_harm'] \n\t\t\tself:setCanEngageHARM(self.dataBaseSupportedTypesCanEngageHARM)\n\t\t\tlocal natoName = dataType['name']['NATO']\n\t\t\tself:buildNatoName(natoName)\n\t\t\tbreak\n\t\tend\t\n\tend\nend\n\nfunction SkynetIADSAbstractRadarElement:setCanEngageHARM(canEngage)\n\tif canEngage == true or canEngage == false then\n\t\tself.canEngageHARM = canEngage\n\t\tif ( canEngage == true and self:getCanEngageAirWeapons() == false ) then\n\t\t\tself:setCanEngageAirWeapons(true)\n\t\tend\n\tend\n\treturn self\nend\n\nfunction SkynetIADSAbstractRadarElement:getCanEngageHARM()\n\treturn self.canEngageHARM\nend\n\nfunction SkynetIADSAbstractRadarElement:setCanEngageAirWeapons(engageAirWeapons)\n\tif self:isDestroyed() == false then\n\t\tlocal controller = self:getDCSRepresentation():getController()\n\t\tif ( engageAirWeapons == true ) then\n\t\t\tcontroller:setOption(AI.Option.Ground.id.ENGAGE_AIR_WEAPONS, true)\n\t\t\t--its important that we set var to true here, to prevent recursion in setCanEngageHARM\n\t\t\tself.engageAirWeapons = true\n\t\t\t--we set the original value we got when loading info about the SAM site\n\t\t\tself:setCanEngageHARM(self.dataBaseSupportedTypesCanEngageHARM)\n\t\telse\n\t\t\tcontroller:setOption(AI.Option.Ground.id.ENGAGE_AIR_WEAPONS, false)\n\t\t\tself:setCanEngageHARM(false)\n\t\t\tself.engageAirWeapons = false\n\t\tend\n\tend\n\treturn self\nend\n\nfunction SkynetIADSAbstractRadarElement:getCanEngageAirWeapons()\n\treturn self.engageAirWeapons\nend\n\nfunction SkynetIADSAbstractRadarElement:buildNatoName(natoName)\n\t--we shorten the SA-XX names and don't return their code names eg goa, gainful..\n\tlocal pos = natoName:find(\" \")\n\tlocal prefix = natoName:sub(1, 2)\n\tif string.lower(prefix) == 'sa' and pos ~= nil then\n\t\tself.natoName = natoName:sub(1, (pos-1))\n\telse\n\t\tself.natoName = natoName\n\tend\nend\n\nfunction SkynetIADSAbstractRadarElement:analyseAndAddUnit(class, tableToAdd, unitData)\n\tlocal units = self:getUnitsToAnalyse()\n\tfor i = 1, #units do\n\t\tlocal unit = units[i]\n\t\tself:buildSingleUnit(unit, class, tableToAdd, unitData)\n\tend\nend\n\nfunction SkynetIADSAbstractRadarElement:buildSingleUnit(unit, class, tableToAdd, unitData)\n\tlocal unitTypeName = unit:getTypeName()\n\tfor unitName, unitPerformanceData in pairs(unitData) do\n\t\tif unitName == unitTypeName then\n\t\t\tsamElement = class:create(unit)\n\t\t\tsamElement:setupRangeData()\n\t\t\ttable.insert(tableToAdd, samElement)\n\t\tend\n\tend\nend\n\nfunction SkynetIADSAbstractRadarElement:getController()\n\tlocal dcsRepresentation = self:getDCSRepresentation()\n\tif dcsRepresentation:isExist() then\n\t\treturn dcsRepresentation:getController()\n\telse\n\t\treturn nil\n\tend\nend\n\nfunction SkynetIADSAbstractRadarElement:getLaunchers()\n\treturn self.launchers\nend\n\nfunction SkynetIADSAbstractRadarElement:getSearchRadars()\n\treturn self.searchRadars\nend\n\nfunction SkynetIADSAbstractRadarElement:getTrackingRadars()\n\treturn self.trackingRadars\nend\n\nfunction SkynetIADSAbstractRadarElement:getRadars()\n\tlocal radarUnits = {}\t\n\tfor i = 1, #self.searchRadars do\n\t\ttable.insert(radarUnits, self.searchRadars[i])\n\tend\t\n\tfor i = 1, #self.trackingRadars do\n\t\ttable.insert(radarUnits, self.trackingRadars[i])\n\tend\n\treturn radarUnits\nend\n\nfunction SkynetIADSAbstractRadarElement:setGoLiveRangeInPercent(percent)\n\tif percent ~= nil then\n\t\tself.firingRangePercent = percent\t\n\t\tfor i = 1, #self.launchers do\n\t\t\tlocal launcher = self.launchers[i]\n\t\t\tlauncher:setFiringRangePercent(self.firingRangePercent)\n\t\tend\n\t\tfor i = 1, #self.searchRadars do\n\t\t\tlocal radar = self.searchRadars[i]\n\t\t\tradar:setFiringRangePercent(self.firingRangePercent)\n\t\tend\n\tend\n\treturn self\nend\n\nfunction SkynetIADSAbstractRadarElement:getGoLiveRangeInPercent()\n\treturn self.firingRangePercent\nend\n\nfunction SkynetIADSAbstractRadarElement:setEngagementZone(engagementZone)\n\tif engagementZone == SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE then\n\t\tself.goLiveRange = engagementZone\n\telseif engagementZone == SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE then\n\t\tself.goLiveRange = engagementZone\n\tend\n\treturn self\nend\n\nfunction SkynetIADSAbstractRadarElement:getEngagementZone()\n\treturn self.goLiveRange\nend\n\nfunction SkynetIADSAbstractRadarElement:goLive()\n\tif ( self.aiState == false and self:hasWorkingPowerSource() and self.harmSilenceID == nil) \n\tand (self:hasRemainingAmmo() == true  )\n\tthen\n\t\tif self:isDestroyed() == false then\n\t\t\tlocal  cont = self:getController()\n\t\t\tcont:setOnOff(true)\n\t\t\tcont:setOption(AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.RED)\t\n\t\t\tcont:setOption(AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_FREE)\n\t\t\tself:getDCSRepresentation():enableEmission(true)\n\t\t\tself.goLiveTime = timer.getTime()\n\t\t\tself.aiState = true\n\t\tend\n\t\tself:pointDefencesStopActingAsEW()\n\t\tif  self.iads:getDebugSettings().radarWentLive then\n\t\t\tself.iads:printOutputToLog(\"GOING LIVE: \"..self:getDescription())\n\t\tend\n\t\tself:scanForHarms()\n\tend\nend\n\nfunction SkynetIADSAbstractRadarElement:pointDefencesStopActingAsEW()\n\tfor i = 1, #self.pointDefences do\n\t\tlocal pointDefence = self.pointDefences[i]\n\t\tpointDefence:setActAsEW(false)\n\tend\nend\n\n\nfunction SkynetIADSAbstractRadarElement:goDark()\n\tif (self:hasWorkingPowerSource() == false) or ( self.aiState == true ) \n\tand (self.harmSilenceID ~= nil or ( self.harmSilenceID == nil and #self:getDetectedTargets() == 0 and self:hasMissilesInFlight() == false) or ( self.harmSilenceID == nil and #self:getDetectedTargets() > 0 and self:hasMissilesInFlight() == false and self:hasRemainingAmmo() == false ) )\t\n\tthen\n\t\tif self:isDestroyed() == false then\n\t\t\tself:getDCSRepresentation():enableEmission(false)\n\t\tend\n\t\t-- point defence will only go live if the Radar Emitting site it is protecting goes dark and this is due to a it defending against a HARM\n\t\tif (self.harmSilenceID ~= nil) then\n\t\t\tself:pointDefencesGoLive()\n\t\t\tif self:isDestroyed() == false then\n\t\t\t\t--if site goes dark due to HARM we turn off AI, this is due to a bug in DCS multiplayer where the harm will find its way to the radar emitter if just setEmissions is set to false\n\t\t\t\tlocal controller = self:getController()\n\t\t\t\tcontroller:setOnOff(false)\n\t\t\tend\n\t\tend\n\t\tself.aiState = false\n\t\tself:stopScanningForHARMs()\n\t\tself.cachedTargets = {}\n\t\tif self.iads:getDebugSettings().radarWentDark then\n\t\t\tself.iads:printOutputToLog(\"GOING DARK: \"..self:getDescription())\n\t\tend\n\tend\nend\n\nfunction SkynetIADSAbstractRadarElement:pointDefencesGoLive()\n\tlocal setActive = false\n\tfor i = 1, #self.pointDefences do\n\t\tlocal pointDefence = self.pointDefences[i]\n\t\tif ( pointDefence:getActAsEW() == false ) then\n\t\t\tsetActive = true\n\t\t\tpointDefence:setActAsEW(true)\n\t\tend\n\tend\n\treturn setActive\nend\n\nfunction SkynetIADSAbstractRadarElement:isActive()\n\treturn self.aiState\nend\n\nfunction SkynetIADSAbstractRadarElement:isTargetInRange(target)\n\n\tlocal isSearchRadarInRange = false\n\tlocal isTrackingRadarInRange = false\n\tlocal isLauncherInRange = false\n\t\n\tlocal isSearchRadarInRange = ( #self.searchRadars == 0 )\n\tfor i = 1, #self.searchRadars do\n\t\tlocal searchRadar = self.searchRadars[i]\n\t\tif searchRadar:isInRange(target) then\n\t\t\tisSearchRadarInRange = true\n\t\t\tbreak\n\t\tend\n\tend\n\t\n\tif self.goLiveRange == SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE then\n\t\t\n\t\tisLauncherInRange = ( #self.launchers == 0 )\n\t\tfor i = 1, #self.launchers do\n\t\t\tlocal launcher = self.launchers[i]\n\t\t\tif launcher:isInRange(target) then\n\t\t\t\tisLauncherInRange = true\n\t\t\t\tbreak\n\t\t\tend\n\t\tend\n\t\t\n\t\tisTrackingRadarInRange = ( #self.trackingRadars == 0 )\n\t\tfor i = 1, #self.trackingRadars do\n\t\t\tlocal trackingRadar = self.trackingRadars[i]\n\t\t\tif trackingRadar:isInRange(target) then\n\t\t\t\tisTrackingRadarInRange = true\n\t\t\t\tbreak\n\t\t\tend\n\t\tend\n\telse\n\t\tisLauncherInRange = true\n\t\tisTrackingRadarInRange = true\n\tend\n\treturn  (isSearchRadarInRange and isTrackingRadarInRange and isLauncherInRange )\nend\n\nfunction SkynetIADSAbstractRadarElement:isInRadarDetectionRangeOf(abstractRadarElement)\n\tlocal radars = self:getRadars()\n\tlocal abstractRadarElementRadars = abstractRadarElement:getRadars()\n\tfor i = 1, #radars do\n\t\tlocal radar = radars[i]\n\t\tfor j = 1, #abstractRadarElementRadars do\n\t\t\tlocal abstractRadarElementRadar = abstractRadarElementRadars[j]\n\t\t\tif  abstractRadarElementRadar:isExist() and radar:isExist() then\n\t\t\t\tlocal distance = self:getDistanceToUnit(radar:getDCSRepresentation():getPosition().p, abstractRadarElementRadar:getDCSRepresentation():getPosition().p)\t\n\t\t\t\tif abstractRadarElementRadar:getMaxRangeFindingTarget() >= distance then\n\t\t\t\t\treturn true\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\tend\n\treturn false\nend\n\nfunction SkynetIADSAbstractRadarElement:getDistanceToUnit(unitPosA, unitPosB)\n\treturn mist.utils.round(mist.utils.get2DDist(unitPosA, unitPosB, 0))\nend\n\nfunction SkynetIADSAbstractRadarElement:hasWorkingRadar()\n\tlocal radars = self:getRadars()\n\tfor i = 1, #radars do\n\t\tlocal radar = radars[i]\n\t\tif radar:isRadarWorking() == true then\n\t\t\treturn true\n\t\tend\n\tend\n\treturn false\nend\n\nfunction SkynetIADSAbstractRadarElement:jam(successProbability)\n\t\tif self:isDestroyed() == false then\n\t\t\tlocal controller = self:getController()\n\t\t\tlocal probability = math.random(1, 100)\n\t\t\tif self.iads:getDebugSettings().jammerProbability then\n\t\t\t\tself.iads:printOutputToLog(\"JAMMER: \"..self:getDescription()..\": Probability: \"..successProbability)\n\t\t\tend\n\t\t\tif successProbability > probability then\n\t\t\t\tcontroller:setOption(AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD)\n\t\t\t\tif self.iads:getDebugSettings().jammerProbability then\n\t\t\t\t\tself.iads:printOutputToLog(\"JAMMER: \"..self:getDescription()..\": jammed, setting to weapon hold\")\n\t\t\t\tend\n\t\t\telse\n\t\t\t\tcontroller:setOption(AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_FREE)\n\t\t\t\tif self.iads:getDebugSettings().jammerProbability then\n\t\t\t\t\tself.iads:printOutputToLog(\"JAMMER: \"..self:getDescription()..\": jammed, setting to weapon free\")\n\t\t\t\tend\n\t\t\tend\n\t\t\tself.lastJammerUpdate = timer:getTime()\n\t\tend\nend\n\nfunction SkynetIADSAbstractRadarElement:scanForHarms()\n\tself:stopScanningForHARMs()\n\tself.harmScanID = mist.scheduleFunction(SkynetIADSAbstractRadarElement.evaluateIfTargetsContainHARMs, {self}, 1, 2)\nend\n\nfunction SkynetIADSAbstractRadarElement:isScanningForHARMs()\n\treturn self.harmScanID ~= nil\nend\n\nfunction SkynetIADSAbstractRadarElement:isDefendingHARM()\n\treturn self.harmSilenceID ~= nil\nend\n\nfunction SkynetIADSAbstractRadarElement:stopScanningForHARMs()\n\tmist.removeFunction(self.harmScanID)\n\tself.harmScanID = nil\nend\n\nfunction SkynetIADSAbstractRadarElement:goSilentToEvadeHARM(timeToImpact)\n\tself:finishHarmDefence(self)\n\tif ( timeToImpact == nil ) then\n\t\ttimeToImpact = 0\n\tend\n\t\n\tself.minHarmShutdownTime = self:calculateMinimalShutdownTimeInSeconds(timeToImpact)\n\tself.maxHarmShutDownTime = self:calculateMaximalShutdownTimeInSeconds(self.minHarmShutdownTime)\n\t\n\tself.harmShutdownTime = self:calculateHARMShutdownTime()\n\tif self.iads:getDebugSettings().harmDefence then\n\t\tself.iads:printOutputToLog(\"HARM DEFENCE SHUTTING DOWN: \"..self:getDCSName()..\" | FOR: \"..self.harmShutdownTime..\" seconds | TTI: \"..timeToImpact)\n\tend\n\tself.harmSilenceID = mist.scheduleFunction(SkynetIADSAbstractRadarElement.finishHarmDefence, {self}, timer.getTime() + self.harmShutdownTime, 1)\n\tself:goDark()\nend\n\nfunction SkynetIADSAbstractRadarElement:getHARMShutdownTime()\n\treturn self.harmShutdownTime\nend\n\nfunction SkynetIADSAbstractRadarElement:calculateHARMShutdownTime()\n\tlocal shutDownTime = math.random(self.minHarmShutdownTime, self.maxHarmShutDownTime)\n\treturn shutDownTime\nend\n\nfunction SkynetIADSAbstractRadarElement.finishHarmDefence(self)\n\tmist.removeFunction(self.harmSilenceID)\n\tself.harmSilenceID = nil\n\tself.harmShutdownTime = 0\n\t\n\tif ( self:getAutonomousState() == true ) then\n\t\tself:goAutonomous()\n\tend\t\nend\n\nfunction SkynetIADSAbstractRadarElement:getDetectedTargets()\n\tif ( timer.getTime() - self.cachedTargetsCurrentAge > self.cachedTargetsMaxAge ) or ( timer.getTime() - self.goLiveTime < self.noCacheActiveForSecondsAfterGoLive ) then\n\t\tself.cachedTargets = {}\n\t\tself.cachedTargetsCurrentAge = timer.getTime()\n\t\tif self:hasWorkingPowerSource() and self:isDestroyed() == false then\n\t\t\tlocal targets = self:getController():getDetectedTargets(Controller.Detection.RADAR)\n\t\t\tfor i = 1, #targets do\n\t\t\t\tlocal target = targets[i]\n\t\t\t\t-- there are cases when a destroyed object is still visible as a target to the radar, don't add it, will cause errors everywhere the dcs object is accessed\n\t\t\t\tif target.object then\n\t\t\t\t\tlocal iadsTarget = SkynetIADSContact:create(target, self)\n\t\t\t\t\tiadsTarget:refresh()\n\t\t\t\t\tif self:isTargetInRange(iadsTarget) then\n\t\t\t\t\t\ttable.insert(self.cachedTargets, iadsTarget)\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\tend\n\treturn self.cachedTargets\nend\n\nfunction SkynetIADSAbstractRadarElement:getSecondsToImpact(distanceNM, speedKT)\n\tlocal tti = 0\n\tif speedKT > 0 then\n\t\ttti = mist.utils.round((distanceNM / speedKT) * 3600, 0)\n\t\tif tti < 0 then\n\t\t\ttti = 0\n\t\tend\n\tend\n\treturn tti\nend\n\nfunction SkynetIADSAbstractRadarElement:getDistanceInMetersToContact(radarUnit, point)\n\treturn mist.utils.round(mist.utils.get3DDist(radarUnit:getPosition().p, point), 0)\nend\n\nfunction SkynetIADSAbstractRadarElement:calculateMinimalShutdownTimeInSeconds(timeToImpact)\n\treturn timeToImpact + self.minHarmPresetShutdownTime\nend\n\nfunction SkynetIADSAbstractRadarElement:calculateMaximalShutdownTimeInSeconds(minShutdownTime)\t\n\treturn minShutdownTime + mist.random(1, self.maxHarmPresetShutdownTime)\nend\n\nfunction SkynetIADSAbstractRadarElement:calculateImpactPoint(target, distanceInMeters)\n\t-- distance needs to be incremented by a certain value for ip calculation to work, check why presumably due to rounding errors in the previous distance calculation\n\treturn land.getIP(target:getPosition().p, target:getPosition().x, distanceInMeters + 50)\nend\n\nfunction SkynetIADSAbstractRadarElement:shallReactToHARM()\n\treturn self.harmDetectionChance >=  math.random(1, 100)\nend\n\n-- will only check for missiles, if DCS ads AAA than can engage HARMs then this code must be updated:\nfunction SkynetIADSAbstractRadarElement:shallIgnoreHARMShutdown()\n\tlocal numOfHarms = self:getNumberOfObjectsItentifiedAsHARMS()\n\t--[[\n\tself.iads:printOutputToLog(\"Self enough launchers: \"..tostring(self:hasEnoughLaunchersToEngageMissiles(numOfHarms)))\n\tself.iads:printOutputToLog(\"Self enough missiles: \"..tostring(self:hasRemainingAmmoToEngageMissiles(numOfHarms)))\n\tself.iads:printOutputToLog(\"PD enough missiles: \"..tostring(self:pointDefencesHaveRemainingAmmo(numOfHarms)))\n\tself.iads:printOutputToLog(\"PD enough launchers: \"..tostring(self:pointDefencesHaveEnoughLaunchers(numOfHarms)))\n\t--]]\n\treturn ( ((self:hasEnoughLaunchersToEngageMissiles(numOfHarms) and self:hasRemainingAmmoToEngageMissiles(numOfHarms) and self:getCanEngageHARM()) or (self:pointDefencesHaveRemainingAmmo(numOfHarms) and self:pointDefencesHaveEnoughLaunchers(numOfHarms))))\nend\n\nfunction SkynetIADSAbstractRadarElement:informOfHARM(harmContact)\n\tlocal radars = self:getRadars()\n\t\tfor j = 1, #radars do\n\t\t\tlocal radar = radars[j]\n\t\t\tif radar:isExist() then\n\t\t\t\tlocal distanceNM =  mist.utils.metersToNM(self:getDistanceInMetersToContact(radar, harmContact:getPosition().p))\n\t\t\t\tlocal harmToSAMHeading = mist.utils.toDegree(mist.utils.getHeadingPoints(harmContact:getPosition().p, radar:getPosition().p))\n\t\t\t\tlocal harmToSAMAspect = self:calculateAspectInDegrees(harmContact:getMagneticHeading(), harmToSAMHeading)\n\t\t\t\tlocal speedKT = harmContact:getGroundSpeedInKnots(0)\n\t\t\t\tlocal secondsToImpact = self:getSecondsToImpact(distanceNM, speedKT)\n\t\t\t\t--TODO: use tti instead of distanceNM?\n\t\t\t\t-- when iterating through the radars, store shortest tti and work with that value??\n\t\t\t\tif ( harmToSAMAspect < SkynetIADSAbstractRadarElement.HARM_TO_SAM_ASPECT and distanceNM < SkynetIADSAbstractRadarElement.HARM_LOOKAHEAD_NM ) then\n\t\t\t\t\tself:addObjectIdentifiedAsHARM(harmContact)\n\t\t\t\t\tif ( #self:getPointDefences() > 0 and self:pointDefencesGoLive() == true and self.iads:getDebugSettings().harmDefence ) then\n\t\t\t\t\t\t\tself.iads:printOutputToLog(\"POINT DEFENCES GOING LIVE FOR: \"..self:getDCSName()..\" | TTI: \"..secondsToImpact)\n\t\t\t\t\tend\n\t\t\t\t\t--self.iads:printOutputToLog(\"Ignore HARM shutdown: \"..tostring(self:shallIgnoreHARMShutdown()))\n\t\t\t\t\tif ( self:getIsAPointDefence() == false and ( self:isDefendingHARM() == false or ( self:getHARMShutdownTime() < secondsToImpact ) ) and self:shallIgnoreHARMShutdown() == false) then\n\t\t\t\t\t\tself:goSilentToEvadeHARM(secondsToImpact)\n\t\t\t\t\t\tbreak\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\tend\nend\n\nfunction SkynetIADSAbstractElement:addObjectIdentifiedAsHARM(harmContact)\n\tself:insertToTableIfNotAlreadyAdded(self.objectsIdentifiedAsHarms, harmContact)\nend\n\nfunction SkynetIADSAbstractRadarElement:calculateAspectInDegrees(harmHeading, harmToSAMHeading)\n\t\tlocal aspect = harmHeading - harmToSAMHeading\n\t\tif ( aspect < 0 ) then\n\t\t\taspect = -1 * aspect\n\t\tend\n\t\tif aspect > 180 then\n\t\t\taspect = 360 - aspect\n\t\tend\n\t\treturn mist.utils.round(aspect)\nend\n\nfunction SkynetIADSAbstractRadarElement:getNumberOfObjectsItentifiedAsHARMS()\n\treturn #self.objectsIdentifiedAsHarms\nend\n\nfunction SkynetIADSAbstractRadarElement:cleanUpOldObjectsIdentifiedAsHARMS()\n\tlocal newHARMS = {}\n\tfor i = 1, #self.objectsIdentifiedAsHarms do\n\t\tlocal harmContact = self.objectsIdentifiedAsHarms[i]\n\t\tif harmContact:getAge() < self.objectsIdentifiedAsHarmsMaxTargetAge then\n\t\t\ttable.insert(newHARMS, harmContact)\n\t\tend\n\tend\n\t--stop point defences acting as ew (always on), will occur if activated via evaluateIfTargetsContainHARMs()\n\t--if in this iteration all harms where cleared we turn of the point defence. But in any other cases we dont turn of point defences, that interferes with other parts of the iads\n\t-- when setting up the iads (letting pds go to read state)\n\tif (#newHARMS == 0 and self:getNumberOfObjectsItentifiedAsHARMS() > 0 ) then\n\t\tself:pointDefencesStopActingAsEW()\n\tend\n\tself.objectsIdentifiedAsHarms = newHARMS\nend\n\n\nfunction SkynetIADSAbstractRadarElement.evaluateIfTargetsContainHARMs(self)\n\n\t--if an emitter dies the SAM site being jammed will revert back to normal operation:\n\tif self.lastJammerUpdate > 0 and ( timer:getTime() - self.lastJammerUpdate ) > 10 then\n\t\tself:jam(0)\n\t\tself.lastJammerUpdate = 0\n\tend\n\t\n\t--we use the regular interval of this method to update to other states: \n\tself:updateMissilesInFlight()\t\n\tself:cleanUpOldObjectsIdentifiedAsHARMS()\nend\n\nend\n"
  },
  {
    "path": "skynet-iads-source/skynet-iads-awacs-radar.lua",
    "content": "do\n--this class is currently used for AWACS and Ships, at a latter date a separate class for ships could be created, currently not needed\nSkynetIADSAWACSRadar = {}\nSkynetIADSAWACSRadar = inheritsFrom(SkynetIADSAbstractRadarElement)\n\nfunction SkynetIADSAWACSRadar:create(radarUnit, iads)\n\tlocal instance = self:superClass():create(radarUnit, iads)\n\tsetmetatable(instance, self)\n\tself.__index = self\n\tinstance.lastUpdatePosition = nil\n\tinstance.natoName = radarUnit:getTypeName()\n\treturn instance\nend\n\nfunction SkynetIADSAWACSRadar:setupElements()\n\tlocal unit = self:getDCSRepresentation()\n\tlocal radar = SkynetIADSSAMSearchRadar:create(unit)\n\tradar:setupRangeData()\n\ttable.insert(self.searchRadars, radar)\nend\n\n\n-- AWACs will not scan for HARMS\nfunction SkynetIADSAWACSRadar:scanForHarms()\n\t\nend\n\nfunction SkynetIADSAWACSRadar:getMaxAllowedMovementForAutonomousUpdateInNM()\n\t--local radarRange = mist.utils.metersToNM(self.searchRadars[1]:getMaxRangeFindingTarget())\n\t--return mist.utils.round(radarRange / 10)\n\t--fixed to 10 nm miles to better fit small SAM sites\n\treturn 10\nend\n\nfunction SkynetIADSAWACSRadar:isUpdateOfAutonomousStateOfSAMSitesRequired()\n\tlocal isUpdateRequired = self:getDistanceTraveledSinceLastUpdate() > self:getMaxAllowedMovementForAutonomousUpdateInNM()\n\tif isUpdateRequired then\n\t\tself.lastUpdatePosition = nil\n\tend\n\treturn isUpdateRequired\nend\n\nfunction SkynetIADSAWACSRadar:getDistanceTraveledSinceLastUpdate()\n\tlocal currentPosition = nil\n\tif self.lastUpdatePosition == nil and self:getDCSRepresentation():isExist() then\n\t\tself.lastUpdatePosition = self:getDCSRepresentation():getPosition().p\n\tend\n\tif self:getDCSRepresentation():isExist() then\n\t\tcurrentPosition = self:getDCSRepresentation():getPosition().p\n\tend\n\treturn mist.utils.round(mist.utils.metersToNM(self:getDistanceToUnit(self.lastUpdatePosition, currentPosition)))\nend\n\nend\n\n"
  },
  {
    "path": "skynet-iads-source/skynet-iads-command-center.lua",
    "content": "do\nSkynetIADSCommandCenter = {}\nSkynetIADSCommandCenter = inheritsFrom(SkynetIADSAbstractRadarElement)\n\nfunction SkynetIADSCommandCenter:create(commandCenter, iads)\n\tlocal instance = self:superClass():create(commandCenter, iads)\n\tsetmetatable(instance, self)\n\tself.__index = self\n\tinstance.natoName = \"COMMAND CENTER\"\n\treturn instance\nend\n\nfunction SkynetIADSCommandCenter:goDark()\n\nend\n\nfunction SkynetIADSCommandCenter:goLive()\n\nend\n\nend\n"
  },
  {
    "path": "skynet-iads-source/skynet-iads-contact.lua",
    "content": "do\n\nSkynetIADSContact = {}\nSkynetIADSContact = inheritsFrom(SkynetIADSAbstractDCSObjectWrapper)\n\nSkynetIADSContact.CLIMB = \"CLIMB\"\nSkynetIADSContact.DESCEND = \"DESCEND\"\n\nSkynetIADSContact.HARM = \"HARM\"\nSkynetIADSContact.NOT_HARM = \"NOT_HARM\"\nSkynetIADSContact.HARM_UNKNOWN = \"HARM_UNKNOWN\"\n\nfunction SkynetIADSContact:create(dcsRadarTarget, abstractRadarElementDetected)\n\tlocal instance = self:superClass():create(dcsRadarTarget.object)\n\tsetmetatable(instance, self)\n\tself.__index = self\n\tinstance.abstractRadarElementsDetected = {}\n\ttable.insert(instance.abstractRadarElementsDetected, abstractRadarElementDetected)\n\tinstance.firstContactTime = timer.getAbsTime()\n\tinstance.lastTimeSeen = 0\n\tinstance.dcsRadarTarget = dcsRadarTarget\n\tinstance.position = instance:getDCSRepresentation():getPosition()\n\tinstance.numOfTimesRefreshed = 0\n\tinstance.speed = 0\n\tinstance.harmState = SkynetIADSContact.HARM_UNKNOWN\n\tinstance.simpleAltitudeProfile = {}\n\treturn instance\nend\n\nfunction SkynetIADSContact:setHARMState(state)\n\tself.harmState = state\nend\n\nfunction SkynetIADSContact:getHARMState()\n\treturn self.harmState\nend\n\nfunction SkynetIADSContact:isIdentifiedAsHARM()\n\treturn self.harmState == SkynetIADSContact.HARM\nend\n\nfunction SkynetIADSContact:isHARMStateUnknown()\n\treturn self.harmState == SkynetIADSContact.HARM_UNKNOWN\nend\n\nfunction SkynetIADSContact:getMagneticHeading()\n\tif ( self:isExist() ) then\n\t\treturn mist.utils.round(mist.utils.toDegree(mist.getHeading(self:getDCSRepresentation())))\n\telse\n\t\treturn -1\n\tend\nend\n\nfunction SkynetIADSContact:getAbstractRadarElementsDetected()\n\treturn self.abstractRadarElementsDetected\nend\n\nfunction SkynetIADSContact:addAbstractRadarElementDetected(radar)\n\tself:insertToTableIfNotAlreadyAdded(self.abstractRadarElementsDetected, radar)\nend\n\nfunction SkynetIADSContact:isTypeKnown()\n\treturn self.dcsRadarTarget.type\nend\n\nfunction SkynetIADSContact:isDistanceKnown()\n\treturn self.dcsRadarTarget.distance\nend\n\nfunction SkynetIADSContact:getTypeName()\n\tif self:isIdentifiedAsHARM() then\n\t\treturn SkynetIADSContact.HARM\n\tend\n\tif self:getDCSRepresentation() ~= nil then\n\t\tlocal category = self:getDCSRepresentation():getCategory()\n\t\tif category == Object.Category.UNIT then\n\t\t\treturn self.typeName\n\t\tend\n\tend\n\treturn \"UNKNOWN\"\nend\n\nfunction SkynetIADSContact:getPosition()\n\treturn self.position\nend\n\nfunction SkynetIADSContact:getGroundSpeedInKnots(decimals)\n\tif decimals == nil then\n\t\tdecimals = 2\n\tend\n\treturn mist.utils.round(self.speed, decimals)\nend\n\nfunction SkynetIADSContact:getHeightInFeetMSL()\n\tif self:isExist() then\n\t\treturn mist.utils.round(mist.utils.metersToFeet(self:getDCSRepresentation():getPosition().p.y), 0)\n\telse\n\t\treturn 0\n\tend\nend\n\nfunction SkynetIADSContact:getDesc()\n\tif self:isExist() then\n\t\treturn self:getDCSRepresentation():getDesc()\n\telse\n\t\treturn {}\n\tend\nend\n\nfunction SkynetIADSContact:getNumberOfTimesHitByRadar()\n\treturn self.numOfTimesRefreshed\nend\n\nfunction SkynetIADSContact:refresh()\n\tif self:isExist() then\n\t\tlocal timeDelta = (timer.getAbsTime() - self.lastTimeSeen)\n\t\tif timeDelta > 0 then\n\t\t\tself.numOfTimesRefreshed = self.numOfTimesRefreshed + 1\n\t\t\tlocal distance = mist.utils.metersToNM(mist.utils.get2DDist(self.position.p, self:getDCSRepresentation():getPosition().p))\n\t\t\tlocal hours = timeDelta / 3600\n\t\t\tself.speed = (distance / hours)\n\t\t\tself:updateSimpleAltitudeProfile()\n\t\t\tself.position = self:getDCSRepresentation():getPosition()\n\t\tend \n\tend\n\tself.lastTimeSeen = timer.getAbsTime()\nend\n\nfunction SkynetIADSContact:updateSimpleAltitudeProfile()\n\tlocal currentAltitude = self:getDCSRepresentation():getPosition().p.y\n\t\n\tlocal previousPath = \"\"\n\tif #self.simpleAltitudeProfile > 0 then\n\t\tpreviousPath = self.simpleAltitudeProfile[#self.simpleAltitudeProfile]\n\tend\n\t\n\tif self.position.p.y > currentAltitude and previousPath ~= SkynetIADSContact.DESCEND then\n\t\ttable.insert(self.simpleAltitudeProfile, SkynetIADSContact.DESCEND)\n\telseif self.position.p.y < currentAltitude and previousPath ~= SkynetIADSContact.CLIMB then\n\t\ttable.insert(self.simpleAltitudeProfile, SkynetIADSContact.CLIMB)\n\tend\nend\n\nfunction SkynetIADSContact:getSimpleAltitudeProfile()\n\treturn self.simpleAltitudeProfile\nend\n\nfunction SkynetIADSContact:getAge()\n\treturn mist.utils.round(timer.getAbsTime() - self.lastTimeSeen)\nend\n\nend\n\n"
  },
  {
    "path": "skynet-iads-source/skynet-iads-early-warning-radar.lua",
    "content": "do\n\nSkynetIADSEWRadar = {}\nSkynetIADSEWRadar = inheritsFrom(SkynetIADSAbstractRadarElement)\n\nfunction SkynetIADSEWRadar:create(radarUnit, iads)\n\tlocal instance = self:superClass():create(radarUnit, iads)\n\tsetmetatable(instance, self)\n\tself.__index = self\n\tinstance.autonomousBehaviour = SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK\n\treturn instance\nend\n\nfunction SkynetIADSEWRadar:setupElements()\n\tlocal unit = self:getDCSRepresentation()\n\tlocal unitType = unit:getTypeName()\n\tfor typeName, dataType in pairs(SkynetIADS.database) do\n\t\tfor entry, unitData in pairs(dataType) do\n\t\t\tif entry == 'searchRadar' then\n\t\t\t\t--buildSingleUnit checks to make sure the EW radar is defined in the Skynet database. If it is not, self.searchRadars will be 0 so no ew radar will be added\n\t\t\t\tself:buildSingleUnit(unit, SkynetIADSSAMSearchRadar, self.searchRadars, unitData)\n\t\t\t\tif #self.searchRadars > 0 then\n\t\t\t\t\tlocal harmDetection = dataType['harm_detection_chance']\n\t\t\t\t\tself:setHARMDetectionChance(harmDetection)\n\t\t\t\t\tif unitData[unitType]['name'] then\n\t\t\t\t\t\tlocal natoName = unitData[unitType]['name']['NATO']\n\t\t\t\t\t\tself:buildNatoName(natoName)\n\t\t\t\t\tend\n\t\t\t\t\treturn\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\tend\nend\n\n--an Early Warning Radar has simplified check to determine if its autonomous or not\nfunction SkynetIADSEWRadar:setToCorrectAutonomousState()\n\tif self:hasActiveConnectionNode() and self:hasWorkingPowerSource() and self.iads:isCommandCenterUsable() then\n\t\tself:resetAutonomousState()\n\t\tself:goLive()\n\tend\n\tif self:hasActiveConnectionNode() == false or self.iads:isCommandCenterUsable() == false then\n\t\tself:goAutonomous()\n\tend\nend\n\nend\n"
  },
  {
    "path": "skynet-iads-source/skynet-iads-harm-detection.lua",
    "content": "do\n\nSkynetIADSHARMDetection = {}\nSkynetIADSHARMDetection.__index = SkynetIADSHARMDetection\n\nSkynetIADSHARMDetection.HARM_THRESHOLD_SPEED_KTS = 800\n\nfunction SkynetIADSHARMDetection:create(iads)\n\tlocal harmDetection = {}\n\tsetmetatable(harmDetection, self)\n\tharmDetection.contacts = {}\n\tharmDetection.iads = iads\n\tharmDetection.contactRadarsEvaluated = {}\n\treturn harmDetection\nend\n\nfunction SkynetIADSHARMDetection:setContacts(contacts)\n\tself.contacts = contacts\nend\n\nfunction SkynetIADSHARMDetection:evaluateContacts()\n\tself:cleanAgedContacts()\n\tfor i = 1, #self.contacts do\n\t\tlocal contact = self.contacts[i]\t\n\t\tlocal groundSpeed  = contact:getGroundSpeedInKnots(0)\n\t\t--if a contact has only been hit by a radar once it's speed is 0\n\t\tif groundSpeed == 0 then\n\t\t\treturn\n\t\tend\n\t\tlocal simpleAltitudeProfile = contact:getSimpleAltitudeProfile()\n\t\tlocal newRadarsToEvaluate = self:getNewRadarsThatHaveDetectedContact(contact)\n\t\t--self.iads:printOutputToLog(contact:getName()..\" new Radars to evaluate: \"..#newRadarsToEvaluate)\n\t\t--self.iads:printOutputToLog(contact:getName()..\" ground speed: \"..groundSpeed)\n\t\tif ( #newRadarsToEvaluate > 0 and contact:isIdentifiedAsHARM() == false and ( groundSpeed > SkynetIADSHARMDetection.HARM_THRESHOLD_SPEED_KTS and #simpleAltitudeProfile <= 2 ) ) then\n\t\t\tlocal detectionProbability = self:getDetectionProbability(newRadarsToEvaluate)\n\t\t\t--self.iads:printOutputToLog(\"DETECTION PROB: \"..detectionProbability)\n\t\t\tif ( self:shallReactToHARM(detectionProbability) ) then\n\t\t\t\tcontact:setHARMState(SkynetIADSContact.HARM)\n\t\t\t\tif (self.iads:getDebugSettings().harmDefence ) then\n\t\t\t\t\tself.iads:printOutputToLog(\"HARM IDENTIFIED: \"..contact:getTypeName()..\" | DETECTION PROBABILITY WAS: \"..detectionProbability..\"%\")\n\t\t\t\tend\n\t\t\telse\n\t\t\t\tcontact:setHARMState(SkynetIADSContact.NOT_HARM)\n\t\t\t\tif (self.iads:getDebugSettings().harmDefence ) then\n\t\t\t\t\tself.iads:printOutputToLog(\"HARM NOT IDENTIFIED: \"..contact:getTypeName()..\" | DETECTION PROBABILITY WAS: \"..detectionProbability..\"%\")\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\t\t\n\t\tif ( #simpleAltitudeProfile > 2 and contact:isIdentifiedAsHARM() ) then\n\t\t\tcontact:setHARMState(SkynetIADSContact.HARM_UNKNOWN)\n\t\t\tif (self.iads:getDebugSettings().harmDefence ) then\n\t\t\t\tself.iads:printOutputToLog(\"CORRECTING HARM STATE: CONTACT IS NOT A HARM: \"..contact:getName())\n\t\t\tend\n\t\tend\n\t\t\n\t\tif ( contact:isIdentifiedAsHARM() ) then\n\t\t\tself:informRadarsOfHARM(contact)\n\t\tend\n\tend\nend\n\nfunction SkynetIADSHARMDetection:cleanAgedContacts()\n\tlocal activeContactRadars = {}\n\tfor contact, radars in pairs (self.contactRadarsEvaluated) do\n\t\tif contact:getAge() < 32 then\n\t\t\tactiveContactRadars[contact] = radars\n\t\tend\n\tend\n\tself.contactRadarsEvaluated = activeContactRadars\nend\n\nfunction SkynetIADSHARMDetection:getNewRadarsThatHaveDetectedContact(contact)\n\tlocal radarsFromContact = contact:getAbstractRadarElementsDetected()\n\tlocal evaluatedRadars = self.contactRadarsEvaluated[contact]\n\tlocal newRadars = {}\n\tif evaluatedRadars == nil then\n\t\tevaluatedRadars = {}\n\t\tself.contactRadarsEvaluated[contact] = evaluatedRadars\n\tend\n\tfor i = 1, #radarsFromContact do\n\t\tlocal contactRadar = radarsFromContact[i]\n\t\tif self:isElementInTable(evaluatedRadars, contactRadar) == false then\n\t\t\ttable.insert(evaluatedRadars, contactRadar)\n\t\t\ttable.insert(newRadars, contactRadar)\n\t\tend\n\tend\n\treturn newRadars\nend\n\nfunction SkynetIADSHARMDetection:isElementInTable(tbl, element)\n\tfor i = 1, #tbl do\n\t\tlocal tblElement = tbl[i]\n\t\tif tblElement == element then\n\t\t\treturn true\n\t\tend\n\tend\n\treturn false\nend\n\nfunction SkynetIADSHARMDetection:informRadarsOfHARM(contact)\n\tlocal samSites = self.iads:getUsableSAMSites()\n\tself:updateRadarsOfSites(samSites, contact)\n\t\n\tlocal ewRadars = self.iads:getUsableEarlyWarningRadars()\n\tself:updateRadarsOfSites(ewRadars, contact)\nend\n\nfunction SkynetIADSHARMDetection:updateRadarsOfSites(sites, contact)\n\tfor i = 1, #sites do\n\t\tlocal site = sites[i]\n\t\tsite:informOfHARM(contact)\n\tend\nend\n\nfunction SkynetIADSHARMDetection:shallReactToHARM(chance)\n\treturn chance >=  math.random(1, 100)\nend\n\nfunction SkynetIADSHARMDetection:getDetectionProbability(radars)\n\tlocal detectionChance = 0\n\tlocal missChance = 100\n\tlocal detection = 0\n\tfor i = 1, #radars do\n\t\tdetection = radars[i]:getHARMDetectionChance()\n\t\tif ( detectionChance == 0 ) then\n\t\t\tdetectionChance = detection\n\t\telse\n\t\t\tdetectionChance = detectionChance + (detection * (missChance / 100))\n\t\tend\t\n\t\tmissChance = 100 - detection\n\tend\n\treturn detectionChance\nend\n\nend\n\n\n"
  },
  {
    "path": "skynet-iads-source/skynet-iads-jammer.lua",
    "content": "do\n\nSkynetIADSJammer = {}\nSkynetIADSJammer.__index = SkynetIADSJammer\n\nfunction SkynetIADSJammer:create(emitter, iads)\n\tlocal jammer = {}\n\tsetmetatable(jammer, SkynetIADSJammer)\n\tjammer.radioMenu = nil\n\tjammer.emitter = emitter\n\tjammer.jammerTaskID = nil\n\tjammer.iads = {iads}\n\tjammer.maximumEffectiveDistanceNM = 200\n\t--jammer probability settings are stored here, visualisation, see: https://docs.google.com/spreadsheets/d/16rnaU49ZpOczPEsdGJ6nfD0SLPxYLEYKmmo4i2Vfoe0/edit#gid=0\n\tjammer.jammerTable = {\n\t\t['SA-2'] = {\n\t\t\t['function'] = function(distanceNauticalMiles) return ( 1.4 ^ distanceNauticalMiles ) + 90 end,\n\t\t\t['canjam'] = true,\n\t\t},\n\t\t['SA-3'] = {\n\t\t\t['function'] = function(distanceNauticalMiles) return ( 1.4 ^ distanceNauticalMiles ) + 80 end,\n\t\t\t['canjam'] = true,\n\t\t},\n\t\t['SA-6'] = {\n\t\t\t['function'] = function(distanceNauticalMiles) return ( 1.4 ^ distanceNauticalMiles ) + 23 end,\n\t\t\t['canjam'] = true,\n\t\t},\n\t\t['SA-8'] = {\n\t\t\t['function'] = function(distanceNauticalMiles) return ( 1.35 ^ distanceNauticalMiles ) + 30 end,\n\t\t\t['canjam'] = true,\n\t\t},\n\t\t['SA-10'] = {\n\t\t\t['function'] = function(distanceNauticalMiles) return ( 1.07 ^ (distanceNauticalMiles / 1.13) ) + 5 end,\n\t\t\t['canjam'] = true,\n\t\t},\n\t\t['SA-11'] = {\n\t\t\t['function'] = function(distanceNauticalMiles) return ( 1.25 ^ distanceNauticalMiles ) + 15 end,\n\t\t\t['canjam'] = true,\n\t\t},\n\t\t['SA-15'] = {\n\t\t\t['function'] = function(distanceNauticalMiles) return ( 1.15 ^ distanceNauticalMiles ) + 5 end,\n\t\t\t['canjam'] = true,\n\t\t},\n\t}\n\treturn jammer\nend\n\nfunction SkynetIADSJammer:masterArmOn()\n\tself:masterArmSafe()\n\tself.jammerTaskID = mist.scheduleFunction(SkynetIADSJammer.runCycle, {self}, 1, 10)\nend\n\nfunction SkynetIADSJammer:addFunction(natoName, jammerFunction)\n\tself.jammerTable[natoName] = {\n\t\t['function'] = jammerFunction,\n\t\t['canjam'] = true\n\t}\nend\n\nfunction SkynetIADSJammer:setMaximumEffectiveDistance(distance)\n\tself.maximumEffectiveDistanceNM = distance\nend\n\nfunction SkynetIADSJammer:disableFor(natoName)\n\tself.jammerTable[natoName]['canjam'] = false\nend\n\nfunction SkynetIADSJammer:isKnownRadarEmitter(natoName)\n\tlocal isActive = false\n\tfor unitName, unit in pairs(self.jammerTable) do\n\t\tif unitName == natoName and unit['canjam'] == true then\n\t\t\tisActive = true\n\t\tend\n\tend\n\treturn isActive\nend\n\nfunction SkynetIADSJammer:addIADS(iads)\n\ttable.insert(self.iads, iads)\nend\n\nfunction SkynetIADSJammer:getSuccessProbability(distanceNauticalMiles, natoName)\n\tlocal probability = 0\n\tlocal jammerSettings = self.jammerTable[natoName]\n\tif jammerSettings ~= nil then\n\t\tprobability = jammerSettings['function'](distanceNauticalMiles)\n\tend\n\treturn probability\nend\n\nfunction SkynetIADSJammer:getDistanceNMToRadarUnit(radarUnit)\n\treturn mist.utils.metersToNM(mist.utils.get3DDist(self.emitter:getPosition().p, radarUnit:getPosition().p))\nend\n\nfunction SkynetIADSJammer.runCycle(self)\n\n\tif self.emitter:isExist() == false then\n\t\tself:masterArmSafe()\n\t\treturn\n\tend\n\n\tfor i = 1, #self.iads do\n\t\tlocal iads = self.iads[i]\n\t\tlocal samSites = iads:getActiveSAMSites()\t\n\t\tfor j = 1, #samSites do\n\t\t\tlocal samSite = samSites[j]\n\t\t\tlocal radars = samSite:getRadars()\n\t\t\tlocal hasLOS = false\n\t\t\tlocal distance = 0\n\t\t\tlocal natoName = samSite:getNatoName()\n\t\t\tfor l = 1, #radars do\n\t\t\t\tlocal radar = radars[l]\n\t\t\t\tdistance = self:getDistanceNMToRadarUnit(radar)\n\t\t\t\t-- I try to emulate the system as it would work in real life, so a jammer can only jam a SAM site if has line of sight to at least one radar in the group\n\t\t\t\tif self:isKnownRadarEmitter(natoName) and self:hasLineOfSightToRadar(radar) and distance <= self.maximumEffectiveDistanceNM then\n\t\t\t\t\tif iads:getDebugSettings().jammerProbability then\n\t\t\t\t\t\tiads:printOutput(\"JAMMER: Distance: \"..distance)\n\t\t\t\t\tend\n\t\t\t\t\tsamSite:jam(self:getSuccessProbability(distance, natoName))\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\tend\nend\n\nfunction SkynetIADSJammer:hasLineOfSightToRadar(radar)\n\tlocal radarPos = radar:getPosition().p\n\t--lift the radar 30 meters off the ground, some 3d models are dug in to the ground, creating issues in calculating LOS\n\tradarPos.y = radarPos.y + 30\n\treturn land.isVisible(radarPos, self.emitter:getPosition().p) \nend\n\nfunction SkynetIADSJammer:masterArmSafe()\n\tmist.removeFunction(self.jammerTaskID)\nend\n\n--TODO: Remove Menu when emitter dies:\nfunction SkynetIADSJammer:addRadioMenu()\n\tself.radioMenu = missionCommands.addSubMenu('Jammer: '..self.emitter:getName())\n\tmissionCommands.addCommand('Master Arm On', self.radioMenu, SkynetIADSJammer.updateMasterArm, {self = self, option = 'masterArmOn'})\n\tmissionCommands.addCommand('Master Arm Safe', self.radioMenu, SkynetIADSJammer.updateMasterArm, {self = self, option = 'masterArmSafe'})\nend\n\nfunction SkynetIADSJammer.updateMasterArm(params)\n\tlocal option = params.option\n\tlocal self = params.self\n\tif option == 'masterArmOn' then\n\t\tself:masterArmOn()\n\telseif option == 'masterArmSafe' then\n\t\tself:masterArmSafe()\n\tend\nend\n\nfunction SkynetIADSJammer:removeRadioMenu()\n\tmissionCommands.removeItem(self.radioMenu)\nend\n\nend\n"
  },
  {
    "path": "skynet-iads-source/skynet-iads-logger.lua",
    "content": "do\n\nSkynetIADSLogger = {}\nSkynetIADSLogger.__index = SkynetIADSLogger\n\nfunction SkynetIADSLogger:create(iads)\n\tlocal logger = {}\n\tsetmetatable(logger, SkynetIADSLogger)\n\tlogger.debugOutput = {}\n\tlogger.debugOutput.IADSStatus = false\n\tlogger.debugOutput.samWentDark = false\n\tlogger.debugOutput.contacts = false\n\tlogger.debugOutput.radarWentLive = false\n\tlogger.debugOutput.jammerProbability = false\n\tlogger.debugOutput.addedEWRadar = false\n\tlogger.debugOutput.addedSAMSite = false\n\tlogger.debugOutput.warnings = true\n\tlogger.debugOutput.harmDefence = false\n\tlogger.debugOutput.samSiteStatusEnvOutput = false\n\tlogger.debugOutput.earlyWarningRadarStatusEnvOutput = false\n\tlogger.debugOutput.commandCenterStatusEnvOutput = false\n\tlogger.iads = iads\n\treturn logger\nend\n\nfunction SkynetIADSLogger:getDebugSettings()\n\treturn self.debugOutput\nend\n\nfunction SkynetIADSLogger:printOutput(output, typeWarning)\n\tif typeWarning == true and self:getDebugSettings().warnings or typeWarning == nil then\n\t\tif typeWarning == true then\n\t\t\toutput = \"WARNING: \"..output\n\t\tend\n\t\ttrigger.action.outText(output, 4)\n\tend\nend\n\nfunction SkynetIADSLogger:printOutputToLog(output)\n\tenv.info(\"SKYNET: \"..output, 4)\nend\n\nfunction SkynetIADSLogger:printEarlyWarningRadarStatus()\n\tlocal ewRadars = self.iads:getEarlyWarningRadars()\n\tself:printOutputToLog(\"------------------------------------------ EW RADAR STATUS: \"..self.iads:getCoalitionString()..\" -------------------------------\")\n\tfor i = 1, #ewRadars do\n\t\tlocal ewRadar = ewRadars[i]\n\t\tlocal numConnectionNodes = #ewRadar:getConnectionNodes()\n\t\tlocal numPowerSources = #ewRadar:getPowerSources()\n\t\tlocal isActive = ewRadar:isActive()\n\t\tlocal connectionNodes = ewRadar:getConnectionNodes()\n\t\tlocal firstRadar = nil\n\t\tlocal radars = ewRadar:getRadars()\n\t\t\n\t\t--get the first existing radar to prevent issues in calculating the distance later on:\n\t\tfor i = 1, #radars do\n\t\t\tif radars[i]:isExist() then\n\t\t\t\tfirstRadar = radars[i]\n\t\t\t\tbreak\n\t\t\tend\n\t\t\n\t\tend\n\t\tlocal numDamagedConnectionNodes = 0\n\t\t\n\t\t\n\t\tfor j = 1, #connectionNodes do\n\t\t\tlocal connectionNode = connectionNodes[j]\n\t\t\tif connectionNode:isExist() == false then\n\t\t\t\tnumDamagedConnectionNodes = numDamagedConnectionNodes + 1\n\t\t\tend\n\t\tend\n\t\tlocal intactConnectionNodes = numConnectionNodes - numDamagedConnectionNodes\n\t\t\n\t\tlocal powerSources = ewRadar:getPowerSources()\n\t\tlocal numDamagedPowerSources = 0\n\t\tfor j = 1, #powerSources do\n\t\t\tlocal powerSource = powerSources[j]\n\t\t\tif powerSource:isExist() == false then\n\t\t\t\tnumDamagedPowerSources = numDamagedPowerSources + 1\n\t\t\tend\n\t\tend\n\t\tlocal intactPowerSources = numPowerSources - numDamagedPowerSources \n\t\t\n\t\tlocal detectedTargets = ewRadar:getDetectedTargets()\n\t\tlocal samSitesInCoveredArea = ewRadar:getChildRadars()\n\t\t\n\t\tlocal unitName = \"DESTROYED\"\n\t\t\n\t\tif ewRadar:getDCSRepresentation():isExist() then\n\t\t\tunitName = ewRadar:getDCSName()\n\t\tend\n\t\t\n\t\tself:printOutputToLog(\"UNIT: \"..unitName..\" | TYPE: \"..ewRadar:getNatoName())\n\t\tself:printOutputToLog(\"ACTIVE: \"..tostring(isActive)..\"| DETECTED TARGETS: \"..#detectedTargets..\" | DEFENDING HARM: \"..tostring(ewRadar:isDefendingHARM()))\n\t\tif numConnectionNodes > 0 then\n\t\t\tself:printOutputToLog(\"CONNECTION NODES: \"..numConnectionNodes..\" | DAMAGED: \"..numDamagedConnectionNodes..\" | INTACT: \"..intactConnectionNodes)\n\t\telse\n\t\t\tself:printOutputToLog(\"NO CONNECTION NODES SET\")\n\t\tend\n\t\tif numPowerSources > 0 then\n\t\t\tself:printOutputToLog(\"POWER SOURCES : \"..numPowerSources..\" | DAMAGED:\"..numDamagedPowerSources..\" | INTACT: \"..intactPowerSources)\n\t\telse\n\t\t\tself:printOutputToLog(\"NO POWER SOURCES SET\")\n\t\tend\n\t\t\n\t\tself:printOutputToLog(\"SAM SITES IN COVERED AREA: \"..#samSitesInCoveredArea)\n\t\tfor j = 1, #samSitesInCoveredArea do\n\t\t\tlocal samSiteCovered = samSitesInCoveredArea[j]\n\t\t\tself:printOutputToLog(samSiteCovered:getDCSName())\n\t\tend\n\t\t\n\t\tfor j = 1, #detectedTargets do\n\t\t\tlocal contact = detectedTargets[j]\n\t\t\tif firstRadar ~= nil and firstRadar:isExist() then\n\t\t\t\tlocal distance = mist.utils.round(mist.utils.metersToNM(ewRadar:getDistanceInMetersToContact(firstRadar:getDCSRepresentation(), contact:getPosition().p)), 2)\n\t\t\t\tself:printOutputToLog(\"CONTACT: \"..contact:getName()..\" | TYPE: \"..contact:getTypeName()..\" | DISTANCE NM: \"..distance)\n\t\t\tend\n\t\tend\n\t\t\n\t\tself:printOutputToLog(\"---------------------------------------------------\")\n\t\t\n\tend\n\nend\n\nfunction SkynetIADSLogger:getMetaInfo(abstractElementSupport)\n\tlocal info = {}\n\tinfo.numSources = #abstractElementSupport\n\tinfo.numDamagedSources = 0\n\tinfo.numIntactSources = 0\n\tfor j = 1, #abstractElementSupport do\n\t\tlocal source = abstractElementSupport[j]\n\t\tif source:isExist() == false then\n\t\t\tinfo.numDamagedSources = info.numDamagedSources + 1\n\t\tend\n\tend\n\tinfo.numIntactSources = info.numSources - info.numDamagedSources\n\treturn info\nend\n\nfunction SkynetIADSLogger:printSAMSiteStatus()\n\tlocal samSites = self.iads:getSAMSites()\n\t\n\tself:printOutputToLog(\"------------------------------------------ SAM STATUS: \"..self.iads:getCoalitionString()..\" -------------------------------\")\n\tfor i = 1, #samSites do\n\t\tlocal samSite = samSites[i]\n\t\tlocal numConnectionNodes = #samSite:getConnectionNodes()\n\t\tlocal numPowerSources = #samSite:getPowerSources()\n\t\tlocal isAutonomous = samSite:getAutonomousState()\n\t\tlocal isActive = samSite:isActive()\n\t\t\n\t\tlocal connectionNodes = samSite:getConnectionNodes()\n\t\tlocal firstRadar = samSite:getRadars()[1]\n\t\tlocal numDamagedConnectionNodes = 0\n\t\tfor j = 1, #connectionNodes do\n\t\t\tlocal connectionNode = connectionNodes[j]\n\t\t\tif connectionNode:isExist() == false then\n\t\t\t\tnumDamagedConnectionNodes = numDamagedConnectionNodes + 1\n\t\t\tend\n\t\tend\n\t\tlocal intactConnectionNodes = numConnectionNodes - numDamagedConnectionNodes\n\t\t\n\t\tlocal powerSources = samSite:getPowerSources()\n\t\tlocal numDamagedPowerSources = 0\n\t\tfor j = 1, #powerSources do\n\t\t\tlocal powerSource = powerSources[j]\n\t\t\tif powerSource:isExist() == false then\n\t\t\t\tnumDamagedPowerSources = numDamagedPowerSources + 1\n\t\t\tend\n\t\tend\n\t\tlocal intactPowerSources = numPowerSources - numDamagedPowerSources \n\t\t\n\t\tlocal detectedTargets = samSite:getDetectedTargets()\n\t\t\n\t\tlocal samSitesInCoveredArea = samSite:getChildRadars()\n\t\t\n\t\tlocal engageAirWeapons = samSite:getCanEngageAirWeapons()\n\t\t\n\t\tlocal engageHARMS = samSite:getCanEngageHARM()\n\t\t\n\t\tlocal hasAmmo = samSite:hasRemainingAmmo()\n\t\t\n\t\tself:printOutputToLog(\"GROUP: \"..samSite:getDCSName()..\" | TYPE: \"..samSite:getNatoName())\n\t\tself:printOutputToLog(\"ACTIVE: \"..tostring(isActive)..\" | AUTONOMOUS: \"..tostring(isAutonomous)..\" | IS ACTING AS EW: \"..tostring(samSite:getActAsEW())..\" | CAN ENGAGE AIR WEAPONS : \"..tostring(engageAirWeapons)..\" | CAN ENGAGE HARMS : \"..tostring(engageHARMS)..\" | HAS AMMO: \"..tostring(hasAmmo)..\" | DETECTED TARGETS: \"..#detectedTargets..\" | DEFENDING HARM: \"..tostring(samSite:isDefendingHARM())..\" | MISSILES IN FLIGHT: \"..tostring(samSite:getNumberOfMissilesInFlight()))\n\t\t\n\t\tif numConnectionNodes > 0 then\n\t\t\tself:printOutputToLog(\"CONNECTION NODES: \"..numConnectionNodes..\" | DAMAGED: \"..numDamagedConnectionNodes..\" | INTACT: \"..intactConnectionNodes)\n\t\telse\n\t\t\tself:printOutputToLog(\"NO CONNECTION NODES SET\")\n\t\tend\n\t\tif numPowerSources > 0 then\n\t\t\tself:printOutputToLog(\"POWER SOURCES : \"..numPowerSources..\" | DAMAGED:\"..numDamagedPowerSources..\" | INTACT: \"..intactPowerSources)\n\t\telse\n\t\t\tself:printOutputToLog(\"NO POWER SOURCES SET\")\n\t\tend\n\t\t\n\t\tself:printOutputToLog(\"SAM SITES IN COVERED AREA: \"..#samSitesInCoveredArea)\n\t\tfor j = 1, #samSitesInCoveredArea do\n\t\t\tlocal samSiteCovered = samSitesInCoveredArea[j]\n\t\t\tself:printOutputToLog(samSiteCovered:getDCSName())\n\t\tend\n\t\t\n\t\tfor j = 1, #detectedTargets do\n\t\t\tlocal contact = detectedTargets[j]\n\t\t\tif firstRadar ~= nil and firstRadar:isExist() then\n\t\t\t\tlocal distance = mist.utils.round(mist.utils.metersToNM(samSite:getDistanceInMetersToContact(firstRadar:getDCSRepresentation(), contact:getPosition().p)), 2)\n\t\t\t\tself:printOutputToLog(\"CONTACT: \"..contact:getName()..\" | TYPE: \"..contact:getTypeName()..\" | DISTANCE NM: \"..distance)\n\t\t\tend\n\t\tend\n\t\t\n\t\tself:printOutputToLog(\"---------------------------------------------------\")\n\tend\nend\n\nfunction SkynetIADSLogger:printCommandCenterStatus()\n\tlocal commandCenters = self.iads:getCommandCenters()\n\tself:printOutputToLog(\"------------------------------------------ COMMAND CENTER STATUS: \"..self.iads:getCoalitionString()..\" -------------------------------\")\n\t\n\tfor i = 1, #commandCenters do\n\t\tlocal commandCenter = commandCenters[i]\n\t\tlocal numConnectionNodes = #commandCenter:getConnectionNodes()\n\t\tlocal powerSourceInfo = self:getMetaInfo(commandCenter:getPowerSources())\n\t\tlocal connectionNodeInfo = self:getMetaInfo(commandCenter:getConnectionNodes())\n\t\tself:printOutputToLog(\"GROUP: \"..commandCenter:getDCSName()..\" | TYPE: \"..commandCenter:getNatoName())\n\t\tif connectionNodeInfo.numSources > 0 then\n\t\t\tself:printOutputToLog(\"CONNECTION NODES: \"..connectionNodeInfo.numSources..\" | DAMAGED: \"..connectionNodeInfo.numDamagedSources..\" | INTACT: \"..connectionNodeInfo.numIntactSources)\n\t\telse\n\t\t\tself:printOutputToLog(\"NO CONNECTION NODES SET\")\n\t\tend\n\t\tif powerSourceInfo.numSources > 0 then\n\t\t\tself:printOutputToLog(\"POWER SOURCES : \"..powerSourceInfo.numSources..\" | DAMAGED: \"..powerSourceInfo.numDamagedSources..\" | INTACT: \"..powerSourceInfo.numIntactSources)\n\t\telse\n\t\t\tself:printOutputToLog(\"NO POWER SOURCES SET\")\n\t\tend\n\t\tself:printOutputToLog(\"---------------------------------------------------\")\n\tend\nend\n\nfunction SkynetIADSLogger:printSystemStatus()\t\n\n\tif self:getDebugSettings().IADSStatus or self:getDebugSettings().contacts then\n\t\tlocal coalitionStr = self.iads:getCoalitionString()\n\t\tself:printOutput(\"---- IADS: \"..coalitionStr..\" ------\")\n\tend\n\t\n\tif self:getDebugSettings().IADSStatus then\n\n\t\tlocal commandCenters = self.iads:getCommandCenters()\n\t\tlocal numComCenters = #commandCenters\n\t\tlocal numDestroyedComCenters = 0\n\t\tlocal numComCentersNoPower = 0\n\t\tlocal numComCentersNoConnectionNode = 0\n\t\tlocal numIntactComCenters = 0\n\t\tfor i = 1, #commandCenters do\n\t\t\tlocal commandCenter = commandCenters[i]\n\t\t\tif commandCenter:hasWorkingPowerSource() == false then\n\t\t\t\tnumComCentersNoPower = numComCentersNoPower + 1\n\t\t\tend\n\t\t\tif commandCenter:hasActiveConnectionNode() == false then\n\t\t\t\tnumComCentersNoConnectionNode = numComCentersNoConnectionNode + 1\n\t\t\tend\n\t\t\tif commandCenter:isDestroyed() == false then\n\t\t\t\tnumIntactComCenters = numIntactComCenters + 1\n\t\t\tend\n\t\tend\n\t\t\n\t\tnumDestroyedComCenters = numComCenters - numIntactComCenters\n\t\t\n\t\t\n\t\tself:printOutput(\"COMMAND CENTERS: \"..numComCenters..\" | Destroyed: \"..numDestroyedComCenters..\" | NoPowr: \"..numComCentersNoPower..\" | NoCon: \"..numComCentersNoConnectionNode)\n\t\n\t\tlocal ewNoPower = 0\n\t\tlocal earlyWarningRadars = self.iads:getEarlyWarningRadars()\n\t\tlocal ewTotal = #earlyWarningRadars\n\t\tlocal ewNoConnectionNode = 0\n\t\tlocal ewActive = 0\n\t\tlocal ewRadarsInactive = 0\n\n\t\tfor i = 1, #earlyWarningRadars do\n\t\t\tlocal ewRadar = earlyWarningRadars[i]\n\t\t\tif ewRadar:hasWorkingPowerSource() == false then\n\t\t\t\tewNoPower = ewNoPower + 1\n\t\t\tend\n\t\t\tif ewRadar:hasActiveConnectionNode() == false then\n\t\t\t\tewNoConnectionNode = ewNoConnectionNode + 1\n\t\t\tend\n\t\t\tif ewRadar:isActive() then\n\t\t\t\tewActive = ewActive + 1\n\t\t\tend\n\t\tend\n\t\t\n\t\tewRadarsInactive = ewTotal - ewActive\t\n\t\tlocal numEWRadarsDestroyed = #self.iads:getDestroyedEarlyWarningRadars()\n\t\tself:printOutput(\"EW: \"..ewTotal..\" | On: \"..ewActive..\" | Off: \"..ewRadarsInactive..\" | Destroyed: \"..numEWRadarsDestroyed..\" | NoPowr: \"..ewNoPower..\" | NoCon: \"..ewNoConnectionNode)\n\t\t\n\t\tlocal samSitesInactive = 0\n\t\tlocal samSitesActive = 0\n\t\tlocal samSites = self.iads:getSAMSites()\n\t\tlocal samSitesTotal = #samSites\n\t\tlocal samSitesNoPower = 0\n\t\tlocal samSitesNoConnectionNode = 0\n\t\tlocal samSitesOutOfAmmo = 0\n\t\tlocal samSiteAutonomous = 0\n\t\tlocal samSiteRadarDestroyed = 0\n\t\tfor i = 1, #samSites do\n\t\t\tlocal samSite = samSites[i]\n\t\t\tif samSite:hasWorkingPowerSource() == false then\n\t\t\t\tsamSitesNoPower = samSitesNoPower + 1\n\t\t\tend\n\t\t\tif samSite:hasActiveConnectionNode() == false then\n\t\t\t\tsamSitesNoConnectionNode = samSitesNoConnectionNode + 1\n\t\t\tend\n\t\t\tif samSite:isActive() then\n\t\t\t\tsamSitesActive = samSitesActive + 1\n\t\t\tend\n\t\t\tif samSite:hasRemainingAmmo() == false then\n\t\t\t\tsamSitesOutOfAmmo = samSitesOutOfAmmo + 1\n\t\t\tend\n\t\t\tif samSite:getAutonomousState() == true then\n\t\t\t\tsamSiteAutonomous = samSiteAutonomous + 1\n\t\t\tend\n\t\t\tif samSite:hasWorkingRadar() == false then\n\t\t\t\tsamSiteRadarDestroyed = samSiteRadarDestroyed + 1\n\t\t\tend\n\t\tend\n\t\t\n\t\tsamSitesInactive = samSitesTotal - samSitesActive\n\t\tself:printOutput(\"SAM: \"..samSitesTotal..\" | On: \"..samSitesActive..\" | Off: \"..samSitesInactive..\" | Autonm: \"..samSiteAutonomous..\" | Raddest: \"..samSiteRadarDestroyed..\" | NoPowr: \"..samSitesNoPower..\" | NoCon: \"..samSitesNoConnectionNode..\" | NoAmmo: \"..samSitesOutOfAmmo)\n\tend\n\t\n\tif self:getDebugSettings().contacts then\n\t\tlocal contacts = self.iads:getContacts()\n\t\tif contacts then\n\t\t\tfor i = 1, #contacts do\n\t\t\t\tlocal contact = contacts[i]\n\t\t\t\t\tself:printOutput(\"CONTACT: \"..contact:getName()..\" | TYPE: \"..contact:getTypeName()..\" | GS: \"..tostring(contact:getGroundSpeedInKnots())..\" | LAST SEEN: \"..contact:getAge())\n\t\t\tend\n\t\tend\n\tend\n\t\n\tif self:getDebugSettings().commandCenterStatusEnvOutput then\n\t\tself:printCommandCenterStatus()\n\tend\n\n\tif self:getDebugSettings().earlyWarningRadarStatusEnvOutput then\n\t\tself:printEarlyWarningRadarStatus()\n\tend\n\t\n\tif self:getDebugSettings().samSiteStatusEnvOutput then\n\t\tself:printSAMSiteStatus()\n\tend\n\nend\n\nend\n"
  },
  {
    "path": "skynet-iads-source/skynet-iads-sam-search-radar.lua",
    "content": "do\n\nSkynetIADSSAMSearchRadar = {}\nSkynetIADSSAMSearchRadar = inheritsFrom(SkynetIADSAbstractDCSObjectWrapper)\n\nfunction SkynetIADSSAMSearchRadar:create(unit)\n\tlocal instance = self:superClass():create(unit)\n\tsetmetatable(instance, self)\n\tself.__index = self\n\tinstance.firingRangePercent = 100\n\tinstance.maximumRange = 0\n\tinstance.initialNumberOfMissiles = 0\n\tinstance.remainingNumberOfMissiles = 0\n\tinstance.initialNumberOfShells = 0\n\tinstance.remainingNumberOfShells = 0\n\tinstance.triedSensors = 0\n\treturn instance\nend\n\n--override in subclasses to match different datastructure of getSensors()\nfunction SkynetIADSSAMSearchRadar:setupRangeData()\n\tif self:isExist() then\n\t\tlocal data = self:getDCSRepresentation():getSensors()\n\t\tif data == nil then\n\t\t\t--this is to prevent infinite calls between launcher and search radar\n\t\t\tself.triedSensors = self.triedSensors + 1\n\t\t\t--the SA-13 does not have any sensor data, but is has launcher data, so we use the stuff from the launcher for the radar range.\n\t\t\tSkynetIADSSAMLauncher.setupRangeData(self)\n\t\t\treturn\n\t\tend\n\t\tfor i = 1, #data do\n\t\t\tlocal subEntries = data[i]\n\t\t\tfor j = 1, #subEntries do\n\t\t\t\tlocal sensorInformation = subEntries[j]\n\t\t\t\t-- some sam sites have  IR and passive EWR detection, we are just interested in the radar data\n\t\t\t\t-- investigate if upperHemisphere and headOn is ok, I guess it will work for most detection cases\n\t\t\t\tif sensorInformation.type == Unit.SensorType.RADAR and sensorInformation['detectionDistanceAir'] then\n\t\t\t\t\tlocal upperHemisphere = sensorInformation['detectionDistanceAir']['upperHemisphere']['headOn']\n\t\t\t\t\tlocal lowerHemisphere = sensorInformation['detectionDistanceAir']['lowerHemisphere']['headOn']\n\t\t\t\t\tself.maximumRange = upperHemisphere\n\t\t\t\t\tif lowerHemisphere > upperHemisphere then\n\t\t\t\t\t\tself.maximumRange = lowerHemisphere\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\tend\nend\n\nfunction SkynetIADSSAMSearchRadar:getMaxRangeFindingTarget()\n\treturn self.maximumRange\nend\n\nfunction SkynetIADSSAMSearchRadar:isRadarWorking()\n\t-- the ammo check is for the SA-13 which does not return any sensor data:\n\treturn (self:isExist() == true and ( self:getDCSRepresentation():getSensors() ~= nil or self:getDCSRepresentation():getAmmo() ~= nil ) )\nend\n\nfunction SkynetIADSSAMSearchRadar:setFiringRangePercent(percent)\n\tself.firingRangePercent = percent\nend\n\nfunction SkynetIADSSAMSearchRadar:getDistance(target)\n\treturn mist.utils.get2DDist(target:getPosition().p, self:getDCSRepresentation():getPosition().p)\nend\n\nfunction SkynetIADSSAMSearchRadar:getHeight(target)\n\tlocal radarElevation = self:getDCSRepresentation():getPosition().p.y\n\tlocal targetElevation = target:getPosition().p.y\n\treturn math.abs(targetElevation - radarElevation)\nend\n\nfunction SkynetIADSSAMSearchRadar:isInHorizontalRange(target)\n\treturn (self:getMaxRangeFindingTarget() / 100 * self.firingRangePercent) >= self:getDistance(target)\nend\n\nfunction SkynetIADSSAMSearchRadar:isInRange(target)\n\tif self:isExist() == false then\n\t\treturn false\n\tend\n\treturn self:isInHorizontalRange(target)\nend\n\nend\n\n"
  },
  {
    "path": "skynet-iads-source/skynet-iads-sam-site.lua",
    "content": "do\n\nSkynetIADSSamSite = {}\nSkynetIADSSamSite = inheritsFrom(SkynetIADSAbstractRadarElement)\n\nfunction SkynetIADSSamSite:create(samGroup, iads)\n\tlocal sam = self:superClass():create(samGroup, iads)\n\tsetmetatable(sam, self)\n\tself.__index = self\n\tsam.targetsInRange = false\n\tsam.goLiveConstraints = {}\n\treturn sam\nend\n\nfunction SkynetIADSSamSite:addGoLiveConstraint(constraintName, constraint)\n\tself.goLiveConstraints[constraintName] = constraint\nend\n\nfunction SkynetIADSAbstractRadarElement:areGoLiveConstraintsSatisfied(contact)\n\tfor constraintName, constraint in pairs(self.goLiveConstraints) do\n\t\tif ( constraint(contact) ~= true ) then\n\t\t\treturn false\n\t\tend\n\tend\n\treturn true\nend\n\nfunction SkynetIADSAbstractRadarElement:removeGoLiveConstraint(constraintName)\n\tlocal constraints = {}\n\tfor cName, constraint in pairs(self.goLiveConstraints) do\n\t\tif cName ~= constraintName then\n\t\t\tconstraints[cName] = constraint\n\t\tend\n\tend\n\tself.goLiveConstraints = constraints\nend\n\nfunction SkynetIADSAbstractRadarElement:getGoLiveConstraints()\n\treturn self.goLiveConstraints\nend\n\nfunction SkynetIADSSamSite:isDestroyed()\n\tlocal isDestroyed = true\n\tfor i = 1, #self.launchers do\n\t\tlocal launcher = self.launchers[i]\n\t\tif launcher:isExist() == true then\n\t\t\tisDestroyed = false\n\t\tend\n\tend\n\tlocal radars = self:getRadars()\n\tfor i = 1, #radars do\n\t\tlocal radar = radars[i]\n\t\tif radar:isExist() == true then\n\t\t\tisDestroyed = false\n\t\tend\n\tend\t\n\treturn isDestroyed\nend\n\nfunction SkynetIADSSamSite:targetCycleUpdateStart()\n\tself.targetsInRange = false\nend\n\nfunction SkynetIADSSamSite:targetCycleUpdateEnd()\n\tif self.targetsInRange == false and self.actAsEW == false and self:getAutonomousState() == false and self:getAutonomousBehaviour() == SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DCS_AI then\n\t\tself:goDark()\n\tend\nend\n\nfunction SkynetIADSSamSite:informOfContact(contact)\n\t-- we make sure isTargetInRange (expensive call) is only triggered if no previous calls to this method resulted in targets in range\n\tif ( self.targetsInRange == false and self:areGoLiveConstraintsSatisfied(contact) == true and self:isTargetInRange(contact) and ( contact:isIdentifiedAsHARM() == false or ( contact:isIdentifiedAsHARM() == true and self:getCanEngageHARM() == true ) ) ) then\n\t\tself:goLive()\n\t\tself.targetsInRange = true\n\tend\nend\n\nend\n"
  },
  {
    "path": "skynet-iads-source/skynet-iads-sam-tracking-radar.lua",
    "content": "do\n\nSkynetIADSSAMTrackingRadar = {}\nSkynetIADSSAMTrackingRadar = inheritsFrom(SkynetIADSSAMSearchRadar)\n\nfunction SkynetIADSSAMTrackingRadar:create(unit)\n\tlocal instance = self:superClass():create(unit)\n\tsetmetatable(instance, self)\n\tself.__index = self\n\treturn instance\nend\n\nend\n"
  },
  {
    "path": "skynet-iads-source/skynet-iads-supported-types.lua",
    "content": "do\n--this file contains the required units per sam type\nsamTypesDB = {\t\n\t['S-200'] = {\n        ['type'] = 'complex',\n        ['searchRadar'] = {\n            ['RLS_19J6'] = {\n                ['name'] = {\n                    ['NATO'] = 'Tin Shield',\n                },\n\t\t\t}, \n\t\t\t['p-19 s-125 sr'] = {\n\t\t\t\t['name'] = {\n\t\t\t\t\t['NATO'] = 'Flat Face',\n\t\t\t\t},\n\t\t\t},\t\n\t\t},\n        ['EWR P-37 BAR LOCK'] = {\n            ['Name'] = {\n              ['NATO'] = \"Bar lock\",\n            },   \n        },\n        ['trackingRadar'] = {\n            ['RPC_5N62V'] = {\n            },\n        },\n        ['launchers'] = {\n            ['S-200_Launcher'] = {\n            },\n        },\n        ['name'] = {\n            ['NATO'] = 'SA-5 Gammon',\n        },\n        ['harm_detection_chance'] = 60\n    },\n\t['S-300'] = {\n\t\t['type'] = 'complex',\n\t\t['searchRadar'] = {\n\t\t\t['S-300PS 40B6MD sr'] = {\n\t\t\t\t['name'] = {\n\t\t\t\t\t['NATO'] = 'Clam Shell',\n\t\t\t\t},\n\t\t\t},\n\t\t\t['S-300PS 64H6E sr'] = {\n\t\t\t\t['name'] = {\n\t\t\t\t\t['NATO'] = 'Big Bird',\n\t\t\t\t},\n\t\t\t},\n\t\t\t['S-300PS 40B6MD sr_19J6'] = {\n\t\t\t\t['name'] = {\n\t\t\t\t\t['NATO'] = 'Tin Shield',\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t['trackingRadar'] = {\n\t\t\t['S-300PS 40B6M tr'] = {\n\t\t\t},\t\n\t\t\t['S-300PS 5H63C 30H6_tr'] = {\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['S-300PS 5P85D ln'] = {\n\t\t\t},\n\t\t\t['S-300PS 5P85C ln'] = {\n\t\t\t},\n\t\t},\n\t\t['misc'] = {\n\t\t\t['S-300PS 54K6 cp'] = {\n\t\t\t\t['required'] = true,\n\t\t\t},\n\t\t},\n\t\t['name'] = {\n\t\t\t['NATO'] = 'SA-10 Grumble',\n\t\t},\n\t\t['harm_detection_chance'] = 90,\n\t\t['can_engage_harm'] = true\n\t},\n\t['Buk'] = {\n\t\t['type'] = 'complex',\n\t\t['searchRadar'] = {\n\t\t\t['SA-11 Buk SR 9S18M1'] = {\n\t\t\t\t['name'] = {\n\t\t\t\t\t['NATO'] = 'Snow Drift',\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['SA-11 Buk LN 9A310M1'] = {\n\t\t\t},\n\t\t},\n\t\t['misc'] = {\n\t\t\t['SA-11 Buk CC 9S470M1'] = {\n\t\t\t\t['required'] = true,\n\t\t\t},\n\t\t},\n\t\t['name'] = {\n\t\t\t['NATO'] = 'SA-11 Gadfly',\n\t\t},\n\t\t['harm_detection_chance'] = 70\n\t},\n\t['S-125'] = {\n\t\t['type'] = 'complex',\n\t\t['searchRadar'] = {\n\t\t\t['p-19 s-125 sr'] = {\n\t\t\t\t['name'] = {\n\t\t\t\t\t['NATO'] = 'Flat Face',\n\t\t\t\t},\n\t\t\t},\t\n\t\t},\n\t\t['trackingRadar'] = {\n\t\t\t['snr s-125 tr'] = {\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['5p73 s-125 ln'] = {\n\t\t\t},\n\t\t},\n\t\t['name'] = {\n\t\t\t['NATO'] = 'SA-3 Goa',\n\t\t},\n\t\t['harm_detection_chance'] = 30\n\t},\n    ['S-75'] = {\n\t\t['type'] = 'complex',\n\t\t['searchRadar'] = {\n\t\t\t['p-19 s-125 sr'] = {\n\t\t\t\t['name'] = {\n\t\t\t\t\t['NATO'] = 'Flat Face',\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t['trackingRadar'] = {\n\t\t\t['SNR_75V'] = {\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['S_75M_Volhov'] = {\n\t\t\t},\n\t\t},\n\t\t['name'] = {\n\t\t\t['NATO'] = 'SA-2 Guideline',\n\t\t},\n\t\t['harm_detection_chance'] = 30\n\t},\n\t['Kub'] = {\n\t\t['type'] = 'complex',\n\t\t['searchRadar'] = {\n\t\t\t['Kub 1S91 str'] = {\n\t\t\t\t['name'] = {\n\t\t\t\t\t['NATO'] = 'Straight Flush',\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['Kub 2P25 ln'] = {\n\t\t\t},\n\t\t},\n\t\t['name'] = {\n\t\t\t['NATO'] = 'SA-6 Gainful',\n\t\t},\n\t\t['harm_detection_chance'] = 40\n\t},\n\t['Patriot'] = {\n\t\t['type'] = 'complex',\n\t\t['searchRadar'] = {\n\t\t\t['Patriot str'] = {\n\t\t\t\t['name'] = {\n\t\t\t\t\t['NATO'] = 'Patriot str',\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['Patriot ln'] = {\n\t\t\t},\n\t\t},\n\t\t['misc'] = {\n\t\t\t['Patriot cp'] = {\n\t\t\t\t['required'] = false,\n\t\t\t},\n\t\t\t['Patriot EPP']  = {\n\t\t\t\t['required'] = false,\n\t\t\t},\n\t\t\t['Patriot ECS']  = {\n\t\t\t\t['required'] = true,\n\t\t\t},\n\t\t\t['Patriot AMG']  = {\n\t\t\t\t['required'] = false,\n\t\t\t},\n\t\t},\n\t\t['name'] = {\n\t\t\t['NATO'] = 'Patriot',\n\t\t},\n\t\t['harm_detection_chance'] = 90,\n\t\t['can_engage_harm'] = true\n\t},\n\t['Hawk'] = {\n\t\t['type'] = 'complex',\n\t\t['searchRadar'] = {\n\t\t\t['Hawk sr'] = {\n\t\t\t\t['name'] = {\n\t\t\t\t\t['NATO'] = 'Hawk str',\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t['trackingRadar'] = {\n\t\t\t['Hawk tr'] = {\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['Hawk ln'] = {\n\t\t\t},\n\t\t},\n\n\t\t['name'] = {\n\t\t\t['NATO'] = 'Hawk',\n\t\t},\n\t\t['harm_detection_chance'] = 40\n\n\t},\t\n\t['Roland ADS'] = {\n\t\t['type'] = 'complex',\n\t\t['searchRadar'] = {\n\t\t\t['Roland Radar'] = {\n\t\t\t\t['name'] = {\n\t\t\t\t\t['NATO'] = 'Roland EWR',\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['Roland ADS'] = {\n\t\t\t},\n\t\t},\n\t\t['name'] = {\n\t\t\t['NATO'] = 'Roland ADS',\n\t\t},\n\t\t['harm_detection_chance'] = 60\n\t},\t\n\t['NASAMS'] = {\n\t\t['type'] = 'complex',\n\t\t['searchRadar'] = {\n\t\t\t['NASAMS_Radar_MPQ64F1'] = {\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['NASAMS_LN_B'] = {\t\t\n\t\t\t},\n\t\t\t['NASAMS_LN_C'] = {\t\t\n\t\t\t},\n\t\t},\n\t\t\n\t\t['name'] = {\n\t\t\t['NATO'] = 'NASAMS',\n\t\t},\n\t\t['misc'] = {\n\t\t\t['NASAMS_Command_Post'] = {\n\t\t\t\t['required'] = false,\n\t\t\t},\n\t\t},\n\t\t['can_engage_harm'] = true,\n\t\t['harm_detection_chance'] = 90\n\t},\t\n\t['2S6 Tunguska'] = {\n\t\t['type'] = 'single',\n\t\t['searchRadar'] = {\n\t\t\t['2S6 Tunguska'] = {\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['2S6 Tunguska'] = {\n\t\t\t},\n\t\t},\n\t\t['name'] = {\n\t\t\t['NATO'] = 'SA-19 Grison',\n\t\t},\n\t},\t\t\n\t['Osa'] = {\n\t\t['type'] = 'single',\n\t\t['searchRadar'] = {\n\t\t\t['Osa 9A33 ln'] = {\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['Osa 9A33 ln'] = {\n\t\t\t\n\t\t\t},\n\t\t},\n\t\t['name'] = {\n\t\t\t['NATO'] = 'SA-8 Gecko',\n\t\t},\n\t\t['harm_detection_chance'] = 20\n\t},\t\n\t['Strela-10M3'] = {\n\t\t['type'] = 'single',\n\t\t['searchRadar'] = {\n\t\t\t['Strela-10M3'] = {\n\t\t\t\t['trackingRadar'] = true,\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['Strela-10M3'] = {\n\t\t\t},\n\t\t},\n\t\t['name'] = {\n\t\t\t['NATO'] = 'SA-13 Gopher',\n\t\t},\n\t},\t\n\t['Strela-1 9P31'] = {\n\t\t['type'] = 'single',\n\t\t['searchRadar'] = {\n\t\t\t['Strela-1 9P31'] = {\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['Strela-1 9P31'] = {\n\t\t\t},\n\t\t},\n\t\t['name'] = {\n\t\t\t['NATO'] = 'SA-9 Gaskin',\n\t\t},\n\t\t['harm_detection_chance'] = 20\n\t},\n\t['Tor'] = {\n\t\t['type'] = 'single',\n\t\t['searchRadar'] = {\n\t\t\t['Tor 9A331'] = {\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['Tor 9A331'] = {\n\t\t\t},\n\t\t},\n\t\t['name'] = {\n\t\t\t['NATO'] = 'SA-15 Gauntlet',\n\t\t},\n\t\t['harm_detection_chance'] = 90,\n\t\t['can_engage_harm'] = true\n\t\t\n\t},\n\t['Gepard'] = {\n\t\t['type'] = 'single',\n\t\t['searchRadar'] = {\n\t\t\t['Gepard'] = {\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['Gepard'] = {\n\t\t\t},\n\t\t},\n\t\t['name'] = {\n\t\t\t['NATO'] = 'Gepard',\n\t\t},\n\t\t['harm_detection_chance'] = 10\n\t},\t\t\n    ['Rapier'] = {\n        ['searchRadar'] = {\n            ['rapier_fsa_blindfire_radar'] = {\n            },\n        },\n        ['launchers'] = {\n        \t['rapier_fsa_launcher'] = {\n\t\t\t\t['trackingRadar'] = true,\n\t\t\t},\n        },\n        ['misc'] = {\n            ['rapier_fsa_optical_tracker_unit'] = {\n                ['required'] = true,\n            },\n        },\n        ['name'] = {\n\t\t\t['NATO'] = 'Rapier',\n\t\t},\n\t\t['harm_detection_chance'] = 10\n    },\t\n\t['ZSU-23-4 Shilka'] = {\n\t\t['type'] = 'single',\n\t\t['searchRadar'] = {\n\t\t\t['ZSU-23-4 Shilka'] = {\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['ZSU-23-4 Shilka'] = {\n\t\t\t},\n\t\t},\n\t\t['name'] = {\n\t\t\t['NATO'] = 'Zues',\n\t\t},\n\t\t['harm_detection_chance'] = 10\n\t},\n\t['HQ-7'] = {\n\t\t['searchRadar'] = {\n\t\t\t['HQ-7_STR_SP'] = {\n\t\t\t\t['name'] = {\n\t\t\t\t\t['NATO'] = 'CSA-4',\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['HQ-7_LN_SP'] = {\n\t\t\t},\n\t\t},\n\t\t['name'] = {\n\t\t\t['NATO'] = 'CSA-4',\n\t\t},\n\t\t['harm_detection_chance'] = 30\n\t},\t\n\t['Phalanx'] = {\n\t\t['type'] = 'single',\n\t\t['searchRadar'] = {\n\t\t\t['HEMTT_C-RAM_Phalanx'] = {\n\t\t\t},\n\t\t},\n\t\t['launchers'] = {\n\t\t\t['HEMTT_C-RAM_Phalanx'] = {\n\t\t\t},\n\t\t},\n\t\t['name'] = {\n\t\t\t['NATO'] = 'Phalanx',\n\t\t},\n\t\t['harm_detection_chance'] = 10\n\t},\t\n-- Start of RED EW radars:\t\n\t['1L13 EWR'] = {\n\t\t['type'] = 'ewr',\n\t\t['searchRadar'] = {\n\t\t\t['1L13 EWR'] = {\n\t\t\t\t['name'] = {\n\t\t\t\t\t['NATO'] = 'Box Spring',\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t['harm_detection_chance'] = 60\n\t},\n\t['55G6 EWR'] = {\n\t\t['type'] = 'ewr',\n\t\t['searchRadar'] = {\n\t\t\t['55G6 EWR'] = {\n\t\t\t\t['name'] = {\n\t\t\t\t\t['NATO'] = 'Tall Rack',\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t['harm_detection_chance'] = 60\n\t},\n\t['Dog Ear'] = {\n\t\t['type'] = 'ewr',\n\t\t['searchRadar'] = {\n\t\t\t['Dog Ear radar'] = {\n\t\t\t\t['name'] = {\n\t\t\t\t\t['NATO'] = 'Dog Ear',\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t['harm_detection_chance'] = 20\n\t},\n-- Start of BLUE EW radars:\n\t['FPS-117 Dome'] = {\n\t\t['type'] = 'ewr',\n\t\t['searchRadar'] = {\n\t\t\t['FPS-117 Dome'] = {\n\t\t\t\t['name'] = {\n\t\t\t\t\t['NATO'] = 'FPS-117 Dome',\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t['harm_detection_chance'] = 80\n\t},\n\t['FPS-117'] = {\n\t\t['type'] = 'ewr',\n\t\t['searchRadar'] = {\n\t\t\t['FPS-117'] = {\n\t\t\t\t['name'] = {\n\t\t\t\t\t['NATO'] = 'FPS-117',\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t['harm_detection_chance'] = 80\n\t}\n}\nend"
  },
  {
    "path": "skynet-iads-source/skynet-iads-table-delegator.lua",
    "content": "do\n\n\nSkynetIADSTableDelegator = {}\n\nfunction SkynetIADSTableDelegator:create()\n\tlocal instance = {}\n\tlocal forwarder = {}\n\tforwarder.__index = function(tbl, name)\n\t\ttbl[name] = function(self, ...)\n\t\t\t\tfor i = 1, #self do\n\t\t\t\t\tself[i][name](self[i], ...)\n\t\t\t\tend\n\t\t\t\treturn self\n\t\t\tend\n\t\treturn tbl[name]\n\tend\n\tsetmetatable(instance, forwarder)\n\tinstance.__index = forwarder\n\treturn instance\nend\n\nend\n"
  },
  {
    "path": "skynet-iads-source/skynet-iads.lua",
    "content": "do\n\nSkynetIADS = {}\nSkynetIADS.__index = SkynetIADS\n\nSkynetIADS.database = samTypesDB\n\nfunction SkynetIADS:create(name)\n\tlocal iads = {}\n\tsetmetatable(iads, SkynetIADS)\n\tiads.radioMenu = nil\n\tiads.earlyWarningRadars = {}\n\tiads.samSites = {}\n\tiads.commandCenters = {}\n\tiads.ewRadarScanMistTaskID = nil\n\tiads.coalition = nil\n\tiads.contacts = {}\n\tiads.maxTargetAge = 32\n\tiads.name = name\n\tiads.harmDetection = SkynetIADSHARMDetection:create(iads)\n\tiads.logger = SkynetIADSLogger:create(iads)\n\tif iads.name == nil then\n\t\tiads.name = \"\"\n\tend\n\tiads.contactUpdateInterval = 5\n\tworld.addEventHandler(iads)\n\treturn iads\nend\n\nfunction SkynetIADS:onEvent(event)\n\tif (event.id == world.event.S_EVENT_BIRTH ) then\n\t\tenv.info(\"New Object Spawned\")\n\t--\tself:addSAMSite(event.initiator:getGroup():getName());\n\tend\nend\n\nfunction SkynetIADS:setUpdateInterval(interval)\n\tself.contactUpdateInterval = interval\nend\n\nfunction SkynetIADS:setCoalition(item)\n\tif item then\n\t\tlocal coalitionID = item:getCoalition()\n\t\tif self.coalitionID == nil then\n\t\t\tself.coalitionID = coalitionID\n\t\tend\n\t\tif self.coalitionID ~= coalitionID then\n\t\t\tself:printOutputToLog(\"element: \"..item:getName()..\" has a different coalition than the IADS\", true)\n\t\tend\n\tend\nend\n\nfunction SkynetIADS:addJammer(jammer)\n\ttable.insert(self.jammers, jammer)\nend\n\nfunction SkynetIADS:getCoalition()\n\treturn self.coalitionID\nend\n\nfunction SkynetIADS:getDestroyedEarlyWarningRadars()\n\tlocal destroyedSites = {}\n\tfor i = 1, #self.earlyWarningRadars do\n\t\tlocal ewSite = self.earlyWarningRadars[i]\n\t\tif ewSite:isDestroyed() then\n\t\t\ttable.insert(destroyedSites, ewSite)\n\t\tend\n\tend\n\treturn destroyedSites\nend\n\nfunction SkynetIADS:getUsableAbstractRadarElemtentsOfTable(abstractRadarTable)\n\tlocal usable = {}\n\tfor i = 1, #abstractRadarTable do\n\t\tlocal abstractRadarElement = abstractRadarTable[i]\n\t\tif abstractRadarElement:hasActiveConnectionNode() and abstractRadarElement:hasWorkingPowerSource() and abstractRadarElement:isDestroyed() == false then\n\t\t\ttable.insert(usable, abstractRadarElement)\n\t\tend\n\tend\n\treturn usable\nend\n\nfunction SkynetIADS:getUsableEarlyWarningRadars()\n\treturn self:getUsableAbstractRadarElemtentsOfTable(self.earlyWarningRadars)\nend\n\nfunction SkynetIADS:createTableDelegator(units) \n\tlocal sites = SkynetIADSTableDelegator:create()\n\tfor i = 1, #units do\n\t\tlocal site = units[i]\n\t\ttable.insert(sites, site)\n\tend\n\treturn sites\nend\n\nfunction SkynetIADS:addEarlyWarningRadarsByPrefix(prefix)\n\tself:deactivateEarlyWarningRadars()\n\tself.earlyWarningRadars = {}\n\tfor unitName, unit in pairs(mist.DBs.unitsByName) do\n\t\tlocal pos = self:findSubString(unitName, prefix)\n\t\t--somehow the MIST unit db contains StaticObject, we check to see we only add Units\n\t\tlocal unit = Unit.getByName(unitName)\n\t\tif pos and pos == 1 and unit then\n\t\t\tself:addEarlyWarningRadar(unitName)\n\t\tend\n\tend\n\treturn self:createTableDelegator(self.earlyWarningRadars)\nend\n\nfunction SkynetIADS:addEarlyWarningRadar(earlyWarningRadarUnitName)\n\tlocal earlyWarningRadarUnit = Unit.getByName(earlyWarningRadarUnitName)\n\tif earlyWarningRadarUnit == nil then\n\t\tself:printOutputToLog(\"you have added an EW Radar that does not exist, check name of Unit in Setup and Mission editor: \"..earlyWarningRadarUnitName, true)\n\t\treturn\n\tend\n\tself:setCoalition(earlyWarningRadarUnit)\n\tlocal ewRadar = nil\n\tlocal category = earlyWarningRadarUnit:getDesc().category\n\tif category == Unit.Category.AIRPLANE or category == Unit.Category.SHIP then\n\t\tewRadar = SkynetIADSAWACSRadar:create(earlyWarningRadarUnit, self)\n\telse\n\t\tewRadar = SkynetIADSEWRadar:create(earlyWarningRadarUnit, self)\n\tend\n\tewRadar:setupElements()\n\tewRadar:setCachedTargetsMaxAge(self:getCachedTargetsMaxAge())\t\n\t-- for performance improvement, if iads is not scanning no update coverage update needs to be done, will be executed once when iads activates\n\tif self.ewRadarScanMistTaskID ~= nil then\n\t\tself:buildRadarCoverageForEarlyWarningRadar(ewRadar)\n\tend\n\tewRadar:setActAsEW(true)\n\tewRadar:setToCorrectAutonomousState()\n\tewRadar:goLive()\n\ttable.insert(self.earlyWarningRadars, ewRadar)\n\tif self:getDebugSettings().addedEWRadar then\n\t\t\tself:printOutputToLog(\"ADDED: \"..ewRadar:getDescription())\n\tend\n\treturn ewRadar\nend\n\nfunction SkynetIADS:getCachedTargetsMaxAge()\n\treturn self.contactUpdateInterval\nend\n\nfunction SkynetIADS:getEarlyWarningRadars()\n\treturn self:createTableDelegator(self.earlyWarningRadars)\nend\n\nfunction SkynetIADS:getEarlyWarningRadarByUnitName(unitName)\n\tfor i = 1, #self.earlyWarningRadars do\n\t\tlocal ewRadar = self.earlyWarningRadars[i]\n\t\tif ewRadar:getDCSName() == unitName then\n\t\t\treturn ewRadar\n\t\tend\n\tend\nend\n\nfunction SkynetIADS:findSubString(haystack, needle)\n\treturn string.find(haystack, needle, 1, true)\nend\n\nfunction SkynetIADS:addSAMSitesByPrefix(prefix)\n\tself:deativateSAMSites()\n\tself.samSites = {}\n\tfor groupName, groupData in pairs(mist.DBs.groupsByName) do\n\t\tlocal pos = self:findSubString(groupName, prefix)\n\t\tif pos and pos == 1 then\n\t\t\t--mist returns groups, units and, StaticObjects\n\t\t\tlocal dcsObject = Group.getByName(groupName)\n\t\t\tif dcsObject and dcsObject:getUnits()[1]:isActive() then\n\t\t\t\tself:addSAMSite(groupName)\n\t\t\tend\n\t\tend\n\tend\n\treturn self:createTableDelegator(self.samSites)\nend\n\nfunction SkynetIADS:getSAMSitesByPrefix(prefix)\n\tlocal returnSams = {}\n\tfor i = 1, #self.samSites do\n\t\tlocal samSite = self.samSites[i]\n\t\tlocal groupName = samSite:getDCSName()\n\t\tlocal pos = self:findSubString(groupName, prefix)\n\t\tif pos and pos == 1 then\n\t\t\ttable.insert(returnSams, samSite)\n\t\tend\n\tend\n\treturn self:createTableDelegator(returnSams)\nend\n\nfunction SkynetIADS:addSAMSite(samSiteName)\n\tlocal samSiteDCS = Group.getByName(samSiteName)\n\tif samSiteDCS == nil then\n\t\tself:printOutputToLog(\"you have added an SAM Site that does not exist, check name of Group in Setup and Mission editor: \"..tostring(samSiteName), true)\n\t\treturn\n\tend\n\tself:setCoalition(samSiteDCS)\n\tlocal samSite = SkynetIADSSamSite:create(samSiteDCS, self)\n\tsamSite:setupElements()\n\tsamSite:setCanEngageAirWeapons(true)\n\tsamSite:goLive()\n\tsamSite:setCachedTargetsMaxAge(self:getCachedTargetsMaxAge())\n\tif samSite:getNatoName() == \"UNKNOWN\" then\n\t\tself:printOutputToLog(\"you have added an SAM site that Skynet IADS can not handle: \"..samSite:getDCSName(), true)\n\t\tsamSite:cleanUp()\n\telse\n\t\tsamSite:goDark()\n\t\ttable.insert(self.samSites, samSite)\n\t\tif self:getDebugSettings().addedSAMSite then\n\t\t\tself:printOutputToLog(\"ADDED: \"..samSite:getDescription())\n\t\tend\n\t\t-- for performance improvement, if iads is not scanning no update coverage update needs to be done, will be executed once when iads activates\n\t\tif self.ewRadarScanMistTaskID ~= nil then\n\t\t\tself:buildRadarCoverageForSAMSite(samSite)\n\t\tend\n\t\treturn samSite\n\tend \nend\n\nfunction SkynetIADS:getUsableSAMSites()\n\treturn self:getUsableAbstractRadarElemtentsOfTable(self.samSites)\nend\n\nfunction SkynetIADS:getDestroyedSAMSites()\n\tlocal destroyedSites = {}\n\tfor i = 1, #self.samSites do\n\t\tlocal samSite = self.samSites[i]\n\t\tif samSite:isDestroyed() then\n\t\t\ttable.insert(destroyedSites, samSite)\n\t\tend\n\tend\n\treturn destroyedSites\nend\n\nfunction SkynetIADS:getSAMSites()\n\treturn self:createTableDelegator(self.samSites)\nend\n\nfunction SkynetIADS:getActiveSAMSites()\n\tlocal activeSAMSites = {}\n\tfor i = 1, #self.samSites do\n\t\tif self.samSites[i]:isActive() then\n\t\t\ttable.insert(activeSAMSites, self.samSites[i])\n\t\tend\n\tend\n\treturn activeSAMSites\nend\n\nfunction SkynetIADS:getSAMSiteByGroupName(groupName)\n\tfor i = 1, #self.samSites do\n\t\tlocal samSite = self.samSites[i]\n\t\tif samSite:getDCSName() == groupName then\n\t\t\treturn samSite\n\t\tend\n\tend\nend\n\nfunction SkynetIADS:getSAMSitesByNatoName(natoName)\n\tlocal selectedSAMSites = SkynetIADSTableDelegator:create()\n\tfor i = 1, #self.samSites do\n\t\tlocal samSite = self.samSites[i]\n\t\tif samSite:getNatoName() == natoName then\n\t\t\ttable.insert(selectedSAMSites, samSite)\n\t\tend\n\tend\n\treturn selectedSAMSites\nend\n\nfunction SkynetIADS:addCommandCenter(commandCenter)\n\tself:setCoalition(commandCenter)\n\tlocal comCenter = SkynetIADSCommandCenter:create(commandCenter, self)\n\ttable.insert(self.commandCenters, comCenter)\n\t-- when IADS is active the radars will be added to the new command center. If it not active this will happen when radar coverage is built\n\tif self.ewRadarScanMistTaskID ~= nil then\n\t\tself:addRadarsToCommandCenters()\n\tend\n\treturn comCenter\nend\n\nfunction SkynetIADS:isCommandCenterUsable()\n\tif #self:getCommandCenters() == 0 then\n\t\treturn true\n\tend\n\tlocal usableComCenters = self:getUsableAbstractRadarElemtentsOfTable(self:getCommandCenters())\n\treturn (#usableComCenters > 0)\nend\n\nfunction SkynetIADS:getCommandCenters()\n\treturn self.commandCenters\nend\n\n\nfunction SkynetIADS.evaluateContacts(self)\n\n\tlocal ewRadars = self:getUsableEarlyWarningRadars()\n\tlocal samSites = self:getUsableSAMSites()\n\t\n\t--will add SAM Sites acting as EW Rardars to the ewRadars array:\n\tfor i = 1, #samSites do\n\t\tlocal samSite = samSites[i]\n\t\t--We inform SAM sites that a target update is about to happen. If they have no targets in range after the cycle they go dark\n\t\tsamSite:targetCycleUpdateStart()\n\t\tif samSite:getActAsEW() then\n\t\t\ttable.insert(ewRadars, samSite)\n\t\tend\n\t\t--if the sam site is not in ew mode and active we grab the detected targets right here\n\t\tif samSite:isActive() and samSite:getActAsEW() == false then\n\t\t\tlocal contacts = samSite:getDetectedTargets()\n\t\t\tfor j = 1, #contacts do\n\t\t\t\tlocal contact = contacts[j]\n\t\t\t\tself:mergeContact(contact)\n\t\t\tend\n\t\tend\n\tend\n\n\tlocal samSitesToTrigger = {}\n\t\n\tfor i = 1, #ewRadars do\n\t\tlocal ewRadar = ewRadars[i]\n\t\t--call go live in case ewRadar had to shut down (HARM attack)\n\t\tewRadar:goLive()\n\t\t-- if an awacs has traveled more than a predeterminded distance we update the autonomous state of the SAMs\n\t\tif getmetatable(ewRadar) == SkynetIADSAWACSRadar and ewRadar:isUpdateOfAutonomousStateOfSAMSitesRequired() then\n\t\t\tself:buildRadarCoverageForEarlyWarningRadar(ewRadar)\n\t\tend\n\t\tlocal ewContacts = ewRadar:getDetectedTargets()\n\t\tif #ewContacts > 0 then\n\t\t\tlocal samSitesUnderCoverage = ewRadar:getUsableChildRadars()\n\t\t\tfor j = 1, #samSitesUnderCoverage do\n\t\t\t\tlocal samSiteUnterCoverage = samSitesUnderCoverage[j]\n\t\t\t\t-- only if a SAM site is not active we add it to the hash of SAM sites to be iterated later on\n\t\t\t\tif samSiteUnterCoverage:isActive() == false then\n\t\t\t\t\t--we add them to a hash to make sure each SAM site is in the collection only once, reducing the number of loops we conduct later on\n\t\t\t\t\tsamSitesToTrigger[samSiteUnterCoverage:getDCSName()] = samSiteUnterCoverage\n\t\t\t\tend\n\t\t\tend\n\t\t\tfor j = 1, #ewContacts do\n\t\t\t\tlocal contact = ewContacts[j]\n\t\t\t\tself:mergeContact(contact)\n\t\t\tend\n\t\tend\n\tend\n\n\tself:cleanAgedTargets()\n\t\n\tfor samName, samToTrigger in pairs(samSitesToTrigger) do\n\t\tfor j = 1, #self.contacts do\n\t\t\tlocal contact = self.contacts[j]\n\t\t\t-- the DCS Radar only returns enemy aircraft, if that should change a coalition check will be required\n\t\t\t-- currently every type of object in the air is handed of to the SAM site, including missiles\n\t\t\tlocal description = contact:getDesc()\n\t\t\tlocal category = description.category\n\t\t\tif category and category ~= Unit.Category.GROUND_UNIT and category ~= Unit.Category.SHIP and category ~= Unit.Category.STRUCTURE then\n\t\t\t\tsamToTrigger:informOfContact(contact)\n\t\t\tend\n\t\tend\n\tend\n\t\n\tfor i = 1, #samSites do\n\t\tlocal samSite = samSites[i]\n\t\tsamSite:targetCycleUpdateEnd()\n\tend\n\t\n\tself.harmDetection:setContacts(self:getContacts())\n\tself.harmDetection:evaluateContacts()\n\t\n\tself.logger:printSystemStatus()\nend\n\nfunction SkynetIADS:cleanAgedTargets()\n\tlocal contactsToKeep = {}\n\tfor i = 1, #self.contacts do\n\t\tlocal contact = self.contacts[i]\n\t\tif contact:getAge() < self.maxTargetAge then\n\t\t\ttable.insert(contactsToKeep, contact)\n\t\tend\n\tend\n\tself.contacts = contactsToKeep\nend\n\n--TODO unit test this method:\nfunction SkynetIADS:getAbstracRadarElements()\n\tlocal abstractRadarElements = {}\n\tlocal ewRadars = self:getEarlyWarningRadars()\n\tlocal samSites = self:getSAMSites()\n\t\n\tfor i = 1, #ewRadars do\n\t\tlocal ewRadar = ewRadars[i]\n\t\ttable.insert(abstractRadarElements, ewRadar)\n\tend\n\t\n\tfor i = 1, #samSites do\n\t\tlocal samSite = samSites[i]\n\t\ttable.insert(abstractRadarElements, samSite)\n\tend\n\treturn abstractRadarElements\nend\n\n\nfunction SkynetIADS:addRadarsToCommandCenters()\n\n\t--we clear any existing radars that may have been added earlier\n\tlocal comCenters = self:getCommandCenters()\n\tfor i = 1, #comCenters do\n\t\tlocal comCenter = comCenters[i]\n\t\tcomCenter:clearChildRadars()\n\tend\t\n\t\n\t-- then we add child radars to the command centers\n\tlocal abstractRadarElements = self:getAbstracRadarElements()\n\t\tfor i = 1, #abstractRadarElements do\n\t\t\tlocal abstractRadar = abstractRadarElements[i]\n\t\t\tself:addSingleRadarToCommandCenters(abstractRadar)\n\t\tend\nend\n\nfunction SkynetIADS:addSingleRadarToCommandCenters(abstractRadarElement)\n\tlocal comCenters = self:getCommandCenters()\n\tfor i = 1, #comCenters do\n\t\tlocal comCenter = comCenters[i]\n\t\tcomCenter:addChildRadar(abstractRadarElement)\n\tend\t\nend\n\n-- this method rebuilds the radar coverage of the IADS, a complete rebuild is only required the first time the IADS is activated\n-- during runtime it is sufficient to call buildRadarCoverageForSAMSite or buildRadarCoverageForEarlyWarningRadar method that just updates the IADS for one unit, this saves script execution time\nfunction SkynetIADS:buildRadarCoverage()\t\n\t\n\t--to build the basic radar coverage we use all SAM sites. Checks if SAM site has power or a connection node is done when using the SAM site later on\n\tlocal samSites = self:getSAMSites()\n\t\n\t--first we clear all child and parent radars that may have been added previously\n\tfor i = 1, #samSites do\n\t\tlocal samSite = samSites[i]\n\t\tsamSite:clearChildRadars()\n\t\tsamSite:clearParentRadars()\n\tend\n\t\n\tlocal ewRadars = self:getEarlyWarningRadars()\n\t\n\tfor i = 1, #ewRadars do\n\t\tlocal ewRadar = ewRadars[i]\n\t\tewRadar:clearChildRadars()\n\tend\t\n\t\n\t--then we rebuild the radar coverage\n\tlocal abstractRadarElements = self:getAbstracRadarElements()\n\tfor i = 1, #abstractRadarElements do\n\t\tlocal abstract = abstractRadarElements[i]\n\t\tself:buildRadarCoverageForAbstractRadarElement(abstract)\n\tend\n\t\n\tself:addRadarsToCommandCenters()\n\t\n\t--we call this once on all sam sites, to make sure autonomous sites go live when IADS activates\n\tfor i = 1, #samSites do\n\t\tlocal samSite = samSites[i]\n\t\tsamSite:informChildrenOfStateChange()\n\tend\n\nend\n\nfunction SkynetIADS:buildRadarCoverageForAbstractRadarElement(abstractRadarElement)\n\tlocal abstractRadarElements = self:getAbstracRadarElements()\n\tfor i = 1, #abstractRadarElements do\n\t\tlocal aElementToCompare = abstractRadarElements[i]\n\t\tif aElementToCompare ~= abstractRadarElement then\n\t\t\tif abstractRadarElement:isInRadarDetectionRangeOf(aElementToCompare) then\n\t\t\t\tself:buildRadarAssociation(aElementToCompare, abstractRadarElement)\n\t\t\tend\n\t\t\tif aElementToCompare:isInRadarDetectionRangeOf(abstractRadarElement) then\n\t\t\t\tself:buildRadarAssociation(abstractRadarElement, aElementToCompare)\n\t\t\tend\n\t\tend\n\tend\nend\n\nfunction SkynetIADS:buildRadarAssociation(parent, child)\n\t--chilren should only be SAM sites not EW radars\n\tif ( getmetatable(child) == SkynetIADSSamSite ) then\n\t\tparent:addChildRadar(child)\n\tend\n\t--Only SAM Sites should have parent Radars, not EW Radars\n\tif ( getmetatable(child) == SkynetIADSSamSite ) then\n\t\tchild:addParentRadar(parent)\n\tend\nend\n\nfunction SkynetIADS:buildRadarCoverageForSAMSite(samSite)\n\tself:buildRadarCoverageForAbstractRadarElement(samSite)\n\tself:addSingleRadarToCommandCenters(samSite)\nend\n\nfunction SkynetIADS:buildRadarCoverageForEarlyWarningRadar(ewRadar)\n\tself:buildRadarCoverageForAbstractRadarElement(ewRadar)\n\tself:addSingleRadarToCommandCenters(ewRadar)\nend\n\nfunction SkynetIADS:mergeContact(contact)\n\tlocal existingContact = false\n\tfor i = 1, #self.contacts do\n\t\tlocal iadsContact = self.contacts[i]\n\t\tif iadsContact:getName() == contact:getName() then\n\t\t\tiadsContact:refresh()\n\t\t\t--these contacts are used in the logger we set a kown harm state of a contact coming from a SAM site. So the logger will show them als HARMs\n\t\t\tcontact:setHARMState(iadsContact:getHARMState())\n\t\t\tlocal radars = contact:getAbstractRadarElementsDetected()\n\t\t\tfor j = 1, #radars do\n\t\t\t\tlocal radar = radars[j]\n\t\t\t\tiadsContact:addAbstractRadarElementDetected(radar)\n\t\t\tend\n\t\t\texistingContact = true\n\t\tend\n\tend\n\tif existingContact == false then\n\t\ttable.insert(self.contacts, contact)\n\tend\nend\n\n\nfunction SkynetIADS:getContacts()\n\treturn self.contacts\nend\n\nfunction SkynetIADS:getDebugSettings()\n\treturn self.logger.debugOutput\nend\n\nfunction SkynetIADS:printOutput(output, typeWarning)\n\tself.logger:printOutput(output, typeWarning)\nend\n\nfunction SkynetIADS:printOutputToLog(output)\n\tself.logger:printOutputToLog(output)\nend\n\n-- will start going through the Early Warning Radars and SAM sites to check what targets they have detected\nfunction SkynetIADS.activate(self)\n\tmist.removeFunction(self.ewRadarScanMistTaskID)\n\tself.ewRadarScanMistTaskID = mist.scheduleFunction(SkynetIADS.evaluateContacts, {self}, 1, self.contactUpdateInterval)\n\tself:buildRadarCoverage()\nend\n\nfunction SkynetIADS:setupSAMSitesAndThenActivate(setupTime)\n\tself:activate()\n\tself.logger:printOutputToLog(\"DEPRECATED: setupSAMSitesAndThenActivate, no longer needed since using enableEmission instead of AI on / off allows for the Ground units to setup with their radars turned off\")\nend\n\nfunction SkynetIADS:deactivate()\n\tmist.removeFunction(self.ewRadarScanMistTaskID)\n\tmist.removeFunction(self.samSetupMistTaskID)\n\tself:deativateSAMSites()\n\tself:deactivateEarlyWarningRadars()\n\tself:deactivateCommandCenters()\nend\n\nfunction SkynetIADS:deactivateCommandCenters()\n\tfor i = 1, #self.commandCenters do\n\t\tlocal comCenter = self.commandCenters[i]\n\t\tcomCenter:cleanUp()\n\tend\nend\n\nfunction SkynetIADS:deativateSAMSites()\n\tfor i = 1, #self.samSites do\n\t\tlocal samSite = self.samSites[i]\n\t\tsamSite:cleanUp()\n\tend\nend\n\nfunction SkynetIADS:deactivateEarlyWarningRadars()\n\tfor i = 1, #self.earlyWarningRadars do\n\t\tlocal ewRadar = self.earlyWarningRadars[i]\n\t\tewRadar:cleanUp()\n\tend\nend\t\n\nfunction SkynetIADS:addRadioMenu()\n\tself.radioMenu = missionCommands.addSubMenu('SKYNET IADS '..self:getCoalitionString())\n\tlocal displayIADSStatus = missionCommands.addCommand('show IADS Status', self.radioMenu, SkynetIADS.updateDisplay, {self = self, value = true, option = 'IADSStatus'})\n\tlocal displayIADSStatus = missionCommands.addCommand('hide IADS Status', self.radioMenu, SkynetIADS.updateDisplay, {self = self, value = false, option = 'IADSStatus'})\n\tlocal displayIADSStatus = missionCommands.addCommand('show contacts', self.radioMenu, SkynetIADS.updateDisplay, {self = self, value = true, option = 'contacts'})\n\tlocal displayIADSStatus = missionCommands.addCommand('hide contacts', self.radioMenu, SkynetIADS.updateDisplay, {self = self, value = false, option = 'contacts'})\nend\n\nfunction SkynetIADS:removeRadioMenu()\n\tmissionCommands.removeItem(self.radioMenu)\nend\n\nfunction SkynetIADS.updateDisplay(params)\n\tlocal option = params.option\n\tlocal self = params.self\n\tlocal value = params.value\n\tif option == 'IADSStatus' then\n\t\tself:getDebugSettings()[option] = value\n\telseif option == 'contacts' then\n\t\tself:getDebugSettings()[option] = value\n\tend\nend\n\nfunction SkynetIADS:getCoalitionString()\n\tlocal coalitionStr = \"RED\"\n\tif self.coalitionID == coalition.side.BLUE then\n\t\tcoalitionStr = \"BLUE\"\n\telseif self.coalitionID == coalition.side.NEUTRAL then\n\t\tcoalitionStr = \"NEUTRAL\"\n\tend\n\t\t\n\tif self.name then\n\t\tcoalitionStr = \"COALITION: \"..coalitionStr..\" | NAME: \"..self.name\n\tend\n\t\n\treturn coalitionStr\nend\n\nfunction SkynetIADS:getMooseConnector()\n\tif self.mooseConnector == nil then\n\t\tself.mooseConnector = SkynetMooseA2ADispatcherConnector:create(self)\n\tend\n\treturn self.mooseConnector\nend\n\nfunction SkynetIADS:addMooseSetGroup(mooseSetGroup)\n\tself:getMooseConnector():addMooseSetGroup(mooseSetGroup)\nend\n\nend\n"
  },
  {
    "path": "skynet-iads-source/skynet-mooose-a2a-dispatcher-connector.lua",
    "content": "do\n\nSkynetMooseA2ADispatcherConnector = {}\n\nfunction SkynetMooseA2ADispatcherConnector:create(iads)\n\tlocal instance = {}\n\tsetmetatable(instance, self)\n\tself.__index = self\n\tinstance.iadsCollection = {}\n\tinstance.mooseGroups = {}\n\tinstance.ewRadarGroupNames = {}\n\tinstance.samSiteGroupNames = {}\n\ttable.insert(instance.iadsCollection, iads)\n\treturn instance\nend\n\nfunction SkynetMooseA2ADispatcherConnector:addIADS(iads)\n\ttable.insert(self.iadsCollection, iads)\nend\n\nfunction SkynetMooseA2ADispatcherConnector:addMooseSetGroup(mooseSetGroup)\n\ttable.insert(self.mooseGroups, mooseSetGroup)\n\tself:update()\nend\n\nfunction SkynetMooseA2ADispatcherConnector:getEarlyWarningRadarGroupNames()\n\tself.ewRadarGroupNames = {}\n\tfor i = 1, #self.iadsCollection do\n\t\tlocal ewRadars = self.iadsCollection[i]:getUsableEarlyWarningRadars()\n\t\tfor j = 1, #ewRadars do\n\t\t\tlocal ewRadar = ewRadars[j]\n\t\t\ttable.insert(self.ewRadarGroupNames, ewRadar:getDCSRepresentation():getGroup():getName())\n\t\tend\n\tend\n\treturn self.ewRadarGroupNames\nend\n\nfunction SkynetMooseA2ADispatcherConnector:getSAMSiteGroupNames()\n\tself.samSiteGroupNames = {}\n\tfor i = 1, #self.iadsCollection do\n\t\tlocal samSites = self.iadsCollection[i]:getUsableSAMSites()\n\t\tfor j = 1, #samSites do\n\t\t\tlocal samSite = samSites[j]\n\t\t\ttable.insert(self.samSiteGroupNames, samSite:getDCSName())\n\t\tend\n\tend\n\treturn self.samSiteGroupNames\nend\n\nfunction SkynetMooseA2ADispatcherConnector:update()\n\t\n\t--mooseGroup elements are type of:\n\t--https://flightcontrol-master.github.io/MOOSE_DOCS_DEVELOP/Documentation/Core.Set.html##(SET_GROUP)\n\t\n\t--remove previously set group names:\n\tfor i = 1, #self.mooseGroups do\n\t\tlocal mooseGroup = self.mooseGroups[i]\n\t\tmooseGroup:RemoveGroupsByName(self.ewRadarGroupNames)\n\t\tmooseGroup:RemoveGroupsByName(self.samSiteGroupNames)\n\tend\n\t\n\t--add group names of IADS radars that are currently usable by the IADS:\n\tfor i = 1, #self.mooseGroups do\n\t\tlocal mooseGroup = self.mooseGroups[i]\n\t\tmooseGroup:AddGroupsByName(self:getEarlyWarningRadarGroupNames())\n\t\tmooseGroup:AddGroupsByName(self:getSAMSiteGroupNames())\n\tend\nend\n\nend\n"
  },
  {
    "path": "skynet-iads-source/syknet-iads-sam-launcher.lua",
    "content": "do\n\nSkynetIADSSAMLauncher = {}\nSkynetIADSSAMLauncher = inheritsFrom(SkynetIADSSAMSearchRadar)\n\nfunction SkynetIADSSAMLauncher:create(unit)\n\tlocal instance = self:superClass():create(unit)\n\tsetmetatable(instance, self)\n\tself.__index = self\n\tinstance.maximumFiringAltitude = 0\n\treturn instance\nend\n\nfunction SkynetIADSSAMLauncher:setupRangeData()\n\tself.remainingNumberOfMissiles = 0\n\tself.remainingNumberOfShells = 0\n\tif self:isExist() then\n\t\tlocal data = self:getDCSRepresentation():getAmmo()\n\t\tlocal initialNumberOfMissiles = 0\n\t\tlocal initialNumberOfShells = 0\n\t\t--data becomes nil, when all missiles are fired\n\t\tif data then\n\t\t\tfor i = 1, #data do\n\t\t\t\tlocal ammo = data[i]\t\t\n\t\t\t\t--we ignore checks on radar guidance types, since we are not interested in how exactly the missile is guided by the SAM site.\n\t\t\t\tif ammo.desc.category == Weapon.Category.MISSILE then\n\t\t\t\t\t--TODO: see what the difference is between Max and Min values, SA-3 has higher Min value than Max?, most likely it has to do with the box parameters supplied by launcher\n\t\t\t\t\t--to simplyfy we just use the larger value, sam sites need a few seconds of tracking time to fire, by that time contact has most likely closed in on the SAM site.\n\t\t\t\t\tlocal altMin = ammo.desc.rangeMaxAltMin\n\t\t\t\t\tlocal altMax = ammo.desc.rangeMaxAltMax\n\t\t\t\t\tself.maximumRange = altMin\n\t\t\t\t\tif altMin < altMax then\n\t\t\t\t\t\tself.maximumRange = altMax\n\t\t\t\t\tend\n\t\t\t\t\tself.maximumFiringAltitude = ammo.desc.altMax\n\t\t\t\t\tself.remainingNumberOfMissiles = self.remainingNumberOfMissiles + ammo.count\n\t\t\t\t\tinitialNumberOfMissiles = self.remainingNumberOfMissiles\n\t\t\t\tend\n\t\t\t\tif ammo.desc.category == Weapon.Category.SHELL then\n\t\t\t\t\tself.remainingNumberOfShells = self.remainingNumberOfShells + ammo.count\n\t\t\t\t\tinitialNumberOfShells = self.remainingNumberOfShells\n\t\t\t\tend\n\t\t\t\t--if no distance was detected we run the code for the search radar. This happens when all in one units are passed like the shilka\n\t\t\t\tif self.maximumRange == 0 then\n\t\t\t\t\t--this is to prevent infinite calls between launcher and search radar\n\t\t\t\t\tif self.triedSensors <= 2 then\n\t\t\t\t\t\tSkynetIADSSAMSearchRadar.setupRangeData(self)\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\t\t-- conditions here are because setupRangeData() is called multiple times in the code to update ammo status, we set initial values only the first time the method is called\n\t\t\tif self.initialNumberOfMissiles == 0 then\n\t\t\t\tself.initialNumberOfMissiles = initialNumberOfMissiles\n\t\t\tend\n\t\t\tif self.initialNumberOfShells == 0 then\n\t\t\t\tself.initialNumberOfShells = initialNumberOfShells\n\t\t\tend\n\t\tend\n\tend\nend\n\nfunction SkynetIADSSAMLauncher:getInitialNumberOfShells()\n\treturn self.initialNumberOfShells\nend\n\nfunction SkynetIADSSAMLauncher:getRemainingNumberOfShells()\n\tself:setupRangeData()\n\treturn self.remainingNumberOfShells\nend\n\nfunction SkynetIADSSAMLauncher:getInitialNumberOfMissiles()\n\treturn self.initialNumberOfMissiles\nend\n\nfunction SkynetIADSSAMLauncher:getRemainingNumberOfMissiles()\n\tself:setupRangeData()\n\treturn self.remainingNumberOfMissiles\nend\n\nfunction SkynetIADSSAMLauncher:getRange()\n\treturn self.maximumRange\nend\n\nfunction SkynetIADSSAMLauncher:getMaximumFiringAltitude()\n\treturn self.maximumFiringAltitude\nend\n\nfunction SkynetIADSSAMLauncher:isWithinFiringHeight(target)\n\t-- if no max firing height is set (radar quided AAA) then we use the vertical range, bit of a hack but probably ok for AAA\n\tif self:getMaximumFiringAltitude() > 0 then\n\t\treturn self:getMaximumFiringAltitude() >= self:getHeight(target) \n\telse\n\t\treturn self:getRange() >= self:getHeight(target)\n\tend\nend\n\nfunction SkynetIADSSAMLauncher:isInRange(target)\n\tif self:isExist() == false then\n\t\treturn false\n\tend\n\treturn self:isWithinFiringHeight(target) and self:isInHorizontalRange(target)\nend\n\nend\n\n--[[\nSA-2 Launcher:\n    {\n        count=1,\n        desc={\n            Nmax=17,\n            RCS=0.39669999480247,\n            _origin=\"\",\n            altMax=25000,\n            altMin=100,\n            box={\n                max={x=4.7303376197815, y=0.84564626216888, z=0.84564626216888},\n                min={x=-5.8387970924377, y=-0.84564626216888, z=-0.84564626216888}\n            },\n            category=1,\n            displayName=\"SA2V755\",\n            fuseDist=20,\n            guidance=4,\n            life=2,\n            missileCategory=2,\n            rangeMaxAltMax=30000,\n            rangeMaxAltMin=40000,\n            rangeMin=7000,\n            typeName=\"SA2V755\",\n            warhead={caliber=500, explosiveMass=196, mass=196, type=1}\n        }\n    }\n}\n--]]\n"
  },
  {
    "path": "unit-tests/highdigitsams/skynet-high-digit-sams-unit-test-setup.lua",
    "content": "do\n\n\nlocal units = Group.getByName('SAM-SA-20B'):getUnits()\nfor i = 1, #units do\n\tlocal unit = units[i]\n\tenv.info(unit:getTypeName())\nend\n\n\nlu.LuaUnit.run()\n\n--activate IADS \n\nredIADS = SkynetIADS:create(\"Red IADS\")\nlocal iadsDebug = redIADS:getDebugSettings()\niadsDebug.IADSStatus = true\niadsDebug.radarWentDark = true\niadsDebug.contacts = true\niadsDebug.radarWentLive = true\niadsDebug.jammerProbability = true\niadsDebug.addedEWRadar = true\niadsDebug.addedSAMSite = true\niadsDebug.harmDefence = true\niadsDebug.commandCenterStatusEnvOutput = true\niadsDebug.samSiteStatusEnvOutput = true\n\nredIADS:addSAMSitesByPrefix('SAM')\nredIADS:activate()\nend"
  },
  {
    "path": "unit-tests/highdigitsams/test-skynet-high-digit-sam-sites.lua",
    "content": "do\n\nTestSyknetIADSHighDigitSAMSites = {}\n\nfunction TestSyknetIADSHighDigitSAMSites:setUp()\n\tif self.samSiteName then\n\t\tself.skynetIADS = SkynetIADS:create()\n\t\tlocal samSite = Group.getByName(self.samSiteName)\n\t\tself.samSite = SkynetIADSSamSite:create(samSite, self.skynetIADS)\n\t\tself.samSite:setupElements()\n\tend\n\tif self.ewName then\n\t\tself.skynetIADS = SkynetIADS:create()\n\t\tlocal ewRadar = Unit.getByName(self.ewName)\n\t\tself.ewRadar = SkynetIADSEWRadar:create(ewRadar, self.skynetIADS)\n\t\tself.ewRadar:setupElements()\n\tend\nend\n\nfunction TestSyknetIADSHighDigitSAMSites:tearDown()\n\tif self.samSite then\t\n\t\tself.samSite:cleanUp()\n\tend\n\t\n\tif self.ewRadar then\n\t\tself.ewRadar:cleanUp()\n\tend\nend\n\nfunction TestSyknetIADSHighDigitSAMSites:testSA20AGargoyle()\n\tself.samSiteName = \"SAM-SA-20A\"\n\tself:setUp()\n\tlu.assertEquals(self.samSite:getNatoName(), \"SA-20A\")\n\t\n\tlocal launchers = self.samSite:getLaunchers()\n\tlu.assertEquals(#launchers, 2)\n\t\n\tlocal launcher1 = launchers[1]\n\tlu.assertEquals(launcher1:getTypeName(), \"S-300PMU1 5P85CE ln\")\n\tlu.assertEquals(launcher1:getRange(), 150000)\n\tlu.assertEquals(launcher1:getMaximumFiringAltitude(), 27000)\n\tlu.assertEquals(launcher1:getInitialNumberOfMissiles(), 4)\n\t\n\tlocal launcher2 = launchers[2]\n\tlu.assertEquals(launcher2:getTypeName(), \"S-300PMU1 5P85DE ln\")\n\tlu.assertEquals(launcher2:getRange(), 150000)\n\tlu.assertEquals(launcher2:getMaximumFiringAltitude(), 27000)\n\tlu.assertEquals(launcher1:getInitialNumberOfMissiles(), 4)\n\t\n\tlocal searchRadars = self.samSite:getSearchRadars()\n\tlu.assertEquals(#searchRadars, 2)\n\t\n\tlocal searchRadars1 = searchRadars[1]\n\tlu.assertEquals(searchRadars1:getTypeName(), \"S-300PMU1 40B6MD sr\")\n\tlu.assertEquals(searchRadars1:getMaxRangeFindingTarget(), 106998.453125)\n\n\tlocal searchRadars2 = searchRadars[2]\n\tlu.assertEquals(searchRadars2:getTypeName(), \"S-300PMU1 64N6E sr\")\n\tlu.assertEquals(searchRadars2:getMaxRangeFindingTarget(), 106998.453125)\n\t\n\tlocal trackingRadars = self.samSite:getTrackingRadars()\n\tlu.assertEquals(#trackingRadars, 2)\n\t\n\tlocal trackingRadar1 = trackingRadars[1]\n\tlu.assertEquals(trackingRadar1:getTypeName(), \"S-300PMU1 40B6M tr\")\n\tlu.assertEquals(trackingRadar1:getMaxRangeFindingTarget(), 106998.453125)\n\t\n\tlocal trackingRadar2 = trackingRadars[2]\n\tlu.assertEquals(trackingRadar2:getTypeName(), \"S-300PMU1 30N6E tr\")\n\tlu.assertEquals(trackingRadar2:getMaxRangeFindingTarget(), 106998.453125)\n\t\n\tlu.assertEquals(self.samSite:getHARMDetectionChance(), 90)\n\tlu.assertEquals(self.samSite:getCanEngageHARM(), true)\n\t\n\t--output sensor data to dcs.log:\n\t--lu.assertEquals(launcher1:getDCSRepresentation():getSensors(), \"00\")\n\nend\n\nfunction TestSyknetIADSHighDigitSAMSites:testBigBird()\n\tself.ewName = \"Big-Bird\"\n\tself:setUp()\n\tlu.assertEquals(self.ewRadar:getNatoName(), \"Big Bird\")\nend\n\nfunction TestSyknetIADSHighDigitSAMSites:testClamShell()\n\tself.ewName = \"Clam-Shell\"\n\tself:setUp()\n\tlu.assertEquals(self.ewRadar:getNatoName(), \"Clam Shell\")\nend\n\nfunction TestSyknetIADSHighDigitSAMSites:testBillBoardC()\n\tself.ewName = \"Bill-Board-C\"\n\tself:setUp()\n\tlu.assertEquals(self.ewRadar:getNatoName(), \"Bill Board-C\")\nend\n\nfunction TestSyknetIADSHighDigitSAMSites:testHighScreenB()\n\tself.ewName = \"High-Screen-B\"\n\tself:setUp()\n\tlu.assertEquals(self.ewRadar:getNatoName(), \"High Screen-B\")\nend\n\nfunction TestSyknetIADSHighDigitSAMSites:testClamShell2()\n\tself.ewName = \"Clam-Shell-2\"\n\tself:setUp()\n\tlu.assertEquals(self.ewRadar:getNatoName(), \"Clam Shell\")\nend\n\nfunction TestSyknetIADSHighDigitSAMSites:testSnowDrift()\n\tself.ewName = \"Snow-Drift\"\n\tself:setUp()\n\tlu.assertEquals(self.ewRadar:getNatoName(), \"Snow Drift\")\nend\n\nfunction TestSyknetIADSHighDigitSAMSites:testUnnamedRadar()\n\tself.ewName = \"unnamed-radar\"\n\tself:setUp()\n\tlu.assertEquals(self.ewRadar:getNatoName(), \"UNKNOWN\")\n\tlu.assertEquals(self.ewRadar:getHARMDetectionChance(), 90)\nend\n\nfunction TestSyknetIADSHighDigitSAMSites:testSA23GladiatorOrGiant()\n\tself.samSiteName = \"SAM-SA-23\"\n\tself:setUp()\n\tlu.assertEquals(self.samSite:getNatoName(), \"SA-23\")\n\t\n\tlocal launchers = self.samSite:getLaunchers()\n\tlu.assertEquals(#launchers, 2)\n\n\tlocal launcher1 = launchers[1]\n\tlu.assertEquals(launcher1:getTypeName(), \"S-300VM 9A83ME ln\")\n\tlu.assertEquals(launcher1:getRange(), 100000)\n\tlu.assertEquals(launcher1:getMaximumFiringAltitude(), 30000)\n\tlu.assertEquals(launcher1:getInitialNumberOfMissiles(), 4)\n\n\tlocal launcher1 = launchers[2]\n\tlu.assertEquals(launcher1:getTypeName(), \"S-300VM 9A82ME ln\")\n\tlu.assertEquals(launcher1:getRange(), 200000)\n\tlu.assertEquals(launcher1:getMaximumFiringAltitude(), 37000)\n\tlu.assertEquals(launcher1:getInitialNumberOfMissiles(), 2)\t\n\t\n\tlocal searchRadars = self.samSite:getSearchRadars()\n\tlu.assertEquals(#searchRadars, 2)\n\t\n\tlocal searchRadars1 = searchRadars[1]\n\tlu.assertEquals(searchRadars1:getTypeName(), \"S-300VM 9S15M2 sr\")\n\tlu.assertEquals(searchRadars1:getMaxRangeFindingTarget(), 213996.90625)\n\t\n\tlocal searchRadars1 = searchRadars[2]\n\tlu.assertEquals(searchRadars1:getTypeName(), \"S-300VM 9S19M2 sr\")\n\tlu.assertEquals(searchRadars1:getMaxRangeFindingTarget(), 213996.90625)\n\t\n\tlocal trackingRadars = self.samSite:getTrackingRadars()\n\tlu.assertEquals(#trackingRadars, 1)\n\t\n\tlocal trackingRadar1 = trackingRadars[1]\n\tlu.assertEquals(trackingRadar1:getTypeName(), \"S-300VM 9S32ME tr\")\n\tlu.assertEquals(trackingRadar1:getMaxRangeFindingTarget(), 213996.90625)\n\n\tlu.assertEquals(self.samSite:getHARMDetectionChance(), 90)\n\tlu.assertEquals(self.samSite:getCanEngageHARM(), true)\n\t\nend\n\nfunction TestSyknetIADSHighDigitSAMSites:testSA10BGrumble()\n\tself.samSiteName = \"SAM-SA-10B\"\n\tself:setUp()\n\tlu.assertEquals(self.samSite:getNatoName(), \"SA-10B\")\n\t\n\tlocal launchers = self.samSite:getLaunchers()\n\tlu.assertEquals(#launchers, 2)\n\t\n\tlocal launcher1 = launchers[1]\n\tlu.assertEquals(launcher1:getTypeName(), \"S-300PS 5P85SE_mod ln\")\n\tlu.assertEquals(launcher1:getRange(), 75000)\n\tlu.assertEquals(launcher1:getMaximumFiringAltitude(), 30000)\n\tlu.assertEquals(launcher1:getInitialNumberOfMissiles(), 4)\n\t\n\tlocal launcher1 = launchers[2]\n\tlu.assertEquals(launcher1:getTypeName(), \"S-300PS 5P85SU_mod ln\")\n\tlu.assertEquals(launcher1:getRange(), 75000)\n\tlu.assertEquals(launcher1:getMaximumFiringAltitude(), 30000)\n\tlu.assertEquals(launcher1:getInitialNumberOfMissiles(), 4)\n\t\n\tlocal searchRadars = self.samSite:getSearchRadars()\n\tlu.assertEquals(#searchRadars, 2)\n\t\n\tlocal searchRadars1 = searchRadars[1]\n\tlu.assertEquals(searchRadars1:getTypeName(), \"S-300PS SA-10B 40B6MD MAST sr\")\n\tlu.assertEquals(searchRadars1:getMaxRangeFindingTarget(), 80248.84375)\n\t\n\tlocal searchRadars1 = searchRadars[2]\n\tlu.assertEquals(searchRadars1:getTypeName(), \"S-300PS 64H6E TRAILER sr\")\n\tlu.assertEquals(searchRadars1:getMaxRangeFindingTarget(), 80248.84375)\n\t\n\tlocal trackingRadars = self.samSite:getTrackingRadars()\n\tlu.assertEquals(#trackingRadars, 2)\n\t\n\tlocal trackingRadar1 = trackingRadars[1]\n\tlu.assertEquals(trackingRadar1:getTypeName(), \"S-300PS 30N6 TRAILER tr\")\n\tlu.assertEquals(trackingRadar1:getMaxRangeFindingTarget(), 80248.84375)\n\t\n\tlocal trackingRadar1 = trackingRadars[2]\n\tlu.assertEquals(trackingRadar1:getTypeName(), \"S-300PS SA-10B 40B6M MAST tr\")\n\tlu.assertEquals(trackingRadar1:getMaxRangeFindingTarget(), 80248.84375)\n\t\n\tlu.assertEquals(self.samSite:getHARMDetectionChance(), 90)\n\tlu.assertEquals(self.samSite:getCanEngageHARM(), true)\n\t\nend\n\nfunction TestSyknetIADSHighDigitSAMSites:testEDDefaultSA10GrubleWith55VRUD()\n\tself.samSiteName = \"SAM-SA-10C-5V55RUD\"\n\tself:setUp()\n\tlu.assertEquals(self.samSite:getNatoName(), \"SA-10\")\n\t\n\tlocal launchers = self.samSite:getLaunchers()\n\tlu.assertEquals(#launchers, 2)\n\t\n\tlocal launcher1 = launchers[1]\n\tlu.assertEquals(launcher1:getTypeName(), \"S-300PS 5P85DE ln\")\n\tlu.assertEquals(launcher1:getRange(), 90000)\n\tlu.assertEquals(launcher1:getMaximumFiringAltitude(), 25000)\n\tlu.assertEquals(launcher1:getInitialNumberOfMissiles(), 4)\nend\n\nfunction TestSyknetIADSHighDigitSAMSites:testSA10BGrumbleWith55VRUD()\n\tself.samSiteName = \"SAM-SA-10B-5V55RUD\"\n\tself:setUp()\n\tlu.assertEquals(self.samSite:getNatoName(), \"SA-10B\")\n\t\n\tlocal launchers = self.samSite:getLaunchers()\n\tlu.assertEquals(#launchers, 2)\nend\n\nfunction TestSyknetIADSHighDigitSAMSites:testSA20AGargoyleWith55VRUD()\n\tself.samSiteName = \"SAM-SA-20A-5V55RUD\"\n\tself:setUp()\n\tlu.assertEquals(self.samSite:getNatoName(), \"SA-20A\")\n\t\n\tlocal launchers = self.samSite:getLaunchers()\n\tlu.assertEquals(#launchers, 2)\nend\n\nfunction TestSyknetIADSHighDigitSAMSites:testSA17Grizzly()\n\tself.samSiteName = \"SAM-SA-17\"\n\tself:setUp()\n\tlu.assertEquals(self.samSite:getNatoName(), \"SA-17\")\n\t\n\tlocal launchers = self.samSite:getLaunchers()\n\tlu.assertEquals(#launchers, 1)\n\t\n\tlocal launcher1 = launchers[1]\n\tlu.assertEquals(launcher1:getTypeName(), \"SA-17 Buk M1-2 LN 9A310M1-2\")\n\tlu.assertEquals(launcher1:getRange(), 50000)\n\tlu.assertEquals(launcher1:getMaximumFiringAltitude(), 50000)\n\tlu.assertEquals(launcher1:getInitialNumberOfMissiles(), 4)\nend\n\nfunction TestSyknetIADSHighDigitSAMSites:testSA2GuidelineWithV7595V23()\n\tself.samSiteName = \"SAM-SA-2-V-759-5V23\"\n\tself:setUp()\n\tlu.assertEquals(self.samSite:getNatoName(), \"SA-2\")\n\n\tlocal launchers = self.samSite:getLaunchers()\n\tlu.assertEquals(#launchers, 1)\n\t\n\tlocal launcher1 = launchers[1]\n\tlu.assertEquals(launcher1:getTypeName(), \"S_75M_Volhov_V759\")\n\tlu.assertEquals(launcher1:getRange(), 56000)\n\tlu.assertEquals(launcher1:getMaximumFiringAltitude(), 30000)\n\tlu.assertEquals(launcher1:getInitialNumberOfMissiles(), 1)\t\nend\t\n\nfunction TestSyknetIADSHighDigitSAMSites:testSA3GoaWithV601P5V27()\n\tself.samSiteName = \"SAM-SA-3-V-601P-5V27\"\n\tself:setUp()\n\tlu.assertEquals(self.samSite:getNatoName(), \"SA-3\")\n\t\n\tlocal launchers = self.samSite:getLaunchers()\n\tlu.assertEquals(#launchers, 1)\n\t\n\tlocal launcher1 = launchers[1]\n\tlu.assertEquals(launcher1:getTypeName(), \"5p73 V-601P ln\")\n\tlu.assertEquals(launcher1:getRange(), 25000)\n\tlu.assertEquals(launcher1:getMaximumFiringAltitude(), 18000)\n\tlu.assertEquals(launcher1:getInitialNumberOfMissiles(), 4)\t\nend\n\nfunction TestSyknetIADSHighDigitSAMSites:testSA2GuidelineWithHQ2()\n\tself.samSiteName = \"SAM-SA-2HQ-2\"\n\tself:setUp()\n\tlu.assertEquals(self.samSite:getNatoName(), \"SA-2\")\n\t\n\tlocal launchers = self.samSite:getLaunchers()\n\tlu.assertEquals(#launchers, 1)\n\t\n\tlocal launcher1 = launchers[1]\n\tlu.assertEquals(launcher1:getTypeName(), \"HQ_2_Guideline_LN\")\n\tlu.assertEquals(launcher1:getRange(), 50000)\n\tlu.assertEquals(launcher1:getMaximumFiringAltitude(), 30000)\n\tlu.assertEquals(launcher1:getInitialNumberOfMissiles(), 1)\t\n\t\nend\n\nfunction TestSyknetIADSHighDigitSAMSites:testSA12GladiatorGiant()\n\tself.samSiteName = \"SAM-SA-12-S300V\"\n\tself:setUp()\n\tlu.assertEquals(self.samSite:getNatoName(), \"SA-12\")\n\t\n\tlocal launchers = self.samSite:getLaunchers()\n\tlu.assertEquals(#launchers, 2)\n\t\n\t\n\tlocal searchRadars = self.samSite:getSearchRadars()\n\tlu.assertEquals(#searchRadars, 2)\n\t\n\tlocal searchRadar1 = searchRadars[1]\n\tlu.assertEquals(searchRadar1:getTypeName(), \"S-300V 9S15 sr\")\n\tlu.assertEquals(searchRadar1:getMaxRangeFindingTarget(), 160497.6875)\n\n\tlocal searchRadar2 = searchRadars[2]\n\tlu.assertEquals(searchRadar2:getTypeName(), \"S-300V 9S19 sr\")\n\tlu.assertEquals(searchRadar2:getMaxRangeFindingTarget(), 160497.6875)\n\t\n\tlocal trackingRadars = self.samSite:getTrackingRadars()\n\tlu.assertEquals(#trackingRadars, 1)\n\t\n\tlocal trackingRadar1 = trackingRadars[1]\n\tlu.assertEquals(trackingRadar1:getTypeName(), \"S-300V 9S32 tr\")\n\tlu.assertEquals(trackingRadar1:getMaxRangeFindingTarget(), 160497.6875)\n\t\t\n\tlocal launcher1 = launchers[1]\n\tlu.assertEquals(launcher1:getTypeName(), \"S-300V 9A83 ln\")\n\tlu.assertEquals(launcher1:getRange(), 75000)\n\tlu.assertEquals(launcher1:getMaximumFiringAltitude(), 25000)\n\tlu.assertEquals(launcher1:getInitialNumberOfMissiles(), 4)\t\n\t\n\tlocal launcher2 = launchers[2]\n\tlu.assertEquals(launcher2:getTypeName(), \"S-300V 9A82 ln\")\n\tlu.assertEquals(launcher2:getRange(), 100000)\n\tlu.assertEquals(launcher2:getMaximumFiringAltitude(), 30000)\n\tlu.assertEquals(launcher2:getInitialNumberOfMissiles(), 2)\t\n\t\n\tlu.assertEquals(self.samSite:getHARMDetectionChance(), 90)\n\tlu.assertEquals(self.samSite:getCanEngageHARM(), true)\n\t\n\t\nend\n\nfunction TestSyknetIADSHighDigitSAMSites:testSA20BGargoyle()\n\tself.samSiteName = \"SAM-SA-20B\"\n\tself:setUp()\n\tlu.assertEquals(self.samSite:getNatoName(), \"SA-20B\")\n\t\n\tlocal searchRadars = self.samSite:getSearchRadars()\n\tlu.assertEquals(#searchRadars, 1)\n\t\n\tlocal searchRadar1 = searchRadars[1]\n\tlu.assertEquals(searchRadar1:getTypeName(), \"S-300PMU2 64H6E2 sr\")\n\tlu.assertEquals(searchRadar1:getMaxRangeFindingTarget(), 220684.3125)\n\t\n\tlocal trackingRadars = self.samSite:getTrackingRadars()\n\tlu.assertEquals(#trackingRadars, 1)\n\t\n\tlocal trackingRadar1 = trackingRadars[1]\n\tlu.assertEquals(trackingRadar1:getTypeName(), \"S-300PMU2 92H6E tr\")\n\tlu.assertEquals(trackingRadar1:getMaxRangeFindingTarget(), 220684.3125)\n\t\n\tlocal launchers = self.samSite:getLaunchers()\n\tlu.assertEquals(#launchers, 1)\n\t\n\tlocal launcher1 = launchers[1]\n\tlu.assertEquals(launcher1:getTypeName(), \"S-300PMU2 5P85SE2 ln\")\n\tlu.assertEquals(launcher1:getRange(), 200000)\n\tlu.assertEquals(launcher1:getMaximumFiringAltitude(), 27000)\n\tlu.assertEquals(launcher1:getInitialNumberOfMissiles(), 4)\n\t\n\tlu.assertEquals(self.samSite:getHARMDetectionChance(), 90)\n\tlu.assertEquals(self.samSite:getCanEngageHARM(), true)\n\t\nend\n\nend"
  },
  {
    "path": "unit-tests/luaunit.lua",
    "content": "--[[\n        luaunit.lua\n\nDescription: A unit testing framework\nHomepage: https://github.com/bluebird75/luaunit\nDevelopment by Philippe Fremy <phil@freehackers.org>\nBased on initial work of Ryu, Gwang (http://www.gpgstudy.com/gpgiki/LuaUnit)\nLicense: BSD License, see LICENSE.txt\n]]--\n\n--require(\"math\")\nlocal M={}\n\n--- update for DCS\nioWrapper = {}\n\nioWrapper.stdout = {}\n\nfunction ioWrapper.stdout:write(str)\n\tif self.string == nil then\n\t\tself.string = str\n\telse\n\t\tself.string = self.string..str\n\tend\nend\nfunction ioWrapper.stdout:flush()\n\tenv.info(self.string)\n\tself.string = nil\nend\n--- end update for DCS\n\n-- private exported functions (for testing)\nM.private = {}\n\nM.VERSION='3.4-dev'\nM._VERSION=M.VERSION -- For LuaUnit v2 compatibility\n\n-- a version which distinguish between regular Lua and LuaJit\nM._LUAVERSION = (jit and jit.version) or _VERSION\n\n--[[ Some people like assertEquals( actual, expected ) and some people prefer\nassertEquals( expected, actual ).\n]]--\nM.ORDER_ACTUAL_EXPECTED = true\nM.PRINT_TABLE_REF_IN_ERROR_MSG = false\nM.LINE_LENGTH = 80\nM.TABLE_DIFF_ANALYSIS_THRESHOLD = 10    -- display deep analysis for more than 10 items\nM.LIST_DIFF_ANALYSIS_THRESHOLD  = 10    -- display deep analysis for more than 10 items\n\n--[[ EPS is meant to help with Lua's floating point math in simple corner\ncases like almostEquals(1.1-0.1, 1), which may not work as-is (e.g. on numbers\nwith rational binary representation) if the user doesn't provide some explicit\nerror margin.\n\nThe default margin used by almostEquals() in such cases is EPS; and since\nLua may be compiled with different numeric precisions (single vs. double), we\ntry to select a useful default for it dynamically. Note: If the initial value\nis not acceptable, it can be changed by the user to better suit specific needs.\n\nSee also: https://en.wikipedia.org/wiki/Machine_epsilon\n]]\nM.EPS = 2^-52 -- = machine epsilon for \"double\", ~2.22E-16\nif math.abs(1.1 - 1 - 0.1) > M.EPS then\n    -- rounding error is above EPS, assume single precision\n    M.EPS = 2^-23 -- = machine epsilon for \"float\", ~1.19E-07\nend\n\n-- set this to false to debug luaunit\nlocal STRIP_LUAUNIT_FROM_STACKTRACE = true\n\nM.VERBOSITY_DEFAULT = 10\nM.VERBOSITY_LOW     = 1\nM.VERBOSITY_QUIET   = 0\nM.VERBOSITY_VERBOSE = 20\nM.DEFAULT_DEEP_ANALYSIS = nil\nM.FORCE_DEEP_ANALYSIS   = true\nM.DISABLE_DEEP_ANALYSIS = false\n\n-- set EXPORT_ASSERT_TO_GLOBALS to have all asserts visible as global values\n-- EXPORT_ASSERT_TO_GLOBALS = true\n\n-- we need to keep a copy of the script args before it is overriden\nlocal cmdline_argv = rawget(_G, \"arg\")\n\nM.FAILURE_PREFIX = 'LuaUnit test FAILURE: ' -- prefix string for failed tests\nM.SUCCESS_PREFIX = 'LuaUnit test SUCCESS: ' -- prefix string for successful tests finished early\nM.SKIP_PREFIX    = 'LuaUnit test SKIP:    ' -- prefix string for skipped tests\n\n\n\nM.USAGE=[[Usage: lua <your_test_suite.lua> [options] [testname1 [testname2] ... ]\nOptions:\n  -h, --help:             Print this help\n  --version:              Print version information\n  -v, --verbose:          Increase verbosity\n  -q, --quiet:            Set verbosity to minimum\n  -e, --error:            Stop on first error\n  -f, --failure:          Stop on first failure or error\n  -s, --shuffle:          Shuffle tests before running them\n  -o, --output OUTPUT:    Set output type to OUTPUT\n                          Possible values: text, tap, junit, nil\n  -n, --name NAME:        For junit only, mandatory name of xml file\n  -r, --repeat NUM:       Execute all tests NUM times, e.g. to trig the JIT\n  -p, --pattern PATTERN:  Execute all test names matching the Lua PATTERN\n                          May be repeated to include several patterns\n                          Make sure you escape magic chars like +? with %\n  -x, --exclude PATTERN:  Exclude all test names matching the Lua PATTERN\n                          May be repeated to exclude several patterns\n                          Make sure you escape magic chars like +? with %\n  testname1, testname2, ... : tests to run in the form of testFunction,\n                              TestClass or TestClass.testMethod\n]]\n\nlocal is_equal -- defined here to allow calling from mismatchFormattingPureList\n\n----------------------------------------------------------------\n--\n--                 general utility functions\n--\n----------------------------------------------------------------\n\nlocal function pcall_or_abort(func, ...)\n    -- unpack is a global function for Lua 5.1, otherwise use table.unpack\n    local unpack = rawget(_G, \"unpack\") or table.unpack\n    local result = {pcall(func, ...)}\n    if not result[1] then\n        -- an error occurred\n        env.info(result[2]) -- error message\n        env.info()\n        env.info(M.USAGE)\n      --  os.exit(-1)\n    end\n    return unpack(result, 2)\nend\n\nlocal crossTypeOrdering = {\n    number = 1, boolean = 2, string = 3, table = 4, other = 5\n}\nlocal crossTypeComparison = {\n    number = function(a, b) return a < b end,\n    string = function(a, b) return a < b end,\n    other = function(a, b) return tostring(a) < tostring(b) end,\n}\n\nlocal function crossTypeSort(a, b)\n    local type_a, type_b = type(a), type(b)\n    if type_a == type_b then\n        local func = crossTypeComparison[type_a] or crossTypeComparison.other\n        return func(a, b)\n    end\n    type_a = crossTypeOrdering[type_a] or crossTypeOrdering.other\n    type_b = crossTypeOrdering[type_b] or crossTypeOrdering.other\n    return type_a < type_b\nend\n\nlocal function __genSortedIndex( t )\n    -- Returns a sequence consisting of t's keys, sorted.\n    local sortedIndex = {}\n\n    for key,_ in pairs(t) do\n        table.insert(sortedIndex, key)\n    end\n\n    table.sort(sortedIndex, crossTypeSort)\n    return sortedIndex\nend\nM.private.__genSortedIndex = __genSortedIndex\n\nlocal function sortedNext(state, control)\n    -- Equivalent of the next() function of table iteration, but returns the\n    -- keys in sorted order (see __genSortedIndex and crossTypeSort).\n    -- The state is a temporary variable during iteration and contains the\n    -- sorted key table (state.sortedIdx). It also stores the last index (into\n    -- the keys) used by the iteration, to find the next one quickly.\n    local key\n\n    --env.info(\"sortedNext: control = \"..tostring(control) )\n    if control == nil then\n        -- start of iteration\n        state.count = #state.sortedIdx\n        state.lastIdx = 1\n        key = state.sortedIdx[1]\n        return key, state.t[key]\n    end\n\n    -- normally, we expect the control variable to match the last key used\n    if control ~= state.sortedIdx[state.lastIdx] then\n        -- strange, we have to find the next value by ourselves\n        -- the key table is sorted in crossTypeSort() order! -> use bisection\n        local lower, upper = 1, state.count\n        repeat\n            state.lastIdx = math.modf((lower + upper) / 2)\n            key = state.sortedIdx[state.lastIdx]\n            if key == control then\n                break -- key found (and thus prev index)\n            end\n            if crossTypeSort(key, control) then\n                -- key < control, continue search \"right\" (towards upper bound)\n                lower = state.lastIdx + 1\n            else\n                -- key > control, continue search \"left\" (towards lower bound)\n                upper = state.lastIdx - 1\n            end\n        until lower > upper\n        if lower > upper then -- only true if the key wasn't found, ...\n            state.lastIdx = state.count -- ... so ensure no match in code below\n        end\n    end\n\n    -- proceed by retrieving the next value (or nil) from the sorted keys\n    state.lastIdx = state.lastIdx + 1\n    key = state.sortedIdx[state.lastIdx]\n    if key then\n        return key, state.t[key]\n    end\n\n    -- getting here means returning `nil`, which will end the iteration\nend\n\nlocal function sortedPairs(tbl)\n    -- Equivalent of the pairs() function on tables. Allows to iterate in\n    -- sorted order. As required by \"generic for\" loops, this will return the\n    -- iterator (function), an \"invariant state\", and the initial control value.\n    -- (see http://www.lua.org/pil/7.2.html)\n    return sortedNext, {t = tbl, sortedIdx = __genSortedIndex(tbl)}, nil\nend\nM.private.sortedPairs = sortedPairs\n\n-- seed the random with a strongly varying seed\n--math.randomseed(os.clock()*1E11)\n\nlocal function randomizeTable( t )\n    -- randomize the item orders of the table t\n    for i = #t, 2, -1 do\n        local j = math.random(i)\n        if i ~= j then\n            t[i], t[j] = t[j], t[i]\n        end\n    end\nend\nM.private.randomizeTable = randomizeTable\n\nlocal function strsplit(delimiter, text)\n-- Split text into a list consisting of the strings in text, separated\n-- by strings matching delimiter (which may _NOT_ be a pattern).\n-- Example: strsplit(\", \", \"Anna, Bob, Charlie, Dolores\")\n    if delimiter == \"\" or delimiter == nil then -- this would result in endless loops\n        error(\"delimiter is nil or empty string!\")\n    end\n    if text == nil then\n        return nil\n    end\n\n    local list, pos, first, last = {}, 1\n    while true do\n        first, last = text:find(delimiter, pos, true)\n        if first then -- found?\n            table.insert(list, text:sub(pos, first - 1))\n            pos = last + 1\n        else\n            table.insert(list, text:sub(pos))\n            break\n        end\n    end\n    return list\nend\nM.private.strsplit = strsplit\n\nlocal function hasNewLine( s )\n    -- return true if s has a newline\n    return (string.find(s, '\\n', 1, true) ~= nil)\nend\nM.private.hasNewLine = hasNewLine\n\nlocal function prefixString( prefix, s )\n    -- Prefix all the lines of s with prefix\n    return prefix .. string.gsub(s, '\\n', '\\n' .. prefix)\nend\nM.private.prefixString = prefixString\n\nlocal function strMatch(s, pattern, start, final )\n    -- return true if s matches completely the pattern from index start to index end\n    -- return false in every other cases\n    -- if start is nil, matches from the beginning of the string\n    -- if final is nil, matches to the end of the string\n    start = start or 1\n    final = final or string.len(s)\n\n    local foundStart, foundEnd = string.find(s, pattern, start, false)\n    return foundStart == start and foundEnd == final\nend\nM.private.strMatch = strMatch\n\nlocal function patternFilter(patterns, expr)\n    -- Run `expr` through the inclusion and exclusion rules defined in patterns\n    -- and return true if expr shall be included, false for excluded.\n    -- Inclusion pattern are defined as normal patterns, exclusions \n    -- patterns start with `!` and are followed by a normal pattern\n\n    -- result: nil = UNKNOWN (not matched yet), true = ACCEPT, false = REJECT\n    -- default: true if no explicit \"include\" is found, set to false otherwise\n    local default, result = true, nil\n\n    if patterns ~= nil then\n        for _, pattern in ipairs(patterns) do\n            local exclude = pattern:sub(1,1) == '!'\n            if exclude then\n                pattern = pattern:sub(2)\n            else\n                -- at least one include pattern specified, a match is required\n                default = false\n            end\n            -- env.info('pattern: ',pattern)\n            -- env.info('exclude: ',exclude)\n            -- env.info('default: ',default)\n\n            if string.find(expr, pattern) then\n                -- set result to false when excluding, true otherwise\n                result = not exclude\n            end\n        end\n    end\n\n    if result ~= nil then\n        return result\n    end\n    return default\nend\nM.private.patternFilter = patternFilter\n\nlocal function xmlEscape( s )\n    -- Return s escaped for XML attributes\n    -- escapes table:\n    -- \"   &quot;\n    -- '   &apos;\n    -- <   &lt;\n    -- >   &gt;\n    -- &   &amp;\n\n    return string.gsub( s, '.', {\n        ['&'] = \"&amp;\",\n        ['\"'] = \"&quot;\",\n        [\"'\"] = \"&apos;\",\n        ['<'] = \"&lt;\",\n        ['>'] = \"&gt;\",\n    } )\nend\nM.private.xmlEscape = xmlEscape\n\nlocal function xmlCDataEscape( s )\n    -- Return s escaped for CData section, escapes: \"]]>\"\n    return string.gsub( s, ']]>', ']]&gt;' )\nend\nM.private.xmlCDataEscape = xmlCDataEscape\n\nlocal function stripLuaunitTrace( stackTrace )\n    --[[\n    -- Example of  a traceback:\n    <<stack traceback:\n        example_with_luaunit.lua:130: in function 'test2_withFailure'\n        ./luaunit.lua:1449: in function <./luaunit.lua:1449>\n        [C]: in function 'xpcall'\n        ./luaunit.lua:1449: in function 'protectedCall'\n        ./luaunit.lua:1508: in function 'execOneFunction'\n        ./luaunit.lua:1596: in function 'runSuiteByInstances'\n        ./luaunit.lua:1660: in function 'runSuiteByNames'\n        ./luaunit.lua:1736: in function 'runSuite'\n        example_with_luaunit.lua:140: in main chunk\n        [C]: in ?>>\n\n        Other example:\n    <<stack traceback:\n        ./luaunit.lua:545: in function 'assertEquals'\n        example_with_luaunit.lua:58: in function 'TestToto.test7'\n        ./luaunit.lua:1517: in function <./luaunit.lua:1517>\n        [C]: in function 'xpcall'\n        ./luaunit.lua:1517: in function 'protectedCall'\n        ./luaunit.lua:1578: in function 'execOneFunction'\n        ./luaunit.lua:1677: in function 'runSuiteByInstances'\n        ./luaunit.lua:1730: in function 'runSuiteByNames'\n        ./luaunit.lua:1806: in function 'runSuite'\n        example_with_luaunit.lua:140: in main chunk\n        [C]: in ?>>\n\n    <<stack traceback:\n        luaunit2/example_with_luaunit.lua:124: in function 'test1_withFailure'\n        luaunit2/luaunit.lua:1532: in function <luaunit2/luaunit.lua:1532>\n        [C]: in function 'xpcall'\n        luaunit2/luaunit.lua:1532: in function 'protectedCall'\n        luaunit2/luaunit.lua:1591: in function 'execOneFunction'\n        luaunit2/luaunit.lua:1679: in function 'runSuiteByInstances'\n        luaunit2/luaunit.lua:1743: in function 'runSuiteByNames'\n        luaunit2/luaunit.lua:1819: in function 'runSuite'\n        luaunit2/example_with_luaunit.lua:140: in main chunk\n        [C]: in ?>>\n\n\n    -- first line is \"stack traceback\": KEEP\n    -- next line may be luaunit line: REMOVE\n    -- next lines are call in the program under testOk: REMOVE\n    -- next lines are calls from luaunit to call the program under test: KEEP\n\n    -- Strategy:\n    -- keep first line\n    -- remove lines that are part of luaunit\n    -- kepp lines until we hit a luaunit line\n    ]]\n\n    local function isLuaunitInternalLine( s )\n        -- return true if line of stack trace comes from inside luaunit\n        return s:find('[/\\\\]luaunit%.lua:%d+: ') ~= nil\n    end\n\n    -- env.info( '<<'..stackTrace..'>>' )\n\n    local t = strsplit( '\\n', stackTrace )\n    -- env.info( prettystr(t) )\n\n    local idx = 2\n\n    -- remove lines that are still part of luaunit\n    while t[idx] and isLuaunitInternalLine( t[idx] ) do\n        -- env.info('Removing : '..t[idx] )\n        table.remove(t, idx)\n    end\n\n    -- keep lines until we hit luaunit again\n    while t[idx] and (not isLuaunitInternalLine(t[idx])) do\n        -- env.info('Keeping : '..t[idx] )\n        idx = idx + 1\n    end\n\n    -- remove remaining luaunit lines\n    while t[idx] do\n        -- env.info('Removing : '..t[idx] )\n        table.remove(t, idx)\n    end\n\n    -- env.info( prettystr(t) )\n    return table.concat( t, '\\n')\n\nend\nM.private.stripLuaunitTrace = stripLuaunitTrace\n\n\nlocal function prettystr_sub(v, indentLevel, printTableRefs, cycleDetectTable )\n    local type_v = type(v)\n    if \"string\" == type_v  then\n        -- use clever delimiters according to content:\n        -- enclose with single quotes if string contains \", but no '\n        if v:find('\"', 1, true) and not v:find(\"'\", 1, true) then\n            return \"'\" .. v .. \"'\"\n        end\n        -- use double quotes otherwise, escape embedded \"\n        return '\"' .. v:gsub('\"', '\\\\\"') .. '\"'\n\n    elseif \"table\" == type_v then\n        --if v.__class__ then\n        --    return string.gsub( tostring(v), 'table', v.__class__ )\n        --end\n        return M.private._table_tostring(v, indentLevel, printTableRefs, cycleDetectTable)\n\n    elseif \"number\" == type_v then\n        -- eliminate differences in formatting between various Lua versions\n        if v ~= v then\n            return \"#NaN\" -- \"not a number\"\n        end\n        if v == math.huge then\n            return \"#Inf\" -- \"infinite\"\n        end\n        if v == -math.huge then\n            return \"-#Inf\"\n        end\n        if _VERSION == \"Lua 5.3\" then\n            local i = math.tointeger(v)\n            if i then\n                return tostring(i)\n            end\n        end\n    end\n\n    return tostring(v)\nend\n\nlocal function prettystr( v )\n    --[[ Pretty string conversion, to display the full content of a variable of any type.\n\n    * string are enclosed with \" by default, or with ' if string contains a \"\n    * tables are expanded to show their full content, with indentation in case of nested tables\n    ]]--\n    local cycleDetectTable = {}\n    local s = prettystr_sub(v, 1, M.PRINT_TABLE_REF_IN_ERROR_MSG, cycleDetectTable)\n    if cycleDetectTable.detected and not M.PRINT_TABLE_REF_IN_ERROR_MSG then\n        -- some table contain recursive references,\n        -- so we must recompute the value by including all table references\n        -- else the result looks like crap\n        cycleDetectTable = {}\n        s = prettystr_sub(v, 1, true, cycleDetectTable)\n    end\n    return s\nend\nM.prettystr = prettystr\n\nfunction M.adjust_err_msg_with_iter( err_msg, iter_msg )\n    --[[ Adjust the error message err_msg: trim the FAILURE_PREFIX or SUCCESS_PREFIX information if needed, \n    add the iteration message if any and return the result.\n\n    err_msg:  string, error message captured with pcall\n    iter_msg: a string describing the current iteration (\"iteration N\") or nil\n              if there is no iteration in this test.\n\n    Returns: (new_err_msg, test_status)\n        new_err_msg: string, adjusted error message, or nil in case of success\n        test_status: M.NodeStatus.FAIL, SUCCESS or ERROR according to the information\n                     contained in the error message.\n    ]]\n    if iter_msg then\n        iter_msg = iter_msg..', '\n    else\n        iter_msg = ''\n    end\n\n    local RE_FILE_LINE = '.*:%d+: '\n\n    -- error message is not necessarily a string, \n    -- so convert the value to string with prettystr()\n    if type( err_msg ) ~= 'string' then\n        err_msg = prettystr( err_msg )\n    end\n\n    if (err_msg:find( M.SUCCESS_PREFIX ) == 1) or err_msg:match( '('..RE_FILE_LINE..')' .. M.SUCCESS_PREFIX .. \".*\" ) then\n        -- test finished early with success()\n        return nil, M.NodeStatus.SUCCESS\n    end\n\n    if (err_msg:find( M.SKIP_PREFIX ) == 1) or (err_msg:match( '('..RE_FILE_LINE..')' .. M.SKIP_PREFIX .. \".*\" ) ~= nil) then\n        -- substitute prefix by iteration message\n        err_msg = err_msg:gsub('.*'..M.SKIP_PREFIX, iter_msg, 1)\n        -- env.info(\"failure detected\")\n        return err_msg, M.NodeStatus.SKIP\n    end\n\n    if (err_msg:find( M.FAILURE_PREFIX ) == 1) or (err_msg:match( '('..RE_FILE_LINE..')' .. M.FAILURE_PREFIX .. \".*\" ) ~= nil) then\n        -- substitute prefix by iteration message\n        err_msg = err_msg:gsub(M.FAILURE_PREFIX, iter_msg, 1)\n        -- env.info(\"failure detected\")\n        return err_msg, M.NodeStatus.FAIL\n    end\n\n\n\n    -- env.info(\"error detected\")\n    -- regular error, not a failure\n    if iter_msg then\n        local match\n        -- \"./test\\\\test_luaunit.lua:2241: some error msg\n        match = err_msg:match( '(.*:%d+: ).*' ) \n        if match then\n            err_msg = err_msg:gsub( match, match .. iter_msg )\n        else\n            -- no file:line: infromation, just add the iteration info at the beginning of the line\n            err_msg = iter_msg .. err_msg\n        end\n    end\n    return err_msg, M.NodeStatus.ERROR\nend\n\nlocal function tryMismatchFormatting( table_a, table_b, doDeepAnalysis )\n    --[[\n    Prepares a nice error message when comparing tables, performing a deeper \n    analysis.\n\n    Arguments:\n    * table_a, table_b: tables to be compared\n    * doDeepAnalysis:\n        M.DEFAULT_DEEP_ANALYSIS: (the default if not specified) perform deep analysis only for big lists and big dictionnaries\n        M.FORCE_DEEP_ANALYSIS  : always perform deep analysis\n        M.DISABLE_DEEP_ANALYSIS: never perform deep analysis\n\n    Returns: {success, result}\n    * success: false if deep analysis could not be performed \n               in this case, just use standard assertion message\n    * result: if success is true, a multi-line string with deep analysis of the two lists\n    ]]\n\n    -- check if table_a & table_b are suitable for deep analysis\n    if type(table_a) ~= 'table' or type(table_b) ~= 'table' then\n        return false\n    end\n\n    if doDeepAnalysis == M.DISABLE_DEEP_ANALYSIS then\n        return false\n    end\n\n    local len_a, len_b, isPureList = #table_a, #table_b, true\n\n    for k1, v1 in pairs(table_a) do\n        if type(k1) ~= 'number' or k1 > len_a then\n            -- this table a mapping\n            isPureList = false\n            break\n        end\n    end\n\n    if isPureList then\n        for k2, v2 in pairs(table_b) do\n            if type(k2) ~= 'number' or k2 > len_b then\n                -- this table a mapping\n                isPureList = false\n                break\n            end\n        end\n    end\n\n    if isPureList and math.min(len_a, len_b) < M.LIST_DIFF_ANALYSIS_THRESHOLD then\n        if not (doDeepAnalysis == M.FORCE_DEEP_ANALYSIS) then\n            return false\n        end\n    end\n\n    if isPureList then\n        return M.private.mismatchFormattingPureList( table_a, table_b )\n    else\n        -- only work on mapping for the moment\n        -- return M.private.mismatchFormattingMapping( table_a, table_b, doDeepAnalysis )\n        return false\n    end\nend\nM.private.tryMismatchFormatting = tryMismatchFormatting\n\nlocal function getTaTbDescr()\n    if not M.ORDER_ACTUAL_EXPECTED then\n        return 'expected', 'actual'\n    end\n    return 'actual', 'expected'\nend\n\nlocal function extendWithStrFmt( res, ... )\n    table.insert( res, string.format( ... ) )\nend\n\nlocal function mismatchFormattingMapping( table_a, table_b, doDeepAnalysis )\n    --[[\n    Prepares a nice error message when comparing tables which are not pure lists, performing a deeper \n    analysis.\n\n    Returns: {success, result}\n    * success: false if deep analysis could not be performed \n               in this case, just use standard assertion message\n    * result: if success is true, a multi-line string with deep analysis of the two lists\n    ]]\n\n    -- disable for the moment\n    --[[\n    local result = {}\n    local descrTa, descrTb = getTaTbDescr()\n\n    local keysCommon = {}\n    local keysOnlyTa = {}\n    local keysOnlyTb = {}\n    local keysDiffTaTb = {}\n\n    local k, v\n\n    for k,v in pairs( table_a ) do\n        if is_equal( v, table_b[k] ) then\n            table.insert( keysCommon, k )\n        else \n            if table_b[k] == nil then\n                table.insert( keysOnlyTa, k )\n            else\n                table.insert( keysDiffTaTb, k )\n            end\n        end\n    end\n\n    for k,v in pairs( table_b ) do\n        if not is_equal( v, table_a[k] ) and table_a[k] == nil then\n            table.insert( keysOnlyTb, k )\n        end\n    end\n\n    local len_a = #keysCommon + #keysDiffTaTb + #keysOnlyTa\n    local len_b = #keysCommon + #keysDiffTaTb + #keysOnlyTb\n    local limited_display = (len_a < 5 or len_b < 5)\n\n    if math.min(len_a, len_b) < M.TABLE_DIFF_ANALYSIS_THRESHOLD then\n        return false\n    end\n\n    if not limited_display then\n        if len_a == len_b then\n            extendWithStrFmt( result, 'Table A (%s) and B (%s) both have %d items', descrTa, descrTb, len_a )\n        else\n            extendWithStrFmt( result, 'Table A (%s) has %d items and table B (%s) has %d items', descrTa, len_a, descrTb, len_b )\n            end\n\n        if #keysCommon == 0 and #keysDiffTaTb == 0 then\n            table.insert( result, 'Table A and B have no keys in common, they are totally different')\n        else\n            local s_other = 'other '\n            if #keysCommon then\n                extendWithStrFmt( result, 'Table A and B have %d identical items', #keysCommon )\n            else\n                table.insert( result, 'Table A and B have no identical items' )\n                s_other = ''\n            end\n\n            if #keysDiffTaTb ~= 0 then\n                result[#result] = string.format( '%s and %d items differing present in both tables', result[#result], #keysDiffTaTb)\n            else\n                result[#result] = string.format( '%s and no %sitems differing present in both tables', result[#result], s_other, #keysDiffTaTb)\n            end\n        end\n\n        extendWithStrFmt( result, 'Table A has %d keys not present in table B and table B has %d keys not present in table A', #keysOnlyTa, #keysOnlyTb ) \n    end\n\n    local function keytostring(k)\n        if \"string\" == type(k) and k:match(\"^[_%a][_%w]*$\") then\n            return k\n        end\n        return prettystr(k)\n    end\n\n    if #keysDiffTaTb ~= 0 then\n        table.insert( result, 'Items differing in A and B:')\n        for k,v in sortedPairs( keysDiffTaTb ) do\n            extendWithStrFmt( result, '  - A[%s]: %s', keytostring(v), prettystr(table_a[v]) )\n            extendWithStrFmt( result, '  + B[%s]: %s', keytostring(v), prettystr(table_b[v]) )\n        end\n    end    \n\n    if #keysOnlyTa ~= 0 then\n        table.insert( result, 'Items only in table A:' )\n        for k,v in sortedPairs( keysOnlyTa ) do\n            extendWithStrFmt( result, '  - A[%s]: %s', keytostring(v), prettystr(table_a[v]) )\n        end\n    end\n\n    if #keysOnlyTb ~= 0 then\n        table.insert( result, 'Items only in table B:' )\n        for k,v in sortedPairs( keysOnlyTb ) do\n            extendWithStrFmt( result, '  + B[%s]: %s', keytostring(v), prettystr(table_b[v]) )\n        end\n    end\n\n    if #keysCommon ~= 0 then\n        table.insert( result, 'Items common to A and B:')\n        for k,v in sortedPairs( keysCommon ) do\n            extendWithStrFmt( result, '  = A and B [%s]: %s', keytostring(v), prettystr(table_a[v]) )\n        end\n    end    \n\n    return true, table.concat( result, '\\n')\n    ]]\nend\nM.private.mismatchFormattingMapping = mismatchFormattingMapping\n\nlocal function mismatchFormattingPureList( table_a, table_b )\n    --[[\n    Prepares a nice error message when comparing tables which are lists, performing a deeper \n    analysis.\n\n    Returns: {success, result}\n    * success: false if deep analysis could not be performed \n               in this case, just use standard assertion message\n    * result: if success is true, a multi-line string with deep analysis of the two lists\n    ]]\n    local result, descrTa, descrTb = {}, getTaTbDescr()\n\n    local len_a, len_b, refa, refb = #table_a, #table_b, '', ''\n    if M.PRINT_TABLE_REF_IN_ERROR_MSG then\n        refa, refb = string.format( '<%s> ', M.private.table_ref(table_a)), string.format('<%s> ', M.private.table_ref(table_b) )\n    end\n    local longest, shortest = math.max(len_a, len_b), math.min(len_a, len_b)\n    local deltalv  = longest - shortest\n\n    local commonUntil = shortest\n    for i = 1, shortest do\n        if not is_equal(table_a[i], table_b[i]) then\n            commonUntil = i - 1\n            break\n        end\n    end\n\n    local commonBackTo = shortest - 1\n    for i = 0, shortest - 1 do\n        if not is_equal(table_a[len_a-i], table_b[len_b-i]) then\n            commonBackTo = i - 1\n            break\n        end\n    end\n\n\n    table.insert( result, 'List difference analysis:' )    \n    if len_a == len_b then\n        -- TODO: handle expected/actual naming\n        extendWithStrFmt( result, '* lists %sA (%s) and %sB (%s) have the same size', refa, descrTa, refb, descrTb )\n    else \n        extendWithStrFmt( result, '* list sizes differ: list %sA (%s) has %d items, list %sB (%s) has %d items', refa, descrTa, len_a, refb, descrTb, len_b )\n    end\n\n    extendWithStrFmt( result, '* lists A and B start differing at index %d', commonUntil+1 ) \n    if commonBackTo >= 0 then\n        if deltalv > 0 then\n            extendWithStrFmt( result, '* lists A and B are equal again from index %d for A, %d for B', len_a-commonBackTo, len_b-commonBackTo )\n        else\n            extendWithStrFmt( result, '* lists A and B are equal again from index %d', len_a-commonBackTo )\n        end\n    end\n\n    local function insertABValue(ai, bi)\n        bi = bi or ai\n        if is_equal( table_a[ai], table_b[bi]) then\n            return extendWithStrFmt( result, '  = A[%d], B[%d]: %s', ai, bi, prettystr(table_a[ai]) )\n        else\n            extendWithStrFmt( result, '  - A[%d]: %s', ai, prettystr(table_a[ai]))\n            extendWithStrFmt( result, '  + B[%d]: %s', bi, prettystr(table_b[bi]))\n        end\n    end\n\n    -- common parts to list A & B, at the beginning\n    if commonUntil > 0 then\n        table.insert( result, '* Common parts:' )\n        for i = 1, commonUntil do\n            insertABValue( i )\n        end\n    end\n\n    -- diffing parts to list A & B\n    if commonUntil < shortest - commonBackTo - 1 then\n        table.insert( result, '* Differing parts:' )\n        for i = commonUntil + 1, shortest - commonBackTo - 1 do\n            insertABValue( i )\n        end\n    end\n\n    -- display indexes of one list, with no match on other list\n    if shortest - commonBackTo <= longest - commonBackTo - 1 then\n        table.insert( result, '* Present only in one list:' )\n        for i = shortest - commonBackTo, longest - commonBackTo - 1 do\n            if len_a > len_b then\n                extendWithStrFmt( result, '  - A[%d]: %s', i, prettystr(table_a[i]) )\n                -- table.insert( result, '+ (no matching B index)')\n            else\n                -- table.insert( result, '- no matching A index')\n                extendWithStrFmt( result, '  + B[%d]: %s', i, prettystr(table_b[i]) )\n            end\n        end\n    end\n\n    -- common parts to list A & B, at the end\n    if commonBackTo >= 0 then\n        table.insert( result, '* Common parts at the end of the lists' )\n        for i = longest - commonBackTo, longest do\n            if len_a > len_b then\n                insertABValue( i, i-deltalv )\n            else\n                insertABValue( i-deltalv, i )\n            end\n        end\n    end\n\n    return true, table.concat( result, '\\n')\nend\nM.private.mismatchFormattingPureList = mismatchFormattingPureList\n\nlocal function prettystrPairs(value1, value2, suffix_a, suffix_b)\n    --[[\n    This function helps with the recurring task of constructing the \"expected\n    vs. actual\" error messages. It takes two arbitrary values and formats\n    corresponding strings with prettystr().\n\n    To keep the (possibly complex) output more readable in case the resulting\n    strings contain line breaks, they get automatically prefixed with additional\n    newlines. Both suffixes are optional (default to empty strings), and get\n    appended to the \"value1\" string. \"suffix_a\" is used if line breaks were\n    encountered, \"suffix_b\" otherwise.\n\n    Returns the two formatted strings (including padding/newlines).\n    ]]\n    local str1, str2 = prettystr(value1), prettystr(value2)\n    if hasNewLine(str1) or hasNewLine(str2) then\n        -- line break(s) detected, add padding\n        return \"\\n\" .. str1 .. (suffix_a or \"\"), \"\\n\" .. str2\n    end\n    return str1 .. (suffix_b or \"\"), str2\nend\nM.private.prettystrPairs = prettystrPairs\n\nlocal UNKNOWN_REF = 'table 00-unknown ref'\nlocal ref_generator = { value=1, [UNKNOWN_REF]=0 }\n\nlocal function table_ref( t )\n    -- return the default tostring() for tables, with the table ID, even if the table has a metatable\n    -- with the __tostring converter\n    local ref = ''\n    local mt = getmetatable( t )\n    if mt == nil then\n        ref = tostring(t)\n    else\n        local success, result\n        success, result = pcall(setmetatable, t, nil)\n        if not success then\n            -- protected table, if __tostring is defined, we can\n            -- not get the reference. And we can not know in advance.\n            ref = tostring(t) \n            if not ref:match( 'table: 0?x?[%x]+' ) then\n                return UNKNOWN_REF\n            end\n        else\n            ref = tostring(t)\n            setmetatable( t, mt )\n        end\n    end\n    -- strip the \"table: \" part\n    ref = ref:sub(8)\n    if ref ~= UNKNOWN_REF and ref_generator[ref] == nil then\n        -- Create a new reference number\n        ref_generator[ref] = ref_generator.value\n        ref_generator.value = ref_generator.value+1\n    end\n    if M.PRINT_TABLE_REF_IN_ERROR_MSG then\n        return string.format('table %02d-%s', ref_generator[ref], ref)\n    else\n        return string.format('table %02d', ref_generator[ref])\n    end\nend\nM.private.table_ref = table_ref\n\nlocal TABLE_TOSTRING_SEP = \", \"\nlocal TABLE_TOSTRING_SEP_LEN = string.len(TABLE_TOSTRING_SEP)\n\nlocal function _table_tostring( tbl, indentLevel, printTableRefs, cycleDetectTable )\n    printTableRefs = printTableRefs or M.PRINT_TABLE_REF_IN_ERROR_MSG\n    cycleDetectTable = cycleDetectTable or {}\n    cycleDetectTable[tbl] = true\n\n    local result, dispOnMultLines = {}, false\n\n    -- like prettystr but do not enclose with \"\" if the string is just alphanumerical\n    -- this is better for displaying table keys who are often simple strings\n    local function keytostring(k)\n        if \"string\" == type(k) and k:match(\"^[_%a][_%w]*$\") then\n            return k\n        end\n        return prettystr_sub(k, indentLevel+1, printTableRefs, cycleDetectTable)\n    end\n\n    local mt = getmetatable( tbl )\n\n    if mt and mt.__tostring then\n        -- if table has a __tostring() function in its metatable, use it to display the table\n        -- else, compute a regular table\n        result = tostring(tbl)\n        if type(result) ~= 'string' then\n            return string.format( '<invalid tostring() result: \"%s\" >', prettystr(result) )\n        end\n        result = strsplit( '\\n', result )\n        return M.private._table_tostring_format_multiline_string( result, indentLevel )\n\n    else\n        -- no metatable, compute the table representation\n\n        local entry, count, seq_index = nil, 0, 1\n        for k, v in sortedPairs( tbl ) do\n\n            -- key part\n            if k == seq_index then\n                -- for the sequential part of tables, we'll skip the \"<key>=\" output\n                entry = ''\n                seq_index = seq_index + 1\n            elseif cycleDetectTable[k] then\n                -- recursion in the key detected\n                cycleDetectTable.detected = true\n                entry = \"<\"..table_ref(k)..\">=\"\n            else\n                entry = keytostring(k) .. \"=\"\n            end\n\n            -- value part \n            if cycleDetectTable[v] then\n                -- recursion in the value detected!\n                cycleDetectTable.detected = true\n                entry = entry .. \"<\"..table_ref(v)..\">\"\n            else\n                entry = entry ..\n                    prettystr_sub( v, indentLevel+1, printTableRefs, cycleDetectTable )\n            end\n            count = count + 1\n            result[count] = entry\n        end\n        return M.private._table_tostring_format_result( tbl, result, indentLevel, printTableRefs )\n    end\n\nend\nM.private._table_tostring = _table_tostring -- prettystr_sub() needs it\n\nlocal function _table_tostring_format_multiline_string( tbl_str, indentLevel )\n    local indentString = '\\n'..string.rep(\"    \", indentLevel - 1)\n    return table.concat( tbl_str, indentString )\n\nend\nM.private._table_tostring_format_multiline_string = _table_tostring_format_multiline_string\n\n\nlocal function _table_tostring_format_result( tbl, result, indentLevel, printTableRefs )\n    -- final function called in _table_to_string() to format the resulting list of \n    -- string describing the table.\n\n    local dispOnMultLines = false\n\n    -- set dispOnMultLines to true if the maximum LINE_LENGTH would be exceeded with the values\n    local totalLength = 0\n    for k, v in ipairs( result ) do\n        totalLength = totalLength + string.len( v )\n        if totalLength >= M.LINE_LENGTH then\n            dispOnMultLines = true\n            break\n        end\n    end\n\n    -- set dispOnMultLines to true if the max LINE_LENGTH would be exceeded\n    -- with the values and the separators.\n    if not dispOnMultLines then\n        -- adjust with length of separator(s):\n        -- two items need 1 sep, three items two seps, ... plus len of '{}'\n        if #result > 0 then\n            totalLength = totalLength + TABLE_TOSTRING_SEP_LEN * (#result - 1)\n        end\n        dispOnMultLines = (totalLength + 2 >= M.LINE_LENGTH)\n    end\n\n    -- now reformat the result table (currently holding element strings)\n    if dispOnMultLines then\n        local indentString = string.rep(\"    \", indentLevel - 1)\n        result = {  \n                    \"{\\n    \", \n                    indentString,\n                    table.concat(result, \",\\n    \" .. indentString), \n                    \"\\n\",\n                    indentString, \n                    \"}\"\n                }\n    else\n        result = {\"{\", table.concat(result, TABLE_TOSTRING_SEP), \"}\"}\n    end\n    if printTableRefs then\n        table.insert(result, 1, \"<\"..table_ref(tbl)..\"> \") -- prepend table ref\n    end\n    return table.concat(result)\nend\nM.private._table_tostring_format_result = _table_tostring_format_result -- prettystr_sub() needs it\n\nlocal function table_findkeyof(t, element)\n    -- Return the key k of the given element in table t, so that t[k] == element\n    -- (or `nil` if element is not present within t). Note that we use our\n    -- 'general' is_equal comparison for matching, so this function should\n    -- handle table-type elements gracefully and consistently.\n    if type(t) == \"table\" then\n        for k, v in pairs(t) do\n            if is_equal(v, element) then\n                return k\n            end\n        end\n    end\n    return nil\nend\n\nlocal function _is_table_items_equals(actual, expected )\n    local type_a, type_e = type(actual), type(expected)\n\n    if type_a ~= type_e then\n        return false\n\n    elseif (type_a == 'table') --[[and (type_e == 'table')]] then\n        for k, v in pairs(actual) do\n            if table_findkeyof(expected, v) == nil then\n                return false -- v not contained in expected\n            end\n        end\n        for k, v in pairs(expected) do\n            if table_findkeyof(actual, v) == nil then\n                return false -- v not contained in actual\n            end\n        end\n        return true\n\n    elseif actual ~= expected then\n        return false\n    end\n\n    return true\nend\n\n--[[\nThis is a specialized metatable to help with the bookkeeping of recursions\nin _is_table_equals(). It provides an __index table that implements utility\nfunctions for easier management of the table. The \"cached\" method queries\nthe state of a specific (actual,expected) pair; and the \"store\" method sets\nthis state to the given value. The state of pairs not \"seen\" / visited is\nassumed to be `nil`.\n]]\nlocal _recursion_cache_MT = {\n    __index = {\n        -- Return the cached value for an (actual,expected) pair (or `nil`)\n        cached = function(t, actual, expected)\n            local subtable = t[actual] or {}\n            return subtable[expected]\n        end,\n\n        -- Store cached value for a specific (actual,expected) pair.\n        -- Returns the value, so it's easy to use for a \"tailcall\" (return ...).\n        store = function(t, actual, expected, value, asymmetric)\n            local subtable = t[actual]\n            if not subtable then\n                subtable = {}\n                t[actual] = subtable\n            end\n            subtable[expected] = value\n\n            -- Unless explicitly marked \"asymmetric\": Consider the recursion\n            -- on (expected,actual) to be equivalent to (actual,expected) by\n            -- default, and thus cache the value for both.\n            if not asymmetric then\n                t:store(expected, actual, value, true)\n            end\n\n            return value\n        end\n    }\n}\n\nlocal function _is_table_equals(actual, expected, cycleDetectTable)\n    local type_a, type_e = type(actual), type(expected)\n\n    if type_a ~= type_e then\n        return false -- different types won't match\n    end\n\n    if type_a ~= 'table' then\n        -- other typtes compare directly\n        return actual == expected\n    end\n\n    -- env.info('_is_table_equals( \\n     '..prettystr(actual)..'\\n      , '..prettystr(expected)..'\\n     , '..prettystr(recursions)..' \\n )')\n\n    cycleDetectTable = cycleDetectTable or { actual={}, expected={} }\n    if cycleDetectTable.actual[ actual ] then\n        -- oh, we hit a cycle in actual\n        if cycleDetectTable.expected[ expected ] then\n            -- uh, we hit a cycle at the same time in expected\n            -- so the two tables have similar structure\n            return true\n        end\n\n        -- cycle was hit only in actual, the structure differs from expected\n        return false\n    end\n\n    if cycleDetectTable.expected[ expected ] then\n        -- no cycle in actual, but cycle in expected\n        -- the structure differ\n        return false\n    end\n\n    -- at this point, no table cycle detected, we are\n    -- seeing this table for the first time\n\n    -- mark the cycle detection\n    cycleDetectTable.actual[ actual ] = true\n    cycleDetectTable.expected[ expected ] = true\n\n\n    local actualKeysMatched = {}\n    for k, v in pairs(actual) do\n        actualKeysMatched[k] = true -- Keep track of matched keys\n        if not _is_table_equals(v, expected[k], cycleDetectTable) then\n            -- table differs on this key\n            -- clear the cycle detection before returning\n            cycleDetectTable.actual[ actual ] = nil\n            cycleDetectTable.expected[ expected ] = nil\n            return false\n        end\n    end\n\n    for k, v in pairs(expected) do\n        if not actualKeysMatched[k] then\n            -- Found a key that we did not see in \"actual\" -> mismatch\n            -- clear the cycle detection before returning\n            cycleDetectTable.actual[ actual ] = nil\n            cycleDetectTable.expected[ expected ] = nil\n            return false\n        end\n        -- Otherwise actual[k] was already matched against v = expected[k].\n    end\n\n    -- all key match, we have a match !\n    cycleDetectTable.actual[ actual ] = nil\n    cycleDetectTable.expected[ expected ] = nil\n    return true\nend\nM.private._is_table_equals = _is_table_equals\nis_equal = _is_table_equals\n\nlocal function failure(main_msg, extra_msg_or_nil, level)\n    -- raise an error indicating a test failure\n    -- for error() compatibility we adjust \"level\" here (by +1), to report the\n    -- calling context\n    local msg\n    if type(extra_msg_or_nil) == 'string' and extra_msg_or_nil:len() > 0 then\n        msg = extra_msg_or_nil .. '\\n' .. main_msg\n    else\n        msg = main_msg\n    end\n    error(M.FAILURE_PREFIX .. msg, (level or 1) + 1)\nend\n\nlocal function fail_fmt(level, extra_msg_or_nil, ...)\n     -- failure with printf-style formatted message and given error level\n    failure(string.format(...), extra_msg_or_nil, (level or 1) + 1)\nend\nM.private.fail_fmt = fail_fmt\n\nlocal function error_fmt(level, ...)\n     -- printf-style error()\n    error(string.format(...), (level or 1) + 1)\nend\n\n----------------------------------------------------------------\n--\n--                     assertions\n--\n----------------------------------------------------------------\n\nlocal function errorMsgEquality(actual, expected, doDeepAnalysis)\n\n    if not M.ORDER_ACTUAL_EXPECTED then\n        expected, actual = actual, expected\n    end\n    if type(expected) == 'string' or type(expected) == 'table' then\n        local strExpected, strActual = prettystrPairs(expected, actual)\n        local result = string.format(\"expected: %s\\nactual: %s\", strExpected, strActual)\n\n        -- extend with mismatch analysis if possible:\n        local success, mismatchResult\n        success, mismatchResult = tryMismatchFormatting( actual, expected, doDeepAnalysis )\n        if success then \n            result = table.concat( { result, mismatchResult }, '\\n' )\n        end\n        return result\n    end\n    return string.format(\"expected: %s, actual: %s\",\n                         prettystr(expected), prettystr(actual))\nend\n\nfunction M.assertError(f, ...)\n    -- assert that calling f with the arguments will raise an error\n    -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error\n    if pcall( f, ... ) then\n        failure( \"Expected an error when calling function but no error generated\", nil, 2 )\n    end\nend\n\nfunction M.fail( msg )\n    -- stops a test due to a failure\n    failure( msg, nil, 2 )\nend\n\nfunction M.failIf( cond, msg )\n    -- Fails a test with \"msg\" if condition is true\n    if cond then\n        failure( msg, nil, 2 )\n    end\nend\n\nfunction M.skip(msg)\n    -- skip a running test\n    error(M.SKIP_PREFIX .. msg, 2)\nend\n\nfunction M.skipIf( cond, msg )\n    -- skip a running test if condition is met\n    if cond then\n        error(M.SKIP_PREFIX .. msg, 2)\n    end\nend\n\nfunction M.runOnlyIf( cond, msg )\n    -- continue a running test if condition is met, else skip it\n    if not cond then\n        error(M.SKIP_PREFIX .. prettystr(msg), 2)\n    end\nend\n\nfunction M.success()\n    -- stops a test with a success\n    error(M.SUCCESS_PREFIX, 2)\nend\n\nfunction M.successIf( cond )\n    -- stops a test with a success if condition is met\n    if cond then\n        error(M.SUCCESS_PREFIX, 2)\n    end\nend\n\n\n------------------------------------------------------------------\n--                  Equality assertions\n------------------------------------------------------------------\n\nfunction M.assertEquals(actual, expected, extra_msg_or_nil, doDeepAnalysis)\n    if type(actual) == 'table' and type(expected) == 'table' then\n        if not _is_table_equals(actual, expected) then\n            failure( errorMsgEquality(actual, expected, doDeepAnalysis), extra_msg_or_nil, 2 )\n        end\n    elseif type(actual) ~= type(expected) then\n        failure( errorMsgEquality(actual, expected), extra_msg_or_nil, 2 )\n    elseif actual ~= expected then\n        failure( errorMsgEquality(actual, expected), extra_msg_or_nil, 2 )\n    end\nend\n\nfunction M.almostEquals( actual, expected, margin )\n    if type(actual) ~= 'number' or type(expected) ~= 'number' or type(margin) ~= 'number' then\n        error_fmt(3, 'almostEquals: must supply only number arguments.\\nArguments supplied: %s, %s, %s',\n            prettystr(actual), prettystr(expected), prettystr(margin))\n    end\n    if margin < 0 then\n        error('almostEquals: margin must not be negative, current value is ' .. margin, 3)\n    end\n    return math.abs(expected - actual) <= margin\nend\n\nfunction M.assertAlmostEquals( actual, expected, margin, extra_msg_or_nil )\n    -- check that two floats are close by margin\n    margin = margin or M.EPS\n    if not M.almostEquals(actual, expected, margin) then\n        if not M.ORDER_ACTUAL_EXPECTED then\n            expected, actual = actual, expected\n        end\n        local delta = math.abs(actual - expected) \n        fail_fmt(2, extra_msg_or_nil, 'Values are not almost equal\\n' ..\n                    'Actual: %s, expected: %s, delta %s above margin of %s',\n                    actual, expected, delta, margin)\n    end\nend\n\nfunction M.assertNotEquals(actual, expected, extra_msg_or_nil)\n    if type(actual) ~= type(expected) then\n        return\n    end\n\n    if type(actual) == 'table' and type(expected) == 'table' then\n        if not _is_table_equals(actual, expected) then\n            return\n        end\n    elseif actual ~= expected then\n        return\n    end\n    fail_fmt(2, extra_msg_or_nil, 'Received the not expected value: %s', prettystr(actual))\nend\n\nfunction M.assertNotAlmostEquals( actual, expected, margin, extra_msg_or_nil )\n    -- check that two floats are not close by margin\n    margin = margin or M.EPS\n    if M.almostEquals(actual, expected, margin) then\n        if not M.ORDER_ACTUAL_EXPECTED then\n            expected, actual = actual, expected\n        end\n        local delta = math.abs(actual - expected)\n        fail_fmt(2, extra_msg_or_nil, 'Values are almost equal\\nActual: %s, expected: %s' ..\n                    ', delta %s below margin of %s',\n                    actual, expected, delta, margin)\n    end\nend\n\nfunction M.assertItemsEquals(actual, expected, extra_msg_or_nil)\n    -- checks that the items of table expected\n    -- are contained in table actual. Warning, this function\n    -- is at least O(n^2)\n    if not _is_table_items_equals(actual, expected ) then\n        expected, actual = prettystrPairs(expected, actual)\n        fail_fmt(2, extra_msg_or_nil, 'Content of the tables are not identical:\\nExpected: %s\\nActual: %s',\n                 expected, actual)\n    end\nend\n\n------------------------------------------------------------------\n--                  String assertion\n------------------------------------------------------------------\n\nfunction M.assertStrContains( str, sub, isPattern, extra_msg_or_nil )\n    -- this relies on lua string.find function\n    -- a string always contains the empty string\n    -- assert( type(str) == 'string', 'Argument 1 of assertStrContains() should be a string.' ) )\n    -- assert( type(sub) == 'string', 'Argument 2 of assertStrContains() should be a string.' ) )\n    if not string.find(str, sub, 1, not isPattern) then\n        sub, str = prettystrPairs(sub, str, '\\n')\n        fail_fmt(2, extra_msg_or_nil, 'Could not find %s %s in string %s',\n                 isPattern and 'pattern' or 'substring', sub, str)\n    end\nend\n\nfunction M.assertStrIContains( str, sub, extra_msg_or_nil )\n    -- this relies on lua string.find function\n    -- a string always contains the empty string\n    if not string.find(str:lower(), sub:lower(), 1, true) then\n        sub, str = prettystrPairs(sub, str, '\\n')\n        fail_fmt(2, extra_msg_or_nil, 'Could not find (case insensitively) substring %s in string %s',\n                 sub, str)\n    end\nend\n\nfunction M.assertNotStrContains( str, sub, isPattern, extra_msg_or_nil )\n    -- this relies on lua string.find function\n    -- a string always contains the empty string\n    if string.find(str, sub, 1, not isPattern) then\n        sub, str = prettystrPairs(sub, str, '\\n')\n        fail_fmt(2, extra_msg_or_nil, 'Found the not expected %s %s in string %s',\n                 isPattern and 'pattern' or 'substring', sub, str)\n    end\nend\n\nfunction M.assertNotStrIContains( str, sub, extra_msg_or_nil )\n    -- this relies on lua string.find function\n    -- a string always contains the empty string\n    if string.find(str:lower(), sub:lower(), 1, true) then\n        sub, str = prettystrPairs(sub, str, '\\n')\n        fail_fmt(2, extra_msg_or_nil, 'Found (case insensitively) the not expected substring %s in string %s',\n                 sub, str)\n    end\nend\n\nfunction M.assertStrMatches( str, pattern, start, final, extra_msg_or_nil )\n    -- Verify a full match for the string\n    if not strMatch( str, pattern, start, final ) then\n        pattern, str = prettystrPairs(pattern, str, '\\n')\n        fail_fmt(2, extra_msg_or_nil, 'Could not match pattern %s with string %s',\n                 pattern, str)\n    end\nend\n\nlocal function _assertErrorMsgEquals( stripFileAndLine, expectedMsg, func, ... )\n    local no_error, error_msg = pcall( func, ... )\n    if no_error then\n        failure( 'No error generated when calling function but expected error: '..M.prettystr(expectedMsg), nil, 3 )\n    end\n    if type(expectedMsg) == \"string\" and type(error_msg) ~= \"string\" then\n        -- table are converted to string automatically\n        error_msg = tostring(error_msg)\n    end\n    local differ = false\n    if stripFileAndLine then\n        if error_msg:gsub(\"^.+:%d+: \", \"\") ~= expectedMsg then\n            differ = true\n        end\n    else\n        if error_msg ~= expectedMsg then\n            local tr = type(error_msg)\n            local te = type(expectedMsg)\n            if te == 'table' then\n                if tr ~= 'table' then\n                    differ = true\n                else\n                     local ok = pcall(M.assertItemsEquals, error_msg, expectedMsg)\n                     if not ok then\n                         differ = true\n                     end\n                end\n            else\n               differ = true\n            end\n        end\n    end\n\n    if differ then\n        error_msg, expectedMsg = prettystrPairs(error_msg, expectedMsg)\n        fail_fmt(3, nil, 'Error message expected: %s\\nError message received: %s\\n',\n                 expectedMsg, error_msg)\n    end\nend\n\nfunction M.assertErrorMsgEquals( expectedMsg, func, ... )\n    -- assert that calling f with the arguments will raise an error\n    -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error\n    _assertErrorMsgEquals(false, expectedMsg, func, ...)\nend\n\nfunction M.assertErrorMsgContentEquals(expectedMsg, func, ...)\n     _assertErrorMsgEquals(true, expectedMsg, func, ...)\nend\n\nfunction M.assertErrorMsgContains( partialMsg, func, ... )\n    -- assert that calling f with the arguments will raise an error\n    -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error\n    local no_error, error_msg = pcall( func, ... )\n    if no_error then\n        failure( 'No error generated when calling function but expected error containing: '..prettystr(partialMsg), nil, 2 )\n    end\n    if type(error_msg) ~= \"string\" then\n        error_msg = tostring(error_msg)\n    end\n    if not string.find( error_msg, partialMsg, nil, true ) then\n        error_msg, partialMsg = prettystrPairs(error_msg, partialMsg)\n        fail_fmt(2, nil, 'Error message does not contain: %s\\nError message received: %s\\n',\n                 partialMsg, error_msg)\n    end\nend\n\nfunction M.assertErrorMsgMatches( expectedMsg, func, ... )\n    -- assert that calling f with the arguments will raise an error\n    -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error\n    local no_error, error_msg = pcall( func, ... )\n    if no_error then\n        failure( 'No error generated when calling function but expected error matching: \"'..expectedMsg..'\"', nil, 2 )\n    end\n    if type(error_msg) ~= \"string\" then\n        error_msg = tostring(error_msg)\n    end\n    if not strMatch( error_msg, expectedMsg ) then\n        expectedMsg, error_msg = prettystrPairs(expectedMsg, error_msg)\n        fail_fmt(2, nil, 'Error message does not match pattern: %s\\nError message received: %s\\n',\n                 expectedMsg, error_msg)\n    end\nend\n\n------------------------------------------------------------------\n--              Type assertions\n------------------------------------------------------------------\n\nfunction M.assertEvalToTrue(value, extra_msg_or_nil)\n    if not value then\n        failure(\"expected: a value evaluating to true, actual: \" ..prettystr(value), extra_msg_or_nil, 2)\n    end\nend\n\nfunction M.assertEvalToFalse(value, extra_msg_or_nil)\n    if value then\n        failure(\"expected: false or nil, actual: \" ..prettystr(value), extra_msg_or_nil, 2)\n    end\nend\n\nfunction M.assertIsTrue(value, extra_msg_or_nil)\n    if value ~= true then\n        failure(\"expected: true, actual: \" ..prettystr(value), extra_msg_or_nil, 2)\n    end\nend\n\nfunction M.assertNotIsTrue(value, extra_msg_or_nil)\n    if value == true then\n        failure(\"expected: not true, actual: \" ..prettystr(value), extra_msg_or_nil, 2)\n    end\nend\n\nfunction M.assertIsFalse(value, extra_msg_or_nil)\n    if value ~= false then\n        failure(\"expected: false, actual: \" ..prettystr(value), extra_msg_or_nil, 2)\n    end\nend\n\nfunction M.assertNotIsFalse(value, extra_msg_or_nil)\n    if value == false then\n        failure(\"expected: not false, actual: \" ..prettystr(value), extra_msg_or_nil, 2)\n    end\nend\n\nfunction M.assertIsNil(value, extra_msg_or_nil)\n    if value ~= nil then\n        failure(\"expected: nil, actual: \" ..prettystr(value), extra_msg_or_nil, 2)\n    end\nend\n\nfunction M.assertNotIsNil(value, extra_msg_or_nil)\n    if value == nil then\n        failure(\"expected: not nil, actual: nil\", extra_msg_or_nil, 2)\n    end\nend\n\n--[[\nAdd type assertion functions to the module table M. Each of these functions\ntakes a single parameter \"value\", and checks that its Lua type matches the\nexpected string (derived from the function name):\n\nM.assertIsXxx(value) -> ensure that type(value) conforms to \"xxx\"\n]]\nfor _, funcName in ipairs(\n    {'assertIsNumber', 'assertIsString', 'assertIsTable', 'assertIsBoolean',\n     'assertIsFunction', 'assertIsUserdata', 'assertIsThread'}\n) do\n    local typeExpected = funcName:match(\"^assertIs([A-Z]%a*)$\")\n    -- Lua type() always returns lowercase, also make sure the match() succeeded\n    typeExpected = typeExpected and typeExpected:lower()\n                   or error(\"bad function name '\"..funcName..\"' for type assertion\")\n\n    M[funcName] = function(value, extra_msg_or_nil)\n        if type(value) ~= typeExpected then\n            if type(value) == 'nil' then\n                fail_fmt(2, extra_msg_or_nil, 'expected: a %s value, actual: nil',\n                         typeExpected, type(value), prettystrPairs(value))\n            else\n                fail_fmt(2, extra_msg_or_nil, 'expected: a %s value, actual: type %s, value %s',\n                         typeExpected, type(value), prettystrPairs(value))\n            end\n        end\n    end\nend\n\n--[[\nAdd shortcuts for verifying type of a variable, without failure (luaunit v2 compatibility)\nM.isXxx(value) -> returns true if type(value) conforms to \"xxx\"\n]]\nfor _, typeExpected in ipairs(\n    {'Number', 'String', 'Table', 'Boolean',\n     'Function', 'Userdata', 'Thread', 'Nil' }\n) do\n    local typeExpectedLower = typeExpected:lower()\n    local isType = function(value)\n        return (type(value) == typeExpectedLower)\n    end\n    M['is'..typeExpected] = isType\n    M['is_'..typeExpectedLower] = isType\nend\n\n--[[\nAdd non-type assertion functions to the module table M. Each of these functions\ntakes a single parameter \"value\", and checks that its Lua type differs from the\nexpected string (derived from the function name):\n\nM.assertNotIsXxx(value) -> ensure that type(value) is not \"xxx\"\n]]\nfor _, funcName in ipairs(\n    {'assertNotIsNumber', 'assertNotIsString', 'assertNotIsTable', 'assertNotIsBoolean',\n     'assertNotIsFunction', 'assertNotIsUserdata', 'assertNotIsThread'}\n) do\n    local typeUnexpected = funcName:match(\"^assertNotIs([A-Z]%a*)$\")\n    -- Lua type() always returns lowercase, also make sure the match() succeeded\n    typeUnexpected = typeUnexpected and typeUnexpected:lower()\n                   or error(\"bad function name '\"..funcName..\"' for type assertion\")\n\n    M[funcName] = function(value, extra_msg_or_nil)\n        if type(value) == typeUnexpected then\n            fail_fmt(2, extra_msg_or_nil, 'expected: not a %s type, actual: value %s',\n                     typeUnexpected, prettystrPairs(value))\n        end\n    end\nend\n\nfunction M.assertIs(actual, expected, extra_msg_or_nil)\n    if actual ~= expected then\n        if not M.ORDER_ACTUAL_EXPECTED then\n            actual, expected = expected, actual\n        end\n        local old_print_table_ref_in_error_msg = M.PRINT_TABLE_REF_IN_ERROR_MSG\n        M.PRINT_TABLE_REF_IN_ERROR_MSG = true\n        expected, actual = prettystrPairs(expected, actual, '\\n', '')\n        M.PRINT_TABLE_REF_IN_ERROR_MSG = old_print_table_ref_in_error_msg\n        fail_fmt(2, extra_msg_or_nil, 'expected and actual object should not be different\\nExpected: %s\\nReceived: %s',\n                 expected, actual)\n    end\nend\n\nfunction M.assertNotIs(actual, expected, extra_msg_or_nil)\n    if actual == expected then\n        local old_print_table_ref_in_error_msg = M.PRINT_TABLE_REF_IN_ERROR_MSG\n        M.PRINT_TABLE_REF_IN_ERROR_MSG = true\n        local s_expected\n        if not M.ORDER_ACTUAL_EXPECTED then\n            s_expected = prettystrPairs(actual)\n        else\n            s_expected = prettystrPairs(expected)\n        end\n        M.PRINT_TABLE_REF_IN_ERROR_MSG = old_print_table_ref_in_error_msg\n        fail_fmt(2, extra_msg_or_nil, 'expected and actual object should be different: %s', s_expected )\n    end\nend\n\n\n------------------------------------------------------------------\n--              Scientific assertions\n------------------------------------------------------------------\n\n\nfunction M.assertIsNaN(value, extra_msg_or_nil)\n    if type(value) ~= \"number\" or value == value then\n        failure(\"expected: NaN, actual: \" ..prettystr(value), extra_msg_or_nil, 2)\n    end\nend\n\nfunction M.assertNotIsNaN(value, extra_msg_or_nil)\n    if type(value) == \"number\" and value ~= value then\n        failure(\"expected: not NaN, actual: NaN\", extra_msg_or_nil, 2)\n    end\nend\n\nfunction M.assertIsInf(value, extra_msg_or_nil)\n    if type(value) ~= \"number\" or math.abs(value) ~= math.huge then\n        failure(\"expected: #Inf, actual: \" ..prettystr(value), extra_msg_or_nil, 2)\n    end\nend\n\nfunction M.assertIsPlusInf(value, extra_msg_or_nil)\n    if type(value) ~= \"number\" or value ~= math.huge then\n        failure(\"expected: #Inf, actual: \" ..prettystr(value), extra_msg_or_nil, 2)\n    end\nend\n\nfunction M.assertIsMinusInf(value, extra_msg_or_nil)\n    if type(value) ~= \"number\" or value ~= -math.huge then\n        failure(\"expected: -#Inf, actual: \" ..prettystr(value), extra_msg_or_nil, 2)\n    end\nend\n\nfunction M.assertNotIsPlusInf(value, extra_msg_or_nil)\n    if type(value) == \"number\" and value == math.huge then\n        failure(\"expected: not #Inf, actual: #Inf\", extra_msg_or_nil, 2)\n    end\nend\n\nfunction M.assertNotIsMinusInf(value, extra_msg_or_nil)\n    if type(value) == \"number\" and value == -math.huge then\n        failure(\"expected: not -#Inf, actual: -#Inf\", extra_msg_or_nil, 2)\n    end\nend\n\nfunction M.assertNotIsInf(value, extra_msg_or_nil)\n    if type(value) == \"number\" and math.abs(value) == math.huge then\n        failure(\"expected: not infinity, actual: \" .. prettystr(value), extra_msg_or_nil, 2)\n    end\nend\n\nfunction M.assertIsPlusZero(value, extra_msg_or_nil)\n    if type(value) ~= 'number' or value ~= 0 then\n        failure(\"expected: +0.0, actual: \" ..prettystr(value), extra_msg_or_nil, 2)\n    else if (1/value == -math.huge) then\n            -- more precise error diagnosis\n            failure(\"expected: +0.0, actual: -0.0\", extra_msg_or_nil, 2)\n        else if (1/value ~= math.huge) then\n                -- strange, case should have already been covered\n                failure(\"expected: +0.0, actual: \" ..prettystr(value), extra_msg_or_nil, 2)\n            end\n        end\n    end\nend\n\nfunction M.assertIsMinusZero(value, extra_msg_or_nil)\n    if type(value) ~= 'number' or value ~= 0 then\n        failure(\"expected: -0.0, actual: \" ..prettystr(value), extra_msg_or_nil, 2)\n    else if (1/value == math.huge) then\n            -- more precise error diagnosis\n            failure(\"expected: -0.0, actual: +0.0\", extra_msg_or_nil, 2)\n        else if (1/value ~= -math.huge) then\n                -- strange, case should have already been covered\n                failure(\"expected: -0.0, actual: \" ..prettystr(value), extra_msg_or_nil, 2)\n            end\n        end\n    end\nend\n\nfunction M.assertNotIsPlusZero(value, extra_msg_or_nil)\n    if type(value) == 'number' and (1/value == math.huge) then\n        failure(\"expected: not +0.0, actual: +0.0\", extra_msg_or_nil, 2)\n    end\nend\n\nfunction M.assertNotIsMinusZero(value, extra_msg_or_nil)\n    if type(value) == 'number' and (1/value == -math.huge) then\n        failure(\"expected: not -0.0, actual: -0.0\", extra_msg_or_nil, 2)\n    end\nend\n\nfunction M.assertTableContains(t, expected)\n    -- checks that table t contains the expected element\n    if table_findkeyof(t, expected) == nil then\n        t, expected = prettystrPairs(t, expected)\n        fail_fmt(2, 'Table %s does NOT contain the expected element %s',\n                 t, expected)\n    end\nend\n\nfunction M.assertNotTableContains(t, expected)\n    -- checks that table t doesn't contain the expected element\n    local k = table_findkeyof(t, expected)\n    if k ~= nil then\n        t, expected = prettystrPairs(t, expected)\n        fail_fmt(2, 'Table %s DOES contain the unwanted element %s (at key %s)',\n                 t, expected, prettystr(k))\n    end\nend\n\n----------------------------------------------------------------\n--                     Compatibility layer\n----------------------------------------------------------------\n\n-- for compatibility with LuaUnit v2.x\nfunction M.wrapFunctions()\n    -- In LuaUnit version <= 2.1 , this function was necessary to include\n    -- a test function inside the global test suite. Nowadays, the functions\n    -- are simply run directly as part of the test discovery process.\n    -- so just do nothing !\n    ioWrapper.stderr:write[[Use of WrapFunctions() is no longer needed.\nJust prefix your test function names with \"test\" or \"Test\" and they\nwill be picked up and run by LuaUnit.\n]]\nend\n\nlocal list_of_funcs = {\n    -- { official function name , alias }\n\n    -- general assertions\n    { 'assertEquals'            , 'assert_equals' },\n    { 'assertItemsEquals'       , 'assert_items_equals' },\n    { 'assertNotEquals'         , 'assert_not_equals' },\n    { 'assertAlmostEquals'      , 'assert_almost_equals' },\n    { 'assertNotAlmostEquals'   , 'assert_not_almost_equals' },\n    { 'assertEvalToTrue'        , 'assert_eval_to_true' },\n    { 'assertEvalToFalse'       , 'assert_eval_to_false' },\n    { 'assertStrContains'       , 'assert_str_contains' },\n    { 'assertStrIContains'      , 'assert_str_icontains' },\n    { 'assertNotStrContains'    , 'assert_not_str_contains' },\n    { 'assertNotStrIContains'   , 'assert_not_str_icontains' },\n    { 'assertStrMatches'        , 'assert_str_matches' },\n    { 'assertError'             , 'assert_error' },\n    { 'assertErrorMsgEquals'    , 'assert_error_msg_equals' },\n    { 'assertErrorMsgContains'  , 'assert_error_msg_contains' },\n    { 'assertErrorMsgMatches'   , 'assert_error_msg_matches' },\n    { 'assertErrorMsgContentEquals', 'assert_error_msg_content_equals' },\n    { 'assertIs'                , 'assert_is' },\n    { 'assertNotIs'             , 'assert_not_is' },\n    { 'assertTableContains'     , 'assert_table_contains' },\n    { 'assertNotTableContains'  , 'assert_not_table_contains' },\n    { 'wrapFunctions'           , 'WrapFunctions' },\n    { 'wrapFunctions'           , 'wrap_functions' },\n\n    -- type assertions: assertIsXXX -> assert_is_xxx\n    { 'assertIsNumber'          , 'assert_is_number' },\n    { 'assertIsString'          , 'assert_is_string' },\n    { 'assertIsTable'           , 'assert_is_table' },\n    { 'assertIsBoolean'         , 'assert_is_boolean' },\n    { 'assertIsNil'             , 'assert_is_nil' },\n    { 'assertIsTrue'            , 'assert_is_true' },\n    { 'assertIsFalse'           , 'assert_is_false' },\n    { 'assertIsNaN'             , 'assert_is_nan' },\n    { 'assertIsInf'             , 'assert_is_inf' },\n    { 'assertIsPlusInf'         , 'assert_is_plus_inf' },\n    { 'assertIsMinusInf'        , 'assert_is_minus_inf' },\n    { 'assertIsPlusZero'        , 'assert_is_plus_zero' },\n    { 'assertIsMinusZero'       , 'assert_is_minus_zero' },\n    { 'assertIsFunction'        , 'assert_is_function' },\n    { 'assertIsThread'          , 'assert_is_thread' },\n    { 'assertIsUserdata'        , 'assert_is_userdata' },\n\n    -- type assertions: assertIsXXX -> assertXxx\n    { 'assertIsNumber'          , 'assertNumber' },\n    { 'assertIsString'          , 'assertString' },\n    { 'assertIsTable'           , 'assertTable' },\n    { 'assertIsBoolean'         , 'assertBoolean' },\n    { 'assertIsNil'             , 'assertNil' },\n    { 'assertIsTrue'            , 'assertTrue' },\n    { 'assertIsFalse'           , 'assertFalse' },\n    { 'assertIsNaN'             , 'assertNaN' },\n    { 'assertIsInf'             , 'assertInf' },\n    { 'assertIsPlusInf'         , 'assertPlusInf' },\n    { 'assertIsMinusInf'        , 'assertMinusInf' },\n    { 'assertIsPlusZero'        , 'assertPlusZero' },\n    { 'assertIsMinusZero'       , 'assertMinusZero'},\n    { 'assertIsFunction'        , 'assertFunction' },\n    { 'assertIsThread'          , 'assertThread' },\n    { 'assertIsUserdata'        , 'assertUserdata' },\n\n    -- type assertions: assertIsXXX -> assert_xxx (luaunit v2 compat)\n    { 'assertIsNumber'          , 'assert_number' },\n    { 'assertIsString'          , 'assert_string' },\n    { 'assertIsTable'           , 'assert_table' },\n    { 'assertIsBoolean'         , 'assert_boolean' },\n    { 'assertIsNil'             , 'assert_nil' },\n    { 'assertIsTrue'            , 'assert_true' },\n    { 'assertIsFalse'           , 'assert_false' },\n    { 'assertIsNaN'             , 'assert_nan' },\n    { 'assertIsInf'             , 'assert_inf' },\n    { 'assertIsPlusInf'         , 'assert_plus_inf' },\n    { 'assertIsMinusInf'        , 'assert_minus_inf' },\n    { 'assertIsPlusZero'        , 'assert_plus_zero' },\n    { 'assertIsMinusZero'       , 'assert_minus_zero' },\n    { 'assertIsFunction'        , 'assert_function' },\n    { 'assertIsThread'          , 'assert_thread' },\n    { 'assertIsUserdata'        , 'assert_userdata' },\n\n    -- type assertions: assertNotIsXXX -> assert_not_is_xxx\n    { 'assertNotIsNumber'       , 'assert_not_is_number' },\n    { 'assertNotIsString'       , 'assert_not_is_string' },\n    { 'assertNotIsTable'        , 'assert_not_is_table' },\n    { 'assertNotIsBoolean'      , 'assert_not_is_boolean' },\n    { 'assertNotIsNil'          , 'assert_not_is_nil' },\n    { 'assertNotIsTrue'         , 'assert_not_is_true' },\n    { 'assertNotIsFalse'        , 'assert_not_is_false' },\n    { 'assertNotIsNaN'          , 'assert_not_is_nan' },\n    { 'assertNotIsInf'          , 'assert_not_is_inf' },\n    { 'assertNotIsPlusInf'      , 'assert_not_plus_inf' },\n    { 'assertNotIsMinusInf'     , 'assert_not_minus_inf' },\n    { 'assertNotIsPlusZero'     , 'assert_not_plus_zero' },\n    { 'assertNotIsMinusZero'    , 'assert_not_minus_zero' },\n    { 'assertNotIsFunction'     , 'assert_not_is_function' },\n    { 'assertNotIsThread'       , 'assert_not_is_thread' },\n    { 'assertNotIsUserdata'     , 'assert_not_is_userdata' },\n\n    -- type assertions: assertNotIsXXX -> assertNotXxx (luaunit v2 compat)\n    { 'assertNotIsNumber'       , 'assertNotNumber' },\n    { 'assertNotIsString'       , 'assertNotString' },\n    { 'assertNotIsTable'        , 'assertNotTable' },\n    { 'assertNotIsBoolean'      , 'assertNotBoolean' },\n    { 'assertNotIsNil'          , 'assertNotNil' },\n    { 'assertNotIsTrue'         , 'assertNotTrue' },\n    { 'assertNotIsFalse'        , 'assertNotFalse' },\n    { 'assertNotIsNaN'          , 'assertNotNaN' },\n    { 'assertNotIsInf'          , 'assertNotInf' },\n    { 'assertNotIsPlusInf'      , 'assertNotPlusInf' },\n    { 'assertNotIsMinusInf'     , 'assertNotMinusInf' },\n    { 'assertNotIsPlusZero'     , 'assertNotPlusZero' },\n    { 'assertNotIsMinusZero'    , 'assertNotMinusZero' },\n    { 'assertNotIsFunction'     , 'assertNotFunction' },\n    { 'assertNotIsThread'       , 'assertNotThread' },\n    { 'assertNotIsUserdata'     , 'assertNotUserdata' },\n\n    -- type assertions: assertNotIsXXX -> assert_not_xxx\n    { 'assertNotIsNumber'       , 'assert_not_number' },\n    { 'assertNotIsString'       , 'assert_not_string' },\n    { 'assertNotIsTable'        , 'assert_not_table' },\n    { 'assertNotIsBoolean'      , 'assert_not_boolean' },\n    { 'assertNotIsNil'          , 'assert_not_nil' },\n    { 'assertNotIsTrue'         , 'assert_not_true' },\n    { 'assertNotIsFalse'        , 'assert_not_false' },\n    { 'assertNotIsNaN'          , 'assert_not_nan' },\n    { 'assertNotIsInf'          , 'assert_not_inf' },\n    { 'assertNotIsPlusInf'      , 'assert_not_plus_inf' },\n    { 'assertNotIsMinusInf'     , 'assert_not_minus_inf' },\n    { 'assertNotIsPlusZero'     , 'assert_not_plus_zero' },\n    { 'assertNotIsMinusZero'    , 'assert_not_minus_zero' },\n    { 'assertNotIsFunction'     , 'assert_not_function' },\n    { 'assertNotIsThread'       , 'assert_not_thread' },\n    { 'assertNotIsUserdata'     , 'assert_not_userdata' },\n\n    -- all assertions with Coroutine duplicate Thread assertions\n    { 'assertIsThread'          , 'assertIsCoroutine' },\n    { 'assertIsThread'          , 'assertCoroutine' },\n    { 'assertIsThread'          , 'assert_is_coroutine' },\n    { 'assertIsThread'          , 'assert_coroutine' },\n    { 'assertNotIsThread'       , 'assertNotIsCoroutine' },\n    { 'assertNotIsThread'       , 'assertNotCoroutine' },\n    { 'assertNotIsThread'       , 'assert_not_is_coroutine' },\n    { 'assertNotIsThread'       , 'assert_not_coroutine' },\n}\n\n-- Create all aliases in M\nfor _,v in ipairs( list_of_funcs ) do\n    local funcname, alias = v[1], v[2]\n    M[alias] = M[funcname]\n\n    if EXPORT_ASSERT_TO_GLOBALS then\n        _G[funcname] = M[funcname]\n        _G[alias] = M[funcname]\n    end\nend\n\n----------------------------------------------------------------\n--\n--                     Outputters\n--\n----------------------------------------------------------------\n\n-- A common \"base\" class for outputters\n-- For concepts involved (class inheritance) see http://www.lua.org/pil/16.2.html\n\nlocal genericOutput = { __class__ = 'genericOutput' } -- class\nlocal genericOutput_MT = { __index = genericOutput } -- metatable\nM.genericOutput = genericOutput -- publish, so that custom classes may derive from it\n\nfunction genericOutput.new(runner, default_verbosity)\n    -- runner is the \"parent\" object controlling the output, usually a LuaUnit instance\n    local t = { runner = runner }\n    if runner then\n        t.result = runner.result\n        t.verbosity = runner.verbosity or default_verbosity\n        t.fname = runner.fname\n    else\n        t.verbosity = default_verbosity\n    end\n    return setmetatable( t, genericOutput_MT)\nend\n\n-- abstract (\"empty\") methods\nfunction genericOutput:startSuite() \n    -- Called once, when the suite is started\nend\n\nfunction genericOutput:startClass(className) \n    -- Called each time a new test class is started\nend\n\nfunction genericOutput:startTest(testName) \n    -- called each time a new test is started, right before the setUp()\n    -- the current test status node is already created and available in: self.result.currentNode\nend\n\nfunction genericOutput:updateStatus(node) \n    -- called with status failed or error as soon as the error/failure is encountered\n    -- this method is NOT called for a successful test because a test is marked as successful by default\n    -- and does not need to be updated\nend\n\nfunction genericOutput:endTest(node) \n    -- called when the test is finished, after the tearDown() method\nend\n\nfunction genericOutput:endClass() \n    -- called when executing the class is finished, before moving on to the next class of at the end of the test execution\nend\n\nfunction genericOutput:endSuite() \n    -- called at the end of the test suite execution\nend\n\n\n----------------------------------------------------------------\n--                     class TapOutput\n----------------------------------------------------------------\n\nlocal TapOutput = genericOutput.new() -- derived class\nlocal TapOutput_MT = { __index = TapOutput } -- metatable\nTapOutput.__class__ = 'TapOutput'\n\n    -- For a good reference for TAP format, check: http://testanything.org/tap-specification.html\n\n    function TapOutput.new(runner)\n        local t = genericOutput.new(runner, M.VERBOSITY_LOW)\n        return setmetatable( t, TapOutput_MT)\n    end\n    function TapOutput:startSuite()\n        env.info(\"1..\"..self.result.selectedCount)\n        env.info('# Started on '..self.result.startDate)\n    end\n    function TapOutput:startClass(className)\n        if className ~= '[TestFunctions]' then\n            env.info('# Starting class: '..className)\n        end\n    end\n\n    function TapOutput:updateStatus( node )\n        if node:isSkipped() then\n            ioWrapper.stdout:write(\"ok \"..self.result.currentTestNumber..\"\\t# SKIP \"..node.msg..\"\\n\" )\n            return\n        end\n\n        ioWrapper.stdout:write(\"not ok \"..self.result.currentTestNumber..\"\\t\"..node.testName..\"\\n\")\n        if self.verbosity > M.VERBOSITY_LOW then\n           env.info( prefixString( '#   ', node.msg ) )\n        end\n        if (node:isFailure() or node:isError()) and self.verbosity > M.VERBOSITY_DEFAULT then\n           env.info( prefixString( '#   ', node.stackTrace ) )\n        end\n    end\n\n    function TapOutput:endTest( node )\n        if node:isSuccess() then\n            ioWrapper.stdout:write(\"ok     \"..self.result.currentTestNumber..\"\\t\"..node.testName..\"\\n\")\n        end\n    end\n\n    function TapOutput:endSuite()\n        env.info( '# '..M.LuaUnit.statusLine( self.result ) )\n        return self.result.notSuccessCount\n    end\n\n\n-- class TapOutput end\n\n----------------------------------------------------------------\n--                     class JUnitOutput\n----------------------------------------------------------------\n\n-- See directory junitxml for more information about the junit format\nlocal JUnitOutput = genericOutput.new() -- derived class\nlocal JUnitOutput_MT = { __index = JUnitOutput } -- metatable\nJUnitOutput.__class__ = 'JUnitOutput'\n\n    function JUnitOutput.new(runner)\n        local t = genericOutput.new(runner, M.VERBOSITY_LOW)\n        t.testList = {}\n        return setmetatable( t, JUnitOutput_MT )\n    end\n\n    function JUnitOutput:startSuite()\n        -- open xml file early to deal with errors\n        if self.fname == nil then\n            error('With Junit, an output filename must be supplied with --name!')\n        end\n        if string.sub(self.fname,-4) ~= '.xml' then\n            self.fname = self.fname..'.xml'\n        end\n        self.fd = ioWrapper.open(self.fname, \"w\")\n        if self.fd == nil then\n            error(\"Could not open file for writing: \"..self.fname)\n        end\n\n        env.info('# XML output to '..self.fname)\n        env.info('# Started on '..self.result.startDate)\n    end\n    function JUnitOutput:startClass(className)\n        if className ~= '[TestFunctions]' then\n            env.info('# Starting class: '..className)\n        end\n    end\n    function JUnitOutput:startTest(testName)\n        env.info('# Starting test: '..testName)\n    end\n\n    function JUnitOutput:updateStatus( node )\n        if node:isFailure() then\n            env.info( '#   Failure: ' .. prefixString( '#   ', node.msg ):sub(4, nil) )\n            -- env.info('# ' .. node.stackTrace)\n        elseif node:isError() then\n            env.info( '#   Error: ' .. prefixString( '#   '  , node.msg ):sub(4, nil) )\n            -- env.info('# ' .. node.stackTrace)\n        end\n    end\n\n    function JUnitOutput:endSuite()\n        env.info( '# '..M.LuaUnit.statusLine(self.result))\n\n        -- XML file writing\n        self.fd:write('<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\\n')\n        self.fd:write('<testsuites>\\n')\n        self.fd:write(string.format(\n            '    <testsuite name=\"LuaUnit\" id=\"00001\" package=\"\" hostname=\"localhost\" tests=\"%d\" timestamp=\"%s\" time=\"%0.3f\" errors=\"%d\" failures=\"%d\" skipped=\"%d\">\\n',\n            self.result.runCount, self.result.startIsodate, self.result.duration, self.result.errorCount, self.result.failureCount, self.result.skippedCount ))\n        self.fd:write(\"        <properties>\\n\")\n        self.fd:write(string.format('            <property name=\"Lua Version\" value=\"%s\"/>\\n', _VERSION ) )\n        self.fd:write(string.format('            <property name=\"LuaUnit Version\" value=\"%s\"/>\\n', M.VERSION) )\n        -- XXX please include system name and version if possible\n        self.fd:write(\"        </properties>\\n\")\n\n        for i,node in ipairs(self.result.allTests) do\n            self.fd:write(string.format('        <testcase classname=\"%s\" name=\"%s\" time=\"%0.3f\">\\n',\n                node.className, node.testName, node.duration ) )\n            if node:isNotSuccess() then\n                self.fd:write(node:statusXML())\n            end\n            self.fd:write('        </testcase>\\n')\n        end\n\n        -- Next two lines are needed to validate junit ANT xsd, but really not useful in general:\n        self.fd:write('    <system-out/>\\n')\n        self.fd:write('    <system-err/>\\n')\n\n        self.fd:write('    </testsuite>\\n')\n        self.fd:write('</testsuites>\\n')\n        self.fd:close()\n        return self.result.notSuccessCount\n    end\n\n\n-- class TapOutput end\n\n----------------------------------------------------------------\n--                     class TextOutput\n----------------------------------------------------------------\n\n--[[    Example of other unit-tests suite text output\n\n-- Python Non verbose:\n\nFor each test: . or F or E\n\nIf some failed tests:\n    ==============\n    ERROR / FAILURE: TestName (testfile.testclass)\n    ---------\n    Stack trace\n\n\nthen --------------\nthen \"Ran x tests in 0.000s\"\nthen OK or FAILED (failures=1, error=1)\n\n-- Python Verbose:\ntestname (filename.classname) ... ok\ntestname (filename.classname) ... FAIL\ntestname (filename.classname) ... ERROR\n\nthen --------------\nthen \"Ran x tests in 0.000s\"\nthen OK or FAILED (failures=1, error=1)\n\n-- Ruby:\nStarted\n .\n Finished in 0.002695 seconds.\n\n 1 tests, 2 assertions, 0 failures, 0 errors\n\n-- Ruby:\n>> ruby tc_simple_number2.rb\nLoaded suite tc_simple_number2\nStarted\nF..\nFinished in 0.038617 seconds.\n\n  1) Failure:\ntest_failure(TestSimpleNumber) [tc_simple_number2.rb:16]:\nAdding doesn't work.\n<3> expected but was\n<4>.\n\n3 tests, 4 assertions, 1 failures, 0 errors\n\n-- Java Junit\n.......F.\nTime: 0,003\nThere was 1 failure:\n1) testCapacity(junit.samples.VectorTest)junit.framework.AssertionFailedError\n    at junit.samples.VectorTest.testCapacity(VectorTest.java:87)\n    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\n    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)\n    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\n\nFAILURES!!!\nTests run: 8,  Failures: 1,  Errors: 0\n\n\n-- Maven\n\n# mvn test\n-------------------------------------------------------\n T E S T S\n-------------------------------------------------------\nRunning math.AdditionTest\nTests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed:\n0.03 sec <<< FAILURE!\n\nResults :\n\nFailed tests:\n  testLireSymbole(math.AdditionTest)\n\nTests run: 2, Failures: 1, Errors: 0, Skipped: 0\n\n\n-- LuaUnit\n---- non verbose\n* display . or F or E when running tests\n---- verbose\n* display test name + ok/fail\n----\n* blank line\n* number) ERROR or FAILURE: TestName\n   Stack trace\n* blank line\n* number) ERROR or FAILURE: TestName\n   Stack trace\n\nthen --------------\nthen \"Ran x tests in 0.000s (%d not selected, %d skipped)\"\nthen OK or FAILED (failures=1, error=1)\n\n\n]]\n\nlocal TextOutput = genericOutput.new() -- derived class\nlocal TextOutput_MT = { __index = TextOutput } -- metatable\nTextOutput.__class__ = 'TextOutput'\n\n    function TextOutput.new(runner)\n        local t = genericOutput.new(runner, M.VERBOSITY_DEFAULT)\n        t.errorList = {}\n        return setmetatable( t, TextOutput_MT )\n    end\n\n    function TextOutput:startSuite()\n        if self.verbosity > M.VERBOSITY_DEFAULT then\n            env.info( 'Started on '.. self.result.startDate )\n        end\n    end\n\n    function TextOutput:startTest(testName)\n        if self.verbosity > M.VERBOSITY_DEFAULT then\n            ioWrapper.stdout:write( \"    \"..self.result.currentNode.testName..\" ... \" )\n        end\n    end\n\n    function TextOutput:endTest( node )\n        if node:isSuccess() then\n            if self.verbosity > M.VERBOSITY_DEFAULT then\n                ioWrapper.stdout:write(\"Ok\\n\")\n\t\t\t\tioWrapper.stdout:flush()\n            else\n                ioWrapper.stdout:write(\".\")\n                ioWrapper.stdout:flush()\n            end\n        else\n            if self.verbosity > M.VERBOSITY_DEFAULT then\n                env.info( node.status )\n                env.info( node.msg )\n                --[[\n                -- find out when to do this:\n                if self.verbosity > M.VERBOSITY_DEFAULT then\n                    env.info( node.stackTrace )\n                end\n                ]]\n            else\n                -- write only the first character of status E, F or S\n                ioWrapper.stdout:write(string.sub(node.status, 1, 1))\n                ioWrapper.stdout:flush()\n            end\n        end\n    end\n\n    function TextOutput:displayOneFailedTest( index, fail )\n        env.info(index..\")  \"..fail.testName )\n        env.info( fail.msg )\n        env.info( fail.stackTrace )\n        env.info()\n    end\n\n    function TextOutput:displayErroredTests()\n        if #self.result.errorTests ~= 0 then\n            env.info(\"Tests with errors:\")\n            env.info(\"------------------\")\n            for i, v in ipairs(self.result.errorTests) do\n                self:displayOneFailedTest(i, v)\n            end\n        end\n    end\n\n    function TextOutput:displayFailedTests()\n        if #self.result.failedTests ~= 0 then\n            env.info(\"Failed tests:\")\n            env.info(\"-------------\")\n            for i, v in ipairs(self.result.failedTests) do\n                self:displayOneFailedTest(i, v)\n            end\n        end\n    end\n\n    function TextOutput:endSuite()\n        if self.verbosity > M.VERBOSITY_DEFAULT then\n            env.info(\"=========================================================\")\n        else\n            env.info()\n        end\n        self:displayErroredTests()\n        self:displayFailedTests()\n        env.info( M.LuaUnit.statusLine( self.result ) )\n        if self.result.notSuccessCount == 0 then\n            env.info('OK')\n        end\n    end\n\n-- class TextOutput end\n\n\n----------------------------------------------------------------\n--                     class NilOutput\n----------------------------------------------------------------\n\nlocal function nopCallable()\n    --env.info(42)\n    return nopCallable\nend\n\nlocal NilOutput = { __class__ = 'NilOuptut' } -- class\nlocal NilOutput_MT = { __index = nopCallable } -- metatable\n\nfunction NilOutput.new(runner)\n    return setmetatable( { __class__ = 'NilOutput' }, NilOutput_MT )\nend\n\n----------------------------------------------------------------\n--\n--                     class LuaUnit\n--\n----------------------------------------------------------------\n\nM.LuaUnit = {\n    outputType = TextOutput,\n    verbosity = M.VERBOSITY_DEFAULT,\n    __class__ = 'LuaUnit'\n}\nlocal LuaUnit_MT = { __index = M.LuaUnit }\n\nif EXPORT_ASSERT_TO_GLOBALS then\n    LuaUnit = M.LuaUnit\nend\n\n    function M.LuaUnit.new()\n        return setmetatable( {}, LuaUnit_MT )\n    end\n\n    -----------------[[ Utility methods ]]---------------------\n\n    function M.LuaUnit.asFunction(aObject)\n        -- return \"aObject\" if it is a function, and nil otherwise\n        if 'function' == type(aObject) then\n            return aObject\n        end\n    end\n\n    function M.LuaUnit.splitClassMethod(someName)\n        --[[\n        Return a pair of className, methodName strings for a name in the form\n        \"class.method\". If no class part (or separator) is found, will return\n        nil, someName instead (the latter being unchanged).\n\n        This convention thus also replaces the older isClassMethod() test:\n        You just have to check for a non-nil className (return) value.\n        ]]\n        local separator = string.find(someName, '.', 1, true)\n        if separator then\n            return someName:sub(1, separator - 1), someName:sub(separator + 1)\n        end\n        return nil, someName\n    end\n\n    function M.LuaUnit.isMethodTestName( s )\n        -- return true is the name matches the name of a test method\n        -- default rule is that is starts with 'Test' or with 'test'\n        return string.sub(s, 1, 4):lower() == 'test'\n    end\n\n    function M.LuaUnit.isTestName( s )\n        -- return true is the name matches the name of a test\n        -- default rule is that is starts with 'Test' or with 'test'\n        return string.sub(s, 1, 4):lower() == 'test'\n    end\n\n    function M.LuaUnit.collectTests()\n        -- return a list of all test names in the global namespace\n        -- that match LuaUnit.isTestName\n\n        local testNames = {}\n        for k, _ in pairs(_G) do\n            if type(k) == \"string\" and M.LuaUnit.isTestName( k ) then\n                table.insert( testNames , k )\n            end\n        end\n        table.sort( testNames )\n        return testNames\n    end\n\n    function M.LuaUnit.parseCmdLine( cmdLine )\n        -- parse the command line\n        -- Supported command line parameters:\n        -- --verbose, -v: increase verbosity\n        -- --quiet, -q: silence output\n        -- --error, -e: treat errors as fatal (quit program)\n        -- --output, -o, + name: select output type\n        -- --pattern, -p, + pattern: run test matching pattern, may be repeated\n        -- --exclude, -x, + pattern: run test not matching pattern, may be repeated\n        -- --shuffle, -s, : shuffle tests before reunning them\n        -- --name, -n, + fname: name of output file for junit, default to stdout\n        -- --repeat, -r, + num: number of times to execute each test\n        -- [testnames, ...]: run selected test names\n        --\n        -- Returns a table with the following fields:\n        -- verbosity: nil, M.VERBOSITY_DEFAULT, M.VERBOSITY_QUIET, M.VERBOSITY_VERBOSE\n        -- output: nil, 'tap', 'junit', 'text', 'nil'\n        -- testNames: nil or a list of test names to run\n        -- exeRepeat: num or 1\n        -- pattern: nil or a list of patterns\n        -- exclude: nil or a list of patterns\n\n        local result, state = {}, nil\n        local SET_OUTPUT = 1\n        local SET_PATTERN = 2\n        local SET_EXCLUDE = 3\n        local SET_FNAME = 4\n        local SET_REPEAT = 5\n\n        if cmdLine == nil then\n            return result\n        end\n\n        local function parseOption( option )\n            if option == '--help' or option == '-h' then\n                result['help'] = true\n                return\n            elseif option == '--version' then\n                result['version'] = true\n                return\n            elseif option == '--verbose' or option == '-v' then\n                result['verbosity'] = M.VERBOSITY_VERBOSE\n                return\n            elseif option == '--quiet' or option == '-q' then\n                result['verbosity'] = M.VERBOSITY_QUIET\n                return\n            elseif option == '--error' or option == '-e' then\n                result['quitOnError'] = true\n                return\n            elseif option == '--failure' or option == '-f' then\n                result['quitOnFailure'] = true\n                return\n            elseif option == '--shuffle' or option == '-s' then\n                result['shuffle'] = true\n                return\n            elseif option == '--output' or option == '-o' then\n                state = SET_OUTPUT\n                return state\n            elseif option == '--name' or option == '-n' then\n                state = SET_FNAME\n                return state\n            elseif option == '--repeat' or option == '-r' then\n                state = SET_REPEAT\n                return state\n            elseif option == '--pattern' or option == '-p' then\n                state = SET_PATTERN\n                return state\n            elseif option == '--exclude' or option == '-x' then\n                state = SET_EXCLUDE\n                return state\n            end\n            error('Unknown option: '..option,3)\n        end\n\n        local function setArg( cmdArg, state )\n            if state == SET_OUTPUT then\n                result['output'] = cmdArg\n                return\n            elseif state == SET_FNAME then\n                result['fname'] = cmdArg\n                return\n            elseif state == SET_REPEAT then\n                result['exeRepeat'] = tonumber(cmdArg)\n                                     or error('Malformed -r argument: '..cmdArg)\n                return\n            elseif state == SET_PATTERN then\n                if result['pattern'] then\n                    table.insert( result['pattern'], cmdArg )\n                else\n                    result['pattern'] = { cmdArg }\n                end\n                return\n            elseif state == SET_EXCLUDE then\n                local notArg = '!'..cmdArg\n                if result['pattern'] then\n                    table.insert( result['pattern'],  notArg )\n                else\n                    result['pattern'] = { notArg }\n                end\n                return\n            end\n            error('Unknown parse state: '.. state)\n        end\n\n\n        for i, cmdArg in ipairs(cmdLine) do\n            if state ~= nil then\n                setArg( cmdArg, state, result )\n                state = nil\n            else\n                if cmdArg:sub(1,1) == '-' then\n                    state = parseOption( cmdArg )\n                else\n                    if result['testNames'] then\n                        table.insert( result['testNames'], cmdArg )\n                    else\n                        result['testNames'] = { cmdArg }\n                    end\n                end\n            end\n        end\n\n        if result['help'] then\n            M.LuaUnit.help()\n        end\n\n        if result['version'] then\n            M.LuaUnit.version()\n        end\n\n        if state ~= nil then\n            error('Missing argument after '..cmdLine[ #cmdLine ],2 )\n        end\n\n        return result\n    end\n\n    function M.LuaUnit.help()\n        env.info(M.USAGE)\n  --      os.exit(0)\n    end\n\n    function M.LuaUnit.version()\n        env.info('LuaUnit v'..M.VERSION..' by Philippe Fremy <phil@freehackers.org>')\n     --   os.exit(0)\n    end\n\n----------------------------------------------------------------\n--                     class NodeStatus\n----------------------------------------------------------------\n\n    local NodeStatus = { __class__ = 'NodeStatus' } -- class\n    local NodeStatus_MT = { __index = NodeStatus } -- metatable\n    M.NodeStatus = NodeStatus\n\n    -- values of status\n    NodeStatus.SUCCESS  = 'SUCCESS'\n    NodeStatus.SKIP     = 'SKIP'\n    NodeStatus.FAIL     = 'FAIL'\n    NodeStatus.ERROR    = 'ERROR'\n\n    function NodeStatus.new( number, testName, className )\n        -- default constructor, test are PASS by default\n        local t = { number = number, testName = testName, className = className }\n        setmetatable( t, NodeStatus_MT )\n        t:success()\n        return t\n    end\n\n    function NodeStatus:success()\n        self.status = self.SUCCESS\n        -- useless because lua does this for us, but it helps me remembering the relevant field names\n        self.msg = nil\n        self.stackTrace = nil\n    end\n\n    function NodeStatus:skip(msg)\n        self.status = self.SKIP\n        self.msg = msg\n        self.stackTrace = nil\n    end\n\n    function NodeStatus:fail(msg, stackTrace)\n        self.status = self.FAIL\n        self.msg = msg\n        self.stackTrace = stackTrace\n    end\n\n    function NodeStatus:error(msg, stackTrace)\n        self.status = self.ERROR\n        self.msg = msg\n        self.stackTrace = stackTrace\n    end\n\n    function NodeStatus:isSuccess()\n        return self.status == NodeStatus.SUCCESS\n    end\n\n    function NodeStatus:isNotSuccess()\n        -- Return true if node is either failure or error or skip\n        return (self.status == NodeStatus.FAIL or self.status == NodeStatus.ERROR or self.status == NodeStatus.SKIP)\n    end\n\n    function NodeStatus:isSkipped()\n        return self.status == NodeStatus.SKIP\n    end\n\n    function NodeStatus:isFailure()\n        return self.status == NodeStatus.FAIL\n    end\n\n    function NodeStatus:isError()\n        return self.status == NodeStatus.ERROR\n    end\n\n    function NodeStatus:statusXML()\n        if self:isError() then\n            return table.concat(\n                {'            <error type=\"', xmlEscape(self.msg), '\">\\n',\n                 '                <![CDATA[', xmlCDataEscape(self.stackTrace),\n                 ']]></error>\\n'})\n        elseif self:isFailure() then\n            return table.concat(\n                {'            <failure type=\"', xmlEscape(self.msg), '\">\\n',\n                 '                <![CDATA[', xmlCDataEscape(self.stackTrace),\n                 ']]></failure>\\n'})\n        elseif self:isSkipped() then\n            return table.concat({'            <skipped>', xmlEscape(self.msg),'</skipped>\\n' } )\n        end\n        return '            <passed/>\\n' -- (not XSD-compliant! normally shouldn't get here)\n    end\n\n    --------------[[ Output methods ]]-------------------------\n\n    local function conditional_plural(number, singular)\n        -- returns a grammatically well-formed string \"%d <singular/plural>\"\n        local suffix = ''\n        if number ~= 1 then -- use plural\n            suffix = (singular:sub(-2) == 'ss') and 'es' or 's'\n        end\n        return string.format('%d %s%s', number, singular, suffix)\n    end\n\n    function M.LuaUnit.statusLine(result)\n        -- return status line string according to results\n        local s = {\n            string.format('Ran %d tests in %0.3f seconds',\n                          result.runCount, result.duration),\n            conditional_plural(result.successCount, 'success'),\n        }\n        if result.notSuccessCount > 0 then\n            if result.failureCount > 0 then\n                table.insert(s, conditional_plural(result.failureCount, 'failure'))\n            end\n            if result.errorCount > 0 then\n                table.insert(s, conditional_plural(result.errorCount, 'error'))\n            end\n        else\n            table.insert(s, '0 failures')\n        end\n        if result.skippedCount > 0 then\n            table.insert(s, string.format(\"%d skipped\", result.skippedCount))\n        end\n        if result.nonSelectedCount > 0 then\n            table.insert(s, string.format(\"%d non-selected\", result.nonSelectedCount))\n        end\n        return table.concat(s, ', ')\n    end\n\n    function M.LuaUnit:startSuite(selectedCount, nonSelectedCount)\n        self.result = {\n            selectedCount = selectedCount,\n            nonSelectedCount = nonSelectedCount,\n            successCount = 0,\n            runCount = 0,\n            currentTestNumber = 0,\n            currentClassName = \"\",\n            currentNode = nil,\n            suiteStarted = true,\n            startTime = timer.getTime0(), ---os.clock(),\n            startDate = 'Date',--os.date(os.getenv('LUAUNIT_DATEFMT')),\n            startIsodate = 'Date',--os.date('%Y-%m-%dT%H:%M:%S'),\n            patternIncludeFilter = self.patternIncludeFilter,\n\n            -- list of test node status\n            allTests = {},\n            failedTests = {},\n            errorTests = {},\n            skippedTests = {},\n\n            failureCount = 0,\n            errorCount = 0,\n            notSuccessCount = 0,\n            skippedCount = 0,\n        }\n\n        self.outputType = self.outputType or TextOutput\n        self.output = self.outputType.new(self)\n        self.output:startSuite()\n    end\n\n    function M.LuaUnit:startClass( className )\n        self.result.currentClassName = className\n        self.output:startClass( className )\n    end\n\n    function M.LuaUnit:startTest( testName  )\n        self.result.currentTestNumber = self.result.currentTestNumber + 1\n        self.result.runCount = self.result.runCount + 1\n        self.result.currentNode = NodeStatus.new(\n            self.result.currentTestNumber,\n            testName,\n            self.result.currentClassName\n        )\n        self.result.currentNode.startTime = timer.getTime() --os.clock()\n        table.insert( self.result.allTests, self.result.currentNode )\n        self.output:startTest( testName )\n    end\n\n    function M.LuaUnit:updateStatus( err )\n        -- \"err\" is expected to be a table / result from protectedCall()\n        if err.status == NodeStatus.SUCCESS then\n            return\n        end\n\n        local node = self.result.currentNode\n\n        --[[ As a first approach, we will report only one error or one failure for one test.\n\n        However, we can have the case where the test is in failure, and the teardown is in error.\n        In such case, it's a good idea to report both a failure and an error in the test suite. This is\n        what Python unittest does for example. However, it mixes up counts so need to be handled carefully: for\n        example, there could be more (failures + errors) count that tests. What happens to the current node ?\n\n        We will do this more intelligent version later.\n        ]]\n\n        -- if the node is already in failure/error, just don't report the new error (see above)\n        if node.status ~= NodeStatus.SUCCESS then\n            return\n        end\n\n        if err.status == NodeStatus.FAIL then\n            node:fail( err.msg, err.trace )\n            table.insert( self.result.failedTests, node )\n        elseif err.status == NodeStatus.ERROR then\n            node:error( err.msg, err.trace )\n            table.insert( self.result.errorTests, node )\n        elseif err.status == NodeStatus.SKIP then\n            node:skip( err.msg )\n            table.insert( self.result.skippedTests, node )\n        else\n            error('No such status: ' .. prettystr(err.status))\n        end\n\n        self.output:updateStatus( node )\n    end\n\n    function M.LuaUnit:endTest()\n        local node = self.result.currentNode\n        -- env.info( 'endTest() '..prettystr(node))\n        -- env.info( 'endTest() '..prettystr(node:isNotSuccess()))\n        node.duration = timer.getTime() --os.clock() - node.startTime\n        node.startTime = nil\n        self.output:endTest( node )\n\n        if node:isSuccess() then\n            self.result.successCount = self.result.successCount + 1\n        elseif node:isError() then\n            if self.quitOnError or self.quitOnFailure then\n                -- Runtime error - abort test execution as requested by\n                -- \"--error\" option. This is done by setting a special\n                -- flag that gets handled in runSuiteByInstances().\n                env.info(\"\\nERROR during LuaUnit test execution:\\n\" .. node.msg)\n                self.result.aborted = true\n            end\n        elseif node:isFailure() then\n            if self.quitOnFailure then\n                -- Failure - abort test execution as requested by\n                -- \"--failure\" option. This is done by setting a special\n                -- flag that gets handled in runSuiteByInstances().\n                env.info(\"\\nFailure during LuaUnit test execution:\\n\" .. node.msg)\n                self.result.aborted = true\n            end\n        elseif node:isSkipped() then\n            self.result.runCount = self.result.runCount - 1\n        else\n            error('No such node status: ' .. prettystr(node.status))\n        end\n        self.result.currentNode = nil\n    end\n\n    function M.LuaUnit:endClass()\n        self.output:endClass()\n    end\n\n    function M.LuaUnit:endSuite()\n        if self.result.suiteStarted == false then\n            error('LuaUnit:endSuite() -- suite was already ended' )\n        end\n        self.result.duration = timer.getTime() --os.clock()-self.result.startTime\n        self.result.suiteStarted = false\n\n        -- Expose test counts for outputter's endSuite(). This could be managed\n        -- internally instead by using the length of the lists of failed tests\n        -- but unit tests rely on these fields being present.\n        self.result.failureCount = #self.result.failedTests\n        self.result.errorCount = #self.result.errorTests\n        self.result.notSuccessCount = self.result.failureCount + self.result.errorCount\n        self.result.skippedCount = #self.result.skippedTests\n\n        self.output:endSuite()\n    end\n\n    function M.LuaUnit:setOutputType(outputType, fname)\n        -- Configures LuaUnit runner output\n        -- outputType is one of: NIL, TAP, JUNIT, TEXT\n        -- when outputType is junit, the additional argument fname is used to set the name of junit output file\n        -- for other formats, fname is ignored\n        if outputType:upper() == \"NIL\" then\n            self.outputType = NilOutput\n            return\n        end\n        if outputType:upper() == \"TAP\" then\n            self.outputType = TapOutput\n            return\n        end\n        if outputType:upper() == \"JUNIT\" then\n            self.outputType = JUnitOutput\n            if fname then\n                self.fname = fname\n            end\n            return\n        end\n        if outputType:upper() == \"TEXT\" then\n            self.outputType = TextOutput\n            return\n        end\n        error( 'No such format: '..outputType,2)\n    end\n\n    --------------[[ Runner ]]-----------------\n\n    function M.LuaUnit:protectedCall(classInstance, methodInstance, prettyFuncName)\n        -- if classInstance is nil, this is just a function call\n        -- else, it's method of a class being called.\n\n        local function err_handler(e)\n            -- transform error into a table, adding the traceback information\n            return {\n                status = NodeStatus.ERROR,\n                msg = e,\n                trace = string.sub(debug.traceback(\"\", 3), 2)\n            }\n        end\n\n        local ok, err\n        if classInstance then\n            -- stupid Lua < 5.2 does not allow xpcall with arguments so let's use a workaround\n            ok, err = xpcall( function () methodInstance(classInstance) end, err_handler )\n        else\n            ok, err = xpcall( function () methodInstance() end, err_handler )\n        end\n        if ok then\n            return {status = NodeStatus.SUCCESS}\n        end\n\n        local iter_msg\n        iter_msg = self.exeRepeat and 'iteration '..self.currentCount\n\n        err.msg, err.status = M.adjust_err_msg_with_iter( err.msg, iter_msg )\n\n        if err.status == NodeStatus.SUCCESS or err.status == NodeStatus.SKIP then\n            err.trace = nil\n            return err\n        end\n\n        -- reformat / improve the stack trace\n        if prettyFuncName then -- we do have the real method name\n            err.trace = err.trace:gsub(\"in (%a+) 'methodInstance'\", \"in %1 '\"..prettyFuncName..\"'\")\n        end\n        if STRIP_LUAUNIT_FROM_STACKTRACE then\n            err.trace = stripLuaunitTrace(err.trace)\n        end\n\n        return err -- return the error \"object\" (table)\n    end\n\n\n    function M.LuaUnit:execOneFunction(className, methodName, classInstance, methodInstance)\n        -- When executing a test function, className and classInstance must be nil\n        -- When executing a class method, all parameters must be set\n\n        if type(methodInstance) ~= 'function' then\n            error( tostring(methodName)..' must be a function, not '..type(methodInstance))\n        end\n\n        local prettyFuncName\n        if className == nil then\n            className = '[TestFunctions]'\n            prettyFuncName = methodName\n        else\n            prettyFuncName = className..'.'..methodName\n        end\n\n        if self.lastClassName ~= className then\n            if self.lastClassName ~= nil then\n                self:endClass()\n            end\n            self:startClass( className )\n            self.lastClassName = className\n        end\n\n        self:startTest(prettyFuncName)\n\n        local node = self.result.currentNode\n        for iter_n = 1, self.exeRepeat or 1 do\n            if node:isNotSuccess() then\n                break\n            end\n            self.currentCount = iter_n\n\n            -- run setUp first (if any)\n            if classInstance then\n                local func = self.asFunction( classInstance.setUp ) or\n                             self.asFunction( classInstance.Setup ) or\n                             self.asFunction( classInstance.setup ) or\n                             self.asFunction( classInstance.SetUp )\n                if func then\n                    self:updateStatus(self:protectedCall(classInstance, func, className..'.setUp'))\n                end\n            end\n\n            -- run testMethod()\n            if node:isSuccess() then\n                self:updateStatus(self:protectedCall(classInstance, methodInstance, prettyFuncName))\n            end\n\n            -- lastly, run tearDown (if any)\n            if classInstance then\n                local func = self.asFunction( classInstance.tearDown ) or\n                             self.asFunction( classInstance.TearDown ) or\n                             self.asFunction( classInstance.teardown ) or\n                             self.asFunction( classInstance.Teardown )\n                if func then\n                    self:updateStatus(self:protectedCall(classInstance, func, className..'.tearDown'))\n                end\n            end\n        end\n\n        self:endTest()\n    end\n\n    function M.LuaUnit.expandOneClass( result, className, classInstance )\n        --[[\n        Input: a list of { name, instance }, a class name, a class instance\n        Ouptut: modify result to add all test method instance in the form:\n        { className.methodName, classInstance }\n        ]]\n        for methodName, methodInstance in sortedPairs(classInstance) do\n            if M.LuaUnit.asFunction(methodInstance) and M.LuaUnit.isMethodTestName( methodName ) then\n                table.insert( result, { className..'.'..methodName, classInstance } )\n            end\n        end\n    end\n\n    function M.LuaUnit.expandClasses( listOfNameAndInst )\n        --[[\n        -- expand all classes (provided as {className, classInstance}) to a list of {className.methodName, classInstance}\n        -- functions and methods remain untouched\n\n        Input: a list of { name, instance }\n\n        Output:\n        * { function name, function instance } : do nothing\n        * { class.method name, class instance }: do nothing\n        * { class name, class instance } : add all method names in the form of (className.methodName, classInstance)\n        ]]\n        local result = {}\n\n        for i,v in ipairs( listOfNameAndInst ) do\n            local name, instance = v[1], v[2]\n            if M.LuaUnit.asFunction(instance) then\n                table.insert( result, { name, instance } )\n            else\n                if type(instance) ~= 'table' then\n                    error( 'Instance must be a table or a function, not a '..type(instance)..' with value '..prettystr(instance))\n                end\n                local className, methodName = M.LuaUnit.splitClassMethod( name )\n                if className then\n                    local methodInstance = instance[methodName]\n                    if methodInstance == nil then\n                        error( \"Could not find method in class \"..tostring(className)..\" for method \"..tostring(methodName) )\n                    end\n                    table.insert( result, { name, instance } )\n                else\n                    M.LuaUnit.expandOneClass( result, name, instance )\n                end\n            end\n        end\n\n        return result\n    end\n\n    function M.LuaUnit.applyPatternFilter( patternIncFilter, listOfNameAndInst )\n        local included, excluded = {}, {}\n        for i, v in ipairs( listOfNameAndInst ) do\n            -- local name, instance = v[1], v[2]\n            if  patternFilter( patternIncFilter, v[1] ) then\n                table.insert( included, v )\n            else\n                table.insert( excluded, v )\n            end\n        end\n        return included, excluded\n    end\n\n    function M.LuaUnit:runSuiteByInstances( listOfNameAndInst )\n        --[[ Run an explicit list of tests. Each item of the list must be one of:\n        * { function name, function instance }\n        * { class name, class instance }\n        * { class.method name, class instance }\n        ]]\n\n        local expandedList = self.expandClasses( listOfNameAndInst )\n        if self.shuffle then\n            randomizeTable( expandedList )\n        end\n        local filteredList, filteredOutList = self.applyPatternFilter(\n            self.patternIncludeFilter, expandedList )\n\n        self:startSuite( #filteredList, #filteredOutList )\n\n        for i,v in ipairs( filteredList ) do\n            local name, instance = v[1], v[2]\n            if M.LuaUnit.asFunction(instance) then\n                self:execOneFunction( nil, name, nil, instance )\n            else\n                -- expandClasses() should have already taken care of sanitizing the input\n                assert( type(instance) == 'table' )\n                local className, methodName = M.LuaUnit.splitClassMethod( name )\n                assert( className ~= nil )\n                local methodInstance = instance[methodName]\n                assert(methodInstance ~= nil)\n                self:execOneFunction( className, methodName, instance, methodInstance )\n            end\n            if self.result.aborted then\n                break -- \"--error\" or \"--failure\" option triggered\n            end\n        end\n\n        if self.lastClassName ~= nil then\n            self:endClass()\n        end\n\n        self:endSuite()\n\n        if self.result.aborted then\n            env.info(\"LuaUnit ABORTED (as requested by --error or --failure option)\")\n            os.exit(-2)\n        end\n    end\n\n    function M.LuaUnit:runSuiteByNames( listOfName )\n        --[[ Run LuaUnit with a list of generic names, coming either from command-line or from global\n            namespace analysis. Convert the list into a list of (name, valid instances (table or function))\n            and calls runSuiteByInstances.\n        ]]\n\n        local instanceName, instance\n        local listOfNameAndInst = {}\n\n        for i,name in ipairs( listOfName ) do\n            local className, methodName = M.LuaUnit.splitClassMethod( name )\n            if className then\n                instanceName = className\n                instance = _G[instanceName]\n\n                if instance == nil then\n                    error( \"No such name in global space: \"..instanceName )\n                end\n\n                if type(instance) ~= 'table' then\n                    error( 'Instance of '..instanceName..' must be a table, not '..type(instance))\n                end\n\n                local methodInstance = instance[methodName]\n                if methodInstance == nil then\n                    error( \"Could not find method in class \"..tostring(className)..\" for method \"..tostring(methodName) )\n                end\n\n            else\n                -- for functions and classes\n                instanceName = name\n                instance = _G[instanceName]\n            end\n\n            if instance == nil then\n                error( \"No such name in global space: \"..instanceName )\n            end\n\n            if (type(instance) ~= 'table' and type(instance) ~= 'function') then\n                error( 'Name must match a function or a table: '..instanceName )\n            end\n\n            table.insert( listOfNameAndInst, { name, instance } )\n        end\n\n        self:runSuiteByInstances( listOfNameAndInst )\n    end\n\n    function M.LuaUnit.run(...)\n        -- Run some specific test classes.\n        -- If no arguments are passed, run the class names specified on the\n        -- command line. If no class name is specified on the command line\n        -- run all classes whose name starts with 'Test'\n        --\n        -- If arguments are passed, they must be strings of the class names\n        -- that you want to run or generic command line arguments (-o, -p, -v, ...)\n\n        local runner = M.LuaUnit.new()\n        return runner:runSuite(...)\n    end\n\n    function M.LuaUnit:runSuite( ... )\n\n        local args = {...}\n        if type(args[1]) == 'table' and args[1].__class__ == 'LuaUnit' then\n            -- run was called with the syntax M.LuaUnit:runSuite()\n            -- we support both M.LuaUnit.run() and M.LuaUnit:run()\n            -- strip out the first argument\n            table.remove(args,1)\n        end\n\n        if #args == 0 then\n            args = cmdline_argv\n        end\n\n        local options = pcall_or_abort( M.LuaUnit.parseCmdLine, args )\n\n        -- We expect these option fields to be either `nil` or contain\n        -- valid values, so it's safe to always copy them directly.\n        self.verbosity     = M.VERBOSITY_VERBOSE\n        self.quitOnError   = options.quitOnError\n        self.quitOnFailure = options.quitOnFailure\n\n        self.exeRepeat            = options.exeRepeat\n        self.patternIncludeFilter = options.pattern\n        self.shuffle              = options.shuffle\n\n        if options.output then\n            if options.output:lower() == 'junit' and options.fname == nil then\n                env.info('With junit output, a filename must be supplied with -n or --name')\n                os.exit(-1)\n            end\n            pcall_or_abort(self.setOutputType, self, options.output, options.fname)\n        end\n\n        self:runSuiteByNames( options.testNames or M.LuaUnit.collectTests() )\n\n        return self.result.notSuccessCount\n    end\n-- class LuaUnit\n\n-- For compatbility with LuaUnit v2\nM.run = M.LuaUnit.run\nM.Run = M.LuaUnit.run\n\nfunction M:setVerbosity( verbosity )\n    M.LuaUnit.verbosity = verbosity\nend\nM.set_verbosity = M.setVerbosity\nM.SetVerbosity = M.setVerbosity\n\nlu = {}\nlu = M\n\nreturn"
  },
  {
    "path": "unit-tests/skynet-unit-test-iads-setup.lua",
    "content": "do\n--- create an iads so the mission can be played, the ones in the unit tests, are cleaned once the tests are finished\n\nredIADS = SkynetIADS:create(\"Red IADS\")\nlocal iadsDebug = redIADS:getDebugSettings()\niadsDebug.IADSStatus = true\niadsDebug.contacts = true\niadsDebug.harmDefence = true\n--[[\niadsDebug.radarWentDark = true\niadsDebug.radarWentLive = true\niadsDebug.jammerProbability = true\niadsDebug.addedEWRadar = true\niadsDebug.addedSAMSite = true\n\niadsDebug.commandCenterStatusEnvOutput = true\niadsDebug.samSiteStatusEnvOutput = true\niadsDebug.earlyWarningRadarStatusEnvOutput = true\n--]]\n\nlocal comCenter = Unit.getByName('connection-node-ew')\nlocal power = StaticObject.getByName('Command Center Power')\nlocal connection = Unit.getByName('connection-node-ew')\nredIADS:addCommandCenter(comCenter):addPowerSource(power):addConnectionNode(connection)\n\nlocal comCenter2 = StaticObject.getByName('Command Center')\nredIADS:addCommandCenter(comCenter2)\n\nredIADS:addEarlyWarningRadarsByPrefix('EW')\nredIADS:addSAMSitesByPrefix('SAM'):setHARMDetectionChance(100)\n\newConnectionNode = Unit.getByName('connection-node-ew')\nredIADS:getEarlyWarningRadarByUnitName('EW-west2'):setHARMDetectionChance(100):addConnectionNode(ewConnectionNode)\nlocal sa15 = redIADS:getSAMSiteByGroupName('SAM-SA-15-1')\nredIADS:getSAMSiteByGroupName('SAM-SA-10'):setActAsEW(true):setHARMDetectionChance(100):addPointDefence(sa15)\nredIADS:getSAMSiteByGroupName('SAM-HQ-7'):setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE)\nlocal connectioNode = StaticObject.getByName('Unused Connection Node')\nredIADS:getSAMSiteByGroupName('SAM-SA-6-2'):addConnectionNode(connectioNode):setGoLiveRangeInPercent(120):setHARMDetectionChance(100)\n\nredIADS:getEarlyWarningRadarByUnitName('EW-SR-P19'):addPointDefence(redIADS:getSAMSiteByGroupName('SAM-SA-15-P19'))\n\n\n\nredIADS:addRadioMenu()\nredIADS:activate()\n\nblueIADS = SkynetIADS:create(\"UAE\")\nblueIADS:addSAMSitesByPrefix('BLUE-SAM')\nblueIADS:addEarlyWarningRadarsByPrefix('BLUE-EW')\nblueIADS:getSAMSitesByNatoName('Rapier'):setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE)\nblueIADS:getSAMSitesByNatoName('Roland ADS'):setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE)\nblueIADS:addRadioMenu()\nblueIADS:activate()\n\n--[[\nlocal iadsDebug = blueIADS:getDebugSettings()\niadsDebug.IADSStatus = true\niadsDebug.radarWentDark = true\niadsDebug.contacts = true\niadsDebug.radarWentLive = true\n--]]\n\n\nlocal jammer = SkynetIADSJammer:create(Unit.getByName('jammer-source'), redIADS)\njammer:addRadioMenu()\n\nposCounter = 0\ninitialPosition = nil\nsecondPoisition = nil\ncalculatedPosition = nil\n\nfunction Vec3CalculationSpike()\n\n\tif posCounter == 1 then\n\t\tinitialPosition = Unit.getByName('Hornet SA-6 Attack'):getPosition().p\n\t\tenv.info(\"Initial Position X:\"..initialPosition.x..\" Y:\"..initialPosition.y..\" Z:\"..initialPosition.z)\n\tend\n\t\n\tif posCounter == 2 then\n\t\tsecondPoisition = Unit.getByName('Hornet SA-6 Attack'):getPosition().p\n\t\tenv.info(\"Second Position X:\"..secondPoisition.x..\" Y:\"..secondPoisition.y..\" Z:\"..secondPoisition.z)\n\tend\n\t\n\tif posCounter >= 2 then\n\t\t\n\t\tlocal deltaX = (secondPoisition.x - initialPosition.x)\n\t\t--y represents altitude in implementation don't increment this value it may skyrocket or go below 0\n\t\tlocal deltaY = (secondPoisition.y - initialPosition.y)\n\t\tlocal deltaZ = (secondPoisition.z - initialPosition.z)\n\t\t\n\t\tenv.info(\"deltas X:\"..deltaX..\" Y:\"..deltaY..\" Z:\"..deltaZ)\n\t\tenv.info(\"------------------------------------------------\")\n\t\t\n\t\tif calculatedPosition == nil then\n\t\t\tcalculatedPosition  = {}\n\t\t\tcalculatedPosition.x = initialPosition.x\n\t\t\tcalculatedPosition.y = initialPosition.y\n\t\t\tcalculatedPosition.z = initialPosition.z\n\t\tend\n\t\t\n\t\tcalculatedPosition.x = calculatedPosition.x + deltaX\n\t\tcalculatedPosition.y = calculatedPosition.y + deltaY\n\t\tcalculatedPosition.z = calculatedPosition.z + deltaZ\n\t\t\n\t\tlocal currentPosition = Unit.getByName('Hornet SA-6 Attack'):getPosition().p\n\t\t\n\t\tenv.info(\"Calculated Position X:\"..calculatedPosition.x..\" Y:\"..calculatedPosition.y..\" Z:\"..calculatedPosition.z)\n\t\tenv.info(\"Current Position X:\"..currentPosition.x..\" Y:\"..currentPosition.y..\" Z:\"..currentPosition.z)\n\t\tlocal difX = currentPosition.x - calculatedPosition.x\n\t\tlocal difY = currentPosition.y - calculatedPosition.y\n\t\tlocal difZ  = currentPosition.z - calculatedPosition.z\n\t\t\n\t\tenv.info(\"Difference X:\"..difX..\" Y:\"..difY..\" Z:\"..difZ)\n\t\tenv.info(\"------------------------------------------------\")\n\t\t\n\tend\n\t\n\tposCounter = posCounter + 1\nend\n\n--mist.scheduleFunction(Vec3CalculationSpike, {}, 1, 1)\n\nend"
  },
  {
    "path": "unit-tests/skynet-unit-tests.lua",
    "content": "do\n\n---IADS Unit Tests\nSKYNET_UNIT_TESTS_NUM_EW_SITES_RED = 17\nSKYNET_UNIT_TESTS_NUM_SAM_SITES_RED = 17\n\n--factory method used in multiple unit tests\nfunction IADSContactFactory(unitName)\n\tlocal contact = Unit.getByName(unitName)\n\tlocal radarContact = {}\n\tradarContact.object = contact\n\tlocal iadsContact = SkynetIADSContact:create(radarContact)\n\tiadsContact:refresh()\n\treturn  iadsContact\nend\n\nfunction createDeadEvent()\n\tlocal event = {}\n\tevent.id = world.event.S_EVENT_DEAD\n\treturn event\nend\n\n\nlu.LuaUnit.run()\n\n--clean mist left over scheduled tasks form unit tests, check there are no left over tasks in the IADS\nlocal i = 0\nwhile i < 10000 do\n\tlocal id =  mist.removeFunction(i)\n\ti = i + 1\n\tif id then\n\t\tenv.info(\"WARNING: IADS left over Tasks\")\n\tend\nend\n\nend"
  },
  {
    "path": "unit-tests/test-skynet-iads-abstract-dcs-object-wrapper.lua",
    "content": "do\n\nTestSkynetIADSAbstractDCSObjectWrapper = {}\n\nfunction TestSkynetIADSAbstractDCSObjectWrapper:setUp()\n\tself.abstractObjectWrapper = SkynetIADSAbstractDCSObjectWrapper:create(Unit.getByName('EW-SA-6'))\nend\n\nfunction TestSkynetIADSAbstractDCSObjectWrapper:tearDown()\n\nend\n\nfunction TestSkynetIADSAbstractDCSObjectWrapper:testGetName()\n\tlu.assertEquals(self.abstractObjectWrapper:getName(), 'EW-SA-6')\n\tself.abstractObjectWrapper.dcsRepresentation = nil\n\t--test to see if name is still returned after object wrapped is nil\n\tlu.assertEquals(self.abstractObjectWrapper:getName(), 'EW-SA-6')\nend\n\nfunction TestSkynetIADSAbstractDCSObjectWrapper:testGetTypeName()\n\tlu.assertEquals(self.abstractObjectWrapper:getTypeName(), 'Kub 1S91 str')\n\tself.abstractObjectWrapper.dcsRepresentation = nil\n\tlu.assertEquals(self.abstractObjectWrapper:getTypeName(), 'Kub 1S91 str')\nend\n\nfunction TestSkynetIADSAbstractDCSObjectWrapper:testIsExist()\n\tlu.assertEquals(self.abstractObjectWrapper:isExist(), true)\n\tself.abstractObjectWrapper.dcsRepresentation = nil\n\tlu.assertEquals(self.abstractObjectWrapper:isExist(), false)\nend\n\nfunction TestSkynetIADSAbstractDCSObjectWrapper:testGetDCSRepresentation()\n\tlu.assertEquals(self.abstractObjectWrapper:getDCSRepresentation(), Unit.getByName('EW-SA-6'))\nend\n\nfunction TestSkynetIADSAbstractDCSObjectWrapper:testInsertToTableIfNotAlreadyAdded()\n\tlocal tbl = {}\n\tlocal mock = {}\n\ttable.insert(tbl, mock)\n\tlocal result = self.abstractObjectWrapper:insertToTableIfNotAlreadyAdded(tbl, mock)\n\tlu.assertEquals(#tbl, 1)\n\tlu.assertEquals(result, false)\n\t\n\t\n\tlocal mock2 = {}\n\tlocal result2 = self.abstractObjectWrapper:insertToTableIfNotAlreadyAdded(tbl, mock2)\n\tlu.assertEquals(#tbl, 2)\n\tlu.assertEquals(result2, true)\nend\n\nend\n"
  },
  {
    "path": "unit-tests/test-skynet-iads-abstract-element.lua",
    "content": "do\n\t\nTestSkynetIADSAbstractElement = {}\n\nfunction TestSkynetIADSAbstractElement:setUp()\n\tself.iads =  SkynetIADS:create()\n\tself.abstractElement = SkynetIADSAbstractElement:create(Group.getByName(\"SAM-SA-6-2\"), self.iads)\n\t\n\t--mock this fucntion we test it once in testCheckOneGenericObjectAliveForUnitWorks\n\tfunction self.abstractElement:setToCorrectAutonomousState()\n\tend\nend\n\nfunction TestSkynetIADSAbstractElement:tearDown()\n\tself.abstractElement:cleanUp()\nend\n\n-- by default an abstractElement will return true if no power source or connection node ist set\nfunction TestSkynetIADSAbstractElement:testHasActiveConnectionNodeByDefaultIfNoneIsSet()\n\tlu.assertEquals(self.abstractElement:genericCheckOneObjectIsAlive({}), true)\n\tlu.assertEquals(self.abstractElement:hasActiveConnectionNode(), true)\n\tlu.assertEquals(self.abstractElement:hasWorkingPowerSource(), true)\nend\n\nfunction TestSkynetIADSAbstractElement:testCheckOneGenericObjectAliveForUnitWorks()\n\tlocal unit = Unit.getByName('SAM-SA-6-2-connection-node-unit')\n\t\n\tlocal called = false\n\t\n\tfunction self.abstractElement:informChildrenOfStateChange()\n\t\tcalled = true\n\tend\n\t\n\tself.abstractElement:addConnectionNode(unit)\n\tlu.assertEquals(called, true)\n\tlu.assertEquals(self.abstractElement:genericCheckOneObjectIsAlive(self.abstractElement.connectionNodes), true)\n\tlu.assertEquals(self.abstractElement:hasActiveConnectionNode(), true)\n\ttrigger.action.explosion(unit:getPosition().p, 1000)\n\tlu.assertEquals(self.abstractElement:genericCheckOneObjectIsAlive(self.abstractElement.connectionNodes), false)\n\tlu.assertEquals(self.abstractElement:hasActiveConnectionNode(), false)\nend\n\n\nfunction TestSkynetIADSAbstractElement:testCheckOneGenericObjectAliveForStaticObjectsWorks()\n\tlocal static = StaticObject.getByName('SAM-SA-6-2-coonection-node-static')\n\tself.abstractElement:addConnectionNode(static)\n\tlu.assertEquals(self.abstractElement:genericCheckOneObjectIsAlive(self.abstractElement.connectionNodes), true)\n\tlu.assertEquals(self.abstractElement:hasActiveConnectionNode(), true)\n\ttrigger.action.explosion(static:getPosition().p, 1000)\n\tlu.assertEquals(self.abstractElement:genericCheckOneObjectIsAlive(self.abstractElement.connectionNodes), false)\n\tlu.assertEquals(self.abstractElement:hasActiveConnectionNode(), false)\nend\n\nfunction TestSkynetIADSAbstractElement:testPowerSourceAndConnectionNodeStaticObjectAndDestrutionSuccessful()\n\n\tlocal powerSource = StaticObject.getByName(\"test-ground-vehicle-power-source\")\n\tlocal connectionNode = StaticObject.getByName(\"test-ground-vehicle-connection-node\")\n\t\t\n\tself.abstractElement:addPowerSource(powerSource)\n\tself.abstractElement:addConnectionNode(connectionNode)\n\tlu.assertEquals(self.abstractElement:hasWorkingPowerSource(), true)\n\tlu.assertEquals(self.abstractElement:hasActiveConnectionNode(), true)\n\t\n\ttrigger.action.explosion(powerSource:getPosition().p, 100)\n\ttrigger.action.explosion(connectionNode:getPosition().p, 500)\n\t\n\tlu.assertEquals(self.abstractElement:hasWorkingPowerSource(), false)\n\tlu.assertEquals(self.abstractElement:hasActiveConnectionNode(), false)\nend\t\n\nfunction TestSkynetIADSAbstractElement:testGetNatoName()\n\tlu.assertEquals(self.abstractElement:getNatoName(), \"UNKNOWN\")\nend\n\nfunction TestSkynetIADSAbstractElement:testGetDescription()\n\tlu.assertEquals(self.abstractElement:getDescription(), \"IADS ELEMENT: SAM-SA-6-2 | Type: UNKNOWN\")\nend\n\nfunction TestSkynetIADSAbstractElement:testGetDCSRepresentation()\n\tlu.assertEquals(self.abstractElement:getDCSRepresentation(), Group.getByName(\"SAM-SA-6-2\"))\nend\n\nfunction TestSkynetIADSAbstractElement:testGetDCSName()\n\tlu.assertEquals(self.abstractElement:getDCSName(), \"SAM-SA-6-2\")\n\t\n\t--overwrite the DCSRepresentation to test caching on the DCS unit / group name\n\tfunction self.abstractElement:getDCSRepresentation()\n\t\treturn nil\n\tend\n\n\tlu.assertEquals(self.abstractElement:getDCSName(), \"SAM-SA-6-2\")\n\nend\nend\n"
  },
  {
    "path": "unit-tests/test-skynet-iads-abstract-radar-element.lua",
    "content": "do\nTestSkynetIADSAbstractRadarElement = {}\n\nfunction TestSkynetIADSAbstractRadarElement:setUp()\n\tif self.samSiteName then\n\t\tself.skynetIADS = SkynetIADS:create()\n\t\tlocal samSite = Group.getByName(self.samSiteName)\n\t\tself.samSite = SkynetIADSSamSite:create(samSite, self.skynetIADS)\n\t\t\n\t\t-- we overrite this method since it returns radar contacts in the DCS world which mess up the tests.\n\t\tfunction self.samSite:getDetectedTargets()\n\t\t\treturn {}\n\t\tend\n\t\t\n\t\tself.samSite:setupElements()\n\t\tself.samSite:goLive()\n\tend\nend\n\nfunction TestSkynetIADSAbstractRadarElement:tearDown()\n\tif self.samSite then\t\n\t\tself.samSite:goDark()\n\t\tself.samSite:cleanUp()\n\tend\n\tif self.skynetIADS then\n\t\tself.skynetIADS:deactivate()\n\tend\n\tself.samSite = nil\n\tself.samSiteName = nil\nend\n\n--TODO: test other calls in the GoDark Method\nfunction TestSkynetIADSAbstractRadarElement:testGoDark()\n\tself.samSiteName = \"SAM-SA-6-2\"\n\tself:setUp()\n\t\n\tlocal mockRepresentation = {}\n\t\n\tlocal emissionState = nil\n\t\n\tfunction mockRepresentation:enableEmission(state)\n\t\temissionState = false\n\tend\n\t\n\tfunction mockRepresentation:isExist()\n\t\treturn true\n\tend\n\t\n\tfunction self.samSite:getDCSRepresentation()\n\t\treturn mockRepresentation\n\tend\n\t\n\t\n\tlocal mockController = {}\n\t\n\tfunction mockController:setOption(option)\n\t\n\tend\n\t\n\tfunction mockRepresentation:getController()\n\t\treturn mockController\n\tend\n\t\n\ttable.insert(self.samSite.cachedTargets,{\"Mock1\"})\n\tself.samSite:goDark()\n\tlu.assertEquals(self.samSite:isActive(), false)\n\tlu.assertEquals(emissionState, false)\n\tlu.assertEquals(#self.samSite.cachedTargets, 0)\n\t\nend\n\n--TODO: test other calls in the GoLive Method\nfunction TestSkynetIADSAbstractRadarElement:testGoLive()\n\tself.samSiteName = \"SAM-SA-6-2\"\n\tself:setUp()\n\tself.samSite:goDark()\n\t\n\tlocal mockRepresentation = {}\n\t\n\tlocal emissionState = nil\n\t\n\tfunction mockRepresentation:enableEmission(state)\n\t\temissionState = true\n\tend\n\t\n\tfunction mockRepresentation:isExist()\n\t\treturn true\n\tend\n\t\n\tfunction self.samSite:getDCSRepresentation()\n\t\treturn mockRepresentation\n\tend\n\t\n\t\n\tlocal mockController = {}\n\t\n\tfunction mockController:setOption(option)\n\t\n\tend\n\t\n\tfunction mockRepresentation:getController()\n\t\treturn mockController\n\tend\n\t\n\t--test so see if controller is called when setting site live:\n\tcall = 0\n\tfunction mockController:setOnOff(state)\n\t\tlu.assertEquals(state, true)\n\t\tcall = 1\n\tend\n\t\n\tself.samSite:goLive()\n\tlu.assertEquals(call, 1)\n\tlu.assertEquals(self.samSite:isActive(), true)\n\tlu.assertEquals(emissionState, true)\nend\n\t\nfunction TestSkynetIADSAbstractRadarElement:testGoDarkDueToHARMTestIfAIisOff()\n\tself.samSiteName = \"SAM-SA-2\"\n\tself:setUp()\n\tlocal mockController = {}\n\tlocal call = 0\n\tfunction mockController:setOnOff(state)\n\t\tlu.assertEquals(state, false)\n\t\tcall = 1\n\tend\n\tfunction self.samSite:getController()\n\t\treturn mockController\n\tend\n\tself.samSite:goSilentToEvadeHARM(10)\n\tlu.assertEquals(self.samSite:isActive(), false)\n\tlu.assertEquals(call, 1)\n\t\n\t--test so no controller call is made if sam site is destroyed:\n\tself.samSiteName = \"SAM-SA-2\"\n\tself:setUp()\n\tlocal mockController = {}\n\tcall = 0\n\tfunction mockController:setOnOff(state)\n\t\tcall = call + 1\n\tend\n\tfunction self.samSite:getController()\n\t\treturn mockController\n\tend\n\tfunction self.samSite:isDestroyed()\n\t\treturn true\n\tend\n\tself.samSite:goSilentToEvadeHARM(10)\n\tlu.assertEquals(self.samSite:isActive(), false)\n\tlu.assertEquals(call, 0)\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testCanEngageAirWeapons()\n\tself.samSiteName = \"SAM-SA-6-2\"\n\tself:setUp()\n\t\n\tlocal called = false\n\tlocal mockController = {}\n\tfunction mockController:setOption(option, value)\n\t\tlu.assertEquals(option, AI.Option.Ground.id.ENGAGE_AIR_WEAPONS)\n\t\tlu.assertEquals(value, true)\n\t\tcalled = true\n\tend\n\t\n\tlocal mockDCSRepresenation = {}\n\tfunction mockDCSRepresenation:getController()\n\t\treturn mockController\n\tend\n\t\n\tfunction self.samSite:getDCSRepresentation()\n\t\treturn mockDCSRepresenation\n\tend\n\n\t\n\tfunction self.samSite:getController()\n\t\treturn mockController\n\tend\n\t--by default SAM site is not set to engage air weapons in Skynet:\n\tlu.assertEquals(self.samSite:getCanEngageAirWeapons(), false)\n\tlu.assertEquals(self.samSite:setCanEngageAirWeapons(true), self.samSite)\n\tlu.assertEquals(self.samSite:getCanEngageAirWeapons(), true)\n\tlu.assertEquals(called, true)\n\t\n\t--we test that calling setEngageAirWeapons with true on a SAM site that can by default engage harms also sets canEngageHarm to true\n\t\n\tfunction mockController:setOption(option, value)\n\tend\n\t\n\tself.samSite.dataBaseSupportedTypesCanEngageHARM = true\n\tself.samSite:setCanEngageAirWeapons(false)\n\tself.samSite:setCanEngageAirWeapons(true)\n\tlu.assertEquals(self.samSite:getCanEngageHARM(), true)\n\tself.samSite = nil\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testCanEngageHARM()\n\tself.samSiteName = \"SAM-SA-6-2\"\n\tself:setUp()\n\t\n\tlocal called = false\n\tfunction self.samSite:setCanEngageAirWeapons(state)\n\t\tlu.assertEquals(state, true)\n\t\tcalled = true\n\tend\n\t\n\tlu.assertEquals(self.samSite:setCanEngageHARM(true), self.samSite)\n\tlu.assertEquals(self.samSite:getCanEngageHARM(), true)\n\tlu.assertEquals(called, true)\n\t\n\tlocal called = false\n\tself.samSite:setCanEngageHARM(false)\n\tlu.assertEquals(self.samSite:getCanEngageHARM(), false)\n\tlu.assertEquals(called, false)\n\t\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testAddParentRadarAndClearParentRadars()\n\tself.samSiteName = \"SAM-SA-6-2\"\n\tself:setUp()\n\t\n\tlocal called = false\n\tfunction self.samSite:setToCorrectAutonomousState()\n\t\tcalled = true\n\tend\n\t\n\tlu.assertEquals(#self.samSite:getParentRadars(), 0)\n\tlocal parentRad1 = {}\n\tself.samSite:addParentRadar(parentRad1)\n\tlu.assertEquals(#self.samSite:getParentRadars(), 1)\n\t\n\t--try adding the same radar again, make sure its not added:\n\tself.samSite:addParentRadar(parentRad1)\n\tlu.assertEquals(#self.samSite:getParentRadars(), 1)\n\t\n\tlocal parentRad2 = {}\n\tself.samSite:addParentRadar(parentRad2)\n\tlu.assertEquals(#self.samSite:getParentRadars(), 2)\n\t\n\tlu.assertEquals(self.samSite:getParentRadars()[1], parentRad2)\n\tlu.assertEquals(self.samSite:getParentRadars()[2], parentRad1)\n\t\n\tlu.assertEquals(called, true)\n\t\n\t--reset array to prevent teardown issues with mock objects\n\tself.samSite:clearParentRadars()\n\tlu.assertEquals(#self.samSite:getParentRadars(), 0)\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testAddChildRadarAndClearChildRadars()\n\tself.samSiteName = \"SAM-SA-6-2\"\n\tself:setUp()\n\tlu.assertEquals(#self.samSite:getChildRadars(), 0)\n\tlocal childRad1 = {}\n\tself.samSite:addChildRadar(childRad1)\n\tlu.assertEquals(#self.samSite:getChildRadars(), 1)\n\t\n\t--try adding the same radar again, make sure its not added:\n\tself.samSite:addChildRadar(childRad1)\n\tlu.assertEquals(#self.samSite:getChildRadars(), 1)\n\t\n\tlocal childRad2 = {}\n\tself.samSite:addChildRadar(childRad2)\n\tlu.assertEquals(#self.samSite:getChildRadars(), 2)\n\t\n\tlu.assertEquals(self.samSite:getChildRadars()[1], childRad1)\n\tlu.assertEquals(self.samSite:getChildRadars()[2], childRad2)\n\t\n\t--reset array to prevent teardown issues with mock objects\n\tself.samSite:clearChildRadars()\n\tlu.assertEquals(#self.samSite:getChildRadars(), 0)\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testGetUsableChildRadars()\n\t\n\tself.samSiteName = \"SAM-SA-6-2\"\n\tself:setUp()\n\t\n\tfunction self.samSite:setToCorrectAutonomousState()\n\t\n\tend\n\t\n\tlocal childRad1 = {}\n\tfunction childRad1:hasWorkingPowerSource()\n\t\treturn false\n\tend\n\t\n\tfunction childRad1:hasActiveConnectionNode()\n\t\treturn true\n\tend\n\t\n\tself.samSite:addChildRadar(childRad1)\n\t\n\tlu.assertEquals(#self.samSite:getUsableChildRadars(), 0)\n\t\n\t\n\tfunction childRad1:hasWorkingPowerSource()\n\t\treturn true\n\tend\n\t\t\n\tfunction childRad1:hasActiveConnectionNode()\n\t\treturn false\n\tend\n\t\n\tlu.assertEquals(#self.samSite:getUsableChildRadars(), 0)\n\t\n\t\n\tfunction childRad1:hasWorkingPowerSource()\n\t\treturn true\n\tend\n\t\n\tfunction childRad1:hasActiveConnectionNode()\n\t\treturn true\n\tend\n\t\n\tlu.assertEquals(#self.samSite:getUsableChildRadars(), 1)\n\n\t--reset array to prevent teardown issues with mock objects\n\tself.samSite.childRadars = {}\n\t\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testInformChildrenOfStateChange()\n\t\n\tself.samSiteName = \"SAM-SA-6-2\"\n\tself:setUp()\n\t\n\t--we ensure the moose connector is updated if a state of an IADS radar changes\n\tlocal updateCalled = false\n\tlocal mockMoose = {}\n\tfunction mockMoose:update()\n\t\tupdateCalled = true\n\tend\n\tfunction self.samSite.iads:getMooseConnector()\n\t\treturn mockMoose\n\tend\n\t\n\tlocal calls = 0\n\tlocal childRad1 = {}\n\tfunction childRad1:setToCorrectAutonomousState()\n\t\tcalls = calls + 1\n\tend\n\tself.samSite:addChildRadar(childRad1)\n\t\n\tlocal childRad2 = {}\n\tfunction childRad2:setToCorrectAutonomousState()\n\t\tcalls = calls + 1\n\tend\n\tself.samSite:addChildRadar(childRad2)\n\t\n\tself.samSite:informChildrenOfStateChange()\n\n\tlu.assertEquals(updateCalled, true)\n\tlu.assertEquals(calls, 2)\nend\n\n--this test is related to testInformChildrenOfStateChange it tests, if SAM site go to their correct state depending on destruction of connection nodes and power sources \n-- TODO: remove SkynetIADS variable to reduce test cupling its not needed for this test, sam and ew site could just be instantiated by ther own.\nfunction TestSkynetIADSAbstractRadarElement:testSAMSiteAndEWRadarLoosesConnectionAndPowerSourceThenAddANewOneAgain()\n\tself:tearDown()\n\tself.testIADS = SkynetIADS:create()\n\tlocal connectionNode = StaticObject.getByName('SA-6 Connection Node-autonomous-test')\n\tlocal nonAutonomousSAM = self.testIADS:addSAMSite('SAM-SA-6'):addConnectionNode(connectionNode)\n\tself.testIADS:addEarlyWarningRadar('EW-west2')\n\t\n\tself.testIADS:buildRadarCoverage()\n\t\n\tlu.assertEquals(nonAutonomousSAM:getAutonomousState(), false)\n\ttrigger.action.explosion(connectionNode:getPosition().p, 500)\n\t--we simulate a call to the event, since in game will be triggered to late to for later checks in this unit test\n\tnonAutonomousSAM:onEvent(createDeadEvent())\n\tlu.assertEquals(nonAutonomousSAM:getAutonomousState(), true)\n\t\n\tlocal connectionNodeReAdd = StaticObject.getByName('SA-6 Connection Node-autonomous-readd')\n\tnonAutonomousSAM:addConnectionNode(connectionNodeReAdd)\n\tlu.assertEquals(nonAutonomousSAM:getAutonomousState(), false)\n\t\n\tlocal ewRadar = self.testIADS:getEarlyWarningRadarByUnitName('EW-west2')\n\tewRadar:addConnectionNode(StaticObject.getByName('ew-west-connection-node-test'))\n\t\n\ttrigger.action.explosion(StaticObject.getByName('ew-west-connection-node-test'):getPosition().p, 500)\n\t--we simulate a call to the event, since in game will be triggered to late to for later checks in this unit test\n\tewRadar:onEvent(createDeadEvent())\n\tlu.assertEquals(ewRadar:hasActiveConnectionNode(), false)\n\tlu.assertEquals(ewRadar:getAutonomousState(), true)\n\tlu.assertEquals(nonAutonomousSAM:getAutonomousState(), true)\n\t\n\tewRadar:addConnectionNode(Unit.getByName('connection-node-ew'))\n\tlu.assertEquals(nonAutonomousSAM:getAutonomousState(), false)\n\t\n\tewRadar:addPowerSource(StaticObject.getByName('ew-power-source'))\n\ttrigger.action.explosion(StaticObject.getByName('ew-power-source'):getPosition().p, 500)\n\t--we simulate a call to the event, since in game will be triggered to late to for later checks in this unit test\n\tewRadar:onEvent(createDeadEvent())\n\tlu.assertEquals(ewRadar:hasWorkingPowerSource(), false)\n\tlu.assertEquals(nonAutonomousSAM:getAutonomousState(), true)\n\t\n\tewRadar:addPowerSource(StaticObject.getByName('ew-power-source-2'))\n\tlu.assertEquals(ewRadar:isActive(), true)\n\tlu.assertEquals(nonAutonomousSAM:getAutonomousState(), false)\n\t\n\t--test if a SAM site will stay active if it's in EW mode and it's parent EW radar becomes inoperable as long as SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK is set this will work\n\tnonAutonomousSAM:setActAsEW(true)\n\ttrigger.action.explosion(StaticObject.getByName('ew-power-source-2'):getPosition().p, 500)\n\t--we simulate a call to the event, since in game will be triggered to late to for later checks in this unit test\n\tewRadar:onEvent(createDeadEvent())\n\tlu.assertEquals(ewRadar:isActive(), false)\n\tlu.assertEquals(ewRadar:hasWorkingPowerSource(), false)\n\tlu.assertEquals(nonAutonomousSAM:isActive(), true)\n\tlu.assertEquals(nonAutonomousSAM:getAutonomousState(), true)\n\n\t--test if command center is destroyed all SAM sites and EW radars should go autonomous:\n\tewRadar.powerSources = {}\n\tnonAutonomousSAM.powerSources = {}\n\tnonAutonomousSAM:setActAsEW(false)\n\tewRadar:setToCorrectAutonomousState()\n\tewRadar:informChildrenOfStateChange()\n\t\n\tlu.assertEquals(ewRadar:isActive(), true)\n\tlu.assertEquals(nonAutonomousSAM:isActive(), false)\n\tlu.assertEquals(nonAutonomousSAM:getAutonomousState(), false)\n\t\n\tlocal commandCenter = StaticObject.getByName('command-center-unit-test')\n\tlocal comCenter = self.testIADS:addCommandCenter(commandCenter)\n\t\n\tself.testIADS:buildRadarCoverage()\n\tlu.assertEquals(#comCenter:getChildRadars(), 2)\n\ttrigger.action.explosion(commandCenter:getPosition().p, 5000)\n\t--we simulate a call to the event, since in game will be triggered to late to for later checks in this unit test\n\tcomCenter:onEvent(createDeadEvent())\n\t\n\tlu.assertEquals(self.testIADS:isCommandCenterUsable(), false)\n\tlu.assertEquals(ewRadar:getAutonomousState(), true)\n\tlu.assertEquals(nonAutonomousSAM:getAutonomousState(), true)\n\t\n\tself.testIADS:deactivate()\nend\n\n\n--TODO: add tests for more check true / false combiations connectionnode power source etc.\nfunction TestSkynetIADSAbstractRadarElement:testSetToCorrectAutonomousState()\n\tself.samSiteName = \"SAM-SA-6-2\"\n\tself:setUp()\n\t\n\tself.samSite:goAutonomous()\n\tlu.assertEquals(self.samSite:getAutonomousState(), true)\n\t\n\tfunction self.samSite:hasActiveConnectionNode()\n\t\treturn true\n\tend\n\t\n\tlocal parentRad = {}\n\tfunction parentRad:hasWorkingPowerSource()\n\t\treturn true\n\tend\n\tfunction parentRad:hasActiveConnectionNode()\n\t\treturn true\n\tend\n\tfunction parentRad:getActAsEW()\n\t\treturn true\n\tend\n\tfunction parentRad:isDestroyed()\n\t\treturn false\n\tend\n\t\n\tself.samSite:addParentRadar(parentRad)\n\tself.samSite:setToCorrectAutonomousState()\n\tlu.assertEquals(self.samSite:getAutonomousState(), false) \n\t\n\t--check when SAM site does not have active connection node\n\tself.samSite:goAutonomous()\n\tlu.assertEquals(self.samSite:getAutonomousState(), true)\n\t\n\tfunction self.samSite:hasActiveConnectionNode()\n\t\treturn false\n\tend\n\t\n\tlocal parentRad = {}\n\tfunction parentRad:hasWorkingPowerSource()\n\t\treturn true\n\tend\n\tfunction parentRad:hasActiveConnectionNode()\n\t\treturn true\n\tend\n\tfunction parentRad:getActAsEW()\n\t\treturn true\n\tend\n\tfunction parentRad:isDestroyed()\n\t\treturn false\n\tend\n\t\n\tself.samSite:addParentRadar(parentRad)\n\tself.samSite:setToCorrectAutonomousState()\n\tlu.assertEquals(self.samSite:getAutonomousState(), true) \n\t\n\t\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testWillGoLiveWhenAutonomousAndHARMDefenceFinished()\n\tself.samSiteName = \"SAM-SA-6-2\"\n\tself:setUp()\n\tself.samSite:setAutonomousBehaviour(SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DCS_AI)\n\tself.samSite:goSilentToEvadeHARM(1)\n\tself.samSite:finishHarmDefence()\n\tlu.assertEquals(self.samSite:isActive(), true)\nend\n\n-- TODO: write test for updateMissilesInFlight in AbstractRadarElement\nfunction TestSkynetIADSAbstractRadarElement:testUpdateMissilesInFlight()\n\tself.samSiteName = \"SAM-SA-6-2\"\n\tself:setUp()\n\t\n\tlocal mockMissile1 = {}\n\tfunction mockMissile1:isExist()\n\t\treturn false\n\tend\n\t\n\tlocal mockMissile2 = {}\n\tfunction mockMissile2:isExist()\n\t\treturn true\n\tend\n\t\n\tself.samSite.missilesInFlight = {mockMissile1, mockMissile2}\n\tlu.assertEquals(#self.samSite.missilesInFlight, 2)\n\tlu.assertEquals(self.samSite:hasMissilesInFlight(), true)\n\t\n\tself.samSite:updateMissilesInFlight()\n\tlu.assertEquals(#self.samSite.missilesInFlight, 1)\n\tlu.assertEquals(self.samSite:hasMissilesInFlight(), true)\n\n\tself.samSite.missilesInFlight = {mockMissile1}\n\tlu.assertEquals(#self.samSite.missilesInFlight, 1)\n\tlu.assertEquals(self.samSite:hasMissilesInFlight(), true)\n\t\n\tself.samSite:updateMissilesInFlight()\n\tlu.assertEquals(#self.samSite.missilesInFlight, 0)\n\tlu.assertEquals(self.samSite:hasMissilesInFlight(), false)\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testShutDownShilkaWhenOutOfAmmo()\n\tlocal launcherData =\n\t{\n\t\t{\n\t\t\tcount=503,\n\t\t\tdesc={\n\t\t\t\t_origin=\"\",\n\t\t\t\tbox={\n\t\t\t\t\tmax={x=2.2344591617584, y=0.12504191696644, z=0.12113922089338},\n\t\t\t\t\tmin={x=-6.61008644104, y=-0.12504199147224, z=-0.12113920599222}\n\t\t\t\t},\n\t\t\t\tcategory=0,\n\t\t\t\tdisplayName=\"23mm AP\",\n\t\t\t\tlife=2,\n\t\t\t\ttypeName=\"weapons.shells.2A7_23_AP\",\n\t\t\t\twarhead={caliber=23, explosiveMass=0, mass=0.189, type=0}\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\tcount=1501,\n\t\t\tdesc={\n\t\t\t\t_origin=\"\",\n\t\t\t\tbox={\n\t\t\t\t\tmax={x=2.2344591617584, y=0.12504191696644, z=0.12113922089338},\n\t\t\t\t\tmin={x=-6.61008644104, y=-0.12504199147224, z=-0.12113920599222}\n\t\t\t\t},\n\t\t\t\tcategory=0,\n\t\t\t\tdisplayName=\"23mm HE\",\n\t\t\t\tlife=2,\n\t\t\t\ttypeName=\"weapons.shells.2A7_23_HE\",\n\t\t\t\twarhead={caliber=23, explosiveMass=0.189, mass=0.189, type=1}\n\t\t\t}\n\t\t}\n\t}\n\n\tself.samSiteName = \"SAM-Shilka\"\n\tself:setUp()\n\n\tlocal launcher = self.samSite:getLaunchers()[1]\n\n\tlocal mockDCSObjcect = {}\n\tfunction mockDCSObjcect:getAmmo()\n\t\tlauncherData[1].count = 300\n\t\tlauncherData[2].count = 200\n\t\treturn launcherData\n\tend\n\t---simulate firing of 1 missile\n\tfunction launcher:getDCSRepresentation()\n\t\treturn mockDCSObjcect\n\tend\n\n\t\n\tlu.assertEquals(launcher:getInitialNumberOfShells(), 2004)\n\tlu.assertEquals(launcher:getRemainingNumberOfShells(), 500)\n\tlu.assertEquals(self.samSite:getInitialNumberOfShells(), 2004)\n\tlu.assertEquals(self.samSite:getRemainingNumberOfShells(), 500)\n\tlu.assertEquals(self.samSite:hasRemainingAmmo(), true)\n\t\n\tlu.assertEquals(self.samSite:isActive(), true)\n\tself.samSite:goDarkIfOutOfAmmo()\n\tlu.assertEquals(self.samSite:isActive(), true)\n\t\n\tlocal mockDCSObjcect = {}\n\tfunction mockDCSObjcect:getAmmo()\n\t\tlauncherData[1].count = 0\n\t\tlauncherData[2].count = 0\n\t\treturn launcherData\n\tend\n\t---simulate firing of 1 missile\n\tfunction launcher:getDCSRepresentation()\n\t\treturn mockDCSObjcect\n\tend\n\t\n\tlu.assertEquals(launcher:getInitialNumberOfShells(), 2004)\n\tlu.assertEquals(launcher:getRemainingNumberOfShells(), 0)\n\tlu.assertEquals(self.samSite:getInitialNumberOfShells(), 2004)\n\tlu.assertEquals(self.samSite:getRemainingNumberOfShells(), 0)\n\tlu.assertEquals(self.samSite:hasRemainingAmmo(), false)\n\t\n\tlu.assertEquals(self.samSite:isActive(), true)\n\tself.samSite:goDarkIfOutOfAmmo()\n\tlu.assertEquals(self.samSite:isActive(), false)\n\t\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testCreateSamSiteFromInvalidGroup()\n\tself.samSiteName = \"Invalid-for-sam\"\n\tself:setUp()\n\tlu.assertStrMatches(self.samSite:getNatoName(), \"UNKNOWN\")\n\tlu.assertEquals(#self.samSite:getRadars(), 0)\n\tlu.assertEquals(#self.samSite:getLaunchers(), 0)\n\tlu.assertEquals(#self.samSite:getSearchRadars(), 0)\n\tlu.assertEquals(#self.samSite:getTrackingRadars(), 0)\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testSamSiteGroupContainingOfOneUnitOnlySA8()\n\tself.samSiteName = \"SAM-SA-8\"\n\tself:setUp()\n\tlu.assertEquals(#self.samSite:getRadars(), 1)\n\tlu.assertEquals(#self.samSite:getLaunchers(), 1)\n\tlu.assertEquals(#self.samSite:getSearchRadars(), 1)\n\tlu.assertEquals(#self.samSite:getTrackingRadars(), 0)\n\tlu.assertEquals(self.samSite:getNatoName(), \"SA-8\")\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testHARMDefenceStates()\n\tself.samSiteName = \"SAM-SA-6\"\n\tself:setUp()\n\tlu.assertEquals(self.samSite:isActive(), true)\n\tlu.assertEquals(self.samSite:isScanningForHARMs(), true)\n\tself.samSite:goSilentToEvadeHARM()\n\tlu.assertEquals(self.samSite:isScanningForHARMs(), false)\n\tlu.assertEquals(self.samSite:isActive(), false)\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testGoLiveFailsWhenInHARMDefenceMode()\n\tself.samSiteName = \"SAM-SA-6\"\n\tself:setUp()\n\tlu.assertEquals(self.samSite:isActive(), true)\n\tlu.assertEquals(self.samSite:isScanningForHARMs(), true)\n\tself.samSite:goSilentToEvadeHARM()\n\tlu.assertEquals(self.samSite:isActive(), false)\n\tself.samSite:goLive()\n\tlu.assertEquals(self.samSite:isActive(), false)\nend\n\n\nfunction TestSkynetIADSAbstractRadarElement:testHARMTimeToImpactCalculation()\n\tself.samSiteName = \"SAM-SA-6\"\n\tself:setUp()\n\tlu.assertEquals(self.samSite:getSecondsToImpact(100, 10), 36000)\n\tlu.assertEquals(self.samSite:getSecondsToImpact(10, 400), 90)\n\tlu.assertEquals(self.samSite:getSecondsToImpact(0, 400), 0)\n\tlu.assertEquals(self.samSite:getSecondsToImpact(400, 0), 0)\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testSlantRangeCalculationForHARMDefence()\n\tself.samSiteName = \"SAM-SA-6-2\"\n\tself:setUp()\n\tlocal iadsContact = IADSContactFactory(\"test-distance-calculation\")\n\tlocal radarUnit = self.samSite:getRadars()[1]\n\tlocal distanceSlantRange = self.samSite:getDistanceInMetersToContact(iadsContact, radarUnit:getPosition().p)\n\tlocal straightLine = mist.utils.round(mist.utils.get2DDist(radarUnit:getPosition().p, iadsContact:getPosition().p), 0)\n\tlu.assertEquals(distanceSlantRange > straightLine, true)\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testFinishHARMDefence()\n\tself.samSiteName = \"SAM-SA-6-2\"\n\tself:setUp()\n\tself.samSite:goSilentToEvadeHARM(10)\n\tlu.assertEquals(self.samSite:isActive(), false)\n\tself.samSite:finishHarmDefence()\n\tlu.assertEquals(self.samSite.harmShutdownTime, 0)\n\tself.samSite:goLive()\n\tlu.assertEquals(self.samSite:isActive(), true)\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testShutDownWhenOutOfMissiles()\n\tself.samSiteName = \"SAM-SA-6-2\"\n\tself:setUp()\n\t\n\tlocal launcher = self.samSite:getLaunchers()[1]\n\tlu.assertEquals(launcher:getInitialNumberOfMissiles(), 3)\n\tlu.assertEquals(launcher:getRemainingNumberOfMissiles(), 3)\n\tlu.assertEquals(self.samSite:getInitialNumberOfMissiles(), 3)\n\tlu.assertEquals(self.samSite:getRemainingNumberOfMissiles(), 3)\n\t\n\tlocal launcherData =\n\t\t{\n\t\t\t{\n\t\t\t\tcount=3,\n\t\t\t\tdesc={\n\t\t\t\t\tNmax=16,\n\t\t\t\t\tRCS=0.1059999987483,\n\t\t\t\t\t_origin=\"\",\n\t\t\t\t\taltMax=8000,\n\t\t\t\t\taltMin=30,\n\t\t\t\t\tbox={\n\t\t\t\t\t\tmax={x=2.9061908721924, y=0.43574807047844, z=0.4395649433136},\n\t\t\t\t\t\tmin={x=-2.9048342704773, y=-0.43574807047844, z=-0.4395649433136},\n\t\t\t\t\t},\n\t\t\t\t\tcategory=1,\n\t\t\t\t\tdisplayName=\"3M9M Kub (SA-6 Gainful)\",\n\t\t\t\t\tfuseDist=12,\n\t\t\t\t\tguidance=4,\n\t\t\t\t\tlife=2,\n\t\t\t\t\tmissileCategory=2,\n\t\t\t\t\trangeMaxAltMax=25000,\n\t\t\t\t\trangeMaxAltMin=25000,\n\t\t\t\t\trangeMin=4000,\n\t\t\t\t\ttypeName=\"SA3M9M\",\n\t\t\t\t\twarhead={caliber=330, explosiveMass=59, mass=59, type=1},\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\tlocal mockDCSObjcect = {}\n\tfunction mockDCSObjcect:getAmmo()\n\t\tlauncherData[1].count = 2\n\t\treturn launcherData\n\tend\n\t---simulate firing of 1 missile\n\tfunction launcher:getDCSRepresentation()\n\t\treturn mockDCSObjcect\n\tend\n\n\tlu.assertEquals(launcher:getInitialNumberOfMissiles(), 3)\n\tlu.assertEquals(launcher:getRemainingNumberOfMissiles(), 2)\n\tlu.assertEquals(self.samSite:getInitialNumberOfMissiles(), 3)\n\tlu.assertEquals(self.samSite:getRemainingNumberOfMissiles(), 2)\n\tlu.assertEquals(self.samSite:hasRemainingAmmo(), true)\n\t\n\tself.samSite:goDarkIfOutOfAmmo()\n\tlu.assertEquals(self.samSite:isActive(), true )\n\t\n\tfunction mockDCSObjcect:getAmmo()\n\t\tlauncherData[1].count = 1\n\t\treturn launcherData\n\tend\n\t\n\tlu.assertEquals(launcher:getInitialNumberOfMissiles(), 3)\n\tlu.assertEquals(launcher:getRemainingNumberOfMissiles(), 1)\n\tlu.assertEquals(self.samSite:getInitialNumberOfMissiles(), 3)\n\tlu.assertEquals(self.samSite:getRemainingNumberOfMissiles(), 1)\n\tlu.assertEquals(self.samSite:hasRemainingAmmo(), true)\n\t\n\tself.samSite:goDarkIfOutOfAmmo()\n\tlu.assertEquals(self.samSite:isActive(), true )\n\t\n\t--DCS missile info is nil when no ammo is remaining\n\tfunction mockDCSObjcect:getAmmo()\n\t\treturn nil\n\tend\n\tlu.assertEquals(launcher:getInitialNumberOfMissiles(), 3)\n\tlu.assertEquals(launcher:getRemainingNumberOfMissiles(), 0)\n\tlu.assertEquals(self.samSite:getInitialNumberOfMissiles(), 3)\n\tlu.assertEquals(self.samSite:getRemainingNumberOfMissiles(), 0)\n\tlu.assertEquals(self.samSite:hasRemainingAmmo(), false)\n\t\n\tself.samSite:goDarkIfOutOfAmmo()\n\tlu.assertEquals(self.samSite:isActive(), false )\n\tself.samSite:goLive()\n\tlu.assertEquals(self.samSite:isActive(), false )\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testActAsEarlyWarningRadar()\n\tself.samSiteName = \"SAM-SA-6\"\n\tself:setUp()\n\tself.samSite:goDark()\n\tlu.assertEquals(self.samSite:isActive(), false)\n\tself.samSite:setActAsEW(true)\n\tlu.assertEquals(self.samSite:isActive(), true)\n\tself.samSite:targetCycleUpdateEnd()\n\t\n\t-- SAM Site should not shut down when out of ammo and in EW Mode\n\tfunction self.samSite:getRemainingNumberOfMissiles()\n\t\treturn 0\n\tend\n\t\n\tself.samSite:goDarkIfOutOfAmmo()\n\tlu.assertEquals(self.samSite:isActive(), true)\n\t\n\tlu.assertEquals(self.samSite:isActive(), true)\n\t\n\t-- test when stopping EW mode the child SAM site should go dark\n\tlocal samSA62 = SkynetIADSSamSite:create(Group.getByName('SAM-SA-6-2'), self.skynetIADS)\n\tsamSA62:setAutonomousBehaviour(SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK)\n\tsamSA62:setupElements()\n\tsamSA62:goLive()\n\tself.samSite:addChildRadar(samSA62)\n\tsamSA62:addParentRadar(self.samSite)\n\tself.samSite:informChildrenOfStateChange()\n\tlu.assertEquals(samSA62:getAutonomousState(), false)\n\t\n\tself.samSite:setActAsEW(false)\n\tlu.assertEquals(self.samSite:isActive(), false)\n\tlu.assertEquals(samSA62:getAutonomousState(), true)\n\tlu.assertEquals(samSA62:isActive(), false)\n\tsamSA62:cleanUp()\n\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testInformOfContactInRangeWhenEarlyWaringRadar()\n\tself.samSiteName = \"SAM-SA-6\"\n\tself:setUp()\n\tself.samSite:setActAsEW(true)\n\tlocal mockContact = {}\n\t\n\tfunction self.samSite:isTargetInRange(target)\n\t\tlu.assertIs(target, mockContact)\n\t\treturn false\n\tend\n\tself.samSite:targetCycleUpdateStart()\n\tlu.assertEquals(self.samSite:isActive(), true)\n\tself.samSite:informOfContact(mockContact)\n\tlu.assertEquals(self.samSite:isActive(), true)\n\tself.samSite:targetCycleUpdateEnd()\n\tlu.assertEquals(self.samSite:isActive(), true)\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testSA2InformOfContactTargetInRangeMethod()\n\tself.samSiteName = \"SAM-SA-2\"\n\tself:setUp()\n\t--DCS AI radar instantly detects contact in test, so Site will not go dark, therefore we overwrite the method in this test\n\tfunction self.samSite:getDetectedTargets()\n\t\treturn {}\n\tend\n\tself.samSite:goDark()\n\tlu.assertEquals(self.samSite:isActive(), false)\n\tlocal target = IADSContactFactory('test-in-firing-range-of-sa-2')\n\t\n\tself.samSite:informOfContact(target)\n\tlocal searchRadar = self.samSite:getSearchRadars()[1]\n\tlu.assertEquals(searchRadar:getTypeName(), 'p-19 s-125 sr')\n\tlocal sensors = Unit.getByName('Unit #005'):getSensors()\n\tlu.assertEquals(searchRadar:getMaxRangeFindingTarget(), 53499.2265625)\n\n\tlocal launcher = self.samSite:getLaunchers()[1]\n\tlu.assertEquals(launcher:getRange(), 40000)\n\t\n\tlocal trackingRadar = self.samSite:getTrackingRadars()[1]\n\t--in its current implementation the SA-2 tracking radar returns the values of the search radar, I presume its only a placeholder in DCS\n\tlu.assertEquals(trackingRadar:getMaxRangeFindingTarget(), 53499.2265625)\t\n\t\t\n\tlu.assertEquals(self.samSite:isActive(), true)\n\tlu.assertEquals(self.samSite:isTargetInRange(target), true)\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testSA2WillNotGoDarkIfTargetIsInRange()\n\tself.samSiteName = \"SAM-SA-2\"\n\tself:setUp()\n\n\tlocal target = IADSContactFactory('test-in-firing-range-of-sa-2')\n\t--we return a detected target, to pervent SAM site going dark\n\tfunction self.samSite:getDetectedTargets()\n\t\tlocal targets = {}\n\t\ttable.insert(targets, target)\n\t\treturn targets\n\tend\n\n\tself.samSite:informOfContact(target)\n\tself.samSite:goDark()\n\tlu.assertEquals(self.samSite:isActive(), true)\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testSA2WillNotGoDarkIfOutOfMisslesAndMissilesAreStillInFlight()\n\tself.samSiteName = \"SAM-SA-2\"\n\tself:setUp()\n\tlu.assertEquals(self.samSite:hasMissilesInFlight(), false)\n\t\n\tlocal mockMissileInFlight = {}\n\tfunction mockMissileInFlight:isExist()\n\t\treturn true\n\tend\n\tlocal missiles = {}\n\ttable.insert(missiles, mockMissileInFlight)\n\tself.samSite.missilesInFlight = missiles\n\tlu.assertEquals(self.samSite:hasMissilesInFlight(), true)\n\tlu.assertEquals(#self.samSite:getDetectedTargets(), 0)\n\tlu.assertEquals(self.samSite:isActive(), true)\n\tself.samSite:goDark()\n\tlu.assertEquals(self.samSite:isActive(), true)\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testSA2WillGoDarkWithTargetsInRangeAndHARMDetected()\n\tself.samSiteName = \"SAM-SA-2\"\n\tself:setUp()\n\t\n\tlocal target = IADSContactFactory('test-in-firing-range-of-sa-2')\n\tfunction self.samSite:getDetectedTargets()\n\t\tlocal targets = {}\n\t\ttable.insert(targets, target)\n\t\treturn targets\n\tend\n\t\n\tself.samSite:informOfContact(target)\n\tself.samSite:goSilentToEvadeHARM(5)\n\n\tlu.assertEquals(self.samSite:isActive(), false)\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testSA2WillgoDarkIfOutOfAmmoNoMissilesAreInFlightAndTargetStillInRange()\n\tself.samSiteName = \"SAM-SA-2\"\n\tself:setUp()\n\t\n\tlocal target = IADSContactFactory('test-in-firing-range-of-sa-2')\n\tfunction self.samSite:getDetectedTargets()\n\t\tlocal targets = {}\n\t\ttable.insert(targets, target)\n\t\treturn targets\n\tend\n\t\n\tfunction self.samSite:getRemainingNumberOfMissiles()\n\t\treturn 0\n\tend\n\t\n\tlocal mockMissileInFlight = {}\n\tfunction mockMissileInFlight:isExist()\n\t\treturn false\n\tend\n\tlocal missiles = {}\n\tlu.assertEquals(self.samSite:hasMissilesInFlight(), false)\n\tlu.assertEquals(self.samSite:getRemainingNumberOfMissiles(), 0)\n\tlu.assertEquals(self.samSite:isActive(), true)\n\tself.samSite:goDark()\n\tlu.assertEquals(self.samSite:isActive(), false)\nend\n\n--TODO: write test case: SAM is out of missiles, is currently dark, is informed of a target in range has not detected it with its own radar is not in harm defence mode\nfunction TestSkynetIADSAbstractRadarElement:testSA2OutOfMissilesNoMissilesInFlightIsInformedOfTargetByIADSHasNotDetectedTargetWithOwnRadar()\n\tself.samSiteName = \"SAM-SA-2\"\n\tself:setUp()\n\t\n\tfunction self.samSite:getDetectedTargets()\n\t\treturn {}\n\tend\n\t\n\tfunction self.samSite:getRemainingNumberOfMissiles()\n\t\treturn 0\n\tend\n\t\n\t\n\tself.samSite:goDark()\n\tlu.assertEquals(self.samSite:hasMissilesInFlight(), false)\n\tlu.assertEquals(self.samSite:isActive(), false)\n\t\n\tlocal target = IADSContactFactory('test-in-firing-range-of-sa-2')\n\tself.samSite:informOfContact(target)\n\tlu.assertEquals(self.samSite:isActive(), false)\n\t\nend\n\n--[[\nThis test is no longer required with setEmission available in dcs 2.7\nfunction TestSkynetIADSAbstractRadarElement:testControllerNotDisabledWhenGoingDarkAndOutOfAmmo()\n\tself.samSiteName = \"test-SAM-SA-2-test\"\n\tself:setUp()\n\t\n\tlocal stateCalled = false\n\tlocal mockController = {}\n\tfunction mockController:setOnOff(state)\n\t\tlu.assertEquals(state, false)\n\t\tstateCalled = true\n\tend\n\t\n\tlocal optionCalled = false\n\tfunction mockController:setOption(opt, val)\n\t\toptionCalled = true\n\tend\n\t\n\tfunction self.samSite:getController()\n\t\treturn mockController\n\tend\n\t\n\tfunction self.samSite:hasRemainingAmmo()\n\t\treturn false\n\tend\n\t\n\tself.samSite:goDark()\n\tlu.assertEquals(stateCalled, false)\n\tlu.assertEquals(optionCalled, true)\n\t\nend\n--]]\n\n--[[\nThis test is no longer required with setEmission available in dcs 2.7\nfunction TestSkynetIADSAbstractRadarElement:testControllerDisabledWhenGoingDarkAndHasRemainingAmmo()\n\tself.samSiteName = \"test-SAM-SA-2-test\"\n\tself:setUp()\n\t\n\tlocal stateCalled = false\n\tlocal mockController = {}\n\tfunction mockController:setOnOff(state)\n\t\tlu.assertEquals(state, false)\n\t\tstateCalled = true\n\tend\n\t\n\tlocal optionCalled = false\n\tfunction mockController:setOption(opt, val)\n\t\toptionCalled = true\n\tend\n\t\n\tfunction self.samSite:getController()\n\t\treturn mockController\n\tend\n\t\n\tfunction self.samSite:hasRemainingAmmo()\n\t\treturn true\n\tend\n\n\tself.samSite:goDark()\n\tlu.assertEquals(stateCalled, true)\n\tlu.assertEquals(optionCalled, false)\nend\n--]]\nfunction TestSkynetIADSAbstractRadarElement:testSA2GoLiveRangeInPercentInKillZone()\n\tself.samSiteName = \"SAM-SA-2\"\n\tself:setUp()\n\tlu.assertIs(self.samSite:getEngagementZone(), SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE)\n\tself.samSite:setGoLiveRangeInPercent(60)\n\t\n\tlocal target = IADSContactFactory('test-in-firing-range-of-sa-2')\n\t\n\tlocal launcher = self.samSite:getLaunchers()[1]\n\tlu.assertEquals(launcher:isInRange(target), false)\n\tlu.assertEquals(self.samSite:isTargetInRange(target), false)\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testSA2GoLiveRangeInPercentSearchRange()\n\tself.samSiteName = \"test-SAM-SA-2-test-2\"\n\tself:setUp()\n\tself.samSite:setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE)\n\tself.samSite:setGoLiveRangeInPercent(80)\n\t\n\tlocal target = IADSContactFactory('test-outer-search-range')\n\n\tlocal radars = self.samSite:getSearchRadars()\n\tfor i = 1, #radars do\n\t\tlocal radar = radars[i]\n\t\tlu.assertEquals(radar:isInRange(target), false)\n\tend\n\tlu.assertEquals(self.samSite:isTargetInRange(target), false)\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testSA8GoLiveRangeInPercent()\t\n\tself.samSiteName = 'SAM-SA-8'\n\tself:setUp()\n\tself.samSite:goDark()\n\tlocal target = IADSContactFactory('test-sa-8-will-go-active')\n\tself.samSite:informOfContact(target)\n\tlu.assertEquals(self.samSite:isActive(), true)\n\tlocal launcher = self.samSite:getLaunchers()[1]\n\tself.samSite:setGoLiveRangeInPercent(20)\n\tself.samSite:goDark()\n\tself.samSite:informOfContact(target)\n\tlu.assertEquals(launcher:isInRange(target), false)\n\tlu.assertEquals(self.samSite:isActive(), false)\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testShutDownTimes()\n\tself.samSiteName = \"SAM-SA-6\"\n\tself:setUp()\n\tlu.assertEquals(self.samSite:calculateMinimalShutdownTimeInSeconds(30), 60)\n\tlocal saveRandom = mist.random\n\tfunction mist.random(low, high)\n\t\treturn 10\n\tend\n\tlu.assertEquals(self.samSite:calculateMaximalShutdownTimeInSeconds(20), 30)\n\tmist.random = saveRandom\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testDaisychainSAMOptions()\n\tself.samSiteName = \"SAM-SA-11\"\n\tself:setUp()\n\tlocal powerSource = StaticObject.getByName('SA-11-power-source')\n\tlocal connectionNode = StaticObject.getByName('SA-11-connection-node')\n\tlocal returnValue = self.samSite:setActAsEW(true):addPowerSource(powerSource):addConnectionNode(connectionNode):setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE):setGoLiveRangeInPercent(90):setAutonomousBehaviour(SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK)\n\tlu.assertIs(self.samSite, returnValue)\n\tlu.assertEquals(self.samSite:getActAsEW(), true)\n\tlu.assertEquals(self.samSite:getEngagementZone(), SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE)\n\tlu.assertEquals(self.samSite:getGoLiveRangeInPercent(), 90)\n\tlu.assertEquals(self.samSite:getAutonomousBehaviour(), SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK)\n\tlu.assertIs(self.samSite:getConnectionNodes()[1], connectionNode)\n\tlu.assertIs(self.samSite:getPowerSources()[1], powerSource)\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testWillSAMShutDownWhenItLoosesPowerAndAMissileIsInFlight()\n\tself.samSiteName = \"SAM-SA-11\"\n\tself:setUp()\n\tlocal powerSource = StaticObject.getByName('SA-11-power-source')\n\tself.samSite:addPowerSource(powerSource)\n\tself.samSite:goLive()\n\t\n\tlu.assertEquals(self.samSite:hasWorkingPowerSource(), true)\n\tlu.assertEquals(self.samSite:isActive(), true)\n\t\n\t-- simulate that the SAM site has a missile in flight\n\tfunction self.samSite:hasMissilesInFlight()\n\t\treturn true\n\tend\n\n\t--trigger the explosion of the power source, this should shut down the SAM site\n\ttrigger.action.explosion(powerSource:getPosition().p, 100)\n\t--we simulate a call to the event, since in game will be triggered to late to for later checks in this unit test\n\tself.samSite:onEvent(createDeadEvent())\n\tlu.assertEquals(self.samSite:hasWorkingPowerSource(), false)\n\tlu.assertEquals(self.samSite:isActive(), false)\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testSetPointDefence()\n\tself.samSiteName = \"SAM-SA-10\"\n\tself:setUp()\n\tlocal pd = self.skynetIADS:addSAMSite(\"SAM-SA-15-1\")\n\tlu.assertEquals(pd:getIsAPointDefence(), false)\n\tself.samSite:addPointDefence(pd)\n\tlu.assertEquals(pd:getIsAPointDefence(), true)\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testPointDefencesGoLive()\n\n\tlocal stateSet = false\n\tlocal mockPD1 = {}\n\tfunction mockPD1:getActAsEW()\n\t\treturn false\n\tend\n\tfunction mockPD1:setIsAPointDefence(state)\n\t\n\tend\n\tfunction mockPD1:setActAsEW(state)\n\t\tstateSet = true\n\tend\n\t\n\tfunction mockPD1:cleanUp()\n\t\n\tend\n\t\n\tself.samSiteName = \"SAM-SA-10\"\n\tself:setUp()\n\tself.samSite:addPointDefence(mockPD1)\n\tlu.assertEquals(self.samSite:pointDefencesGoLive(), true)\n\tlu.assertEquals(stateSet, true)\n\t\n\t\n\tself:tearDown()\n\t\n\tlocal stateSet = false\n\tlocal mockPD1 = {}\n\tfunction mockPD1:getActAsEW()\n\t\treturn true\n\tend\n\tfunction mockPD1:setActAsEW(state)\n\t\tstateSet = true\n\tend\n\tfunction mockPD1:setIsAPointDefence(state)\n\t\n\tend\n\t\n\tfunction mockPD1:cleanUp()\n\t\n\tend\n\t\n\tself.samSiteName = \"SAM-SA-10\"\n\tself:setUp()\n\tself.samSite:addPointDefence(mockPD1)\n\tlu.assertEquals(self.samSite:pointDefencesGoLive(), false)\n\tlu.assertEquals(stateSet, false)\n\t\n\t\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testPointDefenceActiveWhenSAMGoesDarkDueToHARMDefence()\n\tself.samSiteName = \"SAM-SA-10\"\n\tself:setUp()\n\tself.samSite:setActAsEW(true)\n\t\n\t--in this group there are two SA-15 units:\n\tlocal sa15 = Group.getByName(\"SAM-SA-15-1\")\n\tlocal pointDefence = SkynetIADSSamSite:create(sa15, self.skynetIADS)\n\tpointDefence:setupElements()\n\tpointDefence:goLive()\n\tpointDefence:goDark()\n\tlu.assertEquals(self.samSite:addPointDefence(pointDefence), self.samSite)\n\tlu.assertEquals(#self.samSite:getPointDefences(), 1)\n\t\n\tself.samSite:goSilentToEvadeHARM()\n\tlu.assertEquals(self.samSite:isActive(), false)\n\tlu.assertEquals(pointDefence:isActive(), true)\n\t\n\tself.samSite:finishHarmDefence()\n\tself.samSite:goLive()\n\tlu.assertEquals(pointDefence:getActAsEW(), false)\n\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testCleanUpOldObjectsIdentifiedAsHARMS()\n\tself.samSiteName = \"SAM-SA-10\"\n\tself:setUp()\n\tlocal mockContact = {}\n\tfunction mockContact:getAge()\n\t\treturn 10\n\tend\n\ttable.insert(self.samSite.objectsIdentifiedAsHarms, mockContact)\n\tlu.assertEquals(self.samSite:getNumberOfObjectsItentifiedAsHARMS(), 1)\nend\n\n--TODO:write Unit test\nfunction TestSkynetIADSAbstractRadarElement:testPointDefenceWhenOnlyOneEWRadarIsActiveAndAmmoIsStillAvailable()\n\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testPointDefencesAreNotActivatedWhenNoHARMSRemoved()\n\tself.samSiteName = \"SAM-SA-10\"\n\tself:setUp()\t\n\tlocal sa15 = Group.getByName(\"SAM-SA-15-1\")\n\tlocal pointDefence = SkynetIADSSamSite:create(sa15, self.skynetIADS)\n\tself.samSite:addPointDefence(pointDefence)\n\tlocal calledStopPointDefence = false\n\tfunction self.samSite:pointDefencesStopActingAsEW()\n\t\tcalledStopPointDefence = true\n\tend\n\tself.samSite:evaluateIfTargetsContainHARMs()\n\tlu.assertEquals(calledStopPointDefence, false)\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testPointDefenceWillGoDarkWhenSAMItIsProtectingGoesDark()\n\tself.samSiteName = \"SAM-SA-10\"\n\tself:setUp()\t\n\tlocal sa15 = Group.getByName(\"SAM-SA-15-1\")\n\tlocal pointDefence = SkynetIADSSamSite:create(sa15, self.skynetIADS)\n\tself.samSite:addPointDefence(pointDefence)\n\tpointDefence:setActAsEW(true)\n\tself.samSite:goDark()\n\tlu.assertEquals(pointDefence:isActive(), false)\nend\n\n--[[\nfunction TestSkynetIADSAbstractRadarElement:testCallMethodOnTableElements()\n\tlocal test = {}\n\tfunction test:theMethod(value)\n\t\tenv.info(\"call there: \"..value)\n\t\treturn {}\n\tend\n\n\tfunction test:theOtherMethod(value)\n\t\tenv.info(\"call here: \"..value)\n\t\treturn {}\n\tend\n\t\n\ttest.__index = test\n\tsetmetatable(test, test)\n\n\tlocal testContainer = {}\n\tlocal handler = {}\n\t\n\thandler.__index = function(tbl, name)\n\t\ttbl[name] = function(self, ...)\n\t\t\t\tfor i = 1, #self do\n\t\t\t\t\tself[i][name](self[i], ...)\n\t\t\t\tend\n\t\t\t\treturn self\n\t\t\tend\n\t\treturn tbl[name]\n\tend\n\t\n\tsetmetatable(testContainer, handler)\n\t\n\tlocal tast = {}\n\tsetmetatable(tast, test)\n\ttast.__index = test\n\t\n\ttable.insert(testContainer, test)\n\ttable.insert(testContainer, tast)\n\t\n\ttast['theOtherMethod'](tast, '101')\n\t\n\tlu.assertIs(testContainer:theMethod(\"99\"), testContainer)\n\tlu.assertIs(testContainer:theOtherMethod(\"100\"), testContainer)\nend\n--]]\n\n--[[\nthis test ensures that targets are cached in the sam site, calls to the getDetectedTargets function of the controller are cpu intensive\nmultiple calls are made within miliseconds on the same SAM or EW site when updating the IADS status, therefore only the first call ist to the acutall controller\nafter that results are cached for a few seconds (default IADS setting is for one update cycle, e.g. 5 seconds).\n--]]\n\nfunction TestSkynetIADSAbstractRadarElement:testCacheDetectedTargets()\n\tself.skynetIADS:addSAMSitesByPrefix('SAM')\n\tself.samSite = self.skynetIADS:getSAMSiteByGroupName('SAM-SA-10')\n\tself.samSite:goDark()\n\tself.samSite:goLive()\n\t-- deactivate no cache after goLive\n\tself.samSite.noCacheActiveForSecondsAfterGoLive = 0\n\tlu.assertEquals(self.samSite:getDetectedTargets() == self.samSite:getDetectedTargets(), true)\n\tself.samSite.cachedTargetsMaxAge = -1\n\tlu.assertEquals(self.samSite:getDetectedTargets() == self.samSite:getDetectedTargets(), false)\n\t\nend\n\n--[[\nthe IADS turns controllers of a SAM or EW site on and off. This has the advantage that a SAM site will react faster  after beeing waken up by the IADS\nthe down side is that the first call to getDetectedTarget() on a controller after a goLive returns no targets, this result is cached causing the SAM site to misbehave in the IADS.\nTherefore for the first seconds after goLive the cache of getDetectedTargets is bypassed, ensuring targets are stored and the SAM site behaves correctly. \n--]]\nfunction TestSkynetIADSAbstractRadarElement:testCacheInvalidatedFirstfewSecondsAfterControllerIsActivated()\n\tself.skynetIADS:addSAMSitesByPrefix('SAM')\n\tself.samSite = self.skynetIADS:getSAMSiteByGroupName('SAM-SA-10')\n\tself.samSite:goDark()\n\tself.samSite:goLive()\n\tlu.assertEquals(self.samSite:getDetectedTargets() == self.samSite:getDetectedTargets(), false)\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testCalculateAspectInDegrees()\n\tself.samSite = self.skynetIADS:getSAMSiteByGroupName('SAM-SA-10')\n\tlu.assertEquals(self.samSite:calculateAspectInDegrees(0, 90), 90)\n\tlu.assertEquals(self.samSite:calculateAspectInDegrees(300, 90), 150)\n\tlu.assertEquals(self.samSite:calculateAspectInDegrees(010, 280), 90)\n\tlu.assertEquals(self.samSite:calculateAspectInDegrees(190, 350), 160)\n\tlu.assertEquals(self.samSite:calculateAspectInDegrees(090, 270), 180)\n\tlu.assertEquals(self.samSite:calculateAspectInDegrees(010, 170), 160)\nend\n\nfunction TestSkynetIADSAbstractRadarElement:testShallIgnoreHARMShutdown()\n\tself.samSiteName = \"SAM-SA-10\"\n\tself:setUp()\n\t\n\t--older sam site that can not engage HARMs (air weapons)\n\tfunction self.samSite:hasEnoughLaunchersToEngageMissiles(value)\n\t\treturn true\n\tend\n\t\n\tfunction self.samSite:hasRemainingAmmoToEngageMissiles(value)\n\t\treturn true\n\tend\n\t\n\tfunction self.samSite:getCanEngageHARM()\n\t\treturn false\n\tend\n\t\n\tfunction self.samSite:pointDefencesHaveRemainingAmmo(value)\n\t\treturn false\n\tend\n\t\n\tfunction self.samSite:pointDefencesHaveEnoughLaunchers(value)\n\t\treturn false\n\tend\n\n\tlu.assertEquals(self.samSite:shallIgnoreHARMShutdown(), false)\n\t\n\t\n\n\tfunction self.samSite:hasEnoughLaunchersToEngageMissiles(value)\n\t\treturn false\n\tend\n\t\n\tfunction self.samSite:hasRemainingAmmoToEngageMissiles(value)\n\t\treturn false\n\tend\n\t\n\tfunction self.samSite:getCanEngageHARM()\n\t\treturn true\n\tend\n\t\n\tfunction self.samSite:pointDefencesHaveRemainingAmmo(value)\n\t\treturn false\n\tend\n\t\n\tfunction self.samSite:pointDefencesHaveEnoughLaunchers(value)\n\t\treturn false\n\tend\n\n\tlu.assertEquals(self.samSite:shallIgnoreHARMShutdown(), false)\n\t\n\t\n\tfunction self.samSite:hasEnoughLaunchersToEngageMissiles(value)\n\t\treturn false\n\tend\n\t\n\tfunction self.samSite:hasRemainingAmmoToEngageMissiles(value)\n\t\treturn true\n\tend\n\t\n\tfunction self.samSite:getCanEngageHARM()\n\t\treturn true\n\tend\n\t\n\tfunction self.samSite:pointDefencesHaveRemainingAmmo(value)\n\t\treturn false\n\tend\n\t\n\tfunction self.samSite:pointDefencesHaveEnoughLaunchers(value)\n\t\treturn true\n\tend\n\n\tlu.assertEquals(self.samSite:shallIgnoreHARMShutdown(), false)\n\t\n\t\n\tfunction self.samSite:hasEnoughLaunchersToEngageMissiles(value)\n\t\treturn true\n\tend\n\t\n\tfunction self.samSite:hasRemainingAmmoToEngageMissiles(value)\n\t\treturn false\n\tend\n\t\n\tfunction self.samSite:getCanEngageHARM()\n\t\treturn true\n\tend\n\t\n\tfunction self.samSite:pointDefencesHaveRemainingAmmo(value)\n\t\treturn true\n\tend\n\t\n\tfunction self.samSite:pointDefencesHaveEnoughLaunchers(value)\n\t\treturn false\n\tend\n\n\tlu.assertEquals(self.samSite:shallIgnoreHARMShutdown(), false)\n\t\n\t\n\tfunction self.samSite:hasEnoughLaunchersToEngageMissiles(value)\n\t\treturn true\n\tend\n\t\n\tfunction self.samSite:hasRemainingAmmoToEngageMissiles(value)\n\t\treturn true\n\tend\n\t\n\tfunction self.samSite:getCanEngageHARM()\n\t\treturn true\n\tend\n\t\n\tfunction self.samSite:pointDefencesHaveRemainingAmmo(value)\n\t\treturn false\n\tend\n\t\n\tfunction self.samSite:pointDefencesHaveEnoughLaunchers(value)\n\t\treturn false\n\tend\n\n\tlu.assertEquals(self.samSite:shallIgnoreHARMShutdown(), true)\n\t\n\n\t\n\tfunction self.samSite:hasEnoughLaunchersToEngageMissiles(value)\n\t\treturn true\n\tend\n\t\n\tfunction self.samSite:hasRemainingAmmoToEngageMissiles(value)\n\t\treturn true\n\tend\n\t\n\tfunction self.samSite:getCanEngageHARM()\n\t\treturn true\n\tend\n\t\n\tfunction self.samSite:pointDefencesHaveRemainingAmmo(value)\n\t\treturn true\n\tend\n\t\n\tfunction self.samSite:pointDefencesHaveEnoughLaunchers(value)\n\t\treturn true\n\tend\n\n\tlu.assertEquals(self.samSite:shallIgnoreHARMShutdown(), true)\n\t\n\t\n\t\n\tfunction self.samSite:hasEnoughLaunchersToEngageMissiles(value)\n\t\treturn true\n\tend\n\t\n\tfunction self.samSite:hasRemainingAmmoToEngageMissiles(value)\n\t\treturn false\n\tend\n\t\n\tfunction self.samSite:getCanEngageHARM()\n\t\treturn true\n\tend\n\t\n\tfunction self.samSite:pointDefencesHaveRemainingAmmo(value)\n\t\treturn true\n\tend\n\t\n\tfunction self.samSite:pointDefencesHaveEnoughLaunchers(value)\n\t\treturn true\n\tend\n\n\tlu.assertEquals(self.samSite:shallIgnoreHARMShutdown(), true)\n\t\n\t\n\t\n\tfunction self.samSite:hasEnoughLaunchersToEngageMissiles(value)\n\t\treturn true\n\tend\n\t\n\tfunction self.samSite:hasRemainingAmmoToEngageMissiles(value)\n\t\treturn true\n\tend\n\t\n\tfunction self.samSite:getCanEngageHARM()\n\t\treturn true\n\tend\n\t\n\tfunction self.samSite:pointDefencesHaveRemainingAmmo(value)\n\t\treturn false\n\tend\n\t\n\tfunction self.samSite:pointDefencesHaveEnoughLaunchers(value)\n\t\treturn true\n\tend\n\n\tlu.assertEquals(self.samSite:shallIgnoreHARMShutdown(), true)\n\t\n\t\n\t\n\tfunction self.samSite:hasEnoughLaunchersToEngageMissiles(value)\n\t\treturn true\n\tend\n\t\n\tfunction self.samSite:hasRemainingAmmoToEngageMissiles(value)\n\t\treturn true\n\tend\n\t\n\tfunction self.samSite:getCanEngageHARM()\n\t\treturn true\n\tend\n\t\n\tfunction self.samSite:pointDefencesHaveRemainingAmmo(value)\n\t\treturn true\n\tend\n\t\n\tfunction self.samSite:pointDefencesHaveEnoughLaunchers(value)\n\t\treturn false\n\tend\n\n\tlu.assertEquals(self.samSite:shallIgnoreHARMShutdown(), true)\n\t\n\nend\n\nend\n"
  },
  {
    "path": "unit-tests/test-skynet-iads-blue-sam-sites-and-ew-radars.lua",
    "content": "do\nTestSkynetIADSBLUESAMSitesAndEWRadars = {}\n\nfunction TestSkynetIADSBLUESAMSitesAndEWRadars:setUp()\n\tif self.samSiteName then\n\t\tself.skynetIADS = SkynetIADS:create()\n\t\tlocal samSite = Group.getByName(self.samSiteName)\n\t\tself.samSite = SkynetIADSSamSite:create(samSite, self.skynetIADS)\n\t\t\n\t\t-- we overrite this method since it returns radar contacts in the DCS world which mess up the tests.\n\t\tfunction self.samSite:getDetectedTargets()\n\t\t\treturn {}\n\t\tend\n\t\t\n\t\tself.samSite:setupElements()\n\t\tself.samSite:goLive()\n\tend\n\t\n\tif self.ewRadarName then\n\t\tself.iads = SkynetIADS:create()\n\t\tself.iads:addEarlyWarningRadarsByPrefix('BLUE-EW')\n\t\tself.ewRadar = self.iads:getEarlyWarningRadarByUnitName(self.ewRadarName)\n\tend\n\t\nend\n\nfunction TestSkynetIADSBLUESAMSitesAndEWRadars:tearDown()\n\tif self.samSite then\t\n\t\tself.samSite:goDark()\n\t\tself.samSite:cleanUp()\n\tend\n\t\n\tif self.ewRadar then\n\t\tself.ewRadar:cleanUp()\n\tend\n\tif self.iads then\n\t\tself.iads:deactivate()\n\tend\n\tself.iads = nil\n\tself.ewRadar = nil\n\tself.ewRadarName = nil\n\t\n\tself.samSite = nil\n\tself.samSiteName = nil\nend\n\nfunction TestSkynetIADSBLUESAMSitesAndEWRadars:testHawkSTR()\n\tself.ewRadarName = \"BLUE-EW-Hawk\"\n\tself:setUp()\n\tlu.assertEquals(self.ewRadar:getNatoName(), \"Hawk str\")\n\tlu.assertEquals(self.ewRadar:hasWorkingRadar(), true)\nend\n\nfunction TestSkynetIADSBLUESAMSitesAndEWRadars:testPatriotSTR()\n\tself.ewRadarName = \"BLUE-EW\"\n\tself:setUp()\n\tlu.assertEquals(self.ewRadar:getNatoName(), \"Patriot str\")\n\tlu.assertEquals(self.ewRadar:hasWorkingRadar(), true)\nend\n\nfunction TestSkynetIADSBLUESAMSitesAndEWRadars:testRolandEWR()\n\tself.ewRadarName = \"BLUE-EW-Roland\"\n\tself:setUp()\n\tlu.assertEquals(self.ewRadar:getNatoName(), \"Roland EWR\")\n\tlu.assertEquals(self.ewRadar:hasWorkingRadar(), true)\nend\n\nfunction TestSkynetIADSBLUESAMSitesAndEWRadars:testRolandLauncherAndRadar()\n--[[\nRoland:\n\nRadar:\n{\n    0={\n        {opticType=0, type=0, typeName=\"generic SAM search visir\"},\n        {opticType=2, type=0, typeName=\"generic SAM IR search visir\"}\n    },\n    {\n        {\n            detectionDistanceAir={\n                lowerHemisphere={headOn=8024.8837890625, tailOn=8024.8837890625},\n                upperHemisphere={headOn=8024.8837890625, tailOn=8024.8837890625}\n            },\n            type=1,\n            typeName=\"Roland ADS\"\n        }\n    }\n}\n\nLauncher:\n{\n    {\n        count=10,\n        desc={\n            Nmax=14,\n            RCS=0.019600000232458,\n            _origin=\"\",\n            altMax=6000,\n            altMin=10,\n            box={\n                max={x=1.2142661809921, y=0.17386008799076, z=0.1697566062212},\n                min={x=-1.212909579277, y=-0.1738600730896, z=-0.1697566062212}\n            },\n            category=1,\n            displayName=\"XMIM-115 Roland\",\n            fuseDist=5,\n            guidance=4,\n            life=2,\n            missileCategory=2,\n            rangeMaxAltMax=8000,\n            rangeMaxAltMin=8000,\n            rangeMin=500,\n            typeName=\"ROLAND_R\",\n            warhead={caliber=150, explosiveMass=6.5, mass=6.5, type=1}\n        }\n    }\n}\n\n--]]\n\tself.samSiteName = \"BLUE-SAM-ROLAND\"\n\tself:setUp()\n\tlu.assertEquals(self.samSite:getNatoName(), \"Roland ADS\")\t\n\tlu.assertEquals(#self.samSite:getLaunchers(), 1)\n\tlu.assertEquals(#self.samSite:getSearchRadars(), 1)\t\n\t\n\tlu.assertEquals(self.samSite:getSearchRadars()[1]:getMaxRangeFindingTarget(), 23405.912109375)\n\tlu.assertEquals(self.samSite:getLaunchers()[1]:getRange(), 8000)\n\tlu.assertEquals(self.samSite:getLaunchers()[1]:getInitialNumberOfMissiles(), 10)\nend\n\nfunction TestSkynetIADSBLUESAMSitesAndEWRadars:testNASAMS()\n\n\tself.samSiteName = \"BLUE-SAM-NASAMS\"\n\tself:setUp()\n\tlu.assertEquals(self.samSite:getNatoName(), \"NASAMS\")\n\tlu.assertEquals(self.samSite:getHARMDetectionChance(),90)\n\tlu.assertEquals(self.samSite:getCanEngageHARM(), true)\n\tlu.assertEquals(self.samSite:getRadars()[1]:getMaxRangeFindingTarget(), 26749.61328125)\n\tlu.assertEquals(self.samSite:getLaunchers()[1]:getRange(), 57000)\n\tlu.assertEquals(self.samSite:getLaunchers()[1]:getInitialNumberOfMissiles(), 6)\n\t\n\tlu.assertEquals(self.samSite:getLaunchers()[2]:getRange(), 61000)\n\tlu.assertEquals(self.samSite:getLaunchers()[2]:getInitialNumberOfMissiles(), 6)\n\t\nend\n\nfunction TestSkynetIADSBLUESAMSitesAndEWRadars:testRapierLauncherAndRadar()\n--[[\nRapier:\n\nRadar: (for some reason the typeName  is Tor?)\n{\n    {\n        {\n            detectionDistanceAir={\n                lowerHemisphere={headOn=16718.5078125, tailOn=16718.5078125},\n                upperHemisphere={headOn=16718.5078125, tailOn=16718.5078125}\n            },\n            type=1,\n            typeName=\"Tor 9A331\"\n        }\n    }\n}\n\nLauncher:\n{\n    {\n        count=4,\n        desc={\n            Nmax=14,\n            RCS=0.079999998211861,\n            _origin=\"\",\n            altMax=3000,\n            altMin=50,\n            box={\n                max={x=1.4030002355576, y=0.13611803948879, z=0.13611821830273},\n                min={x=-0.84999942779541, y=-0.13611836731434, z=-0.1361181885004}\n            },\n            category=1,\n            displayName=\"Rapier\",\n            fuseDist=0,\n            guidance=8,\n            life=2,\n            missileCategory=2,\n            rangeMaxAltMax=6800,\n            rangeMaxAltMin=6800,\n            rangeMin=400,\n            typeName=\"Rapier\",\n            warhead={caliber=133, explosiveMass=1.3999999761581, mass=1.3999999761581, type=1}\n        }\n    }\n}\n\n--]]\n\tself.samSiteName = \"BLUE-SAM-RAPIER\"\n\tself:setUp()\n\tlu.assertEquals(self.samSite:getNatoName(), \"Rapier\")\n\tlu.assertEquals(self.samSite:getRadars()[1]:getMaxRangeFindingTarget(), 16718.5078125)\n\tlu.assertEquals(self.samSite:getLaunchers()[1]:getRange(), 6800)\n\tlu.assertEquals(self.samSite:getLaunchers()[1]:getInitialNumberOfMissiles(), 4)\n\t\n\tlocal units = Group.getByName(self.samSiteName):getUnits()\n\tfor i = 1, #units do\n\t\tlocal unit = units[i]\n\t\tif unit:getTypeName() == 'rapier_fsa_optical_tracker_unit' then\n\t--\t\tlu.assertEquals(unit:getSensors(), true)\n\t\tend\n\tend\nend\n\nfunction TestSkynetIADSBLUESAMSitesAndEWRadars:testPatriotLauncherAndRadar()\n\n--[[\nPatriot:\n\nRadar:\n{\n    {\n        count=4,\n        desc={\n            Nmax=25,\n            RCS=0.10660000145435,\n            _origin=\"\",\n            altMax=24240,\n            altMin=45,\n            box={\n                max={x=2.5578553676605, y=0.33423712849617, z=0.32681864500046},\n                min={x=-2.5578553676605, y=-0.33423712849617, z=-0.32681867480278}\n            },\n            category=1,\n            displayName=\"MIM-104 Patriot\",\n            fuseDist=13,\n            guidance=4,\n            life=2,\n            missileCategory=2,\n            rangeMaxAltMax=120000,\n            rangeMaxAltMin=30000,\n            rangeMin=3000,\n            typeName=\"MIM_104\",\n            warhead={caliber=410, explosiveMass=73, mass=73, type=1}\n        }\n    }\n}\n\nSearch Radar:\n{\n    {\n        {\n            detectionDistanceAir={\n                lowerHemisphere={headOn=173872.484375, tailOn=173872.484375},\n                upperHemisphere={headOn=173872.484375, tailOn=173872.484375}\n            },\n            type=1,\n            typeName=\"Patriot str\"\n        }\n    }\n}\n--]]\n\tself.samSiteName = \"BLUE-SAM-PATRIOT\"\n\tself:setUp()\n\tlu.assertEquals(self.samSite:getNatoName(), \"Patriot\")\n\tlu.assertEquals(self.samSite:getHARMDetectionChance(), 90)\n\tlu.assertEquals(self.samSite:getCanEngageHARM(), true)\n\t\n\tlocal radar = self.samSite:getSearchRadars()[1]\n\tlu.assertEquals(radar:getMaxRangeFindingTarget(), 173872.484375)\n\t\n\tlocal launcher = self.samSite:getLaunchers()[1]\n\tlu.assertEquals(launcher:getInitialNumberOfMissiles(), 4)\n\tlu.assertEquals(launcher:getRange(), 120000)\nend\n\nfunction TestSkynetIADSBLUESAMSitesAndEWRadars:testLPWSCRAM()\n\t--\"HEMTT_C-RAM_Phalanx\"\n\t\n--[[\t\n\t{\n        {\n            detectionDistanceAir={\n                lowerHemisphere={headOn=13374.806640625, tailOn=13374.806640625},\n                upperHemisphere={headOn=13374.806640625, tailOn=13374.806640625}\n            },\n            type=1,\n            typeName=\"C_RAM_Phalanx\"\n        }\n    }\n\t\n\t    {\n        count=1550,\n        desc={\n            _origin=\"\",\n            box={\n                max={x=2.2344591617584, y=0.12504191696644, z=0.12113922089338},\n                min={x=-6.61008644104, y=-0.12504199147224, z=-0.12113920599222}\n            },\n            category=0,\n            displayName=\"M246_20_HE\",\n            life=2,\n            typeName=\"weapons.shells.M246_20_HE_gr\",\n            warhead={caliber=20, explosiveMass=0.1, mass=0.1, type=1}\n        }\n    }\n}\n\n--]]\n\tself.samSiteName = \"BLUE-SAM-LPWS-C-RAM\"\n\tself:setUp()\n\tlu.assertEquals(#self.samSite:getRadars(),1)\n\tlu.assertEquals(#self.samSite:getLaunchers(), 1)\n\tlu.assertEquals(#self.samSite:getTrackingRadars(), 0)\n\t\n\tlocal searchRadar = self.samSite:getSearchRadars()[1]\n\tlocal launcher = self.samSite:getLaunchers()[1]\n\t\n\tlu.assertEquals(searchRadar:getMaxRangeFindingTarget(), 13374.806640625)\n\tlu.assertEquals(launcher:getRange(), 13374.806640625)\nend\n\n\nfunction TestSkynetIADSBLUESAMSitesAndEWRadars:testEWRANFPS117Domed()\n\t--[[\n\t    {\n        {\n            detectionDistanceAir={\n                lowerHemisphere={headOn=309626.78125, tailOn=309626.78125},\n                upperHemisphere={headOn=309626.78125, tailOn=309626.78125}\n            },\n            type=1,\n            typeName=\"FPS-117\"\n        }\n    }\n}\n--]]\n\tself.ewRadarName = \"BLUE-EW-FPS-117-DOMED\"\n\tself:setUp()\n\t--lu.assertEquals(Unit.getByName(self.ewRadarName):getTypeName(), nil)\n\tlu.assertEquals(self.ewRadar:getNatoName(), \"FPS-117 Dome\")\n\tlu.assertEquals(self.ewRadar:getHARMDetectionChance(), 80)\n\tlocal radar = self.ewRadar:getSearchRadars()[1]\n\tlu.assertEquals(radar:getMaxRangeFindingTarget(), 309626.78125)\nend\n\nfunction TestSkynetIADSBLUESAMSitesAndEWRadars:testEWRANFPS117()\n\t--[[\n\t    {\n        {\n            detectionDistanceAir={\n                lowerHemisphere={headOn=309626.78125, tailOn=309626.78125},\n                upperHemisphere={headOn=309626.78125, tailOn=309626.78125}\n            },\n            type=1,\n            typeName=\"FPS-117\"\n        }\n    }\n}\n--]]\n\tself.ewRadarName = \"BLUE-EW-FPS-117\"\n\tself:setUp()\n\t--lu.assertEquals(Unit.getByName(self.ewRadarName):getTypeName(), nil)\n\tlu.assertEquals(self.ewRadar:getNatoName(), \"FPS-117\")\n\tlu.assertEquals(self.ewRadar:getHARMDetectionChance(), 80)\n\tlocal radar = self.ewRadar:getSearchRadars()[1]\n\tlu.assertEquals(radar:getMaxRangeFindingTarget(), 309626.78125)\nend\n\n--TODO: this test can only be finished once the perry class has radar data:\nfunction TestSkynetIADSBLUESAMSitesAndEWRadars:testOliverHazzardPerryClassShip()\n--[[\nOliver Hazzard:\n\nLaunchers:\n {\n    {\n        count=2016,\n        desc={\n            _origin=\"\",\n            box={\n                max={x=2.2344591617584, y=0.12504191696644, z=0.12113922089338},\n                min={x=-6.61008644104, y=-0.12504199147224, z=-0.12113920599222}\n            },\n            category=0,\n            displayName=\"12.7mm\",\n            life=2,\n            typeName=\"weapons.shells.M2_12_7_T\",\n            warhead={caliber=12.7, explosiveMass=0, mass=0.046, type=0}\n        }\n    },\n    {\n        count=460,\n        desc={\n            _origin=\"\",\n            box={\n                max={x=2.2344591617584, y=0.12504191696644, z=0.12113922089338},\n                min={x=-6.61008644104, y=-0.12504199147224, z=-0.12113920599222}\n            },\n            category=0,\n            displayName=\"25mm HE\",\n            life=2,\n            typeName=\"weapons.shells.M242_25_HE_M792\",\n            warhead={caliber=25, explosiveMass=0.185, mass=0.185, type=1}\n        }\n    },\n    {\n        count=142,\n        desc={\n            _origin=\"\",\n            box={\n                max={x=2.2344591617584, y=0.12504191696644, z=0.12113922089338},\n                min={x=-6.61008644104, y=-0.12504199147224, z=-0.12113920599222}\n            },\n            category=0,\n            displayName=\"25mm AP\",\n            life=2,\n            typeName=\"weapons.shells.M242_25_AP_M791\",\n            warhead={caliber=25, explosiveMass=0, mass=0.155, type=0}\n        }\n    },\n    {\n        count=775,\n        desc={\n            _origin=\"\",\n            box={\n                max={x=2.2344591617584, y=0.12504191696644, z=0.12113922089338},\n                min={x=-6.61008644104, y=-0.12504199147224, z=-0.12113920599222}\n            },\n            category=0,\n            displayName=\"20mm AP\",\n            life=2,\n            typeName=\"weapons.shells.M61_20_AP\",\n            warhead={caliber=20, explosiveMass=0, mass=0.1, type=0}\n        }\n    },\n    {\n        count=775,\n        desc={\n            _origin=\"\",\n            box={\n                max={x=2.2344591617584, y=0.12504191696644, z=0.12113922089338},\n                min={x=-6.61008644104, y=-0.12504199147224, z=-0.12113920599222}\n            },\n            category=0,\n            displayName=\"20mm HE\",\n            life=2,\n            typeName=\"weapons.shells.M61_20_HE\",\n            warhead={caliber=20, explosiveMass=0.1, mass=0.1, type=1}\n        }\n    },\n    {\n        count=24,\n        desc={\n            Nmax=25,\n            RCS=0.1765999943018,\n            _origin=\"\",\n            altMax=24400,\n            altMin=10,\n            box={\n                max={x=2.9796471595764, y=0.39923620223999, z=0.39878171682358},\n                min={x=-1.5204827785492, y=-0.38143759965897, z=-0.39878168702126}\n            },\n            category=1,\n            displayName=\"SM-2\",\n            fuseDist=15,\n            guidance=4,\n            life=2,\n            missileCategory=2,\n            rangeMaxAltMax=100000,\n            rangeMaxAltMin=40000,\n            rangeMin=4000,\n            typeName=\"SM_2\",\n            warhead={caliber=340, explosiveMass=98, mass=98, type=1}\n        }\n    },\n    {\n        count=16,\n        desc={\n            Nmax=18,\n            RCS=0.10580000281334,\n            _origin=\"\",\n            altMax=10000,\n            altMin=-1,\n            box={\n                max={x=2.2758972644806, y=0.13610155880451, z=0.28847914934158},\n                min={x=-1.6704962253571, y=-0.4600305557251, z=-0.28847911953926}\n            },\n            category=1,\n            displayName=\"AGM-84S Harpoon\",\n            fuseDist=0,\n            guidance=1,\n            life=2,\n            missileCategory=4,\n            rangeMaxAltMax=241401,\n            rangeMaxAltMin=95000,\n            rangeMin=3000,\n            typeName=\"AGM_84S\",\n            warhead={caliber=343, explosiveMass=90, mass=90, type=1}\n        }\n    },\n    {\n        count=180,\n        desc={\n            _origin=\"\",\n            box={\n\t\t\t\n\n\tSENSOR:\n\t{\n\t0={{opticType=0, type=0, typeName=\"long-range naval optics\"}},\n\t{\n\t\t{\n\t\t\tdetectionDistanceAir={\n\t\t\t\tlowerHemisphere={headOn=173872.484375, tailOn=173872.484375},\n\t\t\t\tupperHemisphere={headOn=173872.484375, tailOn=173872.484375}\n\t\t\t},\n\t\t\ttype=1,\n\t\t\ttypeName=\"Patriot str\"\n\t\t},\n\t\t{detectionDistanceRBM=336.19998168945, type=1, typeName=\"perry search radar\"}\n\t}\n\t}\t\n--]]\n\n\tself.ewRadarName = \"BLUE-EW-Oliver-Hazzard\"\n\tself:setUp()\n\tlu.assertEquals(self.ewRadar:getNatoName(), \"PERRY\")\n\t--as long as we don't use the PERRY as a SAM site the distance returned is irrelevant, because it's radar wil be on all the time\n\tlu.assertEquals(self.ewRadar:getRadars()[1]:getMaxRangeFindingTarget(), 173872.484375)\nend\n\nend"
  },
  {
    "path": "unit-tests/test-skynet-iads-contact.lua",
    "content": "do\n\nTestSyknetIADSContact = {}\n\nfunction TestSyknetIADSContact:setUp()\n\tlocal radarTarget = {}\n\tradarTarget.object = Unit.getByName('test-outer-search-range')\n\tself.contact = SkynetIADSContact:create(radarTarget)\nend\n\nfunction TestSyknetIADSContact:testGetNumberOfTimesHitByRadar()\n\tlu.assertEquals(self.contact:getNumberOfTimesHitByRadar(), 0)\n\tself.contact:refresh()\n\tlu.assertEquals(self.contact:getNumberOfTimesHitByRadar(), 1)\nend\n\nfunction TestSyknetIADSContact:testRefresh()\n\tlocal called = 0\n\tfunction self.contact:updateSimpleAltitudeProfile()\n\t\tcalled = 1\n\tend\n\tself.contact:refresh()\n\tlu.assertEquals(called, 1)\n\tfunction self.contact:getDCSRepresentation()\n\t\treturn Unit.getByName('test-not-in-firing-range-of-sa-2')\n\tend\n\t--we set time in the past, to simulate distance traveled\n\tself.contact.lastTimeSeen = timer.getAbsTime() - 1000\n\tlu.assertEquals(self.contact:getAge(), 1000)\n\tself.contact:refresh()\n\tlu.assertEquals(self.contact:getGroundSpeedInKnots(0), 989)\nend\n\nfunction TestSyknetIADSContact:testGetHeightInFeetMSL()\n\tlu.assertEquals(self.contact:getHeightInFeetMSL(), 5015)\nend\n\nfunction TestSyknetIADSContact:testUpdateSimpleAltitudeProfile()\n\tlocal mockDCSObject = {}\n\n\t\n\tfunction mockDCSObject:getPosition()\n\t\tlocal p = {}\n\t\tp.y = 100\n\t\tlocal ret = {}\n\t\tret.p = p\n\t\treturn ret\n\tend\n\tself.contact.position.p.y = 200\n\tfunction self.contact:getDCSRepresentation()\n\t return mockDCSObject\n\tend\n\t\n\tself.contact:updateSimpleAltitudeProfile()\n\tlocal altProfile = self.contact:getSimpleAltitudeProfile()\n\tlu.assertEquals(altProfile[1], SkynetIADSContact.DESCEND)\n\tlu.assertEquals(#altProfile, 1)\n\n\n\tfunction mockDCSObject:getPosition()\n\t\tlocal p = {}\n\t\tp.y = 200\n\t\tlocal ret = {}\n\t\tret.p = p\n\t\treturn ret\n\tend\n\tself.contact.position.p.y = 200\n\tfunction self.contact:getDCSRepresentation()\n\t return mockDCSObject\n\tend\n\t\n\tself.contact:updateSimpleAltitudeProfile()\n\tlocal altProfile = self.contact:getSimpleAltitudeProfile()\n\tlu.assertEquals(altProfile[1], SkynetIADSContact.DESCEND)\n\tlu.assertEquals(#altProfile, 1)\n\n\n\n\tfunction mockDCSObject:getPosition()\n\t\tlocal p = {}\n\t\tp.y = 200\n\t\tlocal ret = {}\n\t\tret.p = p\n\t\treturn ret\n\tend\n\tself.contact.position.p.y = 100\n\t\n\tself.contact:updateSimpleAltitudeProfile()\n\tlocal altProfile = self.contact:getSimpleAltitudeProfile()\n\tlu.assertEquals(altProfile[2], SkynetIADSContact.CLIMB)\n\tlu.assertEquals(#altProfile, 2)\n\t\n\t\n\tfunction mockDCSObject:getPosition()\n\t\tlocal p = {}\n\t\tp.y = 200\n\t\tlocal ret = {}\n\t\tret.p = p\n\t\treturn ret\n\tend\n\tself.contact.position.p.y = 100\n\t\n\tself.contact:updateSimpleAltitudeProfile()\n\tlocal altProfile = self.contact:getSimpleAltitudeProfile()\n\tlu.assertEquals(altProfile[2], SkynetIADSContact.CLIMB)\n\tlu.assertEquals(#altProfile, 2)\nend\n\nfunction TestSyknetIADSContact:testSetIsHARM()\n\tlu.assertEquals(self.contact.harmState, SkynetIADSContact.HARM_UNKNOWN)\n\tself.contact:setHARMState(SkynetIADSContact.HARM)\n\tlu.assertEquals(self.contact.harmState, SkynetIADSContact.HARM)\nend\n\nfunction TestSyknetIADSContact:testGetMagneticHeading()\n\tlu.assertEquals(self.contact:getMagneticHeading(), 347)\n\t\n\tfunction self.contact:isExist()\n\t\treturn false\n\tend\n\t\n\tlu.assertEquals(self.contact:getMagneticHeading(), -1)\nend\n\nfunction TestSyknetIADSContact:testIsIdentifiedAsHARM()\n\tlu.assertEquals(self.contact:isIdentifiedAsHARM(), false)\n\tself.contact:setHARMState(SkynetIADSContact.HARM)\n\tlu.assertEquals(self.contact:isIdentifiedAsHARM(), true)\nend\n\nfunction TestSyknetIADSContact:testIsHARMStateUnknown()\n\tlu.assertEquals(self.contact:isHARMStateUnknown(), true)\n\tself.contact:setHARMState(SkynetIADSContact.NOT_HARM)\n\tlu.assertEquals(self.contact:isHARMStateUnknown(), false)\nend\n\nfunction TestSyknetIADSContact:testAddAbstractRadarElementDetected()\n\tlocal radar = {}\n\tself.contact:addAbstractRadarElementDetected(radar)\n\tlu.assertEquals(#self.contact:getAbstractRadarElementsDetected(), 1)\n\t\n\t--adding the same radar again, shall not result in it being added:\n\tself.contact:addAbstractRadarElementDetected(radar)\n\tlu.assertEquals(#self.contact:getAbstractRadarElementsDetected(), 1)\n\t\n\tlocal radar2 = {}\n\tself.contact:addAbstractRadarElementDetected(radar2)\n\tlu.assertEquals(#self.contact:getAbstractRadarElementsDetected(), 2)\nend\t\n\nfunction TestSyknetIADSContact:testGetTypeNameUNKNOWN()\n\tfunction self.contact:getDCSRepresentation()\n\t\treturn nil\n\tend\n\tlu.assertEquals(self.contact:getTypeName(), \"UNKNOWN\")\nend\n\nfunction TestSyknetIADSContact:testGetTypeNameisHARM()\n\tself.contact:setHARMState(SkynetIADSContact.HARM)\n\tlu.assertEquals(self.contact:getTypeName(), SkynetIADSContact.HARM)\nend\n\nfunction TestSyknetIADSContact:testGetTypeNameisUnit()\n\tlu.assertEquals(self.contact:getTypeName(), \"AH-1W\")\nend\n\nend"
  },
  {
    "path": "unit-tests/test-skynet-iads-harm-detection.lua",
    "content": "do\n\nTestSkynetIADSHARMDetection = {}\n\nfunction TestSkynetIADSHARMDetection:setUp()\n\tlocal iads = SkynetIADS:create()\n\tself.harmDetection = SkynetIADSHARMDetection:create(iads)\nend\n\nfunction TestSkynetIADSHARMDetection:testContact0GroundSpeed()\n\t\n\tlocal mockContact = {}\n\tfunction mockContact:getGroundSpeedInKnots(round)\n\t\treturn 0\n\tend\n\t\n\tlocal calledProfileInfo = false\n\tfunction mockContact:getSimpleAltitudeProfile()\n\t\tcalledProfileInfo = true\n\tend\n\tself.harmDetection:setContacts({mockContact})\n\tself.harmDetection:evaluateContacts()\n\tlu.assertEquals(calledProfileInfo, false)\nend\n\nfunction TestSkynetIADSHARMDetection:testEvaluateContactsContactIsHARMInClimb()\n\t\n\t--test with a contact that shall be identified as a HARM\n\tlocal mockContactHARM = {}\n\t\n\tfunction mockContactHARM:getGroundSpeedInKnots(round)\n\t\treturn 1500\n\tend\n\t\n\tfunction mockContactHARM:isHARMStateUnknown()\n\t\treturn true\n\tend\n\t\n\tfunction mockContactHARM:getSimpleAltitudeProfile()\n\t\treturn {SkynetIADSContact.CLIMB}\n\tend\n\t\n\tlocal harmStateCalled = false\n\tfunction mockContactHARM:setHARMState(state)\n\t\tharmStateCalled = true\n\t\tlu.assertEquals(state, SkynetIADSContact.HARM)\n\tend\n\n\tlocal calls = 0\n\tfunction mockContactHARM:isIdentifiedAsHARM()\n\t\tcalls = calls + 1\n\t\tif ( calls == 2 ) then\n\t\t\treturn true\n\t\telse\n\t\t\treturn false\n\t\tend\n\tend\n\t\n\tlocal mockRadar = {}\n\tfunction mockRadar:getHARMDetectionChance()\n\t\treturn 50\n\tend\n\t\n\tfunction mockContactHARM:getAbstractRadarElementsDetected()\n\t\treturn {mockRadar}\n\tend\n\t\n\tlocal probCalled = false\n\tfunction self.harmDetection:shallReactToHARM(prob)\n\t\tlu.assertEquals(prob, 50)\n\t\tprobCalled = true\n\t\treturn true\n\tend\n\n\t\n\tlocal contactInform = false\n\tfunction self.harmDetection:informRadarsOfHARM(contact)\n\t\tlu.assertEquals(mockContactHARM, contact)\n\t\tcontactInform = true\n\tend\n\n\t\n\tself.harmDetection:setContacts({mockContactHARM})\n\t\n\tlocal calledCleanedAgedTargets = false\n\tfunction self.harmDetection:cleanAgedContacts()\n\t\tcalledCleanedAgedTargets = true\n\tend\n\tself.harmDetection:evaluateContacts()\n\t\n\tlu.assertEquals(calledCleanedAgedTargets, true)\n\t\n\tlu.assertEquals(harmStateCalled, true)\n\tlu.assertEquals(probCalled, true)\n\tlu.assertEquals(contactInform, true)\nend\n\nfunction TestSkynetIADSHARMDetection:testEvaluateContactsContactDetectedAsHARMHas3rdAltitudeChangeRecorded()\n\t--a contact previously identified as a HARM has a 3rd altitude change recorded, this means it's an aircraft previously falsely detected as HARM\n\tlocal mockContactHARM = {}\n\t\n\tfunction mockContactHARM:getGroundSpeedInKnots(round)\n\t\treturn 1000\n\tend\n\t\n\tfunction mockContactHARM:isHARMStateUnknown()\n\t\treturn false\n\tend\n\t\n\tfunction mockContactHARM:getSimpleAltitudeProfile()\n\t\treturn {SkynetIADSContact.DESCEND, SkynetIADSContact.CLIMB, SkynetIADSContact.DESCEND }\n\tend\n\t\n\tlocal harmStateCalled = false\n\tfunction mockContactHARM:setHARMState(state)\n\t\tharmStateCalled = true\n\t\tlu.assertEquals(state, SkynetIADSContact.HARM_UNKNOWN)\n\tend\n\t\n\tlocal calls = 0\n\tfunction mockContactHARM:isIdentifiedAsHARM()\n\t\tcalls = calls + 1\n\t\tif ( calls == 2 ) then\n\t\t\treturn true\n\t\telse\n\t\t\treturn false\n\t\tend\n\tend\n\t\n\tlocal contactInform = false\n\tfunction self.harmDetection:informRadarsOfHARM(contact)\n\t\tcontactInform = true\n\tend\n\t\n\tfunction self.harmDetection:getNewRadarsThatHaveDetectedContact(contact)\n\t\treturn {\"MockRadar\"}\n\tend\n\t\n\tself.harmDetection:setContacts({mockContactHARM})\n\tself.harmDetection:evaluateContacts()\n\t\n\tlu.assertEquals(harmStateCalled, true)\n\tlu.assertEquals(contactInform, false)\n\t\nend\n\nfunction TestSkynetIADSHARMDetection:testGetDetectionProbability()\n\t\n\tlocal mockSAM1 = {}\n\tfunction mockSAM1:getHARMDetectionChance()\n\t\treturn 60\n\tend\n\t\n\tlocal mockSam2 = {}\n\tfunction mockSam2:getHARMDetectionChance()\n\t\treturn 30\n\tend\n\t\n\tlocal mockNewRadarsDetected = {mockSAM1, mockSam2}\n\n\tlu.assertEquals(self.harmDetection:getDetectionProbability(mockNewRadarsDetected), 72)\n\t\n\tfunction mockSAM1:getHARMDetectionChance()\n\t\treturn 20\n\tend\n\t\n\tfunction mockSam2:getHARMDetectionChance()\n\t\treturn 90\n\tend\n\t\n\tlu.assertEquals(self.harmDetection:getDetectionProbability(mockNewRadarsDetected), 92)\n\t\nend\n\nfunction TestSkynetIADSHARMDetection:testGetNewRadarsThatHaveDetectedContact()\n\tlocal mockContact = {}\n\tlocal mockRadar1 = {\"MockRadar1\"}\n\tlocal mockRadar2 = {\"MockRadar2\"}\n\tlocal detectedRadars = {mockRadar1, mockRadar2}\n\tfunction mockContact:getAbstractRadarElementsDetected()\n\t\treturn detectedRadars\n\tend\n\tlocal result = self.harmDetection:getNewRadarsThatHaveDetectedContact(mockContact)\n\tlu.assertEquals(result, {mockRadar1, mockRadar2})\n\tlu.assertEquals(self.harmDetection.contactRadarsEvaluated[mockContact], {mockRadar1, mockRadar2})\n\n\tlocal result2 = self.harmDetection:getNewRadarsThatHaveDetectedContact(mockContact)\n\tlu.assertEquals(result2, {})\n\tlu.assertEquals(self.harmDetection.contactRadarsEvaluated[mockContact], {mockRadar1, mockRadar2})\n\t\n\tlocal mockRadar3 = {\"MockRadar3\"}\n\ttable.insert(detectedRadars, mockRadar3)\n\tlu.assertEquals(#mockContact:getAbstractRadarElementsDetected(), 3)\n\tlocal result3 = self.harmDetection:getNewRadarsThatHaveDetectedContact(mockContact)\n\tlu.assertEquals(result3, {mockRadar3})\t\n\tlu.assertEquals(self.harmDetection.contactRadarsEvaluated[mockContact], {mockRadar1, mockRadar2, mockRadar3})\n\t\n\tlocal mockRadar4 = {\"MockRadar4\"}\n\ttable.insert(detectedRadars, mockRadar4)\n\tlocal result4 = self.harmDetection:getNewRadarsThatHaveDetectedContact(mockContact)\n\tlu.assertEquals(result4, {mockRadar4})\t\n\tlu.assertEquals(self.harmDetection.contactRadarsEvaluated[mockContact], {mockRadar1, mockRadar2, mockRadar3, mockRadar4})\nend\n\nfunction TestSkynetIADSHARMDetection:testCleanAgedContacts()\n\tlocal mockContact1 = {}\n\tfunction mockContact1:getAge()\n\t\treturn 1\n\tend\n\t\n\tlocal mockContact2 = {}\n\tfunction mockContact2:getAge()\n\t\treturn 33\n\tend\n\t\n\tlocal contactRadars = {}\n\tcontactRadars[mockContact1] = \"keep\"\n\tcontactRadars[mockContact2] = \"delete\"\n\tself.harmDetection.contactRadarsEvaluated = contactRadars\n\tself.harmDetection:cleanAgedContacts()\n\t\n\tlocal count = 0\n\tfor key, value in pairs(self.harmDetection.contactRadarsEvaluated) do\n\t\tcount = count + 1\n\tend\n\tlu.assertEquals(count, 1)\n\tlu.assertEquals(self.harmDetection.contactRadarsEvaluated[mockContact1], \"keep\")\nend\n\nend"
  },
  {
    "path": "unit-tests/test-skynet-iads-jammer.lua",
    "content": "do\nTestSkynetIADSJammer = {}\n\nfunction TestSkynetIADSJammer:setUp()\n\tself.emitter = Unit.getByName('jammer-source')\t\n\tself.mockIADS = {}\n\tfunction self.mockIADS:getDebugSettings()\n\t\treturn {}\n\tend\n\tself.jammer = SkynetIADSJammer:create(self.emitter, self.mockIADS)\nend\n\nfunction TestSkynetIADSJammer:tearDown()\n\tself.jammer:masterArmSafe()\nend\n\nfunction TestSkynetIADSJammer:testSetJammerDistance()\n\tself.jammer:setMaximumEffectiveDistance(20)\n\tlu.assertEquals(self.jammer.maximumEffectiveDistanceNM, 20)\nend\n\nfunction TestSkynetIADSJammer:testSetupJammerAndRunCycle()\n\tlu.assertEquals(self.jammer.jammerTaskID, nil)\n\tself.jammer:masterArmOn()\n\tlu.assertNotIs(self.jammer.jammerTaskID, nil)\n\t\n\tlocal mockRadar = {}\n\tlocal mockSAM = {}\n\tlocal calledJam = false\n\t\n\tfunction mockSAM:getRadars()\n\t\treturn {mockRadar}\n\tend\n\t\n\tfunction mockSAM:getNatoName()\n\t\treturn \"SA-2\"\n\tend\n\t\n\tfunction mockSAM:jam(prob)\n\t\tcalledJam = true\n\tend\n\t\n\tfunction self.mockIADS:getActiveSAMSites()\n\t\treturn {mockSAM}\n\tend\n\t\n\tfunction self.jammer:getDistanceNMToRadarUnit(radarUnit)\n\t\treturn 50\n\tend\n\t\n\tfunction self.jammer:hasLineOfSightToRadar(radar)\n\t\treturn true\n\tend\n\t\n\tself.jammer.runCycle(self.jammer)\n\tlu.assertEquals(calledJam, true)\nend\n\nfunction TestSkynetIADSJammer:testIsActiveForUnknownType()\n\tlu.assertEquals(self.jammer:isKnownRadarEmitter('ABC-Test'), false)\nend\n\nfunction TestSkynetIADSJammer:testIsActiveForKnownType()\n\tlu.assertEquals(self.jammer:isKnownRadarEmitter('SA-2'), true)\nend\n\nfunction TestSkynetIADSJammer:testCleanUpJammer()\n\tself.jammer:masterArmOn()\n\n\tlocal alive = false\n\tlocal i = 0\n\twhile i < 10000 do\n\t\tlocal id =  mist.removeFunction(i)\n\t\ti = i + 1\n\t\tif id then\n\t\t\talive = true\n\t\tend\n\tend\n\tlu.assertEquals(alive, true)\n\n\tself.jammer:masterArmSafe()\n\t\n\ti = 0\n\talive = false\n\twhile i < 10000 do\n\t\tlocal id =  mist.removeFunction(i)\n\t\ti = i + 1\n\t\tif id then\n\t\t\talive = true\n\t\tend\n\tend\n\tlu.assertEquals(alive, false)\nend\n\nfunction TestSkynetIADSJammer:testAddJammerFunction()\n\n\tlocal function f(distanceNM)\n\t\treturn 2 * distanceNM\n\tend\n\tself.jammer:addFunction('SA-99', f)\n\tlu.assertEquals(self.jammer:getSuccessProbability(20, 'SA-99'), 40)\n\tlu.assertEquals(self.jammer:isKnownRadarEmitter('SA-99'), true)\n\tself.jammer:disableFor('SA-99')\n\tlu.assertEquals(self.jammer:isKnownRadarEmitter('SA-99'), false)\nend\n\nfunction TestSkynetIADSJammer:testDestroyEmitter()\n\tself:tearDown()\n\tself.emitter = Unit.getByName(\"jammer-source-unit-test\")\n\tlocal iads = SkynetIADS:create()\n\tself.jammer = SkynetIADSJammer:create(self.emitter, iads)\n\tself.jammer:masterArmOn()\n\t\n\ttrigger.action.explosion(Unit.getByName(\"jammer-source-unit-test\"):getPosition().p, 500)\n\tself.jammer.runCycle(self.jammer)\n\t\n\tlocal i = 0\n\tlocal alive = false\n\twhile i < 10000 do\n\t\tlocal id =  mist.removeFunction(i)\n\t\ti = i + 1\n\t\tif id then\n\t\t\talive = true\n\t\tend\n\tend\n\tlu.assertEquals(alive, false)\nend\nend"
  },
  {
    "path": "unit-tests/test-skynet-iads-red-sam-sites-and-ew-radars.lua",
    "content": "do\nTestSkynetIADSREDSAMSitesAndEWRadars = {}\n\nfunction TestSkynetIADSREDSAMSitesAndEWRadars:setUp()\n\tself.skynetIADS = SkynetIADS:create()\n\tif self.samSiteName then\n\t\tlocal samSite = Group.getByName(self.samSiteName)\n\t\tself.samSite = SkynetIADSSamSite:create(samSite, self.skynetIADS)\n\t\t\n\t\t-- we overrite this method since it returns radar contacts in the DCS world which mess up the tests.\n\t\tfunction self.samSite:getDetectedTargets()\n\t\t\treturn {}\n\t\tend\n\t\t\n\t\tself.samSite:setupElements()\n\t\tself.samSite:goLive()\n\tend\n\t\n\tif self.ewRadarName then\n\t\tself.skynetIADS:addEarlyWarningRadarsByPrefix('EW')\n\t\tself.ewRadar = self.skynetIADS:getEarlyWarningRadarByUnitName(self.ewRadarName)\n\tend\nend\n\nfunction TestSkynetIADSREDSAMSitesAndEWRadars:tearDown()\n\tif self.samSite then\t\n\t\tself.samSite:goDark()\n\t\tself.samSite:cleanUp()\n\tend\n\t\n\tif self.ewRadar then\n\t\tself.ewRadar:cleanUp()\n\tend\n\t\n\tif self.skynetIADS then\n\t\tself.skynetIADS:deactivate()\n\tend\n\tself.ewRadarName = nil\n\tself.samSiteName = nil\nend\n\nfunction TestSkynetIADSREDSAMSitesAndEWRadars:testCheckSA6GroupNumberOfLaunchersAndRangeValuesAndSearchRadarsAndNatoName()\n--[[\n\tDCS properties SA-6 (Kub / Gainful) \n\t\n\tRadar:\n\t{\n\t\t{\n\t\t\t{\n\t\t\t\tdetectionDistanceAir={\n\t\t\t\t\tlowerHemisphere={headOn=46811.82421875, tailOn=46811.82421875},\n\t\t\t\t\tupperHemisphere={headOn=46811.82421875, tailOn=46811.82421875}\n\t\t\t\t\tupperHemisphere={headOn=46811.82421875, tailOn=46811.82421875}\n\t\t\t\t},\n\t\t\t\ttype=1,\n\t\t\t\ttypeName=\"Kub 1S91 str\"\n\t\t\t}\n\t\t}\n\t}\n\n\tLauncher:\n    {\n        count=3,\n        desc={\n            Nmax=16,\n            RCS=0.1059999987483,\n            _origin=\"\",\n            altMax=8000,\n            altMin=30,\n            box={\n                max={x=2.9061908721924, y=0.43574807047844, z=0.4395649433136},\n                min={x=-2.9048342704773, y=-0.43574807047844, z=-0.4395649433136}\n            },\n            category=1,\n            displayName=\"3M9M Kub (SA-6 Gainful)\",\n            fuseDist=12,\n            guidance=4,\n            life=2,\n            missileCategory=2,\n            rangeMaxAltMax=25000,\n            rangeMaxAltMin=25000,\n            rangeMin=4000,\n            typeName=\"SA3M9M\",\n            warhead={caliber=330, explosiveMass=59, mass=59, type=1}\n        }\n    }\n}\n--]]\n\tself.samSiteName = \"SAM-SA-6-2\"\n\tself:setUp()\n\tlu.assertEquals(#self.samSite:getLaunchers(), 1)\n\tlu.assertEquals(#self.samSite:getSearchRadars(), 1)\n\t\n\tlocal searchRadar = self.samSite:getSearchRadars()[1]\n\tlu.assertEquals(searchRadar:getMaxRangeFindingTarget(), 46811.82421875)\n\t\n\tlu.assertEquals(self.samSite:getNatoName(), \"SA-6\")\n\t\n\tlocal launcher = self.samSite:getLaunchers()[1]\n\tlu.assertEquals(launcher:getRange(), 25000)\n\t\n\tlu.assertEquals(self.samSite:getRemainingNumberOfMissiles(), 3)\nend\n\nfunction TestSkynetIADSREDSAMSitesAndEWRadars:testCheckSA10GroupNumberOfLaunchersAndSearchRadarsAndNatoName()\n--[[\n\tDCS properties SA-10 (S-300 / SA-10 Grumble)\n\t\n\tLauncher:\n\t{\n\t\t{\n\t\t\tcount=4,\n\t\t\tdesc={\n\t\t\t\tNmax=25,\n\t\t\t\tRCS=0.17800000309944,\n\t\t\t\t_origin=\"\",\n\t\t\t\taltMax=30000,\n\t\t\t\taltMin=25,\n\t\t\t\tbox={\n\t\t\t\t\tmax={x=3.6516976356506, y=0.81190091371536, z=0.81109911203384},\n\t\t\t\t\tmin={x=-3.6131811141968, y=-0.80982387065887, z=-0.81062549352646}\n\t\t\t\t},\n\t\t\t\tcategory=1,\n\t\t\t\tdisplayName=\"5V55 S-300PS (SA-10B Grumble)\",\n\t\t\t\tfuseDist=20,\n\t\t\t\tguidance=4,\n\t\t\t\tlife=2,\n\t\t\t\tmissileCategory=2,\n\t\t\t\trangeMaxAltMax=75000,\n\t\t\t\trangeMaxAltMin=40000,\n\t\t\t\trangeMin=5000,\n\t\t\t\ttypeName=\"SA5B55\",\n\t\t\t\twarhead={caliber=508, explosiveMass=133, mass=133, type=1}\n\t\t\t}\n\t\t}\n\t}\n\n--]]\n\tself.samSiteName = \"SAM-SA-10\"\n\tself:setUp()\n\tlu.assertEquals(#self.samSite:getLaunchers(), 2)\n\tlu.assertEquals(#self.samSite:getSearchRadars(), 3)\n\tlu.assertEquals(#self.samSite:getTrackingRadars(), 2)\n\tlu.assertEquals(#self.samSite:getRadars(), 5)\n\tlu.assertEquals(self.samSite:getNatoName(), \"SA-10\")\n\tlu.assertEquals(self.samSite:getCanEngageHARM(), true)\n\t\n\t\n\tlocal launchers = self.samSite:getLaunchers()\n\tlocal numLoops = 0\n\t-- seems like currently both launcher types of the SA-10 have the same range values\n\tfor i = 1, #launchers do\n\t\tlocal launcher = launchers[i]\n\t\tlu.assertEquals(launcher:getInitialNumberOfMissiles(), 4)\n\t\tlu.assertEquals(launcher:getRange(), 75000)\n\t\tlu.assertEquals(launcher:getMaximumFiringAltitude(), 25000)\n\t\tnumLoops = numLoops + 1\n\tend\n\tlu.assertEquals(numLoops, 2)\n\t\n\tlocal radars = self.samSite:getRadars()\n\t\n\t--for some strange reason the s300 does not have any range values in getSensors(), all the data there is empty\n\tlocal tr = self.samSite:getTrackingRadars()[1]\n\tlu.assertEquals(tr:getMaxRangeFindingTarget(),0)\n\t\n\tlocal tr = self.samSite:getTrackingRadars()[2]\n\tlu.assertEquals(tr:getMaxRangeFindingTarget(),0)\n\n\tlocal sr = self.samSite:getSearchRadars()[1]\n\tlu.assertEquals(sr:getMaxRangeFindingTarget(), 0)\n\t\n\tlocal sr = self.samSite:getSearchRadars()[2]\n\tlu.assertEquals(sr:getMaxRangeFindingTarget(), 0)\n\t\n\tlocal sr = self.samSite:getSearchRadars()[3]\n\tlu.assertEquals(sr:getMaxRangeFindingTarget(), 0)\nend\n\nfunction TestSkynetIADSREDSAMSitesAndEWRadars:testCheckSA11GroupNumberOfLaunchersAndSearchRadarsAndNatoName()\n--[[\n\tDCS properties SA-10 (S-300 / SA-10 Grumble)\n\t\n\tRadar:\t\n\t{\n\t\t{\n\t\t\t{\n\t\t\t\tdetectionDistanceAir={\n\t\t\t\t\tlowerHemisphere={headOn=53499.2265625, tailOn=53499.2265625},\n\t\t\t\t\tupperHemisphere={headOn=53499.2265625, tailOn=53499.2265625}\n\t\t\t\t},\n\t\t\t\ttype=1,\n\t\t\t\ttypeName=\"S-300PS 40B6M tr\"\n\t\t\t}\n\t\t}\n\t}\n\t\n\tLauncher:\n\t{\n\t\t{\n\t\t\tcount=4,\n\t\t\tdesc={\n\t\t\t\tNmax=25,\n\t\t\t\tRCS=0.17800000309944,\n\t\t\t\t_origin=\"\",\n\t\t\t\taltMax=30000,\n\t\t\t\taltMin=25,\n\t\t\t\tbox={\n\t\t\t\t\tmax={x=3.6516976356506, y=0.81190091371536, z=0.81109911203384},\n\t\t\t\t\tmin={x=-3.6131811141968, y=-0.80982387065887, z=-0.81062549352646}\n\t\t\t\t},\n\t\t\t\tcategory=1,\n\t\t\t\tdisplayName=\"5V55 S-300PS (SA-10B Grumble)\",\n\t\t\t\tfuseDist=20,\n\t\t\t\tguidance=4,\n\t\t\t\tlife=2,\n\t\t\t\tmissileCategory=2,\n\t\t\t\trangeMaxAltMax=75000,\n\t\t\t\trangeMaxAltMin=40000,\n\t\t\t\trangeMin=5000,\n\t\t\t\ttypeName=\"SA5B55\",\n\t\t\t\twarhead={caliber=508, explosiveMass=133, mass=133, type=1}\n\t\t\t}\n\t\t}\n\t}\n\n--]]\n\tself.samSiteName = \"SAM-SA-11\"\n\tself:setUp()\n\tlu.assertEquals(#self.samSite:getLaunchers(), 1)\n\tlu.assertEquals(#self.samSite:getSearchRadars(), 1)\n\tlu.assertEquals(#self.samSite:getTrackingRadars(), 0)\n\tlu.assertEquals(#self.samSite:getRadars(), 1)\n\tlu.assertEquals(self.samSite:getNatoName(), \"SA-11\")\n\t\n\tlocal launchers = self.samSite:getLaunchers()\n\tlocal launcher = launchers[1]\n\tlu.assertEquals(launcher:getInitialNumberOfMissiles(), 4)\n\tlu.assertEquals(launcher:getRange(), 35000)\n\tlu.assertEquals(launcher:getMaximumFiringAltitude(), 22000)\n\t\n\tlocal radars = self.samSite:getRadars()\n\tlocal radar = radars[1]\n\tlu.assertEquals(radar:getMaxRangeFindingTarget(), 66874.03125)\nend\n\nfunction TestSkynetIADSREDSAMSitesAndEWRadars:testCheckSA3GroupNumberOfLaunchersAndRangeValuesAndSearchRadarsAndNatoName()\n--[[\n\tDCS properties SA-3 (s-125 / SA-3 Goa)\n\t\n\tRadar:\n\t{\n        {\n            detectionDistanceAir={\n                lowerHemisphere={headOn=53499.2265625, tailOn=53499.2265625},\n                upperHemisphere={headOn=53499.2265625, tailOn=53499.2265625}\n            },\n            type=1,\n            typeName=\"p-19 s-125 sr\"\n        }\n    }\n\t\n\tLauncher:\n\t{\n\t\t{\n\t\t\tcount=4,\n\t\t\tdesc={\n\t\t\t\tNmax=16,\n\t\t\t\tRCS=0.1676000058651,\n\t\t\t\t_origin=\"\",\n\t\t\t\taltMax=18000,\n\t\t\t\taltMin=20,\n\t\t\t\tbox={\n\t\t\t\t\tmax={x=3.7270171642303, y=0.94484841823578, z=0.95312494039536},\n\t\t\t\t\tmin={x=-2.6432721614838, y=-0.94484841823578, z=-0.95312494039536}\n\t\t\t\t},\n\t\t\t\tcategory=1,\n\t\t\t\tdisplayName=\"5V27 S-125 Neva (SA-3 Goa)\",\n\t\t\t\tfuseDist=14,\n\t\t\t\tguidance=4,\n\t\t\t\tlife=2,\n\t\t\t\tmissileCategory=2,\n\t\t\t\trangeMaxAltMax=25000,\n\t\t\t\trangeMaxAltMin=11000,\n\t\t\t\trangeMin=3500,\n\t\t\t\ttypeName=\"SA5B27\",\n\t\t\t\twarhead={caliber=400, explosiveMass=60, mass=60, type=1}\n\t\t\t}\n\t\t}\n\t}\n\t\n--]]\n\tself.samSiteName = \"test-SA-3\"\n\tself:setUp()\n\t\n\tlocal array = {}\n\tlocal unitData = {\n\t\t['p-19 s-125 sr'] = {\n\t\t},\n\t}\n\tself.samSite:analyseAndAddUnit(SkynetIADSSAMSearchRadar, array, unitData)\n\tlocal searchRadar = array[1]\n\tlu.assertEquals(#array, 1)\n\tlu.assertEquals(searchRadar:getMaxRangeFindingTarget(), 53499.2265625)\n\t\n\tarray = {}\n\tunitData = {\n\t\t['5p73 s-125 ln'] = {\n\t\t},\n\t}\n\tself.samSite:analyseAndAddUnit(SkynetIADSSAMLauncher, array, unitData)\n\tlocal launcher = array[1]\n\tlu.assertEquals(launcher:getRange(), 25000)\n\tlu.assertEquals(launcher:getMaximumFiringAltitude(), 18000)\n\tarray = {}\n\tunitData = {\n\t\t['snr s-125 tr'] = {\n\t\t},\n\t}\t\n\tself.samSite:analyseAndAddUnit(SkynetIADSSAMTrackingRadar, array, unitData)\n\tlocal searchRadar = array[1]\n\tlu.assertEquals(searchRadar:getMaxRangeFindingTarget(),  53499.2265625)\n\t\n\tlu.assertEquals(#self.samSite:getLaunchers(), 1)\t\n\tlu.assertEquals(#self.samSite:getSearchRadars(), 1)\n\tlu.assertEquals(#self.samSite:getTrackingRadars(), 1)\n\tlu.assertEquals(#self.samSite:getRadars(), 2)\n\tlu.assertEquals(self.samSite:getHARMDetectionChance(), 30)\n\tlu.assertEquals(self.samSite:setHARMDetectionChance(100), self.samSite)\n\t\n\tlu.assertEquals(self.samSite:getNatoName(), \"SA-3\")\nend\n\nfunction TestSkynetIADSREDSAMSitesAndEWRadars:testShilkaGroupLaunchersSearchRadarRangesAndHARMDefenceChance()\n\t--[[\n\t\n\tDCS Properties Shilka / Zues:\n\t\n\tRadar:\t\n\t{\n        {\n            detectionDistanceAir={\n                lowerHemisphere={headOn=5015.552734375, tailOn=5015.552734375},\n                upperHemisphere={headOn=5015.552734375, tailOn=5015.552734375}\n            },\n            type=1,\n            typeName=\"ZSU-23-4 Shilka\"\n        }\n    }\n\t\t\n\t\t\n\tLauncher:\n\t{\n\t\t{\n\t\t\tcount=503,\n\t\t\tdesc={\n\t\t\t\t_origin=\"\",\n\t\t\t\tbox={\n\t\t\t\t\tmax={x=2.2344591617584, y=0.12504191696644, z=0.12113922089338},\n\t\t\t\t\tmin={x=-6.61008644104, y=-0.12504199147224, z=-0.12113920599222}\n\t\t\t\t},\n\t\t\t\tcategory=0,\n\t\t\t\tdisplayName=\"23mm AP\",\n\t\t\t\tlife=2,\n\t\t\t\ttypeName=\"weapons.shells.2A7_23_AP\",\n\t\t\t\twarhead={caliber=23, explosiveMass=0, mass=0.189, type=0}\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\tcount=1501,\n\t\t\tdesc={\n\t\t\t\t_origin=\"\",\n\t\t\t\tbox={\n\t\t\t\t\tmax={x=2.2344591617584, y=0.12504191696644, z=0.12113922089338},\n\t\t\t\t\tmin={x=-6.61008644104, y=-0.12504199147224, z=-0.12113920599222}\n\t\t\t\t},\n\t\t\t\tcategory=0,\n\t\t\t\tdisplayName=\"23mm HE\",\n\t\t\t\tlife=2,\n\t\t\t\ttypeName=\"weapons.shells.2A7_23_HE\",\n\t\t\t\twarhead={caliber=23, explosiveMass=0.189, mass=0.189, type=1}\n\t\t\t}\n\t\t}\n\t}\n\t--]]\n\tself.samSiteName = \"SAM-Shilka\"\n\tself:setUp()\n\tlu.assertEquals(self.samSite:getHARMDetectionChance(), 10)\n\tlu.assertEquals(#self.samSite:getRadars(),1)\n\tlu.assertEquals(#self.samSite:getLaunchers(), 1)\n\tlu.assertEquals(#self.samSite:getTrackingRadars(), 0)\n\tlocal searchRadar = self.samSite:getSearchRadars()[1]\n\t\n\tlu.assertEquals(searchRadar:getMaxRangeFindingTarget(), 5015.552734375)\n\t\n\tlocal target = IADSContactFactory(\"Harrier Pilot\")\n\t\n\tlocal launcher = self.samSite:getLaunchers()[1]\n\t\n\t--shilka has no missiles\n\tlu.assertEquals(launcher:getInitialNumberOfMissiles(), 0)\n\tlu.assertEquals(launcher:getRemainingNumberOfMissiles(), 0)\n\t\n\tlu.assertEquals(#launcher:getDCSRepresentation():getAmmo(), 2)\n\t\n\tlu.assertEquals(launcher:getInitialNumberOfShells(), 2004)\n\tlu.assertEquals(launcher:getRemainingNumberOfShells(), 2004)\n\t\n\tlu.assertEquals(launcher:getRange(), 5015.552734375)\n\t--dcs has no maximum height data for AAA\n\tlu.assertEquals(launcher:getMaximumFiringAltitude(), 0)\n\tlu.assertEquals(launcher:isWithinFiringHeight(target), true)\n\tlu.assertEquals(mist.utils.round(launcher:getHeight(target)), 1909)\n\n\t--this target is at 25k feet\n\tlocal target = IADSContactFactory(\"test-not-in-firing-range-of-sa-2\")\n\tlu.assertEquals(launcher:isWithinFiringHeight(target), false)\nend\n\nfunction TestSkynetIADSREDSAMSitesAndEWRadars:testSA15LaunchersSearchRadarRangeAndHARMDefenceChance()\n\t--[[ \n\tDCS SA-15: properties:\n\tLauncher\n    {\n        count=8,\n        desc={\n            Nmax=30,\n            RCS=0.03070000000298,\n            _origin=\"\",\n            altMax=6000,\n            altMin=10,\n            box={\n                max={x=1.8263295888901, y=0.26701140403748, z=0.26600670814514},\n                min={x=-1.678077340126, y=-0.26701140403748, z=-0.26600670814514}\n            },\n            category=1,\n            displayName=\"9M330 Tor (SA-15 Gauntlet)\",\n            fuseDist=7,\n            guidance=4,\n            life=2,\n            missileCategory=2,\n            rangeMaxAltMax=12000,\n            rangeMaxAltMin=12000,\n            rangeMin=1500,\n            typeName=\"SA9M330\",\n            warhead={caliber=220, explosiveMass=14.5, mass=14.5, type=1}\n        }\n    }\n\t\n\tRadar:\n{\n    0={{opticType=0, type=0, typeName=\"generic SAM search visir\"}},\n    {\n        {\n            detectionDistanceAir={\n                lowerHemisphere={headOn=16718.5078125, tailOn=16718.5078125},\n                upperHemisphere={headOn=16718.5078125, tailOn=16718.5078125}\n            },\n            type=1,\n            typeName=\"Tor 9A331\"\n        }\n    }\n}\t--]]\n\n\tself.samSiteName = \"SAM-SA-15\"\n\tself:setUp()\n\n\tlu.assertEquals(self.samSite:getNatoName(),'SA-15')\n\tlu.assertEquals(self.samSite:getHARMDetectionChance(), 90)\n\tlu.assertEquals(#self.samSite:getRadars(),1)\t\n\tlu.assertEquals(self.samSite:getCanEngageHARM(), true)\n\t\n\tlocal target = IADSContactFactory(\"Harrier Pilot\")\n\t\n\tlocal searchRadar = self.samSite:getSearchRadars()[1]\n\tlu.assertEquals(searchRadar:getMaxRangeFindingTarget(), 16718.5078125)\n\tlu.assertEquals(searchRadar:isInRange(target), false)\n\t\n\tlu.assertEquals(#self.samSite:getSearchRadars(), 1)\n\tlu.assertEquals(#self.samSite:getTrackingRadars(), 0)\n\tlu.assertEquals(#self.samSite:getLaunchers(), 1)\n\t\n\tlocal launcher = self.samSite:getLaunchers()[1]\n\tlu.assertEquals(launcher:getRange(), 12000)\n\tlu.assertEquals(launcher:getMaximumFiringAltitude(), 6000)\n\t\n\tlu.assertEquals(launcher:isInRange(target), false)\n\n\tlu.assertEquals(mist.utils.round(launcher:getHeight(target)), 1930)\n\tlu.assertEquals(launcher:getMaximumFiringAltitude(), 6000)\n\tlu.assertEquals(launcher:isWithinFiringHeight(target), true)\n\tlu.assertEquals(launcher:getRemainingNumberOfMissiles(), 8)\n\t\n\tlauncher.maximumFiringAltitude = 400\n\tlu.assertEquals(launcher:isWithinFiringHeight(target), false)\nend\n\nfunction TestSkynetIADSREDSAMSitesAndEWRadars:testSA13LaunchersSearchRadarRangeAndHARMDefence()\n--[[\nDCS SA-13 Properties (Strela-10M3 / Gopher):\n{\n    {\n        count=8,\n        desc={\n            Nmax=16,\n            RCS=0.050000000745058,\n            _origin=\"\",\n            altMax=3500,\n            altMin=25,\n            box={\n                max={x=1.1227556467056, y=0.13098473846912, z=0.13213211297989},\n                min={x=-1.1213990449905, y=-0.13098473846912, z=-0.13213211297989}\n            },\n            category=1,\n            displayName=\"9M333 Strela-10 (SA-13 Gopher)\",\n            fuseDist=3,\n            guidance=2,\n            life=2,\n            missileCategory=2,\n            rangeMaxAltMax=5000,\n            rangeMaxAltMin=5000,\n            rangeMin=800,\n            typeName=\"SA9M333\",\n            warhead={caliber=120, explosiveMass=3.5, mass=3.5, type=1}\n        }\n    },\n    {\n\tcount=1009,\n        desc={\n            _origin=\"\",\n            box={\n                max={x=2.2344591617584, y=0.12504191696644, z=0.12113922089338},\n                min={x=-6.61008644104, y=-0.12504199147224, z=-0.12113920599222}\n            },\n            category=0,\n            displayName=\"7.62mm\",\n            life=2,\n            typeName=\"weapons.shells.7_62x54\",\n            warhead={caliber=7.62, explosiveMass=0, mass=0.0119, type=0}\n        }\n    }\n\tDoes not have any Radar Properties in DCS\n--]]\n\n\tself.samSiteName = \"SAM-SA-13\"\n\tself:setUp()\n\tlu.assertEquals(#self.samSite:getRadars(), 1)\n\tlu.assertEquals(#self.samSite:getSearchRadars(), 1)\n\tlu.assertEquals(#self.samSite:getTrackingRadars(), 0)\n\tlu.assertEquals(#self.samSite:getLaunchers(), 1)\n\tlu.assertEquals(self.samSite:getCanEngageHARM(), false)\n\t\n\tlocal searchRadar = self.samSite:getSearchRadars()[1]\n\t\n\t--this asset has no radar sensor information, we load the launcher data instead, to keep interface consistent:\n\tlu.assertEquals(searchRadar:getDCSRepresentation():getSensors(), nil)\n\tlu.assertEquals(searchRadar:getMaxRangeFindingTarget(), 5000)\n\t\n\tlocal launcher = self.samSite:getLaunchers()[1]\n\tlu.assertEquals(launcher:getRange(), 5000)\n\tlu.assertEquals(launcher:getMaximumFiringAltitude(), 3500)\n\tlu.assertEquals(launcher:getRemainingNumberOfMissiles(), 8)\nend\n\nfunction TestSkynetIADSREDSAMSitesAndEWRadars:testHQ7LauncherAndRadar()\n--[[\nHQ-7:\n\nRadar:\n{\n    0={\n        {opticType=0, type=0, typeName=\"TKN-3B day\"},\n        {opticType=2, type=0, typeName=\"TKN-3B night\"},\n        {opticType=0, type=0, typeName=\"Tunguska optic sight\"}\n    },\n    {\n        {\n            detectionDistanceAir={\n                lowerHemisphere={headOn=10090.756835938, tailOn=6727.1713867188},\n                upperHemisphere={headOn=8408.9638671875, tailOn=6727.1713867188}\n            },\n            type=1,\n            typeName=\"HQ-7 TR\"\n        }\n    }\n}\n\nLauncher:\n{\n    {\n        count=4,\n        desc={\n            Nmax=18,\n            RCS=0.0099999997764826,\n            _origin=\"\",\n            altMax=5500,\n            altMin=14.5,\n            box={\n                max={x=1.245908498764, y=0.20055842399597, z=0.20074887573719},\n                min={x=-1.754227399826, y=-0.20056092739105, z=-0.20036999881268}\n            },\n            category=1,\n            displayName=\"HQ-7\",\n            fuseDist=7,\n            guidance=4,\n            life=2,\n            missileCategory=2,\n            rangeMaxAltMax=12000,\n            rangeMaxAltMin=12000,\n            rangeMin=500,\n            typeName=\"HQ-7\",\n            warhead={caliber=156, explosiveMass=15, mass=15, type=1}\n        }\n    }\n}\n--]]\n\tself.samSiteName = \"SAM-HQ-7\"\n\tself:setUp()\n\t\n\tlocal group = Group.getByName(self.samSiteName)\n--[[\n\tlocal units = group:getUnits()\n\tfor i = 1, #units do\n\t\tlocal unit = units[i]\n\t\tif unit:getAmmo() then\n\t\t--\tlu.assertEquals(unit:getAmmo(), false)\n\t\tend\n\tend\n--]]\n\tlu.assertEquals(self.samSite:getNatoName(), \"CSA-4\")\n\tlu.assertEquals(self.samSite:getLaunchers()[1]:getRange(), 12000)\n\tlu.assertEquals(mist.utils.round(self.samSite:getRadars()[1]:getMaxRangeFindingTarget()), 12613)\nend\n\nfunction TestSkynetIADSREDSAMSitesAndEWRadars:testSA5()\n\tself.samSiteName = \"SAM-SA-5\"\n\tself:setUp()\n\tlu.assertEquals(self.samSite:getNatoName(), \"SA-5\")\n\tlocal searchRadar = self.samSite:getSearchRadars()[1]\n\tlu.assertEquals(searchRadar:getMaxRangeFindingTarget(), 100311.046875)\n\tlocal trackingRadar = self.samSite:getTrackingRadars()[1]\n\tlu.assertEquals(trackingRadar:getMaxRangeFindingTarget(), 100311.046875)\n\tlu.assertEquals(self.samSite:getLaunchers()[1]:getRange(), 240000)\n\tlu.assertEquals(self.samSite:getLaunchers()[1]:getInitialNumberOfMissiles(), 1)\nend\n\nfunction TestSkynetIADSREDSAMSitesAndEWRadars:testSA5P19()\n\tself.samSiteName = \"SAM-SA-5-p-19\"\n\tself:setUp()\n\tlu.assertEquals(self.samSite:getNatoName(), \"SA-5\")\n\tlocal searchRadar = self.samSite:getSearchRadars()[1]\n\tlu.assertEquals(searchRadar:getMaxRangeFindingTarget(), 53499.2265625)\n\tlocal trackingRadar = self.samSite:getTrackingRadars()[1]\n\tlu.assertEquals(trackingRadar:getMaxRangeFindingTarget(), 53499.2265625)\n\tlu.assertEquals(self.samSite:getLaunchers()[1]:getRange(), 240000)\n\tlu.assertEquals(self.samSite:getLaunchers()[1]:getInitialNumberOfMissiles(), 1)\nend\n\nfunction TestSkynetIADSREDSAMSitesAndEWRadars:test1L13EWRBoxSpring()\n\tself.ewRadarName = \"EW-west2\"\n\tself:setUp()\n\tlu.assertEquals(self.ewRadar:getNatoName(), \"Box Spring\")\n\tlu.assertEquals(self.ewRadar:hasWorkingRadar(), true)\nend\n\nfunction TestSkynetIADSREDSAMSitesAndEWRadars:testCP9S80M1SborkaDogEar()\n\tself.ewRadarName = \"EW-Dog Ear\"\n\tself:setUp()\n\tlu.assertEquals(self.ewRadar:getNatoName(), \"Dog Ear\")\n\tlu.assertEquals(self.ewRadar:hasWorkingRadar(), true)\nend\n\nfunction TestSkynetIADSREDSAMSitesAndEWRadars:test55G6EWRTalRack()\n\tself.ewRadarName = \"EW-west8\"\n\tself:setUp()\n\tlu.assertEquals(self.ewRadar:getNatoName(), \"Tall Rack\")\n\tlu.assertEquals(self.ewRadar:hasWorkingRadar(), true)\nend\n\nfunction TestSkynetIADSREDSAMSitesAndEWRadars:testEWP19FlatFace()\n\tself.ewRadarName = \"EW-SR-P19\"\n\tself:setUp()\n\tlu.assertEquals(self.ewRadar:getNatoName(), \"Flat Face\")\n\tlu.assertEquals(self.ewRadar:hasWorkingRadar(), true)\n\tlu.assertEquals(self.ewRadar:getHARMDetectionChance(), 30)\nend\n\nfunction TestSkynetIADSREDSAMSitesAndEWRadars:testSA10BigBird()\n\tself.ewRadarName = \"EW-SA-10\"\n\tself:setUp()\n\tlu.assertEquals(self.ewRadar:getNatoName(), \"Big Bird\")\n\tlu.assertEquals(self.ewRadar:hasWorkingRadar(), true)\nend\n\nfunction TestSkynetIADSREDSAMSitesAndEWRadars:testSA10ClamShell()\n\tself.ewRadarName = \"EW-SA-10-2\"\n\tself:setUp()\n\tlu.assertEquals(self.ewRadar:getNatoName(), \"Clam Shell\")\n\tlu.assertEquals(self.ewRadar:hasWorkingRadar(), true)\nend\n\nfunction TestSkynetIADSREDSAMSitesAndEWRadars:testSA11SnowDrift()\n\tself.ewRadarName = \"EW-SA-11\"\n\tself:setUp()\n\tlu.assertEquals(self.ewRadar:getNatoName(), \"Snow Drift\")\n\tlu.assertEquals(self.ewRadar:hasWorkingRadar(), true)\nend\n\nfunction TestSkynetIADSREDSAMSitesAndEWRadars:testSA6StraightFlush()\n\tself.ewRadarName = \"EW-SA-6\"\n\tself:setUp()\n\tlu.assertEquals(self.ewRadar:getNatoName(), \"Straight Flush\")\n\tlu.assertEquals(self.ewRadar:hasWorkingRadar(), true)\nend\n\nfunction TestSkynetIADSREDSAMSitesAndEWRadars:testHQ7()\n\tself.ewRadarName = \"EW-HQ7-STR\"\n\tself:setUp()\n\tlu.assertEquals(self.ewRadar:getNatoName(), \"CSA-4\")\n\tlu.assertEquals(self.ewRadar:hasWorkingRadar(), true)\nend\n\n\nfunction TestSkynetIADSREDSAMSitesAndEWRadars:testA50AWACSAsEWRadar()\n--[[\n\tDCS A-50 properties:\n\t\n\tRadar:\n\t    {\n        {\n            detectionDistanceAir={\n                lowerHemisphere={headOn=204461.796875, tailOn=204461.796875},\n                upperHemisphere={headOn=204461.796875, tailOn=204461.796875}\n            },\n            detectionDistanceRBM=2500,\n            type=1,\n            typeName=\"Shmel\"\n        }\n    },\n    3={{type=3, typeName=\"Abstract RWR\"}}\n}\n\n--]]\n\tself.ewRadarName = \"EW-AWACS-A-50\"\n\tself:setUp()\n\tlocal unit = Unit.getByName(self.ewRadarName)\n\tlu.assertEquals(unit:getDesc().category, Unit.Category.AIRPLANE)\n\tlu.assertEquals(self.ewRadar:getNatoName(), 'A-50')\n\tlocal searchRadar = self.ewRadar:getSearchRadars()[1]\n\tlu.assertEquals(searchRadar:getMaxRangeFindingTarget(), 204461.796875)\nend\n\nfunction TestSkynetIADSREDSAMSitesAndEWRadars:testKJ2000AWACSAsEWRadar()\n--[[\n\tDCS KJ-2000 properties:\n\t\n\tRadar:\n\t{\n\t\t{\n\t\t\t{\n\t\t\t\tdetectionDistanceAir={\n\t\t\t\t\tlowerHemisphere={headOn=268356.125, tailOn=268356.125},\n\t\t\t\t\tupperHemisphere={headOn=268356.125, tailOn=268356.125}\n\t\t\t\t},\n\t\t\t\tdetectionDistanceRBM=3500,\n\t\t\t\ttype=1,\n\t\t\t\ttypeName=\"AESA_KJ2000\"\n\t\t\t}\n\t\t},\n\t\t3={{type=3, typeName=\"Abstract RWR\"}}\n\t}\n\n--]]\n\tself.ewRadarName = \"EW-AWACS-KJ-2000\"\n\tself:setUp()\n\tlocal unit = Unit.getByName('EW-AWACS-KJ-2000')\n\tlocal searchRadar = self.ewRadar:getSearchRadars()[1]\n\tlu.assertEquals(self.ewRadar:getNatoName(), 'KJ-2000')\n\tlu.assertEquals(searchRadar:getMaxRangeFindingTarget(), 268356.125)\nend\n\nend\n"
  },
  {
    "path": "unit-tests/test-skynet-iads-sam-site.lua",
    "content": "do\n\nTestSkynetIADSSAMSite = {}\n\nfunction TestSkynetIADSSAMSite:setUp()\n\tself.skynetIADS = SkynetIADS:create()\n\tif self.samSiteName then\n\t\tlocal samSite = Group.getByName(self.samSiteName)\n\t\tself.samSite = SkynetIADSSamSite:create(samSite, self.skynetIADS)\n\t\t\n\t\t-- we overrite this method since it returns radar contacts in the DCS world which mess up the tests.\n\t\tfunction self.samSite:getDetectedTargets()\n\t\t\treturn {}\n\t\tend\n\t\t\n\t\tself.samSite:setupElements()\n\t\tself.samSite:goLive()\n\tend\nend\n\nfunction TestSkynetIADSSAMSite:tearDown()\n\tif self.samSite then\t\n\t\tself.samSite:goDark()\n\t\tself.samSite:cleanUp()\n\tend\n\tif self.skynetIADS then\n\t\tself.skynetIADS:deactivate()\n\tend\n\tself.samSite = nil\n\tself.samSiteName = nil\nend\n\n\nfunction TestSkynetIADSSAMSite:testCompleteDestructionOfSamSiteAndLoadDestroyedSAMSiteInToIADS()\n\n\tlocal samSite = SkynetIADSSamSite:create(Group.getByName(\"Destruction-test-sam\"), self.skynetIADS):setActAsEW(true)\n\tsamSite:setupElements()\n\n\tlocal samSite2 = SkynetIADSSamSite:create(Group.getByName('prefixtest-sam'), self.skynetIADS)\n\tsamSite2:setupElements()\n\t\n\tsamSite:addChildRadar(samSite2)\n\tsamSite2:addParentRadar(samSite)\n\t\n\tlu.assertEquals(samSite2:getAutonomousState(), false)\n\tlu.assertEquals(samSite:isDestroyed(), false)\n\tlu.assertEquals(samSite:hasWorkingRadar(), true)\n\n\tlocal radars = samSite:getRadars()\n\tfor i = 1, #radars do\n\t\tlocal radar = radars[i]\n\t\ttrigger.action.explosion(radar:getDCSRepresentation():getPosition().p, 500)\n\t\t--we simulate a call to the event, since in game will be triggered to late to for later checks in this unit test\n\t\tsamSite:onEvent(createDeadEvent())\n\tend\t\n\tlocal launchers = samSite:getLaunchers()\n\tfor i = 1, #launchers do\n\t\tlocal launcher = launchers[i]\n\t\ttrigger.action.explosion(launcher:getDCSRepresentation():getPosition().p, 900)\n\t\t--we simulate a call to the event, since in game will be triggered to late to for later checks in this unit test\n\t\tsamSite:onEvent(createDeadEvent())\n\tend\t\n\tlu.assertEquals(samSite:isActive(), false)\n\tlu.assertEquals(samSite:isDestroyed(), true)\n\tlu.assertEquals(samSite:hasWorkingRadar(), false)\n\n\tlu.assertEquals(samSite:getRemainingNumberOfMissiles(), 0)\n\tlu.assertEquals(samSite:getInitialNumberOfMissiles(), 6)\n\tlu.assertEquals(samSite:hasRemainingAmmo(), false)\n\t\n\t--after destruction of samSite acting as EW samSite2 must be autonomous:\n\tlu.assertEquals(samSite2:getAutonomousState(), true)\n\t\n\t--test build SAM with destroyed elements\n\tsamSite:cleanUp()\n\tlocal samSite = SkynetIADSSamSite:create(Group.getByName(\"Destruction-test-sam\"), self.skynetIADS)\n\tsamSite:setupElements()\n\tlu.assertEquals(samSite:getNatoName(), \"UNKNOWN\")\n\tlu.assertEquals(#samSite:getRadars(), 0)\n\tlu.assertEquals(#samSite:getLaunchers(), 0)\n\t\n\tsamSite:cleanUp()\n\tsamSite2:cleanUp()\nend\t\n\nfunction TestSkynetIADSSAMSite:testInformOfContactInRange()\n\tself.samSiteName = \"SAM-SA-6\"\n\tself:setUp()\n\tlocal mockContact = {}\n\tfunction mockContact:isIdentifiedAsHARM()\n\t\treturn false\n\tend\n\tfunction self.samSite:isTargetInRange(target)\n\t\tlu.assertIs(target, mockContact)\n\t\treturn true\n\tend\n\tself.samSite:goDark()\n\tself.samSite:targetCycleUpdateStart()\n\tlu.assertEquals(self.samSite:isActive(), false)\n\tself.samSite:informOfContact(mockContact)\n\tlu.assertEquals(self.samSite:isActive(), true)\n\tself.samSite:targetCycleUpdateEnd()\n\tlu.assertEquals(self.samSite:isActive(), true)\nend\n\nfunction TestSkynetIADSSAMSite:testInformOfContactNotInRange()\n\tself.samSiteName = \"SAM-SA-6\"\n\tself:setUp()\n\tlocal mockContact = {}\n\tfunction self.samSite:isTargetInRange(target)\n\t\tlu.assertIs(target, mockContact)\n\t\treturn false\n\tend\n\tself.samSite:goDark()\n\tself.samSite:targetCycleUpdateStart()\n\tlu.assertEquals(self.samSite:isActive(), false)\n\tself.samSite:informOfContact(mockContact)\n\tlu.assertEquals(self.samSite:isActive(), false)\n\tself.samSite:targetCycleUpdateEnd()\n\tlu.assertEquals(self.samSite:isActive(), false)\nend\n\nfunction TestSkynetIADSSAMSite:testInformOfHARMContactSAMCanEngageHARM()\n\tself.samSiteName = \"test-SAM-SA-2-test\"\n\tself:setUp()\n\tfunction self.samSite:isTargetInRange(contact)\n\t\treturn true\n\tend\n\tlocal mockTarget = {}\n\tfunction mockTarget:isIdentifiedAsHARM()\n\t\treturn true\n\tend\n\tself.samSite:goDark()\n\tlu.assertEquals(self.samSite:isActive(), false)\n\tself.samSite:setCanEngageHARM(true)\n\tself.samSite:informOfContact(mockTarget)\n\tlu.assertEquals(self.samSite:isActive(), true)\n\t\nend\n\nfunction TestSkynetIADSSAMSite:testInformOfHARMContactSAMCanNotEngageHARM()\n\tself.samSiteName = \"test-SAM-SA-2-test\"\n\tself:setUp()\n\tfunction self.samSite:isTargetInRange(contact)\n\t\treturn true\n\tend\n\tlocal mockTarget = {}\n\tfunction mockTarget:isIdentifiedAsHARM()\n\t\treturn true\n\tend\n\tself.samSite:goDark()\n\tlu.assertEquals(self.samSite:isActive(), false)\n\tself.samSite:setCanEngageHARM(false)\n\tself.samSite:informOfContact(mockTarget)\n\tlu.assertEquals(self.samSite:isActive(), false)\n\t\nend\n\nfunction TestSkynetIADSSAMSite:testSA2InformOfContactTargetNotInRange()\n\tself.samSiteName = \"test-SAM-SA-2-test\"\n\tself:setUp()\n\tself.samSite:goDark()\n\tlocal target = IADSContactFactory('test-not-in-firing-range-of-sa-2')\n\tself.samSite:informOfContact(target)\n\tlu.assertEquals(self.samSite:isTargetInRange(target), false)\n\tlu.assertEquals(self.samSite:isActive(), false)\nend\n\nfunction TestSkynetIADSSAMSite:testSA2InforOfContactInSearchRangeSAMSiteGoLiveWhenSetToSearchRange()\n\tself.samSiteName = \"test-SAM-SA-2-test\"\n\tself:setUp()\n\tself.samSite:goDark()\n\tlu.assertEquals(self.samSite:isActive(), false)\n\tself.samSite:setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE)\n\tlu.assertIs(self.samSite:getEngagementZone(), SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE)\n\tlocal target = IADSContactFactory('test-not-in-firing-range-of-sa-2')\n\tself.samSite:informOfContact(target)\n\tlu.assertEquals(self.samSite:isActive(), true)\n\nend\n\nfunction TestSkynetIADSSAMSite:testInformOfContactMultipleTimesOnlyOneIsTargetInRangeCall()\n\tself.samSiteName = \"SAM-SA-6\"\n\tself:setUp()\n\t\n\tlocal mockContact = {}\n\tfunction mockContact:isIdentifiedAsHARM()\n\t\treturn false\n\tend\n\tlocal numTimesCalledTargetInRange = 0\n\t\n\tfunction self.samSite:isTargetInRange(target)\n\t\tnumTimesCalledTargetInRange = numTimesCalledTargetInRange + 1\n\t\tlu.assertIs(target, mockContact)\n\t\treturn true\n\tend\n\tself.samSite:targetCycleUpdateStart()\n\tself.samSite:informOfContact(mockContact)\n\tself.samSite:informOfContact(mockContact)\n\tlu.assertEquals(numTimesCalledTargetInRange, 1)\nend\n\nfunction TestSkynetIADSSAMSite:testSAMStaysActiveWhenInAutonomousMode()\n\tself.samSiteName = \"test-SAM-SA-2-test\"\n\tself:setUp()\n\tlu.assertEquals(self.samSite:isActive(), true)\n\tlu.assertEquals(self.samSite:getAutonomousState(), true)\n\tself.samSite:targetCycleUpdateEnd()\n\tlu.assertEquals(self.samSite:isActive(), true)\nend\n\nfunction TestSkynetIADSSAMSite:testGoLiveConstraint()\n\tself.samSiteName = \"SAM-SA-2\"\n\tself:setUp()\n\tlocal contact = IADSContactFactory('test-in-firing-range-of-sa-2')\n\t\n\tlocal function goLiveConstraint(contact)\n\t\treturn ( contact:getHeightInFeetMSL() > 4000 )\n\tend\n\n\tlu.assertEquals(goLiveConstraint(contact), true)\n\t\n\tlu.assertEquals(self.samSite:areGoLiveConstraintsSatisfied(contact), true)\n\tself.samSite:addGoLiveConstraint('helicopter', goLiveConstraint)\n\tlu.assertEquals(self.samSite:areGoLiveConstraintsSatisfied(contact), true)\n\t\n\t--TODO: finish test to check return false if constraint is false\n\n\tlocal function goLiveConstraintFalse(contact)\n\t\treturn ( contact:getHeightInFeetMSL() < 4000 )\n\tend\n\t\n\tself.samSite:addGoLiveConstraint('helicopter', goLiveConstraintFalse)\n\tlu.assertEquals(self.samSite:areGoLiveConstraintsSatisfied(contact), false)\n\nend\n\nfunction TestSkynetIADSSAMSite:testRemoveGoLiveConstraint()\n\tself.samSiteName = \"SAM-SA-2\"\n\tself:setUp()\n\tself.samSite:addGoLiveConstraint(\"constraint\", {})\n\t\n\t--this marker funtion is to test if after removing the first function this one will still exist\n\tfunction testMarkerFunction(contact)\n\t\treturn 3\n\tend\n\t\n\tself.samSite:addGoLiveConstraint(\"test\", testMarkerFunction)\n\t\n\tlocal count = 0\n\tfor constraintName, constraint in pairs(self.samSite:getGoLiveConstraints()) do\n\t\tcount = count + 1\n\tend\n\tlu.assertEquals(count, 2)\n\t\n\tcount = 0\n\tself.samSite:removeGoLiveConstraint(\"constraint\")\n\tfor constraintName, constraint in pairs(self.samSite:getGoLiveConstraints()) do\n\t\tcount = count + 1\n\tend\n\tlu.assertEquals(count, 1)\n\t\n\tlu.assertEquals(self.samSite:getGoLiveConstraints()[\"test\"](contact), 3)\n\t\n\t\nend\n\nfunction TestSkynetIADSSAMSite:testSAMSiteWillNotGoLiveIfConstraintFailesAndContactIsInRange()\n\tself.samSiteName = \"SAM-SA-2\"\n\tself:setUp()\n\tlocal contact = IADSContactFactory('test-in-firing-range-of-sa-2')\n\t\n\tlocal function goLiveConstraintFalse(contact)\n\t\treturn ( contact:getHeightInFeetMSL() < 4000 )\n\tend\n\n\tself.samSite:addGoLiveConstraint('helicopter', goLiveConstraintFalse)\n\tself.samSite:goDark()\n\tself.samSite:targetCycleUpdateStart()\n\tself.samSite:informOfContact(contact)\n\tlu.assertEquals(self.samSite:isActive(), false)\nend\n\nend"
  },
  {
    "path": "unit-tests/test-skynet-iads.lua",
    "content": "do\nTestSkynetIADS = {}\n\nfunction TestSkynetIADS:setUp()\n\tself.numSAMSites = SKYNET_UNIT_TESTS_NUM_SAM_SITES_RED \n\tself.numEWSites = SKYNET_UNIT_TESTS_NUM_EW_SITES_RED\n\tself.testIADS = SkynetIADS:create()\n\tself.testIADS:addEarlyWarningRadarsByPrefix('EW')\n\tself.testIADS:addSAMSitesByPrefix('SAM')\nend\n\nfunction TestSkynetIADS:tearDown()\n\tif\tself.testIADS then\n\t\tself.testIADS:deactivate()\n\tend\n\tself.testIADS = nil\nend\n\n-- this function checks constants in DCS that the IADS relies on. A change to them might indicate that functionallity is broken.\n-- In the code constants are refereed to with their constant name calue, not the values the represent.\nfunction TestSkynetIADS:testDCSContstantsHaveNotChanged()\n\tlu.assertEquals(Weapon.Category.MISSILE, 1)\n\tlu.assertEquals(Weapon.Category.SHELL, 0)\n\tlu.assertEquals(world.event.S_EVENT_SHOT, 1)\n\tlu.assertEquals(world.event.S_EVENT_DEAD, 8)\n\tlu.assertEquals(Unit.Category.AIRPLANE, 0)\nend\n\nfunction TestSkynetIADS:testCaclulateNumberOfSamSitesAndEWRadars()\n\tself:tearDown()\n\tself.testIADS = SkynetIADS:create()\n\tlu.assertEquals(#self.testIADS:getSAMSites(), 0)\n\tlu.assertEquals(#self.testIADS:getEarlyWarningRadars(), 0)\n\tself.testIADS:addEarlyWarningRadarsByPrefix('EW')\n\tself.testIADS:addSAMSitesByPrefix('SAM')\n\tlu.assertEquals(#self.testIADS:getSAMSites(), self.numSAMSites)\n\tlu.assertEquals(#self.testIADS:getEarlyWarningRadars(), self.numEWSites)\nend\n\nfunction TestSkynetIADS:testCaclulateNumberOfSamSitesAndEWRadarsWhenAddMethodsCalledTwice()\n\tself:tearDown()\n\tself.testIADS = SkynetIADS:create()\n\tlu.assertEquals(#self.testIADS:getSAMSites(), 0)\n\tlu.assertEquals(#self.testIADS:getEarlyWarningRadars(), 0)\n\tself.testIADS:addEarlyWarningRadarsByPrefix('EW')\n\tself.testIADS:addEarlyWarningRadarsByPrefix('EW')\n\tself.testIADS:addSAMSitesByPrefix('SAM')\n\tself.testIADS:addSAMSitesByPrefix('SAM')\n\tlu.assertEquals(#self.testIADS:getSAMSites(), self.numSAMSites)\n\tlu.assertEquals(#self.testIADS:getEarlyWarningRadars(), self.numEWSites)\nend\n\nfunction TestSkynetIADS:testWrongCaseStringWillNotLoadSAMGroup()\n\tself:tearDown()\n\tself.testIADS = SkynetIADS:create()\n\tself.testIADS:addSAMSitesByPrefix('sam')\n\tlu.assertEquals(#self.testIADS:getSAMSites(), 0)\nend\t\n\nfunction TestSkynetIADS:testWrongCaseStringWillNotLoadEWRadars()\n\tself:tearDown()\n\tself.testIADS = SkynetIADS:create()\n\tself.testIADS:addEarlyWarningRadarsByPrefix('ew')\n\tlu.assertEquals(#self.testIADS:getEarlyWarningRadars(), 0)\nend\t\n\nfunction TestSkynetIADS:testEvaluateContacts1EWAnd1SAMSiteWithContactInRange()\n\tself:tearDown()\n\tlocal iads = SkynetIADS:create()\n\tlocal ewRadar = iads:addEarlyWarningRadar('EW-west23')\n\t\n\tfunction ewRadar:getDetectedTargets()\n\t\treturn {IADSContactFactory('test-in-firing-range-of-sa-2')}\n\tend\n\t\n\tlocal samSite = iads:addSAMSite('SAM-SA-2')\n\t\n\t\n\tfunction samSite:getDetectedTargets()\n\t\treturn {}\n\tend\n\t\n\tsamSite:goDark()\n\tlu.assertEquals(samSite:isInRadarDetectionRangeOf(ewRadar), true)\n\tiads:activate()\n\tiads:evaluateContacts()\n\tlu.assertEquals(#iads:getContacts(), 1)\n\tlu.assertEquals(samSite:isActive(), true)\n\t\n\t-- we remove the target to test if the sam site will now go dark, was added for the performance optimised code\n\tfunction ewRadar:getDetectedTargets()\n\t\treturn {}\n\tend\n\tiads:evaluateContacts()\n\tlu.assertEquals(samSite:isActive(), false)\n\tiads:deactivate()\nend\n\nfunction TestSkynetIADS:testEarlyWarningRadarHasWorkingPowerSourceByDefault()\n\tlocal ewRadar = self.testIADS:getEarlyWarningRadarByUnitName('EW-west')\n\tlu.assertEquals(ewRadar:hasWorkingPowerSource(), true)\nend\n\nfunction TestSkynetIADS:testAWACSHasMovedAndThereforeRebuildAutonomousStatesOfSAMSites()\n\n\tlocal iads = SkynetIADS:create()\n\tlocal awacs = iads:addEarlyWarningRadar('EW-AWACS-A-50')\n\n\tlocal updateCalls = 0\n\tfunction iads:buildRadarCoverageForEarlyWarningRadar(ewRadar)\n\t\tSkynetIADS.buildRadarCoverageForEarlyWarningRadar(self, ewRadar)\n\t\tupdateCalls = updateCalls + 1\n\tend\n\t\n\tlu.assertEquals(awacs:getDistanceTraveledSinceLastUpdate(), 0)\n\tlu.assertEquals(getmetatable(awacs), SkynetIADSAWACSRadar)\n\tlu.assertEquals(awacs:getMaxAllowedMovementForAutonomousUpdateInNM(), 10)\n\tlu.assertEquals(awacs:isUpdateOfAutonomousStateOfSAMSitesRequired(), false)\n\t\n\tiads:evaluateContacts()\n\tlu.assertEquals(updateCalls, 0)\n\t\n\t--test distance calculation by giving the awacs a different position:\n\tlocal firstPos = Unit.getByName('EW-AWACS-KJ-2000'):getPosition().p\n\tawacs.lastUpdatePosition = firstPos\n\t\n\tlu.assertEquals(awacs:getDistanceTraveledSinceLastUpdate(), 763)\n\tlu.assertEquals(awacs:isUpdateOfAutonomousStateOfSAMSitesRequired(), true)\n\t\n\t-- a second imediate call shall result in false\n\tlu.assertEquals(awacs:getDistanceTraveledSinceLastUpdate(), 0)\n\tlu.assertEquals(awacs:isUpdateOfAutonomousStateOfSAMSitesRequired(), false)\n\t\n\t--we reset lastUpdatePosition to firstPos to test call in the IADS code\n\t-- TODO: when refactoring move this test to te AWACS Radar and use mock objects for integration tests in the IADS\n\tawacs.lastUpdatePosition = firstPos\n\tiads:evaluateContacts()\n\tlu.assertEquals(updateCalls, 1)\n\tiads:deactivate()\nend\n\nfunction TestSkynetIADS:testSAMSiteLoosesPower()\n\tlocal powerSource = StaticObject.getByName('SA-6 Power')\n\tlocal samSite = self.testIADS:getSAMSiteByGroupName('SAM-SA-6'):addPowerSource(powerSource)\n\tlu.assertEquals(#self.testIADS:getUsableSAMSites(), self.numSAMSites)\n\tsamSite:goLive()\n\tlu.assertEquals(samSite:isActive(), true)\n\ttrigger.action.explosion(powerSource:getPosition().p, 100)\n\t--we simulate a call to the event, since in game will be triggered to late to for later checks in this unit test\n\tsamSite:onEvent(createDeadEvent())\n\tlu.assertEquals(#self.testIADS:getUsableSAMSites(), self.numSAMSites-1)\n\tlu.assertEquals(samSite:isActive(), false)\nend\n\nfunction TestSkynetIADS:testSAMSiteSA6LostConnectionNodeAutonomusStateDCSAI()\n\tlocal sa6ConnectionNode = StaticObject.getByName('SA-6 Connection Node')\n\tself.testIADS:getSAMSiteByGroupName('SAM-SA-6'):addConnectionNode(sa6ConnectionNode)\n\t\n\tlu.assertEquals(#self.testIADS:getSAMSites(), self.numSAMSites)\n\tlu.assertEquals(#self.testIADS:getUsableSAMSites(), self.numSAMSites)\n\t\n\ttrigger.action.explosion(sa6ConnectionNode:getPosition().p, 100)\n\tlu.assertEquals(#self.testIADS:getUsableSAMSites(), self.numSAMSites-1)\n\n\tlu.assertEquals(#self.testIADS:getUsableSAMSites(), self.numSAMSites-1)\n\tlu.assertEquals(#self.testIADS:getSAMSites(), self.numSAMSites)\n\t\n\tlocal samSite = self.testIADS:getSAMSiteByGroupName('SAM-SA-6')\n\tlu.assertEquals(samSite:isActive(), true)\n\n\tlu.assertEquals(samSite:getAutonomousState(), true)\n\tlu.assertEquals(samSite:isActive(), true)\nend\n\nfunction TestSkynetIADS:testAddRadarsToCommandCenter()\n\tlocal comCenter = StaticObject.getByName('command-center-3')\n\tself.testIADS:addCommandCenter(comCenter)\n\tlocal comC = self.testIADS:getCommandCenters()[1]\n\tlocal called = false\n\tfunction comC:clearChildRadars()\n\t\tcalled = true\n\tend\n\t--as long as IADS is not active addCommandCenter will not trigger addRadarsToCommandCenters when called:\n\tself.testIADS:addRadarsToCommandCenters()\n\tlu.assertEquals(called, true)\n\tlu.assertEquals(#comC:getChildRadars(), (self.numEWSites + self.numSAMSites))\nend\n\nfunction TestSkynetIADS:testAddCommandCenter()\n\tlocal called = false\n\tfunction self.testIADS:addRadarsToCommandCenters()\n\t\tcalled = true\n\tend\n\t\n\tlocal comCenter = StaticObject.getByName('command-center-3')\n\tself.testIADS:addCommandCenter(comCenter)\n\tlu.assertEquals(called, false)\n\t\n\tself.testIADS:activate()\n\tself.testIADS:addCommandCenter(comCenter)\n\tlu.assertEquals(called, true)\n\tself.testIADS:deactivate()\nend\n\nfunction TestSkynetIADS:testOneCommandCenterHasNoConnectionNode()\n\tlocal commandCenter2 = StaticObject.getByName(\"Command Center2\")\n\tlocal commandCenter2ConnectionNode = StaticObject.getByName(\"command-center-2-connection-node\")\n\tlocal comCenter = self.testIADS:addCommandCenter(commandCenter2):addConnectionNode(commandCenter2ConnectionNode)\n\tlu.assertEquals(#comCenter:getConnectionNodes(), 1)\n\tlu.assertEquals(self.testIADS:isCommandCenterUsable(), true)\n\t\n\tlocal samSites = self.testIADS:getSAMSites()\n\tlu.assertEquals(#samSites, SKYNET_UNIT_TESTS_NUM_SAM_SITES_RED)\n\t\n\tlocal ewRadars = self.testIADS:getEarlyWarningRadars()\n\tlu.assertEquals(#ewRadars, SKYNET_UNIT_TESTS_NUM_EW_SITES_RED)\n\t\n\tself.testIADS:activate()\n\n\ttrigger.action.explosion(commandCenter2ConnectionNode:getPosition().p, 500)\n\t--we simulate a call to the event, since in game will be triggered to late to for later checks in this unit test\n\tcomCenter:onEvent(createDeadEvent())\n\tlu.assertEquals(self.testIADS:isCommandCenterUsable(), false)\n\n\t\n\t--after the command center is no longer reachable we check to see if all SAM and EW radars are in their expected autonomous state:\n\tfor i = 1, #samSites do\n\t\tlocal sam = samSites[i]\n\t\tlu.assertEquals(sam:getAutonomousState(), true)\n\tend\n\t\n\t\n\tfor i = 1, #ewRadars do\n\t\tlocal ewRad = ewRadars[i]\n\t\tlu.assertEquals(ewRad:getAutonomousState(), true)\n\tend\n\t\nend\n\nfunction TestSkynetIADS:testOneCommandCenterLoosesPower()\n\tlocal commandCenter2Power = StaticObject.getByName(\"Command Center2 Power\")\n\tlocal commandCenter2 = StaticObject.getByName(\"Command Center2\")\n\tlu.assertEquals(#self.testIADS:getCommandCenters(), 0)\n\tlu.assertEquals(self.testIADS:isCommandCenterUsable(), true)\n\tlocal comCenter = self.testIADS:addCommandCenter(commandCenter2):addPowerSource(commandCenter2Power)\n\tlu.assertEquals(#comCenter:getPowerSources(), 1)\n\tlu.assertEquals(#self.testIADS:getCommandCenters(), 1)\n\tlu.assertEquals(self.testIADS:isCommandCenterUsable(), true)\n\ttrigger.action.explosion(commandCenter2Power:getPosition().p, 10000)\n\tlu.assertEquals(#self.testIADS:getCommandCenters(), 1)\n\tlu.assertEquals(self.testIADS:isCommandCenterUsable(), false)\nend\n\n\nfunction TestSkynetIADS:testOneCommandCenterIsDestroyed()\n\tlocal commandCenter1 = StaticObject.getByName(\"Command Center\")\t\n\tlu.assertEquals(#self.testIADS:getCommandCenters(), 0)\n\tself.testIADS:addCommandCenter(commandCenter1)\n\tlu.assertEquals(#self.testIADS:getCommandCenters(), 1)\n\tlu.assertEquals(self.testIADS:isCommandCenterUsable(), true)\n\ttrigger.action.explosion(commandCenter1:getPosition().p, 10000)\n\tlu.assertEquals(#self.testIADS:getCommandCenters(), 1)\n\tlu.assertEquals(self.testIADS:isCommandCenterUsable(), false)\nend\n\nfunction TestSkynetIADS:testSetOptionsForSAMSiteType()\n\tlocal powerSource = StaticObject.getByName('SA-11-power-source')\n\tlocal connectionNode = StaticObject.getByName('SA-11-connection-node')\n\tlu.assertEquals(#self.testIADS:getSAMSitesByNatoName('SA-6'), 2)\n\t--lu.assertIs(getmetatable(self.testIADS:getSAMSitesByNatoName('SA-6')), SkynetIADSTableForwarder)\n\tlocal samSites = self.testIADS:getSAMSitesByNatoName('SA-6'):setActAsEW(true):addPowerSource(powerSource):addConnectionNode(connectionNode):setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE):setGoLiveRangeInPercent(90):setAutonomousBehaviour(SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK)\n\tlu.assertEquals(#samSites, 2)\n\tfor i = 1, #samSites do\n\t\tlocal samSite = samSites[i]\n\t\tlu.assertEquals(samSite:getActAsEW(), true)\n\t\tlu.assertEquals(samSite:getEngagementZone(), SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE)\n\t\tlu.assertEquals(samSite:getGoLiveRangeInPercent(), 90)\n\t\tlu.assertEquals(samSite:getAutonomousBehaviour(), SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK)\n\t\tlu.assertIs(samSite:getConnectionNodes()[1], connectionNode)\n\t\tlu.assertIs(samSite:getPowerSources()[1], powerSource)\n\tend\nend\n\nfunction TestSkynetIADS:testSetOptionsForAllAddedSamSitesByPrefix()\n\tself:tearDown()\n\tself.testIADS = SkynetIADS:create()\n\tlocal samSites = self.testIADS:addSAMSitesByPrefix('SAM'):setActAsEW(true):addPowerSource(powerSource):addConnectionNode(connectionNode):setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE):setGoLiveRangeInPercent(90):setAutonomousBehaviour(SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK)\n\tlu.assertEquals(#samSites, self.numSAMSites)\n\tfor i = 1, #samSites do\n\t\tlocal samSite = samSites[i]\n\t\tlu.assertEquals(samSite:getActAsEW(), true)\n\t\tlu.assertEquals(samSite:getEngagementZone(), SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE)\n\t\tlu.assertEquals(samSite:getGoLiveRangeInPercent(), 90)\n\t\tlu.assertEquals(samSite:getAutonomousBehaviour(), SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK)\n\t\tlu.assertIs(samSite:getConnectionNodes()[1], connectionNode)\n\t\tlu.assertIs(samSite:getPowerSources()[1], powerSource)\n\tend\nend\n\nfunction TestSkynetIADS:testSetOptionsForAllAddedSAMSites()\n\tlocal samSites = self.testIADS:getSAMSites():setActAsEW(true):addPowerSource(powerSource):addConnectionNode(connectionNode):setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE):setGoLiveRangeInPercent(90):setAutonomousBehaviour(SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK)\n\tlu.assertEquals(#samSites, self.numSAMSites)\n\tfor i = 1, #samSites do\n\t\tlocal samSite = samSites[i]\n\t\tlu.assertEquals(samSite:getActAsEW(), true)\n\t\tlu.assertEquals(samSite:getEngagementZone(), SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE)\n\t\tlu.assertEquals(samSite:getGoLiveRangeInPercent(), 90)\n\t\tlu.assertEquals(samSite:getAutonomousBehaviour(), SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK)\n\t\tlu.assertIs(samSite:getConnectionNodes()[1], connectionNode)\n\t\tlu.assertIs(samSite:getPowerSources()[1], powerSource)\n\tend\nend\n\nfunction TestSkynetIADS:testSetOptionsForAllAddedEWSitesByPrefix()\n\tself:tearDown()\n\tself.testIADS = SkynetIADS:create()\n\tlocal ewSites = self.testIADS:addEarlyWarningRadarsByPrefix('EW'):addPowerSource(powerSource):addConnectionNode(connectionNode)\n\tlu.assertEquals(#ewSites, self.numEWSites)\n\tfor i = 1, #ewSites do\n\t\tlocal ewSite = ewSites[i]\n\t\tlu.assertIs(ewSite:getConnectionNodes()[1], connectionNode)\n\t\tlu.assertIs(ewSite:getPowerSources()[1], powerSource)\n\tend\n\t\nend\n\nfunction TestSkynetIADS:testSetOptionsForAllAddedEWSites()\n\tlocal ewSites = self.testIADS:getEarlyWarningRadars()\n\tlu.assertEquals(#ewSites, self.numEWSites)\n\tfor i = 1, #ewSites do\n\t\tlocal ewSite = ewSites[i]\n\t\tlu.assertIs(ewSite:getConnectionNodes()[1], connectionNode)\n\t\tlu.assertIs(ewSite:getPowerSources()[1], powerSource)\n\tend\nend\n\nfunction TestSkynetIADS:testMergeContacts()\n\tlu.assertEquals(#self.testIADS:getContacts(), 0)\n\tself.testIADS:mergeContact(IADSContactFactory('Harrier Pilot'))\n\tlu.assertEquals(#self.testIADS:getContacts(), 1)\n\t\n\tlocal contact = IADSContactFactory('Harrier Pilot')\n\tlocal mockRadar = {}\n\tfunction contact:getAbstractRadarElementsDetected()\n\t\treturn {mockRadar}\n\tend\n\tself.testIADS:mergeContact(contact)\n\tlu.assertEquals(#self.testIADS:getContacts(), 1)\n\tlocal iadsContact = self.testIADS:getContacts()[1]\n\tlu.assertEquals(#iadsContact:getAbstractRadarElementsDetected(), 1)\n\t\n\tself.testIADS:mergeContact(IADSContactFactory('test-in-firing-range-of-sa-2'))\n\tlu.assertEquals(#self.testIADS:getContacts(), 2)\n\t\nend\n\nfunction TestSkynetIADS:testCleanAgedTargets()\n\tlocal iads = SkynetIADS:create()\n\t\n\ttarget1 = IADSContactFactory('test-in-firing-range-of-sa-2')\n\tfunction target1:getAge()\n\t\treturn iads.maxTargetAge + 1\n\tend\n\t\n\ttarget2 = IADSContactFactory('test-distance-calculation')\n\tfunction target2:getAge()\n\t\treturn 1\n\tend\n\t\n\tiads.contacts[1] = target1\n\tiads.contacts[2] = target2\n\tlu.assertEquals(#iads:getContacts(), 2)\n\tiads:cleanAgedTargets()\n\tlu.assertEquals(#iads:getContacts(), 1)\n\tiads:deactivate()\nend\n\nfunction TestSkynetIADS:testOnlyLoadGroupsWithPrefixForSAMSiteNotOtherUnitsOrStaticObjectsWithSamePrefix()\n\tself:tearDown()\n\tself.testIADS = SkynetIADS:create()\n\tlocal calledPrint = false\n\tfunction self.testIADS:printOutput(str, isWarning)\n\t\tcalledPrint = true\n\tend\n\tself.testIADS:addSAMSitesByPrefix('prefixtest')\n\tlu.assertEquals(#self.testIADS:getSAMSites(), 1)\n\tlu.assertEquals(calledPrint, false)\nend\n\nfunction TestSkynetIADS:testOnlyLoadGroupsWithPrefixForSAMSiteNotOtherUnitsOrStaticObjectsWithSamePrefix2()\n\tself:tearDown()\n\tself.testIADS = SkynetIADS:create()\n\tlocal calledPrint = false\n\tfunction self.testIADS:printOutput(str, isWarning)\n\t\tcalledPrint = true\n\tend\n\t--happened when the string.find method was not set to plain special characters messed up the regex pattern\n\tself.testIADS:addSAMSitesByPrefix('IADS-EW')\n\tlu.assertEquals(#self.testIADS:getSAMSites(), 1)\n\tlu.assertEquals(calledPrint, false)\nend\n\nfunction TestSkynetIADS:testOnlyLoadUnitsWithPrefixForEWSiteNotStaticObjectssWithSamePrefix()\n\tself:tearDown()\n\tself.testIADS = SkynetIADS:create()\n\tlocal calledPrint = false\n\tfunction self.testIADS:printOutput(str, isWarning)\n\t\tcalledPrint = true\n\tend\n\tself.testIADS:addEarlyWarningRadarsByPrefix('prefixewtest')\n\tlu.assertEquals(#self.testIADS:getEarlyWarningRadars(), 1)\n\tlu.assertEquals(calledPrint, false)\nend\n\n--TODO rework this test for new evaluateContacts code:\n--[[\nfunction TestSkynetIADS:testDontPassShipsGroundUnitsAndStructuresToSAMSites()\n\t\n\t-- make sure we don't get any targets in the test mission\n\tlocal ewRadars = self.testIADS:getEarlyWarningRadars()\n\tfor i = 1, #ewRadars do\n\t\tlocal ewRadar = ewRadars[i]\n\t\tfunction ewRadar:getDetectedTargets()\n\t\t\treturn {}\n\t\tend\n\tend\n\t\n\t\n\tlocal samSites = self.testIADS:getSAMSites()\n\tfor i = 1, #samSites do\n\t\tlocal samSite = samSites[i]\n\t\tfunction samSite:getDetectedTargets()\n\t\t\treturn {}\n\t\tend\n\tend\n\t\n\n\tself.testIADS:evaluateContacts()\n\t-- verifies we have a clean test setup\n\tlu.assertEquals(#self.testIADS.contacts, 0)\n\t\n\n\t\n\t-- ground units should not be passed to the SAM\t\n\tlocal mockContactGroundUnit = {}\n\tfunction mockContactGroundUnit:getDesc()\n\t\treturn {category = Unit.Category.GROUND_UNIT}\n\tend\n\tfunction mockContactGroundUnit:getAge()\n\t\treturn 0\n\tend\n\t\n\t\n\ttable.insert(self.testIADS.contacts, mockContactGroundUnit)\n\t\n\tlocal correlatedCalled = false\n\tfunction self.testIADS:informOfContact(contact)\n\t\tcorrelatedCalled = true\n\tend\n\t\n\tself.testIADS:evaluateContacts()\n\tlu.assertEquals(correlatedCalled, false)\n\tlu.assertEquals(#self.testIADS.contacts, 1)\n\t\n\t\n\t\n\tself.testIADS.contacts = {}\n\t\n\t-- ships should not be passed to the SAM\t\n\tlocal mockContactShip = {}\n\tfunction mockContactShip:getDesc()\n\t\treturn {category = Unit.Category.SHIP}\n\tend\n\tfunction mockContactShip:getAge()\n\t\treturn 0\n\tend\n\t\n\ttable.insert(self.testIADS.contacts, mockContactShip)\n\t\n\tcorrelatedCalled = false\n\tfunction self.testIADS:informOfContact(contact)\n\t\tcorrelatedCalled = true\n\tend\n\tself.testIADS:evaluateContacts()\n\tlu.assertEquals(correlatedCalled, false)\n\tlu.assertEquals(#self.testIADS.contacts, 1)\n\t\n\tself.testIADS.contacts = {}\n\t\n\t-- aircraft should be passed to the SAM\t\n\tlocal mockContactAirplane = {}\n\tfunction mockContactAirplane:getDesc()\n\t\treturn {category = Unit.Category.AIRPLANE}\n\tend\n\tfunction mockContactAirplane:getAge()\n\t\treturn 0\n\tend\n\t\n\ttable.insert(self.testIADS.contacts, mockContactAirplane)\n\t\n\tcorrelatedCalled = false\n\tfunction self.testIADS:informOfContact(contact)\n\t\tcorrelatedCalled = true\n\tend\n\tself.testIADS:evaluateContacts()\n\t--TODO: FIX TEST\n\tlu.assertEquals(correlatedCalled, true)\n\tlu.assertEquals(#self.testIADS.contacts, 1)\n\tself.testIADS.contacts = {}\n\nend\n--]]\n\nfunction TestSkynetIADS:testAddMooseSetGroup()\n\n\tlocal mockMooseSetGroup = {}\n\tlocal mockMooseConnector = {}\n\tlocal setGroupCalled = false\n\t\n\tfunction mockMooseConnector:addMooseSetGroup(group)\n\t\tsetGroupCalled = true\n\t\tlu.assertEquals(mockMooseSetGroup, group)\n\tend\n\t\n\tfunction self.testIADS:getMooseConnector()\n\t\treturn mockMooseConnector\n\tend\n\t\n\tself.testIADS:addMooseSetGroup(mockMooseSetGroup)\n\tlu.assertEquals(setGroupCalled, true)\nend\n\n--TODO: add more comparisons in this test, this test also tests buildRadarCoverageForAbstractRadarElement\nfunction TestSkynetIADS:testBuildRadarCoverage()\t\n\t\n\tself:tearDown()\n\tself.testIADS = SkynetIADS:create()\n\tlocal ewWest2 = self.testIADS:addEarlyWarningRadar('EW-west2')\n\tlocal samSA6 = self.testIADS:addSAMSite('SAM-SA-6')\n\tlocal samSA62 = self.testIADS:addSAMSite('SAM-SA-6-2')\n\tlocal samSA2 = self.testIADS:addSAMSite('SAM-SA-2')\n\tself.testIADS:buildRadarCoverage()\n\n\tlocal ewWestChildren = ewWest2:getChildRadars()\n\tlu.assertEquals(#ewWestChildren, 3)\n\t\n\tlocal containsSa6 = false\n\tlocal containsSA62 = false\n\tlocal containsSA2  = false\n\tfor i =  1, #ewWestChildren do\n\t\tlocal radar = ewWestChildren[i]\n\t\tif radar == samSA6 then\n\t\t\tcontainsSa6 = true\n\t\tend\n\t\tif radar == samSA2 then\n\t\t\tcontainsSA2 = true\n\t\tend\n\t\tif radar == samSA62 then\n\t\t\tcontainsSA62 = true\n\t\tend\n\tend\n\tlu.assertEquals(containsSA2, true)\n\tlu.assertEquals(containsSA62, true)\n\tlu.assertEquals(containsSa6, true)\n\t\n\t--further tests to verify the exact content of the parent radars could be done with these:\n\tlu.assertEquals(#samSA6:getParentRadars(), 2)\n\tlu.assertEquals(#samSA6:getChildRadars(), 1)\n\t\n\tlu.assertEquals(#samSA62:getParentRadars(), 2)\n\tlu.assertEquals(#samSA62:getChildRadars(), 1)\n\t\n\tlu.assertEquals(#samSA2:getParentRadars(), 1)\nend\n\n--this test adds an EW Radar to an existing IADS, SAM site under coverage must then be adopted by the new EW radar\nfunction TestSkynetIADS:testBuildRadarCoverageForSingleEarlyWarningRadar()\t\n\tself:tearDown()\n\tself.testIADS = SkynetIADS:create()\n\t\n\t\n\tself.testIADS:addCommandCenter(StaticObject.getByName(\"Command Center\"))\n\t\n\tlocal ewRadar = self.testIADS:getEarlyWarningRadarByUnitName('EW-west2')\n\tlocal sam2 = self.testIADS:addSAMSite('SAM-SA-6')\n\tlocal sam1 = self.testIADS:addSAMSite('SAM-SA-6-2')\n\t\n\tself.testIADS:buildRadarCoverage()\n\n\t\n\tlu.assertEquals(#sam1:getParentRadars(), 1)\n\tlu.assertEquals(#sam2:getParentRadars(), 1)\n\t\n\tlu.assertEquals(#self.testIADS:getCommandCenters()[1]:getChildRadars(), 2)\n\t\n\tlocal ewWest2 = self.testIADS:addEarlyWarningRadar('EW-west2')\n\t\t\n\tself.testIADS:buildRadarCoverageForEarlyWarningRadar(ewWest2)\n\t\n\tlu.assertEquals(#sam1:getParentRadars(), 2)\n\tlu.assertEquals(#sam2:getParentRadars(), 2)\n\tlu.assertEquals(#ewWest2:getChildRadars(), 2)\n\tlu.assertEquals(ewWest2:getAutonomousState(), false)\n\n\tlu.assertEquals(#self.testIADS:getCommandCenters()[1]:getChildRadars(), 3)\nend\n\n--this tet adds a SAM site to a IADS network, SAM site under coverage must then be adopted by the new SAM site also EW radars must be added as parents\nfunction TestSkynetIADS:testBuildRadarCoverageForSingleSAMSite()\n\tself:tearDown()\n\tself.testIADS = SkynetIADS:create()\n\t\n\tlocal sam1 = self.testIADS:addSAMSite('SAM-SA-6-2')\n\tlocal ewWest2 = self.testIADS:addEarlyWarningRadar('EW-west2')\n\tself.testIADS:buildRadarCoverage()\n\t\n\tlu.assertEquals(#sam1:getParentRadars(), 1)\n\tlu.assertEquals(#ewWest2:getChildRadars(), 1)\n\t\n\tlocal sam2 = self.testIADS:addSAMSite('SAM-SA-6')\n\tlu.assertEquals(sam2:getAutonomousState(), true)\n\t\n\tself.testIADS:buildRadarCoverageForSAMSite(sam2)\n\tlu.assertEquals(sam2:getAutonomousState(), false)\n\tlu.assertEquals(#sam2:getParentRadars(), 2)\n\tlu.assertEquals(#sam1:getParentRadars(), 2)\n\tlu.assertEquals(#ewWest2:getChildRadars(), 2)\nend\n\n\t\nfunction TestSkynetIADS:testGetSAMSitesByPrefix()\n\tlocal samSites = self.testIADS:getSAMSitesByPrefix('SAM-SA-15')\n\tlu.assertEquals(#samSites, 3)\nend\n\nfunction TestSkynetIADS:testSetMaxAgeOfCachedTargets()\n\tlocal iads = SkynetIADS:create()\n\t\n\t-- test default value\n\tlu.assertEquals(iads.contactUpdateInterval, 5)\n\t\n\tiads:setUpdateInterval(10)\n\tlu.assertEquals(iads.contactUpdateInterval, 10)\n\t\n\tlu.assertEquals(iads:getCachedTargetsMaxAge(), 10)\n\t\n\tlocal ewRadar = iads:addEarlyWarningRadar('EW-west')\n\tlocal samSite = iads:addSAMSite('SAM-SA-15-1')\n\t\n\tlu.assertEquals(ewRadar.cachedTargetsMaxAge, 10)\n\tlu.assertEquals(samSite.cachedTargetsMaxAge, 10)\n\tiads:deactivate()\n\t\nend\n\nfunction TestSkynetIADS:testAddSingleEWRadarAndSAMSiteWhenIADSIsActiveWillTriggerCorrectRadarCoverageUpdates()\n\tlocal iads = SkynetIADS:create()\n\tlocal calledSAMUpdate = 0\n\tlocal calledEWUpdate = 0\n\t\n\n\tfunction iads:buildRadarCoverageForSAMSite(samSite)\n\t\tcalledSAMUpdate = calledSAMUpdate + 1\n\tend\n\t\n\tfunction iads:buildRadarCoverageForEarlyWarningRadar(ewRadar)\n\t\tcalledEWUpdate = calledEWUpdate + 1\n\tend\n\t\n\tlocal ewRadar = iads:addEarlyWarningRadar('EW-west')\n\tlu.assertEquals(calledEWUpdate, 0)\n\t\n\tlocal samSite = iads:addSAMSite('SAM-SA-6-2')\n\tlu.assertEquals(calledSAMUpdate, 0)\n\t\n\t--simulate an active IADS:\n\tiads.ewRadarScanMistTaskID = 1\n\t\n\tlocal ewRadar = iads:addEarlyWarningRadar('EW-west')\n\tlu.assertEquals(calledEWUpdate, 1)\n\t\n\tlocal samSite = iads:addSAMSite('SAM-SA-6-2')\n\tlu.assertEquals(calledSAMUpdate, 1)\n\tiads:deactivate()\n\t\nend\n\nfunction TestSkynetIADS:testBuildIADSWithAutonomousSAMS()\n\tlocal iads = SkynetIADS:create()\n\tlocal samSite = iads:addSAMSite('SAM-SA-10')\n\tiads:activate()\n\tlu.assertEquals(samSite:isActive(), true) \n\tiads:deactivate()\nend\n\nend\n"
  },
  {
    "path": "unit-tests/test-skynet-moose-a2a-dispatcher-connector.lua",
    "content": "do\nTestMooseA2ADispatcherConnector = {}\n\nfunction TestMooseA2ADispatcherConnector:setUp()\n\tself.iads = SkynetIADS:create()\n\tself.iads:addEarlyWarningRadarsByPrefix(\"EW\")\n\tself.iads:addSAMSitesByPrefix(\"SAM\")\n\tself.connector = SkynetMooseA2ADispatcherConnector:create(self.iads)\nend\n\nfunction TestMooseA2ADispatcherConnector:tearDown()\n\tself.iads:deactivate()\nend\n\n\nfunction TestMooseA2ADispatcherConnector:testGetEarlyWarningRadarGroupNames()\n\tlocal ewRadarNames = self.connector:getEarlyWarningRadarGroupNames()\n\t\n\t---we iterate through the EW radars of the IADS, to check the table in the connector contains all the names of the EW radars\n\tlocal usableEWRadars = self.iads:getUsableEarlyWarningRadars()\n\tlocal numRadars = 0\n\tfor i = 1, #ewRadarNames do\n\t\tlocal ewName = ewRadarNames[i]\n\t\tlocal ewFound = false\n\t\tfor j = 1, #usableEWRadars do\n\t\t\tlocal ewNameInIADS = usableEWRadars[j]:getDCSRepresentation():getGroup():getName()\n\t\t\tif ewName == ewNameInIADS then\n\t\t\t\tewFound = true\n\t\t\tend\n\t\tend\n\t\tlu.assertEquals(ewFound, true)\n\t\tnumRadars = numRadars + 1\n\tend\n\tlu.assertEquals(numRadars, SKYNET_UNIT_TESTS_NUM_EW_SITES_RED)\nend\n\nfunction TestMooseA2ADispatcherConnector:testGetSAMSitesGroupNames()\n\t\n\tlocal samSiteGroupNames = self.connector:getSAMSiteGroupNames()\n\t\n\t---we iterate through the SAM sites of the IADS, to check the table in the connector contains all the names of the SAM sites\n\tlocal usableSAMSites = self.iads:getUsableSAMSites()\n\tlocal numSams = 0\n\tfor i = 1, #samSiteGroupNames do\n\t\tlocal samSiteName = samSiteGroupNames[i]\n\t\tlocal samFound = false\n\t\tfor j = 1, #usableSAMSites do\n\t\t\tlocal samNameInIADS = usableSAMSites[j]:getDCSName()\n\t\t\tif samSiteName == samNameInIADS then\n\t\t\t\tsamFound = true\n\t\t\tend\n\t\tend\n\t\tlu.assertEquals(samFound, true)\n\t\tnumSams = numSams + 1\n\tend\n\tlu.assertEquals(numSams, SKYNET_UNIT_TESTS_NUM_SAM_SITES_RED)\nend\n\nfunction TestMooseA2ADispatcherConnector:testAddMooseSetGroupAndUpdate()\n\n\tlocal mockMooseSetGroup = {}\n\tmockMooseSetGroup.connector = self.connector\n\tlocal numRemoveCalls = 0\n\t\n\tfunction mockMooseSetGroup:RemoveGroupsByName(groupNames)\n\t\tnumRemoveCalls = numRemoveCalls + 1\n\t\tif\tnumRemoveCalls == 1 then\n\t\t\tlu.assertEquals(groupNames, self.connector.ewRadarGroupNames)\n\t\tend\n\t\t\n\t\tif numRemoveCalls == 2 then\n\t\t\tlu.assertEquals(groupNames, self.connector.samSiteGroupNames)\n\t\tend\n\tend\n\t\n\tlocal samGroups = {}\n\tfunction self.connector:getSAMSiteGroupNames()\n\t\treturn samGroups\n\tend\n\n\tlocal ewGroups = {}\n\tfunction self.connector:getEarlyWarningRadarGroupNames()\n\t\treturn ewGroups\n\tend\n\t\n\tlocal numAddCalls = 0\n\tfunction mockMooseSetGroup:AddGroupsByName(groupNames)\n\t\n\t\tif numAddCalls == 0 then\n\t\t\tlu.assertEquals(groupNames, samGroups)\n\t\tend\n\t\t\n\t\tif numAddCalls == 1 then\n\t\t\tlu.assertEquals(groupNames, ewGroups)\n\t\tend\n\t\t\n\t\tnumAddCalls = numAddCalls + 1\n\tend\n\t\t\n\tself.connector:addMooseSetGroup(mockMooseSetGroup)\n\n\t\n\tlu.assertEquals(numRemoveCalls, 2)\n\tlu.assertEquals(numAddCalls, 2)\nend\n\nend\n"
  },
  {
    "path": "unit-tests/test-syknet-early-warning-radar.lua",
    "content": "do\nTestSkynetIADSEWRadar = {}\n\nfunction TestSkynetIADSEWRadar:setUp()\n\tself.numEWSites = SKYNET_UNIT_TESTS_NUM_EW_SITES_RED\n\tif self.blue == nil then\n\t\tself.blue = \"\"\n\tend\n\tif self.ewRadarName then\n\t\tself.iads = SkynetIADS:create()\n\t\tself.iads:addEarlyWarningRadarsByPrefix(self.blue..'EW')\n\t\tself.ewRadar = self.iads:getEarlyWarningRadarByUnitName(self.ewRadarName)\n\tend\nend\n\nfunction TestSkynetIADSEWRadar:tearDown()\n\tif self.ewRadar then\n\t\tself.ewRadar:cleanUp()\n\tend\n\tif self.iads then\n\t\tself.iads:deactivate()\n\tend\n\tself.iads = nil\n\tself.ewRadar = nil\n\tself.ewRadarName = nil\n\tself.blue = \"\"\nend\n\nfunction TestSkynetIADSEWRadar:testCompleteDestructionOfEarlyWarningRadar()\n\t\t\n\t\tlocal ewRadar = SkynetIADSAWACSRadar:create(Unit.getByName('EW-west22-destroy'), SkynetIADS:create('test'))\n\t\tewRadar:setupElements()\n\t\tewRadar:setActAsEW(true)\n\t\tewRadar:goLive()\n\t\t\n\t\tlocal sa61 = SkynetIADSSamSite:create(Group.getByName('SAM-SA-6'), SkynetIADS:create('test'))\n\t\tlocal sa62 = SkynetIADSSamSite:create(Group.getByName('SAM-SA-6-2'), SkynetIADS:create('test'))\n\t\n\t\t--build radar association\n\t\tewRadar:addChildRadar(sa61)\n\t\tsa61:addParentRadar(ewRadar)\n\t\tewRadar:addChildRadar(sa62)\n\t\tsa62:addParentRadar(ewRadar)\n\t\t\n\t\tsa61:setToCorrectAutonomousState()\n\t\tsa62:setToCorrectAutonomousState()\n\t\t\n\t\tlu.assertEquals(ewRadar:hasRemainingAmmo(), true)\n\t\tlu.assertEquals(ewRadar:isActive(), true)\n\t\tlu.assertEquals(ewRadar:getDCSRepresentation():isExist(), true)\n\t\tlu.assertEquals(sa61:getAutonomousState(), false)\n\t\tlu.assertEquals(sa62:getAutonomousState(), false)\n\t\ttrigger.action.explosion(ewRadar:getDCSRepresentation():getPosition().p, 500)\n\t\t--we simulate a call to the event, since in game will be triggered to late to for later checks in this unit test\n\t\tewRadar:onEvent(createDeadEvent())\n\t\tlu.assertEquals(ewRadar:getDCSRepresentation():isExist(), false)\n\t\n\t\tlu.assertEquals(ewRadar:isActive(), false)\n\n\t\tlu.assertEquals(sa61:getAutonomousState(), true)\n\t\tlu.assertEquals(sa62:getAutonomousState(), true)\n\t\t\n\t\tsa61:cleanUp()\n\t\tsa62:cleanUp()\n\t\tewRadar:cleanUp()\nend\n\nfunction TestSkynetIADSEWRadar:testFinishHARMDefence()\n\tself.ewRadarName = \"EW-west2\"\n\tself:setUp()\n\tlu.assertEquals(self.ewRadar:isActive(), true)\n\tlu.assertEquals(self.ewRadar:hasRemainingAmmo(), true)\n\tself.ewRadar:goSilentToEvadeHARM()\n\tlu.assertEquals(self.ewRadar:isActive(), false)\n\tself.ewRadar.finishHarmDefence(self.ewRadar)\n\tlu.assertEquals(self.ewRadar.harmSilenceID, nil)\n\tself.iads.evaluateContacts(self.iads)\n\tlu.assertEquals(self.ewRadar:isActive(), true)\nend\n\nfunction TestSkynetIADSEWRadar:testGoDarkWhenAutonomousByDefault()\n\tself.ewRadarName = \"EW-west2\"\n\tself:setUp()\n\tlu.assertEquals(self.ewRadar:isActive(), true)\n\tfunction self.ewRadar:hasActiveConnectionNode()\n\t\treturn false\n\tend\n\tself.ewRadar:goAutonomous()\n\tlu.assertEquals(self.ewRadar:isActive(), false)\nend\n\nend"
  }
]