Full Code of walder/Skynet-IADS for AI

master 62aab46901ee cached
52 files
909.9 KB
256.5k tokens
1 requests
Download .txt
Showing preview only (942K chars total). Download the full file or copy to clipboard to get everything.
Repository: walder/Skynet-IADS
Branch: master
Commit: 62aab46901ee
Files: 52
Total size: 909.9 KB

Directory structure:
gitextract_fdixfuk7/

├── .gitattributes
├── .gitignore
├── LICENSE.md
├── README.md
├── build-tools/
│   └── build-compiled-script.ps1
├── contributing.md
├── demo-missions/
│   ├── mist_4_5_107.lua
│   ├── moose_a2a_connector/
│   │   ├── skynet-and-moose-a2a-dispatcher-setup.lua
│   │   └── skynet-and-moose-a2a-dispatcher.miz
│   ├── skynet-iads-compiled.lua
│   ├── skynet-iads-setup-persian-gulf.lua
│   ├── skynet-test-persian-gulf-stress-test.miz
│   └── skynet-test-persian-gulf.miz
├── skynet-iads-source/
│   ├── README_source.md
│   ├── highdigitsams/
│   │   └── skynet-iads-high-digit-sams-suported-types.lua
│   ├── skynet-iads-abstract-dcs-object-wrapper.lua
│   ├── skynet-iads-abstract-element.lua
│   ├── skynet-iads-abstract-radar-element.lua
│   ├── skynet-iads-awacs-radar.lua
│   ├── skynet-iads-command-center.lua
│   ├── skynet-iads-contact.lua
│   ├── skynet-iads-early-warning-radar.lua
│   ├── skynet-iads-harm-detection.lua
│   ├── skynet-iads-jammer.lua
│   ├── skynet-iads-logger.lua
│   ├── skynet-iads-sam-search-radar.lua
│   ├── skynet-iads-sam-site.lua
│   ├── skynet-iads-sam-tracking-radar.lua
│   ├── skynet-iads-supported-types.lua
│   ├── skynet-iads-table-delegator.lua
│   ├── skynet-iads.lua
│   ├── skynet-mooose-a2a-dispatcher-connector.lua
│   └── syknet-iads-sam-launcher.lua
└── unit-tests/
    ├── highdigitsams/
    │   ├── highdigitsams-unit-tests.miz
    │   ├── skynet-high-digit-sams-unit-test-setup.lua
    │   └── test-skynet-high-digit-sam-sites.lua
    ├── luaunit.lua
    ├── skynet-unit-test-iads-setup.lua
    ├── skynet-unit-tests.lua
    ├── skynet-unit-tests.miz
    ├── test-skynet-iads-abstract-dcs-object-wrapper.lua
    ├── test-skynet-iads-abstract-element.lua
    ├── test-skynet-iads-abstract-radar-element.lua
    ├── test-skynet-iads-blue-sam-sites-and-ew-radars.lua
    ├── test-skynet-iads-contact.lua
    ├── test-skynet-iads-harm-detection.lua
    ├── test-skynet-iads-jammer.lua
    ├── test-skynet-iads-red-sam-sites-and-ew-radars.lua
    ├── test-skynet-iads-sam-site.lua
    ├── test-skynet-iads.lua
    ├── test-skynet-moose-a2a-dispatcher-connector.lua
    └── test-syknet-early-warning-radar.lua

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitattributes
================================================
# Auto detect text files and perform LF normalization
* text=auto


================================================
FILE: .gitignore
================================================
.DS_STORE
/demo-missions/spikes/

================================================
FILE: LICENSE.md
================================================
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: README.md
================================================
# Skynet-IADS
![logo](/images/SA3_2.jpg)

An IADS (Integrated Air Defence System) script for DCS (Digital Combat Simulator).

# Abstract
This 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.

A 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.

This all sounds gibberish to you? Watch [this video by Covert Cabal on modern IADS](https://www.youtube.com/watch?v=9J9kntzkSQY).

Visit [this DCS forum thread](https://forums.eagle.ru/topic/226173-skynet-an-iads-for-mission-builders) for development updates.

Join the [Skynet discord group](https://discord.gg/pz8wcQs) and get support setting up your mission.

Skynet supports the [HighDigitSAMs Mod](https://github.com/Auranis/HighDigitSAMs).

You 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.

**So far over 200 hours of work went in to the development of Skynet.  
If you like using it, please consider a donation:**

[![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)




 Table of Contents
 =================
  * [Skynet\-IADS](#skynet-iads)
 * [Abstract](#abstract)
 * [Quick start](#quick-start)
 * [Skynet IADS Elements](#skynet-iads-elements)
   * [IADS](#iads)
   * [Track files](#track-files)
   * [Comand Centers](#comand-centers)
   * [SAM Sites](#sam-sites)
   * [Early Warning Radars](#early-warning-radars)
   * [Power Sources](#power-sources)
   * [Connection Nodes](#connection-nodes)
   * [AWACS (Airborne Early Warning and Control System)
](#awacs-airborne-early-warning-and-control-system)
   * [Ships](#ships)
 * [Tactics](#tactics)
   * [HARM defence](#harm-defence)
     * [HARM detection](#harm-detection)
     * [HARM flight path analysis](#harm-flight-path-analysis)
   * [HARM radar shutdown](#harm-radar-shutdown)
   * [Point defence](#point-defence)
   * [Electronic Warfare](#electronic-warfare)
 * [Using Skynet in the mission editor](#using-skynet-in-the-mission-editor)
   * [Placing units](#placing-units)
   * [Preparing a SAM site](#preparing-a-sam-site)
   * [Preparing an EW radar](#preparing-an-ew-radar)
   * [Adding the Skynet code](#adding-the-skynet-code)
   * [Adding the Skynet IADS](#adding-the-skynet-iads)
 * [Advanced setup](#advanced-setup)
   * [IADS configuration](#iads-configuration)
   * [Adding a command center](#adding-a-command-center)
   * [Power sources and connection nodes](#power-sources-and-connection-nodes)
   * [Warm up the SAM sites of an IADS](#warm-up-the-sam-sites-of-an-iads)
   * [Connecting Skynet to the MOOSE AI\_A2A\_DISPATCHER](#connecting-skynet-to-the-moose-ai_a2a_dispatcher)
   * [SAM site configuration](#sam-site-configuration)
     * [Adding SAM sites](#adding-sam-sites)
       * [Add multiple SAM sites](#add-multiple-sam-sites)
       * [Add a SAM site manually](#add-a-sam-site-manually)
     * [Accessing SAM sites in the IADS](#accessing-sam-sites-in-the-iads)
     * [Act as EW radar](#act-as-ew-radar)
     * [Engagement zone](#engagement-zone)
       * [Engagement zone options](#engagement-zone-options)
     * [Engage air weapons](#engage-air-weapons)
     * [Engage HARM](#engage-harm)
   * [Add go live constraints](#add-go-live-constraints)
     * [Use cases](#use-cases)
   * [Contact](#contact)
   * [EW radar configuration](#ew-radar-configuration)
     * [Adding EW radars](#adding-ew-radars)
       * [Add multiple EW radars](#add-multiple-ew-radars)
       * [Add an EW radar manually](#add-an-ew-radar-manually)
     * [Accessing EW radars in the IADS](#accessing-ew-radars-in-the-iads)
   * [Options for SAM sites and EW radars](#options-for-sam-sites-and-ew-radars)
     * [Setting an option](#setting-an-option)
     * [Daisy chaining options](#daisy-chaining-options)
     * [HARM Defence](#harm-defence-1)
     * [Point defence](#point-defence-1)
     * [Autonomous mode behaviour](#autonomous-mode-behaviour)
       * [Autonomous mode options](#autonomous-mode-options)
   * [Adding a jammer](#adding-a-jammer)
     * [Advanced functions](#advanced-functions)
   * [Setting debug information](#setting-debug-information)
 * [Example Setup](#example-setup)
 * [FAQ](#faq)
   * [Does Skynet IADS have an impact on game performance?](#does-skynet-iads-have-an-impact-on-game-performance)
   * [What air defence units shall I add to the Skynet IADS?](#what-air-defence-units-shall-i-add-to-the-skynet-iads)
   * [Which SAM systems can engage HARMS?](#which-sam-systems-can-engage-harms)
   * [What exactly does Skynet do with the SAMS?](#what-exactly-does-skynet-do-with-the-sams)
   * [Are there known bugs?](#are-there-known-bugs)
   * [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)
   * [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)
 * [Thanks](#thanks)
 

# Quick start
Tired 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.

# Skynet IADS Elements
![Skynet IADS overview](/images/skynet-overview.jpg)

## IADS
A 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.

## Track files
Skynet 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. 

## Comand Centers
You can add multiple command centers to a Skynet IADS. Once all command centers are destroyed the IADS will go in to autonomous mode.

## SAM Sites
Skynet 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. 
Every single launcher and radar unit's distance of a SAM site is analysed individually. 
If at least one launcher and radar is within range, the SAM Site will become active. 
This allows for a scattered placement of radar and launcher units as in real life.

If 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.

If 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.
SAM sites will go autonomous in such a case meaning they will use their organic radars or just stay dark depending on setup.
Once a SAM site is within EW radar coverage again it will be updated by the IADS.

## Early Warning Radars
Skynet 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. 
Some 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).

You 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.
SAM sites that are out of ammo will stay live if they are set to act as EW radars.

Nice to know:
Terrain elevation around an EW radar will create blinds spots, allowing low and fast movers to penetrate radar networks through valleys.

##  Power Sources
By 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.
Once a power source is fully damaged the Skynet IADS unit will stop working.

Nice to know:
Taking out the power source of a command center is a real life tactic used in SEAD (Suppression of Enemy Air Defence).

## Connection Nodes
By 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.

When 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. 
If 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.

Nice to know:
A 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.

## AWACS (Airborne Early Warning and Control System)
Any 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.
These will however not be passed to the SAM sites.

You 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.
Technically 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.

## Ships
Ships will contribute to the IADS the same way AWACS units do. Add them as a regular EW radar. 

# Tactics

## HARM defence
SAM 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.
Each 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.  
See [skynet-iads-supported-types.lua](/skynet-iads-source/skynet-iads-supported-types.lua) field ```['harm_detection_chance']``` for the probability per radar system.

### HARM detection
let'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%.  

With 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.

![Skynet IADS overview](/images/skynet-harm-detection.jpg)

### HARM flight path analysis
The 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.

![Skynet IADS overview](/images/skynet-harm-flightpath.jpg)

This 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.

If 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.
The IADS will calculate time to impact and shut down radar emitters up to a maximum of 180 seconds after time to impact. 

## HARM radar shutdown
Once 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.

![Skynet IADS overview](/images/skynet-harm-radar-shutdown.jpg)

## Point defence
When 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.

Use 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.

See FAQ [Which SAM systems can engage HARMS?](#which-sam-systems-can-engage-harms)

[Point defence setup example](#point-defence-1)

## Electronic Warfare
A 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. 
The 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. 
Older SAM sites are more susceptible to jamming. EW radars are currently not jammable.

I 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. 
The jammer emitter will toggle the ROE state of a SAM site which affects how the SAM site reacts to all threats near or far.

I presume an aircraft very close to a SAM site beeing jammed by a emitter very far away would most likely be detected.
So the farther away you are from the jammer source the more unrealistic your experience will be.

Here 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. 
When 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. 
The jammer effectiveness is not based on any real world data I just read about the different types and made my own conclusions.

Here 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.
I suppose that must have been the effective range of 70's jamming tech.

# Using Skynet in the mission editor
It's quite simple to setup an IADS have a look at the demo missions in the [/demo-missions/](/demo-missions) folder.

## Placing units
This 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.
Place the IADS elements you wish to add on the map.

![Mission Editor IADS Setup](/images/iads-setup.png)  

## Preparing a SAM site
There 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.
The 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'.

![Mission Editor add SAM site](/images/add-sam-site.png)  

## Preparing an EW radar
You 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.

![Mission Editor EW radar](/images/ew-setup.png)  

## Adding the Skynet code
Skynet requires MIST. A version is provided in this repository or you can download the most current version [here](https://github.com/mrSkortch/MissionScriptingTools).
Make 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. 

I 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.
You 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.

![Mission Editor IADS Setup](/images/load-scripts.png)  

## Adding the Skynet IADS
For the IADS to work you need four lines of code.

create an instance of the IADS, the name string is optional and will be displayed in status output:
```lua
redIADS = SkynetIADS:create('name')
``` 


Give 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:  
```lua
redIADS:addSAMSitesByPrefix('SAM')
``` 


Same for the EW radars, name all units with a common prefix in the mission editor eg: 'EW-radar-south':  
```lua
redIADS:addEarlyWarningRadarsByPrefix('EW')
``` 


Activate the IADS:  
```lua
redIADS:activate()
```

# Advanced setup
This is the danger zone. Call Kenny Loggins. Some experience with scripting is recommended.
You 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.
The following examples use static objects for command centers, connection nodes and power sources, you can also use units instead.

## IADS configuration
Call 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:
```lua
redIADS:addRadioMenu()  
```
```lua
redIADS:removeRadioMenu()
```

If you dereference the IADS remember to call ```deactivate()``` otherwise background tasks of the IADS will continue running, resulting in unexpected behaviour:
```lua
redIADS:deactivate()
```

Set 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:
```lua
redIADS:setUpdateInterval(5)
```

## Adding a command center
The command center represents the place where information is collected and analysed. It if is destroyed the IADS disintegrates.

Add a command center like this:
```lua
local commandCenter = StaticObject.getByName("Command Center")
redIADS:addCommandCenter(commandCenter)
```

## Power sources and connection nodes
You can use units or static objects. Call the function multiple times to add more than one power source or connection node:

```unit``` refers to a SAM site, or EW Radar you retrieved from the IADS, see [setting an option for Radar units](#setting-an-option).
```lua
local powerSource = StaticObject.getByName("EW Power Source")  
unit:addPowerSource(powerSource)
```

```lua
local connectionNode = Unit.getByName("EW connection node") 
unit:addConnectionNode(connectionNode)
```

For command centers use:
```lua
local commandCenter = StaticObject.getByName("Command Center2")
local comPowerSource = StaticObject.getByName("Command Center2 Power Source")
redIADS:addCommandCenter(commandCenter):addPowerSource(comPowerSource)
```

## Warm up the SAM sites of an IADS
This function is deprecated and will be removed in a future release.

```lua
redIADS:setupSAMSitesAndThenActivate()
```


## Connecting Skynet to the MOOSE AI_A2A_DISPATCHER
You 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.
Skynet 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.

Add the object of type SET_GROUP to the iads like this (in this example ```DectionSetGroup```):
```lua
redIADS:addMooseSetGroup(DetectionSetGroup)
```

## SAM site configuration

### Adding SAM sites

#### Add multiple SAM sites
Adds SAM sites with prefix in group name to the IADS. Previously added SAM sites are cleared:
```lua
redIADS:addSAMSitesByPrefix('SAM')
```

#### Add a SAM site manually
You can manually add a SAM site, must be a valid group name:
```lua
redIADS:addSAMSite('SA-6 Group2')
```

### Accessing SAM sites in the IADS
The following functions exist to access SAM sites added to the IADS. They all support daisy chaining options:

Returns 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':
```lua
redIADS:getSAMSitesByNatoName('SA-6')
```

Returns all SAM sites in the IADS:
```lua
redIADS:getSAMSites()
```

Returns a SAM site with the specified group name:
```lua
redIADS:getSAMSiteByGroupName('SAM-SA-6')
```

Returns 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. 
Give 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:

```lua
redIADS:getSAMSitesByPrefix('SAM-SECTOR-A')
```

### Act as EW radar
Will 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: 
```lua
samSite:setActAsEW(true)
```

### Engagement zone
Set the distance at which a SAM site will switch on its radar:
```lua
samSite:setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE)
```

#### Engagement zone options  

SAM site will go live when target is within the red circle in the mission editor (default Skynet behaviour): 
```lua
SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE
```

SAM site will go live when target is within the yelow circle in the mission editor: 
```lua
SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE
```

This 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. 
During 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:

```lua
samSite:setGoLiveRangeInPercent(90)
```

### Engage air weapons
Will 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.

```lua
samSite:setCanEngageAirWeapons(true)
```

### Engage HARM
Will 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.

```lua
samSite:setCanEngageHARM(true)
```

## Add go live constraints
You can include constraints wich must be satisfied for the SAM site to go live. Please note this only controls activation of the SAM site. 
There is currently no way to tell a SAM site to only target a certain contact via the lua scripting engine in DCS. 

The constraint must evaluate to true and the contact must be in range of the SAM site (handled by Skynet). 

### Use cases
Place 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.  

Set a SAM site to only go live if aircraft are in a certain altitude band.

SAM site shall only go live once a strike package has destroyed a certain building or unit.  

You do not have to use the contact provided in the function to evaluate the constraint. You can make any assertion you want.

Create a function that will evaluate if the constraint is satisfied. The function will have access to the [contact](#contact) the SAM site is evaluating:
```lua
--SAM site will only go live if the contact is below 1000 feet.
local function goLiveConstraint(contact)
	return ( contact:getHeightInFeetMSL() < 1000 )
end
```

Add the function to the SAM site and give it a name. You can add as many constraints as you wish:
```lua
self.samSite:addGoLiveConstraint('ignore-low-flying-contacts', goLiveConstraint)
```

Remove constraint you no longer wish to use:
```lua
self.samSite:removeGoLiveConstraint('ignore-low-flying-contacts')
```

Get a table of all constraints:
```lua
self.samSite:getGoLiveConstraints()
```

## Contact
You can use the following methods to get information about a contact.

Will return true if contact has been identified as a HARM by Skynet:
```lua
contact:isIdentifiedAsHARM()
```  

Will return the height of a contact:
```lua
contact:getHeightInFeetMSL()
```  

Will 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:
```lua
contact:getMagneticHeading()
```  

Will 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:
```lua
contact:getMagneticHeading()
```  

Will return the time in seconds a contact has been known to the IADS:
```lua
contact:getAge()
```  

Will return the type as a ```Object.Category```:
```lua
contact:getTypeName()
```  

Will return the unit name:
```lua
contact:getName()
```  


## EW radar configuration

### Adding EW radars

#### Add multiple EW radars
Adds EW radars with prefix in unit name to the IADS. Previously added EW sites are cleared:
```lua
redIADS:addEarlyWarningRadarsByPrefix('EW')
``` 

#### Add an EW radar manually
You can add EW radars manually, must be a valid unit name: 
```lua
redIADS:addEarlyWarningRadar('EWR West')
```

### Accessing EW radars in the IADS
The following functions exist to access EW radars added to the IADS. They all support daisy chaining options. 


Returns all EW radars in the IADS:
```lua
redIADS:getEarlyWarningRadars()
```

Returns the EW radar with the specified unit name:
```lua
redIADS:getEarlyWarningRadarByUnitName('EW-west')
```

## Options for SAM sites and EW radars

### Setting an option
In 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).

### Daisy chaining options
 You can daisy chain options on a single SAM site / EW Radar or a table of SAM sites / EW radars like this:
 ```lua
 redIADS:getSAMSites():setActAsEW(true):addPowerSource(powerSource):addConnectionNode(connectionNode):setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE):setGoLiveRangeInPercent(90):setAutonomousBehaviour(SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK)
 ```  

### HARM Defence
You 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:
```lua
ewRadarOrSamSite:setHARMDetectionChance(50)
```

### Point defence
You 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:

If 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**.
Let'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.

```lua
--first get the SAM site you want to use as point defence from the IADS:
local sa15 = redIADS:getSAMSiteByGroupName('SAM-SA-15')
--then add it to the SAM site it should protect:
redIADS:getSAMSiteByGroupName('SAM-SA-10'):addPointDefence(sa15)
```

This function is deprecated and will be removed in a future release.
```lua
ewRadarOrSamSite:setIgnoreHARMSWhilePointDefencesHaveAmmo(true)
```

### Autonomous mode behaviour
Set how the SAM site or EW radar will behave if it looses connection to the IADS:
```lua
ewRadarOrSamSite:setAutonomousBehaviour(SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK)
```

#### Autonomous mode options 
SAM 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):
```lua
SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DCS_AI
```

SAM Site or EW radar will go dark if it looses connection to IADS (default behaviour for EW radars):
```lua
SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK
```

## Adding a jammer
The 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.
Once 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.
Check [skynet-iads-jammer.lua](/skynet-iads-source/skynet-iads-jammer.lua) to see which SAM sites are supported.

Remember 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.
This way it will stick to the preset flight plan.

Create a jammer and assign it to an unit. Also make sure you add the IADS you wan't the jammer to work for:
```lua
local jammerSource = Unit.getByName("F-4 AI")
jammer = SkynetIADSJammer:create(jammerSource, iads)
```

The jammer will start listening for emitters and if it finds one of the emitters it is able to jam it will start jamming it:
```lua
jammer:masterArmOn()
```

Will disable jamming for the specified SAM type, pass the Nato name:
```lua
jammer:disableFor('SA-2')
```

Will 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:
```lua
jammer:masterArmSafe()
```

Will add jammer on / off to the radio menu:
```lua
jammer:addRadioMenu()
```

Will remove jammer on / off from the radio menu:
```lua
jammer:removeRadioMenu()
```

### Advanced functions

Add a second IADS the jammer should be able to jam, for example if you have two separate IADS running:
```lua
jammer:addIADS(iads2)
```

Add a new jammer function:

```lua
-- write a lambda function that expects one parameter:
-- given public available data on jammers their effeciveness drastically decreases the closer you get, so a non-linear function would make sense:
local function f(distanceNM)
	return ( 1.4 ^ distanceNM ) + 80
end

-- add the function: specify which SAM type it should apply for:
self.jammer:addFunction('SA-10', f)
```

Set the maximum range the jammer will work, the default value is set to 200 nautical miles:
```lua
jammer:setMaximumEffectiveDistance(100)
```

## Setting debug information
When 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:

Access the debug settings:
```lua
local iadsDebug = redIADS:getDebugSettings()  
```

Output in game:
```lua
iadsDebug.IADSStatus = true
iadsDebug.contacts = true
iadsDebug.jammerProbability = true
```

Output to dcs.log:
```lua
iadsDebug.addedEWRadar = true
iadsDebug.addedSAMSite = true
iadsDebug.warnings = true
iadsDebug.radarWentLive = true
iadsDebug.radarWentDark = true
iadsDebug.harmDefence = true
```

These 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:
```lua
iadsDebug.samSiteStatusEnvOutput = true
iadsDebug.earlyWarningRadarStatusEnvOutput = true
iadsDebug.commandCenterStatusEnvOutput = true
```
![Mission Editor IADS Setup](/images/skynet-debug.png)  

# Example Setup
This is an example of how you can set up your IADS used in the [demo mission](/demo-missions/skynet-test-persian-gulf.miz):
```lua
do

--create an instance of the IADS
redIADS = SkynetIADS:create('RED IADS')

---debug settings remove from here on if you do not wan't any output on what the IADS is doing by default
local iadsDebug = redIADS:getDebugSettings()
iadsDebug.IADSStatus = true
iadsDebug.radarWentDark = true
iadsDebug.contacts = true
iadsDebug.radarWentLive = true
iadsDebug.noWorkingCommmandCenter = true
iadsDebug.samNoConnection = true
iadsDebug.jammerProbability = true
iadsDebug.addedEWRadar = true
iadsDebug.harmDefence = true
---end remove debug ---

--add all units with unit name beginning with 'EW' to the IADS:
redIADS:addEarlyWarningRadarsByPrefix('EW')

--add all groups begining with group name 'SAM' to the IADS:
redIADS:addSAMSitesByPrefix('SAM')

--add a command center:
commandCenter = StaticObject.getByName('Command-Center')
redIADS:addCommandCenter(commandCenter)

---we add a K-50 AWACs, manually. This could just as well be automated by adding an 'EW' prefix to the unit name:
redIADS:addEarlyWarningRadar('AWACS-K-50')

--add a power source and a connection node for this EW radar:
local powerSource = StaticObject.getByName('Power-Source-EW-Center3')
local connectionNodeEW = StaticObject.getByName('Connection-Node-EW-Center3')
redIADS:getEarlyWarningRadarByUnitName('EW-Center3'):addPowerSource(powerSource):addConnectionNode(connectionNodeEW)

--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:
local connectionNode = Unit.getByName('Mobile-Command-Post-SAM-SA-2')
redIADS:getSAMSiteByGroupName('SAM-SA-2'):addConnectionNode(connectionNode):setAutonomousBehaviour(SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK)

--this SA-2 site will go live at 70% of its max search range:
redIADS:getSAMSiteByGroupName('SAM-SA-2'):setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE):setGoLiveRangeInPercent(70)

--all SA-10 sites shall act as EW sites, meaning their radars will be on all the time:
redIADS:getSAMSitesByNatoName('SA-10'):setActAsEW(true)

--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.
--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.
local sa15 = redIADS:getSAMSiteByGroupName('SAM-SA-15-point-defence-SA-10')
redIADS:getSAMSiteByGroupName('SAM-SA-10'):addPointDefence(sa15):setHARMDetectionChance(100)

--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%)
redIADS:getSAMSiteByGroupName('SAM-SA-11'):setGoLiveRangeInPercent(70):setHARMDetectionChance(50)

--this SA-6 site will always react to a HARM being fired at it:
redIADS:getSAMSiteByGroupName('SAM-SA-6'):setHARMDetectionChance(100)

--set this SA-11 site to go live at maximunm search range (default is at maximung firing range):
redIADS:getSAMSiteByGroupName('SAM-SA-11-2'):setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE)

--activate the radio menu to toggle IADS Status output
redIADS:addRadioMenu()

--activate the IADS
redIADS:activate()	

--add the jammer
local jammer = SkynetIADSJammer:create(Unit.getByName('jammer-emitter'), redIADS)
jammer:masterArmOn()

--setup blue IADS:
blueIADS = SkynetIADS:create('BLUE IADS')
blueIADS:addSAMSitesByPrefix('BLUE-SAM')
blueIADS:addEarlyWarningRadarsByPrefix('BLUE-EW')
blueIADS:activate()
blueIADS:addRadioMenu()

local iadsDebug = blueIADS:getDebugSettings()
iadsDebug.IADSStatus = true
iadsDebug.contacts = true

end
```

# FAQ

## Does Skynet IADS have an impact on game performance?
Skynet 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.

## What air defence units shall I add to the Skynet IADS?
In 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. 
Very 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.
This 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.
The strength of the Skynet IADS lies with handling long range systems that operate by radar.

## Which SAM systems can engage HARMS?
As 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.

## What exactly does Skynet do with the SAMS?
Via 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. 

No god like intervention is used (like magically exploding HARMS via the scripting engine).
If 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.

## Are there known bugs?
Yes, 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.
The 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.

## How do I know if a SAM site is in range of an EW site or a SAM site in EW mode?
To 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.
The 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.

In 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. 


![1L13 EWR range differences](/images/ew-detection-distance-example.png)  

Set the debug options ```samSiteStatusEnvOutput``` and ```earlyWarningRadarStatusEnvOutput``` to get detailed information on every SAM site and EW radar.
The text marked in the red box will show you which SAM sites are in the covered area of a SAM site or EW radar.


![SAM sites in covered area](/images/radar-emitter-status-dcs-log.png) 

## How do I connect Skynet with the MOOSE AI_A2A_DISPATCHER and what are the benefits of that?
IRL 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
to 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.

An example setup of Skynet and the [AI_A2A_DISPATCHER](https://flightcontrol-master.github.io/MOOSE_DOCS/Documentation/AI.AI_A2A_Dispatcher.html) :
```lua

--Setup Syknet IADS:
redIADS = SkynetIADS:create('Enemy IADS')
redIADS:addSAMSitesByPrefix('SAM')
redIADS:addEarlyWarningRadarsByPrefix('EW')
redIADS:activate()

-- START MOOSE CODE:
-- Define a SET_GROUP object that builds a collection of groups that define the EWR network.
DetectionSetGroup = SET_GROUP:New()

-- Setup the detection and group targets to a 30km range!
Detection = DETECTION_AREAS:New( DetectionSetGroup, 30000 )

-- Setup the A2A dispatcher, and initialize it.
A2ADispatcher = AI_A2A_DISPATCHER:New( Detection )

-- Set 100km as the radius to engage any target by airborne friendlies.
A2ADispatcher:SetEngageRadius() -- 100000 is the default value.

-- Set 200km as the radius to ground control intercept.
A2ADispatcher:SetGciRadius() -- 200000 is the default value.

CCCPBorderZone = ZONE_POLYGON:New( "RED-BORDER", GROUP:FindByName( "RED-BORDER" ) )
A2ADispatcher:SetBorderZone( CCCPBorderZone )
A2ADispatcher:SetSquadron( "Kutaisi", AIRBASE.Caucasus.Kutaisi, { "Squadron red SU-27" }, 2 )
A2ADispatcher:SetSquadronGrouping( "Kutaisi", 2 )
A2ADispatcher:SetSquadronGci( "Kutaisi", 900, 1200 )
A2ADispatcher:SetTacticalDisplay(true)
A2ADispatcher:Start()
--END MOOSE CODE

-- 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.
redIADS:addMooseSetGroup(DetectionSetGroup)
```

# Thanks
Special 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.
I 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.




================================================
FILE: build-tools/build-compiled-script.ps1
================================================
$version=$args[0]
if ($version -eq $null){
	echo "No Version supplied, not bulding script"
	return
}
if (Test-Path ./tmp/){
	Remove-Item ./tmp/
}
New-Item ./tmp/ -ItemType Directory
if (Test-Path ./tmp/skynet-iads-compiled.lua) {
	Remove-Item ./tmp/skynet-iads-compiled.lua
}
Add-Content ./tmp/tmp-time.lua ("env.info(`"--- SKYNET VERSION: "+$version+" | BUILD TIME: "+(Get-Date -date (Get-Date).ToUniversalTime()-uformat "%d.%m.%Y %H%MZ")+" ---`")")  
cat ../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 
$code = Get-Content ./tmp/tmp-code.lua
Add-Content ./tmp/tmp-time.lua $code
Rename-Item -Path ./tmp/tmp-time.lua -NewName skynet-iads-compiled.lua
Remove-Item ./tmp/tmp-code.lua

if (Test-Path ../demo-missions/skynet-iads-compiled.lua) {
	Remove-Item ../demo-missions/skynet-iads-compiled.lua
}

Move-Item -Path ./tmp/skynet-iads-compiled.lua ../demo-missions/skynet-iads-compiled.lua

$toc = ./bin/gh-md-toc.exe --hide-footer ../skynet-iads-source/README_source.md
$toc = $toc -replace "=================", "=================`n"
$toc = $toc -replace "Table of Contents", "Table of Contents`n"
$toc = $toc -replace "\)", "`)`n"
$readme = Get-Content ../skynet-iads-source/README_source.md
$readmeWithTOC = $readme -replace "{TOC_PLACEHOLDER}", $toc

if (Test-Path ../README.md) {
	Remove-Item ../README.md
}

Add-Content ../README.md $readmeWithTOC
Remove-Item ./tmp/

================================================
FILE: contributing.md
================================================
This guide is work in progress and will be updated.

# Contributing
Thanks for your interest in contributing to Skynet!
If 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).
It'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 
and you also may get tips on how to best implement an enhancement.

# Versioning
Skynet uses [semantic versioning](https://semver.org/).

# Required software
You will need a working copy of DCS [([Digital Combat Simulator)](https://www.digitalcombatsimulator.com/en/) to contribute to Skynet development.

# Test first design philosophy
Skynet 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.
It 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.

## Writing a unit test
Have 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.
Check 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.


# setting up your editor
I recomend you use [notepad++](https://notepad-plus-plus.org/downloads/) to edit lua files.
This [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++

# Build workflow
All 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.

## Editing the readme file
The 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.  

================================================
FILE: demo-missions/mist_4_5_107.lua
================================================
--[[--
MIST Mission Scripting Tools.
## Description:
MIssion Scripting Tools (MIST) is a collection of Lua functions
and databases that is intended to be a supplement to the standard
Lua functions included in the simulator scripting engine.

MIST functions and databases provide ready-made solutions to many common
scripting tasks and challenges, enabling easier scripting and saving
mission scripters time. The table mist.flagFuncs contains a set of
Lua functions (that are similar to Slmod functions) that do not
require detailed Lua knowledge to use.

However, the majority of MIST does require knowledge of the Lua language,
and, if you are going to utilize these components of MIST, it is necessary
that you read the Simulator Scripting Engine guide on the official ED wiki.

## Links:

ED Forum Thread: <http://forums.eagle.ru/showthread.php?t=98616>

##Github:

Development <https://github.com/mrSkortch/MissionScriptingTools>

Official Releases <https://github.com/mrSkortch/MissionScriptingTools/tree/master>

@script MIST
@author Speed
@author Grimes
@author lukrop
]]
mist = {}

-- don't change these
mist.majorVersion = 4
mist.minorVersion = 5
mist.build = 107

-- forward declaration of log shorthand
local log
local dbLog
    
local mistSettings = {
	errorPopup = false, -- errors printed by mist logger will create popup warning you
	warnPopup = false,
	infoPopup = false,
	logLevel = 'warn',
    dbLog = 'warn',
}

do -- the main scope
	local coroutines = {}

	local tempSpawnedUnits = {} -- birth events added here
	local tempSpawnedGroups = {}
	local tempSpawnGroupsCounter = 0
	
	local mistAddedObjects = {} -- mist.dynAdd unit data added here
	local mistAddedGroups = {} -- mist.dynAdd groupdata added here
	local writeGroups = {}
	local lastUpdateTime = 0

	local updateAliveUnitsCounter = 0
	local updateTenthSecond = 0
	
	local mistGpId = 7000
	local mistUnitId = 7000
	local mistDynAddIndex = {[' air '] = 0, [' hel '] = 0, [' gnd '] = 0, [' bld '] = 0, [' static '] = 0, [' shp '] = 0}

	local scheduledTasks = {}
	local taskId = 0
	local idNum = 0

	mist.nextGroupId = 1
	mist.nextUnitId = 1


	
	local function initDBs() -- mist.DBs scope
		mist.DBs = {}
        mist.DBs.markList = {}
		mist.DBs.missionData = {}
		if env.mission then

			mist.DBs.missionData.startTime = env.mission.start_time
			mist.DBs.missionData.theatre = env.mission.theatre
			mist.DBs.missionData.version = env.mission.version
			mist.DBs.missionData.files = {}
			if type(env.mission.resourceCounter) == 'table' then
				for fIndex, fData in pairs (env.mission.resourceCounter) do
					mist.DBs.missionData.files[#mist.DBs.missionData.files + 1] =	mist.utils.deepCopy(fIndex)
				end
			end
			-- if we add more coalition specific data then bullsye should be categorized by coaliton. For now its just the bullseye table
            mist.DBs.missionData.bullseye = {}
		end

		mist.DBs.zonesByName = {}
		mist.DBs.zonesByNum = {}


		if env.mission.triggers and env.mission.triggers.zones then
			for zone_ind, zone_data in pairs(env.mission.triggers.zones) do
				if type(zone_data) == 'table' then
					local zone = mist.utils.deepCopy(zone_data)
					zone.point = {}	-- point is used by SSE
					zone.point.x = zone_data.x
					zone.point.y = 0
					zone.point.z = zone_data.y
                    zone.properties = {}
                    if zone_data.properties then
                        for propInd, prop in pairs(zone_data.properties) do
                            if prop.value and type(prop.value) == 'string' and prop.value ~= "" then
                                zone.properties[prop.key] = prop.value                                
                            end
                        end
                    end
                    if zone.verticies then -- trust but verify
                        local r = 0
                        for i = 1, #zone.verticies do
                            local dist = mist.utils.get2DDist(zone.point, zone.verticies[i])
                            if dist > r then
                                r = mist.utils.deepCopy(dist)
                            end
                        end
                        zone.radius = r
                    
                    end

					mist.DBs.zonesByName[zone_data.name] = zone
					mist.DBs.zonesByNum[#mist.DBs.zonesByNum + 1] = mist.utils.deepCopy(zone)	--[[deepcopy so that the zone in zones_by_name and the zone in
																								zones_by_num se are different objects.. don't want them linked.]]
				end
			end
		end
        
        mist.DBs.drawingByName = {}
        mist.DBs.drawingIndexed = {}
        
        if env.mission.drawings and env.mission.drawings.layers then
            for i = 1, #env.mission.drawings.layers do
                local l = env.mission.drawings.layers[i]
                
                for j = 1, #l.objects do
                    local copy = mist.utils.deepCopy(l.objects[j])
                    --log:warn(copy)
                    local doOffset = false
                    copy.layer = l.name

                    local theta = copy.angle or 0
                    theta = math.rad(theta)
                    if copy.primitiveType == "Polygon" then
                        
                        if copy.polygonMode == 'rect' then
                            local h, w = copy.height, copy.width
                            copy.points = {}
                            copy.points[1] = {x = h/2, y = w/2}
                            copy.points[2] = {x = -h/2, y = w/2}
                            copy.points[3] = {x = -h/2, y = -w/2}
                            copy.points[4] = {x = h/2, y = -w/2}
                            doOffset = true
                        elseif copy.polygonMode == "circle" then
                            copy.points = {x = copy.mapX, y = copy.mapY}
                        elseif copy.polygonMode == 'oval' then
                            copy.points = {}
                            local numPoints = 24
                            local angleStep = (math.pi*2)/numPoints
                            doOffset = true
                            for v = 1, numPoints do
                                local pointAngle = v * angleStep
                                local x = copy.r1 * math.cos(pointAngle) 
                                local y = copy.r2 * math.sin(pointAngle) 
                                
                                table.insert(copy.points,{x=x,y=y})
                                
                            end
                        elseif copy.polygonMode == "arrow" then
                            doOffset = true
                         end
                       

                        if theta ~= 0 and copy.points and doOffset == true then
                            
                            --log:warn('offsetting Values')
                            for p = 1, #copy.points do
                                local offset = mist.vec.rotateVec2(copy.points[p], theta)
                                copy.points[p] = offset 
                            end
                           --log:warn(copy.points[1])
                        end
                    
                    elseif copy.primitiveType == "Line" and copy.closed == true then
                       table.insert(copy.points, mist.utils.deepCopy(copy.points[1]))
                    end
                    if copy.points and #copy.points > 1 then
                        for u = 1, #copy.points do
                            copy.points[u].x = mist.utils.round(copy.points[u].x + copy.mapX, 2)
                            copy.points[u].y = mist.utils.round(copy.points[u].y + copy.mapY, 2)
                        end
                    
                    end
                    if mist.DBs.drawingByName[copy.name] then
                        log:warn("Drawing by the name of [ $1 ] already exists in DB. Failed to add to mist.DBs.drawingByName.", copy.name)
                    else
                    
                        mist.DBs.drawingByName[copy.name] = copy
                    end
                    table.insert(mist.DBs.drawingIndexed, copy)
                end
            
            end
        
        end
        

		mist.DBs.navPoints = {}
		mist.DBs.units = {}
		--Build mist.db.units and mist.DBs.navPoints
		for coa_name_miz, coa_data in pairs(env.mission.coalition) do
            local coa_name = coa_name_miz
            if string.lower(coa_name_miz) == 'neutrals' then
                coa_name = 'neutral'
            end
			if type(coa_data) == 'table' then
				mist.DBs.units[coa_name] = {}
                
                if coa_data.bullseye then 
                    mist.DBs.missionData.bullseye[coa_name] = {}
                    mist.DBs.missionData.bullseye[coa_name].x = coa_data.bullseye.x
                    mist.DBs.missionData.bullseye[coa_name].y = coa_data.bullseye.y
                end
				-- build nav points DB
				mist.DBs.navPoints[coa_name] = {}
				if coa_data.nav_points then --navpoints
					--mist.debug.writeData (mist.utils.serialize,{'NavPoints',coa_data.nav_points}, 'NavPoints.txt')
					for nav_ind, nav_data in pairs(coa_data.nav_points) do

						if type(nav_data) == 'table' then
							mist.DBs.navPoints[coa_name][nav_ind] = mist.utils.deepCopy(nav_data)

							mist.DBs.navPoints[coa_name][nav_ind].name = nav_data.callsignStr	-- name is a little bit more self-explanatory.
							mist.DBs.navPoints[coa_name][nav_ind].point = {}	-- point is used by SSE, support it.
							mist.DBs.navPoints[coa_name][nav_ind].point.x = nav_data.x
							mist.DBs.navPoints[coa_name][nav_ind].point.y = 0
							mist.DBs.navPoints[coa_name][nav_ind].point.z = nav_data.y
						end
					end
				end
				if coa_data.country then --there is a country table
					for cntry_id, cntry_data in pairs(coa_data.country) do

						local countryName = string.lower(cntry_data.name)
                        if cntry_data.id and country.names[cntry_data.id] then
                            countryName = string.lower(country.names[cntry_data.id])
                        end
						mist.DBs.units[coa_name][countryName] = {}
						mist.DBs.units[coa_name][countryName].countryId = cntry_data.id

						if type(cntry_data) == 'table' then	--just making sure

							for obj_cat_name, obj_cat_data in pairs(cntry_data) do

								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

									local category = obj_cat_name

									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	--there's a group!

										mist.DBs.units[coa_name][countryName][category] = {}

										for group_num, group_data in pairs(obj_cat_data.group) do

											if group_data and group_data.units and type(group_data.units) == 'table' then	--making sure again- this is a valid group

												mist.DBs.units[coa_name][countryName][category][group_num] = {}
												local groupName = group_data.name
												if env.mission.version > 7 and env.mission.version < 19 then
													groupName = env.getValueDictByKey(groupName)
												end
												mist.DBs.units[coa_name][countryName][category][group_num].groupName = groupName
												mist.DBs.units[coa_name][countryName][category][group_num].groupId = group_data.groupId
												mist.DBs.units[coa_name][countryName][category][group_num].category = category
												mist.DBs.units[coa_name][countryName][category][group_num].coalition = coa_name
												mist.DBs.units[coa_name][countryName][category][group_num].country = countryName
												mist.DBs.units[coa_name][countryName][category][group_num].countryId = cntry_data.id
												mist.DBs.units[coa_name][countryName][category][group_num].startTime = group_data.start_time
												mist.DBs.units[coa_name][countryName][category][group_num].task = group_data.task
												mist.DBs.units[coa_name][countryName][category][group_num].hidden = group_data.hidden

												mist.DBs.units[coa_name][countryName][category][group_num].units = {}

												mist.DBs.units[coa_name][countryName][category][group_num].radioSet = group_data.radioSet
												mist.DBs.units[coa_name][countryName][category][group_num].uncontrolled = group_data.uncontrolled
												mist.DBs.units[coa_name][countryName][category][group_num].frequency = group_data.frequency
												mist.DBs.units[coa_name][countryName][category][group_num].modulation = group_data.modulation

												for unit_num, unit_data in pairs(group_data.units) do
													local units_tbl = mist.DBs.units[coa_name][countryName][category][group_num].units	--pointer to the units table for this group

													units_tbl[unit_num] = {}
													if env.mission.version > 7 and env.mission.version < 19 then
														units_tbl[unit_num].unitName = env.getValueDictByKey(unit_data.name)
													else
														units_tbl[unit_num].unitName = unit_data.name
													end
													units_tbl[unit_num].type = unit_data.type
													units_tbl[unit_num].skill = unit_data.skill	--will be nil for statics
													units_tbl[unit_num].unitId = unit_data.unitId
													units_tbl[unit_num].category = category
													units_tbl[unit_num].coalition = coa_name
													units_tbl[unit_num].country = countryName
													units_tbl[unit_num].countryId = cntry_data.id
													units_tbl[unit_num].heading = unit_data.heading
													units_tbl[unit_num].playerCanDrive = unit_data.playerCanDrive
													units_tbl[unit_num].alt = unit_data.alt
													units_tbl[unit_num].alt_type = unit_data.alt_type
													units_tbl[unit_num].speed = unit_data.speed
													units_tbl[unit_num].livery_id = unit_data.livery_id
													if unit_data.point then	--ME currently does not work like this, but it might one day
														units_tbl[unit_num].point = unit_data.point
													else
														units_tbl[unit_num].point = {}
														units_tbl[unit_num].point.x = unit_data.x
														units_tbl[unit_num].point.y = unit_data.y
													end
													units_tbl[unit_num].x = unit_data.x
													units_tbl[unit_num].y = unit_data.y

													units_tbl[unit_num].callsign = unit_data.callsign
													units_tbl[unit_num].onboard_num = unit_data.onboard_num
													units_tbl[unit_num].hardpoint_racks = unit_data.hardpoint_racks
													units_tbl[unit_num].psi = unit_data.psi


													units_tbl[unit_num].groupName = groupName
													units_tbl[unit_num].groupId = group_data.groupId

													if unit_data.AddPropAircraft then
														units_tbl[unit_num].AddPropAircraft = unit_data.AddPropAircraft
													end

													if category == 'static' then
														units_tbl[unit_num].categoryStatic = unit_data.category
														units_tbl[unit_num].shape_name = unit_data.shape_name
                                                        units_tbl[unit_num].linkUnit = unit_data.linkUnit
														if unit_data.mass then
															units_tbl[unit_num].mass = unit_data.mass
														end

														if unit_data.canCargo then
															units_tbl[unit_num].canCargo = unit_data.canCargo
														end
													end

												end --for unit_num, unit_data in pairs(group_data.units) do
											end --if group_data and group_data.units then
										end --for group_num, group_data in pairs(obj_cat_data.group) do
									end --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
								end --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
							end --for obj_cat_name, obj_cat_data in pairs(cntry_data) do
						end --if type(cntry_data) == 'table' then
					end --for cntry_id, cntry_data in pairs(coa_data.country) do
				end --if coa_data.country then --there is a country table
			end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then
		end --for coa_name, coa_data in pairs(mission.coalition) do

		mist.DBs.unitsByName = {}
		mist.DBs.unitsById = {}
		mist.DBs.unitsByCat = {}

		mist.DBs.unitsByCat.helicopter = {}	-- adding default categories
		mist.DBs.unitsByCat.plane = {}
		mist.DBs.unitsByCat.ship = {}
		mist.DBs.unitsByCat.static = {}
		mist.DBs.unitsByCat.vehicle = {}

		mist.DBs.unitsByNum = {}

		mist.DBs.groupsByName = {}
		mist.DBs.groupsById = {}
		mist.DBs.humansByName = {}
		mist.DBs.humansById = {}

		mist.DBs.dynGroupsAdded = {} -- will be filled by mist.dbUpdate from dynamically spawned groups
		mist.DBs.activeHumans = {}

		mist.DBs.aliveUnits = {}	-- will be filled in by the "updateAliveUnits" coroutine in mist.main.

		mist.DBs.removedAliveUnits = {} -- will be filled in by the "updateAliveUnits" coroutine in mist.main.

		mist.DBs.const = {}

		-- not accessible by SSE, must use static list :-/
		mist.DBs.const.callsigns = {
			['NATO'] = {
				['rules'] = {
					['groupLimit'] = 9,
				},
				['AWACS'] = {
					['Overlord'] = 1,
					['Magic'] = 2,
					['Wizard'] = 3,
					['Focus'] =	 4,
					['Darkstar'] =	 5,
				},
                ['TANKER'] = {
					['Texaco'] = 1,
					['Arco'] = 2,
					['Shell'] = 3,
				},
                ['TRANSPORT'] = {
                    ['Heavy'] = 9,
                    ['Trash'] = 10,
                    ['Cargo'] = 11,
                    ['Ascot'] = 12,
				['JTAC'] = {
					['Axeman'] = 1,
					['Darknight'] = 2,
					['Warrior']	= 3,
					['Pointer']	= 4,
					['Eyeball'] = 5,
					['Moonbeam'] = 6,
					['Whiplash'] = 7,
					['Finger'] = 8,
					['Pinpoint'] = 9,
					['Ferret'] = 10,
					['Shaba'] = 11,
					['Playboy'] = 12,
					['Hammer'] = 13,
					['Jaguar'] = 14,
					['Deathstar'] =	15,
					['Anvil'] = 16,
					['Firefly']	= 17,
					['Mantis'] = 18,
					['Badger'] = 19,
				},
				['aircraft'] = {
					['Enfield'] = 1,
					['Springfield'] = 2,
					['Uzi']	= 3,
					['Colt'] = 4,
					['Dodge'] =	5,
					['Ford'] = 6,
					['Chevy'] = 7,
					['Pontiac'] = 8,
				},

				['unique'] = {
					['A10'] = {
						['Hawg'] = 9,
						['Boar'] = 10,
						['Pig'] = 11,
						['Tusk'] = 12,
						['rules'] = {
							['canUseAircraft'] = true,
							['appliesTo'] = {
								'A-10C_2',
                                'A-10C',
								'A-10A',
							},
						},
                    },
					['f16'] = {
                         Viper = 9,
                         Venom = 10,
                         Lobo = 11,
                         Cowboy = 12,
                         Python = 13,
                         Rattler =14,
                         Panther = 15,
                         Wolf = 16,
                         Weasel = 17,
                         Wild = 18,
                         Ninja = 19,
                         Jedi = 20,
                         rules = {
                            ['canUseAircraft'] = true,
                            ['appliesTo'] = {
                                'F-16C_50',
                                'F-16C bl.52d',
                                'F-16C bl.50',
                                'F-16A MLU',
                                'F-16A',
                            },
                         },

                    },
					['f18'] = {
						['Hornet'] = 9,
						['Squid'] = 10,
						['Ragin'] = 11,
						['Roman'] = 12,
                         Sting = 13,
                         Jury =14,
                         Jokey = 15,
                         Ram = 16,
                         Hawk = 17,
                         Devil = 18,
                         Check = 19,
                         Snake = 20,
						['rules'] = {
							['canUseAircraft'] = true,
							['appliesTo'] = {
								
                                "FA-18C_hornet",
								'F/A-18C',
							},
						},
                    },
                    ['b1'] = {
						['Bone'] = 9,
						['Dark'] = 10,
						['Vader'] = 11,
						['rules'] = {
							['canUseAircraft'] = true,
							['appliesTo'] = {
								'B-1B',
							},
						},
                    },
                    ['b52'] = {
						['Buff'] = 9,
						['Dump'] = 10,
						['Kenworth'] = 11,
						['rules'] = {
							['canUseAircraft'] = true,
							['appliesTo'] = {
								'B-52H',
							},
						},
                    },
                    ['f15e'] = {
						['Dude'] = 9,
						['Thud'] = 10,
						['Gunny'] = 11,
						['Trek'] = 12,
                         Sniper = 13,
                         Sled =14,
                         Best = 15,
                         Jazz = 16,
                         Rage = 17,
                         Tahoe = 18,
						['rules'] = {
							['canUseAircraft'] = true,
							['appliesTo'] = {
								'F-15E',
                                --'F-15ERAZBAM',
							},
						},
                    },

                },
            },
        },
    }
		mist.DBs.const.shapeNames = {
			["Landmine"] = "landmine",
			["FARP CP Blindage"] = "kp_ug",
			["Subsidiary structure C"] = "saray-c",
			["Barracks 2"] = "kazarma2",
			["Small house 2C"] = "dom2c",
			["Military staff"] = "aviashtab",
			["Tech hangar A"] = "ceh_ang_a",
			["Oil derrick"] = "neftevyshka",
			["Tech combine"] = "kombinat",
			["Garage B"] = "garage_b",
			["Airshow_Crowd"] = "Crowd1",
			["Hangar A"] = "angar_a",
			["Repair workshop"] = "tech",
			["Subsidiary structure D"] = "saray-d",
			["FARP Ammo Dump Coating"] = "SetkaKP",
			["Small house 1C area"] = "dom2c-all",
			["Tank 2"] = "airbase_tbilisi_tank_01",
			["Boiler-house A"] = "kotelnaya_a",
			["Workshop A"] = "tec_a",
			["Small werehouse 1"] = "s1",
			["Garage small B"] = "garagh-small-b",
			["Small werehouse 4"] = "s4",
			["Shop"] = "magazin",
			["Subsidiary structure B"] = "saray-b",
			["FARP Fuel Depot"] = "GSM Rus",
			["Coach cargo"] = "wagon-gruz",
			["Electric power box"] = "tr_budka",
			["Tank 3"] = "airbase_tbilisi_tank_02",
			["Red_Flag"] = "H-flag_R",
			["Container red 3"] = "konteiner_red3",
			["Garage A"] = "garage_a",
			["Hangar B"] = "angar_b",
			["Black_Tyre"] = "H-tyre_B",
			["Cafe"] = "stolovaya",
			["Restaurant 1"] = "restoran1",
			["Subsidiary structure A"] = "saray-a",
			["Container white"] = "konteiner_white",
			["Warehouse"] = "sklad",
			["Tank"] = "bak",
			["Railway crossing B"] = "pereezd_small",
			["Subsidiary structure F"] = "saray-f",
			["Farm A"] = "ferma_a",
			["Small werehouse 3"] = "s3",
			["Water tower A"] = "wodokachka_a",
			["Railway station"] = "r_vok_sd",
			["Coach a tank blue"] = "wagon-cisterna_blue",
			["Supermarket A"] = "uniwersam_a",
			["Coach a platform"] = "wagon-platforma",
			["Garage small A"] = "garagh-small-a",
			["TV tower"] = "tele_bash",
			["Comms tower M"] = "tele_bash_m",
			["Small house 1A"] = "domik1a",
			["Farm B"] = "ferma_b",
			["GeneratorF"] = "GeneratorF",
			["Cargo1"] = "ab-212_cargo",
			["Container red 2"] = "konteiner_red2",
			["Subsidiary structure E"] = "saray-e",
			["Coach a passenger"] = "wagon-pass",
			["Black_Tyre_WF"] = "H-tyre_B_WF",
			["Electric locomotive"] = "elektrowoz",
			["Shelter"] = "ukrytie",
			["Coach a tank yellow"] = "wagon-cisterna_yellow",
			["Railway crossing A"] = "pereezd_big",
			[".Ammunition depot"] = "SkladC",
			["Small werehouse 2"] = "s2",
			["Windsock"] = "H-Windsock_RW",
			["Shelter B"] = "ukrytie_b",
			["Fuel tank"] = "toplivo-bak",
			["Locomotive"] = "teplowoz",
			[".Command Center"] = "ComCenter",
			["Pump station"] = "nasos",
			["Black_Tyre_RF"] = "H-tyre_B_RF",
			["Coach cargo open"] = "wagon-gruz-otkr",
			["Subsidiary structure 3"] = "hozdomik3",
			["FARP Tent"] = "PalatkaB",
			["White_Tyre"] = "H-tyre_W",
			["Subsidiary structure G"] = "saray-g",
			["Container red 1"] = "konteiner_red1",
			["Small house 1B area"] = "domik1b-all",
			["Subsidiary structure 1"] = "hozdomik1",
			["Container brown"] = "konteiner_brown",
			["Small house 1B"] = "domik1b",
			["Subsidiary structure 2"] = "hozdomik2",
			["Chemical tank A"] = "him_bak_a",
			["WC"] = "WC",
			["Small house 1A area"] = "domik1a-all",
			["White_Flag"] = "H-Flag_W",
			["Airshow_Cone"] = "Comp_cone",
            ["Bulk Cargo Ship Ivanov"] = "barge-1",
            ["Bulk Cargo Ship Yakushev"] = "barge-2",
            ["Outpost"]="block",
            ["Road outpost"]="block-onroad",
            ["Container camo"] = "bw_container_cargo",
            ["Tech Hangar A"] = "ceh_ang_a",
            ["Bunker 1"] = "dot",
            ["Bunker 2"] = "dot2",
            ["Tanker Elnya 160"] = "elnya",
            ["F-shape barrier"] = "f_bar_cargo",
            ["Helipad Single"] = "farp",
            ["FARP"] = "farps",
            ["Fueltank"] = "fueltank_cargo",
            ["Gate"] = "gate",
            ["FARP Fuel Depot"] = "gsm rus",
            ["Armed house"] = "home1_a",
            ["FARP Command Post"] = "kp-ug",
            ["Watch Tower Armed"] = "ohr-vyshka",
            ["Oiltank"] = "oiltank_cargo",
            ["Pipes small"] = "pipes_small_cargo",
            ["Pipes big"] = "pipes_big_cargo",
            ["Oil platform"] = "plavbaza",
            ["Tetrapod"] = "tetrapod_cargo",
            ["Fuel tank"] = "toplivo",
            ["Trunks long"] = "trunks_long_cargo",
            ["Trunks small"] = "trunks_small_cargo",
            ["Passenger liner"] = "yastrebow",
            ["Passenger boat"] = "zwezdny",
            ["Oil rig"] = "oil_platform",
            ["Gas platform"] = "gas_platform",
            ["Container 20ft"] = "container_20ft",
            ["Container 40ft"] = "container_40ft",
            ["Downed pilot"] = "cadaver",
            ["Parachute"] = "parash",
            ["Pilot F15 Parachute"] = "pilot_f15_parachute",
            ["Pilot standing"] = "pilot_parashut",
		}
		
		
		-- create mist.DBs.oldAliveUnits
		-- do
		-- local intermediate_alive_units = {}	-- between 0 and 0.5 secs old
		-- local function make_old_alive_units() -- called every 0.5 secs, makes the old_alive_units DB which is just a copy of alive_units that is 0.5 to 1 sec old
		-- if intermediate_alive_units then
		-- mist.DBs.oldAliveUnits = mist.utils.deepCopy(intermediate_alive_units)
		-- end
		-- intermediate_alive_units = mist.utils.deepCopy(mist.DBs.aliveUnits)
		-- timer.scheduleFunction(make_old_alive_units, nil, timer.getTime() + 0.5)
		-- end

		-- make_old_alive_units()
		-- end

		--Build DBs
		for coa_name, coa_data in pairs(mist.DBs.units) do
			for cntry_name, cntry_data in pairs(coa_data) do
				for category_name, category_data in pairs(cntry_data) do
					if type(category_data) == 'table' then
						for group_ind, group_data in pairs(category_data) do
							if type(group_data) == 'table' and group_data.units and type(group_data.units) == 'table' and #group_data.units > 0 then	-- OCD paradigm programming
								mist.DBs.groupsByName[group_data.groupName] = mist.utils.deepCopy(group_data)
								mist.DBs.groupsById[group_data.groupId] = mist.utils.deepCopy(group_data)
								for unit_ind, unit_data in pairs(group_data.units) do
									mist.DBs.unitsByName[unit_data.unitName] = mist.utils.deepCopy(unit_data)
									mist.DBs.unitsById[unit_data.unitId] = mist.utils.deepCopy(unit_data)

									mist.DBs.unitsByCat[unit_data.category] = mist.DBs.unitsByCat[unit_data.category] or {} -- future-proofing against new categories...
									table.insert(mist.DBs.unitsByCat[unit_data.category], mist.utils.deepCopy(unit_data))
									--dbLog:info('inserting $1', unit_data.unitName)
									table.insert(mist.DBs.unitsByNum, mist.utils.deepCopy(unit_data))

									if unit_data.skill and (unit_data.skill == "Client" or unit_data.skill == "Player") then
										mist.DBs.humansByName[unit_data.unitName] = mist.utils.deepCopy(unit_data)
										mist.DBs.humansById[unit_data.unitId] = mist.utils.deepCopy(unit_data)
										--if Unit.getByName(unit_data.unitName) then
										--	mist.DBs.activeHumans[unit_data.unitName] = mist.utils.deepCopy(unit_data)
										--	mist.DBs.activeHumans[unit_data.unitName].playerName = Unit.getByName(unit_data.unitName):getPlayerName()
										--end
									end
								end
							end
						end
					end
				end
			end
		end

		--DynDBs
		mist.DBs.MEunits = mist.utils.deepCopy(mist.DBs.units)
		mist.DBs.MEunitsByName = mist.utils.deepCopy(mist.DBs.unitsByName)
		mist.DBs.MEunitsById = mist.utils.deepCopy(mist.DBs.unitsById)
		mist.DBs.MEunitsByCat = mist.utils.deepCopy(mist.DBs.unitsByCat)
		mist.DBs.MEunitsByNum = mist.utils.deepCopy(mist.DBs.unitsByNum)
		mist.DBs.MEgroupsByName = mist.utils.deepCopy(mist.DBs.groupsByName)
		mist.DBs.MEgroupsById = mist.utils.deepCopy(mist.DBs.groupsById)

		mist.DBs.deadObjects = {}
               
        do
			local mt = {}

			function mt.__newindex(t, key, val)
				local original_key = key --only for duplicate runtime IDs.
				local key_ind = 1
				while mist.DBs.deadObjects[key] do
					--dbLog:warn('duplicate runtime id of previously dead object key: $1', key)
					key = tostring(original_key) .. ' #' .. tostring(key_ind)
					key_ind = key_ind + 1
				end

				if mist.DBs.aliveUnits and mist.DBs.aliveUnits[val.object.id_] then
					----dbLog:info('object found in alive_units')
					val.objectData = mist.utils.deepCopy(mist.DBs.aliveUnits[val.object.id_])
					local pos = Object.getPosition(val.object)
					if pos then
						val.objectPos = pos.p
					end
					val.objectType = mist.DBs.aliveUnits[val.object.id_].category

				elseif mist.DBs.removedAliveUnits and mist.DBs.removedAliveUnits[val.object.id_] then	-- it didn't exist in alive_units, check old_alive_units
					----dbLog:info('object found in old_alive_units')
					val.objectData = mist.utils.deepCopy(mist.DBs.removedAliveUnits[val.object.id_])
					local pos = Object.getPosition(val.object)
					if pos then
						val.objectPos = pos.p
					end
					val.objectType = mist.DBs.removedAliveUnits[val.object.id_].category

				else	--attempt to determine if static object...
					----dbLog:info('object not found in alive units or old alive units')
					local pos = Object.getPosition(val.object)
					if pos then
						local static_found = false
						for ind, static in pairs(mist.DBs.unitsByCat.static) do
							if ((pos.p.x - static.point.x)^2 + (pos.p.z - static.point.y)^2)^0.5 < 0.1 then --really, it should be zero...
								--dbLog:info('correlated dead static object to position')
								val.objectData = static
								val.objectPos = pos.p
								val.objectType = 'static'
								static_found = true
								break
							end
						end
						if not static_found then
							val.objectPos = pos.p
							val.objectType = 'building'
						end
					else
						val.objectType = 'unknown'
					end
				end
				rawset(t, key, val)
			end

			setmetatable(mist.DBs.deadObjects, mt)
		end

		do -- mist unitID funcs
			for id, idData in pairs(mist.DBs.unitsById) do
				if idData.unitId > mist.nextUnitId then
					mist.nextUnitId = mist.utils.deepCopy(idData.unitId)
				end
				if idData.groupId > mist.nextGroupId then
					mist.nextGroupId = mist.utils.deepCopy(idData.groupId)
				end
			end
		end


	end

	local function updateAliveUnits()	-- coroutine function
		local lalive_units = mist.DBs.aliveUnits -- local references for faster execution
		local lunits = mist.DBs.unitsByNum
		local ldeepcopy = mist.utils.deepCopy
		local lUnit = Unit
		local lremovedAliveUnits = mist.DBs.removedAliveUnits
		local updatedUnits = {}

		if #lunits > 0 then
			local units_per_run = math.ceil(#lunits/20)
			if units_per_run < 5 then
				units_per_run = 5
			end

			for i = 1, #lunits do
				if lunits[i].category ~= 'static' then -- can't get statics with Unit.getByName :(
					local unit = lUnit.getByName(lunits[i].unitName)
					if unit then
						----dbLog:info("unit named $1 alive!", lunits[i].unitName) -- spammy
						local pos = unit:getPosition()
						local newtbl = ldeepcopy(lunits[i])
						if pos then
							newtbl.pos = pos.p
						end
						newtbl.unit = unit
						--newtbl.rt_id = unit.id_
						lalive_units[unit.id_] = newtbl
						updatedUnits[unit.id_] = true
					end
				end
				if i%units_per_run == 0 then
					coroutine.yield()
				end
			end
			-- All units updated, remove any "alive" units that were not updated- they are dead!
			for unit_id, unit in pairs(lalive_units) do
				if not updatedUnits[unit_id] then
					lremovedAliveUnits[unit_id] = unit
					lalive_units[unit_id] = nil
				end
			end
		end
	end

	local function dbUpdate(event, objType)
		--dbLog:info('dbUpdate')
		local newTable = {}
		newTable.startTime =	0
		if type(event) == 'string' then -- if name of an object.
			local newObject
			if Group.getByName(event) then
				newObject = Group.getByName(event)
			elseif StaticObject.getByName(event) then
				newObject = StaticObject.getByName(event)
				--	log:info('its static')
			else
				log:warn('$1 is not a Group or Static Object. This should not be possible. Sent category is: $2', event, objType)
				return false
			end

			newTable.name = newObject:getName()
			newTable.groupId = tonumber(newObject:getID())
			newTable.groupName = newObject:getName()
			local unitOneRef
			if objType == 'static' then
				unitOneRef = newObject
				newTable.countryId = tonumber(newObject:getCountry())
				newTable.coalitionId = tonumber(newObject:getCoalition())
				newTable.category = 'static'
			else
				unitOneRef = newObject:getUnits()
				if #unitOneRef > 0 and unitOneRef[1] and type(unitOneRef[1]) == 'table' then
                    newTable.countryId = tonumber(unitOneRef[1]:getCountry())
                    newTable.coalitionId = tonumber(unitOneRef[1]:getCoalition())
                    newTable.category = tonumber(newObject:getCategory())
                else
                    log:warn('getUnits failed to return on $1 ; Built Data: $2.', event, newTable)
                    return false
                end
			end
			for countryData, countryId in pairs(country.id) do
				if newTable.country and string.upper(countryData) == string.upper(newTable.country) or countryId == newTable.countryId then
					newTable.countryId = countryId
					newTable.country = string.lower(countryData)
					for coaData, coaId in pairs(coalition.side) do
						if coaId == coalition.getCountryCoalition(countryId) then
							newTable.coalition = string.lower(coaData)
						end
					end
				end
			end
			for catData, catId in pairs(Unit.Category) do
				if objType == 'group' and Group.getByName(newTable.groupName):isExist() then
					if catId == Group.getByName(newTable.groupName):getCategory() then
						newTable.category = string.lower(catData)
					end
				elseif objType == 'static' and StaticObject.getByName(newTable.groupName):isExist() then
					if catId == StaticObject.getByName(newTable.groupName):getCategory() then
						newTable.category = string.lower(catData)
					end

				end
			end
			local gfound = false
			for index, data in pairs(mistAddedGroups) do
				if mist.stringMatch(data.name, newTable.groupName) == true then
					gfound = true
					newTable.task = data.task
					newTable.modulation = data.modulation
					newTable.uncontrolled = data.uncontrolled
					newTable.radioSet = data.radioSet
					newTable.hidden = data.hidden
					newTable.startTime = data.start_time
					mistAddedGroups[index] = nil
				end
			end

			if gfound == false then
				newTable.uncontrolled = false
				newTable.hidden = false
			end

			newTable.units = {}
			if objType == 'group' then
				for unitId, unitData in pairs(unitOneRef) do
					newTable.units[unitId] = {}
					newTable.units[unitId].unitName = unitData:getName()

					newTable.units[unitId].x = mist.utils.round(unitData:getPosition().p.x)
					newTable.units[unitId].y = mist.utils.round(unitData:getPosition().p.z)
					newTable.units[unitId].point = {}
					newTable.units[unitId].point.x = newTable.units[unitId].x
					newTable.units[unitId].point.y = newTable.units[unitId].y
					newTable.units[unitId].alt = mist.utils.round(unitData:getPosition().p.y)
					newTable.units[unitId].speed = mist.vec.mag(unitData:getVelocity())

					newTable.units[unitId].heading = mist.getHeading(unitData, true)

					newTable.units[unitId].type = unitData:getTypeName()
					newTable.units[unitId].unitId = tonumber(unitData:getID())


					newTable.units[unitId].groupName = newTable.groupName
					newTable.units[unitId].groupId = newTable.groupId
					newTable.units[unitId].countryId = newTable.countryId
					newTable.units[unitId].coalitionId = newTable.coalitionId
					newTable.units[unitId].coalition = newTable.coalition
					newTable.units[unitId].country = newTable.country
					local found = false
					for index, data in pairs(mistAddedObjects) do
						if mist.stringMatch(data.name, newTable.units[unitId].unitName) == true then
							found = true
							newTable.units[unitId].livery_id = data.livery_id
							newTable.units[unitId].skill = data.skill
							newTable.units[unitId].alt_type = data.alt_type
							newTable.units[unitId].callsign = data.callsign
							newTable.units[unitId].psi = data.psi
							mistAddedObjects[index] = nil
						end
						if found == false then
							newTable.units[unitId].skill = "High"
							newTable.units[unitId].alt_type = "BARO"
						end
						if newTable.units[unitId].alt_type == "RADIO" then -- raw postition MSL was grabbed for group, but spawn is AGL, so re-offset it
							newTable.units[unitId].alt = (newTable.units[unitId].alt - land.getHeight({x = newTable.units[unitId].x, y = newTable.units[unitId].y}))
						end
					end

				end
			else -- its a static
                newTable.category = 'static'
				newTable.units[1] = {}
				newTable.units[1].unitName = newObject:getName()
				newTable.units[1].category = 'static'
				newTable.units[1].x = mist.utils.round(newObject:getPosition().p.x)
				newTable.units[1].y = mist.utils.round(newObject:getPosition().p.z)
				newTable.units[1].point = {}
				newTable.units[1].point.x = newTable.units[1].x
				newTable.units[1].point.y = newTable.units[1].y
				newTable.units[1].alt = mist.utils.round(newObject:getPosition().p.y)
				newTable.units[1].heading = mist.getHeading(newObject, true)
				newTable.units[1].type = newObject:getTypeName()
				newTable.units[1].unitId = tonumber(newObject:getID())
				newTable.units[1].groupName = newTable.name
				newTable.units[1].groupId = newTable.groupId
				newTable.units[1].countryId = newTable.countryId
				newTable.units[1].country = newTable.country
				newTable.units[1].coalitionId = newTable.coalitionId
				newTable.units[1].coalition = newTable.coalition
				if newObject:getCategory() == 6 and newObject:getCargoDisplayName() then
					local mass = newObject:getCargoDisplayName()
					mass = string.gsub(mass, ' ', '')
					mass = string.gsub(mass, 'kg', '')
					newTable.units[1].mass = tonumber(mass)
					newTable.units[1].categoryStatic = 'Cargos'
					newTable.units[1].canCargo = true
					newTable.units[1].shape_name = 'ab-212_cargo'
				end

				----- search mist added objects for extra data if applicable
				for index, data in pairs(mistAddedObjects) do
					if mist.stringMatch(data.name, newTable.units[1].unitName) == true then
						newTable.units[1].shape_name = data.shape_name -- for statics
						newTable.units[1].livery_id = data.livery_id
						newTable.units[1].airdromeId = data.airdromeId
						newTable.units[1].mass = data.mass
						newTable.units[1].canCargo = data.canCargo
						newTable.units[1].categoryStatic = data.categoryStatic
						newTable.units[1].type = data.type
                        newTable.units[1].linkUnit = data.linkUnit
                        
						mistAddedObjects[index] = nil
                        break
					end
				end
			end
		end
		--mist.debug.writeData(mist.utils.serialize,{'msg', newTable}, timer.getAbsTime() ..'Group.lua')
		newTable.timeAdded = timer.getAbsTime() -- only on the dynGroupsAdded table. For other reference, see start time
		--mist.debug.dumpDBs()
		--end
		--dbLog:info('endDbUpdate')
		return newTable
	end

	--[[DB update code... FRACK. I need to refactor some of it. 
	
	The problem is that the DBs need to account better for shared object names. Needs to write over some data and outright remove other.
	
	If groupName is used then entire group needs to be rewritten
		what to do with old groups units DB entries?. Names cant be assumed to be the same.
	
	
	-- new spawn event check.
	-- event handler filters everything into groups: tempSpawnedGroups
	-- this function then checks DBs to see if data has changed
	]]
	local function checkSpawnedEventsNew()
		if tempSpawnGroupsCounter > 0 then
			--[[local updatesPerRun = math.ceil(#tempSpawnedGroupsCounter/20)
			if updatesPerRun < 5 then
				updatesPerRun = 5
			end]]
			
			--dbLog:info('iterate')
			for name, gData in pairs(tempSpawnedGroups) do
				--env.info(name)
                --dbLog:info(gData)
				local updated = false
                local stillExists = false
                if not gData.checked then 
                    tempSpawnedGroups[name].checked = true -- so if there was an error it will get cleared.
                    local _g = gData.gp or Group.getByName(name)
                    if mist.DBs.groupsByName[name] then
                        -- first check group level properties, groupId, countryId, coalition
                        --dbLog:info('Found in DBs, check if updated')
                        local dbTable = mist.DBs.groupsByName[name]
                        --dbLog:info(dbTable)
                        if gData.type ~= 'static' then
                           -- dbLog:info('Not static')
                          
                            if _g and _g:isExist() == true then 
                                stillExists = true
                                local _u = _g:getUnit(1)

                                if _u and (dbTable.groupId ~= tonumber(_g:getID()) or _u:getCountry() ~= dbTable.countryId or _u:getCoalition() ~= dbTable.coaltionId) then
                                    --dbLog:info('Group Data mismatch')
                                    updated = true
                                else
                                  --  dbLog:info('No Mismatch')
                                end
                            else
                                dbLog:warn('$1 : Group was not accessible', name)
                            end
                        end
                    end			
                    --dbLog:info('Updated: $1', updated)
                    if updated == false and gData.type ~= 'static' then -- time to check units
                       --dbLog:info('No Group Mismatch, Check Units')
                        if _g and _g:isExist() == true then 
                            stillExists = true
                            for index, uObject in pairs(_g:getUnits()) do
                             --dbLog:info(index)
                                if mist.DBs.unitsByName[uObject:getName()] then
                                    --dbLog:info('UnitByName table exists')
                                    local uTable = mist.DBs.unitsByName[uObject:getName()]
                                    if tonumber(uObject:getID()) ~= uTable.unitId or uObject:getTypeName() ~= uTable.type  then
                                        --dbLog:info('Unit Data mismatch')
                                        updated = true
                                        break
                                    end
                                end
                            end
                        end
                    else
                        stillExists = true
                    end

                    if stillExists == true and (updated == true or not mist.DBs.groupsByName[name]) then
                        --dbLog:info('Get Table')
                        local dbData =  dbUpdate(name, gData.type)
                        if dbData and type(dbData) == 'table' then 
                            writeGroups[#writeGroups+1] = {data = dbData, isUpdated = updated}
                        end
                    end
                    -- Work done, so remove
                end
                tempSpawnedGroups[name] = nil
                tempSpawnGroupsCounter = tempSpawnGroupsCounter - 1
			end			
		end	
	end
	
	local function updateDBTables()
		local i = #writeGroups

		local savesPerRun = math.ceil(i/10)
		if savesPerRun < 5 then
			savesPerRun = 5
		end
		if i > 0 then
			--dbLog:info('updateDBTables')
			local ldeepCopy = mist.utils.deepCopy
			for x = 1, i do
				--dbLog:info(writeGroups[x])
				local newTable = writeGroups[x].data
				local updated = writeGroups[x].isUpdated
				local mistCategory
				if type(newTable.category) == 'string' then
					mistCategory = string.lower(newTable.category)
				end

				if string.upper(newTable.category) == 'GROUND_UNIT' then
					mistCategory = 'vehicle'
					newTable.category = mistCategory
				elseif string.upper(newTable.category) == 'AIRPLANE' then
					mistCategory = 'plane'
					newTable.category = mistCategory
				elseif string.upper(newTable.category) == 'HELICOPTER' then
					mistCategory = 'helicopter'
					newTable.category = mistCategory
				elseif string.upper(newTable.category) == 'SHIP' then
					mistCategory = 'ship'
					newTable.category = mistCategory
				end
				--dbLog:info('Update unitsBy')
				for newId, newUnitData in pairs(newTable.units) do
					--dbLog:info(newId)
					newUnitData.category = mistCategory
					if newUnitData.unitId then
						--dbLog:info('byId')
						mist.DBs.unitsById[tonumber(newUnitData.unitId)] = ldeepCopy(newUnitData)
					end
					--dbLog:info(updated)
					if mist.DBs.unitsByName[newUnitData.unitName] and updated == true then--if unit existed before and something was updated, write over the entry for a given unit name just in case.
						--dbLog:info('Updating Unit Tables')
						for i = 1, #mist.DBs.unitsByCat[mistCategory] do
							if mist.DBs.unitsByCat[mistCategory][i].unitName == newUnitData.unitName then
								--dbLog:info('Entry Found, Rewriting for unitsByCat')
								mist.DBs.unitsByCat[mistCategory][i] = ldeepCopy(newUnitData)
								break
							end
						end 
						for i = 1, #mist.DBs.unitsByNum do
							if mist.DBs.unitsByNum[i].unitName == newUnitData.unitName then
								--dbLog:info('Entry Found, Rewriting for unitsByNum')
								mist.DBs.unitsByNum[i] = ldeepCopy(newUnitData)
								break
							end
						end
						
					else
						--dbLog:info('Unitname not in use, add as normal')
						mist.DBs.unitsByCat[mistCategory][#mist.DBs.unitsByCat[mistCategory] + 1] = ldeepCopy(newUnitData)
						mist.DBs.unitsByNum[#mist.DBs.unitsByNum + 1] = ldeepCopy(newUnitData)
					end
					mist.DBs.unitsByName[newUnitData.unitName] = ldeepCopy(newUnitData)

					
				end
				-- this is a really annoying DB to populate. Gotta create new tables in case its missing
				--dbLog:info('write mist.DBs.units')
				if not mist.DBs.units[newTable.coalition] then
					mist.DBs.units[newTable.coalition] = {}
				end

				if not mist.DBs.units[newTable.coalition][newTable.country] then
					mist.DBs.units[newTable.coalition][(newTable.country)] = {}
					mist.DBs.units[newTable.coalition][(newTable.country)].countryId = newTable.countryId
				end
				if not mist.DBs.units[newTable.coalition][newTable.country][mistCategory] then
					mist.DBs.units[newTable.coalition][(newTable.country)][mistCategory] = {}
				end
				
				if updated == true then
					--dbLog:info('Updating DBsUnits')
					for i = 1, #mist.DBs.units[newTable.coalition][(newTable.country)][mistCategory] do
						if mist.DBs.units[newTable.coalition][(newTable.country)][mistCategory][i].groupName == newTable.groupName then
							--dbLog:info('Entry Found, Rewriting')
							mist.DBs.units[newTable.coalition][(newTable.country)][mistCategory][i] = ldeepCopy(newTable)
							break
						end
					end
				else
					mist.DBs.units[newTable.coalition][(newTable.country)][mistCategory][#mist.DBs.units[newTable.coalition][(newTable.country)][mistCategory] + 1] = ldeepCopy(newTable)
				end
				

				if newTable.groupId then
					mist.DBs.groupsById[newTable.groupId] = ldeepCopy(newTable)
				end

				mist.DBs.groupsByName[newTable.name] = ldeepCopy(newTable)
				mist.DBs.dynGroupsAdded[#mist.DBs.dynGroupsAdded + 1] = ldeepCopy(newTable)

				writeGroups[x] = nil
				if x%savesPerRun == 0 then
					coroutine.yield()
				end
			end
			if timer.getTime() > lastUpdateTime then
				lastUpdateTime = timer.getTime()
			end
			--dbLog:info('endUpdateTables')
		end
	end

	local function groupSpawned(event)
		-- dont need to add units spawned in at the start of the mission if mist is loaded in init line
		if event.id == world.event.S_EVENT_BIRTH and timer.getTime0() < timer.getAbsTime() then
			--log:info('unitSpawnEvent')
			--log:info(event)
            --log:info(event.initiator:getTypeName())
				--table.insert(tempSpawnedUnits,(event.initiator))
				-------
				-- New functionality below. 
				-------
			if Object.getCategory(event.initiator) == 1 and not Unit.getPlayerName(event.initiator) then -- simple player check, will need to later check to see if unit was spawned with a player in a flight
				--log:info('Object is a Unit')
				if Unit.getGroup(event.initiator) then
				--	log:info(Unit.getGroup(event.initiator):getName())
                    local g = Unit.getGroup(event.initiator)
					if not tempSpawnedGroups[g:getName()] then
						--log:info('added')
						tempSpawnedGroups[g:getName()] = {type = 'group', gp = g}
						tempSpawnGroupsCounter = tempSpawnGroupsCounter + 1
					end
				else
					log:error('Group not accessible by unit in event handler. This is a DCS bug')
				end
			elseif Object.getCategory(event.initiator) == 3 or Object.getCategory(event.initiator) == 6 then
				--log:info('Object is Static')
				tempSpawnedGroups[StaticObject.getName(event.initiator)] = {type = 'static'}
				tempSpawnGroupsCounter = tempSpawnGroupsCounter + 1
			end
				
			
		end
	end

	local function doScheduledFunctions()
		local i = 1
		while i <= #scheduledTasks do
			if not scheduledTasks[i].rep then -- not a repeated process
				if scheduledTasks[i].t <= timer.getTime() then
					local task = scheduledTasks[i] -- local reference
					table.remove(scheduledTasks, i)
					local err, errmsg = pcall(task.f, unpack(task.vars, 1, table.maxn(task.vars)))
					if not err then
						log:error('Error in scheduled function: $1', errmsg)
					end
					--task.f(unpack(task.vars, 1, table.maxn(task.vars)))	-- do the task, do not increment i
				else
					i = i + 1
				end
			else
				if scheduledTasks[i].st and scheduledTasks[i].st <= timer.getTime() then	 --if a stoptime was specified, and the stop time exceeded
					table.remove(scheduledTasks, i) -- stop time exceeded, do not execute, do not increment i
				elseif scheduledTasks[i].t <= timer.getTime() then
					local task = scheduledTasks[i] -- local reference
					task.t = timer.getTime() + task.rep	--schedule next run
					local err, errmsg = pcall(task.f, unpack(task.vars, 1, table.maxn(task.vars)))
					if not err then
						log:error('Error in scheduled function: $1' .. errmsg)
					end
					--scheduledTasks[i].f(unpack(scheduledTasks[i].vars, 1, table.maxn(scheduledTasks[i].vars)))	-- do the task
					i = i + 1
				else
					i = i + 1
				end
			end
		end
	end

	-- Event handler to start creating the dead_objects table
	local function addDeadObject(event)
		if event.id == world.event.S_EVENT_DEAD or event.id == world.event.S_EVENT_CRASH then
			if event.initiator and event.initiator.id_ and event.initiator.id_ > 0 then

				local id = event.initiator.id_	-- initial ID, could change if there is a duplicate id_ already dead.
				local val = {object = event.initiator} -- the new entry in mist.DBs.deadObjects.

				local original_id = id	--only for duplicate runtime IDs.
				local id_ind = 1
				while mist.DBs.deadObjects[id] do
					--log:info('duplicate runtime id of previously dead object id: $1', id)
					id = tostring(original_id) .. ' #' .. tostring(id_ind)
					id_ind = id_ind + 1
				end

				if mist.DBs.aliveUnits and mist.DBs.aliveUnits[val.object.id_] then
					--log:info('object found in alive_units')
					val.objectData = mist.utils.deepCopy(mist.DBs.aliveUnits[val.object.id_])
					local pos = Object.getPosition(val.object)
					if pos then
						val.objectPos = pos.p
					end
					val.objectType = mist.DBs.aliveUnits[val.object.id_].category
					--[[if mist.DBs.activeHumans[Unit.getName(val.object)] then
					--trigger.action.outText('remove via death: ' .. Unit.getName(val.object),20)
						mist.DBs.activeHumans[Unit.getName(val.object)] = nil
					end]]
				elseif mist.DBs.removedAliveUnits and mist.DBs.removedAliveUnits[val.object.id_] then	-- it didn't exist in alive_units, check old_alive_units
					--log:info('object found in old_alive_units')
					val.objectData = mist.utils.deepCopy(mist.DBs.removedAliveUnits[val.object.id_])
					local pos = Object.getPosition(val.object)
					if pos then
						val.objectPos = pos.p
					end
					val.objectType = mist.DBs.removedAliveUnits[val.object.id_].category

				else	--attempt to determine if static object...
					--log:info('object not found in alive units or old alive units')
					local pos = Object.getPosition(val.object)
					if pos then
						local static_found = false
						for ind, static in pairs(mist.DBs.unitsByCat.static) do
							if ((pos.p.x - static.point.x)^2 + (pos.p.z - static.point.y)^2)^0.5 < 0.1 then --really, it should be zero...
								--log:info('correlated dead static object to position')
								val.objectData = static
								val.objectPos = pos.p
								val.objectType = 'static'
								static_found = true
								break
							end
						end
						if not static_found then
							val.objectPos = pos.p
							val.objectType = 'building'
						end
					else
						val.objectType = 'unknown'
					end
				end
				mist.DBs.deadObjects[id] = val
			end
		end
	end

	--[[
		local function addClientsToActive(event)
			if event.id == world.event.S_EVENT_PLAYER_ENTER_UNIT or event.id == world.event.S_EVENT_BIRTH then
				log:info(event)
				if Unit.getPlayerName(event.initiator) then
					log:info(Unit.getPlayerName(event.initiator))
					local newU = mist.utils.deepCopy(mist.DBs.unitsByName[Unit.getName(event.initiator)])
					newU.playerName = Unit.getPlayerName(event.initiator)
					mist.DBs.activeHumans[Unit.getName(event.initiator)] = newU
					--trigger.action.outText('added: ' .. Unit.getName(event.initiator), 20)
				end
			elseif event.id == world.event.S_EVENT_PLAYER_LEAVE_UNIT and event.initiator then
				if mist.DBs.activeHumans[Unit.getName(event.initiator)] then
					mist.DBs.activeHumans[Unit.getName(event.initiator)] = nil
					-- trigger.action.outText('removed via control: ' .. Unit.getName(event.initiator), 20)
				end
			end
		end

	mist.addEventHandler(addClientsToActive)
	]]
    local function verifyDB()
        --log:warn('verfy Run')
        for coaName, coaId in pairs(coalition.side) do
            --env.info(coaName)
            local gps = coalition.getGroups(coaId)
            for i = 1, #gps do
                if gps[i] and Group.getSize(gps[i]) > 0 then
                    local gName = Group.getName(gps[i])
                    if not mist.DBs.groupsByName[gName] then
                            --env.info(Unit.getID(gUnits[j]) .. ' Not found in DB yet')
                        if not tempSpawnedGroups[gName] then
                            --dbLog:info('added')
                            tempSpawnedGroups[gName] = {type = 'group', gp = gps[i]}
                            tempSpawnGroupsCounter = tempSpawnGroupsCounter + 1
                        end
                    end
                end
            end
            local st = coalition.getStaticObjects(coaId)
            for i = 1, #st do
                local s = st[i]
                if StaticObject.isExist(s) then
                    local name = s:getName()
                    if not mist.DBs.unitsByName[name] then
                       dbLog:warn('$1 Not found in DB yet. ID: $2', name, StaticObject.getID(s))
                       if string.len(name) > 0 then  -- because in this mission someone sent the name was returning as an empty string. Gotta be careful. 
                            tempSpawnedGroups[s:getName()] = {type = 'static'}
                            tempSpawnGroupsCounter = tempSpawnGroupsCounter + 1
                       end
                    end
                end
            end
        
        end
    
    end
    

	--- init function.
	-- creates logger, adds default event handler
	-- and calls main the first time.
	-- @function mist.init
	function mist.init()
        
		-- create logger
		mist.log = mist.Logger:new("MIST", mistSettings.logLevel)
		dbLog = mist.Logger:new('MISTDB', 'warn')
		
		log = mist.log -- log shorthand
		-- set warning log level, showing only
		-- warnings and errors
		--log:setLevel("warning")

		log:info("initializing databases")
		initDBs()

		-- add event handler for group spawns
		mist.addEventHandler(groupSpawned)
		mist.addEventHandler(addDeadObject)
        
        log:warn('Init time: $1', timer.getTime())

		-- call main the first time therafter it reschedules itself.
		mist.main()
		--log:msg('MIST version $1.$2.$3 loaded', mist.majorVersion, mist.minorVersion, mist.build)
        
        mist.scheduleFunction(verifyDB, {}, timer.getTime() + 1)
		return
	end

	--- The main function.
	-- Run 100 times per second.
	-- You shouldn't call this function.
	function mist.main()
		timer.scheduleFunction(mist.main, {}, timer.getTime() + 0.01)	--reschedule first in case of Lua error

		updateTenthSecond = updateTenthSecond + 1
		if updateTenthSecond == 20 then
			updateTenthSecond = 0

			checkSpawnedEventsNew()
			
			if not coroutines.updateDBTables then
				coroutines.updateDBTables = coroutine.create(updateDBTables)
			end

			coroutine.resume(coroutines.updateDBTables)

			if coroutine.status(coroutines.updateDBTables) == 'dead' then
				coroutines.updateDBTables = nil
			end
		end

		--updating alive units
		updateAliveUnitsCounter = updateAliveUnitsCounter + 1
		if updateAliveUnitsCounter == 5 then
			updateAliveUnitsCounter = 0

			if not coroutines.updateAliveUnits then
				coroutines.updateAliveUnits = coroutine.create(updateAliveUnits)
			end

			coroutine.resume(coroutines.updateAliveUnits)

			if coroutine.status(coroutines.updateAliveUnits) == 'dead' then
				coroutines.updateAliveUnits = nil
			end
		end

		doScheduledFunctions()
	end -- end of mist.main

	--- Returns next unit id.
	-- @treturn number next unit id.
	function mist.getNextUnitId()
		mist.nextUnitId = mist.nextUnitId + 1
		if mist.nextUnitId > 6900 and mist.nextUnitId < 30000 then
			mist.nextUnitId = 30000
		end
		return mist.utils.deepCopy(mist.nextUnitId)
	end

	--- Returns next group id.
	-- @treturn number next group id.
	function mist.getNextGroupId()
		mist.nextGroupId = mist.nextGroupId + 1
		if mist.nextGroupId > 6900 and mist.nextGroupId < 30000 then
			mist.nextGroupId = 30000
		end
		return mist.utils.deepCopy(mist.nextGroupId)
	end

	--- Returns timestamp of last database update.
	-- @treturn timestamp of last database update
	function mist.getLastDBUpdateTime()
		return lastUpdateTime
	end

	--- Spawns a static object to the game world.
	-- @todo write good docs
	-- @tparam table staticObj table containing data needed for the object creation
	function mist.dynAddStatic(n)
        --log:info(newObj)
        local newObj = mist.utils.deepCopy(n)
		if newObj.units and newObj.units[1] then -- if its mist format
			for entry, val in pairs(newObj.units[1]) do
				if newObj[entry] and newObj[entry] ~= val or not newObj[entry] then
					newObj[entry] = val
				end
			end
		end
		--log:info(newObj)
		
		local cntry = newObj.country
		if newObj.countryId then
			cntry = newObj.countryId
		end
	
		local newCountry = ''

		for countryId, countryName in pairs(country.name) do
			if type(cntry) == 'string' then
				cntry = cntry:gsub("%s+", "_")
				if tostring(countryName) == string.upper(cntry) then
					newCountry = countryName
				end
			elseif type(cntry) == 'number' then
				if countryId == cntry then
					newCountry = countryName
				end
			end
		end
		
		if newCountry == '' then
			log:error("Country not found: $1", cntry)
			return false
		end
	
		if newObj.clone or not newObj.groupId then
			mistGpId = mistGpId + 1
			newObj.groupId = mistGpId
		end

		if newObj.clone or not newObj.unitId then
			mistUnitId = mistUnitId + 1
			newObj.unitId = mistUnitId
		end


        newObj.name = newObj.name or newObj.unitName
        
		if newObj.clone or not newObj.name then
			mistDynAddIndex[' static '] = mistDynAddIndex[' static '] + 1
			newObj.name = (newCountry .. ' static ' .. mistDynAddIndex[' static '])
		end

		if not newObj.dead then
			newObj.dead = false
		end

		if not newObj.heading then
			newObj.heading = math.random(360)
		end
		
		if newObj.categoryStatic then
			newObj.category = newObj.categoryStatic
		end
		if newObj.mass then
			newObj.category = 'Cargos'
		end
		
		if newObj.shapeName then
			newObj.shape_name = newObj.shapeName
		end
		
		if not newObj.shape_name then
			log:info('shape_name not present')
			if mist.DBs.const.shapeNames[newObj.type] then
				newObj.shape_name = mist.DBs.const.shapeNames[newObj.type]
			end
		end
		
		mistAddedObjects[#mistAddedObjects + 1] = mist.utils.deepCopy(newObj)
		if newObj.x and newObj.y and newObj.type and type(newObj.x) == 'number' and type(newObj.y) == 'number' and type(newObj.type) == 'string' then
			--log:warn(newObj)
			coalition.addStaticObject(country.id[newCountry], newObj)

			return newObj
		end
		log:error("Failed to add static object due to missing or incorrect value. X: $1, Y: $2, Type: $3", newObj.x, newObj.y, newObj.type)
		return false
	end

	--- Spawns a dynamic group into the game world.
	-- Same as coalition.add function in SSE. checks the passed data to see if its valid.
	-- Will generate groupId, groupName, unitId, and unitName if needed
	-- @tparam table newGroup table containting values needed for spawning a group.
	function mist.dynAdd(ng)
        
        local newGroup = mist.utils.deepCopy(ng)
        --log:warn(newGroup)
		--mist.debug.writeData(mist.utils.serialize,{'msg', newGroup}, 'newGroupOrig.lua')
		local cntry = newGroup.country
		if newGroup.countryId then
			cntry = newGroup.countryId
		end

		local groupType = newGroup.category
		local newCountry = ''
		-- validate data
		for countryId, countryName in pairs(country.name) do
			if type(cntry) == 'string' then
				cntry = cntry:gsub("%s+", "_")
				if tostring(countryName) == string.upper(cntry) then
					newCountry = countryName
				end
			elseif type(cntry) == 'number' then
				if countryId == cntry then
					newCountry = countryName
				end
			end
		end

		if newCountry == '' then
			log:error("Country not found: $1", cntry)
			return false
		end

		local newCat = ''
		for catName, catId in pairs(Unit.Category) do
			if type(groupType) == 'string' then
				if tostring(catName) == string.upper(groupType) then
					newCat = catName
				end
			elseif type(groupType) == 'number' then
				if catId == groupType then
					newCat = catName
				end
			end

			if catName == 'GROUND_UNIT' and (string.upper(groupType) == 'VEHICLE' or string.upper(groupType) == 'GROUND') then
				newCat = 'GROUND_UNIT'
			elseif catName == 'AIRPLANE' and string.upper(groupType) == 'PLANE' then
				newCat = 'AIRPLANE'
			end
		end
		local typeName
		if newCat == 'GROUND_UNIT' then
			typeName = ' gnd '
		elseif newCat == 'AIRPLANE' then
			typeName = ' air '
		elseif newCat == 'HELICOPTER' then
			typeName = ' hel '
		elseif newCat == 'SHIP' then
			typeName = ' shp '
		elseif newCat == 'BUILDING' then
			typeName = ' bld '
		end
		if newGroup.clone or not newGroup.groupId then
			mistDynAddIndex[typeName] = mistDynAddIndex[typeName] + 1
			mistGpId = mistGpId + 1
			newGroup.groupId = mistGpId
		end
		if newGroup.groupName or newGroup.name then
			if newGroup.groupName then
				newGroup.name = newGroup.groupName
			elseif newGroup.name then
				newGroup.name = newGroup.name
			end
		end

		if newGroup.clone and mist.DBs.groupsByName[newGroup.name] or not newGroup.name then
            --if newGroup.baseName then
                -- idea of later. So custmozed naming can be created
           -- else
                newGroup.name = tostring(newCountry .. tostring(typeName) .. mistDynAddIndex[typeName])
            --end
		end

		if not newGroup.hidden then
			newGroup.hidden = false
		end

		if not newGroup.visible then
			newGroup.visible = false
		end

		if (newGroup.start_time and type(newGroup.start_time) ~= 'number') or not newGroup.start_time then
			if newGroup.startTime then
				newGroup.start_time = mist.utils.round(newGroup.startTime)
			else
				newGroup.start_time = 0
			end
		end


		for unitIndex, unitData in pairs(newGroup.units) do
			local originalName = newGroup.units[unitIndex].unitName or newGroup.units[unitIndex].name
			if newGroup.clone or not unitData.unitId then
				mistUnitId = mistUnitId + 1
				newGroup.units[unitIndex].unitId = mistUnitId
			end
			if newGroup.units[unitIndex].unitName or newGroup.units[unitIndex].name then
				if newGroup.units[unitIndex].unitName then
					newGroup.units[unitIndex].name = newGroup.units[unitIndex].unitName
				elseif newGroup.units[unitIndex].name then
					newGroup.units[unitIndex].name = newGroup.units[unitIndex].name
				end
			end
			if newGroup.clone or not unitData.name then
				newGroup.units[unitIndex].name = tostring(newGroup.name .. ' unit' .. unitIndex)
			end

			if not unitData.skill then
				newGroup.units[unitIndex].skill = 'Random'
			end

			if newCat == 'AIRPLANE' or newCat == 'HELICOPTER' then
				if newGroup.units[unitIndex].alt_type and newGroup.units[unitIndex].alt_type ~= 'BARO' or not newGroup.units[unitIndex].alt_type then
					newGroup.units[unitIndex].alt_type = 'RADIO'
				end
				if not unitData.speed then
					if newCat == 'AIRPLANE' then
						newGroup.units[unitIndex].speed = 150
					elseif newCat == 'HELICOPTER' then
						newGroup.units[unitIndex].speed = 60
					end
				end
				if not unitData.payload then
					newGroup.units[unitIndex].payload = mist.getPayload(originalName)
				end
				if not unitData.alt then
					if newCat == 'AIRPLANE' then
						newGroup.units[unitIndex].alt = 2000
						newGroup.units[unitIndex].alt_type = 'RADIO'
						newGroup.units[unitIndex].speed = 150
					elseif newCat == 'HELICOPTER' then
						newGroup.units[unitIndex].alt = 500
						newGroup.units[unitIndex].alt_type = 'RADIO'
						newGroup.units[unitIndex].speed = 60
					end
				end
				
			elseif newCat == 'GROUND_UNIT' then
				if nil == unitData.playerCanDrive then
					unitData.playerCanDrive = true
				end
			
			end
			mistAddedObjects[#mistAddedObjects + 1] = mist.utils.deepCopy(newGroup.units[unitIndex])
		end
		mistAddedGroups[#mistAddedGroups + 1] = mist.utils.deepCopy(newGroup)
		if newGroup.route then
            if newGroup.route and not newGroup.route.points then
                if newGroup.route[1] then
                    local copyRoute = mist.utils.deepCopy(newGroup.route)
                    newGroup.route = {}
                    newGroup.route.points = copyRoute
                end
            end
		else -- if aircraft and no route assigned. make a quick and stupid route so AI doesnt RTB immediately
			--if newCat == 'AIRPLANE' or newCat == 'HELICOPTER' then
				newGroup.route = {}
				newGroup.route.points = {}
				newGroup.route.points[1] = {}
			--end
		end
		newGroup.country = newCountry

        -- update and verify any self tasks
        if newGroup.route and newGroup.route.points then 
            for i, pData in pairs(newGroup.route.points) do
                if pData.task and pData.task.params and pData.task.params.tasks and #pData.task.params.tasks > 0 then
                    for tIndex, tData in pairs(pData.task.params.tasks) do
                        if tData.params and tData.params.action then  
                            if tData.params.action.id == "EPLRS" then
                                tData.params.action.params.groupId = newGroup.groupId
                            elseif tData.params.action.id == "ActivateBeacon" or tData.params.action.id == "ActivateICLS" then 
                                tData.params.action.params.unitId = newGroup.units[1].unitId
                            end 
                        end
                    end
                end
            
            end
        end
		--mist.debug.writeData(mist.utils.serialize,{'msg', newGroup}, 'newGroupPushedToAddGroup.lua')
        --log:warn(newGroup)
		-- sanitize table
		newGroup.groupName = nil
		newGroup.clone = nil
		newGroup.category = nil
		newGroup.country = nil

		newGroup.tasks = {}

		for unitIndex, unitData in pairs(newGroup.units) do
			newGroup.units[unitIndex].unitName = nil
		end
        
		coalition.addGroup(country.id[newCountry], Unit.Category[newCat], newGroup)

		return newGroup

	end

	--- Schedules a function.
	-- Modified Slmod task scheduler, superior to timer.scheduleFunction
	-- @tparam function f function to schedule
	-- @tparam table vars array containing all parameters passed to the function
	-- @tparam number t time in seconds from mission start to schedule the function to.
	-- @tparam[opt] number rep time between repetitions of the function
	-- @tparam[opt] number st time in seconds from mission start at which the function
	-- should stop to be rescheduled.
	-- @treturn number scheduled function id.
	function mist.scheduleFunction(f, vars, t, rep, st)
		--verify correct types
		assert(type(f) == 'function', 'variable 1, expected function, got ' .. type(f))
		assert(type(vars) == 'table' or vars == nil, 'variable 2, expected table or nil, got ' .. type(f))
		assert(type(t) == 'number', 'variable 3, expected number, got ' .. type(t))
		assert(type(rep) == 'number' or rep == nil, 'variable 4, expected number or nil, got ' .. type(rep))
		assert(type(st) == 'number' or st == nil, 'variable 5, expected number or nil, got ' .. type(st))
		if not vars then
			vars = {}
		end
		taskId = taskId + 1
		table.insert(scheduledTasks, {f = f, vars = vars, t = t, rep = rep, st = st, id = taskId})
		return taskId
	end

	--- Removes a scheduled function.
	-- @tparam number id function id
	-- @treturn boolean true if function was successfully removed, false otherwise.
	function mist.removeFunction(id)
		local i = 1
		while i <= #scheduledTasks do
			if scheduledTasks[i].id == id then
				table.remove(scheduledTasks, i)
                return true
			else
				i = i + 1
			end
		end
        return false
	end

	--- Registers an event handler.
	-- @tparam function f function handling event
	-- @treturn number id of the event handler
	function mist.addEventHandler(f) --id is optional!
		local handler = {}
		idNum = idNum + 1
		handler.id = idNum
		handler.f = f
		function handler:onEvent(event)
			self.f(event)
		end
		world.addEventHandler(handler)
		return handler.id
	end

	--- Removes event handler with given id.
	-- @tparam number id event handler id
	-- @treturn boolean true on success, false otherwise
	function mist.removeEventHandler(id)
		for key, handler in pairs(world.eventHandlers) do
			if handler.id and handler.id == id then
				world.eventHandlers[key] = nil
				return true
			end
		end
		return false
	end
end

-- Begin common funcs
do
	--- Returns MGRS coordinates as string.
	-- @tparam string MGRS MGRS coordinates
	-- @tparam number acc the accuracy of each easting/northing.
	-- Can be: 0, 1, 2, 3, 4, or 5.
	function mist.tostringMGRS(MGRS, acc)
		if acc == 0 then
			return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph
		else
			return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph .. ' ' .. string.format('%0' .. acc .. 'd', mist.utils.round(MGRS.Easting/(10^(5-acc)), 0))
			.. ' ' .. string.format('%0' .. acc .. 'd', mist.utils.round(MGRS.Northing/(10^(5-acc)), 0))
		end
	end

	--[[acc:
	in DM: decimal point of minutes.
	In DMS: decimal point of seconds.
	position after the decimal of the least significant digit:
	So:
	42.32 - acc of 2.
	]]
	function mist.tostringLL(lat, lon, acc, DMS)

		local latHemi, lonHemi
		if lat > 0 then
			latHemi = 'N'
		else
			latHemi = 'S'
		end

		if lon > 0 then
			lonHemi = 'E'
		else
			lonHemi = 'W'
		end

		lat = math.abs(lat)
		lon = math.abs(lon)

		local latDeg = math.floor(lat)
		local latMin = (lat - latDeg)*60

		local lonDeg = math.floor(lon)
		local lonMin = (lon - lonDeg)*60

		if DMS then	-- degrees, minutes, and seconds.
			local oldLatMin = latMin
			latMin = math.floor(latMin)
			local latSec = mist.utils.round((oldLatMin - latMin)*60, acc)

			local oldLonMin = lonMin
			lonMin = math.floor(lonMin)
			local lonSec = mist.utils.round((oldLonMin - lonMin)*60, acc)

			if latSec == 60 then
				latSec = 0
				latMin = latMin + 1
			end

			if lonSec == 60 then
				lonSec = 0
				lonMin = lonMin + 1
			end

			local secFrmtStr -- create the formatting string for the seconds place
			if acc <= 0 then	-- no decimal place.
				secFrmtStr = '%02d'
			else
				local width = 3 + acc	-- 01.310 - that's a width of 6, for example.
				secFrmtStr = '%0' .. width .. '.' .. acc .. 'f'
			end

			return string.format('%02d', latDeg) .. ' ' .. string.format('%02d', latMin) .. '\' ' .. string.format(secFrmtStr, latSec) .. '"' .. latHemi .. '	 '
			.. string.format('%02d', lonDeg) .. ' ' .. string.format('%02d', lonMin) .. '\' ' .. string.format(secFrmtStr, lonSec) .. '"' .. lonHemi

		else	-- degrees, decimal minutes.
			latMin = mist.utils.round(latMin, acc)
			lonMin = mist.utils.round(lonMin, acc)

			if latMin == 60 then
				latMin = 0
				latDeg = latDeg + 1
			end

			if lonMin == 60 then
				lonMin = 0
				lonDeg = lonDeg + 1
			end

			local minFrmtStr -- create the formatting string for the minutes place
			if acc <= 0 then	-- no decimal place.
				minFrmtStr = '%02d'
			else
				local width = 3 + acc	-- 01.310 - that's a width of 6, for example.
				minFrmtStr = '%0' .. width .. '.' .. acc .. 'f'
			end

			return string.format('%02d', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\'' .. latHemi .. '	 '
			.. string.format('%02d', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\'' .. lonHemi

		end
	end

	--[[ required: az - radian
		required: dist - meters
		optional: alt - meters (set to false or nil if you don't want to use it).
		optional: metric - set true to get dist and alt in km and m.
		precision will always be nearest degree and NM or km.]]
	function mist.tostringBR(az, dist, alt, metric)
		az = mist.utils.round(mist.utils.toDegree(az), 0)

		if metric then
			dist = mist.utils.round(dist/1000, 0)
		else
			dist = mist.utils.round(mist.utils.metersToNM(dist), 0)
		end

		local s = string.format('%03d', az) .. ' for ' .. dist

		if alt then
			if metric then
				s = s .. ' at ' .. mist.utils.round(alt, 0)
			else
				s = s .. ' at ' .. mist.utils.round(mist.utils.metersToFeet(alt), 0)
			end
		end
		return s
	end

	function mist.getNorthCorrection(gPoint)	--gets the correction needed for true north
		local point = mist.utils.deepCopy(gPoint)
		if not point.z then --Vec2; convert to Vec3
			point.z = point.y
			point.y = 0
		end
		local lat, lon = coord.LOtoLL(point)
		local north_posit = coord.LLtoLO(lat + 1, lon)
		return math.atan2(north_posit.z - point.z, north_posit.x - point.x)
	end

	--- Returns skill of the given unit.
	-- @tparam string unitName unit name
	-- @return skill of the unit
	function mist.getUnitSkill(unitName)
		if mist.DBs.unitsByName[unitName] then
			if Unit.getByName(unitName) then
				local lunit = Unit.getByName(unitName)
				local data = mist.DBs.unitsByName[unitName]
				if data.unitName == unitName and data.type == lunit:getTypeName() and data.unitId == tonumber(lunit:getID()) and data.skill then
					return data.skill
				end
			end
		end
		log:error("Unit not found in DB: $1", unitName)
		return false
	end

	--- Returns an array containing a group's units positions.
	--	e.g.
	--		{
	--			[1] = {x = 299435.224, y = -1146632.6773},
	--			[2] = {x = 663324.6563, y = 322424.1112}
	--		}
	--	@tparam number|string groupIdent group id or name
	--	@treturn table array containing positions of each group member
	function mist.getGroupPoints(groupIdent)
		-- search by groupId and allow groupId and groupName as inputs
		local gpId = groupIdent
		if type(groupIdent) == 'string' and not tonumber(groupIdent) then
			if mist.DBs.MEgroupsByName[groupIdent] then
				gpId = mist.DBs.MEgroupsByName[groupIdent].groupId
			else
				log:error("Group not found in mist.DBs.MEgroupsByName: $1", groupIdent)
			end
		end

		for coa_name, coa_data in pairs(env.mission.coalition) do
			if  type(coa_data) == 'table' then
				if coa_data.country then --there is a country table
					for cntry_id, cntry_data in pairs(coa_data.country) do
						for obj_cat_name, obj_cat_data in pairs(cntry_data) do
							if obj_cat_name == "helicopter" or obj_cat_name == "ship" or obj_cat_name == "plane" or obj_cat_name == "vehicle" then	-- only these types have points
								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	--there's a group!
									for group_num, group_data in pairs(obj_cat_data.group) do
										if group_data and group_data.groupId == gpId then -- this is the group we are looking for
											if group_data.route and group_data.route.points and #group_data.route.points > 0 then
												local points = {}
												for point_num, point in pairs(group_data.route.points) do
													if not point.point then
														points[point_num] = { x = point.x, y = point.y }
													else
														points[point_num] = point.point	--it's possible that the ME could move to the point = Vec2 notation.
													end
												end
												return points
											end
											return
										end	--if group_data and group_data.name and group_data.name == 'groupname'
									end --for group_num, group_data in pairs(obj_cat_data.group) do
								end --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
							end --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
						end --for obj_cat_name, obj_cat_data in pairs(cntry_data) do
					end --for cntry_id, cntry_data in pairs(coa_data.country) do
				end --if coa_data.country then --there is a country table
			end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then
		end --for coa_name, coa_data in pairs(mission.coalition) do
	end

	--- getUnitAttitude(unit) return values.
	-- Yaw, AoA, ClimbAngle - relative to earth reference
	-- DOES NOT TAKE INTO ACCOUNT WIND.
	-- @table attitude
	-- @tfield number Heading in radians, range of 0 to 2*pi,
	-- relative to true north.
	-- @tfield number Pitch in radians, range of -pi/2 to pi/2
	-- @tfield number Roll in radians, range of 0 to 2*pi,
	-- right roll is positive direction.
	-- @tfield number Yaw in radians, range of -pi to pi,
	-- right yaw is positive direction.
	-- @tfield number AoA in radians, range of -pi to pi,
	-- rotation of aircraft to the right in comparison to
	-- flight direction being positive.
	-- @tfield number ClimbAngle in radians, range of -pi/2 to pi/2

	--- Returns the attitude of a given unit.
	-- Will work on any unit, even if not an aircraft.
	-- @tparam Unit unit unit whose attitude is returned.
	-- @treturn table @{attitude}
	function mist.getAttitude(unit)
		local unitpos = unit:getPosition()
		if unitpos then

			local Heading = math.atan2(unitpos.x.z, unitpos.x.x)

			Heading = Heading + mist.getNorthCorrection(unitpos.p)

			if Heading < 0 then
				Heading = Heading + 2*math.pi	-- put heading in range of 0 to 2*pi
			end
			---- heading complete.----

			local Pitch = math.asin(unitpos.x.y)
			---- pitch complete.----

			-- now get roll:
			--maybe not the best way to do it, but it works.

			--first, make a vector that is perpendicular to y and unitpos.x with cross product
			local cp = mist.vec.cp(unitpos.x, {x = 0, y = 1, z = 0})

			--now, get dot product of of this cross product with unitpos.z
			local dp = mist.vec.dp(cp, unitpos.z)

			--now get the magnitude of the roll (magnitude of the angle between two vectors is acos(vec1.vec2/|vec1||vec2|)
			local Roll = math.acos(dp/(mist.vec.mag(cp)*mist.vec.mag(unitpos.z)))

			--now, have to get sign of roll.
			-- by convention, making right roll positive
			-- to get sign of roll, use the y component of unitpos.z.	For right roll, y component is negative.

			if unitpos.z.y > 0 then -- left roll, flip the sign of the roll
				Roll = -Roll
			end
			---- roll complete. ----

			--now, work on yaw, AoA, climb, and abs velocity
			local Yaw
			local AoA
			local ClimbAngle

			-- get unit velocity
			local unitvel = unit:getVelocity()
			if mist.vec.mag(unitvel) ~= 0 then --must have non-zero velocity!
				local AxialVel = {}	--unit velocity transformed into aircraft axes directions

				--transform velocity components in direction of aircraft axes.
				AxialVel.x = mist.vec.dp(unitpos.x, unitvel)
				AxialVel.y = mist.vec.dp(unitpos.y, unitvel)
				AxialVel.z = mist.vec.dp(unitpos.z, unitvel)

				--Yaw is the angle between unitpos.x and the x and z velocities
				--define right yaw as positive
				Yaw = math.acos(mist.vec.dp({x = 1, y = 0, z = 0}, {x = AxialVel.x, y = 0, z = AxialVel.z})/mist.vec.mag({x = AxialVel.x, y = 0, z = AxialVel.z}))

				--now set correct direction:
				if AxialVel.z > 0 then
					Yaw = -Yaw
				end

				-- AoA is angle between unitpos.x and the x and y velocities
				AoA = math.acos(mist.vec.dp({x = 1, y = 0, z = 0}, {x = AxialVel.x, y = AxialVel.y, z = 0})/mist.vec.mag({x = AxialVel.x, y = AxialVel.y, z = 0}))

				--now set correct direction:
				if AxialVel.y > 0 then
					AoA = -AoA
				end

				ClimbAngle = math.asin(unitvel.y/mist.vec.mag(unitvel))
			end
			return { Heading = Heading, Pitch = Pitch, Roll = Roll, Yaw = Yaw, AoA = AoA, ClimbAngle = ClimbAngle}
		else
			log:error("Couldn't get unit's position")
		end
	end

	--- Returns heading of given unit.
	-- @tparam Unit unit unit whose heading is returned.
	-- @param rawHeading
	-- @treturn number heading of the unit, in range
	-- of 0 to 2*pi.
	function mist.getHeading(unit, rawHeading)
		local unitpos = unit:getPosition()
		if unitpos then
			local Heading = math.atan2(unitpos.x.z, unitpos.x.x)
			if not rawHeading then
				Heading = Heading + mist.getNorthCorrection(unitpos.p)
			end
			if Heading < 0 then
				Heading = Heading + 2*math.pi	-- put heading in range of 0 to 2*pi
			end
			return Heading
		end
	end

	--- Returns given unit's pitch
	-- @tparam Unit unit unit whose pitch is returned.
	-- @treturn number pitch of given unit
	function mist.getPitch(unit)
		local unitpos = unit:getPosition()
		if unitpos then
			return math.asin(unitpos.x.y)
		end
	end

	--- Returns given unit's roll.
	-- @tparam Unit unit unit whose roll is returned.
	-- @treturn number roll of given unit
	function mist.getRoll(unit)
		local unitpos = unit:getPosition()
		if unitpos then
			-- now get roll:
			--maybe not the best way to do it, but it works.

			--first, make a vector that is perpendicular to y and unitpos.x with cross product
			local cp = mist.vec.cp(unitpos.x, {x = 0, y = 1, z = 0})

			--now, get dot product of of this cross product with unitpos.z
			local dp = mist.vec.dp(cp, unitpos.z)

			--now get the magnitude of the roll (magnitude of the angle between two vectors is acos(vec1.vec2/|vec1||vec2|)
			local Roll = math.acos(dp/(mist.vec.mag(cp)*mist.vec.mag(unitpos.z)))

			--now, have to get sign of roll.
			-- by convention, making right roll positive
			-- to get sign of roll, use the y component of unitpos.z.	For right roll, y component is negative.

			if unitpos.z.y > 0 then -- left roll, flip the sign of the roll
				Roll = -Roll
			end
			return Roll
		end
	end

	--- Returns given unit's yaw.
	-- @tparam Unit unit unit whose yaw is returned.
	-- @treturn number yaw of given unit.
	function mist.getYaw(unit)
		local unitpos = unit:getPosition()
		if unitpos then
			-- get unit velocity
			local unitvel = unit:getVelocity()
			if mist.vec.mag(unitvel) ~= 0 then --must have non-zero velocity!
				local AxialVel = {}	--unit velocity transformed into aircraft axes directions

				--transform velocity components in direction of aircraft axes.
				AxialVel.x = mist.vec.dp(unitpos.x, unitvel)
				AxialVel.y = mist.vec.dp(unitpos.y, unitvel)
				AxialVel.z = mist.vec.dp(unitpos.z, unitvel)

				--Yaw is the angle between unitpos.x and the x and z velocities
				--define right yaw as positive
				local Yaw = math.acos(mist.vec.dp({x = 1, y = 0, z = 0}, {x = AxialVel.x, y = 0, z = AxialVel.z})/mist.vec.mag({x = AxialVel.x, y = 0, z = AxialVel.z}))

				--now set correct direction:
				if AxialVel.z > 0 then
					Yaw = -Yaw
				end
				return Yaw
			end
		end
	end

	--- Returns given unit's angle of attack.
	-- @tparam Unit unit unit to get AoA from.
	-- @treturn number angle of attack of the given unit.
	function mist.getAoA(unit)
		local unitpos = unit:getPosition()
		if unitpos then
			local unitvel = unit:getVelocity()
			if mist.vec.mag(unitvel) ~= 0 then --must have non-zero velocity!
				local AxialVel = {}	--unit velocity transformed into aircraft axes directions

				--transform velocity components in direction of aircraft axes.
				AxialVel.x = mist.vec.dp(unitpos.x, unitvel)
				AxialVel.y = mist.vec.dp(unitpos.y, unitvel)
				AxialVel.z = mist.vec.dp(unitpos.z, unitvel)

				-- AoA is angle between unitpos.x and the x and y velocities
				local AoA = math.acos(mist.vec.dp({x = 1, y = 0, z = 0}, {x = AxialVel.x, y = AxialVel.y, z = 0})/mist.vec.mag({x = AxialVel.x, y = AxialVel.y, z = 0}))

				--now set correct direction:
				if AxialVel.y > 0 then
					AoA = -AoA
				end
				return AoA
			end
		end
	end

	--- Returns given unit's climb angle.
	-- @tparam Unit unit unit to get climb angle from.
	-- @treturn number climb angle of given unit.
	function mist.getClimbAngle(unit)
		local unitpos = unit:getPosition()
		if unitpos then
			local unitvel = unit:getVelocity()
			if mist.vec.mag(unitvel) ~= 0 then --must have non-zero velocity!
				return math.asin(unitvel.y/mist.vec.mag(unitvel))
			end
		end
	end

	--[[--
	Unit name table.
	Many Mist functions require tables of unit names, which are known
	in Mist as UnitNameTables. These follow a special set of shortcuts
	borrowed from Slmod. These shortcuts alleviate the problem of entering
	huge lists of unit names by hand, and in many cases, they remove the
	need to even know the names of the units in the first place!

	These are the unit table "short-cut" commands:

	Prefixes:
			"[-u]<unit name>" - subtract this unit if its in the table
			"[g]<group name>" - add this group to the table
			"[-g]<group name>" - subtract this group from the table
			"[c]<country name>"	- add this country's units
			"[-c]<country name>" - subtract this country's units if any are in the table

	Stand-alone identifiers
			"[all]" - add all units
			"[-all]" - subtract all units (not very useful by itself)
			"[blue]" - add all blue units
			"[-blue]" - subtract all blue units
			"[red]" - add all red coalition units
			"[-red]" - subtract all red units

	Compound Identifiers:
			"[c][helicopter]<country name>"	- add all of this country's helicopters
			"[-c][helicopter]<country name>" - subtract all of this country's helicopters
			"[c][plane]<country name>"	- add all of this country's planes
			"[-c][plane]<country name>" - subtract all of this country's planes
			"[c][ship]<country name>"	- add all of this country's ships
			"[-c][ship]<country name>" - subtract all of this country's ships
			"[c][vehicle]<country name>"	- add all of this country's vehicles
			"[-c][vehicle]<country name>" - subtract all of this country's vehicles

			"[all][helicopter]" -	add all helicopters
			"[-all][helicopter]" - subtract all helicopters
			"[all][plane]" - add all	planes
			"[-all][plane]" - subtract all planes
			"[all][ship]" - add all ships
			"[-all][ship]" - subtract all ships
			"[all][vehicle]" - add all vehicles
			"[-all][vehicle]" - subtract all vehicles

			"[blue][helicopter]" -	add all blue coalition helicopters
			"[-blue][helicopter]" - subtract all blue coalition helicopters
			"[blue][plane]" - add all blue coalition planes
			"[-blue][plane]" - subtract all blue coalition planes
			"[blue][ship]" - add all blue coalition ships
			"[-blue][ship]" - subtract all blue coalition ships
			"[blue][vehicle]" - add all blue coalition vehicles
			"[-blue][vehicle]" - subtract all blue coalition vehicles

			"[red][helicopter]" -	add all red coalition helicopters
			"[-red][helicopter]" - subtract all red coalition helicopters
			"[red][plane]" - add all red coalition planes
			"[-red][plane]" - subtract all red coalition planes
			"[red][ship]" - add all red coalition ships
			"[-red][ship]" - subtract all red coalition ships
			"[red][vehicle]" - add all red coalition vehicles
			"[-red][vehicle]" - subtract all red coalition vehicles

	Country names to be used in [c] and [-c] short-cuts:
			Turkey
			Norway
			The Netherlands
			Spain
			11
			UK
			Denmark
			USA
			Georgia
			Germany
			Belgium
			Canada
			France
			Israel
			Ukraine
			Russia
			South Ossetia
			Abkhazia
			Italy
			Australia
			Austria
			Belarus
			Bulgaria
			Czech Republic
			China
			Croatia
			Finland
			Greece
			Hungary
			India
			Iran
			Iraq
			Japan
			Kazakhstan
			North Korea
			Pakistan
			Poland
			Romania
			Saudi Arabia
			Serbia, Slovakia
			South Korea
			Sweden
			Switzerland
			Syria
			USAF Aggressors

	Do NOT use a '[u]' notation for single units. Single units are referenced
	the same way as before: Simply input their names as strings.

	These unit tables are evaluated in order, and you cannot subtract a unit
	from a table before it is added. For example:

			{'[blue]', '[-c]Georgia'}

	will evaluate to all of blue coalition except those units owned by the
	country named "Georgia"; however:

			{'[-c]Georgia', '[blue]'}

	will evaluate to all of the units in blue coalition, because the addition
	of all units owned by blue coalition occurred AFTER the subtraction of all
	units owned by Georgia (which actually subtracted nothing at all, since
	there were no units in the table when the subtraction occurred).

	More examples:

			{'[blue][plane]', '[-c]Georgia', '[-g]Hawg 1'}

	Evaluates to all blue planes, except those blue units owned by the country
	named "Georgia" and the units in the group named "Hawg1".


			{'[g]arty1', '[g]arty2', '[-u]arty1_AD', '[-u]arty2_AD', 'Shark 11' }

	Evaluates to the unit named "Shark 11", plus all the units in groups named
	"arty1" and "arty2" except those that are named "arty1\_AD" and "arty2\_AD".

	@table UnitNameTable
	]]

	--- Returns a table containing unit names.
	-- @tparam table tbl sequential strings
	-- @treturn table @{UnitNameTable}
	function mist.makeUnitTable(tbl, exclude)
		--Assumption: will be passed a table of strings, sequential
		--log:info(tbl)
        
        
        local excludeType = {}
        if exclude then
            if type(exclude) == 'table' then
                for x, y in pairs(exclude) do
                    excludeType[x] = true
                    excludeType[y] = true
                end
            else
                excludeType[exclude] = true
            end
        
        end
        
        
		local units_by_name = {}

		local l_munits = mist.DBs.units	--local reference for faster execution
		for i = 1, #tbl do
			local unit = tbl[i]
			if unit:sub(1,4) == '[-u]' then --subtract a unit
				if units_by_name[unit:sub(5)] then -- 5 to end
				units_by_name[unit:sub(5)] = nil	--remove
			end
		elseif unit:sub(1,3) == '[g]' then -- add a group
			for coa, coa_tbl in pairs(l_munits) do
				for country, country_table in pairs(coa_tbl) do
					for unit_type, unit_type_tbl in pairs(country_table) do
						if type(unit_type_tbl) == 'table' then
							for group_ind, group_tbl in pairs(unit_type_tbl) do
								if type(group_tbl) == 'table' and group_tbl.groupName == unit:sub(4) then
									-- index 4 to end
									for unit_ind, unit in pairs(group_tbl.units) do
										units_by_name[unit.unitName] = true	--add
									end
								end
							end
						end
					end
				end
			end
		elseif unit:sub(1,4) == '[-g]' then -- subtract a group
			for coa, coa_tbl in pairs(l_munits) do
				for country, country_table in pairs(coa_tbl) do
					for unit_type, unit_type_tbl in pairs(country_table) do
						if type(unit_type_tbl) == 'table' then
							for group_ind, group_tbl in pairs(unit_type_tbl) do
								if type(group_tbl) == 'table' and group_tbl.groupName == unit:sub(5) then
									-- index 5 to end
									for unit_ind, unit in pairs(group_tbl.units) do
										if units_by_name[unit.unitName] then
											units_by_name[unit.unitName] = nil --remove
										end
									end
								end
							end
						end
					end
				end
			end
		elseif unit:sub(1,3) == '[c]' then -- add a country
			local category = ''
			local country_start = 4
			if unit:sub(4,15) == '[helicopter]' then
				category = 'helicopter'
				country_start = 16
			elseif unit:sub(4,10) == '[plane]' then
				category = 'plane'
				country_start = 11
			elseif unit:sub(4,9) == '[ship]' then
				category = 'ship'
				country_start = 10
			elseif unit:sub(4,12) == '[vehicle]' then
				category = 'vehicle'
				country_start = 13
            elseif unit:sub(4, 11) == '[static]' then
				category = 'static'
                country_start = 12
			end
			for coa, coa_tbl in pairs(l_munits) do
				for country, country_table in pairs(coa_tbl) do
					if country == string.lower(unit:sub(country_start)) then	 -- match
						for unit_type, unit_type_tbl in pairs(country_table) do
							if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then
								for group_ind, group_tbl in pairs(unit_type_tbl) do
									if type(group_tbl) == 'table' then
										for unit_ind, unit in pairs(group_tbl.units) do
											units_by_name[unit.unitName] = true	--add
										end
									end
								end
							end
						end
					end
				end
			end
		elseif unit:sub(1,4) == '[-c]' then -- subtract a country
			local category = ''
			local country_start = 5
			if unit:sub(5,16) == '[helicopter]' then
				category = 'helicopter'
				country_start = 17
			elseif unit:sub(5,11) == '[plane]' then
				category = 'plane'
				country_start = 12
			elseif unit:sub(5,10) == '[ship]' then
				category = 'ship'
				country_start = 11
			elseif unit:sub(5,13) == '[vehicle]' then
				category = 'vehicle'
				country_start = 14
            elseif unit:sub(5, 12) == '[static]' then
				category = 'static'
                country_start = 13
			end
			for coa, coa_tbl in pairs(l_munits) do
				for country, country_table in pairs(coa_tbl) do
					if country == string.lower(unit:sub(country_start)) then	 -- match
						for unit_type, unit_type_tbl in pairs(country_table) do
							if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type]  then
								for group_ind, group_tbl in pairs(unit_type_tbl) do
									if type(group_tbl) == 'table' then
										for unit_ind, unit in pairs(group_tbl.units) do
											if units_by_name[unit.unitName] then
												units_by_name[unit.unitName] = nil	--remove
											end
										end
									end
								end
							end
						end
					end
				end
			end
		elseif unit:sub(1,6) ==	'[blue]' then -- add blue coalition
			local category = ''
			if unit:sub(7) == '[helicopter]' then
				category = 'helicopter'
			elseif unit:sub(7) == '[plane]' then
				category = 'plane'
			elseif unit:sub(7) == '[ship]' then
				category = 'ship'
			elseif unit:sub(7) == '[vehicle]' then
				category = 'vehicle'
            elseif unit:sub(7) == '[static]'  then
				category = 'static'
            end
			for coa, coa_tbl in pairs(l_munits) do
				if coa == 'blue' then
					for country, country_table in pairs(coa_tbl) do
						for unit_type, unit_type_tbl in pairs(country_table) do
							if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type]  then
								for group_ind, group_tbl in pairs(unit_type_tbl) do
									if type(group_tbl) == 'table' then
										for unit_ind, unit in pairs(group_tbl.units) do
											units_by_name[unit.unitName] = true	--add
										end
									end
								end
							end
						end
					end
				end
			end
		elseif unit:sub(1,7) == '[-blue]' then -- subtract blue coalition
			local category = ''
			if unit:sub(8) == '[helicopter]' then
				category = 'helicopter'
			elseif unit:sub(8) == '[plane]' then
				category = 'plane'
			elseif unit:sub(8) == '[ship]' then
				category = 'ship'
			elseif unit:sub(8) == '[vehicle]' then
				category = 'vehicle'
            elseif unit:sub(8) == '[static]' then
				category = 'static'
			end
			for coa, coa_tbl in pairs(l_munits) do
				if coa == 'blue' then
					for country, country_table in pairs(coa_tbl) do
						for unit_type, unit_type_tbl in pairs(country_table) do
							if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then
								for group_ind, group_tbl in pairs(unit_type_tbl) do
									if type(group_tbl) == 'table' then
										for unit_ind, unit in pairs(group_tbl.units) do
											if units_by_name[unit.unitName] then
												units_by_name[unit.unitName] = nil	--remove
											end
										end
									end
								end
							end
						end
					end
				end
			end
		elseif unit:sub(1,5) == '[red]' then -- add red coalition
			local category = ''
			if unit:sub(6) == '[helicopter]' then
				category = 'helicopter'
			elseif unit:sub(6) == '[plane]' then
				category = 'plane'
			elseif unit:sub(6) == '[ship]' then
				category = 'ship'
			elseif unit:sub(6) == '[vehicle]' then
				category = 'vehicle'
            elseif unit:sub(6) == '[static]'  then
				category = 'static'
			end
			for coa, coa_tbl in pairs(l_munits) do
				if coa == 'red' then
					for country, country_table in pairs(coa_tbl) do
						for unit_type, unit_type_tbl in pairs(country_table) do
							if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then
								for group_ind, group_tbl in pairs(unit_type_tbl) do
									if type(group_tbl) == 'table' then
										for unit_ind, unit in pairs(group_tbl.units) do
											units_by_name[unit.unitName] = true	--add
										end
									end
								end
							end
						end
					end
				end
			end
		elseif unit:sub(1,6) == '[-red]' then -- subtract red coalition
			local category = ''
			if unit:sub(7) == '[helicopter]' then
				category = 'helicopter'
			elseif unit:sub(7) == '[plane]' then
				category = 'plane'
			elseif unit:sub(7) == '[ship]' then
				category = 'ship'
			elseif unit:sub(7) == '[vehicle]' then
				category = 'vehicle'
            elseif unit:sub(7) == '[static]'  then
				category = 'static'
			end
			for coa, coa_tbl in pairs(l_munits) do
				if coa == 'red' then
					for country, country_table in pairs(coa_tbl) do
						for unit_type, unit_type_tbl in pairs(country_table) do
							if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then
								for group_ind, group_tbl in pairs(unit_type_tbl) do
									if type(group_tbl) == 'table' then
										for unit_ind, unit in pairs(group_tbl.units) do
											if units_by_name[unit.unitName] then
												units_by_name[unit.unitName] = nil	--remove
											end
										end
									end
								end
							end
						end
					end
				end
			end
		elseif unit:sub(1,5) == '[all]' then -- add all of a certain category (or all categories)
			local category = ''
			if unit:sub(6) == '[helicopter]' then
				category = 'helicopter'
			elseif unit:sub(6) == '[plane]' then
				category = 'plane'
			elseif unit:sub(6) == '[ship]' then
				category = 'ship'
			elseif unit:sub(6) == '[vehicle]' then
				category = 'vehicle'
            elseif unit:sub(6) == '[static]' then
				category = 'static'
			end
			for coa, coa_tbl in pairs(l_munits) do
				for country, country_table in pairs(coa_tbl) do
					for unit_type, unit_type_tbl in pairs(country_table) do
						if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then
							for group_ind, group_tbl in pairs(unit_type_tbl) do
								if type(group_tbl) == 'table' then
									for unit_ind, unit in pairs(group_tbl.units) do
										units_by_name[unit.unitName] = true	--add
									end
								end
							end
						end
					end
				end
			end
		elseif unit:sub(1,6) == '[-all]' then -- subtract all of a certain category (or all categories)
			local category = ''
			if unit:sub(7) == '[helicopter]' then
				category = 'helicopter'
			elseif unit:sub(7) == '[plane]' then
				category = 'plane'
			elseif unit:sub(7) == '[ship]' then
				category = 'ship'
			elseif unit:sub(7) == '[vehicle]' then
				category = 'vehicle'
            elseif unit:sub(7) == '[static]'  then
				category = 'static'
			end
			for coa, coa_tbl in pairs(l_munits) do
				for country, country_table in pairs(coa_tbl) do
					for unit_type, unit_type_tbl in pairs(country_table) do
						if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then
							for group_ind, group_tbl in pairs(unit_type_tbl) do
								if type(group_tbl) == 'table' then
									for unit_ind, unit in pairs(group_tbl.units) do
										if units_by_name[unit.unitName] then
											units_by_name[unit.unitName] = nil	--remove
										end
									end
								end
							end
						end
					end
				end
			end
		else -- just a regular unit
			units_by_name[unit] = true	--add
		end
	end

	local units_tbl = {}	-- indexed sequentially
	for unit_name, val in pairs(units_by_name) do
		if val then
			units_tbl[#units_tbl + 1] = unit_name	-- add all the units to the table
		end
	end


	units_tbl.processed = timer.getTime()	--add the processed flag
	return units_tbl
end

function mist.getUnitsByAttribute(att, rnum, id)
    local cEntry = {}
    cEntry.typeName = att.type or att.typeName or att.typename
    cEntry.country = att.country
    cEntry.coalition = att.coalition
    cEntry.skill = att.skill
    cEntry.categry = att.category
    
    local num = rnum or 1
    
    if cEntry.skill == 'human' then
        cEntry.skill = {'Client', 'Player'}
    end
    

    local checkedVal = {}
    local units = {}
    for uName, uData in pairs(mist.DBs.unitsByName) do
        local matched = 0
        for cName, cVal in pairs(cEntry) do
            if type(cVal) == 'table' then
                for sName, sVal in pairs(cVal) do
                    if (uData[cName] and uData[cName] == sVal) or (uData[cName] and uData[cName] == sName) then
                         matched = matched + 1
                    end
                end
            else
                if uData[cName] and uData[cName] == cVal then
                    matched = matched + 1
                end
            end
        end
        if matched >= num then
            if id then 
                units[uData.unitId] = true
            else
            
                units[uName] = true
            end
        end
    end
    
    local rtn = {}
    for name, _ in pairs(units) do
        table.insert(rtn, name)
    end
    return rtn
    
end

function mist.getGroupsByAttribute(att, rnum, id)
    local cEntry = {}
    cEntry.typeName = att.type or att.typeName or att.typename
    cEntry.country = att.country
    cEntry.coalition = att.coalition
    cEntry.skill = att.skill
    cEntry.categry = att.category
    
    local num = rnum or 1
    
    if cEntry.skill == 'human' then
        cEntry.skill = {'Client', 'Player'}
    end
    local groups = {}
    for gName, gData in pairs(mist.DBs.groupsByName) do
        local matched = 0
        for cName, cVal in pairs(cEntry) do
            if type(cVal) == 'table' then
                for sName, sVal in pairs(cVal) do
                    if cName == 'skill' or cName == 'typeName' then 
                        local lMatch = 0
                        for uId, uData in pairs(gData.units) do
                            if (uData[cName] and uData[cName] == sVal) or (gData[cName] and gData[cName] == sName) then
                                lMatch = lMatch + 1
                                break
                            end
                        end
                        if lMatch > 0 then  
                            matched = matched + 1                    
                        end
                    end
                    if (gData[cName] and gData[cName] == sVal) or (gData[cName] and gData[cName] == sName) then
                         matched = matched + 1
                        break
                    end
                end
            else
                if cName == 'skill' or cName == 'typeName' then
                    local lMatch = 0
                    for uId, uData in pairs(gData.units) do
                        if (uData[cName] and uData[cName] == sVal) then
                            lMatch = lMatch + 1
                            break
                        end
                    end
                    if lMatch > 0 then  
                        matched = matched + 1                    
                    end
                end
                if gData[cName] and gData[cName] == cVal then
                    matched = matched + 1
                end
            end
        end
        if matched >= num then
            if id then 
                groups[gData.groupid] = true
            else
                groups[gName] = true
            end
        end
    end
    local rtn = {}
    for name, _ in pairs(groups) do
        table.insert(rtn, name)
    end
    return rtn
    
end

function mist.getDeadMapObjsInZones(zone_names)
	-- zone_names: table of zone names
	-- returns: table of dead map objects (indexed numerically)
	local map_objs = {}
	local zones = {}
	for i = 1, #zone_names do
		if mist.DBs.zonesByName[zone_names[i]] then
			zones[#zones + 1] = mist.DBs.zonesByName[zone_names[i]]
		end
	end
	for obj_id, obj in pairs(mist.DBs.deadObjects) do
		if obj.objectType and obj.objectType == 'building' then --dead map object
			for i = 1, #zones do
				if ((zones[i].point.x - obj.objectPos.x)^2 + (zones[i].point.z - obj.objectPos.z)^2)^0.5 <= zones[i].radius then
					map_objs[#map_objs + 1] = mist.utils.deepCopy(obj)
				end
			end
		end
	end
	return map_objs
end

function mist.getDeadMapObjsInPolygonZone(zone)
	-- zone_names: table of zone names
	-- returns: table of dead map objects (indexed numerically)
	local map_objs = {}
	for obj_id, obj in pairs(mist.DBs.deadObjects) do
		if obj.objectType and obj.objectType == 'building' then --dead map object
			if mist.pointInPolygon(obj.objectPos, zone) then
				map_objs[#map_objs + 1] = mist.utils.deepCopy(obj)
			end
		end
	end
	return map_objs
end
mist.shape = {}
function mist.shape.insideShape(shape1, shape2, full)
    if shape1.radius then -- probably a circle
        if shape2.radius then
             return mist.shape.circleInCircle(shape1, shape2, full)
        elseif shape2[1] then
             return mist.shape.circleInPoly(shape1, shape2, full)
        end
    
    elseif shape1[1] then -- shape1 is probably a polygon
        if shape2.radius then
            return  mist.shape.polyInCircle(shape1, shape2, full)
        elseif shape2[1] then
            return  mist.shape.polyInPoly(shape1, shape2, full)
        end
    end
    return false
end

function mist.shape.circleInCircle(c1, c2, full)
    if not full then -- quick partial check
        if mist.utils.get2DDist(c1.point, c2.point) <= c2.radius then
            return true
        end
    end
    local theta = mist.utils.getHeadingPoints(c2.point, c1.point) -- heading from 
    if full then
        return  mist.utils.get2DDist(mist.projectPoint(c1.point, c1.radius, theta), c2.point) <= c2.radius
    else
        return mist.utils.get2DDist(mist.projectPoint(c1.point, c1.radius, theta + math.pi), c2.point) <= c2.radius
    end
    return false
end


function mist.shape.circleInPoly(circle, poly, full) 

    if poly and type(poly) == 'table' and circle and type(circle) == 'table' and circle.radius and circle.point then
        if not full then 
            for i = 1, #poly do
                if mist.utils.get2DDist(circle.point, poly[i]) <= circle.radius then
                    return true
                end
            end
        end
        -- no point is inside of the zone, now check if any part is
        local count = 0
        for i = 1, #poly do
            local theta -- heading of each set of points
            if i == #poly then
                theta = mist.utils.getHeadingPoints(poly[i],poly[1])
            else
                theta = mist.utils.getHeadingPoints(poly[i],poly[i+1])
            end
            -- offset 
            local pPoint = mist.projectPoint(circle.point, circle.radius, theta - (math.pi/180))
            local oPoint = mist.projectPoint(circle.point, circle.radius, theta + (math.pi/180))

           
            if mist.pointInPolygon(pPoint, poly) == true then
                 if (full and mist.pointInPolygon(oPoint, poly) == true) or not full then
                    return true
                
                end
               
            end
        end      
        
    end
    return false
end


function mist.shape.polyInPoly(p1, p2, full)
    local count = 0
    for i = 1, #p1 do
        
        if mist.pointInPolygon(p1[i], p2) then
            count = count + 1
        end
        if (not full) and count > 0 then
            return true
        end
    end
    if count == #p1 then
        return true
    end
    
    return false
end

function mist.shape.polyInCircle(poly, circle, full)
        local count = 0
        for i = 1, #poly do
            if mist.utils.get2DDist(circle.point, poly[i]) <= circle.radius then
                if full then
                    count = count + 1
                else
                   return true
                end
            end
        end
        if count == #poly then
            return true
        end

    return false
end

function mist.shape.getPointOnSegment(point, seg, isSeg)
    local p = mist.utils.makeVec2(point)
    local s1 = mist.utils.makeVec2(seg[1])
    local s2 = mist.utils.makeVec2(seg[2])
    
    
    local cx, cy = p.x - s1.x, p.y - s1.y
    local dx, dy = s2.x - s1.x, s2.x - s1.y
    local d = (dx*dx + dy*dy)
      
    if d == 0 then
       return {x = s1.x, y = s1.y}
    end
    local u = (cx*dx + cy*dy)/d
    if isSeg then 
       if u < 0 then
            u = 0
        elseif u > 1 then
            u = 1
        end
    end
    return {x = s1.x + u*dx, y = s1.y + u*dy}
end


function mist.shape.segmentIntersect(segA, segB)
    local dx1, dy1 = segA[2].x - segA[1].x, segA[2] - segA[1].y
    local dx2, dy2 = segB[2].x - segB[1].x, segB[2] - segB[1].y
    local dx3, dy3 = segA[1].x - segB[1].x, segA[1].y - segB[1].y
    local d = dx1*dy2 - dy1*dx2
    if d == 0 then
       return false
    end
    local t1 = (dx2*dy3 - dy2*dx3)/d
    if t1 < 0 or t1 > 1 then
      return false
    end
    local t2 = (dx1*dy3 - dy1*dx3)/d
    if t2 < 0 or t2 > 1 then
      return false
    end
      -- point of intersection
      return true, segA[1].x + t1*dx1, segA[1].y + t1*dy1
end


function mist.pointInPolygon(point, poly, maxalt) --raycasting point in polygon. Code from http://softsurfer.com/Archive/algorithm_0103/algorithm_0103.htm
	--[[local type_tbl = {
		point = {'table'},
		poly = {'table'},
		maxalt = {'number', 'nil'},
		}

	local err, errmsg = mist.utils.typeCheck('mist.pointInPolygon', type_tbl, {point, poly, maxalt})
	assert(err, errmsg)
	]]
	point = mist.utils.makeVec3(point)
	local px = point.x
	local pz = point.z
	local cn = 0
	local newpoly = mist.utils.deepCopy(poly)

	if not maxalt or (point.y <= maxalt) then
		local polysize = #newpoly
		newpoly[#newpoly + 1] = newpoly[1]

		newpoly[1] = mist.utils.makeVec3(newpoly[1])

		for k = 1, polysize do
			newpoly[k+1] = mist.utils.makeVec3(newpoly[k+1])
			if ((newpoly[k].z <= pz) and (newpoly[k+1].z > pz)) or ((newpoly[k].z > pz) and (newpoly[k+1].z <= pz)) then
				local vt = (pz - newpoly[k].z) / (newpoly[k+1].z - newpoly[k].z)
				if (px < newpoly[k].x + vt*(newpoly[k+1].x - newpoly[k].x)) then
					cn = cn + 1
				end
			end
		end

		return cn%2 == 1
	else
		return false
	end
end

function mist.mapValue(val, inMin, inMax, outMin, outMax)
     return (val - inMin) * (outMax - outMin) / (inMax - inMin) + outMin
end

function mist.getUnitsInPolygon(unit_names, polyZone, max_alt)
	local units = {}

	for i = 1, #unit_names do
		units[#units + 1] = Unit.getByName(unit_names[i]) or StaticObject.getByName(unit_names[i])
	end

	local inZoneUnits = {}
	for i =1, #units do
		local lUnit = units[i]
        local lCat = lUnit:getCategory()
        if ((lCat == 1 and lUnit:isActive()) or lCat ~= 1) and mist.pointInPolygon(lUnit:getPosition().p, polyZone, max_alt) then
			inZoneUnits[#inZoneUnits + 1] = lUnit
		end
	end

	return inZoneUnits
end

function mist.getUnitsInZones(unit_names, zone_names, zone_type)
    zone_type = zone_type or 'cylinder'
	if zone_type == 'c' or zone_type == 'cylindrical' or zone_type == 'C' then
		zone_type = 'cylinder'
	end
	if zone_type == 's' or zone_type == 'spherical' or zone_type == 'S' then
		zone_type = 'sphere'
	end

	assert(zone_type == 'cylinder' or zone_type == 'sphere', 'invalid zone_type: ' .. tostring(zone_type))

	local units = {}
	local zones = {}
    
    if zone_names and type(zone_names) == 'string' then
        zone_names = {zone_names}
    end
	for k = 1, #unit_names do
		
        local unit = Unit.getByName(unit_names[k]) or StaticObject.getByName(unit_names[k])
		if unit then
			units[#units + 1] = unit
		end
	end


	for k = 1, #zone_names do
		local zone = mist.DBs.zonesByName[zone_names[k]]
		if zone then
			zones[#zones + 1] = {radius = zone.radius, x = zone.point.x, y = zone.point.y, z = zone.point.z, verts = zone.verticies}
		end
	end

	local in_zone_units = {}
	for units_ind = 1, #units do
        local lUnit = units[units_ind]
        local unit_pos = lUnit:getPosition().p
        local lCat = lUnit:getCategory()
        for zones_ind = 1, #zones do
			if zone_type == 'sphere' then	--add land height value for sphere zone type
				local alt = land.getHeight({x = zones[zones_ind].x, y = zones[zones_ind].z})
				if alt then
					zones[zones_ind].y = alt
				end
			end

            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
				if zones[zones_ind].verts  then
                    if mist.pointInPolygon(unit_pos, zones[zones_ind].verts) then
                        in_zone_units[#in_zone_units + 1] = lUnit
                    end

                else
                    if zone_type == 'cylinder' and (((unit_pos.x - zones[zones_ind].x)^2 + (unit_pos.z - zones[zones_ind].z)^2)^0.5 <= zones[zones_ind].radius) then
                        in_zone_units[#in_zone_units + 1] = lUnit
                        break
                    elseif zone_type == 'sphere' and (((unit_pos.x - zones[zones_ind].x)^2 + (unit_pos.y - zones[zones_ind].y)^2 + (unit_pos.z - zones[zones_ind].z)^2)^0.5 <= zones[zones_ind].radius) then
                        in_zone_units[#in_zone_units + 1] = lUnit
                        break
                    end
                end
			end
		end
	end
	return in_zone_units
end

function mist.getUnitsInMovingZones(unit_names, zone_unit_names, radius, zone_type)

	zone_type = zone_type or 'cylinder'
	if zone_type == 'c' or zone_type == 'cylindrical' or zone_type == 'C' then
		zone_type = 'cylinder'
	end
	if zone_type == 's' or zone_type == 'spherical' or zone_type == 'S' then
		zone_type = 'sphere'
	end

	assert(zone_type == 'cylinder' or zone_type == 'sphere', 'invalid zone_type: ' .. tostring(zone_type))

	local units = {}
	local zone_units = {}

	for k = 1, #unit_names do
		local unit = Unit.getByName(unit_names[k]) or StaticObject.getByName(unit_names[k])
		if unit then
			units[#units + 1] = unit
		end
	end

	for k = 1, #zone_unit_names do
		local unit = Unit.getByName(zone_unit_names[k]) or StaticObject.getByName(zone_unit_names[k])
		if unit then
			zone_units[#zone_units + 1] = unit
		end
	end

	local in_zone_units = {}

	for units_ind = 1, #units do
        local lUnit = units[units_ind]
        local lCat = lUnit:getCategory()
        local unit_pos = lUnit:getPosition().p
		for zone_units_ind = 1, #zone_units do
			
			local zone_unit_pos = zone_units[zone_units_ind]:getPosition().p
			if unit_pos and zone_unit_pos and ((lCat == 1 and lUnit:isActive()) or lCat ~= 1) then
				if zone_type == 'cylinder' and (((unit_pos.x - zone_unit_pos.x)^2 + (unit_pos.z - zone_unit_pos.z)^2)^0.5 <= radius) then
					in_zone_units[#in_zone_units + 1] = lUnit
					break
				elseif zone_type == 'sphere' and (((unit_pos.x - zone_unit_pos.x)^2 + (unit_pos.y - zone_unit_pos.y)^2 + (unit_pos.z - zone_unit_pos.z)^2)^0.5 <= radius) then
					in_zone_units[#in_zone_units + 1] = lUnit
					break
				end
			end
		end
	end
	return in_zone_units
end

function mist.getUnitsLOS(unitset1, altoffset1, unitset2, altoffset2, radius)
	log:info("$1, $2, $3, $4, $5", unitset1, altoffset1, unitset2, altoffset2, radius)
	radius = radius or math.huge
	local unit_info1 = {}
	local unit_info2 = {}

	-- get the positions all in one step, saves execution time.
	for unitset1_ind = 1, #unitset1 do
		local unit1 = Unit.getByName(unitset1[unitset1_ind])
        local lCat = unit1:getCategory()
		if unit1 and ((lCat == 1 and unit1:isActive()) or lCat ~= 1) then
			unit_info1[#unit_info1 + 1] = {}
			unit_info1[#unit_info1].unit = unit1
			unit_info1[#unit_info1].pos	= unit1:getPosition().p
		end
	end

	for unitset2_ind = 1, #unitset2 do
		local unit2 = Unit.getByName(unitset2[unitset2_ind])
        local lCat = unit2:getCategory()
		if unit2 and ((lCat == 1 and unit2:isActive()) or lCat ~= 1) then
			unit_info2[#unit_info2 + 1] = {}
			unit_info2[#unit_info2].unit = unit2
			unit_info2[#unit_info2].pos	= unit2:getPosition().p
		end
	end

	local LOS_data = {}
	-- now compute los
	for unit1_ind = 1, #unit_info1 do
		local unit_added = false
		for unit2_ind = 1, #unit_info2 do
			if radius == math.huge or (mist.vec.mag(mist.vec.sub(unit_info1[unit1_ind].pos, unit_info2[unit2_ind].pos)) < radius) then -- inside radius
				local point1 = { x = unit_info1[unit1_ind].pos.x, y = unit_info1[unit1_ind].pos.y + altoffset1, z = unit_info1[unit1_ind].pos.z}
				local point2 = { x = unit_info2[unit2_ind].pos.x, y = unit_info2[unit2_ind].pos.y + altoffset2, z = unit_info2[unit2_ind].pos.z}
				if land.isVisible(point1, point2) then
					if unit_added == false then
						unit_added = true
						LOS_data[#LOS_data + 1] = {}
						LOS_data[#LOS_data].unit = unit_info1[unit1_ind].unit
						LOS_data[#LOS_data].vis = {}
						LOS_data[#LOS_data].vis[#LOS_data[#LOS_data].vis + 1] = unit_info2[unit2_ind].unit
					else
						LOS_data[#LOS_data].vis[#LOS_data[#LOS_data].vis + 1] = unit_info2[unit2_ind].unit
					end
				end
			end
		end
	end

	return LOS_data
end

function mist.getAvgPoint(points)
	local avgX, avgY, avgZ, totNum = 0, 0, 0, 0
	for i = 1, #points do
        --log:warn(points[i])
        local nPoint = mist.utils.makeVec3(points[i])
		if nPoint.z then
			avgX = avgX + nPoint.x
			avgY = avgY + nPoint.y
			avgZ = avgZ + nPoint.z
			totNum = totNum + 1
		end
	end
	if totNum ~= 0 then
		return {x = avgX/totNum, y = avgY/totNum, z = avgZ/totNum}
	end
end

--Gets the average position of a group of units (by name)
function mist.getAvgPos(unitNames)
	local avgX, avgY, avgZ, totNum = 0, 0, 0, 0
	for i = 1, #unitNames do
		local unit
		if Unit.getByName(unitNames[i]) then
			unit = Unit.getByName(unitNames[i])
		elseif StaticObject.getByName(unitNames[i]) then
			unit = StaticObject.getByName(unitNames[i])
		end
		if unit then
			local pos = unit:getPosition().p
			if pos then -- you never know O.o
				avgX = avgX + pos.x
				avgY = avgY + pos.y
				avgZ = avgZ + pos.z
				totNum = totNum + 1
			end
		end
	end
	if totNum ~= 0 then
		return {x = avgX/totNum, y = avgY/totNum, z = avgZ/totNum}
	end
end

function mist.getAvgGroupPos(groupName)
	if type(groupName) == 'string' and Group.getByName(groupName) and Group.getByName(groupName):isExist() == true then
		groupName = Group.getByName(groupName)
	end
	local units = {}
	for i = 1, groupName:getSize() do
		table.insert(units, groupName:getUnit(i):getName())
	end

	return mist.getAvgPos(units)

end

--[[ vars for mist.getMGRSString:
vars.units - table of unit names (NOT unitNameTable- maybe this should change).
vars.acc - integer between 0 and 5, inclusive
]]
function mist.getMGRSString(vars)
	local units = vars.units
	local acc = vars.acc or 5
	local avgPos = mist.getAvgPos(units)
	if avgPos then
		return mist.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(avgPos)), acc)
	end
end

--[[ vars for mist.getLLString
vars.units - table of unit names (NOT unitNameTable- maybe this should change).
vars.acc - integer, number of numbers after decimal place
vars.DMS - if true, output in degrees, minutes, seconds.	Otherwise, output in degrees, minutes.
]]
function mist.getLLString(vars)
	local units = vars.units
	local acc = vars.acc or 3
	local DMS = vars.DMS
	local avgPos = mist.getAvgPos(units)
	if avgPos then
		local lat, lon = coord.LOtoLL(avgPos)
		return mist.tostringLL(lat, lon, acc, DMS)
	end
end

--[[
vars.units- table of unit names (NOT unitNameTable- maybe this should change).
vars.ref -	vec3 ref point, maybe overload for vec2 as well?
vars.alt - boolean, if used, includes altitude in string
vars.metric - boolean, gives distance in km instead of NM.
]]
function mist.getBRString(vars)
	local units = vars.units
	local ref = mist.utils.makeVec3(vars.ref, 0)	-- turn it into Vec3 if it is not already.
	local alt = vars.alt
	local metric = vars.metric
	local avgPos = mist.getAvgPos(units)
	if avgPos then
        local vec = {x = avgPos.x - ref.x, y = avgPos.y - ref.y, z = avgPos.z - ref.z}
        local dir = mist.utils.getDir(vec, ref)
        local dist = mist.utils.get2DDist(avgPos, ref)
        if alt then
            alt = avgPos.y
        end
        return mist.tostringBR(dir, dist, alt, metric)
	end
end

-- Returns the Vec3 coordinates of the average position of the concentration of units most in the heading direction.
--[[ vars for mist.getLeadingPos:
vars.units - table of unit names
vars.heading - direction
vars.radius - number
vars.headingDegrees - boolean, switches heading to degrees
]]
function mist.getLeadingPos(vars)
	local units = vars.units
	local heading = vars.heading
	local radius = vars.radius
	if vars.headingDegrees then
		heading = mist.utils.toRadian(vars.headingDegrees)
	end

	local unitPosTbl = {}
	for i = 1, #units do
		local unit = Unit.getByName(units[i])
		if unit and unit:isExist() then
			unitPosTbl[#unitPosTbl + 1] = unit:getPosition().p
		end
	end
    
	if #unitPosTbl > 0 then	-- one more more units found.
		-- first, find the unit most in the heading direction
		local maxPos = -math.huge
        heading = heading * -1 -- rotated value appears to be opposite of what was expected
		local maxPosInd	-- maxPos - the furthest in direction defined by heading; maxPosInd =
		for i = 1, #unitPosTbl do
			local rotatedVec2 = mist.vec.rotateVec2(mist.utils.makeVec2(unitPosTbl[i]), heading)
			if (not maxPos) or maxPos < rotatedVec2.x then
				maxPos = rotatedVec2.x
				maxPosInd = i
			end
		end

		--now, get all the units around this unit...
		local avgPos
		if radius then
			local maxUnitPos = unitPosTbl[maxPosInd]
			local avgx, avgy, avgz, totNum = 0, 0, 0, 0
			for i = 1, #unitPosTbl do
				if mist.utils.get2DDist(maxUnitPos, unitPosTbl[i]) <= radius then
					avgx = avgx + unitPosTbl[i].x
					avgy = avgy + unitPosTbl[i].y
					avgz = avgz + unitPosTbl[i].z
					totNum = totNum + 1
				end
			end
			avgPos = { x = avgx/totNum, y = avgy/totNum, z = avgz/totNum}
		else
			avgPos = unitPosTbl[maxPosInd]
		end

		return avgPos
	end
end

--[[ vars for mist.getLeadingMGRSString:
vars.units - table of unit names
vars.heading - direction
vars.radius - number
vars.headingDegrees - boolean, switches heading to degrees
vars.acc - number, 0 to 5.
]]
function mist.getLeadingMGRSString(vars)
	local pos = mist.getLeadingPos(vars)
	if pos then
		local acc = vars.acc or 5
		return mist.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(pos)), acc)
	end
end

--[[ vars for mist.getLeadingLLString:
vars.units - table of unit names
vars.heading - direction, number
vars.radius - number
vars.headingDegrees - boolean, switches heading to degrees
vars.acc - number of digits after decimal point (can be negative)
vars.DMS -	boolean, true if you want DMS.
]]
function mist.getLeadingLLString(vars)
	local pos = mist.getLeadingPos(vars)
	if pos then
		local acc = vars.acc or 3
		local DMS = vars.DMS
		local lat, lon = coord.LOtoLL(pos)
		return mist.tostringLL(lat, lon, acc, DMS)
	end
end

--[[ vars for mist.getLeadingBRString:
vars.units - table of unit names
vars.heading - direction, number
vars.radius - number
vars.headingDegrees - boolean, switches heading to degrees
vars.metric - boolean, if true, use km instead of NM.
vars.alt - boolean, if true, include altitude.
vars.ref - vec3/vec2 reference point.
]]
function mist.getLeadingBRString(vars)
	local pos = mist.getLeadingPos(vars)
	if pos then
		local ref = vars.ref
		local alt = vars.alt
		local metric = vars.metric

		local vec = {x = pos.x - ref.x, y = pos.y - ref.y, z = pos.z - ref.z}
		local dir = mist.utils.getDir(vec, ref)
		local dist = mist.utils.get2DDist(pos, ref)
		if alt then
			alt = pos.y
		end
		return mist.tostringBR(dir, dist, alt, metric)
	end
end

--[[getPathLength from GSH
-- Returns the length between the defined set of points. Can also return the point index before the cutoff was achieved
p - table of path points, vec2 or vec3
cutoff - number distance after which to stop at
topo  - boolean for if it should get the topographical distance

]]

function mist.getPathLength(p, cutoff, topo)
    local l = 0
    local cut = 0 or cutOff
    local path = {}

    for i = 1, #p do
        if topo then
            table.insert(path, mist.utils.makeVec3GL(p[i]))
        else
            table.insert(path, mist.utils.makeVec3(p[i]))
        end
    end
    
    for i = 1, #path do
        if i + 1 <= #path then 
            if topo then 
                l = mist.utils.get3DDist(path[i], path[i+1]) + l
            else
                l = mist.utils.get2DDist(path[i], path[i+1]) + l
            end
        end
        if cut ~= 0 and l > cut  then
            return l, i
        end
    end
    return l
end

--[[
Return 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. 
p - table of path points, can be vec2 or vec3
num - number of segments. 
exact - boolean for whether or not it returns the exact distance or uses the first WP to that distance. 


]]

function mist.getPathInSegments(p, num, exact)
    local tot = mist.getPathLength(p)
    local checkDist = tot/num
    local typeUsed = 'vec2'

    local points = {[1] = p[1]}
    local curDist = 0
    for i = 1, #p do
        if i + 1 <= #p then
            curDist = mist.utils.get2DDist(p[i], p[i+1]) + curDist
            if curDist > checkDist then
                curDist = 0
                if exact then
                    -- get avg point between the two
                    -- insert into point table
                    -- need to be accurate... maybe reassign the point for the value it is checking?
                    -- insert into p table?
                else
                    table.insert(points, p[i])                
                end
            end
        
        end

    end
    return points

end


function mist.getPointAtDistanceOnPath(p, dist, r, rtn)
    log:info('find distance: $1', dist)
    local rType = r or 'roads'
    local point = {x= 0, y = 0, z = 0}
    local path = {}
    local ret = rtn or 'vec2'
    local l = 0
    if p[1] and #p == 2 then
        path = land.findPathOnRoads(rType, p[1].x, p[1].y, p[2].x, p[2].y)
    else
        path = p
    end
    for i = 1, #path do
        if i + 1 <= #path then 
            nextPoint = path[i+1]
            if topo then 
                l = mist.utils.get3DDist(path[i], path[i+1]) + l
            else
                l = mist.utils.get2DDist(path[i], path[i+1]) + l
            end
        end
        if l > dist then
            local diff = dist
            if i ~= 1 then -- get difference
                diff = l - dist
            end
            local dir = mist.utils.getHeadingPoints(mist.utils.makeVec3(path[i]), mist.utils.makeVec3(path[i+1]))
            local x, y 
            if r then 
                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))
            else
                x, y = mist.utils.round((math.cos(dir) * diff) + path[i].x,1),  mist.utils.round((math.sin(dir) * diff) + path[i].y,1)
            end
            
            if ret == 'vec2' then
                return {x = x, y = y}, dir
            elseif ret == 'vec3' then
                return {x = x, y = 0, z = y}, dir
            end
            
            return {x = x, y = y}, dir
        end
    end
    log:warn('Find point at distance: $1, path distance $2', dist, l)
    return false
end


function mist.projectPoint(point, dist, theta)
    local newPoint = {}
    if point.z then
       newPoint.z = mist.utils.round(math.sin(theta) * dist + point.z, 3)
       newPoint.y = mist.utils.deepCopy(point.y)
    else
       newPoint.y = mist.utils.round(math.sin(theta) * dist + point.y, 3)
    end
    newPoint.x = mist.utils.round(math.cos(theta) * dist + point.x, 3)

    return newPoint
end

end




--- Group functions.
-- @section groups
do -- group functions scope

	--- Check table used for group creation.
	-- @tparam table groupData table to check.
	-- @treturn boolean true if a group can be spawned using
	-- this table, false otherwise.
	function mist.groupTableCheck(groupData)
		-- return false if country, category
		-- or units are missing
		if not groupData.country or
			not groupData.category or
			not groupData.units then
			return false
		end
		-- return false if unitData misses
		-- x, y or type
		for unitId, unitData in pairs(groupData.units) do
			if not unitData.x or
				not unitData.y or
				not unitData.type then
					return false
			end
		end
		-- everything we need is here return true
		return true
	end

	--- Returns group data table of give group.
	function mist.getCurrentGroupData(gpName)
		local dbData = mist.getGroupData(gpName)

		if Group.getByName(gpName) and Group.getByName(gpName):isExist() == true then
			local newGroup = Group.getByName(gpName)
			local newData = {}
			newData.name = gpName
			newData.groupId = tonumber(newGroup:getID())
			newData.category = newGroup:getCategory()
			newData.groupName = gpName
			newData.hidden = dbData.hidden

			if newData.category == 2 then
				newData.category = 'vehicle'
			elseif newData.category == 3 then
				newData.category = 'ship'
			end

			newData.units = {}
			local newUnits = newGroup:getUnits()
            if #newUnits == 0 then
                log:warn('getCurrentGroupData has returned no units for: $1', gpName)
            end
			for unitNum, unitData in pairs(newGroup:getUnits()) do
				newData.units[unitNum] = {}
                local uName = unitData:getName()

                if mist.DBs.unitsByName[uName] and unitData:getTypeName() ==  mist.DBs.unitsByName[uName].type and mist.DBs.unitsByName[uName].unitId == tonumber(unitData:getID()) then -- If old data matches most of new data
                    newData.units[unitNum] = mist.utils.deepCopy(mist.DBs.unitsByName[uName])
                else
                    newData.units[unitNum].unitId = tonumber(unitData:getID())
                    newData.units[unitNum].type = unitData:getTypeName()
                    newData.units[unitNum].skill = mist.getUnitSkill(uName)
                    newData.country = string.lower(country.name[unitData:getCountry()])
                    newData.units[unitNum].callsign = unitData:getCallsign()
                    newData.units[unitNum].unitName = uName
                end

				newData.units[unitNum].x = unitData:getPosition().p.x
				newData.units[unitNum].y = unitData:getPosition().p.z
                newData.units[unitNum].point = {x = newData.units[unitNum].x, y = newData.units[unitNum].y}
                newData.units[unitNum].heading = mist.getHeading(unitData, true) -- added to DBs
				newData.units[unitNum].alt = unitData:getPosition().p.y
                newData.units[unitNum].speed = mist.vec.mag(unitData:getVelocity())
               
			end

			return newData
		elseif StaticObject.getByName(gpName) and StaticObject.getByName(gpName):isExist() == true then
			local staticObj = StaticObject.getByName(gpName)
			dbData.units[1].x = staticObj:getPosition().p.x
			dbData.units[1].y = staticObj:getPosition().p.z
			dbData.units[1].alt = staticObj:getPosition().p.y
			dbData.units[1].heading = mist.getHeading(staticObj, true)

			return dbData
		end

	end

	function mist.getGroupData(gpName, route)
		local found = false
		local newData = {}
		if mist.DBs.groupsByName[gpName] then
			newData = mist.utils.deepCopy(mist.DBs.groupsByName[gpName])
			found = true
		end

		if found == false then
			for groupName, groupData in pairs(mist.DBs.groupsByName) do
				if mist.stringMatch(groupName, gpName) == true then
					newData = mist.utils.deepCopy(groupData)
					newData.groupName = groupName
					found = true
					break
				end
			end
		end

		local payloads
		if newData.category == 'plane' or newData.category == 'helicopter' then
			payloads = mist.getGroupPayload(newData.groupName)
		end
		if found == true then
			--newData.hidden = false -- maybe add this to DBs

			for unitNum, unitData in pairs(newData.units) do
				newData.units[unitNum] = {}

				newData.units[unitNum].unitId = unitData.unitId
				--newData.units[unitNum].point = unitData.point
				newData.units[unitNum].x = unitData.point.x
				newData.units[unitNum].y = unitData.point.y
				newData.units[unitNum].alt = unitData.alt
				newData.units[unitNum].alt_type = unitData.alt_type
				newData.units[unitNum].speed = unitData.speed
				newData.units[unitNum].type = unitData.type
				newData.units[unitNum].skill = unitData.skill
				newData.units[unitNum].unitName = unitData.unitName
				newData.units[unitNum].heading = unitData.heading -- added to DBs
				newData.units[unitNum].playerCanDrive = unitData.playerCanDrive -- added to DBs
                newData.units[unitNum].livery_id = unitData.livery_id
                newData.units[unitNum].AddPropAircraft = unitData.AddPropAircraft
                newData.units[unitNum].AddPropVehicle = unitData.AddPropVehicle
                

				if newData.category == 'plane' or newData.category == 'helicopter' then
					newData.units[unitNum].payload = payloads[unitNum]
					
					newData.units[unitNum].onboard_num = unitData.onboard_num
					newData.units[unitNum].callsign = unitData.callsign
					
				end
				if newData.category == 'static' then
					newData.units[unitNum].categoryStatic = unitData.categoryStatic
					newData.units[unitNum].mass = unitData.mass
					newData.units[unitNum].canCargo = unitData.canCargo
					newData.units[unitNum].shape_name = unitData.shape_name
				end
			end
			--log:info(newData)
            if route then
                newData.route = mist.getGroupRoute(gpName, true)
            end
            
			return newData
		else
			log:error('$1 not found in MIST database', gpName)
			return
		end
	end

	function mist.getPayload(unitIdent)
		-- refactor to search by groupId and allow groupId and groupName as inputs
		local unitId = unitIdent
		if type(unitIdent) == 'string' and not tonumber(unitIdent) then
			if mist.DBs.MEunitsByName[unitIdent] then
				unitId = mist.DBs.MEunitsByName[unitIdent].unitId
			else
				log:error("Unit not found in mist.DBs.MEunitsByName: $1", unitIdent)
			end
		end
		local gpId = mist.DBs.MEunitsById[unitId].groupId

		if gpId and unitId then
			for coa_name, coa_data in pairs(env.mission.coalition) do
				if (coa_name == 'red' or coa_name == 'blue') and type(coa_data) == 'table' then
					if coa_data.country then --there is a country table
						for cntry_id, cntry_data in pairs(coa_data.country) do
							for obj_cat_name, obj_cat_data in pairs(cntry_data) do
								if obj_cat_name == "helicopter" or obj_cat_name == "ship" or obj_cat_name == "plane" or obj_cat_name == "vehicle" then	-- only these types have points
									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	--there's a group!
										for group_num, group_data in pairs(obj_cat_data.group) do
											if group_data and group_data.groupId == gpId then
												for unitIndex, unitData in pairs(group_data.units) do --group index
													if unitData.unitId == unitId then
														return unitData.payload
													end
												end
											end
										end
									end
								end
							end
						end
					end
				end
			end
		else
			log:error('Need string or number. Got: $1', type(unitIdent))
			return false
		end
		log:warn("Couldn't find payload for unit: $1", unitIdent)
		return
	end

	function mist.getGroupPayload(groupIdent)
		local gpId = groupIdent
		if type(groupIdent) == 'string' and not tonumber(groupIdent) then
			if mist.DBs.MEgroupsByName[groupIdent] then
				gpId = mist.DBs.MEgroupsByName[groupIdent].groupId
			else
				log:error('$1 not found in mist.DBs.MEgroupsByName', groupIdent)
			end
		end

		if gpId then
			for coa_name, coa_data in pairs(env.mission.coalition) do
				if type(coa_data) == 'table' then
					if coa_data.country then --there is a country table
						for cntry_id, cntry_data in pairs(coa_data.country) do
							for obj_cat_name, obj_cat_data in pairs(cntry_data) do
								if obj_cat_name == "helicopter" or obj_cat_name == "s
Download .txt
gitextract_fdixfuk7/

├── .gitattributes
├── .gitignore
├── LICENSE.md
├── README.md
├── build-tools/
│   └── build-compiled-script.ps1
├── contributing.md
├── demo-missions/
│   ├── mist_4_5_107.lua
│   ├── moose_a2a_connector/
│   │   ├── skynet-and-moose-a2a-dispatcher-setup.lua
│   │   └── skynet-and-moose-a2a-dispatcher.miz
│   ├── skynet-iads-compiled.lua
│   ├── skynet-iads-setup-persian-gulf.lua
│   ├── skynet-test-persian-gulf-stress-test.miz
│   └── skynet-test-persian-gulf.miz
├── skynet-iads-source/
│   ├── README_source.md
│   ├── highdigitsams/
│   │   └── skynet-iads-high-digit-sams-suported-types.lua
│   ├── skynet-iads-abstract-dcs-object-wrapper.lua
│   ├── skynet-iads-abstract-element.lua
│   ├── skynet-iads-abstract-radar-element.lua
│   ├── skynet-iads-awacs-radar.lua
│   ├── skynet-iads-command-center.lua
│   ├── skynet-iads-contact.lua
│   ├── skynet-iads-early-warning-radar.lua
│   ├── skynet-iads-harm-detection.lua
│   ├── skynet-iads-jammer.lua
│   ├── skynet-iads-logger.lua
│   ├── skynet-iads-sam-search-radar.lua
│   ├── skynet-iads-sam-site.lua
│   ├── skynet-iads-sam-tracking-radar.lua
│   ├── skynet-iads-supported-types.lua
│   ├── skynet-iads-table-delegator.lua
│   ├── skynet-iads.lua
│   ├── skynet-mooose-a2a-dispatcher-connector.lua
│   └── syknet-iads-sam-launcher.lua
└── unit-tests/
    ├── highdigitsams/
    │   ├── highdigitsams-unit-tests.miz
    │   ├── skynet-high-digit-sams-unit-test-setup.lua
    │   └── test-skynet-high-digit-sam-sites.lua
    ├── luaunit.lua
    ├── skynet-unit-test-iads-setup.lua
    ├── skynet-unit-tests.lua
    ├── skynet-unit-tests.miz
    ├── test-skynet-iads-abstract-dcs-object-wrapper.lua
    ├── test-skynet-iads-abstract-element.lua
    ├── test-skynet-iads-abstract-radar-element.lua
    ├── test-skynet-iads-blue-sam-sites-and-ew-radars.lua
    ├── test-skynet-iads-contact.lua
    ├── test-skynet-iads-harm-detection.lua
    ├── test-skynet-iads-jammer.lua
    ├── test-skynet-iads-red-sam-sites-and-ew-radars.lua
    ├── test-skynet-iads-sam-site.lua
    ├── test-skynet-iads.lua
    ├── test-skynet-moose-a2a-dispatcher-connector.lua
    └── test-syknet-early-warning-radar.lua
Condensed preview — 52 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,002K chars).
[
  {
    "path": ".gitattributes",
    "chars": 66,
    "preview": "# Auto detect text files and perform LF normalization\n* text=auto\n"
  },
  {
    "path": ".gitignore",
    "chars": 32,
    "preview": ".DS_STORE\n/demo-missions/spikes/"
  },
  {
    "path": "LICENSE.md",
    "chars": 11357,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "README.md",
    "chars": 43223,
    "preview": "# Skynet-IADS\n![logo](/images/SA3_2.jpg)\n\nAn IADS (Integrated Air Defence System) script for DCS (Digital Combat Simulat"
  },
  {
    "path": "build-tools/build-compiled-script.ps1",
    "chars": 2403,
    "preview": "$version=$args[0]\nif ($version -eq $null){\n\techo \"No Version supplied, not bulding script\"\n\treturn\n}\nif (Test-Path ./tmp"
  },
  {
    "path": "contributing.md",
    "chars": 2438,
    "preview": "This guide is work in progress and will be updated.\n\n# Contributing\nThanks for your interest in contributing to Skynet!\n"
  },
  {
    "path": "demo-missions/mist_4_5_107.lua",
    "chars": 312747,
    "preview": "--[[--\nMIST Mission Scripting Tools.\n## Description:\nMIssion Scripting Tools (MIST) is a collection of Lua functions\nand"
  },
  {
    "path": "demo-missions/moose_a2a_connector/skynet-and-moose-a2a-dispatcher-setup.lua",
    "chars": 2264,
    "preview": "do\n\n\n--Setup Syknet IADS:\nredIADS = SkynetIADS:create('Enemy IADS')\n\n\nlocal iadsDebug = redIADS:getDebugSettings()  \niad"
  },
  {
    "path": "demo-missions/skynet-iads-compiled.lua",
    "chars": 116802,
    "preview": "env.info(\"--- SKYNET VERSION: 3.3.0 | BUILD TIME: 29.12.2023 2311Z ---\")\ndo\n--this file contains the required units per "
  },
  {
    "path": "demo-missions/skynet-iads-setup-persian-gulf.lua",
    "chars": 4045,
    "preview": "do\n--create an instance of the IADS\nredIADS = SkynetIADS:create('IRAN')\n\n---debug settings remove from here on if you do"
  },
  {
    "path": "skynet-iads-source/README_source.md",
    "chars": 39289,
    "preview": "# Skynet-IADS\n![logo](/images/SA3_2.jpg)\n\nAn IADS (Integrated Air Defence System) script for DCS (Digital Combat Simulat"
  },
  {
    "path": "skynet-iads-source/highdigitsams/skynet-iads-high-digit-sams-suported-types.lua",
    "chars": 7342,
    "preview": "do\n-- this file contains the definitions for the HightDigitSAMSs: https://github.com/Auranis/HighDigitSAMs\n\n--EW radars "
  },
  {
    "path": "skynet-iads-source/skynet-iads-abstract-dcs-object-wrapper.lua",
    "chars": 2735,
    "preview": "do\n\nSkynetIADSAbstractDCSObjectWrapper = {}\n\nfunction SkynetIADSAbstractDCSObjectWrapper:create(dcsRepresentation)\n\tloca"
  },
  {
    "path": "skynet-iads-source/skynet-iads-abstract-element.lua",
    "chars": 3699,
    "preview": "do\n\nSkynetIADSAbstractElement = {}\nSkynetIADSAbstractElement = inheritsFrom(SkynetIADSAbstractDCSObjectWrapper)\n\nfunctio"
  },
  {
    "path": "skynet-iads-source/skynet-iads-abstract-radar-element.lua",
    "chars": 31173,
    "preview": "do\n\nSkynetIADSAbstractRadarElement = {}\nSkynetIADSAbstractRadarElement = inheritsFrom(SkynetIADSAbstractElement)\n\nSkynet"
  },
  {
    "path": "skynet-iads-source/skynet-iads-awacs-radar.lua",
    "chars": 1866,
    "preview": "do\n--this class is currently used for AWACS and Ships, at a latter date a separate class for ships could be created, cur"
  },
  {
    "path": "skynet-iads-source/skynet-iads-command-center.lua",
    "chars": 440,
    "preview": "do\nSkynetIADSCommandCenter = {}\nSkynetIADSCommandCenter = inheritsFrom(SkynetIADSAbstractRadarElement)\n\nfunction SkynetI"
  },
  {
    "path": "skynet-iads-source/skynet-iads-contact.lua",
    "chars": 4291,
    "preview": "do\n\nSkynetIADSContact = {}\nSkynetIADSContact = inheritsFrom(SkynetIADSAbstractDCSObjectWrapper)\n\nSkynetIADSContact.CLIMB"
  },
  {
    "path": "skynet-iads-source/skynet-iads-early-warning-radar.lua",
    "chars": 1629,
    "preview": "do\n\nSkynetIADSEWRadar = {}\nSkynetIADSEWRadar = inheritsFrom(SkynetIADSAbstractRadarElement)\n\nfunction SkynetIADSEWRadar:"
  },
  {
    "path": "skynet-iads-source/skynet-iads-harm-detection.lua",
    "chars": 4484,
    "preview": "do\n\nSkynetIADSHARMDetection = {}\nSkynetIADSHARMDetection.__index = SkynetIADSHARMDetection\n\nSkynetIADSHARMDetection.HARM"
  },
  {
    "path": "skynet-iads-source/skynet-iads-jammer.lua",
    "chars": 5005,
    "preview": "do\n\nSkynetIADSJammer = {}\nSkynetIADSJammer.__index = SkynetIADSJammer\n\nfunction SkynetIADSJammer:create(emitter, iads)\n\t"
  },
  {
    "path": "skynet-iads-source/skynet-iads-logger.lua",
    "chars": 13521,
    "preview": "do\n\nSkynetIADSLogger = {}\nSkynetIADSLogger.__index = SkynetIADSLogger\n\nfunction SkynetIADSLogger:create(iads)\n\tlocal log"
  },
  {
    "path": "skynet-iads-source/skynet-iads-sam-search-radar.lua",
    "chars": 3037,
    "preview": "do\n\nSkynetIADSSAMSearchRadar = {}\nSkynetIADSSAMSearchRadar = inheritsFrom(SkynetIADSAbstractDCSObjectWrapper)\n\nfunction "
  },
  {
    "path": "skynet-iads-source/skynet-iads-sam-site.lua",
    "chars": 2308,
    "preview": "do\n\nSkynetIADSSamSite = {}\nSkynetIADSSamSite = inheritsFrom(SkynetIADSAbstractRadarElement)\n\nfunction SkynetIADSSamSite:"
  },
  {
    "path": "skynet-iads-source/skynet-iads-sam-tracking-radar.lua",
    "chars": 280,
    "preview": "do\n\nSkynetIADSSAMTrackingRadar = {}\nSkynetIADSSAMTrackingRadar = inheritsFrom(SkynetIADSSAMSearchRadar)\n\nfunction Skynet"
  },
  {
    "path": "skynet-iads-source/skynet-iads-supported-types.lua",
    "chars": 8053,
    "preview": "do\n--this file contains the required units per sam type\nsamTypesDB = {\t\n\t['S-200'] = {\n        ['type'] = 'complex',\n   "
  },
  {
    "path": "skynet-iads-source/skynet-iads-table-delegator.lua",
    "chars": 400,
    "preview": "do\n\n\nSkynetIADSTableDelegator = {}\n\nfunction SkynetIADSTableDelegator:create()\n\tlocal instance = {}\n\tlocal forwarder = {"
  },
  {
    "path": "skynet-iads-source/skynet-iads.lua",
    "chars": 19867,
    "preview": "do\n\nSkynetIADS = {}\nSkynetIADS.__index = SkynetIADS\n\nSkynetIADS.database = samTypesDB\n\nfunction SkynetIADS:create(name)\n"
  },
  {
    "path": "skynet-iads-source/skynet-mooose-a2a-dispatcher-connector.lua",
    "chars": 2088,
    "preview": "do\n\nSkynetMooseA2ADispatcherConnector = {}\n\nfunction SkynetMooseA2ADispatcherConnector:create(iads)\n\tlocal instance = {}"
  },
  {
    "path": "skynet-iads-source/syknet-iads-sam-launcher.lua",
    "chars": 4510,
    "preview": "do\n\nSkynetIADSSAMLauncher = {}\nSkynetIADSSAMLauncher = inheritsFrom(SkynetIADSSAMSearchRadar)\n\nfunction SkynetIADSSAMLau"
  },
  {
    "path": "unit-tests/highdigitsams/skynet-high-digit-sams-unit-test-setup.lua",
    "chars": 645,
    "preview": "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:g"
  },
  {
    "path": "unit-tests/highdigitsams/test-skynet-high-digit-sam-sites.lua",
    "chars": 13332,
    "preview": "do\n\nTestSyknetIADSHighDigitSAMSites = {}\n\nfunction TestSyknetIADSHighDigitSAMSites:setUp()\n\tif self.samSiteName then\n\t\ts"
  },
  {
    "path": "unit-tests/luaunit.lua",
    "chars": 117262,
    "preview": "--[[\n        luaunit.lua\n\nDescription: A unit testing framework\nHomepage: https://github.com/bluebird75/luaunit\nDevelopm"
  },
  {
    "path": "unit-tests/skynet-unit-test-iads-setup.lua",
    "chars": 4628,
    "preview": "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"
  },
  {
    "path": "unit-tests/skynet-unit-tests.lua",
    "chars": 765,
    "preview": "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 metho"
  },
  {
    "path": "unit-tests/test-skynet-iads-abstract-dcs-object-wrapper.lua",
    "chars": 1746,
    "preview": "do\n\nTestSkynetIADSAbstractDCSObjectWrapper = {}\n\nfunction TestSkynetIADSAbstractDCSObjectWrapper:setUp()\n\tself.abstractO"
  },
  {
    "path": "unit-tests/test-skynet-iads-abstract-element.lua",
    "chars": 3889,
    "preview": "do\n\t\nTestSkynetIADSAbstractElement = {}\n\nfunction TestSkynetIADSAbstractElement:setUp()\n\tself.iads =  SkynetIADS:create("
  },
  {
    "path": "unit-tests/test-skynet-iads-abstract-radar-element.lua",
    "chars": 46745,
    "preview": "do\nTestSkynetIADSAbstractRadarElement = {}\n\nfunction TestSkynetIADSAbstractRadarElement:setUp()\n\tif self.samSiteName the"
  },
  {
    "path": "unit-tests/test-skynet-iads-blue-sam-sites-and-ew-radars.lua",
    "chars": 15323,
    "preview": "do\nTestSkynetIADSBLUESAMSitesAndEWRadars = {}\n\nfunction TestSkynetIADSBLUESAMSitesAndEWRadars:setUp()\n\tif self.samSiteNa"
  },
  {
    "path": "unit-tests/test-skynet-iads-contact.lua",
    "chars": 4692,
    "preview": "do\n\nTestSyknetIADSContact = {}\n\nfunction TestSyknetIADSContact:setUp()\n\tlocal radarTarget = {}\n\tradarTarget.object = Uni"
  },
  {
    "path": "unit-tests/test-skynet-iads-harm-detection.lua",
    "chars": 6214,
    "preview": "do\n\nTestSkynetIADSHARMDetection = {}\n\nfunction TestSkynetIADSHARMDetection:setUp()\n\tlocal iads = SkynetIADS:create()\n\tse"
  },
  {
    "path": "unit-tests/test-skynet-iads-jammer.lua",
    "chars": 2876,
    "preview": "do\nTestSkynetIADSJammer = {}\n\nfunction TestSkynetIADSJammer:setUp()\n\tself.emitter = Unit.getByName('jammer-source')\t\n\tse"
  },
  {
    "path": "unit-tests/test-skynet-iads-red-sam-sites-and-ew-radars.lua",
    "chars": 22836,
    "preview": "do\nTestSkynetIADSREDSAMSitesAndEWRadars = {}\n\nfunction TestSkynetIADSREDSAMSitesAndEWRadars:setUp()\n\tself.skynetIADS = S"
  },
  {
    "path": "unit-tests/test-skynet-iads-sam-site.lua",
    "chars": 8985,
    "preview": "do\n\nTestSkynetIADSSAMSite = {}\n\nfunction TestSkynetIADSSAMSite:setUp()\n\tself.skynetIADS = SkynetIADS:create()\n\tif self.s"
  },
  {
    "path": "unit-tests/test-skynet-iads.lua",
    "chars": 24603,
    "preview": "do\nTestSkynetIADS = {}\n\nfunction TestSkynetIADS:setUp()\n\tself.numSAMSites = SKYNET_UNIT_TESTS_NUM_SAM_SITES_RED \n\tself.n"
  },
  {
    "path": "unit-tests/test-skynet-moose-a2a-dispatcher-connector.lua",
    "chars": 2957,
    "preview": "do\nTestMooseA2ADispatcherConnector = {}\n\nfunction TestMooseA2ADispatcherConnector:setUp()\n\tself.iads = SkynetIADS:create"
  },
  {
    "path": "unit-tests/test-syknet-early-warning-radar.lua",
    "chars": 2861,
    "preview": "do\nTestSkynetIADSEWRadar = {}\n\nfunction TestSkynetIADSEWRadar:setUp()\n\tself.numEWSites = SKYNET_UNIT_TESTS_NUM_EW_SITES_"
  }
]

// ... and 5 more files (download for full content)

About this extraction

This page contains the full source code of the walder/Skynet-IADS GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 52 files (909.9 KB), approximately 256.5k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!