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

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:**
[](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

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

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

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.

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

## 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'.

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

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

## 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
```

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

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.

## 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
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\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\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.