Repository: walder/Skynet-IADS Branch: master Commit: 62aab46901ee Files: 52 Total size: 909.9 KB Directory structure: gitextract_fdixfuk7/ ├── .gitattributes ├── .gitignore ├── LICENSE.md ├── README.md ├── build-tools/ │ └── build-compiled-script.ps1 ├── contributing.md ├── demo-missions/ │ ├── mist_4_5_107.lua │ ├── moose_a2a_connector/ │ │ ├── skynet-and-moose-a2a-dispatcher-setup.lua │ │ └── skynet-and-moose-a2a-dispatcher.miz │ ├── skynet-iads-compiled.lua │ ├── skynet-iads-setup-persian-gulf.lua │ ├── skynet-test-persian-gulf-stress-test.miz │ └── skynet-test-persian-gulf.miz ├── skynet-iads-source/ │ ├── README_source.md │ ├── highdigitsams/ │ │ └── skynet-iads-high-digit-sams-suported-types.lua │ ├── skynet-iads-abstract-dcs-object-wrapper.lua │ ├── skynet-iads-abstract-element.lua │ ├── skynet-iads-abstract-radar-element.lua │ ├── skynet-iads-awacs-radar.lua │ ├── skynet-iads-command-center.lua │ ├── skynet-iads-contact.lua │ ├── skynet-iads-early-warning-radar.lua │ ├── skynet-iads-harm-detection.lua │ ├── skynet-iads-jammer.lua │ ├── skynet-iads-logger.lua │ ├── skynet-iads-sam-search-radar.lua │ ├── skynet-iads-sam-site.lua │ ├── skynet-iads-sam-tracking-radar.lua │ ├── skynet-iads-supported-types.lua │ ├── skynet-iads-table-delegator.lua │ ├── skynet-iads.lua │ ├── skynet-mooose-a2a-dispatcher-connector.lua │ └── syknet-iads-sam-launcher.lua └── unit-tests/ ├── highdigitsams/ │ ├── highdigitsams-unit-tests.miz │ ├── skynet-high-digit-sams-unit-test-setup.lua │ └── test-skynet-high-digit-sam-sites.lua ├── luaunit.lua ├── skynet-unit-test-iads-setup.lua ├── skynet-unit-tests.lua ├── skynet-unit-tests.miz ├── test-skynet-iads-abstract-dcs-object-wrapper.lua ├── test-skynet-iads-abstract-element.lua ├── test-skynet-iads-abstract-radar-element.lua ├── test-skynet-iads-blue-sam-sites-and-ew-radars.lua ├── test-skynet-iads-contact.lua ├── test-skynet-iads-harm-detection.lua ├── test-skynet-iads-jammer.lua ├── test-skynet-iads-red-sam-sites-and-ew-radars.lua ├── test-skynet-iads-sam-site.lua ├── test-skynet-iads.lua ├── test-skynet-moose-a2a-dispatcher-connector.lua └── test-syknet-early-warning-radar.lua ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ # Auto detect text files and perform LF normalization * text=auto ================================================ FILE: .gitignore ================================================ .DS_STORE /demo-missions/spikes/ ================================================ FILE: LICENSE.md ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # Skynet-IADS ![logo](/images/SA3_2.jpg) An IADS (Integrated Air Defence System) script for DCS (Digital Combat Simulator). # Abstract This script simulates an IADS within the scripting possibilities of DCS. Early Warning Radar Stations (EW Radar) scan the sky for contacts. These contacts are correlated with SAM (Surface to Air Missile) sites. If a contact is within firing range of the SAM site it will become active. A modern IADS also depends on command centers and datalinks to the SAM sites. The IADS can be set up with this infrastructure. Destroying it will degrade the capability of the IADS. This all sounds gibberish to you? Watch [this video by Covert Cabal on modern IADS](https://www.youtube.com/watch?v=9J9kntzkSQY). Visit [this DCS forum thread](https://forums.eagle.ru/topic/226173-skynet-an-iads-for-mission-builders) for development updates. Join the [Skynet discord group](https://discord.gg/pz8wcQs) and get support setting up your mission. Skynet supports the [HighDigitSAMs Mod](https://github.com/Auranis/HighDigitSAMs). You can also connect [Skynet with the AI_A2A_DISPATCHER](#how-do-i-connect-skynet-with-the-moose-ai_a2a_dispatcher-and-what-are-the-benefits-of-that) by MOOSE to add interceptors to the IADS. **So far over 200 hours of work went in to the development of Skynet. If you like using it, please consider a donation:** [![Skynet IADS donation](/images/btn_donateCC_LG.gif.png)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=7GSVFH448BWFQ&source=url) Table of Contents ================= * [Skynet\-IADS](#skynet-iads) * [Abstract](#abstract) * [Quick start](#quick-start) * [Skynet IADS Elements](#skynet-iads-elements) * [IADS](#iads) * [Track files](#track-files) * [Comand Centers](#comand-centers) * [SAM Sites](#sam-sites) * [Early Warning Radars](#early-warning-radars) * [Power Sources](#power-sources) * [Connection Nodes](#connection-nodes) * [AWACS (Airborne Early Warning and Control System) ](#awacs-airborne-early-warning-and-control-system) * [Ships](#ships) * [Tactics](#tactics) * [HARM defence](#harm-defence) * [HARM detection](#harm-detection) * [HARM flight path analysis](#harm-flight-path-analysis) * [HARM radar shutdown](#harm-radar-shutdown) * [Point defence](#point-defence) * [Electronic Warfare](#electronic-warfare) * [Using Skynet in the mission editor](#using-skynet-in-the-mission-editor) * [Placing units](#placing-units) * [Preparing a SAM site](#preparing-a-sam-site) * [Preparing an EW radar](#preparing-an-ew-radar) * [Adding the Skynet code](#adding-the-skynet-code) * [Adding the Skynet IADS](#adding-the-skynet-iads) * [Advanced setup](#advanced-setup) * [IADS configuration](#iads-configuration) * [Adding a command center](#adding-a-command-center) * [Power sources and connection nodes](#power-sources-and-connection-nodes) * [Warm up the SAM sites of an IADS](#warm-up-the-sam-sites-of-an-iads) * [Connecting Skynet to the MOOSE AI\_A2A\_DISPATCHER](#connecting-skynet-to-the-moose-ai_a2a_dispatcher) * [SAM site configuration](#sam-site-configuration) * [Adding SAM sites](#adding-sam-sites) * [Add multiple SAM sites](#add-multiple-sam-sites) * [Add a SAM site manually](#add-a-sam-site-manually) * [Accessing SAM sites in the IADS](#accessing-sam-sites-in-the-iads) * [Act as EW radar](#act-as-ew-radar) * [Engagement zone](#engagement-zone) * [Engagement zone options](#engagement-zone-options) * [Engage air weapons](#engage-air-weapons) * [Engage HARM](#engage-harm) * [Add go live constraints](#add-go-live-constraints) * [Use cases](#use-cases) * [Contact](#contact) * [EW radar configuration](#ew-radar-configuration) * [Adding EW radars](#adding-ew-radars) * [Add multiple EW radars](#add-multiple-ew-radars) * [Add an EW radar manually](#add-an-ew-radar-manually) * [Accessing EW radars in the IADS](#accessing-ew-radars-in-the-iads) * [Options for SAM sites and EW radars](#options-for-sam-sites-and-ew-radars) * [Setting an option](#setting-an-option) * [Daisy chaining options](#daisy-chaining-options) * [HARM Defence](#harm-defence-1) * [Point defence](#point-defence-1) * [Autonomous mode behaviour](#autonomous-mode-behaviour) * [Autonomous mode options](#autonomous-mode-options) * [Adding a jammer](#adding-a-jammer) * [Advanced functions](#advanced-functions) * [Setting debug information](#setting-debug-information) * [Example Setup](#example-setup) * [FAQ](#faq) * [Does Skynet IADS have an impact on game performance?](#does-skynet-iads-have-an-impact-on-game-performance) * [What air defence units shall I add to the Skynet IADS?](#what-air-defence-units-shall-i-add-to-the-skynet-iads) * [Which SAM systems can engage HARMS?](#which-sam-systems-can-engage-harms) * [What exactly does Skynet do with the SAMS?](#what-exactly-does-skynet-do-with-the-sams) * [Are there known bugs?](#are-there-known-bugs) * [How do I know if a SAM site is in range of an EW site or a SAM site in EW mode?](#how-do-i-know-if-a-sam-site-is-in-range-of-an-ew-site-or-a-sam-site-in-ew-mode) * [How do I connect Skynet with the MOOSE AI\_A2A\_DISPATCHER and what are the benefits of that?](#how-do-i-connect-skynet-with-the-moose-ai_a2a_dispatcher-and-what-are-the-benefits-of-that) * [Thanks](#thanks) # Quick start Tired of reading already? Download the [demo mission](/demo-missions/skynet-test-persian-gulf.miz) in the persian gulf map and see Skynet in action. More complex demo missions will follow soon. # Skynet IADS Elements ![Skynet IADS overview](/images/skynet-overview.jpg) ## IADS A Skynet IADS is a complete operational network. You can have multiple Skynet IADS instances per coalition in a DCS mission. A simple setup would be one IADS for the blue side and one IADS for the red side. ## Track files Skynet keeps a global track file of all detected targets. It queries all its units with radars and deduplicates contacts. By default lost contacts are stored up to 32 seconds in memory. ## Comand Centers You can add multiple command centers to a Skynet IADS. Once all command centers are destroyed the IADS will go in to autonomous mode. ## SAM Sites Skynet can handle multiple SAM sites, it will try and keep emissions to a minimum, therefore by default SAM sites will be turned on only if a target is in range. Every single launcher and radar unit's distance of a SAM site is analysed individually. If at least one launcher and radar is within range, the SAM Site will become active. This allows for a scattered placement of radar and launcher units as in real life. If SAM sites or radar guided AAA run out of ammo they will go dark. In the case of a SAM site it will wait with going dark as long as the last fired missile is still in the air. If an EW radar or a SAM site acting as EW radar is destoyed surrounding SAM sites can be left withouth EW radar coverage. This can also happen if a SAM site is outside of AWACS coverage. SAM sites will go autonomous in such a case meaning they will use their organic radars or just stay dark depending on setup. Once a SAM site is within EW radar coverage again it will be updated by the IADS. ## Early Warning Radars Skynet can handle 0-n EW radars. For detection of a target the DCS radar detection logic is used. You can use any type of radar listed in [skynet-iads-supported-types.lua](/skynet-iads-source/skynet-iads-supported-types.lua) in an EW role in Skynet. Some modern SAM radars have a greater detection range than older EW radars, e.g. the S-300PS 64H6E (160 km) vs EWR 55G6 (120 km). You can also designate SAM sites to act as EW radars, in this case a SAM site will constantly have their radar on. Long range systems like the S-300 are used as EW radars in real life. SAM sites that are out of ammo will stay live if they are set to act as EW radars. Nice to know: Terrain elevation around an EW radar will create blinds spots, allowing low and fast movers to penetrate radar networks through valleys. ## Power Sources By default Skynet IADS will run without having to add power sources. You can add multiple power sources to SAM sites, EW radars and command centers. Once a power source is fully damaged the Skynet IADS unit will stop working. Nice to know: Taking out the power source of a command center is a real life tactic used in SEAD (Suppression of Enemy Air Defence). ## Connection Nodes By default Skynet IADS will run without having to add connection nodes. You can add multiple connection nodes to SAM sites, EW radars and command centers. When all the unit's connection nodes are fully damaged an EW radar or SAM site will go in to autonomous mode. For a SAM site this means it will behave in its autonomous mode setting. If an EW Radar looses its node it will no longer contribute information to the IADS but otherwise the IADS will still work. Command centers do not have an autonomous mode. Nice to know: A single node can be used to connect an arbitrary number of Skynet IADS units. This way you can add a single point of failure in to an IADS. ## AWACS (Airborne Early Warning and Control System) Any aircraft with an air to air radar can be added as AWACS. Contacts detected will be added to the IADS. The AWACS will also detect ground units like ships. These will however not be passed to the SAM sites. You can add a connection node for the AWACS like an antenna, if it is destroyed, the AWACS will no longer be able to contribute contacts to the IADS. Technically you can also add a power source. In this context it would represent the power source for the connection node, since an aircraft provides its own power. ## Ships Ships will contribute to the IADS the same way AWACS units do. Add them as a regular EW radar. # Tactics ## HARM defence SAM sites and EW radars will shut down their radars if they believe a HARM (High speed anti radiation missile) is heading for them. For this to happen, the IADS will evaluate contacts and determine if they are likely to be HARMs. Each SAM site or EW radar has HARM detection chance set. If a HARM is detected by more than one radar, the chance of it being identified as a HARM is increased. See [skynet-iads-supported-types.lua](/skynet-iads-source/skynet-iads-supported-types.lua) field ```['harm_detection_chance']``` for the probability per radar system. ### HARM detection let's say SAM site A has a 60% HARM detection chance and SAM site B has a 50% HARM detection cance. If a HARM is picked up by both radars the chance the IADS will identify the HARM will be 80%. With the radar cross section updates of HARMs in DCS 2.7 older radars like the ones used in the SA-2 and SA-6 can only identifiy a HARM at very close range usualy less than 10 seconds before impact. These systems will not have a very good HARM defence with Skynet. ![Skynet IADS overview](/images/skynet-harm-detection.jpg) ### HARM flight path analysis The contact needs to be traveling faster than 800 kt and it may not have changed its flight path more than 2 times (eg ```climb-descend```, ```climb``` or ```descend```).This is to minimise false positives, for example a fighter flying very fast. ![Skynet IADS overview](/images/skynet-harm-flightpath.jpg) This implementation is closer to real life. SAM sites like the patriot and most likely modern Russian systems calculate the flight path and analyse the radar cross section to determine if a contact heading inbound is a HARM. If identified as a HARM the IADS will shut down radars 15 degrees left and right of the HARM's fight path up to a distance of 20 nautical miles in front of the HARM. The IADS will calculate time to impact and shut down radar emitters up to a maximum of 180 seconds after time to impact. ## HARM radar shutdown Once a HARM has been identified by Skynet, radars up to 20 nm ahead and 15 degrees left or right of the HARM will be notified. Depending on their settins radar emitters will shut down or start defending against the HARM. ![Skynet IADS overview](/images/skynet-harm-radar-shutdown.jpg) ## Point defence When a radar emitter (EW radar or SAM site) is attacked by a HARM there is a chance it may detect the HARM and go dark. If this radar emitter is acting as the sole EW radar in the area, surrounding SAM sites will not be able to go live since they rely on the EW radar for target information. This is an issue if you have SA-15 Tors next to the EW radar for point defence protection. They will stay dark and not engange the HARM. Use this feature if you don't want the IADS to loose situational awareness just because a HARM is inbound. The radar emitter will shut down, if it believes its point defences won't be able to handle the number of HARMs inbound. As long as there is one point defence launcher and missile per HARM inbound the radar emitter will keep emitting. If the HARMs exeed the number of point defence launchers and missiles the protected asset will shut down. Tests in DCS have shown that this is roughly the saturation point. If the SAM site reling on point defence can engagen HARMs its launchers an missiles will also count to the saturation point. See FAQ [Which SAM systems can engage HARMS?](#which-sam-systems-can-engage-harms) [Point defence setup example](#point-defence-1) ## Electronic Warfare A simple form of jamming is part of the Skynet IADS package. It's off by default. The jamming works by setting the ROE state of a SAM Site. The closer the jamming emitter gets to a SAM site the less effective jamming will become (burn through). For the jammer to work it will need LOS (line of sight) to a radar unit. Older SAM sites are more susceptible to jamming. EW radars are currently not jammable. I recommend you add an AI unit that follows the strike package you're flying in to act as a jammer aircraft. This will give you the most realistic experience. The jammer emitter will toggle the ROE state of a SAM site which affects how the SAM site reacts to all threats near or far. I presume an aircraft very close to a SAM site beeing jammed by a emitter very far away would most likely be detected. So the farther away you are from the jammer source the more unrealistic your experience will be. Here is a [list of SAM sites currently supported by the jammer](https://docs.google.com/spreadsheets/d/16rnaU49ZpOczPEsdGJ6nfD0SLPxYLEYKmmo4i2Vfoe0/edit#gid=0) and the jammer's effectiveness on them. When setting up a jammer you can decide which SAM sites it is able to jam. For example you could design a mission in which the jammer is not able to jam a SA-6 but is able to jam a SA-2. The jammer effectiveness is not based on any real world data I just read about the different types and made my own conclusions. Here is an old school documentary [showing the Prowler in action](https://www.youtube.com/watch?v=su44ZU7NcQU). They brief to turn on their jamming equipement at 60 nm from the target. I suppose that must have been the effective range of 70's jamming tech. # Using Skynet in the mission editor It's quite simple to setup an IADS have a look at the demo missions in the [/demo-missions/](/demo-missions) folder. ## Placing units This tutorial assumes you are familiar on how to set up a SAM site in DCS. If not I suggest you watch [this video](https://www.youtube.com/watch?v=YZPh-JNf6Ww) by the Grim Reapers. Place the IADS elements you wish to add on the map. ![Mission Editor IADS Setup](/images/iads-setup.png) ## Preparing a SAM site There may be only be **one type of SAM site per group**. More than one type of SAM site per group will result in Skynet no being able to properly controll the group. Also please refrain from from adding units to the SAM group that are not required for the SAM like trucks, tanks and soldiers. The skill level you set on a SAM group is retained by Skynet. Make sure you name the **SAM site group** in a consistent manner with a prefix e.g. 'SAM-SA-2'. ![Mission Editor add SAM site](/images/add-sam-site.png) ## Preparing an EW radar You can use any type of radar as an EW radar. Make sure you **name the unit** in a consistent manner with a prefix, e.g. 'EW-center3'. Make sure you have only **one EW radar in a group** otherwise Skynet will not be able to control single EW radars. ![Mission Editor EW radar](/images/ew-setup.png) ## Adding the Skynet code Skynet requires MIST. A version is provided in this repository or you can download the most current version [here](https://github.com/mrSkortch/MissionScriptingTools). Make sure you load MIST and the compiled skynet code in to a mission. The [skynet-iads-compiled.lua](/demo-missions/skynet-iads-compiled.lua) and [mist_4_5_107.lua](/demo-missions/mist_4_5_107.lua) files are located in the [/demo-missions/](/demo-missions) folder. I recommend you create a text file e.g. 'my-iads-setup.lua' and then add the code needed to get the IADS runing. When updating the setup remember to reload the file in the mission editor. Otherwise changes will not become effective. You can also add the code directly in the mission editor, however that input field is quite small if you write more than a few lines of code. ![Mission Editor IADS Setup](/images/load-scripts.png) ## Adding the Skynet IADS For the IADS to work you need four lines of code. create an instance of the IADS, the name string is optional and will be displayed in status output: ```lua redIADS = SkynetIADS:create('name') ``` Give all SAM groups you want to add a common prefix in the mission editor eg: 'SAM-SA-10 west', then add this line of code: ```lua redIADS:addSAMSitesByPrefix('SAM') ``` Same for the EW radars, name all units with a common prefix in the mission editor eg: 'EW-radar-south': ```lua redIADS:addEarlyWarningRadarsByPrefix('EW') ``` Activate the IADS: ```lua redIADS:activate() ``` # Advanced setup This is the danger zone. Call Kenny Loggins. Some experience with scripting is recommended. You can handcraft your IADS with the following functions. If you refrence units that don't exist a message will be displayed when the mission loads. The following examples use static objects for command centers, connection nodes and power sources, you can also use units instead. ## IADS configuration Call this method to add or remove a radio menu to toggle the status output of the IADS. By default the radio menu option is not visible: ```lua redIADS:addRadioMenu() ``` ```lua redIADS:removeRadioMenu() ``` If you dereference the IADS remember to call ```deactivate()``` otherwise background tasks of the IADS will continue running, resulting in unexpected behaviour: ```lua redIADS:deactivate() ``` Set the update interval in seconds of the IADS. This determines in what interval the IADS wil turn SAM sites of or on according to targets it has detected: ```lua redIADS:setUpdateInterval(5) ``` ## Adding a command center The command center represents the place where information is collected and analysed. It if is destroyed the IADS disintegrates. Add a command center like this: ```lua local commandCenter = StaticObject.getByName("Command Center") redIADS:addCommandCenter(commandCenter) ``` ## Power sources and connection nodes You can use units or static objects. Call the function multiple times to add more than one power source or connection node: ```unit``` refers to a SAM site, or EW Radar you retrieved from the IADS, see [setting an option for Radar units](#setting-an-option). ```lua local powerSource = StaticObject.getByName("EW Power Source") unit:addPowerSource(powerSource) ``` ```lua local connectionNode = Unit.getByName("EW connection node") unit:addConnectionNode(connectionNode) ``` For command centers use: ```lua local commandCenter = StaticObject.getByName("Command Center2") local comPowerSource = StaticObject.getByName("Command Center2 Power Source") redIADS:addCommandCenter(commandCenter):addPowerSource(comPowerSource) ``` ## Warm up the SAM sites of an IADS This function is deprecated and will be removed in a future release. ```lua redIADS:setupSAMSitesAndThenActivate() ``` ## Connecting Skynet to the MOOSE AI_A2A_DISPATCHER You can connect Skynet with MOOSE's [AI_A2A_DISPATCHER](https://flightcontrol-master.github.io/MOOSE_DOCS/Documentation/AI.AI_A2A_Dispatcher.html). This allows the IADS not only to direct SAM sites but also to scramble fighters. Skynet will set the radars it can use on the SET_GROUP object of a dispatcher. Meaning that if a radar is lost in Skynet it will no longer be availabe to detect and scramble interceptors. Add the object of type SET_GROUP to the iads like this (in this example ```DectionSetGroup```): ```lua redIADS:addMooseSetGroup(DetectionSetGroup) ``` ## SAM site configuration ### Adding SAM sites #### Add multiple SAM sites Adds SAM sites with prefix in group name to the IADS. Previously added SAM sites are cleared: ```lua redIADS:addSAMSitesByPrefix('SAM') ``` #### Add a SAM site manually You can manually add a SAM site, must be a valid group name: ```lua redIADS:addSAMSite('SA-6 Group2') ``` ### Accessing SAM sites in the IADS The following functions exist to access SAM sites added to the IADS. They all support daisy chaining options: Returns all SAM sites with the corresponding Nato name, see [skynet-iads-supported-types.lua](/skynet-iads-source/skynet-iads-supported-types.lua). For all units beginning with 'SA-': Don't add Nato code names (Guideline, Gainful), just write 'SA-2', 'SA-6': ```lua redIADS:getSAMSitesByNatoName('SA-6') ``` Returns all SAM sites in the IADS: ```lua redIADS:getSAMSites() ``` Returns a SAM site with the specified group name: ```lua redIADS:getSAMSiteByGroupName('SAM-SA-6') ``` Returns a SAM site with the specified group name prefix. Let's say you have a bunch of SAM sites that all will share the same power source. Give these sites a special prefix in the group name, e.g.: ```'SAM-SECTOR-A'```. Once you have added the SAM sites you can access them via the prefix to set whatever options you want: ```lua redIADS:getSAMSitesByPrefix('SAM-SECTOR-A') ``` ### Act as EW radar Will set the SAM site to act as an EW radar. This will result in the SAM site always having its radar on. Contacts the SAM site sees are reported to the IADS. This option is recomended for long range systems like the S-300: ```lua samSite:setActAsEW(true) ``` ### Engagement zone Set the distance at which a SAM site will switch on its radar: ```lua samSite:setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE) ``` #### Engagement zone options SAM site will go live when target is within the red circle in the mission editor (default Skynet behaviour): ```lua SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE ``` SAM site will go live when target is within the yelow circle in the mission editor: ```lua SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE ``` This option sets the range in relation to the zone you set in ``setEngagementZone`` for a SAM site to go live. Be careful not to set the value too low. Some SAM sites need up to 30 seconds until they can fire. During this time a target might have already left the engagement zone of SAM site. This option is intended for long range systems like the S-300. You can also set the range above 100 this will have the effect that the SAM site goes live earlier: ```lua samSite:setGoLiveRangeInPercent(90) ``` ### Engage air weapons Will set the SAM site to engage air weapons, if it is able to do so in DCS. It is a wrapper for the [ENGAGE_AIR_WEAPONS](https://wiki.hoggitworld.com/view/DCS_option_engage_air_weapons) setting. ```lua samSite:setCanEngageAirWeapons(true) ``` ### Engage HARM Will set the SAM site to engage HARMs, if it is able to do so in DCS. If set to false the SAM site will shut down if a HARM that has been identified by the IADS is inbound. SAM sites that can engage HARMS are set to true by default. ```lua samSite:setCanEngageHARM(true) ``` ## Add go live constraints You can include constraints wich must be satisfied for the SAM site to go live. Please note this only controls activation of the SAM site. There is currently no way to tell a SAM site to only target a certain contact via the lua scripting engine in DCS. The constraint must evaluate to true and the contact must be in range of the SAM site (handled by Skynet). ### Use cases Place a SAM site on an flight path that you suspect strike strike fighters will pass. Add a heading constraint to ensure that the SAM site will only go ive when fighters are on their way back from the target. Set a SAM site to only go live if aircraft are in a certain altitude band. SAM site shall only go live once a strike package has destroyed a certain building or unit. You do not have to use the contact provided in the function to evaluate the constraint. You can make any assertion you want. Create a function that will evaluate if the constraint is satisfied. The function will have access to the [contact](#contact) the SAM site is evaluating: ```lua --SAM site will only go live if the contact is below 1000 feet. local function goLiveConstraint(contact) return ( contact:getHeightInFeetMSL() < 1000 ) end ``` Add the function to the SAM site and give it a name. You can add as many constraints as you wish: ```lua self.samSite:addGoLiveConstraint('ignore-low-flying-contacts', goLiveConstraint) ``` Remove constraint you no longer wish to use: ```lua self.samSite:removeGoLiveConstraint('ignore-low-flying-contacts') ``` Get a table of all constraints: ```lua self.samSite:getGoLiveConstraints() ``` ## Contact You can use the following methods to get information about a contact. Will return true if contact has been identified as a HARM by Skynet: ```lua contact:isIdentifiedAsHARM() ``` Will return the height of a contact: ```lua contact:getHeightInFeetMSL() ``` Will return the current magnetic heading of a contact. Note the heading is availble only after a contact has been tracked in more than one cycle by the IADS. Until that has happened heading will be 0: ```lua contact:getMagneticHeading() ``` Will return the current ground speed of a contact. Note the speed is availble only after a contact has been tracked in more than one cycle by the IADS. Until that has happend speed will be 0: ```lua contact:getMagneticHeading() ``` Will return the time in seconds a contact has been known to the IADS: ```lua contact:getAge() ``` Will return the type as a ```Object.Category```: ```lua contact:getTypeName() ``` Will return the unit name: ```lua contact:getName() ``` ## EW radar configuration ### Adding EW radars #### Add multiple EW radars Adds EW radars with prefix in unit name to the IADS. Previously added EW sites are cleared: ```lua redIADS:addEarlyWarningRadarsByPrefix('EW') ``` #### Add an EW radar manually You can add EW radars manually, must be a valid unit name: ```lua redIADS:addEarlyWarningRadar('EWR West') ``` ### Accessing EW radars in the IADS The following functions exist to access EW radars added to the IADS. They all support daisy chaining options. Returns all EW radars in the IADS: ```lua redIADS:getEarlyWarningRadars() ``` Returns the EW radar with the specified unit name: ```lua redIADS:getEarlyWarningRadarByUnitName('EW-west') ``` ## Options for SAM sites and EW radars ### Setting an option In the following examples ```ewRadarOrSamSite``` refers to an single EW radar or SAM site or a table of EW radars and SAM sites you got from the Skynet IADS, by calling one of the functions named in [accessing EW radars](#accessing-ew-radars-in-the-iads) or [accessing SAM sites](#accessing-sam-sites-in-the-iads). ### Daisy chaining options You can daisy chain options on a single SAM site / EW Radar or a table of SAM sites / EW radars like this: ```lua redIADS:getSAMSites():setActAsEW(true):addPowerSource(powerSource):addConnectionNode(connectionNode):setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE):setGoLiveRangeInPercent(90):setAutonomousBehaviour(SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK) ``` ### HARM Defence You can set the reaction probability (between 0 and 100 percent). See [skynet-iads-supported-types.lua](/skynet-iads-source/skynet-iads-supported-types.lua) field ```['harm_detection_chance']``` for default detection probabilities: ```lua ewRadarOrSamSite:setHARMDetectionChance(50) ``` ### Point defence You must use a point defence SAM that can engage HARM missiles. Can be used to protect SAM sites or EW radars. See [point defence](#point-defence) for information what this does: If you want the point defences to coordinate their HARM defence then you can add multiple point defence SAM sites in to one group. **This is the only place where you should add multiple SAM sites in to one group in Skynet**. Let's assume you have two SA-15 units defending a radar. If the SA-15 units are in separate groups they will both fire at the same HARM inbound. However if they are in the same group and multiple HARMS are inbound they will each pick a separate HARM to engage. ```lua --first get the SAM site you want to use as point defence from the IADS: local sa15 = redIADS:getSAMSiteByGroupName('SAM-SA-15') --then add it to the SAM site it should protect: redIADS:getSAMSiteByGroupName('SAM-SA-10'):addPointDefence(sa15) ``` This function is deprecated and will be removed in a future release. ```lua ewRadarOrSamSite:setIgnoreHARMSWhilePointDefencesHaveAmmo(true) ``` ### Autonomous mode behaviour Set how the SAM site or EW radar will behave if it looses connection to the IADS: ```lua ewRadarOrSamSite:setAutonomousBehaviour(SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK) ``` #### Autonomous mode options SAM site or EW radar will behave in the default DCS AI. Alarm State will be red and ROE weapons free (default Skynet behaviour for SAM sites): ```lua SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DCS_AI ``` SAM Site or EW radar will go dark if it looses connection to IADS (default behaviour for EW radars): ```lua SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK ``` ## Adding a jammer The jammer is quite easy to set up. You need a unit that acts as a jammer source, preferably it will be an aircraft in the strike package. Once the jammer detects an emitter it starts jamming the radar. Set the [coresponding debug variable jammerProbability](#setting-debug-information) to see what the jammer is doing. Check [skynet-iads-jammer.lua](/skynet-iads-source/skynet-iads-jammer.lua) to see which SAM sites are supported. Remember to set the AI aircraft acting as jammer in the Mission editor to ```Reaction to Threat = EVADE FIRE``` otherwise the AI will try and actively attack the SAM site. This way it will stick to the preset flight plan. Create a jammer and assign it to an unit. Also make sure you add the IADS you wan't the jammer to work for: ```lua local jammerSource = Unit.getByName("F-4 AI") jammer = SkynetIADSJammer:create(jammerSource, iads) ``` The jammer will start listening for emitters and if it finds one of the emitters it is able to jam it will start jamming it: ```lua jammer:masterArmOn() ``` Will disable jamming for the specified SAM type, pass the Nato name: ```lua jammer:disableFor('SA-2') ``` Will turn off the jammer. Make sure you call this function before you dereference a jammer in the code, otherwise a background task will keep on jamming: ```lua jammer:masterArmSafe() ``` Will add jammer on / off to the radio menu: ```lua jammer:addRadioMenu() ``` Will remove jammer on / off from the radio menu: ```lua jammer:removeRadioMenu() ``` ### Advanced functions Add a second IADS the jammer should be able to jam, for example if you have two separate IADS running: ```lua jammer:addIADS(iads2) ``` Add a new jammer function: ```lua -- write a lambda function that expects one parameter: -- given public available data on jammers their effeciveness drastically decreases the closer you get, so a non-linear function would make sense: local function f(distanceNM) return ( 1.4 ^ distanceNM ) + 80 end -- add the function: specify which SAM type it should apply for: self.jammer:addFunction('SA-10', f) ``` Set the maximum range the jammer will work, the default value is set to 200 nautical miles: ```lua jammer:setMaximumEffectiveDistance(100) ``` ## Setting debug information When developing a mission I suggest you add debug output to check how the IADS reacts to threats. Debug output may slow down DCS, so it's recommended to turn it off in a live environment: Access the debug settings: ```lua local iadsDebug = redIADS:getDebugSettings() ``` Output in game: ```lua iadsDebug.IADSStatus = true iadsDebug.contacts = true iadsDebug.jammerProbability = true ``` Output to dcs.log: ```lua iadsDebug.addedEWRadar = true iadsDebug.addedSAMSite = true iadsDebug.warnings = true iadsDebug.radarWentLive = true iadsDebug.radarWentDark = true iadsDebug.harmDefence = true ``` These three options will output detailed information on every radar in the IADS to the dcs.log file. Enabling these may have an impact on performance: ```lua iadsDebug.samSiteStatusEnvOutput = true iadsDebug.earlyWarningRadarStatusEnvOutput = true iadsDebug.commandCenterStatusEnvOutput = true ``` ![Mission Editor IADS Setup](/images/skynet-debug.png) # Example Setup This is an example of how you can set up your IADS used in the [demo mission](/demo-missions/skynet-test-persian-gulf.miz): ```lua do --create an instance of the IADS redIADS = SkynetIADS:create('RED IADS') ---debug settings remove from here on if you do not wan't any output on what the IADS is doing by default local iadsDebug = redIADS:getDebugSettings() iadsDebug.IADSStatus = true iadsDebug.radarWentDark = true iadsDebug.contacts = true iadsDebug.radarWentLive = true iadsDebug.noWorkingCommmandCenter = true iadsDebug.samNoConnection = true iadsDebug.jammerProbability = true iadsDebug.addedEWRadar = true iadsDebug.harmDefence = true ---end remove debug --- --add all units with unit name beginning with 'EW' to the IADS: redIADS:addEarlyWarningRadarsByPrefix('EW') --add all groups begining with group name 'SAM' to the IADS: redIADS:addSAMSitesByPrefix('SAM') --add a command center: commandCenter = StaticObject.getByName('Command-Center') redIADS:addCommandCenter(commandCenter) ---we add a K-50 AWACs, manually. This could just as well be automated by adding an 'EW' prefix to the unit name: redIADS:addEarlyWarningRadar('AWACS-K-50') --add a power source and a connection node for this EW radar: local powerSource = StaticObject.getByName('Power-Source-EW-Center3') local connectionNodeEW = StaticObject.getByName('Connection-Node-EW-Center3') redIADS:getEarlyWarningRadarByUnitName('EW-Center3'):addPowerSource(powerSource):addConnectionNode(connectionNodeEW) --add a connection node to this SA-2 site, and set the option for it to go dark, if it looses connection to the IADS: local connectionNode = Unit.getByName('Mobile-Command-Post-SAM-SA-2') redIADS:getSAMSiteByGroupName('SAM-SA-2'):addConnectionNode(connectionNode):setAutonomousBehaviour(SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK) --this SA-2 site will go live at 70% of its max search range: redIADS:getSAMSiteByGroupName('SAM-SA-2'):setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE):setGoLiveRangeInPercent(70) --all SA-10 sites shall act as EW sites, meaning their radars will be on all the time: redIADS:getSAMSitesByNatoName('SA-10'):setActAsEW(true) --set the SA-15's as point defence for the SA-10 site. We set the SA-10 to always identify HARMs so we can demonstrate the point defence mechanism in Skynet. --the SA-10 will stay online when shot at by HARMS as long as the point defences and SAM site have ammo and the saturation point is not reached. local sa15 = redIADS:getSAMSiteByGroupName('SAM-SA-15-point-defence-SA-10') redIADS:getSAMSiteByGroupName('SAM-SA-10'):addPointDefence(sa15):setHARMDetectionChance(100) --set this SA-11 site to go live 70% of max range of its missiles (default value: 100%), its HARM detection probability is set to 50% (default value: 70%) redIADS:getSAMSiteByGroupName('SAM-SA-11'):setGoLiveRangeInPercent(70):setHARMDetectionChance(50) --this SA-6 site will always react to a HARM being fired at it: redIADS:getSAMSiteByGroupName('SAM-SA-6'):setHARMDetectionChance(100) --set this SA-11 site to go live at maximunm search range (default is at maximung firing range): redIADS:getSAMSiteByGroupName('SAM-SA-11-2'):setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE) --activate the radio menu to toggle IADS Status output redIADS:addRadioMenu() --activate the IADS redIADS:activate() --add the jammer local jammer = SkynetIADSJammer:create(Unit.getByName('jammer-emitter'), redIADS) jammer:masterArmOn() --setup blue IADS: blueIADS = SkynetIADS:create('BLUE IADS') blueIADS:addSAMSitesByPrefix('BLUE-SAM') blueIADS:addEarlyWarningRadarsByPrefix('BLUE-EW') blueIADS:activate() blueIADS:addRadioMenu() local iadsDebug = blueIADS:getDebugSettings() iadsDebug.IADSStatus = true iadsDebug.contacts = true end ``` # FAQ ## Does Skynet IADS have an impact on game performance? Skynet may actually improve game performance when using a lot of SAM AI units. This is because Skynet will turn off radar emissions of all SAM groups currently not in range of a target. By default these SAM groups would otherwise have their radars on. Skynet caches target information for a few seconds to reduce expensive calls on DCS radar detection. ## What air defence units shall I add to the Skynet IADS? In theory you can add all the types that are listed in the [skynet-iads-supported-types.lua](skynet-iads-source/skynet-iads-supported-types.lua) file. Very short range units (like the Shilka AAA, Rapier) won't really benefit from the IADS apart from reacting to HARMs. These are better just placed in a mission and handeled by the default AI of DCS. This is due to the short range of their radars. By the time the IADS wakes them up, the contact has likely passed their engagement range. The strength of the Skynet IADS lies with handling long range systems that operate by radar. ## Which SAM systems can engage HARMS? As of July 2022 I have only been able to get the SA-15, SA-10, NASAMS and Patriot to engage HARMS. The best option for a solid HARM defence is to add SA-15's around EW radars or high value SAM sites. ## What exactly does Skynet do with the SAMS? Via the scripting engine one can toggle the radar emitters on and off. Further options are the alarm state and the rules of engagement. In a nutshell that's all that Skynet does. Skynet does also read the radar and firing range properties of a SAM site. Based on that data and the setup options a mission designer provides Skynet will turn a SAM site on or off. No god like intervention is used (like magically exploding HARMS via the scripting engine). If a SAM site or EW radar detects an inbound HARM it just turns off its radar as in real life. The HARM as it is programmed in DCS will try and glide in to the last known position mostly resulting in misses by 50-100 meters. ## Are there known bugs? Yes, when placing multi unit SAM sites (e.g. SA-3, Patriot..) make sure the first unit you place is the search radar. If you add any other element as the first unit, Skynet will not be able to read radar data. The result will be that the SAM site won't go live. This bug was observed in DCS 2.5.5. The SAM site will work fine when used as a standalone unit outside of Skynet. ## How do I know if a SAM site is in range of an EW site or a SAM site in EW mode? To get a rough idea you can look at the range circles in the mission editor. However these ranges are greater than the actual in game detection ranges of an EW radar or SAM site. The following screenshot shows the range of the 1L13 EWR. The mission editor shows a range of 64 NM (nautical miles) where as the in game range is 43 NM. In this example the SAM site to the north east would not be in range of the EW radar, therefore it would go in to autonomous mode once the mission starts. ![1L13 EWR range differences](/images/ew-detection-distance-example.png) Set the debug options ```samSiteStatusEnvOutput``` and ```earlyWarningRadarStatusEnvOutput``` to get detailed information on every SAM site and EW radar. The text marked in the red box will show you which SAM sites are in the covered area of a SAM site or EW radar. ![SAM sites in covered area](/images/radar-emitter-status-dcs-log.png) ## How do I connect Skynet with the MOOSE AI_A2A_DISPATCHER and what are the benefits of that? IRL an IADS would most likely not only handle SAM sites but also pass information to interceptor aircraft. By connecting Skynet to the [AI_A2A_DISPATCHER](https://flightcontrol-master.github.io/MOOSE_DOCS/Documentation/AI.AI_A2A_Dispatcher.html) by MOOSE you are able to add interceptors to the IADS. See [Skynet Moose AI_A2A_DISPATCHER](#connecting-skynet-to-the-moose-ai_a2a_dispatcher) and the [moose_a2a_connector demo mission](demo-missions/moose_a2a_connector) for more information. An example setup of Skynet and the [AI_A2A_DISPATCHER](https://flightcontrol-master.github.io/MOOSE_DOCS/Documentation/AI.AI_A2A_Dispatcher.html) : ```lua --Setup Syknet IADS: redIADS = SkynetIADS:create('Enemy IADS') redIADS:addSAMSitesByPrefix('SAM') redIADS:addEarlyWarningRadarsByPrefix('EW') redIADS:activate() -- START MOOSE CODE: -- Define a SET_GROUP object that builds a collection of groups that define the EWR network. DetectionSetGroup = SET_GROUP:New() -- Setup the detection and group targets to a 30km range! Detection = DETECTION_AREAS:New( DetectionSetGroup, 30000 ) -- Setup the A2A dispatcher, and initialize it. A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) -- Set 100km as the radius to engage any target by airborne friendlies. A2ADispatcher:SetEngageRadius() -- 100000 is the default value. -- Set 200km as the radius to ground control intercept. A2ADispatcher:SetGciRadius() -- 200000 is the default value. CCCPBorderZone = ZONE_POLYGON:New( "RED-BORDER", GROUP:FindByName( "RED-BORDER" ) ) A2ADispatcher:SetBorderZone( CCCPBorderZone ) A2ADispatcher:SetSquadron( "Kutaisi", AIRBASE.Caucasus.Kutaisi, { "Squadron red SU-27" }, 2 ) A2ADispatcher:SetSquadronGrouping( "Kutaisi", 2 ) A2ADispatcher:SetSquadronGci( "Kutaisi", 900, 1200 ) A2ADispatcher:SetTacticalDisplay(true) A2ADispatcher:Start() --END MOOSE CODE -- add the MOOSE SET_GROUP to the IADS, from now on Skynet will update active radars that the MOOSE SET_GROUP can use for EW detection. redIADS:addMooseSetGroup(DetectionSetGroup) ``` # Thanks Special thaks to Spearzone and Coranthia for researching public available information on IADS networks and getting me up to speed on how such a system works. I based the SAM site setup on [Grimes SAM DB](https://forums.eagle.ru/showthread.php?t=118175) from his IADS script, however I removed range data since Skynet loads that from DCS. ================================================ FILE: build-tools/build-compiled-script.ps1 ================================================ $version=$args[0] if ($version -eq $null){ echo "No Version supplied, not bulding script" return } if (Test-Path ./tmp/){ Remove-Item ./tmp/ } New-Item ./tmp/ -ItemType Directory if (Test-Path ./tmp/skynet-iads-compiled.lua) { Remove-Item ./tmp/skynet-iads-compiled.lua } Add-Content ./tmp/tmp-time.lua ("env.info(`"--- SKYNET VERSION: "+$version+" | BUILD TIME: "+(Get-Date -date (Get-Date).ToUniversalTime()-uformat "%d.%m.%Y %H%MZ")+" ---`")") cat ../skynet-iads-source/skynet-iads-supported-types.lua, ../skynet-iads-source/highdigitsams/skynet-iads-high-digit-sams-suported-types.lua, ../skynet-iads-source/skynet-iads-logger.lua, ../skynet-iads-source/skynet-iads.lua, ../skynet-iads-source/skynet-mooose-a2a-dispatcher-connector.lua, ../skynet-iads-source/skynet-iads-table-delegator.lua, ../skynet-iads-source/skynet-iads-abstract-dcs-object-wrapper.lua, ../skynet-iads-source/skynet-iads-abstract-element.lua, ../skynet-iads-source/skynet-iads-abstract-radar-element.lua, ../skynet-iads-source/skynet-iads-awacs-radar.lua, ../skynet-iads-source/skynet-iads-command-center.lua, ../skynet-iads-source/skynet-iads-contact.lua, ../skynet-iads-source/skynet-iads-early-warning-radar.lua, ../skynet-iads-source/skynet-iads-jammer.lua, ../skynet-iads-source/skynet-iads-sam-search-radar.lua, ../skynet-iads-source/skynet-iads-sam-site.lua, ../skynet-iads-source/skynet-iads-sam-tracking-radar.lua, ../skynet-iads-source/syknet-iads-sam-launcher.lua, ../skynet-iads-source/skynet-iads-harm-detection.lua | sc ./tmp/tmp-code.lua $code = Get-Content ./tmp/tmp-code.lua Add-Content ./tmp/tmp-time.lua $code Rename-Item -Path ./tmp/tmp-time.lua -NewName skynet-iads-compiled.lua Remove-Item ./tmp/tmp-code.lua if (Test-Path ../demo-missions/skynet-iads-compiled.lua) { Remove-Item ../demo-missions/skynet-iads-compiled.lua } Move-Item -Path ./tmp/skynet-iads-compiled.lua ../demo-missions/skynet-iads-compiled.lua $toc = ./bin/gh-md-toc.exe --hide-footer ../skynet-iads-source/README_source.md $toc = $toc -replace "=================", "=================`n" $toc = $toc -replace "Table of Contents", "Table of Contents`n" $toc = $toc -replace "\)", "`)`n" $readme = Get-Content ../skynet-iads-source/README_source.md $readmeWithTOC = $readme -replace "{TOC_PLACEHOLDER}", $toc if (Test-Path ../README.md) { Remove-Item ../README.md } Add-Content ../README.md $readmeWithTOC Remove-Item ./tmp/ ================================================ FILE: contributing.md ================================================ This guide is work in progress and will be updated. # Contributing Thanks for your interest in contributing to Skynet! If you think you have a good idea on how to improve or enhance Skynet, please propose your idea on the [discord channel](https://discord.gg/ZEyp3g). It's encuraged that you run your idea by the community before you spend time coding. This way you will get feedback on how the feature is perceived by the community and you also may get tips on how to best implement an enhancement. # Versioning Skynet uses [semantic versioning](https://semver.org/). # Required software You will need a working copy of DCS [([Digital Combat Simulator)](https://www.digitalcombatsimulator.com/en/) to contribute to Skynet development. # Test first design philosophy Skynet is developed with the [test first philosophy](https://resources.collab.net/agile-101/test-first-programming). Once you get the hang of it test first development is really great. It may take a bit longer to develop a new feature but you will save a lot of time not having to test existing code after a small change. Writing unit tests also makes the code more modular and therefore understandable. ## Writing a unit test Have a look at the existing unit tests to get an idea on how to write one yourself. Unit tests shall be added to the skynet-unit-tests.miz file. Check the output of the dcs.log file for information whether the tests have passed or not. Please don't create a pull request with tests failing. # setting up your editor I recomend you use [notepad++](https://notepad-plus-plus.org/downloads/) to edit lua files. This [Post](https://community.notepad-plus-plus.org/topic/15662/help-function-list-doesn-t-support-my-language/12) has some information on how you can add a function list for LUA in notepad++ # Build workflow All the .miz files in this respository load the skynet-iads-compiled.lua file including the unit test mission. To create a new build run build-compiled-script.ps1 in the Power Shell on Windows. After that you can load the skynet-iads-compiled.lua in to the .miz file of your choosing. Rund build-compiled-script.ps1 1.0.0 to add the version number in the script. ## Editing the readme file The README.md in the root of this repository is autogenerated. Don't add new documentation in this file. Instead edit README_source.md. When running build-compiled-script.ps1 the README.md is updated with a table of contents. ================================================ FILE: demo-missions/mist_4_5_107.lua ================================================ --[[-- MIST Mission Scripting Tools. ## Description: MIssion Scripting Tools (MIST) is a collection of Lua functions and databases that is intended to be a supplement to the standard Lua functions included in the simulator scripting engine. MIST functions and databases provide ready-made solutions to many common scripting tasks and challenges, enabling easier scripting and saving mission scripters time. The table mist.flagFuncs contains a set of Lua functions (that are similar to Slmod functions) that do not require detailed Lua knowledge to use. However, the majority of MIST does require knowledge of the Lua language, and, if you are going to utilize these components of MIST, it is necessary that you read the Simulator Scripting Engine guide on the official ED wiki. ## Links: ED Forum Thread: ##Github: Development Official Releases @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]" - subtract this unit if its in the table "[g]" - add this group to the table "[-g]" - subtract this group from the table "[c]" - add this country's units "[-c]" - subtract this country's units if any are in the table Stand-alone identifiers "[all]" - add all units "[-all]" - subtract all units (not very useful by itself) "[blue]" - add all blue units "[-blue]" - subtract all blue units "[red]" - add all red coalition units "[-red]" - subtract all red units Compound Identifiers: "[c][helicopter]" - add all of this country's helicopters "[-c][helicopter]" - subtract all of this country's helicopters "[c][plane]" - add all of this country's planes "[-c][plane]" - subtract all of this country's planes "[c][ship]" - add all of this country's ships "[-c][ship]" - subtract all of this country's ships "[c][vehicle]" - add all of this country's vehicles "[-c][vehicle]" - subtract all of this country's vehicles "[all][helicopter]" - add all helicopters "[-all][helicopter]" - subtract all helicopters "[all][plane]" - add all planes "[-all][plane]" - subtract all planes "[all][ship]" - add all ships "[-all][ship]" - subtract all ships "[all][vehicle]" - add all vehicles "[-all][vehicle]" - subtract all vehicles "[blue][helicopter]" - add all blue coalition helicopters "[-blue][helicopter]" - subtract all blue coalition helicopters "[blue][plane]" - add all blue coalition planes "[-blue][plane]" - subtract all blue coalition planes "[blue][ship]" - add all blue coalition ships "[-blue][ship]" - subtract all blue coalition ships "[blue][vehicle]" - add all blue coalition vehicles "[-blue][vehicle]" - subtract all blue coalition vehicles "[red][helicopter]" - add all red coalition helicopters "[-red][helicopter]" - subtract all red coalition helicopters "[red][plane]" - add all red coalition planes "[-red][plane]" - subtract all red coalition planes "[red][ship]" - add all red coalition ships "[-red][ship]" - subtract all red coalition ships "[red][vehicle]" - add all red coalition vehicles "[-red][vehicle]" - subtract all red coalition vehicles Country names to be used in [c] and [-c] short-cuts: Turkey Norway The Netherlands Spain 11 UK Denmark USA Georgia Germany Belgium Canada France Israel Ukraine Russia South Ossetia Abkhazia Italy Australia Austria Belarus Bulgaria Czech Republic China Croatia Finland Greece Hungary India Iran Iraq Japan Kazakhstan North Korea Pakistan Poland Romania Saudi Arabia Serbia, Slovakia South Korea Sweden Switzerland Syria USAF Aggressors Do NOT use a '[u]' notation for single units. Single units are referenced the same way as before: Simply input their names as strings. These unit tables are evaluated in order, and you cannot subtract a unit from a table before it is added. For example: {'[blue]', '[-c]Georgia'} will evaluate to all of blue coalition except those units owned by the country named "Georgia"; however: {'[-c]Georgia', '[blue]'} will evaluate to all of the units in blue coalition, because the addition of all units owned by blue coalition occurred AFTER the subtraction of all units owned by Georgia (which actually subtracted nothing at all, since there were no units in the table when the subtraction occurred). More examples: {'[blue][plane]', '[-c]Georgia', '[-g]Hawg 1'} Evaluates to all blue planes, except those blue units owned by the country named "Georgia" and the units in the group named "Hawg1". {'[g]arty1', '[g]arty2', '[-u]arty1_AD', '[-u]arty2_AD', 'Shark 11' } Evaluates to the unit named "Shark 11", plus all the units in groups named "arty1" and "arty2" except those that are named "arty1\_AD" and "arty2\_AD". @table UnitNameTable ]] --- Returns a table containing unit names. -- @tparam table tbl sequential strings -- @treturn table @{UnitNameTable} function mist.makeUnitTable(tbl, 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 == "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 local payloads = {} for unitIndex, unitData in pairs(group_data.units) do --group index payloads[unitIndex] = unitData.payload end return payloads end end end end end end end end end else log:error('Need string or number. Got: $1', type(groupIdent)) return false end log:warn("Couldn't find payload for group: $1", groupIdent) return end function mist.getGroupTable(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 == "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 return group_data end end end end end end end end end else log:error('Need string or number. Got: $1', type(groupIdent)) return false end log:warn("Couldn't find table for group: $1", groupIdent) end function mist.getValidRandomPoint(vars) end function mist.teleportToPoint(vars) -- main teleport function that all of teleport/respawn functions call --log:warn(vars) local point = vars.point local gpName if vars.gpName then gpName = vars.gpName elseif vars.groupName then gpName = vars.groupName else log:error('Missing field groupName or gpName in variable table') end --[[New vars to add, mostly for when called via inZone functions anyTerrain offsetWP1 offsetRoute initTasks ]] local action = vars.action local disperse = vars.disperse or false local maxDisp = vars.maxDisp or 200 local radius = vars.radius or 0 local innerRadius = vars.innerRadius local dbData = false local newGroupData if gpName and not vars.groupData then if string.lower(action) == 'teleport' or string.lower(action) == 'tele' then newGroupData = mist.getCurrentGroupData(gpName) elseif string.lower(action) == 'respawn' then newGroupData = mist.getGroupData(gpName) dbData = true elseif string.lower(action) == 'clone' then newGroupData = mist.getGroupData(gpName) newGroupData.clone = 'order66' dbData = true else action = 'tele' newGroupData = mist.getCurrentGroupData(gpName) end else action = 'tele' newGroupData = vars.groupData end if vars.newGroupName then newGroupData.groupName = vars.newGroupName end if #newGroupData.units == 0 then log:warn('$1 has no units in group table', gpName) return end --log:info('get Randomized Point') local diff = {x = 0, y = 0} local newCoord, origCoord local validTerrain = {'LAND', 'ROAD', 'SHALLOW_WATER', 'WATER', 'RUNWAY'} if vars.anyTerrain then -- do nothing elseif vars.validTerrain then validTerrain = vars.validTerrain else if string.lower(newGroupData.category) == 'ship' then validTerrain = {'SHALLOW_WATER' , 'WATER'} elseif string.lower(newGroupData.category) == 'vehicle' then validTerrain = {'LAND', 'ROAD'} end end if point and radius >= 0 then local valid = false -- new thoughts --[[ Get AVG position of group and max radius distance to that avg point, otherwise use disperse data to get zone area to check if disperse then else end -- ]] ---- old for i = 1, 100 do newCoord = mist.getRandPointInCircle(point, radius, innerRadius) if vars.anyTerrain or mist.isTerrainValid(newCoord, validTerrain) then origCoord = mist.utils.deepCopy(newCoord) diff = {x = (newCoord.x - newGroupData.units[1].x), y = (newCoord.y - newGroupData.units[1].y)} valid = true break end end if valid == false then log:error('Point supplied in variable table is not a valid coordinate. Valid coords: $1', validTerrain) return false end end if not newGroupData.country and mist.DBs.groupsByName[newGroupData.groupName].country then newGroupData.country = mist.DBs.groupsByName[newGroupData.groupName].country end if not newGroupData.category and mist.DBs.groupsByName[newGroupData.groupName].category then newGroupData.category = mist.DBs.groupsByName[newGroupData.groupName].category end --log:info(point) for unitNum, unitData in pairs(newGroupData.units) do --log:info(unitNum) if disperse then local unitCoord if maxDisp and type(maxDisp) == 'number' and unitNum ~= 1 then for i = 1, 100 do unitCoord = mist.getRandPointInCircle(origCoord, maxDisp) if mist.isTerrainValid(unitCoord, validTerrain) == true then --log:warn('Index: $1, Itered: $2. AT: $3', unitNum, i, unitCoord) break end end --else --newCoord = mist.getRandPointInCircle(zone.point, zone.radius) end if unitNum == 1 then unitCoord = mist.utils.deepCopy(newCoord) end if unitCoord then newGroupData.units[unitNum].x = unitCoord.x newGroupData.units[unitNum].y = unitCoord.y end else newGroupData.units[unitNum].x = unitData.x + diff.x newGroupData.units[unitNum].y = unitData.y + diff.y end if point then if (newGroupData.category == 'plane' or newGroupData.category == 'helicopter') then if point.z and point.y > 0 and point.y > land.getHeight({newGroupData.units[unitNum].x, newGroupData.units[unitNum].y}) + 10 then newGroupData.units[unitNum].alt = point.y --log:info('far enough from ground') else if newGroupData.category == 'plane' then --log:info('setNewAlt') newGroupData.units[unitNum].alt = land.getHeight({newGroupData.units[unitNum].x, newGroupData.units[unitNum].y}) + math.random(300, 9000) else newGroupData.units[unitNum].alt = land.getHeight({newGroupData.units[unitNum].x, newGroupData.units[unitNum].y}) + math.random(200, 3000) end end end end end if newGroupData.start_time then newGroupData.startTime = newGroupData.start_time end if newGroupData.startTime and newGroupData.startTime ~= 0 and dbData == true then local timeDif = timer.getAbsTime() - timer.getTime0() if timeDif > newGroupData.startTime then newGroupData.startTime = 0 else newGroupData.startTime = newGroupData.startTime - timeDif end end local tempRoute if mist.DBs.MEgroupsByName[gpName] and not vars.route then -- log:warn('getRoute') tempRoute = mist.getGroupRoute(gpName, true) elseif vars.route then -- log:warn('routeExist') tempRoute = mist.utils.deepCopy(vars.route) end -- log:warn(tempRoute) if tempRoute then if (vars.offsetRoute or vars.offsetWP1 or vars.initTasks) then for i = 1, #tempRoute do -- log:warn(i) if (vars.offsetRoute) or (i == 1 and vars.offsetWP1) or (i == 1 and vars.initTasks) then -- log:warn('update offset') tempRoute[i].x = tempRoute[i].x + diff.x tempRoute[i].y = tempRoute[i].y + diff.y elseif vars.initTasks and i > 1 then --log:warn('deleteWP') tempRoute[i] = nil end end end newGroupData.route = tempRoute end --log:warn(newGroupData) --mist.debug.writeData(mist.utils.serialize,{'teleportToPoint', newGroupData}, 'newGroupData.lua') if string.lower(newGroupData.category) == 'static' then --log:warn(newGroupData) return mist.dynAddStatic(newGroupData) end return mist.dynAdd(newGroupData) end function mist.respawnInZone(gpName, zone, disperse, maxDisp, v) if type(gpName) == 'table' and gpName:getName() then gpName = gpName:getName() elseif type(gpName) == 'table' and gpName[1]:getName() then gpName = math.random(#gpName) else gpName = tostring(gpName) end if type(zone) == 'string' then zone = mist.DBs.zonesByName[zone] elseif type(zone) == 'table' and not zone.radius then zone = mist.DBs.zonesByName[zone[math.random(1, #zone)]] end local vars = {} vars.gpName = gpName vars.action = 'respawn' vars.point = zone.point vars.radius = zone.radius vars.disperse = disperse vars.maxDisp = maxDisp if v and type(v) == 'table' then for index, val in pairs(v) do vars[index] = val end end return mist.teleportToPoint(vars) end function mist.cloneInZone(gpName, zone, disperse, maxDisp, v) --log:info('cloneInZone') if type(gpName) == 'table' then gpName = gpName:getName() else gpName = tostring(gpName) end if type(zone) == 'string' then zone = mist.DBs.zonesByName[zone] elseif type(zone) == 'table' and not zone.radius then zone = mist.DBs.zonesByName[zone[math.random(1, #zone)]] end local vars = {} vars.gpName = gpName vars.action = 'clone' vars.point = zone.point vars.radius = zone.radius vars.disperse = disperse vars.maxDisp = maxDisp --log:info('do teleport') if v and type(v) == 'table' then for index, val in pairs(v) do vars[index] = val end end return mist.teleportToPoint(vars) end function mist.teleportInZone(gpName, zone, disperse, maxDisp, v) -- groupName, zoneName or table of Zone Names, keepForm is a boolean if type(gpName) == 'table' and gpName:getName() then gpName = gpName:getName() else gpName = tostring(gpName) end if type(zone) == 'string' then zone = mist.DBs.zonesByName[zone] elseif type(zone) == 'table' and not zone.radius then zone = mist.DBs.zonesByName[zone[math.random(1, #zone)]] end local vars = {} vars.gpName = gpName vars.action = 'tele' vars.point = zone.point vars.radius = zone.radius vars.disperse = disperse vars.maxDisp = maxDisp if v and type(v) == 'table' then for index, val in pairs(v) do vars[index] = val end end return mist.teleportToPoint(vars) end function mist.respawnGroup(gpName, task) local vars = {} vars.gpName = gpName vars.action = 'respawn' if task and type(task) ~= 'number' then vars.route = mist.getGroupRoute(gpName, 'task') end local newGroup = mist.teleportToPoint(vars) if task and type(task) == 'number' then local newRoute = mist.getGroupRoute(gpName, 'task') mist.scheduleFunction(mist.goRoute, {newGroup, newRoute}, timer.getTime() + task) end return newGroup end function mist.cloneGroup(gpName, task) local vars = {} vars.gpName = gpName vars.action = 'clone' if task and type(task) ~= 'number' then vars.route = mist.getGroupRoute(gpName, 'task') end local newGroup = mist.teleportToPoint(vars) if task and type(task) == 'number' then local newRoute = mist.getGroupRoute(gpName, 'task') mist.scheduleFunction(mist.goRoute, {newGroup, newRoute}, timer.getTime() + task) end return newGroup end function mist.teleportGroup(gpName, task) local vars = {} vars.gpName = gpName vars.action = 'teleport' if task and type(task) ~= 'number' then vars.route = mist.getGroupRoute(gpName, 'task') end local newGroup = mist.teleportToPoint(vars) if task and type(task) == 'number' then local newRoute = mist.getGroupRoute(gpName, 'task') mist.scheduleFunction(mist.goRoute, {newGroup, newRoute}, timer.getTime() + task) end return newGroup end function mist.spawnRandomizedGroup(groupName, vars) -- need to debug if Group.getByName(groupName) and Group.getByName(groupName):isExist() == true then local gpData = mist.getGroupData(groupName) gpData.units = mist.randomizeGroupOrder(gpData.units, vars) gpData.route = mist.getGroupRoute(groupName, 'task') mist.dynAdd(gpData) end return true end function mist.randomizeNumTable(vars) local newTable = {} local excludeIndex = {} local randomTable = {} if vars and vars.exclude and type(vars.exclude) == 'table' then for index, data in pairs(vars.exclude) do excludeIndex[data] = true end end local low, hi, size if vars.size then size = vars.size end if vars and vars.lowerLimit and type(vars.lowerLimit) == 'number' then low = mist.utils.round(vars.lowerLimit) else low = 1 end if vars and vars.upperLimit and type(vars.upperLimit) == 'number' then hi = mist.utils.round(vars.upperLimit) else hi = size end local choices = {} -- add to exclude list and create list of what to randomize for i = 1, size do if not (i >= low and i <= hi) then excludeIndex[i] = true end if not excludeIndex[i] then table.insert(choices, i) else newTable[i] = i end end for ind, num in pairs(choices) do local found = false local x = 0 while found == false do x = mist.random(size) -- get random number from list local addNew = true for index, _ in pairs(excludeIndex) do if index == x then addNew = false break end end if addNew == true then excludeIndex[x] = true found = true end excludeIndex[x] = true end newTable[num] = x end --[[ for i = 1, #newTable do log:info(newTable[i]) end ]] return newTable end function mist.randomizeGroupOrder(passedUnits, vars) -- figure out what to exclude, and send data to other func local units = passedUnits if passedUnits.units then units = passUnits.units end local exclude = {} local excludeNum = {} if vars and vars.excludeType and type(vars.excludeType) == 'table' then exclude = vars.excludeType end if vars and vars.excludeNum and type(vars.excludeNum) == 'table' then excludeNum = vars.excludeNum end local low, hi if vars and vars.lowerLimit and type(vars.lowerLimit) == 'number' then low = mist.utils.round(vars.lowerLimit) else low = 1 end if vars and vars.upperLimit and type(vars.upperLimit) == 'number' then hi = mist.utils.round(vars.upperLimit) else hi = #units end local excludeNum = {} for unitIndex, unitData in pairs(units) do if unitIndex >= low and unitIndex <= hi then -- if within range local found = false if #exclude > 0 then for excludeType, index in pairs(exclude) do -- check if excluded if mist.stringMatch(excludeType, unitData.type) then -- if excluded excludeNum[unitIndex] = unitIndex found = true end end end else -- unitIndex is either to low, or to high: added to exclude list excludeNum[unitIndex] = unitId end end local newGroup = {} local newOrder = mist.randomizeNumTable({exclude = excludeNum, size = #units}) for unitIndex, unitData in pairs(units) do for i = 1, #newOrder do if newOrder[i] == unitIndex then newGroup[i] = mist.utils.deepCopy(units[i]) -- gets all of the unit data newGroup[i].type = mist.utils.deepCopy(unitData.type) newGroup[i].skill = mist.utils.deepCopy(unitData.skill) newGroup[i].unitName = mist.utils.deepCopy(unitData.unitName) newGroup[i].unitIndex = mist.utils.deepCopy(unitData.unitIndex) -- replaces the units data with a new type end end end return newGroup end function mist.random(firstNum, secondNum) -- no support for decimals local lowNum, highNum if not secondNum then highNum = firstNum lowNum = 1 else lowNum = firstNum highNum = secondNum end local total = 1 if math.abs(highNum - lowNum + 1) < 50 then -- if total values is less than 50 total = math.modf(50/math.abs(highNum - lowNum + 1)) -- make x copies required to be above 50 end local choices = {} for i = 1, total do -- iterate required number of times for x = lowNum, highNum do -- iterate between the range choices[#choices +1] = x -- add each entry to a table end end local rtnVal = math.random(#choices) -- will now do a math.random of at least 50 choices for i = 1, 10 do rtnVal = math.random(#choices) -- iterate a few times for giggles end return choices[rtnVal] end function mist.stringCondense(s) local exclude = {'%-', '%(', '%)', '%_', '%[', '%]', '%.', '%#', '% ', '%{', '%}', '%$', '%%', '%?', '%+', '%^'} for i , str in pairs(exclude) do s = string.gsub(s, str, '') end return s end function mist.stringMatch(s1, s2, bool) if type(s1) == 'string' and type(s2) == 'string' then s1 = mist.stringCondense(s1) s2 = mist.stringCondense(s2) if not bool then s1 = string.lower(s1) s2 = string.lower(s2) end --log:info('Comparing: $1 and $2', s1, s2) if s1 == s2 then return true else return false end else log:error('Either the first or second variable were not a string') return false end end mist.matchString = mist.stringMatch -- both commands work because order out type of I --[[ scope: { units = {...}, -- unit names. coa = {...}, -- coa names countries = {...}, -- country names CA = {...}, -- looks just like coa. unitTypes = { red = {}, blue = {}, all = {}, Russia = {},} } scope examples: { units = { 'Hawg11', 'Hawg12' }, CA = {'blue'} } { countries = {'Georgia'}, unitTypes = {blue = {'A-10C', 'A-10A'}}} { coa = {'all'}} {unitTypes = { blue = {'A-10C'}}} ]] end --- Utility functions. -- E.g. conversions between units etc. -- @section mist.utils do -- mist.util scope mist.utils = {} --- Converts angle in radians to degrees. -- @param angle angle in radians -- @return angle in degrees function mist.utils.toDegree(angle) return angle*180/math.pi end --- Converts angle in degrees to radians. -- @param angle angle in degrees -- @return angle in degrees function mist.utils.toRadian(angle) return angle*math.pi/180 end --- Converts meters to nautical miles. -- @param meters distance in meters -- @return distance in nautical miles function mist.utils.metersToNM(meters) return meters/1852 end --- Converts meters to feet. -- @param meters distance in meters -- @return distance in feet function mist.utils.metersToFeet(meters) return meters/0.3048 end --- Converts nautical miles to meters. -- @param nm distance in nautical miles -- @return distance in meters function mist.utils.NMToMeters(nm) return nm*1852 end --- Converts feet to meters. -- @param feet distance in feet -- @return distance in meters function mist.utils.feetToMeters(feet) return feet*0.3048 end --- Converts meters per second to knots. -- @param mps speed in m/s -- @return speed in knots function mist.utils.mpsToKnots(mps) return mps*3600/1852 end --- Converts meters per second to kilometers per hour. -- @param mps speed in m/s -- @return speed in km/h function mist.utils.mpsToKmph(mps) return mps*3.6 end --- Converts knots to meters per second. -- @param knots speed in knots -- @return speed in m/s function mist.utils.knotsToMps(knots) return knots*1852/3600 end --- Converts kilometers per hour to meters per second. -- @param kmph speed in km/h -- @return speed in m/s function mist.utils.kmphToMps(kmph) return kmph/3.6 end function mist.utils.kelvinToCelsius(t) return t - 273.15 end function mist.utils.FahrenheitToCelsius(f) return (f - 32) * (5/9) end function mist.utils.celsiusToFahrenheit(c) return c*(9/5)+32 end function mist.utils.hexToRGB(hex, l) -- because for some reason the draw tools use hex when everything is rgba 0 - 1 local int = 255 if l then int = 1 end if hex and type(hex) == 'string' then local val = {} hex = string.gsub(hex, '0x', '') if string.len(hex) == 8 then val[1] = tonumber("0x"..hex:sub(1,2)) / int val[2] = tonumber("0x"..hex:sub(3,4)) / int val[3] = tonumber("0x"..hex:sub(5,6)) / int val[4] = tonumber("0x"..hex:sub(7,8)) / int return val end end end function mist.utils.converter(t1, t2, val) if type(t1) == 'string' then t1 = string.lower(t1) end if type(t2) == 'string' then t2 = string.lower(t2) end if val and type(val) ~= 'number' then if tonumber(val) then val = tonumber(val) else log:warn("Value given is not a number: $1", val) return 0 end end -- speed if t1 == 'mps' then if t2 == 'kmph' then return val * 3.6 elseif t2 == 'knots' or t2 == 'knot' then return val * 3600/1852 end elseif t1 == 'kmph' then if t2 == 'mps' then return val/3.6 elseif t2 == 'knots' or t2 == 'knot' then return val*0.539957 end elseif t1 == 'knot' or t1 == 'knots' then if t2 == 'kmph' then return val * 1.852 elseif t2 == 'mps' then return val * 0.514444 end -- Distance elseif t1 == 'feet' or t1 == 'ft' then if t2 == 'nm' then return val/6076.12 elseif t2 == 'km' then return (val*0.3048)/1000 elseif t2 == 'm' then return val*0.3048 end elseif t1 == 'nm' then if t2 == 'feet' or t2 == 'ft' then return val*6076.12 elseif t2 == 'km' then return val*1.852 elseif t2 == 'm' then return val*1852 end elseif t1 == 'km' then if t2 == 'nm' then return val/1.852 elseif t2 == 'feet' or t2 == 'ft' then return (val/0.3048)*1000 elseif t2 == 'm' then return val*1000 end elseif t1 == 'm' then if t2 == 'nm' then return val/1852 elseif t2 == 'km' then return val/1000 elseif t2 == 'feet' or t2 == 'ft' then return val/0.3048 end -- Temperature elseif t1 == 'f' or t1 == 'fahrenheit' then if t2 == 'c' or t2 == 'celsius' then return (val - 32) * (5/9) elseif t2 == 'k' or t2 == 'kelvin' then return (val + 459.67) * (5/9) end elseif t1 == 'c' or t1 == 'celsius' then if t2 == 'f' or t2 == 'fahrenheit' then return val*(9/5)+32 elseif t2 == 'k' or t2 == 'kelvin' then return val + 273.15 end elseif t1 == 'k' or t1 == 'kelvin' then if t2 == 'c' or t2 == 'celsius' then return val - 273.15 elseif t2 == 'f' or t2 == 'fahrenheit' then return ((val*(9/5))-459.67) end -- Pressure elseif t1 == 'p' or t1 == 'pascal' or t1 == 'pascals' then if t2 == 'hpa' or t2 == 'hectopascal' then return val/100 elseif t2 == 'mmhg' then return val * 0.00750061561303 elseif t2 == 'inhg' then return val * 0.0002953 end elseif t1 == 'hpa' or t1 == 'hectopascal' then if t2 == 'p' or t2 == 'pascal' or t2 == 'pascals' then return val*100 elseif t2 == 'mmhg' then return val * 0.00750061561303 elseif t2 == 'inhg' then return val * 0.02953 end elseif t1 == 'mmhg' then if t2 == 'p' or t2 == 'pascal' or t2 == 'pascals' then return val / 0.00750061561303 elseif t2 == 'hpa' or t2 == 'hectopascal' then return val * 1.33322 elseif t2 == 'inhg' then return val/25.4 end elseif t1 == 'inhg' then if t2 == 'p' or t2 == 'pascal' or t2 == 'pascals' then return val*3386.39 elseif t2 == 'mmhg' then return val*25.4 elseif t2 == 'hpa' or t2 == 'hectopascal' then return val * 33.8639 end else log:warn("First value doesn't match with list. Value given: $1", t1) end log:warn("Match not found. Unable to convert: $1 into $2", t1, t2) end mist.converter = mist.utils.converter function mist.utils.getQFE(point, inchHg) local t, p = 0, 0 if atmosphere.getTemperatureAndPressure then t, p = atmosphere.getTemperatureAndPressure(mist.utils.makeVec3GL(point)) end if p == 0 then local h = land.getHeight(mist.utils.makeVec2(point))/0.3048 -- convert to feet if inchHg then return (env.mission.weather.qnh - (h/30)) * 0.0295299830714 else return env.mission.weather.qnh - (h/30) end else if inchHg then return mist.converter('p', 'inhg', p) else return mist.converter('p', 'hpa', p) end end end --- Converts a Vec3 to a Vec2. -- @tparam Vec3 vec the 3D vector -- @return vector converted to Vec2 function mist.utils.makeVec2(vec) if vec.z then return {x = vec.x, y = vec.z} else return {x = vec.x, y = vec.y} -- it was actually already vec2. end end --- Converts a Vec2 to a Vec3. -- @tparam Vec2 vec the 2D vector -- @param y optional new y axis (altitude) value. If omitted it's 0. function mist.utils.makeVec3(vec, y) if not vec.z then if vec.alt and not y then y = vec.alt elseif not y then y = 0 end return {x = vec.x, y = y, z = vec.y} else return {x = vec.x, y = vec.y, z = vec.z} -- it was already Vec3, actually. end end --- Converts a Vec2 to a Vec3 using ground level as altitude. -- The ground level at the specific point is used as altitude (y-axis) -- for the new vector. Optionally a offset can be specified. -- @tparam Vec2 vec the 2D vector -- @param[opt] offset offset to be applied to the ground level -- @return new 3D vector function mist.utils.makeVec3GL(vec, offset) local adj = offset or 0 if not vec.z then return {x = vec.x, y = (land.getHeight(vec) + adj), z = vec.y} else return {x = vec.x, y = (land.getHeight({x = vec.x, y = vec.z}) + adj), z = vec.z} end end --- Returns the center of a zone as Vec3. -- @tparam string|table zone trigger zone name or table -- @treturn Vec3 center of the zone function mist.utils.zoneToVec3(zone, gl) local new = {} if type(zone) == 'table' then if zone.point then new.x = zone.point.x new.y = zone.point.y new.z = zone.point.z elseif zone.x and zone.y and zone.z then new = mist.utils.deepCopy(zone) end return new elseif type(zone) == 'string' then zone = trigger.misc.getZone(zone) if zone then new.x = zone.point.x new.y = zone.point.y new.z = zone.point.z end end if new.x and gl then new.y = land.getHeight({x = new.x, y = new.z}) end return new end function mist.utils.getHeadingPoints(point1, point2, north) -- sick of writing this out. if north then return mist.utils.getDir(mist.vec.sub(mist.utils.makeVec3(point2), mist.utils.makeVec3(point1)), (mist.utils.makeVec3(point1))) else return mist.utils.getDir(mist.vec.sub(mist.utils.makeVec3(point2), mist.utils.makeVec3(point1))) end end --- Returns heading-error corrected direction. -- True-north corrected direction from point along vector vec. -- @tparam Vec3 vec -- @tparam Vec2 point -- @return heading-error corrected direction from point. function mist.utils.getDir(vec, point) local dir = math.atan2(vec.z, vec.x) if point then dir = dir + mist.getNorthCorrection(point) end if dir < 0 then dir = dir + 2 * math.pi -- put dir in range of 0 to 2*pi end return dir end --- Returns distance in meters between two points. -- @tparam Vec2|Vec3 point1 first point -- @tparam Vec2|Vec3 point2 second point -- @treturn number distance between given points. function mist.utils.get2DDist(point1, point2) if not point1 then log:warn("mist.utils.get2DDist 1st input value is nil") end if not point2 then log:warn("mist.utils.get2DDist 2nd input value is nil") end point1 = mist.utils.makeVec3(point1) point2 = mist.utils.makeVec3(point2) return mist.vec.mag({x = point1.x - point2.x, y = 0, z = point1.z - point2.z}) end --- Returns distance in meters between two points in 3D space. -- @tparam Vec3 point1 first point -- @tparam Vec3 point2 second point -- @treturn number distancen between given points in 3D space. function mist.utils.get3DDist(point1, point2) if not point1 then log:warn("mist.utils.get2DDist 1st input value is nil") end if not point2 then log:warn("mist.utils.get2DDist 2nd input value is nil") end return mist.vec.mag({x = point1.x - point2.x, y = point1.y - point2.y, z = point1.z - point2.z}) end --- Creates a waypoint from a vector. -- @tparam Vec2|Vec3 vec position of the new waypoint -- @treturn Waypoint a new waypoint to be used inside paths. function mist.utils.vecToWP(vec) local newWP = {} newWP.x = vec.x newWP.y = vec.y if vec.z then newWP.alt = vec.y newWP.y = vec.z else newWP.alt = land.getHeight({x = vec.x, y = vec.y}) end return newWP end --- Creates a waypoint from a unit. -- This function also considers the units speed. -- The alt_type of this waypoint is set to "BARO". -- @tparam Unit pUnit Unit whose position and speed will be used. -- @treturn Waypoint new waypoint. function mist.utils.unitToWP(pUnit) local unit = mist.utils.deepCopy(pUnit) if type(unit) == 'string' then if Unit.getByName(unit) then unit = Unit.getByName(unit) end end if unit:isExist() == true then local new = mist.utils.vecToWP(unit:getPosition().p) new.speed = mist.vec.mag(unit:getVelocity()) new.alt_type = "BARO" return new end log:error("$1 not found or doesn't exist", pUnit) return false end --- Creates a deep copy of a object. -- Usually this object is a table. -- See also: from http://lua-users.org/wiki/CopyTable -- @param object object to copy -- @return copy of object function mist.utils.deepCopy(object) local lookup_table = {} local function _copy(object) if type(object) ~= "table" then return object elseif lookup_table[object] then return lookup_table[object] end local new_table = {} lookup_table[object] = new_table for index, value in pairs(object) do new_table[_copy(index)] = _copy(value) end return setmetatable(new_table, getmetatable(object)) end return _copy(object) end --- Simple rounding function. -- From http://lua-users.org/wiki/SimpleRound -- use negative idp for rounding ahead of decimal place, positive for rounding after decimal place -- @tparam number num number to round -- @param idp function mist.utils.round(num, idp) local mult = 10^(idp or 0) return math.floor(num * mult + 0.5) / mult end --- Rounds all numbers inside a table. -- @tparam table tbl table in which to round numbers -- @param idp function mist.utils.roundTbl(tbl, idp) for id, val in pairs(tbl) do if type(val) == 'number' then tbl[id] = mist.utils.round(val, idp) end end return tbl end --- Executes the given string. -- borrowed from Slmod -- @tparam string s string containing LUA code. -- @treturn boolean true if successfully executed, false otherwise function mist.utils.dostring(s) local f, err = loadstring(s) if f then return true, f() else return false, err end end --- Checks a table's types. -- This function checks a tables types against a specifically forged type table. -- @param fname -- @tparam table type_tbl -- @tparam table var_tbl -- @usage -- specifically forged type table -- type_tbl = { -- {'table', 'number'}, -- 'string', -- 'number', -- 'number', -- {'string','nil'}, -- {'number', 'nil'} -- } -- -- my_tbl index 1 must be a table or a number; -- -- index 2, a string; index 3, a number; -- -- index 4, a number; index 5, either a string or nil; -- -- and index 6, either a number or nil. -- mist.utils.typeCheck(type_tbl, my_tb) -- @return true if table passes the check, false otherwise. function mist.utils.typeCheck(fname, type_tbl, var_tbl) -- log:info('type check') for type_key, type_val in pairs(type_tbl) do -- log:info('type_key: $1 type_val: $2', type_key, type_val) --type_key can be a table of accepted keys- so try to find one that is not nil local type_key_str = '' local act_key = type_key -- actual key within var_tbl - necessary to use for multiple possible key variables. Initialize to type_key if type(type_key) == 'table' then for i = 1, #type_key do if i ~= 1 then type_key_str = type_key_str .. '/' end type_key_str = type_key_str .. tostring(type_key[i]) if var_tbl[type_key[i]] ~= nil then act_key = type_key[i] -- found a non-nil entry, make act_key now this val. end end else type_key_str = tostring(type_key) end local err_msg = 'Error in function ' .. fname .. ', parameter "' .. type_key_str .. '", expected: ' local passed_check = false if type(type_tbl[type_key]) == 'table' then -- log:info('err_msg, before: $1', err_msg) for j = 1, #type_tbl[type_key] do if j == 1 then err_msg = err_msg .. type_tbl[type_key][j] else err_msg = err_msg .. ' or ' .. type_tbl[type_key][j] end if type(var_tbl[act_key]) == type_tbl[type_key][j] then passed_check = true end end -- log:info('err_msg, after: $1', err_msg) else -- log:info('err_msg, before: $1', err_msg) err_msg = err_msg .. type_tbl[type_key] -- log:info('err_msg, after: $1', err_msg) if type(var_tbl[act_key]) == type_tbl[type_key] then passed_check = true end end if not passed_check then err_msg = err_msg .. ', got ' .. type(var_tbl[act_key]) return false, err_msg end end return true end --- Serializes the give variable to a string. -- borrowed from slmod -- @param var variable to serialize -- @treturn string variable serialized to string function mist.utils.basicSerialize(var) if var == nil then return "\"\"" else if ((type(var) == 'number') or (type(var) == 'boolean') or (type(var) == 'function') or (type(var) == 'table') or (type(var) == 'userdata') ) then return tostring(var) elseif type(var) == 'string' then var = string.format('%q', var) return var end end end --- Serialize value -- borrowed from slmod (serialize_slmod) -- @param name -- @param value value to serialize -- @param level function mist.utils.serialize(name, value, level) --Based on ED's serialize_simple2 local function basicSerialize(o) if type(o) == "number" then return tostring(o) elseif type(o) == "boolean" then return tostring(o) else -- assume it is a string return mist.utils.basicSerialize(o) end end local function serializeToTbl(name, value, level) local var_str_tbl = {} if level == nil then level = "" end if level ~= "" then level = level.."" end table.insert(var_str_tbl, level .. name .. " = ") if type(value) == "number" or type(value) == "string" or type(value) == "boolean" then table.insert(var_str_tbl, basicSerialize(value) .. ",\n") elseif type(value) == "table" then table.insert(var_str_tbl, "\n"..level.."{\n") for k,v in pairs(value) do -- serialize its fields local key if type(k) == "number" then key = string.format("[%s]", k) else key = string.format("[%q]", k) end table.insert(var_str_tbl, mist.utils.serialize(key, v, level.." ")) end if level == "" then table.insert(var_str_tbl, level.."} -- end of "..name.."\n") else table.insert(var_str_tbl, level.."}, -- end of "..name.."\n") end else log:error('Cannot serialize a $1', type(value)) end return var_str_tbl end local t_str = serializeToTbl(name, value, level) return table.concat(t_str) end --- Serialize value supporting cycles. -- borrowed from slmod (serialize_wcycles) -- @param name -- @param value value to serialize -- @param saved function mist.utils.serializeWithCycles(name, value, saved) --mostly straight out of Programming in Lua local function basicSerialize(o) if type(o) == "number" then return tostring(o) elseif type(o) == "boolean" then return tostring(o) else -- assume it is a string return mist.utils.basicSerialize(o) end end local t_str = {} saved = saved or {} -- initial value if ((type(value) == 'string') or (type(value) == 'number') or (type(value) == 'table') or (type(value) == 'boolean')) then table.insert(t_str, name .. " = ") if type(value) == "number" or type(value) == "string" or type(value) == "boolean" then table.insert(t_str, basicSerialize(value) .. "\n") else if saved[value] then -- value already saved? table.insert(t_str, saved[value] .. "\n") else saved[value] = name -- save name for next time table.insert(t_str, "{}\n") for k,v in pairs(value) do -- save its fields local fieldname = string.format("%s[%s]", name, basicSerialize(k)) table.insert(t_str, mist.utils.serializeWithCycles(fieldname, v, saved)) end end end return table.concat(t_str) else return "" end end --- Serialize a table to a single line string. -- serialization of a table all on a single line, no comments, made to replace old get_table_string function -- borrowed from slmod -- @tparam table tbl table to serialize. -- @treturn string string containing serialized table function mist.utils.oneLineSerialize(tbl) if type(tbl) == 'table' then --function only works for tables! local tbl_str = {} tbl_str[#tbl_str + 1] = '{ ' for ind,val in pairs(tbl) do -- serialize its fields if type(ind) == "number" then tbl_str[#tbl_str + 1] = '[' tbl_str[#tbl_str + 1] = tostring(ind) tbl_str[#tbl_str + 1] = '] = ' else --must be a string tbl_str[#tbl_str + 1] = '[' tbl_str[#tbl_str + 1] = mist.utils.basicSerialize(ind) tbl_str[#tbl_str + 1] = '] = ' end if ((type(val) == 'number') or (type(val) == 'boolean')) then tbl_str[#tbl_str + 1] = tostring(val) tbl_str[#tbl_str + 1] = ', ' elseif type(val) == 'string' then tbl_str[#tbl_str + 1] = mist.utils.basicSerialize(val) tbl_str[#tbl_str + 1] = ', ' elseif type(val) == 'nil' then -- won't ever happen, right? tbl_str[#tbl_str + 1] = 'nil, ' elseif type(val) == 'table' then tbl_str[#tbl_str + 1] = mist.utils.oneLineSerialize(val) tbl_str[#tbl_str + 1] = ', ' --I think this is right, I just added it else log:warn('Unable to serialize value type $1 at index $2', mist.utils.basicSerialize(type(val)), tostring(ind)) end end tbl_str[#tbl_str + 1] = '}' return table.concat(tbl_str) else return mist.utils.basicSerialize(tbl) end end --- Returns table in a easy readable string representation. -- this function is not meant for serialization because it uses -- newlines for better readability. -- @param tbl table to show -- @param loc -- @param indent -- @param tableshow_tbls -- @return human readable string representation of given table function mist.utils.tableShow(tbl, loc, indent, tableshow_tbls) --based on serialize_slmod, this is a _G serialization tableshow_tbls = tableshow_tbls or {} --create table of tables loc = loc or "" indent = indent or "" if type(tbl) == 'table' then --function only works for tables! tableshow_tbls[tbl] = loc local tbl_str = {} tbl_str[#tbl_str + 1] = indent .. '{\n' for ind,val in pairs(tbl) do -- serialize its fields if type(ind) == "number" then tbl_str[#tbl_str + 1] = indent tbl_str[#tbl_str + 1] = loc .. '[' tbl_str[#tbl_str + 1] = tostring(ind) tbl_str[#tbl_str + 1] = '] = ' else tbl_str[#tbl_str + 1] = indent tbl_str[#tbl_str + 1] = loc .. '[' tbl_str[#tbl_str + 1] = mist.utils.basicSerialize(ind) tbl_str[#tbl_str + 1] = '] = ' end if ((type(val) == 'number') or (type(val) == 'boolean')) then tbl_str[#tbl_str + 1] = tostring(val) tbl_str[#tbl_str + 1] = ',\n' elseif type(val) == 'string' then tbl_str[#tbl_str + 1] = mist.utils.basicSerialize(val) tbl_str[#tbl_str + 1] = ',\n' elseif type(val) == 'nil' then -- won't ever happen, right? tbl_str[#tbl_str + 1] = 'nil,\n' elseif type(val) == 'table' then if tableshow_tbls[val] then tbl_str[#tbl_str + 1] = tostring(val) .. ' already defined: ' .. tableshow_tbls[val] .. ',\n' else tableshow_tbls[val] = loc .. '[' .. mist.utils.basicSerialize(ind) .. ']' tbl_str[#tbl_str + 1] = tostring(val) .. ' ' tbl_str[#tbl_str + 1] = mist.utils.tableShow(val, loc .. '[' .. mist.utils.basicSerialize(ind).. ']', indent .. ' ', tableshow_tbls) tbl_str[#tbl_str + 1] = ',\n' end elseif type(val) == 'function' then if debug and debug.getinfo then local fcnname = tostring(val) local info = debug.getinfo(val, "S") if info.what == "C" then tbl_str[#tbl_str + 1] = string.format('%q', fcnname .. ', C function') .. ',\n' else if (string.sub(info.source, 1, 2) == [[./]]) then tbl_str[#tbl_str + 1] = string.format('%q', fcnname .. ', defined in (' .. info.linedefined .. '-' .. info.lastlinedefined .. ')' .. info.source) ..',\n' else tbl_str[#tbl_str + 1] = string.format('%q', fcnname .. ', defined in (' .. info.linedefined .. '-' .. info.lastlinedefined .. ')') ..',\n' end end else tbl_str[#tbl_str + 1] = 'a function,\n' end else tbl_str[#tbl_str + 1] = 'unable to serialize value type ' .. mist.utils.basicSerialize(type(val)) .. ' at index ' .. tostring(ind) end end tbl_str[#tbl_str + 1] = indent .. '}' return table.concat(tbl_str) end end end --- Debug functions -- @section mist.debug do -- mist.debug scope mist.debug = {} function mist.debug.changeSetting(s) if type(s) == 'table' then for sName, sVal in pairs(s) do if type(sVal) == 'string' or type(sVal) == 'number' then if sName == 'log' then mistSettings[sName] = sVal mist.log:setLevel(sVal) elseif sName == 'dbLog' then mistSettings[sName] = sVal dblog:setLevel(sVal) end else mistSettings[sName] = sVal end end end end --- Dumps the global table _G. -- This dumps the global table _G to a file in -- the DCS\Logs directory. -- This function requires you to disable script sanitization -- in $DCS_ROOT\Scripts\MissionScripting.lua to access lfs and io -- libraries. -- @param fname function mist.debug.dump_G(fname, simp) if lfs and io then local fdir = lfs.writedir() .. [[Logs\]] .. fname local f = io.open(fdir, 'w') if simp then local g = mist.utils.deepCopy(_G) g.mist = nil g.slmod = nil g.env.mission = nil g.env.warehouses = nil g.country.by_idx = nil g.country.by_country = nil f:write(mist.utils.tableShow(g)) else f:write(mist.utils.tableShow(_G)) end f:close() log:info('Wrote debug data to $1', fdir) --trigger.action.outText(errmsg, 10) else log:alert('insufficient libraries to run mist.debug.dump_G, you must disable the sanitization of the io and lfs libraries in ./Scripts/MissionScripting.lua') --trigger.action.outText(errmsg, 10) end end --- Write debug data to file. -- This function requires you to disable script sanitization -- in $DCS_ROOT\Scripts\MissionScripting.lua to access lfs and io -- libraries. -- @param fcn -- @param fcnVars -- @param fname function mist.debug.writeData(fcn, fcnVars, fname) if lfs and io then local fdir = lfs.writedir() .. [[Logs\]] .. fname local f = io.open(fdir, 'w') f:write(fcn(unpack(fcnVars, 1, table.maxn(fcnVars)))) f:close() log:info('Wrote debug data to $1', fdir) local errmsg = 'mist.debug.writeData wrote data to ' .. fdir trigger.action.outText(errmsg, 10) else local errmsg = 'Error: insufficient libraries to run mist.debug.writeData, you must disable the sanitization of the io and lfs libraries in ./Scripts/MissionScripting.lua' log:alert(errmsg) trigger.action.outText(errmsg, 10) end end --- Write mist databases to file. -- This function requires you to disable script sanitization -- in $DCS_ROOT\Scripts\MissionScripting.lua to access lfs and io -- libraries. function mist.debug.dumpDBs() for DBname, DB in pairs(mist.DBs) do if type(DB) == 'table' and type(DBname) == 'string' then mist.debug.writeData(mist.utils.serialize, {DBname, DB}, 'mist_DBs_' .. DBname .. '.lua') end end end -- write group table function mist.debug.writeGroup(gName, data) if gName and mist.DBs.groupsByName[gName] then local dat if data then dat = mist.getGroupData(gName) else dat = mist.getGroupTable(gName) end if dat then dat.route = {points = mist.getGroupRoute(gName, true)} end if io and lfs and dat then mist.debug.writeData(mist.utils.serialize, {gName, dat}, gName .. '_table.lua') else if dat then trigger.action.outText('Error: insufficient libraries to run mist.debug.writeGroup, you must disable the sanitization of the io and lfs libraries in ./Scripts/MissionScripting.lua \nGroup table written to DCS.log file instead.', 10) log:warn('$1 dataTable: $2', gName, dat) else trigger.action.outText('Unable to write group table for: ' .. gName .. '\n Error: insufficient libraries to run mist.debug.writeGroup, you must disable the sanitization of the io and lfs libraries in ./Scripts/MissionScripting.lua', 10) end end end end -- write all object types in mission. function mist.debug.writeTypes(fName) local wt = 'mistDebugWriteTypes.lua' if fName and type(fName) == 'string' and string.find(fName, '.lua') then wt = fName end local output = {units = {}, countries = {}} for coa_name_miz, 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 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 output.countries[countryName] = {} 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 not output.countries[countryName][category] then -- log:warn('Create: $1', category) output.countries[countryName][category] = {} 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 --there's a group! 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 for i = 1, #group_data.units do if group_data.units[i] then local u = group_data.units[i] local liv = u.livery_id or 'default' if not output.units[u.type] then -- create unit table -- log:warn('Create: $1', u.type) output.units[u.type] = {count = 0, livery_id = {}} end if not output.countries[countryName][category][u.type] then -- log:warn('Create country, category, unit: $1', u.type) output.countries[countryName][category][u.type] = 0 end -- add to count output.countries[countryName][category][u.type] = output.countries[countryName][category][u.type] + 1 output.units[u.type].count = output.units[u.type].count + 1 if liv and not output.units[u.type].livery_id[countryName] then -- log:warn('Create livery country: $1', countryName) output.units[u.type].livery_id[countryName] = {} end if liv and not output.units[u.type].livery_id[countryName][liv] then --log:warn('Create Livery: $1', liv) output.units[u.type].livery_id[countryName][liv] = 0 end if liv then output.units[u.type].livery_id[countryName][liv] = output.units[u.type].livery_id[countryName][liv] + 1 end if u.payload and u.payload.pylons then if not output.units[u.type].CLSID then output.units[u.type].CLSID = {} output.units[u.type].pylons = {} end for pyIndex, pData in pairs(u.payload.pylons) do if not output.units[u.type].CLSID[pData.CLSID] then output.units[u.type].CLSID[pData.CLSID] = 0 end output.units[u.type].CLSID[pData.CLSID] = output.units[u.type].CLSID[pData.CLSID] + 1 if not output.units[u.type].pylons[pyIndex] then output.units[u.type].pylons[pyIndex] = {} end if not output.units[u.type].pylons[pyIndex][pData.CLSID] then output.units[u.type].pylons[pyIndex][pData.CLSID] = 0 end output.units[u.type].pylons[pyIndex][pData.CLSID] = output.units[u.type].pylons[pyIndex][pData.CLSID] + 1 end end end end end end end end end end end end end end if io and lfs then mist.debug.writeData(mist.utils.serialize, {'mistDebugWriteTypes', output}, wt) else trigger.action.outText('Error: insufficient libraries to run mist.debug.writeTypes, you must disable the sanitization of the io and lfs libraries in ./Scripts/MissionScripting.lua \n writeTypes table written to DCS.log file instead.', 10) log:warn('mist.debug.writeTypes: $1', output) end return output end function mist.debug.writeWeapons(unit) end function mist.debug.mark(msg, coord) mist.marker.add({point = coord, text = msg}) log:warn('debug.mark: $1 $2', msg, coord) end end --- 3D Vector functions -- @section mist.vec do -- mist.vec scope mist.vec = {} --- Vector addition. -- @tparam Vec3 vec1 first vector -- @tparam Vec3 vec2 second vector -- @treturn Vec3 new vector, sum of vec1 and vec2. function mist.vec.add(vec1, vec2) return {x = vec1.x + vec2.x, y = vec1.y + vec2.y, z = vec1.z + vec2.z} end --- Vector substraction. -- @tparam Vec3 vec1 first vector -- @tparam Vec3 vec2 second vector -- @treturn Vec3 new vector, vec2 substracted from vec1. function mist.vec.sub(vec1, vec2) return {x = vec1.x - vec2.x, y = vec1.y - vec2.y, z = vec1.z - vec2.z} end --- Vector scalar multiplication. -- @tparam Vec3 vec vector to multiply -- @tparam number mult scalar multiplicator -- @treturn Vec3 new vector multiplied with the given scalar function mist.vec.scalarMult(vec, mult) return {x = vec.x*mult, y = vec.y*mult, z = vec.z*mult} end mist.vec.scalar_mult = mist.vec.scalarMult --- Vector dot product. -- @tparam Vec3 vec1 first vector -- @tparam Vec3 vec2 second vector -- @treturn number dot product of given vectors function mist.vec.dp (vec1, vec2) return vec1.x*vec2.x + vec1.y*vec2.y + vec1.z*vec2.z end --- Vector cross product. -- @tparam Vec3 vec1 first vector -- @tparam Vec3 vec2 second vector -- @treturn Vec3 new vector, cross product of vec1 and vec2. function mist.vec.cp(vec1, vec2) return { x = vec1.y*vec2.z - vec1.z*vec2.y, y = vec1.z*vec2.x - vec1.x*vec2.z, z = vec1.x*vec2.y - vec1.y*vec2.x} end --- Vector magnitude -- @tparam Vec3 vec vector -- @treturn number magnitude of vector vec function mist.vec.mag(vec) return (vec.x^2 + vec.y^2 + vec.z^2)^0.5 end --- Unit vector -- @tparam Vec3 vec -- @treturn Vec3 unit vector of vec function mist.vec.getUnitVec(vec) local mag = mist.vec.mag(vec) return { x = vec.x/mag, y = vec.y/mag, z = vec.z/mag } end --- Rotate vector. -- @tparam Vec2 vec2 to rotoate -- @tparam number theta -- @return Vec2 rotated vector. function mist.vec.rotateVec2(vec2, theta) return { x = vec2.x*math.cos(theta) - vec2.y*math.sin(theta), y = vec2.x*math.sin(theta) + vec2.y*math.cos(theta)} end function mist.vec.normalize(vec3) local mag = mist.vec.mag(vec3) if mag ~= 0 then return mist.vec.scalar_mult(vec3, 1.0 / mag) end end end --- Flag functions. -- The mist "Flag functions" are functions that are similar to Slmod functions -- that detect a game condition and set a flag when that game condition is met. -- -- They are intended to be used by persons with little or no experience in Lua -- programming, but with a good knowledge of the DCS mission editor. -- @section mist.flagFunc do -- mist.flagFunc scope mist.flagFunc = {} --- Sets a flag if map objects are destroyed inside a zone. -- Once this function is run, it will start a continuously evaluated process -- that will set a flag true if map objects (such as bridges, buildings in -- town, etc.) die (or have died) in a mission editor zone (or set of zones). -- This will only happen once; once the flag is set true, the process ends. -- @usage -- -- Example vars table -- vars = { -- zones = { "zone1", "zone2" }, -- can also be a single string -- flag = 3, -- number of the flag -- stopflag = 4, -- optional number of the stop flag -- req_num = 10, -- optional minimum amount of map objects needed to die -- } -- mist.flagFuncs.mapobjs_dead_zones(vars) -- @tparam table vars table containing parameters. function mist.flagFunc.mapobjs_dead_zones(vars) --[[vars needs to be: zones = table or string, flag = number, stopflag = number or nil, req_num = number or nil AND used by function, initial_number ]] -- type_tbl local type_tbl = { [{'zones', 'zone'}] = {'table', 'string'}, flag = {'number', 'string'}, [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'}, [{'req_num', 'reqnum'}] = {'number', 'nil'}, } local err, errmsg = mist.utils.typeCheck('mist.flagFunc.mapobjs_dead_zones', type_tbl, vars) assert(err, errmsg) local zones = vars.zones or vars.zone local flag = vars.flag local stopflag = vars.stopflag or vars.stopFlag or -1 local req_num = vars.req_num or vars.reqnum or 1 local initial_number = vars.initial_number if type(zones) == 'string' then zones = {zones} end if not initial_number then initial_number = #mist.getDeadMapObjsInZones(zones) end if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then if (#mist.getDeadMapObjsInZones(zones) - initial_number) >= req_num and trigger.misc.getUserFlag(flag) == 0 then trigger.action.setUserFlag(flag, true) return else mist.scheduleFunction(mist.flagFunc.mapobjs_dead_zones, {{zones = zones, flag = flag, stopflag = stopflag, req_num = req_num, initial_number = initial_number}}, timer.getTime() + 1) end end end --- Sets a flag if map objects are destroyed inside a polygon. -- Once this function is run, it will start a continuously evaluated process -- that will set a flag true if map objects (such as bridges, buildings in -- town, etc.) die (or have died) in a polygon. -- This will only happen once; once the flag is set true, the process ends. -- @usage -- -- Example vars table -- vars = { -- zone = { -- [1] = mist.DBs.unitsByName['NE corner'].point, -- [2] = mist.DBs.unitsByName['SE corner'].point, -- [3] = mist.DBs.unitsByName['SW corner'].point, -- [4] = mist.DBs.unitsByName['NW corner'].point -- } -- flag = 3, -- number of the flag -- stopflag = 4, -- optional number of the stop flag -- req_num = 10, -- optional minimum amount of map objects needed to die -- } -- mist.flagFuncs.mapobjs_dead_zones(vars) -- @tparam table vars table containing parameters. function mist.flagFunc.mapobjs_dead_polygon(vars) --[[vars needs to be: zone = table, flag = number, stopflag = number or nil, req_num = number or nil AND used by function, initial_number ]] -- type_tbl local type_tbl = { [{'zone', 'polyzone'}] = 'table', flag = {'number', 'string'}, [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'}, [{'req_num', 'reqnum'}] = {'number', 'nil'}, } local err, errmsg = mist.utils.typeCheck('mist.flagFunc.mapobjs_dead_polygon', type_tbl, vars) assert(err, errmsg) local zone = vars.zone or vars.polyzone local flag = vars.flag local stopflag = vars.stopflag or vars.stopFlag or -1 local req_num = vars.req_num or vars.reqnum or 1 local initial_number = vars.initial_number if not initial_number then initial_number = #mist.getDeadMapObjsInPolygonZone(zone) end if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then if (#mist.getDeadMapObjsInPolygonZone(zone) - initial_number) >= req_num and trigger.misc.getUserFlag(flag) == 0 then trigger.action.setUserFlag(flag, true) return else mist.scheduleFunction(mist.flagFunc.mapobjs_dead_polygon, {{zone = zone, flag = flag, stopflag = stopflag, req_num = req_num, initial_number = initial_number}}, timer.getTime() + 1) end end end --- Sets a flag if unit(s) is/are inside a polygon. -- @tparam table vars @{unitsInPolygonVars} -- @usage -- set flag 11 to true as soon as any blue vehicles -- -- are inside the polygon shape created off of the waypoints -- -- of the group forest1 -- mist.flagFunc.units_in_polygon { -- units = {'[blue][vehicle]'}, -- zone = mist.getGroupPoints('forest1'), -- flag = 11 -- } function mist.flagFunc.units_in_polygon(vars) --[[vars needs to be: units = table, zone = table, flag = number, stopflag = number or nil, maxalt = number or nil, interval = number or nil, req_num = number or nil toggle = boolean or nil unitTableDef = table or nil ]] -- type_tbl local type_tbl = { [{'units', 'unit'}] = 'table', [{'zone', 'polyzone'}] = 'table', flag = {'number', 'string'}, [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'}, [{'maxalt', 'alt'}] = {'number', 'nil'}, interval = {'number', 'nil'}, [{'req_num', 'reqnum'}] = {'number', 'nil'}, toggle = {'boolean', 'nil'}, unitTableDef = {'table', 'nil'}, } local err, errmsg = mist.utils.typeCheck('mist.flagFunc.units_in_polygon', type_tbl, vars) assert(err, errmsg) local units = vars.units or vars.unit local zone = vars.zone or vars.polyzone local flag = vars.flag local stopflag = vars.stopflag or vars.stopFlag or -1 local interval = vars.interval or 1 local maxalt = vars.maxalt or vars.alt local req_num = vars.req_num or vars.reqnum or 1 local toggle = vars.toggle or nil local unitTableDef = vars.unitTableDef if not units.processed then unitTableDef = mist.utils.deepCopy(units) end if (units.processed and units.processed < mist.getLastDBUpdateTime()) or not units.processed then -- run unit table short cuts if unitTableDef then units = mist.makeUnitTable(unitTableDef) end end if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == 0) then local num_in_zone = 0 for i = 1, #units do local unit = Unit.getByName(units[i]) or StaticObject.getByName(units[i]) if unit then local pos = unit:getPosition().p if mist.pointInPolygon(pos, zone, maxalt) then num_in_zone = num_in_zone + 1 if num_in_zone >= req_num and trigger.misc.getUserFlag(flag) == 0 then trigger.action.setUserFlag(flag, true) break end end end end if toggle and (num_in_zone < req_num) and trigger.misc.getUserFlag(flag) > 0 then trigger.action.setUserFlag(flag, false) end -- do another check in case stopflag was set true by this function if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == 0) then mist.scheduleFunction(mist.flagFunc.units_in_polygon, {{units = units, zone = zone, flag = flag, stopflag = stopflag, interval = interval, req_num = req_num, maxalt = maxalt, toggle = toggle, unitTableDef = unitTableDef}}, timer.getTime() + interval) end end end --- Sets a flag if unit(s) is/are inside a trigger zone. -- @todo document function mist.flagFunc.units_in_zones(vars) --[[vars needs to be: units = table, zones = table, flag = number, stopflag = number or nil, zone_type = string or nil, req_num = number or nil, interval = number or nil toggle = boolean or nil ]] -- type_tbl local type_tbl = { units = 'table', zones = 'table', flag = {'number', 'string'}, [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'}, [{'zone_type', 'zonetype'}] = {'string', 'nil'}, [{'req_num', 'reqnum'}] = {'number', 'nil'}, interval = {'number', 'nil'}, toggle = {'boolean', 'nil'}, unitTableDef = {'table', 'nil'}, } local err, errmsg = mist.utils.typeCheck('mist.flagFunc.units_in_zones', type_tbl, vars) assert(err, errmsg) local units = vars.units local zones = vars.zones local flag = vars.flag local stopflag = vars.stopflag or vars.stopFlag or -1 local zone_type = vars.zone_type or vars.zonetype or 'cylinder' local req_num = vars.req_num or vars.reqnum or 1 local interval = vars.interval or 1 local toggle = vars.toggle or nil local unitTableDef = vars.unitTableDef if not units.processed then unitTableDef = mist.utils.deepCopy(units) end if (units.processed and units.processed < mist.getLastDBUpdateTime()) or not units.processed then -- run unit table short cuts if unitTableDef then units = mist.makeUnitTable(unitTableDef) end end if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then local in_zone_units = mist.getUnitsInZones(units, zones, zone_type) if #in_zone_units >= req_num and trigger.misc.getUserFlag(flag) == 0 then trigger.action.setUserFlag(flag, true) elseif #in_zone_units < req_num and toggle then trigger.action.setUserFlag(flag, false) end -- do another check in case stopflag was set true by this function if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then mist.scheduleFunction(mist.flagFunc.units_in_zones, {{units = units, zones = zones, flag = flag, stopflag = stopflag, zone_type = zone_type, req_num = req_num, interval = interval, toggle = toggle, unitTableDef = unitTableDef}}, timer.getTime() + interval) end end end --[[ function mist.flagFunc.weapon_in_zones(vars) -- borrow from suchoi surprise. While running enabled event handler that checks for weapons in zone. -- Choice is weapon category or weapon strings. end ]] --- Sets a flag if unit(s) is/are inside a moving zone. -- @todo document function mist.flagFunc.units_in_moving_zones(vars) --[[vars needs to be: units = table, zone_units = table, radius = number, flag = number, stopflag = number or nil, zone_type = string or nil, req_num = number or nil, interval = number or nil toggle = boolean or nil ]] -- type_tbl local type_tbl = { units = 'table', [{'zone_units', 'zoneunits'}] = 'table', radius = 'number', flag = {'number', 'string'}, [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'}, [{'zone_type', 'zonetype'}] = {'string', 'nil'}, [{'req_num', 'reqnum'}] = {'number', 'nil'}, interval = {'number', 'nil'}, toggle = {'boolean', 'nil'}, unitTableDef = {'table', 'nil'}, zUnitTableDef = {'table', 'nil'}, } local err, errmsg = mist.utils.typeCheck('mist.flagFunc.units_in_moving_zones', type_tbl, vars) assert(err, errmsg) local units = vars.units local zone_units = vars.zone_units or vars.zoneunits local radius = vars.radius local flag = vars.flag local stopflag = vars.stopflag or vars.stopFlag or -1 local zone_type = vars.zone_type or vars.zonetype or 'cylinder' local req_num = vars.req_num or vars.reqnum or 1 local interval = vars.interval or 1 local toggle = vars.toggle or nil local unitTableDef = vars.unitTableDef local zUnitTableDef = vars.zUnitTableDef if not units.processed then unitTableDef = mist.utils.deepCopy(units) end if not zone_units.processed then zUnitTableDef = mist.utils.deepCopy(zone_units) end if (units.processed and units.processed < mist.getLastDBUpdateTime()) or not units.processed then -- run unit table short cuts if unitTableDef then units = mist.makeUnitTable(unitTableDef) end end if (zone_units.processed and zone_units.processed < mist.getLastDBUpdateTime()) or not zone_units.processed then -- run unit table short cuts if zUnitTableDef then zone_units = mist.makeUnitTable(zUnitTableDef) end end if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then local in_zone_units = mist.getUnitsInMovingZones(units, zone_units, radius, zone_type) if #in_zone_units >= req_num and trigger.misc.getUserFlag(flag) == 0 then trigger.action.setUserFlag(flag, true) elseif #in_zone_units < req_num and toggle then trigger.action.setUserFlag(flag, false) end -- do another check in case stopflag was set true by this function if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then mist.scheduleFunction(mist.flagFunc.units_in_moving_zones, {{units = units, zone_units = zone_units, radius = radius, flag = flag, stopflag = stopflag, zone_type = zone_type, req_num = req_num, interval = interval, toggle = toggle, unitTableDef = unitTableDef, zUnitTableDef = zUnitTableDef}}, timer.getTime() + interval) end end end --- Sets a flag if units have line of sight to each other. -- @todo document function mist.flagFunc.units_LOS(vars) --[[vars needs to be: unitset1 = table, altoffset1 = number, unitset2 = table, altoffset2 = number, flag = number, stopflag = number or nil, radius = number or nil, interval = number or nil, req_num = number or nil toggle = boolean or nil ]] -- type_tbl local type_tbl = { [{'unitset1', 'units1'}] = 'table', [{'altoffset1', 'alt1'}] = 'number', [{'unitset2', 'units2'}] = 'table', [{'altoffset2', 'alt2'}] = 'number', flag = {'number', 'string'}, [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'}, [{'req_num', 'reqnum'}] = {'number', 'nil'}, interval = {'number', 'nil'}, radius = {'number', 'nil'}, toggle = {'boolean', 'nil'}, unitTableDef1 = {'table', 'nil'}, unitTableDef2 = {'table', 'nil'}, } local err, errmsg = mist.utils.typeCheck('mist.flagFunc.units_LOS', type_tbl, vars) assert(err, errmsg) local unitset1 = vars.unitset1 or vars.units1 local altoffset1 = vars.altoffset1 or vars.alt1 local unitset2 = vars.unitset2 or vars.units2 local altoffset2 = vars.altoffset2 or vars.alt2 local flag = vars.flag local stopflag = vars.stopflag or vars.stopFlag or -1 local interval = vars.interval or 1 local radius = vars.radius or math.huge local req_num = vars.req_num or vars.reqnum or 1 local toggle = vars.toggle or nil local unitTableDef1 = vars.unitTableDef1 local unitTableDef2 = vars.unitTableDef2 if not unitset1.processed then unitTableDef1 = mist.utils.deepCopy(unitset1) end if not unitset2.processed then unitTableDef2 = mist.utils.deepCopy(unitset2) end if (unitset1.processed and unitset1.processed < mist.getLastDBUpdateTime()) or not unitset1.processed then -- run unit table short cuts if unitTableDef1 then unitset1 = mist.makeUnitTable(unitTableDef1) end end if (unitset2.processed and unitset2.processed < mist.getLastDBUpdateTime()) or not unitset2.processed then -- run unit table short cuts if unitTableDef2 then unitset2 = mist.makeUnitTable(unitTableDef2) end end if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then local unitLOSdata = mist.getUnitsLOS(unitset1, altoffset1, unitset2, altoffset2, radius) if #unitLOSdata >= req_num and trigger.misc.getUserFlag(flag) == 0 then trigger.action.setUserFlag(flag, true) elseif #unitLOSdata < req_num and toggle then trigger.action.setUserFlag(flag, false) end -- do another check in case stopflag was set true by this function if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then mist.scheduleFunction(mist.flagFunc.units_LOS, {{unitset1 = unitset1, altoffset1 = altoffset1, unitset2 = unitset2, altoffset2 = altoffset2, flag = flag, stopflag = stopflag, radius = radius, req_num = req_num, interval = interval, toggle = toggle, unitTableDef1 = unitTableDef1, unitTableDef2 = unitTableDef2}}, timer.getTime() + interval) end end end --- Sets a flag if group is alive. -- @todo document function mist.flagFunc.group_alive(vars) --[[vars groupName flag toggle interval stopFlag ]] local type_tbl = { [{'group', 'groupname', 'gp', 'groupName'}] = 'string', flag = {'number', 'string'}, [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'}, interval = {'number', 'nil'}, toggle = {'boolean', 'nil'}, } local err, errmsg = mist.utils.typeCheck('mist.flagFunc.group_alive', type_tbl, vars) assert(err, errmsg) local groupName = vars.groupName or vars.group or vars.gp or vars.Groupname local flag = vars.flag local stopflag = vars.stopflag or vars.stopFlag or -1 local interval = vars.interval or 1 local toggle = vars.toggle or nil if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then if Group.getByName(groupName) and Group.getByName(groupName):isExist() == true and #Group.getByName(groupName):getUnits() > 0 then if trigger.misc.getUserFlag(flag) == 0 then trigger.action.setUserFlag(flag, true) end else if toggle then trigger.action.setUserFlag(flag, false) end end end if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then mist.scheduleFunction(mist.flagFunc.group_alive, {{groupName = groupName, flag = flag, stopflag = stopflag, interval = interval, toggle = toggle}}, timer.getTime() + interval) end end --- Sets a flag if group is dead. -- @todo document function mist.flagFunc.group_dead(vars) local type_tbl = { [{'group', 'groupname', 'gp', 'groupName'}] = 'string', flag = {'number', 'string'}, [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'}, interval = {'number', 'nil'}, toggle = {'boolean', 'nil'}, } local err, errmsg = mist.utils.typeCheck('mist.flagFunc.group_dead', type_tbl, vars) assert(err, errmsg) local groupName = vars.groupName or vars.group or vars.gp or vars.Groupname local flag = vars.flag local stopflag = vars.stopflag or vars.stopFlag or -1 local interval = vars.interval or 1 local toggle = vars.toggle or nil if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then if (Group.getByName(groupName) and Group.getByName(groupName):isExist() == false) or (Group.getByName(groupName) and #Group.getByName(groupName):getUnits() < 1) or not Group.getByName(groupName) then if trigger.misc.getUserFlag(flag) == 0 then trigger.action.setUserFlag(flag, true) end else if toggle then trigger.action.setUserFlag(flag, false) end end end if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then mist.scheduleFunction(mist.flagFunc.group_dead, {{groupName = groupName, flag = flag, stopflag = stopflag, interval = interval, toggle = toggle}}, timer.getTime() + interval) end end --- Sets a flag if less than given percent of group is alive. -- @todo document function mist.flagFunc.group_alive_less_than(vars) local type_tbl = { [{'group', 'groupname', 'gp', 'groupName'}] = 'string', percent = 'number', flag = {'number', 'string'}, [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'}, interval = {'number', 'nil'}, toggle = {'boolean', 'nil'}, } local err, errmsg = mist.utils.typeCheck('mist.flagFunc.group_alive_less_than', type_tbl, vars) assert(err, errmsg) local groupName = vars.groupName or vars.group or vars.gp or vars.Groupname local flag = vars.flag local percent = vars.percent local stopflag = vars.stopflag or vars.stopFlag or -1 local interval = vars.interval or 1 local toggle = vars.toggle or nil if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then if Group.getByName(groupName) and Group.getByName(groupName):isExist() == true then if Group.getByName(groupName):getSize()/Group.getByName(groupName):getInitialSize() < percent/100 then if trigger.misc.getUserFlag(flag) == 0 then trigger.action.setUserFlag(flag, true) end else if toggle then trigger.action.setUserFlag(flag, false) end end else if trigger.misc.getUserFlag(flag) == 0 then trigger.action.setUserFlag(flag, true) end end end if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then mist.scheduleFunction(mist.flagFunc.group_alive_less_than, {{groupName = groupName, flag = flag, stopflag = stopflag, interval = interval, toggle = toggle, percent = percent}}, timer.getTime() + interval) end end --- Sets a flag if more than given percent of group is alive. -- @todo document function mist.flagFunc.group_alive_more_than(vars) local type_tbl = { [{'group', 'groupname', 'gp', 'groupName'}] = 'string', percent = 'number', flag = {'number', 'string'}, [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'}, interval = {'number', 'nil'}, toggle = {'boolean', 'nil'}, } local err, errmsg = mist.utils.typeCheck('mist.flagFunc.group_alive_more_than', type_tbl, vars) assert(err, errmsg) local groupName = vars.groupName or vars.group or vars.gp or vars.Groupname local flag = vars.flag local percent = vars.percent local stopflag = vars.stopflag or vars.stopFlag or -1 local interval = vars.interval or 1 local toggle = vars.toggle or nil if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then if Group.getByName(groupName) and Group.getByName(groupName):isExist() == true then if Group.getByName(groupName):getSize()/Group.getByName(groupName):getInitialSize() > percent/100 then if trigger.misc.getUserFlag(flag) == 0 then trigger.action.setUserFlag(flag, true) end else if toggle and trigger.misc.getUserFlag(flag) == 1 then trigger.action.setUserFlag(flag, false) end end else --- just in case if toggle and trigger.misc.getUserFlag(flag) == 1 then trigger.action.setUserFlag(flag, false) end end end if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then mist.scheduleFunction(mist.flagFunc.group_alive_more_than, {{groupName = groupName, flag = flag, stopflag = stopflag, interval = interval, toggle = toggle, percent = percent}}, timer.getTime() + interval) end end mist.flagFunc.mapobjsDeadPolygon = mist.flagFunc.mapobjs_dead_polygon mist.flagFunc.mapobjsDeadZones = mist.flagFunc.Mapobjs_dead_zones mist.flagFunc.unitsInZones = mist.flagFunc.units_in_zones mist.flagFunc.unitsInMovingZones = mist.flagFunc.units_in_moving_zones mist.flagFunc.unitsInPolygon = mist.flagFunc.units_in_polygon mist.flagFunc.unitsLOS = mist.flagFunc.units_LOS mist.flagFunc.groupAlive = mist.flagFunc.group_alive mist.flagFunc.groupDead = mist.flagFunc.group_dead mist.flagFunc.groupAliveMoreThan = mist.flagFunc.group_alive_more_than mist.flagFunc.groupAliveLessThan = mist.flagFunc.group_alive_less_than end --- Message functions. -- Messaging system -- @section mist.msg do -- mist.msg scope local messageList = {} -- this defines the max refresh rate of the message box it honestly only needs to -- go faster than this for precision timing stuff (which could be its own function) local messageDisplayRate = 0.1 local messageID = 0 local displayActive = false local displayFuncId = 0 local caSlots = false local caMSGtoGroup = false local anyUpdate = false local lastMessageTime = nil if env.mission.groundControl then -- just to be sure? for index, value in pairs(env.mission.groundControl) do if type(value) == 'table' then for roleName, roleVal in pairs(value) do for rIndex, rVal in pairs(roleVal) do if type(rVal) == 'number' and rVal > 0 then caSlots = true break end end end elseif type(value) == 'boolean' and value == true then caSlots = true break end end end local function mistdisplayV5() --log:warn("mistdisplayV5: $1", timer.getTime()) local clearView = true if #messageList > 0 then --log:warn('Updates: $1', anyUpdate) if anyUpdate == true then local activeClients = {} for clientId, clientData in pairs(mist.DBs.humansById) do if Unit.getByName(clientData.unitName) and Unit.getByName(clientData.unitName):isExist() == true then activeClients[clientData.groupId] = clientData.groupName end end anyUpdate = false if displayActive == false then displayActive = true end --mist.debug.writeData(mist.utils.serialize,{'msg', messageList}, 'messageList.lua') local msgTableText = {} local msgTableSound = {} local curTime = timer.getTime() for mInd, messageData in pairs(messageList) do --log:warn(messageData) if messageData.displayTill < curTime then messageData:remove() -- now using the remove/destroy function. else if messageData.displayedFor then messageData.displayedFor = curTime - messageData.addedAt end local nextSound = 1000 local soundIndex = 0 if messageData.multSound and #messageData.multSound > 0 then for index, sData in pairs(messageData.multSound) do if sData.time <= messageData.displayedFor and sData.played == false and sData.time < nextSound then -- find index of the next sound to be played nextSound = sData.time soundIndex = index end end if soundIndex ~= 0 then messageData.multSound[soundIndex].played = true end end for recIndex, recData in pairs(messageData.msgFor) do -- iterate recipiants if recData == 'RED' or recData == 'BLUE' or activeClients[recData] then -- rec exists if messageData.text then -- text if not msgTableText[recData] then -- create table entry for text msgTableText[recData] = {} msgTableText[recData].text = {} if recData == 'RED' or recData == 'BLUE' then msgTableText[recData].text[1] = '-------Combined Arms Message-------- \n' end msgTableText[recData].text[#msgTableText[recData].text + 1] = messageData.text msgTableText[recData].displayTime = messageData.displayTime - messageData.displayedFor else -- add to table entry and adjust display time if needed if recData == 'RED' or recData == 'BLUE' then msgTableText[recData].text[#msgTableText[recData].text + 1] = '\n ---------------- Combined Arms Message: \n' else msgTableText[recData].text[#msgTableText[recData].text + 1] = '\n ---------------- \n' end table.insert(msgTableText[recData].text, messageData.text) if msgTableText[recData].displayTime < messageData.displayTime - messageData.displayedFor then msgTableText[recData].displayTime = messageData.displayTime - messageData.displayedFor else --msgTableText[recData].displayTime = 10 end end end if soundIndex ~= 0 then msgTableSound[recData] = messageData.multSound[soundIndex].file end end end messageData.update = nil end end ------- new display if caSlots == true and caMSGtoGroup == false then if msgTableText.RED then trigger.action.outTextForCoalition(coalition.side.RED, table.concat(msgTableText.RED.text), msgTableText.RED.displayTime, clearView) end if msgTableText.BLUE then trigger.action.outTextForCoalition(coalition.side.BLUE, table.concat(msgTableText.BLUE.text), msgTableText.BLUE.displayTime, clearView) end end for index, msgData in pairs(msgTableText) do if type(index) == 'number' then -- its a groupNumber trigger.action.outTextForGroup(index, table.concat(msgData.text), msgData.displayTime, clearView) end end --- new audio if msgTableSound.RED then trigger.action.outSoundForCoalition(coalition.side.RED, msgTableSound.RED) end if msgTableSound.BLUE then trigger.action.outSoundForCoalition(coalition.side.BLUE, msgTableSound.BLUE) end for index, file in pairs(msgTableSound) do if type(index) == 'number' then -- its a groupNumber trigger.action.outSoundForGroup(index, file) end end end else mist.removeFunction(displayFuncId) displayActive = false end end local function mistdisplayV4() local activeClients = {} for clientId, clientData in pairs(mist.DBs.humansById) do if Unit.getByName(clientData.unitName) and Unit.getByName(clientData.unitName):isExist() == true then activeClients[clientData.groupId] = clientData.groupName end end --[[if caSlots == true and caMSGtoGroup == true then end]] if #messageList > 0 then if displayActive == false then displayActive = true end --mist.debug.writeData(mist.utils.serialize,{'msg', messageList}, 'messageList.lua') local msgTableText = {} local msgTableSound = {} for messageId, messageData in pairs(messageList) do if messageData.displayedFor > messageData.displayTime then messageData:remove() -- now using the remove/destroy function. else if messageData.displayedFor then messageData.displayedFor = messageData.displayedFor + messageDisplayRate end local nextSound = 1000 local soundIndex = 0 if messageData.multSound and #messageData.multSound > 0 then for index, sData in pairs(messageData.multSound) do if sData.time <= messageData.displayedFor and sData.played == false and sData.time < nextSound then -- find index of the next sound to be played nextSound = sData.time soundIndex = index end end if soundIndex ~= 0 then messageData.multSound[soundIndex].played = true end end for recIndex, recData in pairs(messageData.msgFor) do -- iterate recipiants if recData == 'RED' or recData == 'BLUE' or activeClients[recData] then -- rec exists if messageData.text then -- text if not msgTableText[recData] then -- create table entry for text msgTableText[recData] = {} msgTableText[recData].text = {} if recData == 'RED' or recData == 'BLUE' then msgTableText[recData].text[1] = '-------Combined Arms Message-------- \n' end msgTableText[recData].text[#msgTableText[recData].text + 1] = messageData.text msgTableText[recData].displayTime = messageData.displayTime - messageData.displayedFor else -- add to table entry and adjust display time if needed if recData == 'RED' or recData == 'BLUE' then msgTableText[recData].text[#msgTableText[recData].text + 1] = '\n ---------------- Combined Arms Message: \n' else msgTableText[recData].text[#msgTableText[recData].text + 1] = '\n ---------------- \n' end msgTableText[recData].text[#msgTableText[recData].text + 1] = messageData.text if msgTableText[recData].displayTime < messageData.displayTime - messageData.displayedFor then msgTableText[recData].displayTime = messageData.displayTime - messageData.displayedFor else msgTableText[recData].displayTime = 1 end end end if soundIndex ~= 0 then msgTableSound[recData] = messageData.multSound[soundIndex].file end end end end end ------- new display if caSlots == true and caMSGtoGroup == false then if msgTableText.RED then trigger.action.outTextForCoalition(coalition.side.RED, table.concat(msgTableText.RED.text), msgTableText.RED.displayTime, true) end if msgTableText.BLUE then trigger.action.outTextForCoalition(coalition.side.BLUE, table.concat(msgTableText.BLUE.text), msgTableText.BLUE.displayTime, true) end end for index, msgData in pairs(msgTableText) do if type(index) == 'number' then -- its a groupNumber trigger.action.outTextForGroup(index, table.concat(msgData.text), msgData.displayTime, true) end end --- new audio if msgTableSound.RED then trigger.action.outSoundForCoalition(coalition.side.RED, msgTableSound.RED) end if msgTableSound.BLUE then trigger.action.outSoundForCoalition(coalition.side.BLUE, msgTableSound.BLUE) end for index, file in pairs(msgTableSound) do if type(index) == 'number' then -- its a groupNumber trigger.action.outSoundForGroup(index, file) end end else mist.removeFunction(displayFuncId) displayActive = false end end local typeBase = { ['Mi-8MT'] = {'Mi-8MTV2', 'Mi-8MTV', 'Mi-8'}, ['MiG-21Bis'] = {'Mig-21'}, ['MiG-15bis'] = {'Mig-15'}, ['FW-190D9'] = {'FW-190'}, ['Bf-109K-4'] = {'Bf-109'}, } --[[function mist.setCAGroupMSG(val) if type(val) == 'boolean' then caMSGtoGroup = val return true end return false end]] mist.message = { add = function(vars) local function msgSpamFilter(recList, spamBlockOn) for id, name in pairs(recList) do if name == spamBlockOn then -- log:info('already on recList') return recList end end --log:info('add to recList') table.insert(recList, spamBlockOn) return recList end --[[ local vars = {} vars.text = 'Hello World' vars.displayTime = 20 vars.msgFor = {coa = {'red'}, countries = {'Ukraine', 'Georgia'}, unitTypes = {'A-10C'}} mist.message.add(vars) Displays the message for all red coalition players. Players belonging to Ukraine and Georgia, and all A-10Cs on the map ]] local new = {} new.text = vars.text -- The actual message new.displayTime = vars.displayTime -- How long will the message appear for new.displayedFor = 0 -- how long the message has been displayed so far new.displayTill = timer.getTime() + vars.displayTime new.name = vars.name -- ID to overwrite the older message (if it exists) Basically it replaces a message that is displayed with new text. new.addedAt = timer.getTime() --log:warn('New Message: $1', new.text) if vars.multSound and vars.multSound[1] then new.multSound = vars.multSound else new.multSound = {} end if vars.sound or vars.fileName then -- converts old sound file system into new multSound format local sound = vars.sound if vars.fileName then sound = vars.fileName end new.multSound[#new.multSound+1] = {time = 0.1, file = sound} end if #new.multSound > 0 then for i, data in pairs(new.multSound) do data.played = false end end local newMsgFor = {} -- list of all groups message displays for for forIndex, forData in pairs(vars.msgFor) do for list, listData in pairs(forData) do for clientId, clientData in pairs(mist.DBs.humansById) do forIndex = string.lower(forIndex) if type(listData) == 'string' then listData = string.lower(listData) end if (forIndex == 'coa' and (listData == string.lower(clientData.coalition) or listData == 'all')) or (forIndex == 'countries' and string.lower(clientData.country) == listData) or (forIndex == 'units' and string.lower(clientData.unitName) == listData) then -- newMsgFor = msgSpamFilter(newMsgFor, clientData.groupId) -- so units dont get the same message twice if complex rules are given --table.insert(newMsgFor, clientId) elseif forIndex == 'unittypes' then for typeId, typeData in pairs(listData) do local found = false for clientDataEntry, clientDataVal in pairs(clientData) do if type(clientDataVal) == 'string' then if mist.matchString(list, clientDataVal) == true or list == 'all' then local sString = typeData for rName, pTbl in pairs(typeBase) do -- just a quick check to see if the user may have meant something and got the specific type of the unit wrong for pIndex, pName in pairs(pTbl) do if mist.stringMatch(sString, pName) then sString = rName end end end if sString == clientData.type then found = true newMsgFor = msgSpamFilter(newMsgFor, clientData.groupId) -- sends info oto other function to see if client is already recieving the current message. --table.insert(newMsgFor, clientId) end end end if found == true then -- shouldn't this be elsewhere too? break end end end end end for coaData, coaId in pairs(coalition.side) do if string.lower(forIndex) == 'coa' or string.lower(forIndex) == 'ca' then if listData == string.lower(coaData) or listData == 'all' then newMsgFor = msgSpamFilter(newMsgFor, coaData) end end end end end if #newMsgFor > 0 then new.msgFor = newMsgFor -- I swear its not confusing else return false end if vars.name and type(vars.name) == 'string' then for i = 1, #messageList do if messageList[i].name then if messageList[i].name == vars.name then --log:info('updateMessage') messageList[i].displayTill = timer.getTime() + messageList[i].displayTime messageList[i].displayedFor = 0 messageList[i].addedAt = timer.getTime() messageList[i].sound = new.sound messageList[i].text = new.text messageList[i].msgFor = new.msgFor messageList[i].multSound = new.multSound anyUpdate = true --log:warn('Message updated: $1', new.messageID) return messageList[i].messageID end end end end anyUpdate = true messageID = messageID + 1 new.messageID = messageID --mist.debug.writeData(mist.utils.serialize,{'msg', new}, 'newMsg.lua') messageList[#messageList + 1] = new local mt = { __index = mist.message} setmetatable(new, mt) if displayActive == false then displayActive = true displayFuncId = mist.scheduleFunction(mistdisplayV5, {}, timer.getTime() + messageDisplayRate, messageDisplayRate) end return messageID end, remove = function(self) -- Now a self variable; the former functionality taken up by mist.message.removeById. for i, msgData in pairs(messageList) do if messageList[i] == self then table.remove(messageList, i) anyUpdate = true return true --removal successful end end return false -- removal not successful this script fails at life! end, removeById = function(id) -- This function is NOT passed a self variable; it is the remove by id function. for i, msgData in pairs(messageList) do if messageList[i].messageID == id then table.remove(messageList, i) anyUpdate = true return true --removal successful end end return false -- removal not successful this script fails at life! end, } --[[ vars for mist.msgMGRS vars.units - table of unit names (NOT unitNameTable- maybe this should change). vars.acc - integer between 0 and 5, inclusive vars.text - text in the message vars.displayTime - self explanatory vars.msgFor - scope ]] function mist.msgMGRS(vars) local units = vars.units local acc = vars.acc local text = vars.text local displayTime = vars.displayTime local msgFor = vars.msgFor local s = mist.getMGRSString{units = units, acc = acc} local newText if text then if string.find(text, '%%s') then -- look for %s newText = string.format(text, s) -- insert the coordinates into the message else -- just append to the end. newText = text .. s end else newText = s end mist.message.add{ text = newText, displayTime = displayTime, msgFor = msgFor } end --[[ vars for mist.msgLL vars.units - table of unit names (NOT unitNameTable- maybe this should change) (Yes). vars.acc - integer, number of numbers after decimal place vars.DMS - if true, output in degrees, minutes, seconds. Otherwise, output in degrees, minutes. vars.text - text in the message vars.displayTime - self explanatory vars.msgFor - scope ]] function mist.msgLL(vars) local units = vars.units -- technically, I don't really need to do this, but it helps readability. local acc = vars.acc local DMS = vars.DMS local text = vars.text local displayTime = vars.displayTime local msgFor = vars.msgFor local s = mist.getLLString{units = units, acc = acc, DMS = DMS} local newText if text then if string.find(text, '%%s') then -- look for %s newText = string.format(text, s) -- insert the coordinates into the message else -- just append to the end. newText = text .. s end else newText = s end mist.message.add{ text = newText, displayTime = displayTime, msgFor = msgFor } end --[[ vars.units- table of unit names (NOT unitNameTable- maybe this should change). vars.ref - vec3 ref point, maybe overload for vec2 as well? vars.alt - boolean, if used, includes altitude in string vars.metric - boolean, gives distance in km instead of NM. vars.text - text of the message vars.displayTime vars.msgFor - scope ]] function mist.msgBR(vars) local units = vars.units -- technically, I don't really need to do this, but it helps readability. local ref = vars.ref -- vec2/vec3 will be handled in mist.getBRString local alt = vars.alt local metric = vars.metric local text = vars.text local displayTime = vars.displayTime local msgFor = vars.msgFor local s = mist.getBRString{units = units, ref = ref, alt = alt, metric = metric} local newText if text then if string.find(text, '%%s') then -- look for %s newText = string.format(text, s) -- insert the coordinates into the message else -- just append to the end. newText = text .. s end else newText = s end mist.message.add{ text = newText, displayTime = displayTime, msgFor = msgFor } end -- basically, just sub-types of mist.msgBR... saves folks the work of getting the ref point. --[[ vars.units- table of unit names (NOT unitNameTable- maybe this should change). vars.ref - string red, blue vars.alt - boolean, if used, includes altitude in string vars.metric - boolean, gives distance in km instead of NM. vars.text - text of the message vars.displayTime vars.msgFor - scope ]] function mist.msgBullseye(vars) if mist.DBs.missionData.bullseye[string.lower(vars.ref)] then vars.ref = mist.DBs.missionData.bullseye[string.lower(vars.ref)] mist.msgBR(vars) end end --[[ vars.units- table of unit names (NOT unitNameTable- maybe this should change). vars.ref - unit name of reference point vars.alt - boolean, if used, includes altitude in string vars.metric - boolean, gives distance in km instead of NM. vars.text - text of the message vars.displayTime vars.msgFor - scope ]] function mist.msgBRA(vars) if Unit.getByName(vars.ref) and Unit.getByName(vars.ref):isExist() == true then vars.ref = Unit.getByName(vars.ref):getPosition().p if not vars.alt then vars.alt = true end mist.msgBR(vars) end end --[[ vars for mist.msgLeadingMGRS: vars.units - table of unit names vars.heading - direction vars.radius - number vars.headingDegrees - boolean, switches heading to degrees (optional) vars.acc - number, 0 to 5. vars.text - text of the message vars.displayTime vars.msgFor - scope ]] function mist.msgLeadingMGRS(vars) local units = vars.units -- technically, I don't really need to do this, but it helps readability. local heading = vars.heading local radius = vars.radius local headingDegrees = vars.headingDegrees local acc = vars.acc local text = vars.text local displayTime = vars.displayTime local msgFor = vars.msgFor local s = mist.getLeadingMGRSString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc} local newText if text then if string.find(text, '%%s') then -- look for %s newText = string.format(text, s) -- insert the coordinates into the message else -- just append to the end. newText = text .. s end else newText = s end mist.message.add{ text = newText, displayTime = displayTime, msgFor = msgFor } end --[[ vars for mist.msgLeadingLL: vars.units - table of unit names vars.heading - direction, number vars.radius - number vars.headingDegrees - boolean, switches heading to degrees (optional) vars.acc - number of digits after decimal point (can be negative) vars.DMS - boolean, true if you want DMS. (optional) vars.text - text of the message vars.displayTime vars.msgFor - scope ]] function mist.msgLeadingLL(vars) local units = vars.units -- technically, I don't really need to do this, but it helps readability. local heading = vars.heading local radius = vars.radius local headingDegrees = vars.headingDegrees local acc = vars.acc local DMS = vars.DMS local text = vars.text local displayTime = vars.displayTime local msgFor = vars.msgFor local s = mist.getLeadingLLString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc, DMS = DMS} local newText if text then if string.find(text, '%%s') then -- look for %s newText = string.format(text, s) -- insert the coordinates into the message else -- just append to the end. newText = text .. s end else newText = s end mist.message.add{ text = newText, displayTime = displayTime, msgFor = msgFor } end --[[ vars.units - table of unit names vars.heading - direction, number vars.radius - number vars.headingDegrees - boolean, switches heading to degrees (optional) vars.metric - boolean, if true, use km instead of NM. (optional) vars.alt - boolean, if true, include altitude. (optional) vars.ref - vec3/vec2 reference point. vars.text - text of the message vars.displayTime vars.msgFor - scope ]] function mist.msgLeadingBR(vars) local units = vars.units -- technically, I don't really need to do this, but it helps readability. local heading = vars.heading local radius = vars.radius local headingDegrees = vars.headingDegrees local metric = vars.metric local alt = vars.alt local ref = vars.ref -- vec2/vec3 will be handled in mist.getBRString local text = vars.text local displayTime = vars.displayTime local msgFor = vars.msgFor local s = mist.getLeadingBRString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, metric = metric, alt = alt, ref = ref} local newText if text then if string.find(text, '%%s') then -- look for %s newText = string.format(text, s) -- insert the coordinates into the message else -- just append to the end. newText = text .. s end else newText = s end mist.message.add{ text = newText, displayTime = displayTime, msgFor = msgFor } end end --- Demo functions. -- @section mist.demos do -- mist.demos scope mist.demos = {} function mist.demos.printFlightData(unit) if unit:isExist() then local function printData(unit, prevVel, prevE, prevTime) local angles = mist.getAttitude(unit) if angles then local Heading = angles.Heading local Pitch = angles.Pitch local Roll = angles.Roll local Yaw = angles.Yaw local AoA = angles.AoA local ClimbAngle = angles.ClimbAngle if not Heading then Heading = 'NA' else Heading = string.format('%12.2f', mist.utils.toDegree(Heading)) end if not Pitch then Pitch = 'NA' else Pitch = string.format('%12.2f', mist.utils.toDegree(Pitch)) end if not Roll then Roll = 'NA' else Roll = string.format('%12.2f', mist.utils.toDegree(Roll)) end local AoAplusYaw = 'NA' if AoA and Yaw then AoAplusYaw = string.format('%12.2f', mist.utils.toDegree((AoA^2 + Yaw^2)^0.5)) end if not Yaw then Yaw = 'NA' else Yaw = string.format('%12.2f', mist.utils.toDegree(Yaw)) end if not AoA then AoA = 'NA' else AoA = string.format('%12.2f', mist.utils.toDegree(AoA)) end if not ClimbAngle then ClimbAngle = 'NA' else ClimbAngle = string.format('%12.2f', mist.utils.toDegree(ClimbAngle)) end local unitPos = unit:getPosition() local unitVel = unit:getVelocity() local curTime = timer.getTime() local absVel = string.format('%12.2f', mist.vec.mag(unitVel)) local unitAcc = 'NA' local Gs = 'NA' local axialGs = 'NA' local transGs = 'NA' if prevVel and prevTime then local xAcc = (unitVel.x - prevVel.x)/(curTime - prevTime) local yAcc = (unitVel.y - prevVel.y)/(curTime - prevTime) local zAcc = (unitVel.z - prevVel.z)/(curTime - prevTime) unitAcc = string.format('%12.2f', mist.vec.mag({x = xAcc, y = yAcc, z = zAcc})) Gs = string.format('%12.2f', mist.vec.mag({x = xAcc, y = yAcc + 9.81, z = zAcc})/9.81) axialGs = string.format('%12.2f', mist.vec.dp({x = xAcc, y = yAcc + 9.81, z = zAcc}, unitPos.x)/9.81) transGs = string.format('%12.2f', mist.vec.mag(mist.vec.cp({x = xAcc, y = yAcc + 9.81, z = zAcc}, unitPos.x))/9.81) end local E = 0.5*mist.vec.mag(unitVel)^2 + 9.81*unitPos.p.y local energy = string.format('%12.2e', E) local dEdt = 'NA' if prevE and prevTime then dEdt = string.format('%12.2e', (E - prevE)/(curTime - prevTime)) end trigger.action.outText(string.format('%-25s', 'Heading: ') .. Heading .. ' degrees\n' .. string.format('%-25s', 'Roll: ') .. Roll .. ' degrees\n' .. string.format('%-25s', 'Pitch: ') .. Pitch .. ' degrees\n' .. string.format('%-25s', 'Yaw: ') .. Yaw .. ' degrees\n' .. string.format('%-25s', 'AoA: ') .. AoA .. ' degrees\n' .. string.format('%-25s', 'AoA plus Yaw: ') .. AoAplusYaw .. ' degrees\n' .. string.format('%-25s', 'Climb Angle: ') .. ClimbAngle .. ' degrees\n' .. string.format('%-25s', 'Absolute Velocity: ') .. absVel .. ' m/s\n' .. string.format('%-25s', 'Absolute Acceleration: ') .. unitAcc ..' m/s^2\n' .. string.format('%-25s', 'Axial G loading: ') .. axialGs .. ' g\n' .. string.format('%-25s', 'Transverse G loading: ') .. transGs .. ' g\n' .. string.format('%-25s', 'Absolute G loading: ') .. Gs .. ' g\n' .. string.format('%-25s', 'Energy: ') .. energy .. ' J/kg\n' .. string.format('%-25s', 'dE/dt: ') .. dEdt ..' J/(kg*s)', 1) return unitVel, E, curTime end end local function frameFinder(unit, prevVel, prevE, prevTime) if unit:isExist() then local currVel = unit:getVelocity() if prevVel and (prevVel.x ~= currVel.x or prevVel.y ~= currVel.y or prevVel.z ~= currVel.z) or (prevTime and (timer.getTime() - prevTime) > 0.25) then prevVel, prevE, prevTime = printData(unit, prevVel, prevE, prevTime) end mist.scheduleFunction(frameFinder, {unit, prevVel, prevE, prevTime}, timer.getTime() + 0.005) -- it can't go this fast, limited to the 100 times a sec check right now. end end local curVel = unit:getVelocity() local curTime = timer.getTime() local curE = 0.5*mist.vec.mag(curVel)^2 + 9.81*unit:getPosition().p.y frameFinder(unit, curVel, curE, curTime) end end end do --[[ stuff for marker panels marker.add() add marker. Point of these functions is to simplify process and to store all mark panels added. -- generates Id if not specified or if multiple marks created. -- makes marks for countries by creating a mark for each client group in the country -- can create multiple marks if needed for groups and countries. -- adds marks to table for parsing and removing -- Uses similar structure as messages. Big differences is it doesn't only mark to groups. If to All, then mark is for All if to coa mark is to coa if to specific units, mark is to group -------- STUFF TO Check -------- If mark added to a group before a client joins slot is synced. Mark made for cliet A in Slot A. Client A leaves, Client B joins in slot A. What do they see? May need to automate process... Could release this. But things I might need to add/change before doing so. - removing marks and re-adding in same sequence doesn't appear to work. May need to schedule adding mark if updating an entry. - I really dont like the old message style code for which groups get the message. Perhaps change to unitsTable and create function for getting humanUnitsTable. = Event Handler, and check it, for marks added via script or user to deconflict Ids. - Full validation of passed values for a specific shape type. ]] local usedMarks = {} local mDefs = { coa = { ['red'] = {fillColor = {.8, 0 , 0, .5}, color = {.8, 0 , 0, .5}, lineType = 2, fontSize = 16}, ['blue'] = {fillColor = {0, 0 , 0.8, .5}, color = {0, 0 , 0.8, .5}, lineType = 2, fontSize = 16}, ['all'] = {fillColor = {.1, .1 , .1, .5}, color = {.9, .9 , .9, .5}, lineType = 2, fontSize = 16}, ['neutral'] = {fillColor = {.1, .1 , .1, .5}, color = {.2, .2 , .2, .5}, lineType = 2, fontSize = 16}, }, } local userDefs = {['red'] = {},['blue'] = {},['all'] = {},['neutral'] = {}} local mId = 1000 local tNames = {'line', 'circle','rect', 'arrow', 'text', 'quad', 'freeform'} local tLines = {[0] = 'no line', [1] = 'solid', [2] = 'dashed',[3] = 'dotted', [4] = 'dot dash' ,[5] = 'long dash', [6] = 'two dash'} local coas = {[-1] = 'all', [0] = 'neutral', [1] = 'red', [2] = 'blue'} local altNames = {['poly'] = 7, ['lines'] = 1, ['polygon'] = 7 } local function draw(s) --log:warn(s) if type(s) == 'table' then local mType = s.markType if mType == 'panel' then if markScope == 'coa' then trigger.action.markToCoalition(s.markId, s.text, s.pos, s.markFor, s.readOnly) elseif markScope == 'group' then trigger.action.markToGroup(s.markId, s.text, s.pos, s.markFor, s.readOnly) else trigger.action.markToAll(s.markId, s.text, s.pos, s.readOnly) end elseif mType == 'line' then trigger.action.lineToAll(s.coa, s.markId, s.pos[1], s.pos[2], s.color, s.fillColor, s.lineType, s.readOnly, s.message) elseif mType == 'circle' then trigger.action.circleToAll(s.coa, s.markId, s.pos[1], s.radius, s.color, s.fillColor, s.lineType, s.readOnly, s.message) elseif mType == 'rect' then trigger.action.rectToAll(s.coa, s.markId, s.pos[1], s.pos[2], s.color, s.fillColor, s.lineType, s.readOnly, s.message) elseif mType == 'arrow' then trigger.action.arrowToAll(s.coa, s.markId, s.pos[1], s.pos[2], s.color, s.fillColor, s.lineType, s.readOnly, s.message) elseif mType == 'text' then trigger.action.textToAll(s.coa, s.markId, s.pos[1], s.color, s.fillColor, s.fontSize, s.readOnly, s.text) elseif mType == 'quad' then trigger.action.quadToAll(s.coa, s.markId, s.pos[1], s.pos[2], s.pos[3], s.pos[4], s.color, s.fillColor, s.lineType, s.readOnly, s.message) end if s.name and not usedMarks[s.name] then usedMarks[s.name] = s.markId end elseif type(s) == 'string' then --log:warn(s) mist.utils.dostring(s) end end mist.marker = {} local function markSpamFilter(recList, spamBlockOn) for id, name in pairs(recList) do if name == spamBlockOn then --log:info('already on recList') return recList end end --log:info('add to recList') table.insert(recList, spamBlockOn) return recList end local function iterate() while mId < 10000000 do if usedMarks[mId] then mId = mId + 1 else return mist.utils.deepCopy(mId) end end return mist.utils.deepCopy(mId) end local function validateColor(val) if type(val) == 'table' then for i = 1, #val do if type(val[i]) == 'number' and val[i] > 1 then val[i] = val[i]/255 -- convert RGB values from 0-255 to 0-1 equivilent. end end elseif type(val) == 'string' then val = mist.utils.hexToRGB(val) end return val end local function checkDefs(vName, coa) --log:warn('CheckDefs: $1 $2', vName, coa) local coaName if type(coa) == 'number' then if coas[coa] then coaName = coas[coa] end elseif type(coa) == 'string' then coaName = coa end -- log:warn(coaName) if userDefs[coaName] and userDefs[coaName][vName] then return userDefs[coaName][vName] elseif mDefs.coa[coaName] and mDefs.coa[coaName][vName] then return mDefs.coa[coaName][vName] end end function mist.marker.getNextId() return iterate() end local handle = {} function handle:onEvent(e) if world.event.S_EVENT_MARK_ADDED == e.id and e.idx then usedMarks[e.idx] = e.idx if not mist.DBs.markList[e.idx] then --log:info('create maker DB: $1', e.idx) mist.DBs.markList[e.idx] = {time = e.time, pos = e.pos, groupId = e.groupId, mType = 'panel', text = e.text, markId = e.idx, coalition = e.coalition} if e.unit then mist.DBs.markList[e.idx].unit = e.initiaor:getName() end --log:info(mist.marker.list[e.idx]) end elseif world.event.S_EVENT_MARK_CHANGE == e.id and e.idx then if mist.DBs.markList[e.idx] then mist.DBs.markList[e.idx].text = e.text end elseif world.event.S_EVENT_MARK_REMOVE == e.id and e.idx then if mist.DBs.markList[e.idx] then mist.DBs.markList[e.idx] = nil end end end local function getMarkId(id) if mist.DBs.markList[id] then return id else for mEntry, mData in pairs(mist.DBs.markList) do if id == mData.name or id == mData.id then return mData.id end end end end local function removeMark(id) --log:info("Removing Mark: $1", id local removed = false if type(id) == 'table' then for ind, val in pairs(id) do local r = getMarkId(val) if r then trigger.action.removeMark(r) mist.DBs.markList[r] = nil removed = true end end else local r = getMarkId(id) trigger.action.removeMark(r) mist.DBs.markList[r] = nil removed = true end return removed end world.addEventHandler(handle) function mist.marker.setDefault(vars) local anyChange = false if vars and type(vars) == 'table' then for l1, l1Data in pairs(vars) do if type(l1Data) == 'table' then if not userDefs[l1] then userDefs[l1] = {} end for l2, l2Data in pairs(l1Data) do userDefs[l1][l2] = l2Data anyChange = true end else userDefs[l1] = l1Data anyChange = true end end end return anyChange end function mist.marker.add(vars) --log:warn('markerFunc') --log:warn(vars) local pos = vars.point or vars.points or vars.pos local text = vars.text or '' local markFor = vars.markFor local markForCoa = vars.markForCoa or vars.coa -- optional, can be used if you just want to mark to a specific coa/all local id = vars.id or vars.markId or vars.markid local mType = vars.mType or vars.markType or vars.type or 0 local color = vars.color local fillColor = vars.fillColor local lineType = vars.lineType or 2 local readOnly = vars.readOnly or true local message = vars.message local fontSize = vars.fontSize local name = vars.name local radius = vars.radius or 500 local coa = -1 local usedId = 0 if id then if type(id) ~= 'number' then name = id usedId = iterate() end --log:info('checkIfIdExist: $1', id) --[[ Maybe it should treat id or name as the same thing/single value. If passed number it will use that as the first Id used and will delete/update any marks associated with that same value. ]] local lId = id or name if mist.DBs.markList[id] then ---------- NEED A BETTER WAY TO ASSOCIATE THE ID VALUE. CUrrnetly deleting from table and checking if that deleted entry exists which it wont. --log:warn('active mark to be removed: $1', id) name = mist.DBs.markList[id].name or id removeMark(id) elseif usedMarks[id] then --log:info('exists in usedMarks: $1', id) removeMark(usedMarks[id]) elseif name and usedMarks[name] then --log:info('exists in usedMarks: $1', name) removeMark(usedMarks[name]) end usedId = iterate() usedMarks[id] = usedId -- redefine the value used end if name then usedMarks[name] = usedId end if usedId == 0 then usedId = iterate() end if mType then if type(mType) == 'string' then for i = 1, #tNames do --log:warn(tNames[i]) if mist.stringMatch(mType, tNames[i]) then mType = i break end end elseif type(mType) == 'number' and mType > #tNames then mType = 0 end end --log:warn(mType) local markScope = 'all' local markForTable = {} if pos then if pos[1] then for i = 1, #pos do pos[i] = mist.utils.makeVec3(pos[i]) end else pos[1] = mist.utils.makeVec3(pos) end end if text and type(text) ~= string then text = tostring(text) end if markForCoa then if type(markForCoa) == 'string' then if tonumber(markForCoa) then coa = coas[tonumber(markForCoa)] markScope = 'coa' else for ind, cName in pairs(coas) do if mist.stringMatch(cName, markForCoa) then coa = ind markScope = 'coa' break end end end elseif type(markForCoa) == 'number' and markForCoa >=-1 and markForCoa <= #coas then coa = markForCoa markScore = 'coa' end elseif markFor then if type(markFor) == 'number' then -- groupId if mist.DBs.groupsById[markFor] then markScope = 'group' end elseif type(markFor) == 'string' then -- groupName if mist.DBs.groupsByName[markFor] then markScope = 'group' markFor = mist.DBs.groupsByName[markFor].groupId end elseif type(markFor) == 'table' then -- multiple groupName, country, coalition, all markScope = 'table' --log:warn(markFor) for forIndex, forData in pairs(markFor) do -- need to rethink this part and organization. Gotta be a more logical way to send messages to coa, groups, or all. for list, listData in pairs(forData) do --log:warn(listData) forIndex = string.lower(forIndex) if type(listData) == 'string' then listData = string.lower(listData) end if listData == 'all' then markScope = 'all' break elseif (forIndex == 'coa' or forIndex == 'ca') then -- mark for coa or CA. local matches = 0 for name, index in pairs (coalition.side) do if listData == string.lower(name) then markScope = 'coa' markFor = index coa = index matches = matches + 1 end end if matches > 1 then markScope = 'all' end elseif forIndex == 'countries' then for clienId, clientData in pairs(mist.DBs.humansById) do if (string.lower(clientData.country) == listData) or (forIndex == 'units' and string.lower(clientData.unitName) == listData) then markForTable = markSpamFilter(markForTable, clientData.groupId) end end elseif forIndex == 'unittypes' then -- mark to group -- iterate play units for clientId, clientData in pairs(mist.DBs.humansById) do for typeId, typeData in pairs(listData) do --log:warn(typeData) local found = false if list == 'all' or clientData.coalition and type(clientData.coalition) == 'string' and mist.stringMatch(clientData.coalition, list) then if mist.matchString(typeData, clientData.type) then found = true else -- check other known names for aircraft end end if found == true then markForTable = markSpamFilter(markForTable, clientData.groupId) -- sends info to other function to see if client is already recieving the current message. end for clientDataEntry, clientDataVal in pairs(clientData) do if type(clientDataVal) == 'string' then if mist.matchString(list, clientDataVal) == true or list == 'all' then local sString = typeData for rName, pTbl in pairs(typeBase) do -- just a quick check to see if the user may have meant something and got the specific type of the unit wrong for pIndex, pName in pairs(pTbl) do if mist.stringMatch(sString, pName) then sString = rName end end end if mist.stringMatch(sString, clientData.type) then found = true markForTable = markSpamFilter(markForTable, clientData.groupId) -- sends info oto other function to see if client is already recieving the current message. --table.insert(newMsgFor, clientId) end end end if found == true then -- shouldn't this be elsewhere too? break end end end end end end end end else markScope = 'all' end if mType == 0 then local data = {markId = usedId, text = text, pos = pos[1], markScope = markScope, markFor = markFor, markType = 'panel', name = name, time = timer.getTime()} if markScope ~= 'table' then -- create marks mist.DBs.markList[usedId] = data-- add to the DB else if #markForTable > 0 then --log:info('iterate') local list = {} if id and not name then name = id end for i = 1, #markForTable do local newId = iterate() local data = {markId = newId, text = text, pos = pos[i], markFor = markForTable[i], markType = 'panel', name = name, readOnly = readOnly, time = timer.getTime()} mist.DBs.markList[newId] = data table.insert(list, data) draw(data) end return list end end draw(data) return data elseif mType > 0 then local newId = iterate() local fCal = {} fCal[#fCal+1] = mType fCal[#fCal+1] = coa fCal[#fCal+1] = usedId local likeARainCoat = false if mType == 7 then local score = 0 for i = 1, #pos do if i < #pos then local val = ((pos[i+1].x - pos[i].x)*(pos[i+1].z + pos[i].z)) --log:warn("$1 index score is: $2", i, val) score = score + val else score = score + ((pos[1].x - pos[i].x)*(pos[1].z + pos[i].z)) end end --log:warn(score) if score > 0 then -- it is anti-clockwise. Due to DCS bug make it clockwise. likeARainCoat = true --log:warn('flip') for i = #pos, 1, -1 do fCal[#fCal+1] = pos[i] end end end if likeARainCoat == false then for i = 1, #pos do fCal[#fCal+1] = pos[i] end end if radius and mType == 2 then fCal[#fCal+1] = radius end if not color then color = checkDefs('color', coa) else color = validateColor(color) end fCal[#fCal+1] = color if not fillColor then fillColor = checkDefs('fillColor', coa) else fillColor = validateColor(fillColor) end fCal[#fCal+1] = fillColor if mType == 5 then -- text to all if not fontSize then fontSize = checkDefs('fontSize', coa) or 16 end fCal[#fCal+1] = fontSize else if not lineType then lineType = checkDefs('lineType', coa) or 2 end end fCal[#fCal+1] = lineType if not readOnly then readOnly = true end fCal[#fCal+1] = readOnly if mType == 5 then fCal[#fCal+1] = text else fCal[#fCal+1] = message end local data = {coa = coa, markId = usedId, pos = pos, markFor = markFor, color = color, readOnly = readOnly, message = message, fillColor = fillColor, lineType = lineType, markType = tNames[mType], name = name, radius = radius, text = text, fontSize = fontSize, time = timer.getTime()} mist.DBs.markList[usedId] = data if mType == 7 or mType == 1 then local s = "trigger.action.markupToAll(" for i = 1, #fCal do --log:warn(fCal[i]) if type(fCal[i]) == 'table' or type(fCal[i]) == 'boolean' then s = s .. mist.utils.oneLineSerialize(fCal[i]) else s = s .. fCal[i] end if i < #fCal then s = s .. ',' end end s = s .. ')' if name then usedMarks[name] = usedId end draw(s) else draw(data) end return data end end function mist.marker.remove(id) return removeMark(id) end function mist.marker.get(id) if mist.DBs.markList[id] then return mist.DBs.markList[id] end local names = {} for markId, data in pairs(mist.DBs.markList) do if data.name and data.name == id then table.insert(names, data) end end if #names >= 1 then return names end end function mist.marker.drawZone(name, v) if mist.DBs.zonesByName[name] then --log:warn(mist.DBs.zonesByName[name]) local vars = v or {} local ref = mist.utils.deepCopy(mist.DBs.zonesByName[name]) if ref.type == 2 then -- it is a quad, but use freeform cause it isnt as bugged vars.mType = 6 vars.point = ref.verticies else vars.mType = 2 vars.radius = ref.radius vars.point = ref.point end if not (vars.ignoreColor and vars.ignoreColor == true) and not vars.fillColor then vars.fillColor = ref.color end --log:warn(vars) return mist.marker.add(vars) end end function mist.marker.drawShape(name, v) if mist.DBs.drawingByName[name] then local d = v or {} local o = mist.utils.deepCopy(mist.DBs.drawingByName[name]) --mist.marker.add({point = {x = o.mapX, z = o.mapY}, text = name}) --log:warn(o) d.points = o.points or {} if o.primitiveType == "Polygon" then d.mType = 7 if o.polygonMode == "rect" then d.mType = 6 elseif o.polygonMode == "circle" then d.mType = 2 d.points = {x = o.mapX, y = o.mapY} d.radius = o.radius end elseif o.primitiveType == "TextBox" then d.mType = 5 d.points = {x = o.mapX, y = o.mapY} d.text = o.text or d.text d.fontSize = d.fontSize or o.fontSize end -- NOTE TO SELF. FIGURE OUT WHICH SHAPES NEED TO BE OFFSET. OVAL YES. if o.fillColorString and not d.fillColor then d.fillColor = mist.utils.hexToRGB(o.fillColorString) end if o.colorString then d.color = mist.utils.hexToRGB(o.colorString) end if o.thickness == 0 then d.lineType = 0 elseif o.style == 'solid' then d.lineType = 1 elseif o.style == 'dot' then d.lineType = 2 elseif o.style == 'dash' then d.lineType = 3 else d.lineType = 1 end if o.primitiveType == "Line" and #d.points >= 2 then d.mType = 1 local rtn = {} for i = 1, #d.points -1 do local var = mist.utils.deepCopy(d) var.points = {} var.points[1] = d.points[i] var.points[2] = d.points[i+1] table.insert(rtn, mist.marker.add(var)) end return rtn else if d.mType then --log:warn(d) return mist.marker.add(d) end end end end --[[ function mist.marker.circle(v) end ]] end --- Time conversion functions. -- @section mist.time do -- mist.time scope mist.time = {} -- returns a string for specified military time -- theTime is optional -- if present current time in mil time is returned -- if number or table the time is converted into mil tim function mist.time.convertToSec(timeTable) local timeInSec = 0 if timeTable and type(timeTable) == 'number' then timeInSec = timeTable elseif timeTable and type(timeTable) == 'table' and (timeTable.d or timeTable.h or timeTable.m or timeTable.s) then if timeTable.d and type(timeTable.d) == 'number' then timeInSec = timeInSec + (timeTable.d*86400) end if timeTable.h and type(timeTable.h) == 'number' then timeInSec = timeInSec + (timeTable.h*3600) end if timeTable.m and type(timeTable.m) == 'number' then timeInSec = timeInSec + (timeTable.m*60) end if timeTable.s and type(timeTable.s) == 'number' then timeInSec = timeInSec + timeTable.s end end return timeInSec end function mist.time.getDHMS(timeInSec) if timeInSec and type(timeInSec) == 'number' then local tbl = {d = 0, h = 0, m = 0, s = 0} if timeInSec > 86400 then while timeInSec > 86400 do tbl.d = tbl.d + 1 timeInSec = timeInSec - 86400 end end if timeInSec > 3600 then while timeInSec > 3600 do tbl.h = tbl.h + 1 timeInSec = timeInSec - 3600 end end if timeInSec > 60 then while timeInSec > 60 do tbl.m = tbl.m + 1 timeInSec = timeInSec - 60 end end tbl.s = timeInSec return tbl else log:error("Didn't recieve number") return end end function mist.getMilString(theTime) local timeInSec = 0 if theTime then timeInSec = mist.time.convertToSec(theTime) else timeInSec = mist.utils.round(timer.getAbsTime(), 0) end local DHMS = mist.time.getDHMS(timeInSec) return tostring(string.format('%02d', DHMS.h) .. string.format('%02d',DHMS.m)) end function mist.getClockString(theTime, hour) local timeInSec = 0 if theTime then timeInSec = mist.time.convertToSec(theTime) else timeInSec = mist.utils.round(timer.getAbsTime(), 0) end local DHMS = mist.time.getDHMS(timeInSec) if hour then if DHMS.h > 12 then DHMS.h = DHMS.h - 12 return tostring(string.format('%02d', DHMS.h) .. ':' .. string.format('%02d',DHMS.m) .. ':' .. string.format('%02d',DHMS.s) .. ' PM') else return tostring(string.format('%02d', DHMS.h) .. ':' .. string.format('%02d',DHMS.m) .. ':' .. string.format('%02d',DHMS.s) .. ' AM') end else return tostring(string.format('%02d', DHMS.h) .. ':' .. string.format('%02d',DHMS.m) .. ':' .. string.format('%02d',DHMS.s)) end end -- returns the date in string format -- both variables optional -- first val returns with the month as a string -- 2nd val defins if it should be written the American way or the wrong way. function mist.time.getDate(convert) local cal = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} -- local date = {} if not env.mission.date then -- Not likely to happen. Resaving mission auto updates this to remove it. date.d = 0 date.m = 6 date.y = 2011 else date.d = env.mission.date.Day date.m = env.mission.date.Month date.y = env.mission.date.Year end local start = 86400 local timeInSec = mist.utils.round(timer.getAbsTime()) if convert and type(convert) == 'number' then timeInSec = convert end if timeInSec > 86400 then while start < timeInSec do if date.d >= cal[date.m] then if date.m == 2 and date.d == 28 then -- HOLY COW we can edit years now. Gotta re-add this! if date.y % 4 == 0 and date.y % 100 == 0 and date.y % 400 ~= 0 or date.y % 4 > 0 then date.m = date.m + 1 date.d = 0 end --date.d = 29 else date.m = date.m + 1 date.d = 0 end end if date.m == 13 then date.m = 1 date.y = date.y + 1 end date.d = date.d + 1 start = start + 86400 end end return date end function mist.time.relativeToStart(time) if type(time) == 'number' then return time - timer.getTime0() end end function mist.getDateString(rtnType, murica, oTime) -- returns date based on time local word = {'January', 'Feburary', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' } -- 'etc local curTime = 0 if oTime then curTime = oTime else curTime = mist.utils.round(timer.getAbsTime()) end local tbl = mist.time.getDate(curTime) if rtnType then if murica then return tostring(word[tbl.m] .. ' ' .. tbl.d .. ' ' .. tbl.y) else return tostring(tbl.d .. ' ' .. word[tbl.m] .. ' ' .. tbl.y) end else if murica then return tostring(tbl.m .. '.' .. tbl.d .. '.' .. tbl.y) else return tostring(tbl.d .. '.' .. tbl.m .. '.' .. tbl.y) end end end --WIP function mist.time.milToGame(milString, rtnType) --converts a military time. By default returns the abosolute time that event would occur. With optional value it returns how many seconds from time of call till that time. local curTime = mist.utils.round(timer.getAbsTime()) local milTimeInSec = 0 if milString and type(milString) == 'string' and string.len(milString) >= 4 then local hr = tonumber(string.sub(milString, 1, 2)) local mi = tonumber(string.sub(milString, 3)) milTimeInSec = milTimeInSec + (mi*60) + (hr*3600) elseif milString and type(milString) == 'table' and (milString.d or milString.h or milString.m or milString.s) then milTimeInSec = mist.time.convertToSec(milString) end local startTime = timer.getTime0() local daysOffset = 0 if startTime > 86400 then daysOffset = mist.utils.round(startTime/86400) if daysOffset > 0 then milTimeInSec = milTimeInSec *daysOffset end end if curTime > milTimeInSec then milTimeInSec = milTimeInSec + 86400 end if rtnType then milTimeInSec = milTimeInSec - startTime end return milTimeInSec end end --- Group task functions. -- @section tasks do -- group tasks scope mist.ground = {} mist.fixedWing = {} mist.heli = {} mist.air = {} mist.air.fixedWing = {} mist.air.heli = {} mist.ship = {} --- Tasks group to follow a route. -- This sets the mission task for the given group. -- Any wrapped actions inside the path (like enroute -- tasks) will be executed. -- @tparam Group group group to task. -- @tparam table path containing -- points defining a route. function mist.goRoute(group, path) local misTask = { id = 'Mission', params = { route = { points = mist.utils.deepCopy(path), }, }, } if type(group) == 'string' then group = Group.getByName(group) end if group then local groupCon = group:getController() if groupCon then --log:warn(misTask) groupCon:setTask(misTask) return true end end return false end -- same as getGroupPoints but returns speed and formation type along with vec2 of point} function mist.getGroupRoute(groupIdent, task) -- refactor to search by groupId and allow groupId and groupName as inputs local gpId = groupIdent if mist.DBs.MEgroupsByName[groupIdent] then gpId = mist.DBs.MEgroupsByName[groupIdent].groupId else log:error('$1 not found in mist.DBs.MEgroupsByName', groupIdent) end for coa_name, coa_data in pairs(env.mission.coalition) do if type(coa_data) == 'table' then if coa_data.country then --there is a country table for cntry_id, cntry_data in pairs(coa_data.country) do for obj_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 local routeData = {} if env.mission.version > 7 and env.mission.version < 19 then routeData.name = env.getValueDictByKey(point.name) else routeData.name = point.name end if not point.point then routeData.x = point.x routeData.y = point.y else routeData.point = point.point --it's possible that the ME could move to the point = Vec2 notation. end routeData.form = point.action routeData.speed = point.speed routeData.alt = point.alt routeData.alt_type = point.alt_type routeData.airdromeId = point.airdromeId routeData.helipadId = point.helipadId routeData.type = point.type routeData.action = point.action if task then routeData.task = point.task end points[point_num] = routeData end return points end log:error('Group route not defined in mission editor for groupId: $1', gpId) return end --if group_data and group_data.name and group_data.name == 'groupname' end --for group_num, group_data in pairs(obj_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 -- function mist.ground.buildPath() end -- ???? function mist.ground.patrolRoute(vars) --log:info('patrol') local tempRoute = {} local useRoute = {} local gpData = vars.gpData if type(gpData) == 'string' then gpData = Group.getByName(gpData) end local useGroupRoute if not vars.useGroupRoute then useGroupRoute = vars.gpData else useGroupRoute = vars.useGroupRoute end local routeProvided = false if not vars.route then if useGroupRoute then tempRoute = mist.getGroupRoute(useGroupRoute) end else useRoute = vars.route local posStart = mist.getLeadPos(gpData) useRoute[1] = mist.ground.buildWP(posStart, useRoute[1].action, useRoute[1].speed) routeProvided = true end local overRideSpeed = vars.speed or 'default' local pType = vars.pType local offRoadForm = vars.offRoadForm or 'default' local onRoadForm = vars.onRoadForm or 'default' if routeProvided == false and #tempRoute > 0 then local posStart = mist.getLeadPos(gpData) useRoute[#useRoute + 1] = mist.ground.buildWP(posStart, offRoadForm, overRideSpeed) for i = 1, #tempRoute do local tempForm = tempRoute[i].action local tempSpeed = tempRoute[i].speed if offRoadForm == 'default' then tempForm = tempRoute[i].action end if onRoadForm == 'default' then onRoadForm = 'On Road' end if (string.lower(tempRoute[i].action) == 'on road' or string.lower(tempRoute[i].action) == 'onroad' or string.lower(tempRoute[i].action) == 'on_road') then tempForm = onRoadForm else tempForm = offRoadForm end if type(overRideSpeed) == 'number' then tempSpeed = overRideSpeed end useRoute[#useRoute + 1] = mist.ground.buildWP(tempRoute[i], tempForm, tempSpeed) end if pType and string.lower(pType) == 'doubleback' then local curRoute = mist.utils.deepCopy(useRoute) for i = #curRoute, 2, -1 do useRoute[#useRoute + 1] = mist.ground.buildWP(curRoute[i], curRoute[i].action, curRoute[i].speed) end end useRoute[1].action = useRoute[#useRoute].action -- make it so the first WP matches the last WP end local cTask3 = {} local newPatrol = {} newPatrol.route = useRoute newPatrol.gpData = gpData:getName() cTask3[#cTask3 + 1] = 'mist.ground.patrolRoute(' cTask3[#cTask3 + 1] = mist.utils.oneLineSerialize(newPatrol) cTask3[#cTask3 + 1] = ')' cTask3 = table.concat(cTask3) local tempTask = { id = 'WrappedAction', params = { action = { id = 'Script', params = { command = cTask3, }, }, }, } useRoute[#useRoute].task = tempTask log:info(useRoute) mist.goRoute(gpData, useRoute) return end function mist.ground.patrol(gpData, pType, form, speed) local vars = {} if type(gpData) == 'table' and gpData:getName() then gpData = gpData:getName() end vars.useGroupRoute = gpData vars.gpData = gpData vars.pType = pType vars.offRoadForm = form vars.speed = speed mist.ground.patrolRoute(vars) return end -- No longer accepts path function mist.ground.buildWP(point, overRideForm, overRideSpeed) local wp = {} wp.x = point.x if point.z then wp.y = point.z else wp.y = point.y end local form, speed if point.speed and not overRideSpeed then wp.speed = point.speed elseif type(overRideSpeed) == 'number' then wp.speed = overRideSpeed else wp.speed = mist.utils.kmphToMps(20) end if point.form and not overRideForm then form = point.form else form = overRideForm end if not form then wp.action = 'Cone' else form = string.lower(form) if form == 'off_road' or form == 'off road' then wp.action = 'Off Road' elseif form == 'on_road' or form == 'on road' then wp.action = 'On Road' elseif form == 'rank' or form == 'line_abrest' or form == 'line abrest' or form == 'lineabrest'then wp.action = 'Rank' elseif form == 'cone' then wp.action = 'Cone' elseif form == 'diamond' then wp.action = 'Diamond' elseif form == 'vee' then wp.action = 'Vee' elseif form == 'echelon_left' or form == 'echelon left' or form == 'echelonl' then wp.action = 'EchelonL' elseif form == 'echelon_right' or form == 'echelon right' or form == 'echelonr' then wp.action = 'EchelonR' else wp.action = 'Cone' -- if nothing matched end end wp.type = 'Turning Point' return wp end function mist.fixedWing.buildWP(point, WPtype, speed, alt, altType) local wp = {} wp.x = point.x if point.z then wp.y = point.z else wp.y = point.y end if alt and type(alt) == 'number' then wp.alt = alt else wp.alt = 2000 end if altType then altType = string.lower(altType) if altType == 'radio' or altType == 'agl' then wp.alt_type = 'RADIO' elseif altType == 'baro' or altType == 'asl' then wp.alt_type = 'BARO' end else wp.alt_type = 'RADIO' end if point.speed then speed = point.speed end if point.type then WPtype = point.type end if not speed then wp.speed = mist.utils.kmphToMps(500) else wp.speed = speed end if not WPtype then wp.action = 'Turning Point' else WPtype = string.lower(WPtype) if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then wp.action = 'Fly Over Point' elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then wp.action = 'Turning Point' else wp.action = 'Turning Point' end end wp.type = 'Turning Point' return wp end function mist.heli.buildWP(point, WPtype, speed, alt, altType) local wp = {} wp.x = point.x if point.z then wp.y = point.z else wp.y = point.y end if alt and type(alt) == 'number' then wp.alt = alt else wp.alt = 500 end if altType then altType = string.lower(altType) if altType == 'radio' or altType == 'agl' then wp.alt_type = 'RADIO' elseif altType == 'baro' or altType == 'asl' then wp.alt_type = 'BARO' end else wp.alt_type = 'RADIO' end if point.speed then speed = point.speed end if point.type then WPtype = point.type end if not speed then wp.speed = mist.utils.kmphToMps(200) else wp.speed = speed end if not WPtype then wp.action = 'Turning Point' else WPtype = string.lower(WPtype) if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then wp.action = 'Fly Over Point' elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then wp.action = 'Turning Point' else wp.action = 'Turning Point' end end wp.type = 'Turning Point' return wp end -- need to return a Vec3 or Vec2? function mist.getRandPointInCircle(p, r, innerRadius, maxA, minA) local point = mist.utils.makeVec3(p) local theta = 2*math.pi*math.random() local radius = r or 1000 local minR = innerRadius or 0 if maxA and not minA then theta = math.rad(math.random(0, maxA - math.random())) elseif maxA and minA then if minA < maxA then theta = math.rad(math.random(minA, maxA) - math.random()) else theta = math.rad(math.random(maxA, minA) - math.random()) end end local rad = math.random() + math.random() if rad > 1 then rad = 2 - rad end local radMult if minR and minR <= radius then --radMult = (radius - innerRadius)*rad + innerRadius radMult = radius * math.sqrt((minR^2 + (radius^2 - minR^2) * math.random()) / radius^2) else radMult = radius*rad end local rndCoord if radius > 0 then rndCoord = {x = math.cos(theta)*radMult + point.x, y = math.sin(theta)*radMult + point.z} else rndCoord = {x = point.x, y = point.z} end return rndCoord end function mist.getRandomPointInZone(zoneName, innerRadius, maxA, minA) if type(zoneName) == 'string' then local zone = mist.DBs.zonesByName[zoneName] if zone.type and zone.type == 2 then return mist.getRandomPointInPoly(zone.verticies) else return mist.getRandPointInCircle(zone.point, zone.radius, innerRadius, maxA, minA) end end return false end function mist.getRandomPointInPoly(zone) --env.info('Zone Size: '.. #zone) local avg = mist.getAvgPoint(zone) --log:warn(avg) local radius = 0 local minR = math.huge local newCoord = {} for i = 1, #zone do if mist.utils.get2DDist(avg, zone[i]) > radius then radius = mist.utils.get2DDist(avg, zone[i]) end if mist.utils.get2DDist(avg, zone[i]) < minR then minR = mist.utils.get2DDist(avg, zone[i]) end end --log:warn('Radius: $1', radius) --log:warn('minR: $1', minR) local lSpawnPos = {} for j = 1, 100 do newCoord = mist.getRandPointInCircle(avg, radius) if mist.pointInPolygon(newCoord, zone) then break end if j == 100 then newCoord = mist.getRandPointInCircle(avg, 50000) log:warn("Failed to find point in poly; Giving random point from center of the poly") end end return newCoord end function mist.getWindBearingAndVel(p) local point = mist.utils.makeVec3(o) local gLevel = land.getHeight({x = point.x, y = point.z}) if point.y <= gLevel then point.y = gLevel + 10 end local t = atmosphere.getWind(point) local bearing = math.tan(t.z/t.x) local vel = math.sqrt(t.x^2 + t.z^2) return bearing, vel end function mist.groupToRandomPoint(vars) local group = vars.group --Required local point = vars.point --required local radius = vars.radius or 0 local innerRadius = vars.innerRadius local form = vars.form or 'Cone' local heading = vars.heading or math.random()*2*math.pi local headingDegrees = vars.headingDegrees local speed = vars.speed or mist.utils.kmphToMps(20) local useRoads if not vars.disableRoads then useRoads = true else useRoads = false end local path = {} if headingDegrees then heading = headingDegrees*math.pi/180 end if heading >= 2*math.pi then heading = heading - 2*math.pi end local rndCoord = mist.getRandPointInCircle(point, radius, innerRadius) local offset = {} local posStart = mist.getLeadPos(group) if posStart then offset.x = mist.utils.round(math.sin(heading - (math.pi/2)) * 50 + rndCoord.x, 3) offset.z = mist.utils.round(math.cos(heading + (math.pi/2)) * 50 + rndCoord.y, 3) path[#path + 1] = mist.ground.buildWP(posStart, form, speed) if useRoads == true and ((point.x - posStart.x)^2 + (point.z - posStart.z)^2)^0.5 > radius * 1.3 then path[#path + 1] = mist.ground.buildWP({x = posStart.x + 11, z = posStart.z + 11}, 'off_road', speed) path[#path + 1] = mist.ground.buildWP(posStart, 'on_road', speed) path[#path + 1] = mist.ground.buildWP(offset, 'on_road', speed) else path[#path + 1] = mist.ground.buildWP({x = posStart.x + 25, z = posStart.z + 25}, form, speed) end end path[#path + 1] = mist.ground.buildWP(offset, form, speed) path[#path + 1] = mist.ground.buildWP(rndCoord, form, speed) mist.goRoute(group, path) return end function mist.groupRandomDistSelf(gpData, dist, form, heading, speed, disableRoads) local pos = mist.getLeadPos(gpData) local fakeZone = {} fakeZone.radius = dist or math.random(300, 1000) fakeZone.point = {x = pos.x, y = pos.y, z = pos.z} mist.groupToRandomZone(gpData, fakeZone, form, heading, speed, disableRoads) return end function mist.groupToRandomZone(gpData, zone, form, heading, speed, disableRoads) if type(gpData) == 'string' then gpData = Group.getByName(gpData) end if type(zone) == 'string' then zone = mist.DBs.zonesByName[zone] elseif type(zone) == 'table' and not zone.radius then zone = mist.DBs.zonesByName[zone[math.random(1, #zone)]] end if speed then speed = mist.utils.kmphToMps(speed) end local vars = {} vars.group = gpData vars.radius = zone.radius vars.form = form vars.headingDegrees = heading vars.speed = speed vars.point = mist.utils.zoneToVec3(zone) vars.disableRoads = disableRoads mist.groupToRandomPoint(vars) return end function mist.isTerrainValid(coord, terrainTypes) -- vec2/3 and enum or table of acceptable terrain types if coord.z then coord.y = coord.z end local typeConverted = {} if type(terrainTypes) == 'string' then -- if its a string it does this check for constId, constData in pairs(land.SurfaceType) do if string.lower(constId) == string.lower(terrainTypes) or string.lower(constData) == string.lower(terrainTypes) then table.insert(typeConverted, constId) end end elseif type(terrainTypes) == 'table' then -- if its a table it does this check for typeId, typeData in pairs(terrainTypes) do for constId, constData in pairs(land.SurfaceType) do if string.lower(constId) == string.lower(typeData) or string.lower(constData) == string.lower(typeData) then table.insert(typeConverted, constId) end end end end for validIndex, validData in pairs(typeConverted) do if land.getSurfaceType(coord) == land.SurfaceType[validData] then log:info('Surface is : $1', validData) return true end end return false end function mist.terrainHeightDiff(coord, searchSize) local samples = {} local searchRadius = 5 if searchSize then searchRadius = searchSize end if type(coord) == 'string' then coord = mist.utils.zoneToVec3(coord) end coord = mist.utils.makeVec2(coord) samples[#samples + 1] = land.getHeight(coord) for i = 0, 360, 30 do samples[#samples + 1] = land.getHeight({x = (coord.x + (math.sin(math.rad(i))*searchRadius)), y = (coord.y + (math.cos(math.rad(i))*searchRadius))}) if searchRadius >= 20 then -- if search radius is sorta large, take a sample halfway between center and outer edge samples[#samples + 1] = land.getHeight({x = (coord.x + (math.sin(math.rad(i))*(searchRadius/2))), y = (coord.y + (math.cos(math.rad(i))*(searchRadius/2)))}) end end local tMax, tMin = 0, 1000000 for index, height in pairs(samples) do if height > tMax then tMax = height end if height < tMin then tMin = height end end return mist.utils.round(tMax - tMin, 2) end function mist.groupToPoint(gpData, point, form, heading, speed, useRoads) if type(point) == 'string' then point = mist.DBs.zonesByName[point] end if speed then speed = mist.utils.kmphToMps(speed) end local vars = {} vars.group = gpData vars.form = form vars.headingDegrees = heading vars.speed = speed vars.disableRoads = useRoads vars.point = mist.utils.zoneToVec3(point) mist.groupToRandomPoint(vars) return end function mist.getLeadPos(group) if type(group) == 'string' then -- group name group = Group.getByName(group) end local units = group:getUnits() local leader = units[1] if Unit.getLife(leader) == 0 or not Unit.isExist(leader) then -- SHOULD be good, but if there is a bug, this code future-proofs it then. local lowestInd = math.huge for ind, unit in pairs(units) do if Unit.isExist(unit) and ind < lowestInd then lowestInd = ind return unit:getPosition().p end end end if leader and Unit.isExist(leader) then -- maybe a little too paranoid now... return leader:getPosition().p end end function mist.groupIsDead(groupName) -- copy more or less from on station if Group.getByName(groupName) then local gp = Group.getByName(groupName) if #gp:getUnits() > 0 or gp:isExist() == true then return false end end return true end end --- Database tables. -- @section mist.DBs --- Mission data -- @table mist.DBs.missionData -- @field startTime mission start time -- @field theatre mission theatre/map e.g. Caucasus -- @field version mission version -- @field files mission resources --- Tables used as parameters. -- @section varTables --- mist.flagFunc.units_in_polygon parameter table. -- @table unitsInPolygonVars -- @tfield table unit name table @{UnitNameTable}. -- @tfield table zone table defining a polygon. -- @tfield number|string flag flag to set to true. -- @tfield[opt] number|string stopflag if set to true the function -- will stop evaluating. -- @tfield[opt] number maxalt maximum altitude (MSL) for the -- polygon. -- @tfield[opt] number req_num minimum number of units that have -- to be in the polygon. -- @tfield[opt] number interval sets the interval for -- checking if units are inside of the polygon in seconds. Default: 1. -- @tfield[opt] boolean toggle switch the flag to false if required -- conditions are not met. Default: false. -- @tfield[opt] table unitTableDef --- Logger class. -- @type mist.Logger do -- mist.Logger scope mist.Logger = {} --- parses text and substitutes keywords with values from given array. -- @param text string containing keywords to substitute with values -- or a variable. -- @param ... variables to use for substitution in string. -- @treturn string new string with keywords substituted or -- value of variable as string. local function formatText(text, ...) if type(text) ~= 'string' then if type(text) == 'table' then text = mist.utils.oneLineSerialize(text) else text = tostring(text) end else for index,value in ipairs(arg) do -- TODO: check for getmetatabel(value).__tostring if type(value) == 'table' then value = mist.utils.oneLineSerialize(value) else value = tostring(value) end text = text:gsub('$' .. index, value) end end local fName = nil local cLine = nil if debug then local dInfo = debug.getinfo(3) fName = dInfo.name cLine = dInfo.currentline -- local fsrc = dinfo.short_src --local fLine = dInfo.linedefined end if fName and cLine then return fName .. '|' .. cLine .. ': ' .. text elseif cLine then return cLine .. ': ' .. text else return ' ' .. text end end local function splitText(text) local tbl = {} while text:len() > 4000 do local sub = text:sub(1, 4000) text = text:sub(4001) table.insert(tbl, sub) end table.insert(tbl, text) return tbl end --- Creates a new logger. -- Each logger has it's own tag and log level. -- @tparam string tag tag which appears at the start of -- every log line produced by this logger. -- @tparam[opt] number|string level the log level defines which messages -- will be logged and which will be omitted. Log level 3 beeing the most verbose -- and 0 disabling all output. This can also be a string. Allowed strings are: -- "none" (0), "error" (1), "warning" (2) and "info" (3). -- @usage myLogger = mist.Logger:new("MyScript") -- @usage myLogger = mist.Logger:new("MyScript", 2) -- @usage myLogger = mist.Logger:new("MyScript", "info") -- @treturn mist.Logger function mist.Logger:new(tag, level) local l = {tag = tag} setmetatable(l, self) self.__index = self l:setLevel(level) return l end --- Sets the level of verbosity for this logger. -- @tparam[opt] number|string level the log level defines which messages -- will be logged and which will be omitted. Log level 3 beeing the most verbose -- and 0 disabling all output. This can also[ be a string. Allowed strings are: -- "none" (0), "error" (1), "warning" (2) and "info" (3). -- @usage myLogger:setLevel("info") -- @usage -- log everything --myLogger:setLevel(3) function mist.Logger:setLevel(level) if not level then self.level = 2 else if type(level) == 'string' then if level == 'none' or level == 'off' then self.level = 0 elseif level == 'error' then self.level = 1 elseif level == 'warning' or level == 'warn' then self.level = 2 elseif level == 'info' then self.level = 3 end elseif type(level) == 'number' then self.level = level else self.level = 2 end end end --- Logs error and shows alert window. -- This logs an error to the dcs.log and shows a popup window, -- pausing the simulation. This works always even if logging is -- disabled by setting a log level of "none" or 0. -- @tparam string text the text with keywords to substitute. -- @param ... variables to be used for substitution. -- @usage myLogger:alert("Shit just hit the fan! WEEEE!!!11") function mist.Logger:alert(text, ...) text = formatText(text, unpack(arg)) if text:len() > 4000 then local texts = splitText(text) for i = 1, #texts do if i == 1 then env.error(self.tag .. '|' .. texts[i], true) else env.error(texts[i]) end end else env.error(self.tag .. '|' .. text, true) end end --- Logs a message, disregarding the log level. -- @tparam string text the text with keywords to substitute. -- @param ... variables to be used for substitution. -- @usage myLogger:msg("Always logged!") function mist.Logger:msg(text, ...) text = formatText(text, unpack(arg)) if text:len() > 4000 then local texts = splitText(text) for i = 1, #texts do if i == 1 then env.info(self.tag .. '|' .. texts[i]) else env.info(texts[i]) end end else env.info(self.tag .. '|' .. text) end end --- Logs an error. -- logs a message prefixed with this loggers tag to dcs.log as -- long as at least the "error" log level (1) is set. -- @tparam string text the text with keywords to substitute. -- @param ... variables to be used for substitution. -- @usage myLogger:error("Just an error!") -- @usage myLogger:error("Foo is $1 instead of $2", foo, "bar") function mist.Logger:error(text, ...) if self.level >= 1 then text = formatText(text, unpack(arg)) if text:len() > 4000 then local texts = splitText(text) for i = 1, #texts do if i == 1 then env.error(self.tag .. '|' .. texts[i]) else env.error(texts[i]) end end else env.error(self.tag .. '|' .. text, mistSettings.errorPopup) end end end --- Logs a warning. -- logs a message prefixed with this loggers tag to dcs.log as -- long as at least the "warning" log level (2) is set. -- @tparam string text the text with keywords to substitute. -- @param ... variables to be used for substitution. -- @usage myLogger:warn("Mother warned you! Those $1 from the interwebs are $2", {"geeks", 1337}) function mist.Logger:warn(text, ...) if self.level >= 2 then text = formatText(text, unpack(arg)) if text:len() > 4000 then local texts = splitText(text) for i = 1, #texts do if i == 1 then env.warning(self.tag .. '|' .. texts[i]) else env.warning(texts[i]) end end else env.warning(self.tag .. '|' .. text, mistSettings.warnPopup) end end end --- Logs a info. -- logs a message prefixed with this loggers tag to dcs.log as -- long as the highest log level (3) "info" is set. -- @tparam string text the text with keywords to substitute. -- @param ... variables to be used for substitution. -- @see warn function mist.Logger:info(text, ...) if self.level >= 3 then text = formatText(text, unpack(arg)) if text:len() > 4000 then local texts = splitText(text) for i = 1, #texts do if i == 1 then env.info(self.tag .. '|' .. texts[i]) else env.info(texts[i]) end end else env.info(self.tag .. '|' .. text, mistSettings.infoPopup) end end end end -- initialize mist mist.init() env.info(('Mist version ' .. mist.majorVersion .. '.' .. mist.minorVersion .. '.' .. mist.build .. ' loaded.')) -- vim: noet:ts=2:sw=2 ================================================ FILE: demo-missions/moose_a2a_connector/skynet-and-moose-a2a-dispatcher-setup.lua ================================================ do --Setup Syknet IADS: redIADS = SkynetIADS:create('Enemy IADS') local iadsDebug = redIADS:getDebugSettings() iadsDebug.IADSStatus = true iadsDebug.contacts = true --[[ iadsDebug.radarWentDark = true iadsDebug.radarWentLive = true iadsDebug.ewRadarNoConnection = true iadsDebug.samNoConnection = true iadsDebug.jammerProbability = true iadsDebug.addedEWRadar = true iadsDebug.hasNoPower = true iadsDebug.addedSAMSite = true iadsDebug.warnings = true iadsDebug.harmDefence = true iadsDebug.samSiteStatusEnvOutput = true iadsDebug.earlyWarningRadarStatusEnvOutput = true --]] redIADS:addSAMSitesByPrefix('SAM') local power = StaticObject.getByName('power-source') redIADS:addEarlyWarningRadarsByPrefix('EW') redIADS:getEarlyWarningRadarByUnitName('EW-1'):addConnectionNode(power) redIADS:activate() -- Define a SET_GROUP object that builds a collection of groups that define the EWR network. DetectionSetGroup = SET_GROUP:New() -- add the MOOSE SET_GROUP to the Skynet IADS, from now on Skynet will update active radars that the MOOSE SET_GROUP can use for EW detection. redIADS:addMooseSetGroup(DetectionSetGroup) -- 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() --test to see which groups are added and removed to the SET_GROUP at runtime by Skynet: function outputNames() env.info("IADS Radar Groups added by Skynet:") env.info(DetectionSetGroup:GetObjectNames()) end mist.scheduleFunction(outputNames, self, 1, 2) --end test end ================================================ FILE: demo-missions/skynet-iads-compiled.lua ================================================ env.info("--- SKYNET VERSION: 3.3.0 | BUILD TIME: 29.12.2023 2311Z ---") do --this file contains the required units per sam type samTypesDB = { ['S-200'] = { ['type'] = 'complex', ['searchRadar'] = { ['RLS_19J6'] = { ['name'] = { ['NATO'] = 'Tin Shield', }, }, ['p-19 s-125 sr'] = { ['name'] = { ['NATO'] = 'Flat Face', }, }, }, ['EWR P-37 BAR LOCK'] = { ['Name'] = { ['NATO'] = "Bar lock", }, }, ['trackingRadar'] = { ['RPC_5N62V'] = { }, }, ['launchers'] = { ['S-200_Launcher'] = { }, }, ['name'] = { ['NATO'] = 'SA-5 Gammon', }, ['harm_detection_chance'] = 60 }, ['S-300'] = { ['type'] = 'complex', ['searchRadar'] = { ['S-300PS 40B6MD sr'] = { ['name'] = { ['NATO'] = 'Clam Shell', }, }, ['S-300PS 64H6E sr'] = { ['name'] = { ['NATO'] = 'Big Bird', }, }, ['S-300PS 40B6MD sr_19J6'] = { ['name'] = { ['NATO'] = 'Tin Shield', }, } }, ['trackingRadar'] = { ['S-300PS 40B6M tr'] = { }, ['S-300PS 5H63C 30H6_tr'] = { }, }, ['launchers'] = { ['S-300PS 5P85D ln'] = { }, ['S-300PS 5P85C ln'] = { }, }, ['misc'] = { ['S-300PS 54K6 cp'] = { ['required'] = true, }, }, ['name'] = { ['NATO'] = 'SA-10 Grumble', }, ['harm_detection_chance'] = 90, ['can_engage_harm'] = true }, ['Buk'] = { ['type'] = 'complex', ['searchRadar'] = { ['SA-11 Buk SR 9S18M1'] = { ['name'] = { ['NATO'] = 'Snow Drift', }, }, }, ['launchers'] = { ['SA-11 Buk LN 9A310M1'] = { }, }, ['misc'] = { ['SA-11 Buk CC 9S470M1'] = { ['required'] = true, }, }, ['name'] = { ['NATO'] = 'SA-11 Gadfly', }, ['harm_detection_chance'] = 70 }, ['S-125'] = { ['type'] = 'complex', ['searchRadar'] = { ['p-19 s-125 sr'] = { ['name'] = { ['NATO'] = 'Flat Face', }, }, }, ['trackingRadar'] = { ['snr s-125 tr'] = { }, }, ['launchers'] = { ['5p73 s-125 ln'] = { }, }, ['name'] = { ['NATO'] = 'SA-3 Goa', }, ['harm_detection_chance'] = 30 }, ['S-75'] = { ['type'] = 'complex', ['searchRadar'] = { ['p-19 s-125 sr'] = { ['name'] = { ['NATO'] = 'Flat Face', }, }, }, ['trackingRadar'] = { ['SNR_75V'] = { }, }, ['launchers'] = { ['S_75M_Volhov'] = { }, }, ['name'] = { ['NATO'] = 'SA-2 Guideline', }, ['harm_detection_chance'] = 30 }, ['Kub'] = { ['type'] = 'complex', ['searchRadar'] = { ['Kub 1S91 str'] = { ['name'] = { ['NATO'] = 'Straight Flush', }, }, }, ['launchers'] = { ['Kub 2P25 ln'] = { }, }, ['name'] = { ['NATO'] = 'SA-6 Gainful', }, ['harm_detection_chance'] = 40 }, ['Patriot'] = { ['type'] = 'complex', ['searchRadar'] = { ['Patriot str'] = { ['name'] = { ['NATO'] = 'Patriot str', }, }, }, ['launchers'] = { ['Patriot ln'] = { }, }, ['misc'] = { ['Patriot cp'] = { ['required'] = false, }, ['Patriot EPP'] = { ['required'] = false, }, ['Patriot ECS'] = { ['required'] = true, }, ['Patriot AMG'] = { ['required'] = false, }, }, ['name'] = { ['NATO'] = 'Patriot', }, ['harm_detection_chance'] = 90, ['can_engage_harm'] = true }, ['Hawk'] = { ['type'] = 'complex', ['searchRadar'] = { ['Hawk sr'] = { ['name'] = { ['NATO'] = 'Hawk str', }, }, }, ['trackingRadar'] = { ['Hawk tr'] = { }, }, ['launchers'] = { ['Hawk ln'] = { }, }, ['name'] = { ['NATO'] = 'Hawk', }, ['harm_detection_chance'] = 40 }, ['Roland ADS'] = { ['type'] = 'complex', ['searchRadar'] = { ['Roland Radar'] = { ['name'] = { ['NATO'] = 'Roland EWR', }, }, }, ['launchers'] = { ['Roland ADS'] = { }, }, ['name'] = { ['NATO'] = 'Roland ADS', }, ['harm_detection_chance'] = 60 }, ['NASAMS'] = { ['type'] = 'complex', ['searchRadar'] = { ['NASAMS_Radar_MPQ64F1'] = { }, }, ['launchers'] = { ['NASAMS_LN_B'] = { }, ['NASAMS_LN_C'] = { }, }, ['name'] = { ['NATO'] = 'NASAMS', }, ['misc'] = { ['NASAMS_Command_Post'] = { ['required'] = false, }, }, ['can_engage_harm'] = true, ['harm_detection_chance'] = 90 }, ['2S6 Tunguska'] = { ['type'] = 'single', ['searchRadar'] = { ['2S6 Tunguska'] = { }, }, ['launchers'] = { ['2S6 Tunguska'] = { }, }, ['name'] = { ['NATO'] = 'SA-19 Grison', }, }, ['Osa'] = { ['type'] = 'single', ['searchRadar'] = { ['Osa 9A33 ln'] = { }, }, ['launchers'] = { ['Osa 9A33 ln'] = { }, }, ['name'] = { ['NATO'] = 'SA-8 Gecko', }, ['harm_detection_chance'] = 20 }, ['Strela-10M3'] = { ['type'] = 'single', ['searchRadar'] = { ['Strela-10M3'] = { ['trackingRadar'] = true, }, }, ['launchers'] = { ['Strela-10M3'] = { }, }, ['name'] = { ['NATO'] = 'SA-13 Gopher', }, }, ['Strela-1 9P31'] = { ['type'] = 'single', ['searchRadar'] = { ['Strela-1 9P31'] = { }, }, ['launchers'] = { ['Strela-1 9P31'] = { }, }, ['name'] = { ['NATO'] = 'SA-9 Gaskin', }, ['harm_detection_chance'] = 20 }, ['Tor'] = { ['type'] = 'single', ['searchRadar'] = { ['Tor 9A331'] = { }, }, ['launchers'] = { ['Tor 9A331'] = { }, }, ['name'] = { ['NATO'] = 'SA-15 Gauntlet', }, ['harm_detection_chance'] = 90, ['can_engage_harm'] = true }, ['Gepard'] = { ['type'] = 'single', ['searchRadar'] = { ['Gepard'] = { }, }, ['launchers'] = { ['Gepard'] = { }, }, ['name'] = { ['NATO'] = 'Gepard', }, ['harm_detection_chance'] = 10 }, ['Rapier'] = { ['searchRadar'] = { ['rapier_fsa_blindfire_radar'] = { }, }, ['launchers'] = { ['rapier_fsa_launcher'] = { ['trackingRadar'] = true, }, }, ['misc'] = { ['rapier_fsa_optical_tracker_unit'] = { ['required'] = true, }, }, ['name'] = { ['NATO'] = 'Rapier', }, ['harm_detection_chance'] = 10 }, ['ZSU-23-4 Shilka'] = { ['type'] = 'single', ['searchRadar'] = { ['ZSU-23-4 Shilka'] = { }, }, ['launchers'] = { ['ZSU-23-4 Shilka'] = { }, }, ['name'] = { ['NATO'] = 'Zues', }, ['harm_detection_chance'] = 10 }, ['HQ-7'] = { ['searchRadar'] = { ['HQ-7_STR_SP'] = { ['name'] = { ['NATO'] = 'CSA-4', }, }, }, ['launchers'] = { ['HQ-7_LN_SP'] = { }, }, ['name'] = { ['NATO'] = 'CSA-4', }, ['harm_detection_chance'] = 30 }, ['Phalanx'] = { ['type'] = 'single', ['searchRadar'] = { ['HEMTT_C-RAM_Phalanx'] = { }, }, ['launchers'] = { ['HEMTT_C-RAM_Phalanx'] = { }, }, ['name'] = { ['NATO'] = 'Phalanx', }, ['harm_detection_chance'] = 10 }, -- Start of RED EW radars: ['1L13 EWR'] = { ['type'] = 'ewr', ['searchRadar'] = { ['1L13 EWR'] = { ['name'] = { ['NATO'] = 'Box Spring', }, }, }, ['harm_detection_chance'] = 60 }, ['55G6 EWR'] = { ['type'] = 'ewr', ['searchRadar'] = { ['55G6 EWR'] = { ['name'] = { ['NATO'] = 'Tall Rack', }, }, }, ['harm_detection_chance'] = 60 }, ['Dog Ear'] = { ['type'] = 'ewr', ['searchRadar'] = { ['Dog Ear radar'] = { ['name'] = { ['NATO'] = 'Dog Ear', }, }, }, ['harm_detection_chance'] = 20 }, -- Start of BLUE EW radars: ['FPS-117 Dome'] = { ['type'] = 'ewr', ['searchRadar'] = { ['FPS-117 Dome'] = { ['name'] = { ['NATO'] = 'FPS-117 Dome', }, }, }, ['harm_detection_chance'] = 80 }, ['FPS-117'] = { ['type'] = 'ewr', ['searchRadar'] = { ['FPS-117'] = { ['name'] = { ['NATO'] = 'FPS-117', }, }, }, ['harm_detection_chance'] = 80 } } end do -- this file contains the definitions for the HightDigitSAMSs: https://github.com/Auranis/HighDigitSAMs --EW radars used in multiple SAM systems: s300PMU164N6Esr = { ['name'] = { ['NATO'] = 'Big Bird', }, } s300PMU140B6MDsr = { ['name'] = { ['NATO'] = 'Clam Shell', }, } --[[ units in SA-10 group Gargoyle: 2020-12-10 18:27:27.050 INFO SCRIPTING: S-300PMU1 54K6 cp 2020-12-10 18:27:27.050 INFO SCRIPTING: S-300PMU1 5P85CE ln 2020-12-10 18:27:27.050 INFO SCRIPTING: S-300PMU1 5P85DE ln 2020-12-10 18:27:27.050 INFO SCRIPTING: S-300PMU1 40B6MD sr 2020-12-10 18:27:27.050 INFO SCRIPTING: S-300PMU1 64N6E sr 2020-12-10 18:27:27.050 INFO SCRIPTING: S-300PMU1 40B6M tr 2020-12-10 18:27:27.050 INFO SCRIPTING: S-300PMU1 30N6E tr --]] samTypesDB['S-300PMU1'] = { ['type'] = 'complex', ['searchRadar'] = { ['S-300PMU1 40B6MD sr'] = s300PMU140B6MDsr, ['S-300PMU1 64N6E sr'] = s300PMU164N6Esr, ['S-300PS 40B6MD sr'] = { ['name'] = { ['NATO'] = '', }, }, ['S-300PS 64H6E sr'] = { ['name'] = { ['NATO'] = '', }, }, }, ['trackingRadar'] = { ['S-300PMU1 40B6M tr'] = { ['name'] = { ['NATO'] = 'Grave Stone', }, }, ['S-300PMU1 30N6E tr'] = { ['name'] = { ['NATO'] = 'Flap Lid', }, }, ['S-300PS 40B6M tr'] = { ['name'] = { ['NATO'] = '', }, }, }, ['misc'] = { ['S-300PMU1 54K6 cp'] = { ['required'] = true, }, }, ['launchers'] = { ['S-300PMU1 5P85CE ln'] = { }, ['S-300PMU1 5P85DE ln'] = { }, }, ['name'] = { ['NATO'] = 'SA-20A Gargoyle' }, ['harm_detection_chance'] = 90, ['can_engage_harm'] = true } --[[ Units in the SA-23 Group: 2020-12-11 16:40:52.072 INFO SCRIPTING: S-300VM 9A82ME ln 2020-12-11 16:40:52.072 INFO SCRIPTING: S-300VM 9A83ME ln 2020-12-11 16:40:52.072 INFO SCRIPTING: S-300VM 9S15M2 sr 2020-12-11 16:40:52.072 INFO SCRIPTING: S-300VM 9S19M2 sr 2020-12-11 16:40:52.072 INFO SCRIPTING: S-300VM 9S32ME tr 2020-12-11 16:40:52.072 INFO SCRIPTING: S-300VM 9S457ME cp ]]-- samTypesDB['S-300VM'] = { ['type'] = 'complex', ['searchRadar'] = { ['S-300VM 9S15M2 sr'] = { ['name'] = { ['NATO'] = 'Bill Board-C', }, }, ['S-300VM 9S19M2 sr'] = { ['name'] = { ['NATO'] = 'High Screen-B', }, }, }, ['trackingRadar'] = { ['S-300VM 9S32ME tr'] = { }, }, ['misc'] = { ['S-300VM 9S457ME cp'] = { ['required'] = true, }, }, ['launchers'] = { ['S-300VM 9A82ME ln'] = { }, ['S-300VM 9A83ME ln'] = { }, }, ['name'] = { ['NATO'] = 'SA-23 Antey-2500' }, ['harm_detection_chance'] = 90, ['can_engage_harm'] = true } --[[ Units in the SA-10B Group: 2021-01-01 20:39:14.413 INFO SCRIPTING: S-300PS SA-10B 40B6MD MAST sr 2021-01-01 20:39:14.413 INFO SCRIPTING: S-300PS SA-10B 54K6 cp 2021-01-01 20:39:14.413 INFO SCRIPTING: S-300PS 5P85SE_mod ln 2021-01-01 20:39:14.413 INFO SCRIPTING: S-300PS 5P85SU_mod ln 2021-01-01 20:39:14.413 INFO SCRIPTING: S-300PS 64H6E TRAILER sr 2021-01-01 20:39:14.413 INFO SCRIPTING: S-300PS 30N6 TRAILER tr 2021-01-01 20:39:14.413 INFO SCRIPTING: S-300PS SA-10B 40B6M MAST tr --]] samTypesDB['S-300PS'] = { ['type'] = 'complex', ['searchRadar'] = { ['S-300PS SA-10B 40B6MD MAST sr'] = { ['name'] = { ['NATO'] = 'Clam Shell', }, }, ['S-300PS 64H6E TRAILER sr'] = { }, }, ['trackingRadar'] = { ['S-300PS 30N6 TRAILER tr'] = { }, ['S-300PS SA-10B 40B6M MAST tr'] = { }, ['S-300PS 40B6M tr'] = { }, ['S-300PMU1 40B6M tr'] = { }, ['S-300PMU1 30N6E tr'] = { }, }, ['misc'] = { ['S-300PS SA-10B 54K6 cp'] = { ['required'] = true, }, }, ['launchers'] = { ['S-300PS 5P85SE_mod ln'] = { }, ['S-300PS 5P85SU_mod ln'] = { }, }, ['name'] = { ['NATO'] = 'SA-10B Grumble' }, ['harm_detection_chance'] = 90, ['can_engage_harm'] = true } --[[ Extra launchers for the in game SA-10C and HighDigitSAMs SA-10B, SA-20B 2021-01-01 21:04:19.908 INFO SCRIPTING: S-300PS 5P85DE ln 2021-01-01 21:04:19.908 INFO SCRIPTING: S-300PS 5P85CE ln --]] local s300launchers = samTypesDB['S-300']['launchers'] s300launchers['S-300PS 5P85DE ln'] = {} s300launchers['S-300PS 5P85CE ln'] = {} local s300launchers = samTypesDB['S-300PS']['launchers'] s300launchers['S-300PS 5P85DE ln'] = {} s300launchers['S-300PS 5P85CE ln'] = {} local s300launchers = samTypesDB['S-300PMU1']['launchers'] s300launchers['S-300PS 5P85DE ln'] = {} s300launchers['S-300PS 5P85CE ln'] = {} --[[ New launcher for the SA-11 complex, will identify as SA-17 SA-17 Buk M1-2 LN 9A310M1-2 --]] samTypesDB['Buk-M2'] = { ['type'] = 'complex', ['searchRadar'] = { ['SA-11 Buk SR 9S18M1'] = { ['name'] = { ['NATO'] = 'Snow Drift', }, }, }, ['launchers'] = { ['SA-17 Buk M1-2 LN 9A310M1-2'] = { }, }, ['misc'] = { ['SA-11 Buk CC 9S470M1'] = { ['required'] = true, }, }, ['name'] = { ['NATO'] = 'SA-17 Grizzly', }, ['harm_detection_chance'] = 90 } --[[ New launcher for the SA-2 complex: S_75M_Volhov_V759 --]] local s75launchers = samTypesDB['S-75']['launchers'] s75launchers['S_75M_Volhov_V759'] = {} --[[ New launcher for the SA-3 complex: --]] local s125launchers = samTypesDB['S-125']['launchers'] s125launchers['5p73 V-601P ln'] = {} --[[ New launcher for the SA-2 complex: HQ_2_Guideline_LN --]] local s125launchers = samTypesDB['S-75']['launchers'] s125launchers['HQ_2_Guideline_LN'] = {} --[[ SA-12 Gladiator / Giant: 2021-03-19 21:24:22.620 INFO SCRIPTING: S-300V 9S15 sr 2021-03-19 21:24:22.620 INFO SCRIPTING: S-300V 9S19 sr 2021-03-19 21:24:22.620 INFO SCRIPTING: S-300V 9S32 tr 2021-03-19 21:24:22.620 INFO SCRIPTING: S-300V 9S457 cp 2021-03-19 21:24:22.620 INFO SCRIPTING: S-300V 9A83 ln 2021-03-19 21:24:22.620 INFO SCRIPTING: S-300V 9A82 ln --]] samTypesDB['S-300V'] = { ['type'] = 'complex', ['searchRadar'] = { ['S-300V 9S15 sr'] = { ['name'] = { ['NATO'] = 'Bill Board', }, }, ['S-300V 9S19 sr'] = { ['name'] = { ['NATO'] = 'High Screen', }, }, }, ['trackingRadar'] = { ['S-300V 9S32 tr'] = { ['NATO'] = 'Grill Pan', }, }, ['misc'] = { ['S-300V 9S457 cp'] = { ['required'] = true, }, }, ['launchers'] = { ['S-300V 9A83 ln'] = { }, ['S-300V 9A82 ln'] = { }, }, ['name'] = { ['NATO'] = 'SA-12 Gladiator/Giant' }, ['harm_detection_chance'] = 90, ['can_engage_harm'] = true } --[[ SA-20B Gargoyle B: 2021-03-25 19:15:02.135 INFO SCRIPTING: S-300PMU2 64H6E2 sr 2021-03-25 19:15:02.135 INFO SCRIPTING: S-300PMU2 92H6E tr 2021-03-25 19:15:02.135 INFO SCRIPTING: S-300PMU2 5P85SE2 ln 2021-03-25 19:15:02.135 INFO SCRIPTING: S-300PMU2 54K6E2 cp --]] samTypesDB['S-300PMU2'] = { ['type'] = 'complex', ['searchRadar'] = { ['S-300PMU2 64H6E2 sr'] = { ['name'] = { ['NATO'] = '', }, }, ['S-300PMU1 40B6MD sr'] = s300PMU140B6MDsr, ['S-300PMU1 64N6E sr'] = s300PMU164N6Esr, }, ['trackingRadar'] = { ['S-300PMU2 92H6E tr'] = { }, ['S-300PS 40B6M tr'] = { }, ['S-300PMU1 40B6M tr'] = { }, ['S-300PMU1 30N6E tr'] = { }, }, ['misc'] = { ['S-300PMU2 54K6E2 cp'] = { ['required'] = true, }, }, ['launchers'] = { ['S-300PMU2 5P85SE2 ln'] = { }, }, ['name'] = { ['NATO'] = 'SA-20B Gargoyle B' }, ['harm_detection_chance'] = 90, ['can_engage_harm'] = true } --[[ --]] end do SkynetIADSLogger = {} SkynetIADSLogger.__index = SkynetIADSLogger function SkynetIADSLogger:create(iads) local logger = {} setmetatable(logger, SkynetIADSLogger) logger.debugOutput = {} logger.debugOutput.IADSStatus = false logger.debugOutput.samWentDark = false logger.debugOutput.contacts = false logger.debugOutput.radarWentLive = false logger.debugOutput.jammerProbability = false logger.debugOutput.addedEWRadar = false logger.debugOutput.addedSAMSite = false logger.debugOutput.warnings = true logger.debugOutput.harmDefence = false logger.debugOutput.samSiteStatusEnvOutput = false logger.debugOutput.earlyWarningRadarStatusEnvOutput = false logger.debugOutput.commandCenterStatusEnvOutput = false logger.iads = iads return logger end function SkynetIADSLogger:getDebugSettings() return self.debugOutput end function SkynetIADSLogger:printOutput(output, typeWarning) if typeWarning == true and self:getDebugSettings().warnings or typeWarning == nil then if typeWarning == true then output = "WARNING: "..output end trigger.action.outText(output, 4) end end function SkynetIADSLogger:printOutputToLog(output) env.info("SKYNET: "..output, 4) end function SkynetIADSLogger:printEarlyWarningRadarStatus() local ewRadars = self.iads:getEarlyWarningRadars() self:printOutputToLog("------------------------------------------ EW RADAR STATUS: "..self.iads:getCoalitionString().." -------------------------------") for i = 1, #ewRadars do local ewRadar = ewRadars[i] local numConnectionNodes = #ewRadar:getConnectionNodes() local numPowerSources = #ewRadar:getPowerSources() local isActive = ewRadar:isActive() local connectionNodes = ewRadar:getConnectionNodes() local firstRadar = nil local radars = ewRadar:getRadars() --get the first existing radar to prevent issues in calculating the distance later on: for i = 1, #radars do if radars[i]:isExist() then firstRadar = radars[i] break end end local numDamagedConnectionNodes = 0 for j = 1, #connectionNodes do local connectionNode = connectionNodes[j] if connectionNode:isExist() == false then numDamagedConnectionNodes = numDamagedConnectionNodes + 1 end end local intactConnectionNodes = numConnectionNodes - numDamagedConnectionNodes local powerSources = ewRadar:getPowerSources() local numDamagedPowerSources = 0 for j = 1, #powerSources do local powerSource = powerSources[j] if powerSource:isExist() == false then numDamagedPowerSources = numDamagedPowerSources + 1 end end local intactPowerSources = numPowerSources - numDamagedPowerSources local detectedTargets = ewRadar:getDetectedTargets() local samSitesInCoveredArea = ewRadar:getChildRadars() local unitName = "DESTROYED" if ewRadar:getDCSRepresentation():isExist() then unitName = ewRadar:getDCSName() end self:printOutputToLog("UNIT: "..unitName.." | TYPE: "..ewRadar:getNatoName()) self:printOutputToLog("ACTIVE: "..tostring(isActive).."| DETECTED TARGETS: "..#detectedTargets.." | DEFENDING HARM: "..tostring(ewRadar:isDefendingHARM())) if numConnectionNodes > 0 then self:printOutputToLog("CONNECTION NODES: "..numConnectionNodes.." | DAMAGED: "..numDamagedConnectionNodes.." | INTACT: "..intactConnectionNodes) else self:printOutputToLog("NO CONNECTION NODES SET") end if numPowerSources > 0 then self:printOutputToLog("POWER SOURCES : "..numPowerSources.." | DAMAGED:"..numDamagedPowerSources.." | INTACT: "..intactPowerSources) else self:printOutputToLog("NO POWER SOURCES SET") end self:printOutputToLog("SAM SITES IN COVERED AREA: "..#samSitesInCoveredArea) for j = 1, #samSitesInCoveredArea do local samSiteCovered = samSitesInCoveredArea[j] self:printOutputToLog(samSiteCovered:getDCSName()) end for j = 1, #detectedTargets do local contact = detectedTargets[j] if firstRadar ~= nil and firstRadar:isExist() then local distance = mist.utils.round(mist.utils.metersToNM(ewRadar:getDistanceInMetersToContact(firstRadar:getDCSRepresentation(), contact:getPosition().p)), 2) self:printOutputToLog("CONTACT: "..contact:getName().." | TYPE: "..contact:getTypeName().." | DISTANCE NM: "..distance) end end self:printOutputToLog("---------------------------------------------------") end end function SkynetIADSLogger:getMetaInfo(abstractElementSupport) local info = {} info.numSources = #abstractElementSupport info.numDamagedSources = 0 info.numIntactSources = 0 for j = 1, #abstractElementSupport do local source = abstractElementSupport[j] if source:isExist() == false then info.numDamagedSources = info.numDamagedSources + 1 end end info.numIntactSources = info.numSources - info.numDamagedSources return info end function SkynetIADSLogger:printSAMSiteStatus() local samSites = self.iads:getSAMSites() self:printOutputToLog("------------------------------------------ SAM STATUS: "..self.iads:getCoalitionString().." -------------------------------") for i = 1, #samSites do local samSite = samSites[i] local numConnectionNodes = #samSite:getConnectionNodes() local numPowerSources = #samSite:getPowerSources() local isAutonomous = samSite:getAutonomousState() local isActive = samSite:isActive() local connectionNodes = samSite:getConnectionNodes() local firstRadar = samSite:getRadars()[1] local numDamagedConnectionNodes = 0 for j = 1, #connectionNodes do local connectionNode = connectionNodes[j] if connectionNode:isExist() == false then numDamagedConnectionNodes = numDamagedConnectionNodes + 1 end end local intactConnectionNodes = numConnectionNodes - numDamagedConnectionNodes local powerSources = samSite:getPowerSources() local numDamagedPowerSources = 0 for j = 1, #powerSources do local powerSource = powerSources[j] if powerSource:isExist() == false then numDamagedPowerSources = numDamagedPowerSources + 1 end end local intactPowerSources = numPowerSources - numDamagedPowerSources local detectedTargets = samSite:getDetectedTargets() local samSitesInCoveredArea = samSite:getChildRadars() local engageAirWeapons = samSite:getCanEngageAirWeapons() local engageHARMS = samSite:getCanEngageHARM() local hasAmmo = samSite:hasRemainingAmmo() self:printOutputToLog("GROUP: "..samSite:getDCSName().." | TYPE: "..samSite:getNatoName()) self:printOutputToLog("ACTIVE: "..tostring(isActive).." | AUTONOMOUS: "..tostring(isAutonomous).." | IS ACTING AS EW: "..tostring(samSite:getActAsEW()).." | CAN ENGAGE AIR WEAPONS : "..tostring(engageAirWeapons).." | CAN ENGAGE HARMS : "..tostring(engageHARMS).." | HAS AMMO: "..tostring(hasAmmo).." | DETECTED TARGETS: "..#detectedTargets.." | DEFENDING HARM: "..tostring(samSite:isDefendingHARM()).." | MISSILES IN FLIGHT: "..tostring(samSite:getNumberOfMissilesInFlight())) if numConnectionNodes > 0 then self:printOutputToLog("CONNECTION NODES: "..numConnectionNodes.." | DAMAGED: "..numDamagedConnectionNodes.." | INTACT: "..intactConnectionNodes) else self:printOutputToLog("NO CONNECTION NODES SET") end if numPowerSources > 0 then self:printOutputToLog("POWER SOURCES : "..numPowerSources.." | DAMAGED:"..numDamagedPowerSources.." | INTACT: "..intactPowerSources) else self:printOutputToLog("NO POWER SOURCES SET") end self:printOutputToLog("SAM SITES IN COVERED AREA: "..#samSitesInCoveredArea) for j = 1, #samSitesInCoveredArea do local samSiteCovered = samSitesInCoveredArea[j] self:printOutputToLog(samSiteCovered:getDCSName()) end for j = 1, #detectedTargets do local contact = detectedTargets[j] if firstRadar ~= nil and firstRadar:isExist() then local distance = mist.utils.round(mist.utils.metersToNM(samSite:getDistanceInMetersToContact(firstRadar:getDCSRepresentation(), contact:getPosition().p)), 2) self:printOutputToLog("CONTACT: "..contact:getName().." | TYPE: "..contact:getTypeName().." | DISTANCE NM: "..distance) end end self:printOutputToLog("---------------------------------------------------") end end function SkynetIADSLogger:printCommandCenterStatus() local commandCenters = self.iads:getCommandCenters() self:printOutputToLog("------------------------------------------ COMMAND CENTER STATUS: "..self.iads:getCoalitionString().." -------------------------------") for i = 1, #commandCenters do local commandCenter = commandCenters[i] local numConnectionNodes = #commandCenter:getConnectionNodes() local powerSourceInfo = self:getMetaInfo(commandCenter:getPowerSources()) local connectionNodeInfo = self:getMetaInfo(commandCenter:getConnectionNodes()) self:printOutputToLog("GROUP: "..commandCenter:getDCSName().." | TYPE: "..commandCenter:getNatoName()) if connectionNodeInfo.numSources > 0 then self:printOutputToLog("CONNECTION NODES: "..connectionNodeInfo.numSources.." | DAMAGED: "..connectionNodeInfo.numDamagedSources.." | INTACT: "..connectionNodeInfo.numIntactSources) else self:printOutputToLog("NO CONNECTION NODES SET") end if powerSourceInfo.numSources > 0 then self:printOutputToLog("POWER SOURCES : "..powerSourceInfo.numSources.." | DAMAGED: "..powerSourceInfo.numDamagedSources.." | INTACT: "..powerSourceInfo.numIntactSources) else self:printOutputToLog("NO POWER SOURCES SET") end self:printOutputToLog("---------------------------------------------------") end end function SkynetIADSLogger:printSystemStatus() if self:getDebugSettings().IADSStatus or self:getDebugSettings().contacts then local coalitionStr = self.iads:getCoalitionString() self:printOutput("---- IADS: "..coalitionStr.." ------") end if self:getDebugSettings().IADSStatus then local commandCenters = self.iads:getCommandCenters() local numComCenters = #commandCenters local numDestroyedComCenters = 0 local numComCentersNoPower = 0 local numComCentersNoConnectionNode = 0 local numIntactComCenters = 0 for i = 1, #commandCenters do local commandCenter = commandCenters[i] if commandCenter:hasWorkingPowerSource() == false then numComCentersNoPower = numComCentersNoPower + 1 end if commandCenter:hasActiveConnectionNode() == false then numComCentersNoConnectionNode = numComCentersNoConnectionNode + 1 end if commandCenter:isDestroyed() == false then numIntactComCenters = numIntactComCenters + 1 end end numDestroyedComCenters = numComCenters - numIntactComCenters self:printOutput("COMMAND CENTERS: "..numComCenters.." | Destroyed: "..numDestroyedComCenters.." | NoPowr: "..numComCentersNoPower.." | NoCon: "..numComCentersNoConnectionNode) local ewNoPower = 0 local earlyWarningRadars = self.iads:getEarlyWarningRadars() local ewTotal = #earlyWarningRadars local ewNoConnectionNode = 0 local ewActive = 0 local ewRadarsInactive = 0 for i = 1, #earlyWarningRadars do local ewRadar = earlyWarningRadars[i] if ewRadar:hasWorkingPowerSource() == false then ewNoPower = ewNoPower + 1 end if ewRadar:hasActiveConnectionNode() == false then ewNoConnectionNode = ewNoConnectionNode + 1 end if ewRadar:isActive() then ewActive = ewActive + 1 end end ewRadarsInactive = ewTotal - ewActive local numEWRadarsDestroyed = #self.iads:getDestroyedEarlyWarningRadars() self:printOutput("EW: "..ewTotal.." | On: "..ewActive.." | Off: "..ewRadarsInactive.." | Destroyed: "..numEWRadarsDestroyed.." | NoPowr: "..ewNoPower.." | NoCon: "..ewNoConnectionNode) local samSitesInactive = 0 local samSitesActive = 0 local samSites = self.iads:getSAMSites() local samSitesTotal = #samSites local samSitesNoPower = 0 local samSitesNoConnectionNode = 0 local samSitesOutOfAmmo = 0 local samSiteAutonomous = 0 local samSiteRadarDestroyed = 0 for i = 1, #samSites do local samSite = samSites[i] if samSite:hasWorkingPowerSource() == false then samSitesNoPower = samSitesNoPower + 1 end if samSite:hasActiveConnectionNode() == false then samSitesNoConnectionNode = samSitesNoConnectionNode + 1 end if samSite:isActive() then samSitesActive = samSitesActive + 1 end if samSite:hasRemainingAmmo() == false then samSitesOutOfAmmo = samSitesOutOfAmmo + 1 end if samSite:getAutonomousState() == true then samSiteAutonomous = samSiteAutonomous + 1 end if samSite:hasWorkingRadar() == false then samSiteRadarDestroyed = samSiteRadarDestroyed + 1 end end samSitesInactive = samSitesTotal - samSitesActive self:printOutput("SAM: "..samSitesTotal.." | On: "..samSitesActive.." | Off: "..samSitesInactive.." | Autonm: "..samSiteAutonomous.." | Raddest: "..samSiteRadarDestroyed.." | NoPowr: "..samSitesNoPower.." | NoCon: "..samSitesNoConnectionNode.." | NoAmmo: "..samSitesOutOfAmmo) end if self:getDebugSettings().contacts then local contacts = self.iads:getContacts() if contacts then for i = 1, #contacts do local contact = contacts[i] self:printOutput("CONTACT: "..contact:getName().." | TYPE: "..contact:getTypeName().." | GS: "..tostring(contact:getGroundSpeedInKnots()).." | LAST SEEN: "..contact:getAge()) end end end if self:getDebugSettings().commandCenterStatusEnvOutput then self:printCommandCenterStatus() end if self:getDebugSettings().earlyWarningRadarStatusEnvOutput then self:printEarlyWarningRadarStatus() end if self:getDebugSettings().samSiteStatusEnvOutput then self:printSAMSiteStatus() end end end do SkynetIADS = {} SkynetIADS.__index = SkynetIADS SkynetIADS.database = samTypesDB function SkynetIADS:create(name) local iads = {} setmetatable(iads, SkynetIADS) iads.radioMenu = nil iads.earlyWarningRadars = {} iads.samSites = {} iads.commandCenters = {} iads.ewRadarScanMistTaskID = nil iads.coalition = nil iads.contacts = {} iads.maxTargetAge = 32 iads.name = name iads.harmDetection = SkynetIADSHARMDetection:create(iads) iads.logger = SkynetIADSLogger:create(iads) if iads.name == nil then iads.name = "" end iads.contactUpdateInterval = 5 world.addEventHandler(iads) return iads end function SkynetIADS:onEvent(event) if (event.id == world.event.S_EVENT_BIRTH ) then env.info("New Object Spawned") -- self:addSAMSite(event.initiator:getGroup():getName()); end end function SkynetIADS:setUpdateInterval(interval) self.contactUpdateInterval = interval end function SkynetIADS:setCoalition(item) if item then local coalitionID = item:getCoalition() if self.coalitionID == nil then self.coalitionID = coalitionID end if self.coalitionID ~= coalitionID then self:printOutputToLog("element: "..item:getName().." has a different coalition than the IADS", true) end end end function SkynetIADS:addJammer(jammer) table.insert(self.jammers, jammer) end function SkynetIADS:getCoalition() return self.coalitionID end function SkynetIADS:getDestroyedEarlyWarningRadars() local destroyedSites = {} for i = 1, #self.earlyWarningRadars do local ewSite = self.earlyWarningRadars[i] if ewSite:isDestroyed() then table.insert(destroyedSites, ewSite) end end return destroyedSites end function SkynetIADS:getUsableAbstractRadarElemtentsOfTable(abstractRadarTable) local usable = {} for i = 1, #abstractRadarTable do local abstractRadarElement = abstractRadarTable[i] if abstractRadarElement:hasActiveConnectionNode() and abstractRadarElement:hasWorkingPowerSource() and abstractRadarElement:isDestroyed() == false then table.insert(usable, abstractRadarElement) end end return usable end function SkynetIADS:getUsableEarlyWarningRadars() return self:getUsableAbstractRadarElemtentsOfTable(self.earlyWarningRadars) end function SkynetIADS:createTableDelegator(units) local sites = SkynetIADSTableDelegator:create() for i = 1, #units do local site = units[i] table.insert(sites, site) end return sites end function SkynetIADS:addEarlyWarningRadarsByPrefix(prefix) self:deactivateEarlyWarningRadars() self.earlyWarningRadars = {} for unitName, unit in pairs(mist.DBs.unitsByName) do local pos = self:findSubString(unitName, prefix) --somehow the MIST unit db contains StaticObject, we check to see we only add Units local unit = Unit.getByName(unitName) if pos and pos == 1 and unit then self:addEarlyWarningRadar(unitName) end end return self:createTableDelegator(self.earlyWarningRadars) end function SkynetIADS:addEarlyWarningRadar(earlyWarningRadarUnitName) local earlyWarningRadarUnit = Unit.getByName(earlyWarningRadarUnitName) if earlyWarningRadarUnit == nil then self:printOutputToLog("you have added an EW Radar that does not exist, check name of Unit in Setup and Mission editor: "..earlyWarningRadarUnitName, true) return end self:setCoalition(earlyWarningRadarUnit) local ewRadar = nil local category = earlyWarningRadarUnit:getDesc().category if category == Unit.Category.AIRPLANE or category == Unit.Category.SHIP then ewRadar = SkynetIADSAWACSRadar:create(earlyWarningRadarUnit, self) else ewRadar = SkynetIADSEWRadar:create(earlyWarningRadarUnit, self) end ewRadar:setupElements() ewRadar:setCachedTargetsMaxAge(self:getCachedTargetsMaxAge()) -- for performance improvement, if iads is not scanning no update coverage update needs to be done, will be executed once when iads activates if self.ewRadarScanMistTaskID ~= nil then self:buildRadarCoverageForEarlyWarningRadar(ewRadar) end ewRadar:setActAsEW(true) ewRadar:setToCorrectAutonomousState() ewRadar:goLive() table.insert(self.earlyWarningRadars, ewRadar) if self:getDebugSettings().addedEWRadar then self:printOutputToLog("ADDED: "..ewRadar:getDescription()) end return ewRadar end function SkynetIADS:getCachedTargetsMaxAge() return self.contactUpdateInterval end function SkynetIADS:getEarlyWarningRadars() return self:createTableDelegator(self.earlyWarningRadars) end function SkynetIADS:getEarlyWarningRadarByUnitName(unitName) for i = 1, #self.earlyWarningRadars do local ewRadar = self.earlyWarningRadars[i] if ewRadar:getDCSName() == unitName then return ewRadar end end end function SkynetIADS:findSubString(haystack, needle) return string.find(haystack, needle, 1, true) end function SkynetIADS:addSAMSitesByPrefix(prefix) self:deativateSAMSites() self.samSites = {} for groupName, groupData in pairs(mist.DBs.groupsByName) do local pos = self:findSubString(groupName, prefix) if pos and pos == 1 then --mist returns groups, units and, StaticObjects local dcsObject = Group.getByName(groupName) if dcsObject and dcsObject:getUnits()[1]:isActive() then self:addSAMSite(groupName) end end end return self:createTableDelegator(self.samSites) end function SkynetIADS:getSAMSitesByPrefix(prefix) local returnSams = {} for i = 1, #self.samSites do local samSite = self.samSites[i] local groupName = samSite:getDCSName() local pos = self:findSubString(groupName, prefix) if pos and pos == 1 then table.insert(returnSams, samSite) end end return self:createTableDelegator(returnSams) end function SkynetIADS:addSAMSite(samSiteName) local samSiteDCS = Group.getByName(samSiteName) if samSiteDCS == nil then self:printOutputToLog("you have added an SAM Site that does not exist, check name of Group in Setup and Mission editor: "..tostring(samSiteName), true) return end self:setCoalition(samSiteDCS) local samSite = SkynetIADSSamSite:create(samSiteDCS, self) samSite:setupElements() samSite:setCanEngageAirWeapons(true) samSite:goLive() samSite:setCachedTargetsMaxAge(self:getCachedTargetsMaxAge()) if samSite:getNatoName() == "UNKNOWN" then self:printOutputToLog("you have added an SAM site that Skynet IADS can not handle: "..samSite:getDCSName(), true) samSite:cleanUp() else samSite:goDark() table.insert(self.samSites, samSite) if self:getDebugSettings().addedSAMSite then self:printOutputToLog("ADDED: "..samSite:getDescription()) end -- for performance improvement, if iads is not scanning no update coverage update needs to be done, will be executed once when iads activates if self.ewRadarScanMistTaskID ~= nil then self:buildRadarCoverageForSAMSite(samSite) end return samSite end end function SkynetIADS:getUsableSAMSites() return self:getUsableAbstractRadarElemtentsOfTable(self.samSites) end function SkynetIADS:getDestroyedSAMSites() local destroyedSites = {} for i = 1, #self.samSites do local samSite = self.samSites[i] if samSite:isDestroyed() then table.insert(destroyedSites, samSite) end end return destroyedSites end function SkynetIADS:getSAMSites() return self:createTableDelegator(self.samSites) end function SkynetIADS:getActiveSAMSites() local activeSAMSites = {} for i = 1, #self.samSites do if self.samSites[i]:isActive() then table.insert(activeSAMSites, self.samSites[i]) end end return activeSAMSites end function SkynetIADS:getSAMSiteByGroupName(groupName) for i = 1, #self.samSites do local samSite = self.samSites[i] if samSite:getDCSName() == groupName then return samSite end end end function SkynetIADS:getSAMSitesByNatoName(natoName) local selectedSAMSites = SkynetIADSTableDelegator:create() for i = 1, #self.samSites do local samSite = self.samSites[i] if samSite:getNatoName() == natoName then table.insert(selectedSAMSites, samSite) end end return selectedSAMSites end function SkynetIADS:addCommandCenter(commandCenter) self:setCoalition(commandCenter) local comCenter = SkynetIADSCommandCenter:create(commandCenter, self) table.insert(self.commandCenters, comCenter) -- when IADS is active the radars will be added to the new command center. If it not active this will happen when radar coverage is built if self.ewRadarScanMistTaskID ~= nil then self:addRadarsToCommandCenters() end return comCenter end function SkynetIADS:isCommandCenterUsable() if #self:getCommandCenters() == 0 then return true end local usableComCenters = self:getUsableAbstractRadarElemtentsOfTable(self:getCommandCenters()) return (#usableComCenters > 0) end function SkynetIADS:getCommandCenters() return self.commandCenters end function SkynetIADS.evaluateContacts(self) local ewRadars = self:getUsableEarlyWarningRadars() local samSites = self:getUsableSAMSites() --will add SAM Sites acting as EW Rardars to the ewRadars array: for i = 1, #samSites do local samSite = samSites[i] --We inform SAM sites that a target update is about to happen. If they have no targets in range after the cycle they go dark samSite:targetCycleUpdateStart() if samSite:getActAsEW() then table.insert(ewRadars, samSite) end --if the sam site is not in ew mode and active we grab the detected targets right here if samSite:isActive() and samSite:getActAsEW() == false then local contacts = samSite:getDetectedTargets() for j = 1, #contacts do local contact = contacts[j] self:mergeContact(contact) end end end local samSitesToTrigger = {} for i = 1, #ewRadars do local ewRadar = ewRadars[i] --call go live in case ewRadar had to shut down (HARM attack) ewRadar:goLive() -- if an awacs has traveled more than a predeterminded distance we update the autonomous state of the SAMs if getmetatable(ewRadar) == SkynetIADSAWACSRadar and ewRadar:isUpdateOfAutonomousStateOfSAMSitesRequired() then self:buildRadarCoverageForEarlyWarningRadar(ewRadar) end local ewContacts = ewRadar:getDetectedTargets() if #ewContacts > 0 then local samSitesUnderCoverage = ewRadar:getUsableChildRadars() for j = 1, #samSitesUnderCoverage do local samSiteUnterCoverage = samSitesUnderCoverage[j] -- only if a SAM site is not active we add it to the hash of SAM sites to be iterated later on if samSiteUnterCoverage:isActive() == false then --we add them to a hash to make sure each SAM site is in the collection only once, reducing the number of loops we conduct later on samSitesToTrigger[samSiteUnterCoverage:getDCSName()] = samSiteUnterCoverage end end for j = 1, #ewContacts do local contact = ewContacts[j] self:mergeContact(contact) end end end self:cleanAgedTargets() for samName, samToTrigger in pairs(samSitesToTrigger) do for j = 1, #self.contacts do local contact = self.contacts[j] -- the DCS Radar only returns enemy aircraft, if that should change a coalition check will be required -- currently every type of object in the air is handed of to the SAM site, including missiles local description = contact:getDesc() local category = description.category if category and category ~= Unit.Category.GROUND_UNIT and category ~= Unit.Category.SHIP and category ~= Unit.Category.STRUCTURE then samToTrigger:informOfContact(contact) end end end for i = 1, #samSites do local samSite = samSites[i] samSite:targetCycleUpdateEnd() end self.harmDetection:setContacts(self:getContacts()) self.harmDetection:evaluateContacts() self.logger:printSystemStatus() end function SkynetIADS:cleanAgedTargets() local contactsToKeep = {} for i = 1, #self.contacts do local contact = self.contacts[i] if contact:getAge() < self.maxTargetAge then table.insert(contactsToKeep, contact) end end self.contacts = contactsToKeep end --TODO unit test this method: function SkynetIADS:getAbstracRadarElements() local abstractRadarElements = {} local ewRadars = self:getEarlyWarningRadars() local samSites = self:getSAMSites() for i = 1, #ewRadars do local ewRadar = ewRadars[i] table.insert(abstractRadarElements, ewRadar) end for i = 1, #samSites do local samSite = samSites[i] table.insert(abstractRadarElements, samSite) end return abstractRadarElements end function SkynetIADS:addRadarsToCommandCenters() --we clear any existing radars that may have been added earlier local comCenters = self:getCommandCenters() for i = 1, #comCenters do local comCenter = comCenters[i] comCenter:clearChildRadars() end -- then we add child radars to the command centers local abstractRadarElements = self:getAbstracRadarElements() for i = 1, #abstractRadarElements do local abstractRadar = abstractRadarElements[i] self:addSingleRadarToCommandCenters(abstractRadar) end end function SkynetIADS:addSingleRadarToCommandCenters(abstractRadarElement) local comCenters = self:getCommandCenters() for i = 1, #comCenters do local comCenter = comCenters[i] comCenter:addChildRadar(abstractRadarElement) end end -- this method rebuilds the radar coverage of the IADS, a complete rebuild is only required the first time the IADS is activated -- during runtime it is sufficient to call buildRadarCoverageForSAMSite or buildRadarCoverageForEarlyWarningRadar method that just updates the IADS for one unit, this saves script execution time function SkynetIADS:buildRadarCoverage() --to build the basic radar coverage we use all SAM sites. Checks if SAM site has power or a connection node is done when using the SAM site later on local samSites = self:getSAMSites() --first we clear all child and parent radars that may have been added previously for i = 1, #samSites do local samSite = samSites[i] samSite:clearChildRadars() samSite:clearParentRadars() end local ewRadars = self:getEarlyWarningRadars() for i = 1, #ewRadars do local ewRadar = ewRadars[i] ewRadar:clearChildRadars() end --then we rebuild the radar coverage local abstractRadarElements = self:getAbstracRadarElements() for i = 1, #abstractRadarElements do local abstract = abstractRadarElements[i] self:buildRadarCoverageForAbstractRadarElement(abstract) end self:addRadarsToCommandCenters() --we call this once on all sam sites, to make sure autonomous sites go live when IADS activates for i = 1, #samSites do local samSite = samSites[i] samSite:informChildrenOfStateChange() end end function SkynetIADS:buildRadarCoverageForAbstractRadarElement(abstractRadarElement) local abstractRadarElements = self:getAbstracRadarElements() for i = 1, #abstractRadarElements do local aElementToCompare = abstractRadarElements[i] if aElementToCompare ~= abstractRadarElement then if abstractRadarElement:isInRadarDetectionRangeOf(aElementToCompare) then self:buildRadarAssociation(aElementToCompare, abstractRadarElement) end if aElementToCompare:isInRadarDetectionRangeOf(abstractRadarElement) then self:buildRadarAssociation(abstractRadarElement, aElementToCompare) end end end end function SkynetIADS:buildRadarAssociation(parent, child) --chilren should only be SAM sites not EW radars if ( getmetatable(child) == SkynetIADSSamSite ) then parent:addChildRadar(child) end --Only SAM Sites should have parent Radars, not EW Radars if ( getmetatable(child) == SkynetIADSSamSite ) then child:addParentRadar(parent) end end function SkynetIADS:buildRadarCoverageForSAMSite(samSite) self:buildRadarCoverageForAbstractRadarElement(samSite) self:addSingleRadarToCommandCenters(samSite) end function SkynetIADS:buildRadarCoverageForEarlyWarningRadar(ewRadar) self:buildRadarCoverageForAbstractRadarElement(ewRadar) self:addSingleRadarToCommandCenters(ewRadar) end function SkynetIADS:mergeContact(contact) local existingContact = false for i = 1, #self.contacts do local iadsContact = self.contacts[i] if iadsContact:getName() == contact:getName() then iadsContact:refresh() --these contacts are used in the logger we set a kown harm state of a contact coming from a SAM site. So the logger will show them als HARMs contact:setHARMState(iadsContact:getHARMState()) local radars = contact:getAbstractRadarElementsDetected() for j = 1, #radars do local radar = radars[j] iadsContact:addAbstractRadarElementDetected(radar) end existingContact = true end end if existingContact == false then table.insert(self.contacts, contact) end end function SkynetIADS:getContacts() return self.contacts end function SkynetIADS:getDebugSettings() return self.logger.debugOutput end function SkynetIADS:printOutput(output, typeWarning) self.logger:printOutput(output, typeWarning) end function SkynetIADS:printOutputToLog(output) self.logger:printOutputToLog(output) end -- will start going through the Early Warning Radars and SAM sites to check what targets they have detected function SkynetIADS.activate(self) mist.removeFunction(self.ewRadarScanMistTaskID) self.ewRadarScanMistTaskID = mist.scheduleFunction(SkynetIADS.evaluateContacts, {self}, 1, self.contactUpdateInterval) self:buildRadarCoverage() end function SkynetIADS:setupSAMSitesAndThenActivate(setupTime) self:activate() self.logger:printOutputToLog("DEPRECATED: setupSAMSitesAndThenActivate, no longer needed since using enableEmission instead of AI on / off allows for the Ground units to setup with their radars turned off") end function SkynetIADS:deactivate() mist.removeFunction(self.ewRadarScanMistTaskID) mist.removeFunction(self.samSetupMistTaskID) self:deativateSAMSites() self:deactivateEarlyWarningRadars() self:deactivateCommandCenters() end function SkynetIADS:deactivateCommandCenters() for i = 1, #self.commandCenters do local comCenter = self.commandCenters[i] comCenter:cleanUp() end end function SkynetIADS:deativateSAMSites() for i = 1, #self.samSites do local samSite = self.samSites[i] samSite:cleanUp() end end function SkynetIADS:deactivateEarlyWarningRadars() for i = 1, #self.earlyWarningRadars do local ewRadar = self.earlyWarningRadars[i] ewRadar:cleanUp() end end function SkynetIADS:addRadioMenu() self.radioMenu = missionCommands.addSubMenu('SKYNET IADS '..self:getCoalitionString()) local displayIADSStatus = missionCommands.addCommand('show IADS Status', self.radioMenu, SkynetIADS.updateDisplay, {self = self, value = true, option = 'IADSStatus'}) local displayIADSStatus = missionCommands.addCommand('hide IADS Status', self.radioMenu, SkynetIADS.updateDisplay, {self = self, value = false, option = 'IADSStatus'}) local displayIADSStatus = missionCommands.addCommand('show contacts', self.radioMenu, SkynetIADS.updateDisplay, {self = self, value = true, option = 'contacts'}) local displayIADSStatus = missionCommands.addCommand('hide contacts', self.radioMenu, SkynetIADS.updateDisplay, {self = self, value = false, option = 'contacts'}) end function SkynetIADS:removeRadioMenu() missionCommands.removeItem(self.radioMenu) end function SkynetIADS.updateDisplay(params) local option = params.option local self = params.self local value = params.value if option == 'IADSStatus' then self:getDebugSettings()[option] = value elseif option == 'contacts' then self:getDebugSettings()[option] = value end end function SkynetIADS:getCoalitionString() local coalitionStr = "RED" if self.coalitionID == coalition.side.BLUE then coalitionStr = "BLUE" elseif self.coalitionID == coalition.side.NEUTRAL then coalitionStr = "NEUTRAL" end if self.name then coalitionStr = "COALITION: "..coalitionStr.." | NAME: "..self.name end return coalitionStr end function SkynetIADS:getMooseConnector() if self.mooseConnector == nil then self.mooseConnector = SkynetMooseA2ADispatcherConnector:create(self) end return self.mooseConnector end function SkynetIADS:addMooseSetGroup(mooseSetGroup) self:getMooseConnector():addMooseSetGroup(mooseSetGroup) end end do SkynetMooseA2ADispatcherConnector = {} function SkynetMooseA2ADispatcherConnector:create(iads) local instance = {} setmetatable(instance, self) self.__index = self instance.iadsCollection = {} instance.mooseGroups = {} instance.ewRadarGroupNames = {} instance.samSiteGroupNames = {} table.insert(instance.iadsCollection, iads) return instance end function SkynetMooseA2ADispatcherConnector:addIADS(iads) table.insert(self.iadsCollection, iads) end function SkynetMooseA2ADispatcherConnector:addMooseSetGroup(mooseSetGroup) table.insert(self.mooseGroups, mooseSetGroup) self:update() end function SkynetMooseA2ADispatcherConnector:getEarlyWarningRadarGroupNames() self.ewRadarGroupNames = {} for i = 1, #self.iadsCollection do local ewRadars = self.iadsCollection[i]:getUsableEarlyWarningRadars() for j = 1, #ewRadars do local ewRadar = ewRadars[j] table.insert(self.ewRadarGroupNames, ewRadar:getDCSRepresentation():getGroup():getName()) end end return self.ewRadarGroupNames end function SkynetMooseA2ADispatcherConnector:getSAMSiteGroupNames() self.samSiteGroupNames = {} for i = 1, #self.iadsCollection do local samSites = self.iadsCollection[i]:getUsableSAMSites() for j = 1, #samSites do local samSite = samSites[j] table.insert(self.samSiteGroupNames, samSite:getDCSName()) end end return self.samSiteGroupNames end function SkynetMooseA2ADispatcherConnector:update() --mooseGroup elements are type of: --https://flightcontrol-master.github.io/MOOSE_DOCS_DEVELOP/Documentation/Core.Set.html##(SET_GROUP) --remove previously set group names: for i = 1, #self.mooseGroups do local mooseGroup = self.mooseGroups[i] mooseGroup:RemoveGroupsByName(self.ewRadarGroupNames) mooseGroup:RemoveGroupsByName(self.samSiteGroupNames) end --add group names of IADS radars that are currently usable by the IADS: for i = 1, #self.mooseGroups do local mooseGroup = self.mooseGroups[i] mooseGroup:AddGroupsByName(self:getEarlyWarningRadarGroupNames()) mooseGroup:AddGroupsByName(self:getSAMSiteGroupNames()) end end end do SkynetIADSTableDelegator = {} function SkynetIADSTableDelegator:create() local instance = {} local forwarder = {} forwarder.__index = function(tbl, name) tbl[name] = function(self, ...) for i = 1, #self do self[i][name](self[i], ...) end return self end return tbl[name] end setmetatable(instance, forwarder) instance.__index = forwarder return instance end end do SkynetIADSAbstractDCSObjectWrapper = {} function SkynetIADSAbstractDCSObjectWrapper:create(dcsRepresentation) local instance = {} setmetatable(instance, self) self.__index = self instance.dcsName = "" instance.typeName = "" instance:setDCSRepresentation(dcsRepresentation) if getmetatable(dcsRepresentation) ~= Group then instance.typeName = dcsRepresentation:getTypeName() end return instance end function SkynetIADSAbstractDCSObjectWrapper:setDCSRepresentation(representation) self.dcsRepresentation = representation if self.dcsRepresentation then self.dcsName = self.dcsRepresentation:getName() if (self.dcsName == nil or string.len(self.dcsName) == 0) and self.dcsRepresentation.id_ then self.dcsName = self.dcsRepresentation.id_ end end end function SkynetIADSAbstractDCSObjectWrapper:getDCSRepresentation() return self.dcsRepresentation end function SkynetIADSAbstractDCSObjectWrapper:getName() return self.dcsName end function SkynetIADSAbstractDCSObjectWrapper:getTypeName() return self.typeName end function SkynetIADSAbstractDCSObjectWrapper:getPosition() return self.dcsRepresentation:getPosition() end function SkynetIADSAbstractDCSObjectWrapper:isExist() if self.dcsRepresentation then return self.dcsRepresentation:isExist() else return false end end function SkynetIADSAbstractDCSObjectWrapper:insertToTableIfNotAlreadyAdded(tbl, object) local isAdded = false for i = 1, #tbl do local child = tbl[i] if child == object then isAdded = true end end if isAdded == false then table.insert(tbl, object) end return not isAdded end -- helper code for class inheritance function inheritsFrom( baseClass ) local new_class = {} local class_mt = { __index = new_class } function new_class:create() local newinst = {} setmetatable( newinst, class_mt ) return newinst end if nil ~= baseClass then setmetatable( new_class, { __index = baseClass } ) end -- Implementation of additional OO properties starts here -- -- Return the class object of the instance function new_class:class() return new_class end -- Return the super class object of the instance function new_class:superClass() return baseClass end -- Return true if the caller is an instance of theClass function new_class:isa( theClass ) local b_isa = false local cur_class = new_class while ( nil ~= cur_class ) and ( false == b_isa ) do if cur_class == theClass then b_isa = true else cur_class = cur_class:superClass() end end return b_isa end return new_class end end do SkynetIADSAbstractElement = {} SkynetIADSAbstractElement = inheritsFrom(SkynetIADSAbstractDCSObjectWrapper) function SkynetIADSAbstractElement:create(dcsRepresentation, iads) local instance = self:superClass():create(dcsRepresentation) setmetatable(instance, self) self.__index = self instance.connectionNodes = {} instance.powerSources = {} instance.iads = iads instance.natoName = "UNKNOWN" world.addEventHandler(instance) return instance end function SkynetIADSAbstractElement:removeEventHandlers() world.removeEventHandler(self) end function SkynetIADSAbstractElement:cleanUp() self:removeEventHandlers() end function SkynetIADSAbstractElement:isDestroyed() return self:getDCSRepresentation():isExist() == false end function SkynetIADSAbstractElement:addPowerSource(powerSource) table.insert(self.powerSources, powerSource) self:informChildrenOfStateChange() return self end function SkynetIADSAbstractElement:getPowerSources() return self.powerSources end function SkynetIADSAbstractElement:addConnectionNode(connectionNode) table.insert(self.connectionNodes, connectionNode) self:informChildrenOfStateChange() return self end function SkynetIADSAbstractElement:getConnectionNodes() return self.connectionNodes end function SkynetIADSAbstractElement:hasActiveConnectionNode() local connectionNode = self:genericCheckOneObjectIsAlive(self.connectionNodes) if connectionNode == false and self.iads:getDebugSettings().samNoConnection then self.iads:printOutput(self:getDescription().." no connection to Command Center") end return connectionNode end function SkynetIADSAbstractElement:hasWorkingPowerSource() local power = self:genericCheckOneObjectIsAlive(self.powerSources) if power == false and self.iads:getDebugSettings().hasNoPower then self.iads:printOutput(self:getDescription().." has no power") end return power end function SkynetIADSAbstractElement:getDCSName() return self.dcsName end -- generic function to theck if power plants, command centers, connection nodes are still alive function SkynetIADSAbstractElement:genericCheckOneObjectIsAlive(objects) local isAlive = (#objects == 0) for i = 1, #objects do local object = objects[i] --if we find one object that is not fully destroyed we assume the IADS is still working if object:isExist() then isAlive = true break end end return isAlive end function SkynetIADSAbstractElement:getNatoName() return self.natoName end function SkynetIADSAbstractElement:getDescription() return "IADS ELEMENT: "..self:getDCSName().." | Type: "..tostring(self:getNatoName()) end function SkynetIADSAbstractElement:onEvent(event) --if a unit is destroyed we check to see if its a power plant powering the unit or a connection node if event.id == world.event.S_EVENT_DEAD then if self:hasWorkingPowerSource() == false or self:isDestroyed() then self:goDark() self:informChildrenOfStateChange() end if self:hasActiveConnectionNode() == false then self:informChildrenOfStateChange() end end if event.id == world.event.S_EVENT_SHOT then self:weaponFired(event) end end --placeholder method, can be implemented by subclasses function SkynetIADSAbstractElement:weaponFired(event) end --placeholder method, can be implemented by subclasses function SkynetIADSAbstractElement:goDark() end --placeholder method, can be implemented by subclasses function SkynetIADSAbstractElement:goAutonomous() end --placeholder method, can be implemented by subclasses function SkynetIADSAbstractElement:setToCorrectAutonomousState() end --placeholder method, can be implemented by subclasses function SkynetIADSAbstractElement:informChildrenOfStateChange() end end do SkynetIADSAbstractRadarElement = {} SkynetIADSAbstractRadarElement = inheritsFrom(SkynetIADSAbstractElement) SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DCS_AI = 1 SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK = 2 SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE = 1 SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE = 2 SkynetIADSAbstractRadarElement.HARM_TO_SAM_ASPECT = 15 SkynetIADSAbstractRadarElement.HARM_LOOKAHEAD_NM = 20 function SkynetIADSAbstractRadarElement:create(dcsElementWithRadar, iads) local instance = self:superClass():create(dcsElementWithRadar, iads) setmetatable(instance, self) self.__index = self instance.aiState = false instance.harmScanID = nil instance.harmSilenceID = nil instance.lastJammerUpdate = 0 instance.objectsIdentifiedAsHarms = {} instance.objectsIdentifiedAsHarmsMaxTargetAge = 60 instance.launchers = {} instance.trackingRadars = {} instance.searchRadars = {} instance.parentRadars = {} instance.childRadars = {} instance.missilesInFlight = {} instance.pointDefences = {} instance.harmDecoys = {} instance.autonomousBehaviour = SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DCS_AI instance.goLiveRange = SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE instance.isAutonomous = true instance.harmDetectionChance = 0 instance.minHarmShutdownTime = 0 instance.maxHarmShutDownTime = 0 instance.minHarmPresetShutdownTime = 30 instance.maxHarmPresetShutdownTime = 180 instance.harmShutdownTime = 0 instance.firingRangePercent = 100 instance.actAsEW = false instance.cachedTargets = {} instance.cachedTargetsMaxAge = 1 instance.cachedTargetsCurrentAge = 0 instance.goLiveTime = 0 instance.engageAirWeapons = false instance.isAPointDefence = false instance.canEngageHARM = false instance.dataBaseSupportedTypesCanEngageHARM = false -- 5 seconds seems to be a good value for the sam site to find the target with its organic radar instance.noCacheActiveForSecondsAfterGoLive = 5 return instance end --TODO: this method could be updated to only return Radar weapons fired, this way a SAM firing an IR weapon could go dark faster in the goDark() method function SkynetIADSAbstractRadarElement:weaponFired(event) if event.id == world.event.S_EVENT_SHOT then local weapon = event.weapon local launcherFired = event.initiator for i = 1, #self.launchers do local launcher = self.launchers[i] if launcher:getDCSRepresentation() == launcherFired then table.insert(self.missilesInFlight, weapon) end end end end function SkynetIADSAbstractRadarElement:setCachedTargetsMaxAge(maxAge) self.cachedTargetsMaxAge = maxAge end function SkynetIADSAbstractRadarElement:cleanUp() for i = 1, #self.pointDefences do local pointDefence = self.pointDefences[i] pointDefence:cleanUp() end mist.removeFunction(self.harmScanID) mist.removeFunction(self.harmSilenceID) --call method from super class self:removeEventHandlers() end function SkynetIADSAbstractRadarElement:setIsAPointDefence(state) if (state == true or state == false) then self.isAPointDefence = state end end function SkynetIADSAbstractRadarElement:getIsAPointDefence() return self.isAPointDefence end function SkynetIADSAbstractRadarElement:addPointDefence(pointDefence) table.insert(self.pointDefences, pointDefence) pointDefence:setIsAPointDefence(true) return self end function SkynetIADSAbstractRadarElement:getPointDefences() return self.pointDefences end function SkynetIADSAbstractRadarElement:addHARMDecoy(harmDecoy) table.insert(self.harmDecoys, harmDecoy) end function SkynetIADSAbstractRadarElement:addParentRadar(parentRadar) self:insertToTableIfNotAlreadyAdded(self.parentRadars, parentRadar) self:informChildrenOfStateChange() end function SkynetIADSAbstractRadarElement:getParentRadars() return self.parentRadars end function SkynetIADSAbstractRadarElement:clearParentRadars() self.parentRadars = {} end function SkynetIADSAbstractRadarElement:addChildRadar(childRadar) self:insertToTableIfNotAlreadyAdded(self.childRadars, childRadar) end function SkynetIADSAbstractRadarElement:getChildRadars() return self.childRadars end function SkynetIADSAbstractRadarElement:clearChildRadars() self.childRadars = {} end --TODO: unit test this method function SkynetIADSAbstractRadarElement:getUsableChildRadars() local usableRadars = {} for i = 1, #self.childRadars do local childRadar = self.childRadars[i] if childRadar:hasWorkingPowerSource() and childRadar:hasActiveConnectionNode() then table.insert(usableRadars, childRadar) end end return usableRadars end function SkynetIADSAbstractRadarElement:informChildrenOfStateChange() self:setToCorrectAutonomousState() local children = self:getChildRadars() for i = 1, #children do local childRadar = children[i] childRadar:setToCorrectAutonomousState() end self.iads:getMooseConnector():update() end function SkynetIADSAbstractRadarElement:setToCorrectAutonomousState() local parents = self:getParentRadars() for i = 1, #parents do local parent = parents[i] --of one parent exists that still is connected to the IADS, the SAM site does not have to go autonomous --instead of isDestroyed() write method, hasWorkingSearchRadars() if self:hasActiveConnectionNode() and self.iads:isCommandCenterUsable() and parent:hasWorkingPowerSource() and parent:hasActiveConnectionNode() and parent:getActAsEW() == true and parent:isDestroyed() == false then self:resetAutonomousState() return end end self:goAutonomous() end function SkynetIADSAbstractRadarElement:setAutonomousBehaviour(mode) if mode ~= nil then self.autonomousBehaviour = mode end return self end function SkynetIADSAbstractRadarElement:getAutonomousBehaviour() return self.autonomousBehaviour end function SkynetIADSAbstractRadarElement:resetAutonomousState() self.isAutonomous = false self:goDark() end function SkynetIADSAbstractRadarElement:goAutonomous() self.isAutonomous = true if self.autonomousBehaviour == SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK then self:goDark() else self:goLive() end end function SkynetIADSAbstractRadarElement:getAutonomousState() return self.isAutonomous end function SkynetIADSAbstractRadarElement:pointDefencesHaveRemainingAmmo(minNumberOfMissiles) local remainingMissiles = 0 for i = 1, #self.pointDefences do local pointDefence = self.pointDefences[i] remainingMissiles = remainingMissiles + pointDefence:getRemainingNumberOfMissiles() end return self:hasRequiredNumberOfMissiles(minNumberOfMissiles, remainingMissiles) end function SkynetIADSAbstractRadarElement:hasRequiredNumberOfMissiles(minNumberOfMissiles, remainingMissiles) local returnValue = false if ( remainingMissiles > 0 and remainingMissiles >= minNumberOfMissiles ) then returnValue = true end return returnValue end function SkynetIADSAbstractRadarElement:hasRemainingAmmoToEngageMissiles(minNumberOfMissiles) local remainingMissiles = self:getRemainingNumberOfMissiles() return self:hasRequiredNumberOfMissiles(minNumberOfMissiles, remainingMissiles) end -- this method needs to be refactored so that it works for ew radars that don't have launchers, or that it is only called by sam sites function SkynetIADSAbstractRadarElement:hasEnoughLaunchersToEngageMissiles(minNumberOfLaunchers) local launchers = self:getLaunchers() if(launchers ~= nil) then launchers = #self:getLaunchers() else launchers = 0 end return self:hasRequiredNumberOfMissiles(minNumberOfLaunchers, launchers) end function SkynetIADSAbstractRadarElement:pointDefencesHaveEnoughLaunchers(minNumberOfLaunchers) local numOfLaunchers = 0 for i = 1, #self.pointDefences do local pointDefence = self.pointDefences[i] numOfLaunchers = numOfLaunchers + #pointDefence:getLaunchers() end return self:hasRequiredNumberOfMissiles(minNumberOfLaunchers, numOfLaunchers) end function SkynetIADSAbstractRadarElement:setIgnoreHARMSWhilePointDefencesHaveAmmo(state) self.iads:printOutputToLog("DEPRECATED: setIgnoreHARMSWhilePointDefencesHaveAmmo SAM Site will stay live automaticall as long as itself or it's point defences can defend against a HARM") return self end function SkynetIADSAbstractRadarElement:hasMissilesInFlight() return #self.missilesInFlight > 0 end function SkynetIADSAbstractRadarElement:getNumberOfMissilesInFlight() return #self.missilesInFlight end -- DCS does not send an event, when a missile is destroyed, so this method needs to be polled so that the missiles in flight are current, polling is done in the HARM Search call: evaluateIfTargetsContainHARMs function SkynetIADSAbstractRadarElement:updateMissilesInFlight() local missilesInFlight = {} for i = 1, #self.missilesInFlight do local missile = self.missilesInFlight[i] if missile:isExist() then table.insert(missilesInFlight, missile) end end self.missilesInFlight = missilesInFlight self:goDarkIfOutOfAmmo() end function SkynetIADSAbstractRadarElement:goDarkIfOutOfAmmo() if self:hasRemainingAmmo() == false and self:getActAsEW() == false then self:goDark() end end function SkynetIADSAbstractRadarElement:getActAsEW() return self.actAsEW end function SkynetIADSAbstractRadarElement:setActAsEW(ewState) if ewState == true or ewState == false then local stateChange = false if ewState ~= self.actAsEW then stateChange = true end self.actAsEW = ewState if stateChange then self:informChildrenOfStateChange() end end if self.actAsEW == true then self:goLive() else self:goDark() end return self end function SkynetIADSAbstractRadarElement:getUnitsToAnalyse() local units = {} table.insert(units, self:getDCSRepresentation()) if getmetatable(self:getDCSRepresentation()) == Group then units = self:getDCSRepresentation():getUnits() end return units end function SkynetIADSAbstractRadarElement:getRemainingNumberOfMissiles() local remainingNumberOfMissiles = 0 for i = 1, #self.launchers do local launcher = self.launchers[i] remainingNumberOfMissiles = remainingNumberOfMissiles + launcher:getRemainingNumberOfMissiles() end return remainingNumberOfMissiles end function SkynetIADSAbstractRadarElement:getInitialNumberOfMissiles() local initalNumberOfMissiles = 0 for i = 1, #self.launchers do local launcher = self.launchers[i] initalNumberOfMissiles = launcher:getInitialNumberOfMissiles() + initalNumberOfMissiles end return initalNumberOfMissiles end function SkynetIADSAbstractRadarElement:getRemainingNumberOfShells() local remainingNumberOfShells = 0 for i = 1, #self.launchers do local launcher = self.launchers[i] remainingNumberOfShells = remainingNumberOfShells + launcher:getRemainingNumberOfShells() end return remainingNumberOfShells end function SkynetIADSAbstractRadarElement:getInitialNumberOfShells() local initialNumberOfShells = 0 for i = 1, #self.launchers do local launcher = self.launchers[i] initialNumberOfShells = initialNumberOfShells + launcher:getInitialNumberOfShells() end return initialNumberOfShells end function SkynetIADSAbstractRadarElement:hasRemainingAmmo() --the launcher check is due to ew radars they have no launcher and no ammo and therefore are never out of ammo return ( #self.launchers == 0 ) or ((self:getRemainingNumberOfMissiles() > 0 ) or ( self:getRemainingNumberOfShells() > 0 ) ) end function SkynetIADSAbstractRadarElement:getHARMDetectionChance() return self.harmDetectionChance end function SkynetIADSAbstractRadarElement:setHARMDetectionChance(chance) if chance and chance >= 0 and chance <= 100 then self.harmDetectionChance = chance end return self end function SkynetIADSAbstractRadarElement:setupElements() local numUnits = #self:getUnitsToAnalyse() for typeName, dataType in pairs(SkynetIADS.database) do local hasSearchRadar = false local hasTrackingRadar = false local hasLauncher = false self.searchRadars = {} self.trackingRadars = {} self.launchers = {} for entry, unitData in pairs(dataType) do if entry == 'searchRadar' then self:analyseAndAddUnit(SkynetIADSSAMSearchRadar, self.searchRadars, unitData) hasSearchRadar = true end if entry == 'launchers' then self:analyseAndAddUnit(SkynetIADSSAMLauncher, self.launchers, unitData) hasLauncher = true end if entry == 'trackingRadar' then self:analyseAndAddUnit(SkynetIADSSAMTrackingRadar, self.trackingRadars, unitData) hasTrackingRadar = true end end --this check ensures a unit or group has all required elements for the specific sam or ew type: if (hasLauncher and hasSearchRadar and hasTrackingRadar and #self.launchers > 0 and #self.searchRadars > 0 and #self.trackingRadars > 0 ) or (hasSearchRadar and hasLauncher and #self.searchRadars > 0 and #self.launchers > 0) then self:setHARMDetectionChance(dataType['harm_detection_chance']) self.dataBaseSupportedTypesCanEngageHARM = dataType['can_engage_harm'] self:setCanEngageHARM(self.dataBaseSupportedTypesCanEngageHARM) local natoName = dataType['name']['NATO'] self:buildNatoName(natoName) break end end end function SkynetIADSAbstractRadarElement:setCanEngageHARM(canEngage) if canEngage == true or canEngage == false then self.canEngageHARM = canEngage if ( canEngage == true and self:getCanEngageAirWeapons() == false ) then self:setCanEngageAirWeapons(true) end end return self end function SkynetIADSAbstractRadarElement:getCanEngageHARM() return self.canEngageHARM end function SkynetIADSAbstractRadarElement:setCanEngageAirWeapons(engageAirWeapons) if self:isDestroyed() == false then local controller = self:getDCSRepresentation():getController() if ( engageAirWeapons == true ) then controller:setOption(AI.Option.Ground.id.ENGAGE_AIR_WEAPONS, true) --its important that we set var to true here, to prevent recursion in setCanEngageHARM self.engageAirWeapons = true --we set the original value we got when loading info about the SAM site self:setCanEngageHARM(self.dataBaseSupportedTypesCanEngageHARM) else controller:setOption(AI.Option.Ground.id.ENGAGE_AIR_WEAPONS, false) self:setCanEngageHARM(false) self.engageAirWeapons = false end end return self end function SkynetIADSAbstractRadarElement:getCanEngageAirWeapons() return self.engageAirWeapons end function SkynetIADSAbstractRadarElement:buildNatoName(natoName) --we shorten the SA-XX names and don't return their code names eg goa, gainful.. local pos = natoName:find(" ") local prefix = natoName:sub(1, 2) if string.lower(prefix) == 'sa' and pos ~= nil then self.natoName = natoName:sub(1, (pos-1)) else self.natoName = natoName end end function SkynetIADSAbstractRadarElement:analyseAndAddUnit(class, tableToAdd, unitData) local units = self:getUnitsToAnalyse() for i = 1, #units do local unit = units[i] self:buildSingleUnit(unit, class, tableToAdd, unitData) end end function SkynetIADSAbstractRadarElement:buildSingleUnit(unit, class, tableToAdd, unitData) local unitTypeName = unit:getTypeName() for unitName, unitPerformanceData in pairs(unitData) do if unitName == unitTypeName then samElement = class:create(unit) samElement:setupRangeData() table.insert(tableToAdd, samElement) end end end function SkynetIADSAbstractRadarElement:getController() local dcsRepresentation = self:getDCSRepresentation() if dcsRepresentation:isExist() then return dcsRepresentation:getController() else return nil end end function SkynetIADSAbstractRadarElement:getLaunchers() return self.launchers end function SkynetIADSAbstractRadarElement:getSearchRadars() return self.searchRadars end function SkynetIADSAbstractRadarElement:getTrackingRadars() return self.trackingRadars end function SkynetIADSAbstractRadarElement:getRadars() local radarUnits = {} for i = 1, #self.searchRadars do table.insert(radarUnits, self.searchRadars[i]) end for i = 1, #self.trackingRadars do table.insert(radarUnits, self.trackingRadars[i]) end return radarUnits end function SkynetIADSAbstractRadarElement:setGoLiveRangeInPercent(percent) if percent ~= nil then self.firingRangePercent = percent for i = 1, #self.launchers do local launcher = self.launchers[i] launcher:setFiringRangePercent(self.firingRangePercent) end for i = 1, #self.searchRadars do local radar = self.searchRadars[i] radar:setFiringRangePercent(self.firingRangePercent) end end return self end function SkynetIADSAbstractRadarElement:getGoLiveRangeInPercent() return self.firingRangePercent end function SkynetIADSAbstractRadarElement:setEngagementZone(engagementZone) if engagementZone == SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE then self.goLiveRange = engagementZone elseif engagementZone == SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE then self.goLiveRange = engagementZone end return self end function SkynetIADSAbstractRadarElement:getEngagementZone() return self.goLiveRange end function SkynetIADSAbstractRadarElement:goLive() if ( self.aiState == false and self:hasWorkingPowerSource() and self.harmSilenceID == nil) and (self:hasRemainingAmmo() == true ) then if self:isDestroyed() == false then local cont = self:getController() cont:setOnOff(true) cont:setOption(AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.RED) cont:setOption(AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_FREE) self:getDCSRepresentation():enableEmission(true) self.goLiveTime = timer.getTime() self.aiState = true end self:pointDefencesStopActingAsEW() if self.iads:getDebugSettings().radarWentLive then self.iads:printOutputToLog("GOING LIVE: "..self:getDescription()) end self:scanForHarms() end end function SkynetIADSAbstractRadarElement:pointDefencesStopActingAsEW() for i = 1, #self.pointDefences do local pointDefence = self.pointDefences[i] pointDefence:setActAsEW(false) end end function SkynetIADSAbstractRadarElement:goDark() if (self:hasWorkingPowerSource() == false) or ( self.aiState == true ) and (self.harmSilenceID ~= nil or ( self.harmSilenceID == nil and #self:getDetectedTargets() == 0 and self:hasMissilesInFlight() == false) or ( self.harmSilenceID == nil and #self:getDetectedTargets() > 0 and self:hasMissilesInFlight() == false and self:hasRemainingAmmo() == false ) ) then if self:isDestroyed() == false then self:getDCSRepresentation():enableEmission(false) end -- point defence will only go live if the Radar Emitting site it is protecting goes dark and this is due to a it defending against a HARM if (self.harmSilenceID ~= nil) then self:pointDefencesGoLive() if self:isDestroyed() == false then --if site goes dark due to HARM we turn off AI, this is due to a bug in DCS multiplayer where the harm will find its way to the radar emitter if just setEmissions is set to false local controller = self:getController() controller:setOnOff(false) end end self.aiState = false self:stopScanningForHARMs() self.cachedTargets = {} if self.iads:getDebugSettings().radarWentDark then self.iads:printOutputToLog("GOING DARK: "..self:getDescription()) end end end function SkynetIADSAbstractRadarElement:pointDefencesGoLive() local setActive = false for i = 1, #self.pointDefences do local pointDefence = self.pointDefences[i] if ( pointDefence:getActAsEW() == false ) then setActive = true pointDefence:setActAsEW(true) end end return setActive end function SkynetIADSAbstractRadarElement:isActive() return self.aiState end function SkynetIADSAbstractRadarElement:isTargetInRange(target) local isSearchRadarInRange = false local isTrackingRadarInRange = false local isLauncherInRange = false local isSearchRadarInRange = ( #self.searchRadars == 0 ) for i = 1, #self.searchRadars do local searchRadar = self.searchRadars[i] if searchRadar:isInRange(target) then isSearchRadarInRange = true break end end if self.goLiveRange == SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE then isLauncherInRange = ( #self.launchers == 0 ) for i = 1, #self.launchers do local launcher = self.launchers[i] if launcher:isInRange(target) then isLauncherInRange = true break end end isTrackingRadarInRange = ( #self.trackingRadars == 0 ) for i = 1, #self.trackingRadars do local trackingRadar = self.trackingRadars[i] if trackingRadar:isInRange(target) then isTrackingRadarInRange = true break end end else isLauncherInRange = true isTrackingRadarInRange = true end return (isSearchRadarInRange and isTrackingRadarInRange and isLauncherInRange ) end function SkynetIADSAbstractRadarElement:isInRadarDetectionRangeOf(abstractRadarElement) local radars = self:getRadars() local abstractRadarElementRadars = abstractRadarElement:getRadars() for i = 1, #radars do local radar = radars[i] for j = 1, #abstractRadarElementRadars do local abstractRadarElementRadar = abstractRadarElementRadars[j] if abstractRadarElementRadar:isExist() and radar:isExist() then local distance = self:getDistanceToUnit(radar:getDCSRepresentation():getPosition().p, abstractRadarElementRadar:getDCSRepresentation():getPosition().p) if abstractRadarElementRadar:getMaxRangeFindingTarget() >= distance then return true end end end end return false end function SkynetIADSAbstractRadarElement:getDistanceToUnit(unitPosA, unitPosB) return mist.utils.round(mist.utils.get2DDist(unitPosA, unitPosB, 0)) end function SkynetIADSAbstractRadarElement:hasWorkingRadar() local radars = self:getRadars() for i = 1, #radars do local radar = radars[i] if radar:isRadarWorking() == true then return true end end return false end function SkynetIADSAbstractRadarElement:jam(successProbability) if self:isDestroyed() == false then local controller = self:getController() local probability = math.random(1, 100) if self.iads:getDebugSettings().jammerProbability then self.iads:printOutputToLog("JAMMER: "..self:getDescription()..": Probability: "..successProbability) end if successProbability > probability then controller:setOption(AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD) if self.iads:getDebugSettings().jammerProbability then self.iads:printOutputToLog("JAMMER: "..self:getDescription()..": jammed, setting to weapon hold") end else controller:setOption(AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_FREE) if self.iads:getDebugSettings().jammerProbability then self.iads:printOutputToLog("JAMMER: "..self:getDescription()..": jammed, setting to weapon free") end end self.lastJammerUpdate = timer:getTime() end end function SkynetIADSAbstractRadarElement:scanForHarms() self:stopScanningForHARMs() self.harmScanID = mist.scheduleFunction(SkynetIADSAbstractRadarElement.evaluateIfTargetsContainHARMs, {self}, 1, 2) end function SkynetIADSAbstractRadarElement:isScanningForHARMs() return self.harmScanID ~= nil end function SkynetIADSAbstractRadarElement:isDefendingHARM() return self.harmSilenceID ~= nil end function SkynetIADSAbstractRadarElement:stopScanningForHARMs() mist.removeFunction(self.harmScanID) self.harmScanID = nil end function SkynetIADSAbstractRadarElement:goSilentToEvadeHARM(timeToImpact) self:finishHarmDefence(self) if ( timeToImpact == nil ) then timeToImpact = 0 end self.minHarmShutdownTime = self:calculateMinimalShutdownTimeInSeconds(timeToImpact) self.maxHarmShutDownTime = self:calculateMaximalShutdownTimeInSeconds(self.minHarmShutdownTime) self.harmShutdownTime = self:calculateHARMShutdownTime() if self.iads:getDebugSettings().harmDefence then self.iads:printOutputToLog("HARM DEFENCE SHUTTING DOWN: "..self:getDCSName().." | FOR: "..self.harmShutdownTime.." seconds | TTI: "..timeToImpact) end self.harmSilenceID = mist.scheduleFunction(SkynetIADSAbstractRadarElement.finishHarmDefence, {self}, timer.getTime() + self.harmShutdownTime, 1) self:goDark() end function SkynetIADSAbstractRadarElement:getHARMShutdownTime() return self.harmShutdownTime end function SkynetIADSAbstractRadarElement:calculateHARMShutdownTime() local shutDownTime = math.random(self.minHarmShutdownTime, self.maxHarmShutDownTime) return shutDownTime end function SkynetIADSAbstractRadarElement.finishHarmDefence(self) mist.removeFunction(self.harmSilenceID) self.harmSilenceID = nil self.harmShutdownTime = 0 if ( self:getAutonomousState() == true ) then self:goAutonomous() end end function SkynetIADSAbstractRadarElement:getDetectedTargets() if ( timer.getTime() - self.cachedTargetsCurrentAge > self.cachedTargetsMaxAge ) or ( timer.getTime() - self.goLiveTime < self.noCacheActiveForSecondsAfterGoLive ) then self.cachedTargets = {} self.cachedTargetsCurrentAge = timer.getTime() if self:hasWorkingPowerSource() and self:isDestroyed() == false then local targets = self:getController():getDetectedTargets(Controller.Detection.RADAR) for i = 1, #targets do local target = targets[i] -- there are cases when a destroyed object is still visible as a target to the radar, don't add it, will cause errors everywhere the dcs object is accessed if target.object then local iadsTarget = SkynetIADSContact:create(target, self) iadsTarget:refresh() if self:isTargetInRange(iadsTarget) then table.insert(self.cachedTargets, iadsTarget) end end end end end return self.cachedTargets end function SkynetIADSAbstractRadarElement:getSecondsToImpact(distanceNM, speedKT) local tti = 0 if speedKT > 0 then tti = mist.utils.round((distanceNM / speedKT) * 3600, 0) if tti < 0 then tti = 0 end end return tti end function SkynetIADSAbstractRadarElement:getDistanceInMetersToContact(radarUnit, point) return mist.utils.round(mist.utils.get3DDist(radarUnit:getPosition().p, point), 0) end function SkynetIADSAbstractRadarElement:calculateMinimalShutdownTimeInSeconds(timeToImpact) return timeToImpact + self.minHarmPresetShutdownTime end function SkynetIADSAbstractRadarElement:calculateMaximalShutdownTimeInSeconds(minShutdownTime) return minShutdownTime + mist.random(1, self.maxHarmPresetShutdownTime) end function SkynetIADSAbstractRadarElement:calculateImpactPoint(target, distanceInMeters) -- distance needs to be incremented by a certain value for ip calculation to work, check why presumably due to rounding errors in the previous distance calculation return land.getIP(target:getPosition().p, target:getPosition().x, distanceInMeters + 50) end function SkynetIADSAbstractRadarElement:shallReactToHARM() return self.harmDetectionChance >= math.random(1, 100) end -- will only check for missiles, if DCS ads AAA than can engage HARMs then this code must be updated: function SkynetIADSAbstractRadarElement:shallIgnoreHARMShutdown() local numOfHarms = self:getNumberOfObjectsItentifiedAsHARMS() --[[ self.iads:printOutputToLog("Self enough launchers: "..tostring(self:hasEnoughLaunchersToEngageMissiles(numOfHarms))) self.iads:printOutputToLog("Self enough missiles: "..tostring(self:hasRemainingAmmoToEngageMissiles(numOfHarms))) self.iads:printOutputToLog("PD enough missiles: "..tostring(self:pointDefencesHaveRemainingAmmo(numOfHarms))) self.iads:printOutputToLog("PD enough launchers: "..tostring(self:pointDefencesHaveEnoughLaunchers(numOfHarms))) --]] return ( ((self:hasEnoughLaunchersToEngageMissiles(numOfHarms) and self:hasRemainingAmmoToEngageMissiles(numOfHarms) and self:getCanEngageHARM()) or (self:pointDefencesHaveRemainingAmmo(numOfHarms) and self:pointDefencesHaveEnoughLaunchers(numOfHarms)))) end function SkynetIADSAbstractRadarElement:informOfHARM(harmContact) local radars = self:getRadars() for j = 1, #radars do local radar = radars[j] if radar:isExist() then local distanceNM = mist.utils.metersToNM(self:getDistanceInMetersToContact(radar, harmContact:getPosition().p)) local harmToSAMHeading = mist.utils.toDegree(mist.utils.getHeadingPoints(harmContact:getPosition().p, radar:getPosition().p)) local harmToSAMAspect = self:calculateAspectInDegrees(harmContact:getMagneticHeading(), harmToSAMHeading) local speedKT = harmContact:getGroundSpeedInKnots(0) local secondsToImpact = self:getSecondsToImpact(distanceNM, speedKT) --TODO: use tti instead of distanceNM? -- when iterating through the radars, store shortest tti and work with that value?? if ( harmToSAMAspect < SkynetIADSAbstractRadarElement.HARM_TO_SAM_ASPECT and distanceNM < SkynetIADSAbstractRadarElement.HARM_LOOKAHEAD_NM ) then self:addObjectIdentifiedAsHARM(harmContact) if ( #self:getPointDefences() > 0 and self:pointDefencesGoLive() == true and self.iads:getDebugSettings().harmDefence ) then self.iads:printOutputToLog("POINT DEFENCES GOING LIVE FOR: "..self:getDCSName().." | TTI: "..secondsToImpact) end --self.iads:printOutputToLog("Ignore HARM shutdown: "..tostring(self:shallIgnoreHARMShutdown())) if ( self:getIsAPointDefence() == false and ( self:isDefendingHARM() == false or ( self:getHARMShutdownTime() < secondsToImpact ) ) and self:shallIgnoreHARMShutdown() == false) then self:goSilentToEvadeHARM(secondsToImpact) break end end end end end function SkynetIADSAbstractElement:addObjectIdentifiedAsHARM(harmContact) self:insertToTableIfNotAlreadyAdded(self.objectsIdentifiedAsHarms, harmContact) end function SkynetIADSAbstractRadarElement:calculateAspectInDegrees(harmHeading, harmToSAMHeading) local aspect = harmHeading - harmToSAMHeading if ( aspect < 0 ) then aspect = -1 * aspect end if aspect > 180 then aspect = 360 - aspect end return mist.utils.round(aspect) end function SkynetIADSAbstractRadarElement:getNumberOfObjectsItentifiedAsHARMS() return #self.objectsIdentifiedAsHarms end function SkynetIADSAbstractRadarElement:cleanUpOldObjectsIdentifiedAsHARMS() local newHARMS = {} for i = 1, #self.objectsIdentifiedAsHarms do local harmContact = self.objectsIdentifiedAsHarms[i] if harmContact:getAge() < self.objectsIdentifiedAsHarmsMaxTargetAge then table.insert(newHARMS, harmContact) end end --stop point defences acting as ew (always on), will occur if activated via evaluateIfTargetsContainHARMs() --if in this iteration all harms where cleared we turn of the point defence. But in any other cases we dont turn of point defences, that interferes with other parts of the iads -- when setting up the iads (letting pds go to read state) if (#newHARMS == 0 and self:getNumberOfObjectsItentifiedAsHARMS() > 0 ) then self:pointDefencesStopActingAsEW() end self.objectsIdentifiedAsHarms = newHARMS end function SkynetIADSAbstractRadarElement.evaluateIfTargetsContainHARMs(self) --if an emitter dies the SAM site being jammed will revert back to normal operation: if self.lastJammerUpdate > 0 and ( timer:getTime() - self.lastJammerUpdate ) > 10 then self:jam(0) self.lastJammerUpdate = 0 end --we use the regular interval of this method to update to other states: self:updateMissilesInFlight() self:cleanUpOldObjectsIdentifiedAsHARMS() end end do --this class is currently used for AWACS and Ships, at a latter date a separate class for ships could be created, currently not needed SkynetIADSAWACSRadar = {} SkynetIADSAWACSRadar = inheritsFrom(SkynetIADSAbstractRadarElement) function SkynetIADSAWACSRadar:create(radarUnit, iads) local instance = self:superClass():create(radarUnit, iads) setmetatable(instance, self) self.__index = self instance.lastUpdatePosition = nil instance.natoName = radarUnit:getTypeName() return instance end function SkynetIADSAWACSRadar:setupElements() local unit = self:getDCSRepresentation() local radar = SkynetIADSSAMSearchRadar:create(unit) radar:setupRangeData() table.insert(self.searchRadars, radar) end -- AWACs will not scan for HARMS function SkynetIADSAWACSRadar:scanForHarms() end function SkynetIADSAWACSRadar:getMaxAllowedMovementForAutonomousUpdateInNM() --local radarRange = mist.utils.metersToNM(self.searchRadars[1]:getMaxRangeFindingTarget()) --return mist.utils.round(radarRange / 10) --fixed to 10 nm miles to better fit small SAM sites return 10 end function SkynetIADSAWACSRadar:isUpdateOfAutonomousStateOfSAMSitesRequired() local isUpdateRequired = self:getDistanceTraveledSinceLastUpdate() > self:getMaxAllowedMovementForAutonomousUpdateInNM() if isUpdateRequired then self.lastUpdatePosition = nil end return isUpdateRequired end function SkynetIADSAWACSRadar:getDistanceTraveledSinceLastUpdate() local currentPosition = nil if self.lastUpdatePosition == nil and self:getDCSRepresentation():isExist() then self.lastUpdatePosition = self:getDCSRepresentation():getPosition().p end if self:getDCSRepresentation():isExist() then currentPosition = self:getDCSRepresentation():getPosition().p end return mist.utils.round(mist.utils.metersToNM(self:getDistanceToUnit(self.lastUpdatePosition, currentPosition))) end end do SkynetIADSCommandCenter = {} SkynetIADSCommandCenter = inheritsFrom(SkynetIADSAbstractRadarElement) function SkynetIADSCommandCenter:create(commandCenter, iads) local instance = self:superClass():create(commandCenter, iads) setmetatable(instance, self) self.__index = self instance.natoName = "COMMAND CENTER" return instance end function SkynetIADSCommandCenter:goDark() end function SkynetIADSCommandCenter:goLive() end end do SkynetIADSContact = {} SkynetIADSContact = inheritsFrom(SkynetIADSAbstractDCSObjectWrapper) SkynetIADSContact.CLIMB = "CLIMB" SkynetIADSContact.DESCEND = "DESCEND" SkynetIADSContact.HARM = "HARM" SkynetIADSContact.NOT_HARM = "NOT_HARM" SkynetIADSContact.HARM_UNKNOWN = "HARM_UNKNOWN" function SkynetIADSContact:create(dcsRadarTarget, abstractRadarElementDetected) local instance = self:superClass():create(dcsRadarTarget.object) setmetatable(instance, self) self.__index = self instance.abstractRadarElementsDetected = {} table.insert(instance.abstractRadarElementsDetected, abstractRadarElementDetected) instance.firstContactTime = timer.getAbsTime() instance.lastTimeSeen = 0 instance.dcsRadarTarget = dcsRadarTarget instance.position = instance:getDCSRepresentation():getPosition() instance.numOfTimesRefreshed = 0 instance.speed = 0 instance.harmState = SkynetIADSContact.HARM_UNKNOWN instance.simpleAltitudeProfile = {} return instance end function SkynetIADSContact:setHARMState(state) self.harmState = state end function SkynetIADSContact:getHARMState() return self.harmState end function SkynetIADSContact:isIdentifiedAsHARM() return self.harmState == SkynetIADSContact.HARM end function SkynetIADSContact:isHARMStateUnknown() return self.harmState == SkynetIADSContact.HARM_UNKNOWN end function SkynetIADSContact:getMagneticHeading() if ( self:isExist() ) then return mist.utils.round(mist.utils.toDegree(mist.getHeading(self:getDCSRepresentation()))) else return -1 end end function SkynetIADSContact:getAbstractRadarElementsDetected() return self.abstractRadarElementsDetected end function SkynetIADSContact:addAbstractRadarElementDetected(radar) self:insertToTableIfNotAlreadyAdded(self.abstractRadarElementsDetected, radar) end function SkynetIADSContact:isTypeKnown() return self.dcsRadarTarget.type end function SkynetIADSContact:isDistanceKnown() return self.dcsRadarTarget.distance end function SkynetIADSContact:getTypeName() if self:isIdentifiedAsHARM() then return SkynetIADSContact.HARM end if self:getDCSRepresentation() ~= nil then local category = self:getDCSRepresentation():getCategory() if category == Object.Category.UNIT then return self.typeName end end return "UNKNOWN" end function SkynetIADSContact:getPosition() return self.position end function SkynetIADSContact:getGroundSpeedInKnots(decimals) if decimals == nil then decimals = 2 end return mist.utils.round(self.speed, decimals) end function SkynetIADSContact:getHeightInFeetMSL() if self:isExist() then return mist.utils.round(mist.utils.metersToFeet(self:getDCSRepresentation():getPosition().p.y), 0) else return 0 end end function SkynetIADSContact:getDesc() if self:isExist() then return self:getDCSRepresentation():getDesc() else return {} end end function SkynetIADSContact:getNumberOfTimesHitByRadar() return self.numOfTimesRefreshed end function SkynetIADSContact:refresh() if self:isExist() then local timeDelta = (timer.getAbsTime() - self.lastTimeSeen) if timeDelta > 0 then self.numOfTimesRefreshed = self.numOfTimesRefreshed + 1 local distance = mist.utils.metersToNM(mist.utils.get2DDist(self.position.p, self:getDCSRepresentation():getPosition().p)) local hours = timeDelta / 3600 self.speed = (distance / hours) self:updateSimpleAltitudeProfile() self.position = self:getDCSRepresentation():getPosition() end end self.lastTimeSeen = timer.getAbsTime() end function SkynetIADSContact:updateSimpleAltitudeProfile() local currentAltitude = self:getDCSRepresentation():getPosition().p.y local previousPath = "" if #self.simpleAltitudeProfile > 0 then previousPath = self.simpleAltitudeProfile[#self.simpleAltitudeProfile] end if self.position.p.y > currentAltitude and previousPath ~= SkynetIADSContact.DESCEND then table.insert(self.simpleAltitudeProfile, SkynetIADSContact.DESCEND) elseif self.position.p.y < currentAltitude and previousPath ~= SkynetIADSContact.CLIMB then table.insert(self.simpleAltitudeProfile, SkynetIADSContact.CLIMB) end end function SkynetIADSContact:getSimpleAltitudeProfile() return self.simpleAltitudeProfile end function SkynetIADSContact:getAge() return mist.utils.round(timer.getAbsTime() - self.lastTimeSeen) end end do SkynetIADSEWRadar = {} SkynetIADSEWRadar = inheritsFrom(SkynetIADSAbstractRadarElement) function SkynetIADSEWRadar:create(radarUnit, iads) local instance = self:superClass():create(radarUnit, iads) setmetatable(instance, self) self.__index = self instance.autonomousBehaviour = SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK return instance end function SkynetIADSEWRadar:setupElements() local unit = self:getDCSRepresentation() local unitType = unit:getTypeName() for typeName, dataType in pairs(SkynetIADS.database) do for entry, unitData in pairs(dataType) do if entry == 'searchRadar' then --buildSingleUnit checks to make sure the EW radar is defined in the Skynet database. If it is not, self.searchRadars will be 0 so no ew radar will be added self:buildSingleUnit(unit, SkynetIADSSAMSearchRadar, self.searchRadars, unitData) if #self.searchRadars > 0 then local harmDetection = dataType['harm_detection_chance'] self:setHARMDetectionChance(harmDetection) if unitData[unitType]['name'] then local natoName = unitData[unitType]['name']['NATO'] self:buildNatoName(natoName) end return end end end end end --an Early Warning Radar has simplified check to determine if its autonomous or not function SkynetIADSEWRadar:setToCorrectAutonomousState() if self:hasActiveConnectionNode() and self:hasWorkingPowerSource() and self.iads:isCommandCenterUsable() then self:resetAutonomousState() self:goLive() end if self:hasActiveConnectionNode() == false or self.iads:isCommandCenterUsable() == false then self:goAutonomous() end end end do SkynetIADSJammer = {} SkynetIADSJammer.__index = SkynetIADSJammer function SkynetIADSJammer:create(emitter, iads) local jammer = {} setmetatable(jammer, SkynetIADSJammer) jammer.radioMenu = nil jammer.emitter = emitter jammer.jammerTaskID = nil jammer.iads = {iads} jammer.maximumEffectiveDistanceNM = 200 --jammer probability settings are stored here, visualisation, see: https://docs.google.com/spreadsheets/d/16rnaU49ZpOczPEsdGJ6nfD0SLPxYLEYKmmo4i2Vfoe0/edit#gid=0 jammer.jammerTable = { ['SA-2'] = { ['function'] = function(distanceNauticalMiles) return ( 1.4 ^ distanceNauticalMiles ) + 90 end, ['canjam'] = true, }, ['SA-3'] = { ['function'] = function(distanceNauticalMiles) return ( 1.4 ^ distanceNauticalMiles ) + 80 end, ['canjam'] = true, }, ['SA-6'] = { ['function'] = function(distanceNauticalMiles) return ( 1.4 ^ distanceNauticalMiles ) + 23 end, ['canjam'] = true, }, ['SA-8'] = { ['function'] = function(distanceNauticalMiles) return ( 1.35 ^ distanceNauticalMiles ) + 30 end, ['canjam'] = true, }, ['SA-10'] = { ['function'] = function(distanceNauticalMiles) return ( 1.07 ^ (distanceNauticalMiles / 1.13) ) + 5 end, ['canjam'] = true, }, ['SA-11'] = { ['function'] = function(distanceNauticalMiles) return ( 1.25 ^ distanceNauticalMiles ) + 15 end, ['canjam'] = true, }, ['SA-15'] = { ['function'] = function(distanceNauticalMiles) return ( 1.15 ^ distanceNauticalMiles ) + 5 end, ['canjam'] = true, }, } return jammer end function SkynetIADSJammer:masterArmOn() self:masterArmSafe() self.jammerTaskID = mist.scheduleFunction(SkynetIADSJammer.runCycle, {self}, 1, 10) end function SkynetIADSJammer:addFunction(natoName, jammerFunction) self.jammerTable[natoName] = { ['function'] = jammerFunction, ['canjam'] = true } end function SkynetIADSJammer:setMaximumEffectiveDistance(distance) self.maximumEffectiveDistanceNM = distance end function SkynetIADSJammer:disableFor(natoName) self.jammerTable[natoName]['canjam'] = false end function SkynetIADSJammer:isKnownRadarEmitter(natoName) local isActive = false for unitName, unit in pairs(self.jammerTable) do if unitName == natoName and unit['canjam'] == true then isActive = true end end return isActive end function SkynetIADSJammer:addIADS(iads) table.insert(self.iads, iads) end function SkynetIADSJammer:getSuccessProbability(distanceNauticalMiles, natoName) local probability = 0 local jammerSettings = self.jammerTable[natoName] if jammerSettings ~= nil then probability = jammerSettings['function'](distanceNauticalMiles) end return probability end function SkynetIADSJammer:getDistanceNMToRadarUnit(radarUnit) return mist.utils.metersToNM(mist.utils.get3DDist(self.emitter:getPosition().p, radarUnit:getPosition().p)) end function SkynetIADSJammer.runCycle(self) if self.emitter:isExist() == false then self:masterArmSafe() return end for i = 1, #self.iads do local iads = self.iads[i] local samSites = iads:getActiveSAMSites() for j = 1, #samSites do local samSite = samSites[j] local radars = samSite:getRadars() local hasLOS = false local distance = 0 local natoName = samSite:getNatoName() for l = 1, #radars do local radar = radars[l] distance = self:getDistanceNMToRadarUnit(radar) -- I try to emulate the system as it would work in real life, so a jammer can only jam a SAM site if has line of sight to at least one radar in the group if self:isKnownRadarEmitter(natoName) and self:hasLineOfSightToRadar(radar) and distance <= self.maximumEffectiveDistanceNM then if iads:getDebugSettings().jammerProbability then iads:printOutput("JAMMER: Distance: "..distance) end samSite:jam(self:getSuccessProbability(distance, natoName)) end end end end end function SkynetIADSJammer:hasLineOfSightToRadar(radar) local radarPos = radar:getPosition().p --lift the radar 30 meters off the ground, some 3d models are dug in to the ground, creating issues in calculating LOS radarPos.y = radarPos.y + 30 return land.isVisible(radarPos, self.emitter:getPosition().p) end function SkynetIADSJammer:masterArmSafe() mist.removeFunction(self.jammerTaskID) end --TODO: Remove Menu when emitter dies: function SkynetIADSJammer:addRadioMenu() self.radioMenu = missionCommands.addSubMenu('Jammer: '..self.emitter:getName()) missionCommands.addCommand('Master Arm On', self.radioMenu, SkynetIADSJammer.updateMasterArm, {self = self, option = 'masterArmOn'}) missionCommands.addCommand('Master Arm Safe', self.radioMenu, SkynetIADSJammer.updateMasterArm, {self = self, option = 'masterArmSafe'}) end function SkynetIADSJammer.updateMasterArm(params) local option = params.option local self = params.self if option == 'masterArmOn' then self:masterArmOn() elseif option == 'masterArmSafe' then self:masterArmSafe() end end function SkynetIADSJammer:removeRadioMenu() missionCommands.removeItem(self.radioMenu) end end do SkynetIADSSAMSearchRadar = {} SkynetIADSSAMSearchRadar = inheritsFrom(SkynetIADSAbstractDCSObjectWrapper) function SkynetIADSSAMSearchRadar:create(unit) local instance = self:superClass():create(unit) setmetatable(instance, self) self.__index = self instance.firingRangePercent = 100 instance.maximumRange = 0 instance.initialNumberOfMissiles = 0 instance.remainingNumberOfMissiles = 0 instance.initialNumberOfShells = 0 instance.remainingNumberOfShells = 0 instance.triedSensors = 0 return instance end --override in subclasses to match different datastructure of getSensors() function SkynetIADSSAMSearchRadar:setupRangeData() if self:isExist() then local data = self:getDCSRepresentation():getSensors() if data == nil then --this is to prevent infinite calls between launcher and search radar self.triedSensors = self.triedSensors + 1 --the SA-13 does not have any sensor data, but is has launcher data, so we use the stuff from the launcher for the radar range. SkynetIADSSAMLauncher.setupRangeData(self) return end for i = 1, #data do local subEntries = data[i] for j = 1, #subEntries do local sensorInformation = subEntries[j] -- some sam sites have IR and passive EWR detection, we are just interested in the radar data -- investigate if upperHemisphere and headOn is ok, I guess it will work for most detection cases if sensorInformation.type == Unit.SensorType.RADAR and sensorInformation['detectionDistanceAir'] then local upperHemisphere = sensorInformation['detectionDistanceAir']['upperHemisphere']['headOn'] local lowerHemisphere = sensorInformation['detectionDistanceAir']['lowerHemisphere']['headOn'] self.maximumRange = upperHemisphere if lowerHemisphere > upperHemisphere then self.maximumRange = lowerHemisphere end end end end end end function SkynetIADSSAMSearchRadar:getMaxRangeFindingTarget() return self.maximumRange end function SkynetIADSSAMSearchRadar:isRadarWorking() -- the ammo check is for the SA-13 which does not return any sensor data: return (self:isExist() == true and ( self:getDCSRepresentation():getSensors() ~= nil or self:getDCSRepresentation():getAmmo() ~= nil ) ) end function SkynetIADSSAMSearchRadar:setFiringRangePercent(percent) self.firingRangePercent = percent end function SkynetIADSSAMSearchRadar:getDistance(target) return mist.utils.get2DDist(target:getPosition().p, self:getDCSRepresentation():getPosition().p) end function SkynetIADSSAMSearchRadar:getHeight(target) local radarElevation = self:getDCSRepresentation():getPosition().p.y local targetElevation = target:getPosition().p.y return math.abs(targetElevation - radarElevation) end function SkynetIADSSAMSearchRadar:isInHorizontalRange(target) return (self:getMaxRangeFindingTarget() / 100 * self.firingRangePercent) >= self:getDistance(target) end function SkynetIADSSAMSearchRadar:isInRange(target) if self:isExist() == false then return false end return self:isInHorizontalRange(target) end end do SkynetIADSSamSite = {} SkynetIADSSamSite = inheritsFrom(SkynetIADSAbstractRadarElement) function SkynetIADSSamSite:create(samGroup, iads) local sam = self:superClass():create(samGroup, iads) setmetatable(sam, self) self.__index = self sam.targetsInRange = false sam.goLiveConstraints = {} return sam end function SkynetIADSSamSite:addGoLiveConstraint(constraintName, constraint) self.goLiveConstraints[constraintName] = constraint end function SkynetIADSAbstractRadarElement:areGoLiveConstraintsSatisfied(contact) for constraintName, constraint in pairs(self.goLiveConstraints) do if ( constraint(contact) ~= true ) then return false end end return true end function SkynetIADSAbstractRadarElement:removeGoLiveConstraint(constraintName) local constraints = {} for cName, constraint in pairs(self.goLiveConstraints) do if cName ~= constraintName then constraints[cName] = constraint end end self.goLiveConstraints = constraints end function SkynetIADSAbstractRadarElement:getGoLiveConstraints() return self.goLiveConstraints end function SkynetIADSSamSite:isDestroyed() local isDestroyed = true for i = 1, #self.launchers do local launcher = self.launchers[i] if launcher:isExist() == true then isDestroyed = false end end local radars = self:getRadars() for i = 1, #radars do local radar = radars[i] if radar:isExist() == true then isDestroyed = false end end return isDestroyed end function SkynetIADSSamSite:targetCycleUpdateStart() self.targetsInRange = false end function SkynetIADSSamSite:targetCycleUpdateEnd() if self.targetsInRange == false and self.actAsEW == false and self:getAutonomousState() == false and self:getAutonomousBehaviour() == SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DCS_AI then self:goDark() end end function SkynetIADSSamSite:informOfContact(contact) -- we make sure isTargetInRange (expensive call) is only triggered if no previous calls to this method resulted in targets in range if ( self.targetsInRange == false and self:areGoLiveConstraintsSatisfied(contact) == true and self:isTargetInRange(contact) and ( contact:isIdentifiedAsHARM() == false or ( contact:isIdentifiedAsHARM() == true and self:getCanEngageHARM() == true ) ) ) then self:goLive() self.targetsInRange = true end end end do SkynetIADSSAMTrackingRadar = {} SkynetIADSSAMTrackingRadar = inheritsFrom(SkynetIADSSAMSearchRadar) function SkynetIADSSAMTrackingRadar:create(unit) local instance = self:superClass():create(unit) setmetatable(instance, self) self.__index = self return instance end end do SkynetIADSSAMLauncher = {} SkynetIADSSAMLauncher = inheritsFrom(SkynetIADSSAMSearchRadar) function SkynetIADSSAMLauncher:create(unit) local instance = self:superClass():create(unit) setmetatable(instance, self) self.__index = self instance.maximumFiringAltitude = 0 return instance end function SkynetIADSSAMLauncher:setupRangeData() self.remainingNumberOfMissiles = 0 self.remainingNumberOfShells = 0 if self:isExist() then local data = self:getDCSRepresentation():getAmmo() local initialNumberOfMissiles = 0 local initialNumberOfShells = 0 --data becomes nil, when all missiles are fired if data then for i = 1, #data do local ammo = data[i] --we ignore checks on radar guidance types, since we are not interested in how exactly the missile is guided by the SAM site. if ammo.desc.category == Weapon.Category.MISSILE then --TODO: see what the difference is between Max and Min values, SA-3 has higher Min value than Max?, most likely it has to do with the box parameters supplied by launcher --to simplyfy we just use the larger value, sam sites need a few seconds of tracking time to fire, by that time contact has most likely closed in on the SAM site. local altMin = ammo.desc.rangeMaxAltMin local altMax = ammo.desc.rangeMaxAltMax self.maximumRange = altMin if altMin < altMax then self.maximumRange = altMax end self.maximumFiringAltitude = ammo.desc.altMax self.remainingNumberOfMissiles = self.remainingNumberOfMissiles + ammo.count initialNumberOfMissiles = self.remainingNumberOfMissiles end if ammo.desc.category == Weapon.Category.SHELL then self.remainingNumberOfShells = self.remainingNumberOfShells + ammo.count initialNumberOfShells = self.remainingNumberOfShells end --if no distance was detected we run the code for the search radar. This happens when all in one units are passed like the shilka if self.maximumRange == 0 then --this is to prevent infinite calls between launcher and search radar if self.triedSensors <= 2 then SkynetIADSSAMSearchRadar.setupRangeData(self) end end end -- conditions here are because setupRangeData() is called multiple times in the code to update ammo status, we set initial values only the first time the method is called if self.initialNumberOfMissiles == 0 then self.initialNumberOfMissiles = initialNumberOfMissiles end if self.initialNumberOfShells == 0 then self.initialNumberOfShells = initialNumberOfShells end end end end function SkynetIADSSAMLauncher:getInitialNumberOfShells() return self.initialNumberOfShells end function SkynetIADSSAMLauncher:getRemainingNumberOfShells() self:setupRangeData() return self.remainingNumberOfShells end function SkynetIADSSAMLauncher:getInitialNumberOfMissiles() return self.initialNumberOfMissiles end function SkynetIADSSAMLauncher:getRemainingNumberOfMissiles() self:setupRangeData() return self.remainingNumberOfMissiles end function SkynetIADSSAMLauncher:getRange() return self.maximumRange end function SkynetIADSSAMLauncher:getMaximumFiringAltitude() return self.maximumFiringAltitude end function SkynetIADSSAMLauncher:isWithinFiringHeight(target) -- if no max firing height is set (radar quided AAA) then we use the vertical range, bit of a hack but probably ok for AAA if self:getMaximumFiringAltitude() > 0 then return self:getMaximumFiringAltitude() >= self:getHeight(target) else return self:getRange() >= self:getHeight(target) end end function SkynetIADSSAMLauncher:isInRange(target) if self:isExist() == false then return false end return self:isWithinFiringHeight(target) and self:isInHorizontalRange(target) end end --[[ SA-2 Launcher: { count=1, desc={ Nmax=17, RCS=0.39669999480247, _origin="", altMax=25000, altMin=100, box={ max={x=4.7303376197815, y=0.84564626216888, z=0.84564626216888}, min={x=-5.8387970924377, y=-0.84564626216888, z=-0.84564626216888} }, category=1, displayName="SA2V755", fuseDist=20, guidance=4, life=2, missileCategory=2, rangeMaxAltMax=30000, rangeMaxAltMin=40000, rangeMin=7000, typeName="SA2V755", warhead={caliber=500, explosiveMass=196, mass=196, type=1} } } } --]] do SkynetIADSHARMDetection = {} SkynetIADSHARMDetection.__index = SkynetIADSHARMDetection SkynetIADSHARMDetection.HARM_THRESHOLD_SPEED_KTS = 800 function SkynetIADSHARMDetection:create(iads) local harmDetection = {} setmetatable(harmDetection, self) harmDetection.contacts = {} harmDetection.iads = iads harmDetection.contactRadarsEvaluated = {} return harmDetection end function SkynetIADSHARMDetection:setContacts(contacts) self.contacts = contacts end function SkynetIADSHARMDetection:evaluateContacts() self:cleanAgedContacts() for i = 1, #self.contacts do local contact = self.contacts[i] local groundSpeed = contact:getGroundSpeedInKnots(0) --if a contact has only been hit by a radar once it's speed is 0 if groundSpeed == 0 then return end local simpleAltitudeProfile = contact:getSimpleAltitudeProfile() local newRadarsToEvaluate = self:getNewRadarsThatHaveDetectedContact(contact) --self.iads:printOutputToLog(contact:getName().." new Radars to evaluate: "..#newRadarsToEvaluate) --self.iads:printOutputToLog(contact:getName().." ground speed: "..groundSpeed) if ( #newRadarsToEvaluate > 0 and contact:isIdentifiedAsHARM() == false and ( groundSpeed > SkynetIADSHARMDetection.HARM_THRESHOLD_SPEED_KTS and #simpleAltitudeProfile <= 2 ) ) then local detectionProbability = self:getDetectionProbability(newRadarsToEvaluate) --self.iads:printOutputToLog("DETECTION PROB: "..detectionProbability) if ( self:shallReactToHARM(detectionProbability) ) then contact:setHARMState(SkynetIADSContact.HARM) if (self.iads:getDebugSettings().harmDefence ) then self.iads:printOutputToLog("HARM IDENTIFIED: "..contact:getTypeName().." | DETECTION PROBABILITY WAS: "..detectionProbability.."%") end else contact:setHARMState(SkynetIADSContact.NOT_HARM) if (self.iads:getDebugSettings().harmDefence ) then self.iads:printOutputToLog("HARM NOT IDENTIFIED: "..contact:getTypeName().." | DETECTION PROBABILITY WAS: "..detectionProbability.."%") end end end if ( #simpleAltitudeProfile > 2 and contact:isIdentifiedAsHARM() ) then contact:setHARMState(SkynetIADSContact.HARM_UNKNOWN) if (self.iads:getDebugSettings().harmDefence ) then self.iads:printOutputToLog("CORRECTING HARM STATE: CONTACT IS NOT A HARM: "..contact:getName()) end end if ( contact:isIdentifiedAsHARM() ) then self:informRadarsOfHARM(contact) end end end function SkynetIADSHARMDetection:cleanAgedContacts() local activeContactRadars = {} for contact, radars in pairs (self.contactRadarsEvaluated) do if contact:getAge() < 32 then activeContactRadars[contact] = radars end end self.contactRadarsEvaluated = activeContactRadars end function SkynetIADSHARMDetection:getNewRadarsThatHaveDetectedContact(contact) local radarsFromContact = contact:getAbstractRadarElementsDetected() local evaluatedRadars = self.contactRadarsEvaluated[contact] local newRadars = {} if evaluatedRadars == nil then evaluatedRadars = {} self.contactRadarsEvaluated[contact] = evaluatedRadars end for i = 1, #radarsFromContact do local contactRadar = radarsFromContact[i] if self:isElementInTable(evaluatedRadars, contactRadar) == false then table.insert(evaluatedRadars, contactRadar) table.insert(newRadars, contactRadar) end end return newRadars end function SkynetIADSHARMDetection:isElementInTable(tbl, element) for i = 1, #tbl do local tblElement = tbl[i] if tblElement == element then return true end end return false end function SkynetIADSHARMDetection:informRadarsOfHARM(contact) local samSites = self.iads:getUsableSAMSites() self:updateRadarsOfSites(samSites, contact) local ewRadars = self.iads:getUsableEarlyWarningRadars() self:updateRadarsOfSites(ewRadars, contact) end function SkynetIADSHARMDetection:updateRadarsOfSites(sites, contact) for i = 1, #sites do local site = sites[i] site:informOfHARM(contact) end end function SkynetIADSHARMDetection:shallReactToHARM(chance) return chance >= math.random(1, 100) end function SkynetIADSHARMDetection:getDetectionProbability(radars) local detectionChance = 0 local missChance = 100 local detection = 0 for i = 1, #radars do detection = radars[i]:getHARMDetectionChance() if ( detectionChance == 0 ) then detectionChance = detection else detectionChance = detectionChance + (detection * (missChance / 100)) end missChance = 100 - detection end return detectionChance end end ================================================ FILE: demo-missions/skynet-iads-setup-persian-gulf.lua ================================================ do --create an instance of the IADS redIADS = SkynetIADS:create('IRAN') ---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 = false iadsDebug.ewRadarNoConnection = false iadsDebug.samNoConnection = false iadsDebug.jammerProbability = true iadsDebug.addedEWRadar = false iadsDebug.hasNoPower = false iadsDebug.harmDefence = true iadsDebug.samSiteStatusEnvOutput = true iadsDebug.earlyWarningRadarStatusEnvOutput = true iadsDebug.commandCenterStatusEnvOutput = 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 sa15 as point defence for the SA-10 site, we set it to always react to a HARM so we can demonstrate the point defence mechanism in Skynet 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() jammer:addRadioMenu() ---some special code to remove the jammer aircraft if player is not flying with it in formation, has nothing to do with the IADS: local hornet = Unit.getByName('Hornet SA-11-2 Attack') if hornet == nil then Unit.getByName('jammer-emitter'):destroy() jammer:removeRadioMenu() end --end special code ------setup blue IADS: blueIADS = SkynetIADS:create('UAE') blueIADS:addSAMSitesByPrefix('BLUE-SAM') blueIADS:addEarlyWarningRadarsByPrefix('BLUE-EW') blueIADS:activate() blueIADS:addRadioMenu() local iadsDebug = blueIADS:getDebugSettings() iadsDebug.IADSStatus = true iadsDebug.contacts = true end ================================================ FILE: skynet-iads-source/README_source.md ================================================ # Skynet-IADS ![logo](/images/SA3_2.jpg) An IADS (Integrated Air Defence System) script for DCS (Digital Combat Simulator). # Abstract This script simulates an IADS within the scripting possibilities of DCS. Early Warning Radar Stations (EW Radar) scan the sky for contacts. These contacts are correlated with SAM (Surface to Air Missile) sites. If a contact is within firing range of the SAM site it will become active. A modern IADS also depends on command centers and datalinks to the SAM sites. The IADS can be set up with this infrastructure. Destroying it will degrade the capability of the IADS. This all sounds gibberish to you? Watch [this video by Covert Cabal on modern IADS](https://www.youtube.com/watch?v=9J9kntzkSQY). Visit [this DCS forum thread](https://forums.eagle.ru/topic/226173-skynet-an-iads-for-mission-builders) for development updates. Join the [Skynet discord group](https://discord.gg/pz8wcQs) and get support setting up your mission. Skynet supports the [HighDigitSAMs Mod](https://github.com/Auranis/HighDigitSAMs). You can also connect [Skynet with the AI_A2A_DISPATCHER](#how-do-i-connect-skynet-with-the-moose-ai_a2a_dispatcher-and-what-are-the-benefits-of-that) by MOOSE to add interceptors to the IADS. **So far over 200 hours of work went in to the development of Skynet. If you like using it, please consider a donation:** [![Skynet IADS donation](/images/btn_donateCC_LG.gif.png)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=7GSVFH448BWFQ&source=url) {TOC_PLACEHOLDER} # Quick start Tired of reading already? Download the [demo mission](/demo-missions/skynet-test-persian-gulf.miz) in the persian gulf map and see Skynet in action. More complex demo missions will follow soon. # Skynet IADS Elements ![Skynet IADS overview](/images/skynet-overview.jpg) ## IADS A Skynet IADS is a complete operational network. You can have multiple Skynet IADS instances per coalition in a DCS mission. A simple setup would be one IADS for the blue side and one IADS for the red side. ## Track files Skynet keeps a global track file of all detected targets. It queries all its units with radars and deduplicates contacts. By default lost contacts are stored up to 32 seconds in memory. ## Comand Centers You can add multiple command centers to a Skynet IADS. Once all command centers are destroyed the IADS will go in to autonomous mode. ## SAM Sites Skynet can handle multiple SAM sites, it will try and keep emissions to a minimum, therefore by default SAM sites will be turned on only if a target is in range. Every single launcher and radar unit's distance of a SAM site is analysed individually. If at least one launcher and radar is within range, the SAM Site will become active. This allows for a scattered placement of radar and launcher units as in real life. If SAM sites or radar guided AAA run out of ammo they will go dark. In the case of a SAM site it will wait with going dark as long as the last fired missile is still in the air. If an EW radar or a SAM site acting as EW radar is destoyed surrounding SAM sites can be left withouth EW radar coverage. This can also happen if a SAM site is outside of AWACS coverage. SAM sites will go autonomous in such a case meaning they will use their organic radars or just stay dark depending on setup. Once a SAM site is within EW radar coverage again it will be updated by the IADS. ## Early Warning Radars Skynet can handle 0-n EW radars. For detection of a target the DCS radar detection logic is used. You can use any type of radar listed in [skynet-iads-supported-types.lua](/skynet-iads-source/skynet-iads-supported-types.lua) in an EW role in Skynet. Some modern SAM radars have a greater detection range than older EW radars, e.g. the S-300PS 64H6E (160 km) vs EWR 55G6 (120 km). You can also designate SAM sites to act as EW radars, in this case a SAM site will constantly have their radar on. Long range systems like the S-300 are used as EW radars in real life. SAM sites that are out of ammo will stay live if they are set to act as EW radars. Nice to know: Terrain elevation around an EW radar will create blinds spots, allowing low and fast movers to penetrate radar networks through valleys. ## Power Sources By default Skynet IADS will run without having to add power sources. You can add multiple power sources to SAM sites, EW radars and command centers. Once a power source is fully damaged the Skynet IADS unit will stop working. Nice to know: Taking out the power source of a command center is a real life tactic used in SEAD (Suppression of Enemy Air Defence). ## Connection Nodes By default Skynet IADS will run without having to add connection nodes. You can add multiple connection nodes to SAM sites, EW radars and command centers. When all the unit's connection nodes are fully damaged an EW radar or SAM site will go in to autonomous mode. For a SAM site this means it will behave in its autonomous mode setting. If an EW Radar looses its node it will no longer contribute information to the IADS but otherwise the IADS will still work. Command centers do not have an autonomous mode. Nice to know: A single node can be used to connect an arbitrary number of Skynet IADS units. This way you can add a single point of failure in to an IADS. ## AWACS (Airborne Early Warning and Control System) Any aircraft with an air to air radar can be added as AWACS. Contacts detected will be added to the IADS. The AWACS will also detect ground units like ships. These will however not be passed to the SAM sites. You can add a connection node for the AWACS like an antenna, if it is destroyed, the AWACS will no longer be able to contribute contacts to the IADS. Technically you can also add a power source. In this context it would represent the power source for the connection node, since an aircraft provides its own power. ## Ships Ships will contribute to the IADS the same way AWACS units do. Add them as a regular EW radar. # Tactics ## HARM defence SAM sites and EW radars will shut down their radars if they believe a HARM (High speed anti radiation missile) is heading for them. For this to happen, the IADS will evaluate contacts and determine if they are likely to be HARMs. Each SAM site or EW radar has HARM detection chance set. If a HARM is detected by more than one radar, the chance of it being identified as a HARM is increased. See [skynet-iads-supported-types.lua](/skynet-iads-source/skynet-iads-supported-types.lua) field ```['harm_detection_chance']``` for the probability per radar system. ### HARM detection let's say SAM site A has a 60% HARM detection chance and SAM site B has a 50% HARM detection cance. If a HARM is picked up by both radars the chance the IADS will identify the HARM will be 80%. With the radar cross section updates of HARMs in DCS 2.7 older radars like the ones used in the SA-2 and SA-6 can only identifiy a HARM at very close range usualy less than 10 seconds before impact. These systems will not have a very good HARM defence with Skynet. ![Skynet IADS overview](/images/skynet-harm-detection.jpg) ### HARM flight path analysis The contact needs to be traveling faster than 800 kt and it may not have changed its flight path more than 2 times (eg ```climb-descend```, ```climb``` or ```descend```).This is to minimise false positives, for example a fighter flying very fast. ![Skynet IADS overview](/images/skynet-harm-flightpath.jpg) This implementation is closer to real life. SAM sites like the patriot and most likely modern Russian systems calculate the flight path and analyse the radar cross section to determine if a contact heading inbound is a HARM. If identified as a HARM the IADS will shut down radars 15 degrees left and right of the HARM's fight path up to a distance of 20 nautical miles in front of the HARM. The IADS will calculate time to impact and shut down radar emitters up to a maximum of 180 seconds after time to impact. ## HARM radar shutdown Once a HARM has been identified by Skynet, radars up to 20 nm ahead and 15 degrees left or right of the HARM will be notified. Depending on their settins radar emitters will shut down or start defending against the HARM. ![Skynet IADS overview](/images/skynet-harm-radar-shutdown.jpg) ## Point defence When a radar emitter (EW radar or SAM site) is attacked by a HARM there is a chance it may detect the HARM and go dark. If this radar emitter is acting as the sole EW radar in the area, surrounding SAM sites will not be able to go live since they rely on the EW radar for target information. This is an issue if you have SA-15 Tors next to the EW radar for point defence protection. They will stay dark and not engange the HARM. Use this feature if you don't want the IADS to loose situational awareness just because a HARM is inbound. The radar emitter will shut down, if it believes its point defences won't be able to handle the number of HARMs inbound. As long as there is one point defence launcher and missile per HARM inbound the radar emitter will keep emitting. If the HARMs exeed the number of point defence launchers and missiles the protected asset will shut down. Tests in DCS have shown that this is roughly the saturation point. If the SAM site reling on point defence can engagen HARMs its launchers an missiles will also count to the saturation point. See FAQ [Which SAM systems can engage HARMS?](#which-sam-systems-can-engage-harms) [Point defence setup example](#point-defence-1) ## Electronic Warfare A simple form of jamming is part of the Skynet IADS package. It's off by default. The jamming works by setting the ROE state of a SAM Site. The closer the jamming emitter gets to a SAM site the less effective jamming will become (burn through). For the jammer to work it will need LOS (line of sight) to a radar unit. Older SAM sites are more susceptible to jamming. EW radars are currently not jammable. I recommend you add an AI unit that follows the strike package you're flying in to act as a jammer aircraft. This will give you the most realistic experience. The jammer emitter will toggle the ROE state of a SAM site which affects how the SAM site reacts to all threats near or far. I presume an aircraft very close to a SAM site beeing jammed by a emitter very far away would most likely be detected. So the farther away you are from the jammer source the more unrealistic your experience will be. Here is a [list of SAM sites currently supported by the jammer](https://docs.google.com/spreadsheets/d/16rnaU49ZpOczPEsdGJ6nfD0SLPxYLEYKmmo4i2Vfoe0/edit#gid=0) and the jammer's effectiveness on them. When setting up a jammer you can decide which SAM sites it is able to jam. For example you could design a mission in which the jammer is not able to jam a SA-6 but is able to jam a SA-2. The jammer effectiveness is not based on any real world data I just read about the different types and made my own conclusions. Here is an old school documentary [showing the Prowler in action](https://www.youtube.com/watch?v=su44ZU7NcQU). They brief to turn on their jamming equipement at 60 nm from the target. I suppose that must have been the effective range of 70's jamming tech. # Using Skynet in the mission editor It's quite simple to setup an IADS have a look at the demo missions in the [/demo-missions/](/demo-missions) folder. ## Placing units This tutorial assumes you are familiar on how to set up a SAM site in DCS. If not I suggest you watch [this video](https://www.youtube.com/watch?v=YZPh-JNf6Ww) by the Grim Reapers. Place the IADS elements you wish to add on the map. ![Mission Editor IADS Setup](/images/iads-setup.png) ## Preparing a SAM site There may be only be **one type of SAM site per group**. More than one type of SAM site per group will result in Skynet no being able to properly controll the group. Also please refrain from from adding units to the SAM group that are not required for the SAM like trucks, tanks and soldiers. The skill level you set on a SAM group is retained by Skynet. Make sure you name the **SAM site group** in a consistent manner with a prefix e.g. 'SAM-SA-2'. ![Mission Editor add SAM site](/images/add-sam-site.png) ## Preparing an EW radar You can use any type of radar as an EW radar. Make sure you **name the unit** in a consistent manner with a prefix, e.g. 'EW-center3'. Make sure you have only **one EW radar in a group** otherwise Skynet will not be able to control single EW radars. ![Mission Editor EW radar](/images/ew-setup.png) ## Adding the Skynet code Skynet requires MIST. A version is provided in this repository or you can download the most current version [here](https://github.com/mrSkortch/MissionScriptingTools). Make sure you load MIST and the compiled skynet code in to a mission. The [skynet-iads-compiled.lua](/demo-missions/skynet-iads-compiled.lua) and [mist_4_5_107.lua](/demo-missions/mist_4_5_107.lua) files are located in the [/demo-missions/](/demo-missions) folder. I recommend you create a text file e.g. 'my-iads-setup.lua' and then add the code needed to get the IADS runing. When updating the setup remember to reload the file in the mission editor. Otherwise changes will not become effective. You can also add the code directly in the mission editor, however that input field is quite small if you write more than a few lines of code. ![Mission Editor IADS Setup](/images/load-scripts.png) ## Adding the Skynet IADS For the IADS to work you need four lines of code. create an instance of the IADS, the name string is optional and will be displayed in status output: ```lua redIADS = SkynetIADS:create('name') ``` Give all SAM groups you want to add a common prefix in the mission editor eg: 'SAM-SA-10 west', then add this line of code: ```lua redIADS:addSAMSitesByPrefix('SAM') ``` Same for the EW radars, name all units with a common prefix in the mission editor eg: 'EW-radar-south': ```lua redIADS:addEarlyWarningRadarsByPrefix('EW') ``` Activate the IADS: ```lua redIADS:activate() ``` # Advanced setup This is the danger zone. Call Kenny Loggins. Some experience with scripting is recommended. You can handcraft your IADS with the following functions. If you refrence units that don't exist a message will be displayed when the mission loads. The following examples use static objects for command centers, connection nodes and power sources, you can also use units instead. ## IADS configuration Call this method to add or remove a radio menu to toggle the status output of the IADS. By default the radio menu option is not visible: ```lua redIADS:addRadioMenu() ``` ```lua redIADS:removeRadioMenu() ``` If you dereference the IADS remember to call ```deactivate()``` otherwise background tasks of the IADS will continue running, resulting in unexpected behaviour: ```lua redIADS:deactivate() ``` Set the update interval in seconds of the IADS. This determines in what interval the IADS wil turn SAM sites of or on according to targets it has detected: ```lua redIADS:setUpdateInterval(5) ``` ## Adding a command center The command center represents the place where information is collected and analysed. It if is destroyed the IADS disintegrates. Add a command center like this: ```lua local commandCenter = StaticObject.getByName("Command Center") redIADS:addCommandCenter(commandCenter) ``` ## Power sources and connection nodes You can use units or static objects. Call the function multiple times to add more than one power source or connection node: ```unit``` refers to a SAM site, or EW Radar you retrieved from the IADS, see [setting an option for Radar units](#setting-an-option). ```lua local powerSource = StaticObject.getByName("EW Power Source") unit:addPowerSource(powerSource) ``` ```lua local connectionNode = Unit.getByName("EW connection node") unit:addConnectionNode(connectionNode) ``` For command centers use: ```lua local commandCenter = StaticObject.getByName("Command Center2") local comPowerSource = StaticObject.getByName("Command Center2 Power Source") redIADS:addCommandCenter(commandCenter):addPowerSource(comPowerSource) ``` ## Warm up the SAM sites of an IADS This function is deprecated and will be removed in a future release. ```lua redIADS:setupSAMSitesAndThenActivate() ``` ## Connecting Skynet to the MOOSE AI_A2A_DISPATCHER You can connect Skynet with MOOSE's [AI_A2A_DISPATCHER](https://flightcontrol-master.github.io/MOOSE_DOCS/Documentation/AI.AI_A2A_Dispatcher.html). This allows the IADS not only to direct SAM sites but also to scramble fighters. Skynet will set the radars it can use on the SET_GROUP object of a dispatcher. Meaning that if a radar is lost in Skynet it will no longer be availabe to detect and scramble interceptors. Add the object of type SET_GROUP to the iads like this (in this example ```DectionSetGroup```): ```lua redIADS:addMooseSetGroup(DetectionSetGroup) ``` ## SAM site configuration ### Adding SAM sites #### Add multiple SAM sites Adds SAM sites with prefix in group name to the IADS. Previously added SAM sites are cleared: ```lua redIADS:addSAMSitesByPrefix('SAM') ``` #### Add a SAM site manually You can manually add a SAM site, must be a valid group name: ```lua redIADS:addSAMSite('SA-6 Group2') ``` ### Accessing SAM sites in the IADS The following functions exist to access SAM sites added to the IADS. They all support daisy chaining options: Returns all SAM sites with the corresponding Nato name, see [skynet-iads-supported-types.lua](/skynet-iads-source/skynet-iads-supported-types.lua). For all units beginning with 'SA-': Don't add Nato code names (Guideline, Gainful), just write 'SA-2', 'SA-6': ```lua redIADS:getSAMSitesByNatoName('SA-6') ``` Returns all SAM sites in the IADS: ```lua redIADS:getSAMSites() ``` Returns a SAM site with the specified group name: ```lua redIADS:getSAMSiteByGroupName('SAM-SA-6') ``` Returns a SAM site with the specified group name prefix. Let's say you have a bunch of SAM sites that all will share the same power source. Give these sites a special prefix in the group name, e.g.: ```'SAM-SECTOR-A'```. Once you have added the SAM sites you can access them via the prefix to set whatever options you want: ```lua redIADS:getSAMSitesByPrefix('SAM-SECTOR-A') ``` ### Act as EW radar Will set the SAM site to act as an EW radar. This will result in the SAM site always having its radar on. Contacts the SAM site sees are reported to the IADS. This option is recomended for long range systems like the S-300: ```lua samSite:setActAsEW(true) ``` ### Engagement zone Set the distance at which a SAM site will switch on its radar: ```lua samSite:setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE) ``` #### Engagement zone options SAM site will go live when target is within the red circle in the mission editor (default Skynet behaviour): ```lua SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE ``` SAM site will go live when target is within the yelow circle in the mission editor: ```lua SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE ``` This option sets the range in relation to the zone you set in ``setEngagementZone`` for a SAM site to go live. Be careful not to set the value too low. Some SAM sites need up to 30 seconds until they can fire. During this time a target might have already left the engagement zone of SAM site. This option is intended for long range systems like the S-300. You can also set the range above 100 this will have the effect that the SAM site goes live earlier: ```lua samSite:setGoLiveRangeInPercent(90) ``` ### Engage air weapons Will set the SAM site to engage air weapons, if it is able to do so in DCS. It is a wrapper for the [ENGAGE_AIR_WEAPONS](https://wiki.hoggitworld.com/view/DCS_option_engage_air_weapons) setting. ```lua samSite:setCanEngageAirWeapons(true) ``` ### Engage HARM Will set the SAM site to engage HARMs, if it is able to do so in DCS. If set to false the SAM site will shut down if a HARM that has been identified by the IADS is inbound. SAM sites that can engage HARMS are set to true by default. ```lua samSite:setCanEngageHARM(true) ``` ## Add go live constraints You can include constraints wich must be satisfied for the SAM site to go live. Please note this only controls activation of the SAM site. There is currently no way to tell a SAM site to only target a certain contact via the lua scripting engine in DCS. The constraint must evaluate to true and the contact must be in range of the SAM site (handled by Skynet). ### Use cases Place a SAM site on an flight path that you suspect strike strike fighters will pass. Add a heading constraint to ensure that the SAM site will only go ive when fighters are on their way back from the target. Set a SAM site to only go live if aircraft are in a certain altitude band. SAM site shall only go live once a strike package has destroyed a certain building or unit. You do not have to use the contact provided in the function to evaluate the constraint. You can make any assertion you want. Create a function that will evaluate if the constraint is satisfied. The function will have access to the [contact](#contact) the SAM site is evaluating: ```lua --SAM site will only go live if the contact is below 1000 feet. local function goLiveConstraint(contact) return ( contact:getHeightInFeetMSL() < 1000 ) end ``` Add the function to the SAM site and give it a name. You can add as many constraints as you wish: ```lua self.samSite:addGoLiveConstraint('ignore-low-flying-contacts', goLiveConstraint) ``` Remove constraint you no longer wish to use: ```lua self.samSite:removeGoLiveConstraint('ignore-low-flying-contacts') ``` Get a table of all constraints: ```lua self.samSite:getGoLiveConstraints() ``` ## Contact You can use the following methods to get information about a contact. Will return true if contact has been identified as a HARM by Skynet: ```lua contact:isIdentifiedAsHARM() ``` Will return the height of a contact: ```lua contact:getHeightInFeetMSL() ``` Will return the current magnetic heading of a contact. Note the heading is availble only after a contact has been tracked in more than one cycle by the IADS. Until that has happened heading will be 0: ```lua contact:getMagneticHeading() ``` Will return the current ground speed of a contact. Note the speed is availble only after a contact has been tracked in more than one cycle by the IADS. Until that has happend speed will be 0: ```lua contact:getMagneticHeading() ``` Will return the time in seconds a contact has been known to the IADS: ```lua contact:getAge() ``` Will return the type as a ```Object.Category```: ```lua contact:getTypeName() ``` Will return the unit name: ```lua contact:getName() ``` ## EW radar configuration ### Adding EW radars #### Add multiple EW radars Adds EW radars with prefix in unit name to the IADS. Previously added EW sites are cleared: ```lua redIADS:addEarlyWarningRadarsByPrefix('EW') ``` #### Add an EW radar manually You can add EW radars manually, must be a valid unit name: ```lua redIADS:addEarlyWarningRadar('EWR West') ``` ### Accessing EW radars in the IADS The following functions exist to access EW radars added to the IADS. They all support daisy chaining options. Returns all EW radars in the IADS: ```lua redIADS:getEarlyWarningRadars() ``` Returns the EW radar with the specified unit name: ```lua redIADS:getEarlyWarningRadarByUnitName('EW-west') ``` ## Options for SAM sites and EW radars ### Setting an option In the following examples ```ewRadarOrSamSite``` refers to an single EW radar or SAM site or a table of EW radars and SAM sites you got from the Skynet IADS, by calling one of the functions named in [accessing EW radars](#accessing-ew-radars-in-the-iads) or [accessing SAM sites](#accessing-sam-sites-in-the-iads). ### Daisy chaining options You can daisy chain options on a single SAM site / EW Radar or a table of SAM sites / EW radars like this: ```lua redIADS:getSAMSites():setActAsEW(true):addPowerSource(powerSource):addConnectionNode(connectionNode):setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE):setGoLiveRangeInPercent(90):setAutonomousBehaviour(SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK) ``` ### HARM Defence You can set the reaction probability (between 0 and 100 percent). See [skynet-iads-supported-types.lua](/skynet-iads-source/skynet-iads-supported-types.lua) field ```['harm_detection_chance']``` for default detection probabilities: ```lua ewRadarOrSamSite:setHARMDetectionChance(50) ``` ### Point defence You must use a point defence SAM that can engage HARM missiles. Can be used to protect SAM sites or EW radars. See [point defence](#point-defence) for information what this does: If you want the point defences to coordinate their HARM defence then you can add multiple point defence SAM sites in to one group. **This is the only place where you should add multiple SAM sites in to one group in Skynet**. Let's assume you have two SA-15 units defending a radar. If the SA-15 units are in separate groups they will both fire at the same HARM inbound. However if they are in the same group and multiple HARMS are inbound they will each pick a separate HARM to engage. ```lua --first get the SAM site you want to use as point defence from the IADS: local sa15 = redIADS:getSAMSiteByGroupName('SAM-SA-15') --then add it to the SAM site it should protect: redIADS:getSAMSiteByGroupName('SAM-SA-10'):addPointDefence(sa15) ``` This function is deprecated and will be removed in a future release. ```lua ewRadarOrSamSite:setIgnoreHARMSWhilePointDefencesHaveAmmo(true) ``` ### Autonomous mode behaviour Set how the SAM site or EW radar will behave if it looses connection to the IADS: ```lua ewRadarOrSamSite:setAutonomousBehaviour(SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK) ``` #### Autonomous mode options SAM site or EW radar will behave in the default DCS AI. Alarm State will be red and ROE weapons free (default Skynet behaviour for SAM sites): ```lua SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DCS_AI ``` SAM Site or EW radar will go dark if it looses connection to IADS (default behaviour for EW radars): ```lua SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK ``` ## Adding a jammer The jammer is quite easy to set up. You need a unit that acts as a jammer source, preferably it will be an aircraft in the strike package. Once the jammer detects an emitter it starts jamming the radar. Set the [coresponding debug variable jammerProbability](#setting-debug-information) to see what the jammer is doing. Check [skynet-iads-jammer.lua](/skynet-iads-source/skynet-iads-jammer.lua) to see which SAM sites are supported. Remember to set the AI aircraft acting as jammer in the Mission editor to ```Reaction to Threat = EVADE FIRE``` otherwise the AI will try and actively attack the SAM site. This way it will stick to the preset flight plan. Create a jammer and assign it to an unit. Also make sure you add the IADS you wan't the jammer to work for: ```lua local jammerSource = Unit.getByName("F-4 AI") jammer = SkynetIADSJammer:create(jammerSource, iads) ``` The jammer will start listening for emitters and if it finds one of the emitters it is able to jam it will start jamming it: ```lua jammer:masterArmOn() ``` Will disable jamming for the specified SAM type, pass the Nato name: ```lua jammer:disableFor('SA-2') ``` Will turn off the jammer. Make sure you call this function before you dereference a jammer in the code, otherwise a background task will keep on jamming: ```lua jammer:masterArmSafe() ``` Will add jammer on / off to the radio menu: ```lua jammer:addRadioMenu() ``` Will remove jammer on / off from the radio menu: ```lua jammer:removeRadioMenu() ``` ### Advanced functions Add a second IADS the jammer should be able to jam, for example if you have two separate IADS running: ```lua jammer:addIADS(iads2) ``` Add a new jammer function: ```lua -- write a lambda function that expects one parameter: -- given public available data on jammers their effeciveness drastically decreases the closer you get, so a non-linear function would make sense: local function f(distanceNM) return ( 1.4 ^ distanceNM ) + 80 end -- add the function: specify which SAM type it should apply for: self.jammer:addFunction('SA-10', f) ``` Set the maximum range the jammer will work, the default value is set to 200 nautical miles: ```lua jammer:setMaximumEffectiveDistance(100) ``` ## Setting debug information When developing a mission I suggest you add debug output to check how the IADS reacts to threats. Debug output may slow down DCS, so it's recommended to turn it off in a live environment: Access the debug settings: ```lua local iadsDebug = redIADS:getDebugSettings() ``` Output in game: ```lua iadsDebug.IADSStatus = true iadsDebug.contacts = true iadsDebug.jammerProbability = true ``` Output to dcs.log: ```lua iadsDebug.addedEWRadar = true iadsDebug.addedSAMSite = true iadsDebug.warnings = true iadsDebug.radarWentLive = true iadsDebug.radarWentDark = true iadsDebug.harmDefence = true ``` These three options will output detailed information on every radar in the IADS to the dcs.log file. Enabling these may have an impact on performance: ```lua iadsDebug.samSiteStatusEnvOutput = true iadsDebug.earlyWarningRadarStatusEnvOutput = true iadsDebug.commandCenterStatusEnvOutput = true ``` ![Mission Editor IADS Setup](/images/skynet-debug.png) # Example Setup This is an example of how you can set up your IADS used in the [demo mission](/demo-missions/skynet-test-persian-gulf.miz): ```lua do --create an instance of the IADS redIADS = SkynetIADS:create('RED IADS') ---debug settings remove from here on if you do not wan't any output on what the IADS is doing by default local iadsDebug = redIADS:getDebugSettings() iadsDebug.IADSStatus = true iadsDebug.radarWentDark = true iadsDebug.contacts = true iadsDebug.radarWentLive = true iadsDebug.noWorkingCommmandCenter = true iadsDebug.samNoConnection = true iadsDebug.jammerProbability = true iadsDebug.addedEWRadar = true iadsDebug.harmDefence = true ---end remove debug --- --add all units with unit name beginning with 'EW' to the IADS: redIADS:addEarlyWarningRadarsByPrefix('EW') --add all groups begining with group name 'SAM' to the IADS: redIADS:addSAMSitesByPrefix('SAM') --add a command center: commandCenter = StaticObject.getByName('Command-Center') redIADS:addCommandCenter(commandCenter) ---we add a K-50 AWACs, manually. This could just as well be automated by adding an 'EW' prefix to the unit name: redIADS:addEarlyWarningRadar('AWACS-K-50') --add a power source and a connection node for this EW radar: local powerSource = StaticObject.getByName('Power-Source-EW-Center3') local connectionNodeEW = StaticObject.getByName('Connection-Node-EW-Center3') redIADS:getEarlyWarningRadarByUnitName('EW-Center3'):addPowerSource(powerSource):addConnectionNode(connectionNodeEW) --add a connection node to this SA-2 site, and set the option for it to go dark, if it looses connection to the IADS: local connectionNode = Unit.getByName('Mobile-Command-Post-SAM-SA-2') redIADS:getSAMSiteByGroupName('SAM-SA-2'):addConnectionNode(connectionNode):setAutonomousBehaviour(SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK) --this SA-2 site will go live at 70% of its max search range: redIADS:getSAMSiteByGroupName('SAM-SA-2'):setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE):setGoLiveRangeInPercent(70) --all SA-10 sites shall act as EW sites, meaning their radars will be on all the time: redIADS:getSAMSitesByNatoName('SA-10'):setActAsEW(true) --set the SA-15's as point defence for the SA-10 site. We set the SA-10 to always identify HARMs so we can demonstrate the point defence mechanism in Skynet. --the SA-10 will stay online when shot at by HARMS as long as the point defences and SAM site have ammo and the saturation point is not reached. local sa15 = redIADS:getSAMSiteByGroupName('SAM-SA-15-point-defence-SA-10') redIADS:getSAMSiteByGroupName('SAM-SA-10'):addPointDefence(sa15):setHARMDetectionChance(100) --set this SA-11 site to go live 70% of max range of its missiles (default value: 100%), its HARM detection probability is set to 50% (default value: 70%) redIADS:getSAMSiteByGroupName('SAM-SA-11'):setGoLiveRangeInPercent(70):setHARMDetectionChance(50) --this SA-6 site will always react to a HARM being fired at it: redIADS:getSAMSiteByGroupName('SAM-SA-6'):setHARMDetectionChance(100) --set this SA-11 site to go live at maximunm search range (default is at maximung firing range): redIADS:getSAMSiteByGroupName('SAM-SA-11-2'):setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE) --activate the radio menu to toggle IADS Status output redIADS:addRadioMenu() --activate the IADS redIADS:activate() --add the jammer local jammer = SkynetIADSJammer:create(Unit.getByName('jammer-emitter'), redIADS) jammer:masterArmOn() --setup blue IADS: blueIADS = SkynetIADS:create('BLUE IADS') blueIADS:addSAMSitesByPrefix('BLUE-SAM') blueIADS:addEarlyWarningRadarsByPrefix('BLUE-EW') blueIADS:activate() blueIADS:addRadioMenu() local iadsDebug = blueIADS:getDebugSettings() iadsDebug.IADSStatus = true iadsDebug.contacts = true end ``` # FAQ ## Does Skynet IADS have an impact on game performance? Skynet may actually improve game performance when using a lot of SAM AI units. This is because Skynet will turn off radar emissions of all SAM groups currently not in range of a target. By default these SAM groups would otherwise have their radars on. Skynet caches target information for a few seconds to reduce expensive calls on DCS radar detection. ## What air defence units shall I add to the Skynet IADS? In theory you can add all the types that are listed in the [skynet-iads-supported-types.lua](skynet-iads-source/skynet-iads-supported-types.lua) file. Very short range units (like the Shilka AAA, Rapier) won't really benefit from the IADS apart from reacting to HARMs. These are better just placed in a mission and handeled by the default AI of DCS. This is due to the short range of their radars. By the time the IADS wakes them up, the contact has likely passed their engagement range. The strength of the Skynet IADS lies with handling long range systems that operate by radar. ## Which SAM systems can engage HARMS? As of July 2022 I have only been able to get the SA-15, SA-10, NASAMS and Patriot to engage HARMS. The best option for a solid HARM defence is to add SA-15's around EW radars or high value SAM sites. ## What exactly does Skynet do with the SAMS? Via the scripting engine one can toggle the radar emitters on and off. Further options are the alarm state and the rules of engagement. In a nutshell that's all that Skynet does. Skynet does also read the radar and firing range properties of a SAM site. Based on that data and the setup options a mission designer provides Skynet will turn a SAM site on or off. No god like intervention is used (like magically exploding HARMS via the scripting engine). If a SAM site or EW radar detects an inbound HARM it just turns off its radar as in real life. The HARM as it is programmed in DCS will try and glide in to the last known position mostly resulting in misses by 50-100 meters. ## Are there known bugs? Yes, when placing multi unit SAM sites (e.g. SA-3, Patriot..) make sure the first unit you place is the search radar. If you add any other element as the first unit, Skynet will not be able to read radar data. The result will be that the SAM site won't go live. This bug was observed in DCS 2.5.5. The SAM site will work fine when used as a standalone unit outside of Skynet. ## How do I know if a SAM site is in range of an EW site or a SAM site in EW mode? To get a rough idea you can look at the range circles in the mission editor. However these ranges are greater than the actual in game detection ranges of an EW radar or SAM site. The following screenshot shows the range of the 1L13 EWR. The mission editor shows a range of 64 NM (nautical miles) where as the in game range is 43 NM. In this example the SAM site to the north east would not be in range of the EW radar, therefore it would go in to autonomous mode once the mission starts. ![1L13 EWR range differences](/images/ew-detection-distance-example.png) Set the debug options ```samSiteStatusEnvOutput``` and ```earlyWarningRadarStatusEnvOutput``` to get detailed information on every SAM site and EW radar. The text marked in the red box will show you which SAM sites are in the covered area of a SAM site or EW radar. ![SAM sites in covered area](/images/radar-emitter-status-dcs-log.png) ## How do I connect Skynet with the MOOSE AI_A2A_DISPATCHER and what are the benefits of that? IRL an IADS would most likely not only handle SAM sites but also pass information to interceptor aircraft. By connecting Skynet to the [AI_A2A_DISPATCHER](https://flightcontrol-master.github.io/MOOSE_DOCS/Documentation/AI.AI_A2A_Dispatcher.html) by MOOSE you are able to add interceptors to the IADS. See [Skynet Moose AI_A2A_DISPATCHER](#connecting-skynet-to-the-moose-ai_a2a_dispatcher) and the [moose_a2a_connector demo mission](demo-missions/moose_a2a_connector) for more information. An example setup of Skynet and the [AI_A2A_DISPATCHER](https://flightcontrol-master.github.io/MOOSE_DOCS/Documentation/AI.AI_A2A_Dispatcher.html) : ```lua --Setup Syknet IADS: redIADS = SkynetIADS:create('Enemy IADS') redIADS:addSAMSitesByPrefix('SAM') redIADS:addEarlyWarningRadarsByPrefix('EW') redIADS:activate() -- START MOOSE CODE: -- Define a SET_GROUP object that builds a collection of groups that define the EWR network. DetectionSetGroup = SET_GROUP:New() -- Setup the detection and group targets to a 30km range! Detection = DETECTION_AREAS:New( DetectionSetGroup, 30000 ) -- Setup the A2A dispatcher, and initialize it. A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) -- Set 100km as the radius to engage any target by airborne friendlies. A2ADispatcher:SetEngageRadius() -- 100000 is the default value. -- Set 200km as the radius to ground control intercept. A2ADispatcher:SetGciRadius() -- 200000 is the default value. CCCPBorderZone = ZONE_POLYGON:New( "RED-BORDER", GROUP:FindByName( "RED-BORDER" ) ) A2ADispatcher:SetBorderZone( CCCPBorderZone ) A2ADispatcher:SetSquadron( "Kutaisi", AIRBASE.Caucasus.Kutaisi, { "Squadron red SU-27" }, 2 ) A2ADispatcher:SetSquadronGrouping( "Kutaisi", 2 ) A2ADispatcher:SetSquadronGci( "Kutaisi", 900, 1200 ) A2ADispatcher:SetTacticalDisplay(true) A2ADispatcher:Start() --END MOOSE CODE -- add the MOOSE SET_GROUP to the IADS, from now on Skynet will update active radars that the MOOSE SET_GROUP can use for EW detection. redIADS:addMooseSetGroup(DetectionSetGroup) ``` # Thanks Special thaks to Spearzone and Coranthia for researching public available information on IADS networks and getting me up to speed on how such a system works. I based the SAM site setup on [Grimes SAM DB](https://forums.eagle.ru/showthread.php?t=118175) from his IADS script, however I removed range data since Skynet loads that from DCS. ================================================ FILE: skynet-iads-source/highdigitsams/skynet-iads-high-digit-sams-suported-types.lua ================================================ do -- this file contains the definitions for the HightDigitSAMSs: https://github.com/Auranis/HighDigitSAMs --EW radars used in multiple SAM systems: s300PMU164N6Esr = { ['name'] = { ['NATO'] = 'Big Bird', }, } s300PMU140B6MDsr = { ['name'] = { ['NATO'] = 'Clam Shell', }, } --[[ units in SA-10 group Gargoyle: 2020-12-10 18:27:27.050 INFO SCRIPTING: S-300PMU1 54K6 cp 2020-12-10 18:27:27.050 INFO SCRIPTING: S-300PMU1 5P85CE ln 2020-12-10 18:27:27.050 INFO SCRIPTING: S-300PMU1 5P85DE ln 2020-12-10 18:27:27.050 INFO SCRIPTING: S-300PMU1 40B6MD sr 2020-12-10 18:27:27.050 INFO SCRIPTING: S-300PMU1 64N6E sr 2020-12-10 18:27:27.050 INFO SCRIPTING: S-300PMU1 40B6M tr 2020-12-10 18:27:27.050 INFO SCRIPTING: S-300PMU1 30N6E tr --]] samTypesDB['S-300PMU1'] = { ['type'] = 'complex', ['searchRadar'] = { ['S-300PMU1 40B6MD sr'] = s300PMU140B6MDsr, ['S-300PMU1 64N6E sr'] = s300PMU164N6Esr, ['S-300PS 40B6MD sr'] = { ['name'] = { ['NATO'] = '', }, }, ['S-300PS 64H6E sr'] = { ['name'] = { ['NATO'] = '', }, }, }, ['trackingRadar'] = { ['S-300PMU1 40B6M tr'] = { ['name'] = { ['NATO'] = 'Grave Stone', }, }, ['S-300PMU1 30N6E tr'] = { ['name'] = { ['NATO'] = 'Flap Lid', }, }, ['S-300PS 40B6M tr'] = { ['name'] = { ['NATO'] = '', }, }, }, ['misc'] = { ['S-300PMU1 54K6 cp'] = { ['required'] = true, }, }, ['launchers'] = { ['S-300PMU1 5P85CE ln'] = { }, ['S-300PMU1 5P85DE ln'] = { }, }, ['name'] = { ['NATO'] = 'SA-20A Gargoyle' }, ['harm_detection_chance'] = 90, ['can_engage_harm'] = true } --[[ Units in the SA-23 Group: 2020-12-11 16:40:52.072 INFO SCRIPTING: S-300VM 9A82ME ln 2020-12-11 16:40:52.072 INFO SCRIPTING: S-300VM 9A83ME ln 2020-12-11 16:40:52.072 INFO SCRIPTING: S-300VM 9S15M2 sr 2020-12-11 16:40:52.072 INFO SCRIPTING: S-300VM 9S19M2 sr 2020-12-11 16:40:52.072 INFO SCRIPTING: S-300VM 9S32ME tr 2020-12-11 16:40:52.072 INFO SCRIPTING: S-300VM 9S457ME cp ]]-- samTypesDB['S-300VM'] = { ['type'] = 'complex', ['searchRadar'] = { ['S-300VM 9S15M2 sr'] = { ['name'] = { ['NATO'] = 'Bill Board-C', }, }, ['S-300VM 9S19M2 sr'] = { ['name'] = { ['NATO'] = 'High Screen-B', }, }, }, ['trackingRadar'] = { ['S-300VM 9S32ME tr'] = { }, }, ['misc'] = { ['S-300VM 9S457ME cp'] = { ['required'] = true, }, }, ['launchers'] = { ['S-300VM 9A82ME ln'] = { }, ['S-300VM 9A83ME ln'] = { }, }, ['name'] = { ['NATO'] = 'SA-23 Antey-2500' }, ['harm_detection_chance'] = 90, ['can_engage_harm'] = true } --[[ Units in the SA-10B Group: 2021-01-01 20:39:14.413 INFO SCRIPTING: S-300PS SA-10B 40B6MD MAST sr 2021-01-01 20:39:14.413 INFO SCRIPTING: S-300PS SA-10B 54K6 cp 2021-01-01 20:39:14.413 INFO SCRIPTING: S-300PS 5P85SE_mod ln 2021-01-01 20:39:14.413 INFO SCRIPTING: S-300PS 5P85SU_mod ln 2021-01-01 20:39:14.413 INFO SCRIPTING: S-300PS 64H6E TRAILER sr 2021-01-01 20:39:14.413 INFO SCRIPTING: S-300PS 30N6 TRAILER tr 2021-01-01 20:39:14.413 INFO SCRIPTING: S-300PS SA-10B 40B6M MAST tr --]] samTypesDB['S-300PS'] = { ['type'] = 'complex', ['searchRadar'] = { ['S-300PS SA-10B 40B6MD MAST sr'] = { ['name'] = { ['NATO'] = 'Clam Shell', }, }, ['S-300PS 64H6E TRAILER sr'] = { }, }, ['trackingRadar'] = { ['S-300PS 30N6 TRAILER tr'] = { }, ['S-300PS SA-10B 40B6M MAST tr'] = { }, ['S-300PS 40B6M tr'] = { }, ['S-300PMU1 40B6M tr'] = { }, ['S-300PMU1 30N6E tr'] = { }, }, ['misc'] = { ['S-300PS SA-10B 54K6 cp'] = { ['required'] = true, }, }, ['launchers'] = { ['S-300PS 5P85SE_mod ln'] = { }, ['S-300PS 5P85SU_mod ln'] = { }, }, ['name'] = { ['NATO'] = 'SA-10B Grumble' }, ['harm_detection_chance'] = 90, ['can_engage_harm'] = true } --[[ Extra launchers for the in game SA-10C and HighDigitSAMs SA-10B, SA-20B 2021-01-01 21:04:19.908 INFO SCRIPTING: S-300PS 5P85DE ln 2021-01-01 21:04:19.908 INFO SCRIPTING: S-300PS 5P85CE ln --]] local s300launchers = samTypesDB['S-300']['launchers'] s300launchers['S-300PS 5P85DE ln'] = {} s300launchers['S-300PS 5P85CE ln'] = {} local s300launchers = samTypesDB['S-300PS']['launchers'] s300launchers['S-300PS 5P85DE ln'] = {} s300launchers['S-300PS 5P85CE ln'] = {} local s300launchers = samTypesDB['S-300PMU1']['launchers'] s300launchers['S-300PS 5P85DE ln'] = {} s300launchers['S-300PS 5P85CE ln'] = {} --[[ New launcher for the SA-11 complex, will identify as SA-17 SA-17 Buk M1-2 LN 9A310M1-2 --]] samTypesDB['Buk-M2'] = { ['type'] = 'complex', ['searchRadar'] = { ['SA-11 Buk SR 9S18M1'] = { ['name'] = { ['NATO'] = 'Snow Drift', }, }, }, ['launchers'] = { ['SA-17 Buk M1-2 LN 9A310M1-2'] = { }, }, ['misc'] = { ['SA-11 Buk CC 9S470M1'] = { ['required'] = true, }, }, ['name'] = { ['NATO'] = 'SA-17 Grizzly', }, ['harm_detection_chance'] = 90 } --[[ New launcher for the SA-2 complex: S_75M_Volhov_V759 --]] local s75launchers = samTypesDB['S-75']['launchers'] s75launchers['S_75M_Volhov_V759'] = {} --[[ New launcher for the SA-3 complex: --]] local s125launchers = samTypesDB['S-125']['launchers'] s125launchers['5p73 V-601P ln'] = {} --[[ New launcher for the SA-2 complex: HQ_2_Guideline_LN --]] local s125launchers = samTypesDB['S-75']['launchers'] s125launchers['HQ_2_Guideline_LN'] = {} --[[ SA-12 Gladiator / Giant: 2021-03-19 21:24:22.620 INFO SCRIPTING: S-300V 9S15 sr 2021-03-19 21:24:22.620 INFO SCRIPTING: S-300V 9S19 sr 2021-03-19 21:24:22.620 INFO SCRIPTING: S-300V 9S32 tr 2021-03-19 21:24:22.620 INFO SCRIPTING: S-300V 9S457 cp 2021-03-19 21:24:22.620 INFO SCRIPTING: S-300V 9A83 ln 2021-03-19 21:24:22.620 INFO SCRIPTING: S-300V 9A82 ln --]] samTypesDB['S-300V'] = { ['type'] = 'complex', ['searchRadar'] = { ['S-300V 9S15 sr'] = { ['name'] = { ['NATO'] = 'Bill Board', }, }, ['S-300V 9S19 sr'] = { ['name'] = { ['NATO'] = 'High Screen', }, }, }, ['trackingRadar'] = { ['S-300V 9S32 tr'] = { ['NATO'] = 'Grill Pan', }, }, ['misc'] = { ['S-300V 9S457 cp'] = { ['required'] = true, }, }, ['launchers'] = { ['S-300V 9A83 ln'] = { }, ['S-300V 9A82 ln'] = { }, }, ['name'] = { ['NATO'] = 'SA-12 Gladiator/Giant' }, ['harm_detection_chance'] = 90, ['can_engage_harm'] = true } --[[ SA-20B Gargoyle B: 2021-03-25 19:15:02.135 INFO SCRIPTING: S-300PMU2 64H6E2 sr 2021-03-25 19:15:02.135 INFO SCRIPTING: S-300PMU2 92H6E tr 2021-03-25 19:15:02.135 INFO SCRIPTING: S-300PMU2 5P85SE2 ln 2021-03-25 19:15:02.135 INFO SCRIPTING: S-300PMU2 54K6E2 cp --]] samTypesDB['S-300PMU2'] = { ['type'] = 'complex', ['searchRadar'] = { ['S-300PMU2 64H6E2 sr'] = { ['name'] = { ['NATO'] = '', }, }, ['S-300PMU1 40B6MD sr'] = s300PMU140B6MDsr, ['S-300PMU1 64N6E sr'] = s300PMU164N6Esr, }, ['trackingRadar'] = { ['S-300PMU2 92H6E tr'] = { }, ['S-300PS 40B6M tr'] = { }, ['S-300PMU1 40B6M tr'] = { }, ['S-300PMU1 30N6E tr'] = { }, }, ['misc'] = { ['S-300PMU2 54K6E2 cp'] = { ['required'] = true, }, }, ['launchers'] = { ['S-300PMU2 5P85SE2 ln'] = { }, }, ['name'] = { ['NATO'] = 'SA-20B Gargoyle B' }, ['harm_detection_chance'] = 90, ['can_engage_harm'] = true } --[[ --]] end ================================================ FILE: skynet-iads-source/skynet-iads-abstract-dcs-object-wrapper.lua ================================================ do SkynetIADSAbstractDCSObjectWrapper = {} function SkynetIADSAbstractDCSObjectWrapper:create(dcsRepresentation) local instance = {} setmetatable(instance, self) self.__index = self instance.dcsName = "" instance.typeName = "" instance:setDCSRepresentation(dcsRepresentation) if getmetatable(dcsRepresentation) ~= Group then instance.typeName = dcsRepresentation:getTypeName() end return instance end function SkynetIADSAbstractDCSObjectWrapper:setDCSRepresentation(representation) self.dcsRepresentation = representation if self.dcsRepresentation then self.dcsName = self.dcsRepresentation:getName() if (self.dcsName == nil or string.len(self.dcsName) == 0) and self.dcsRepresentation.id_ then self.dcsName = self.dcsRepresentation.id_ end end end function SkynetIADSAbstractDCSObjectWrapper:getDCSRepresentation() return self.dcsRepresentation end function SkynetIADSAbstractDCSObjectWrapper:getName() return self.dcsName end function SkynetIADSAbstractDCSObjectWrapper:getTypeName() return self.typeName end function SkynetIADSAbstractDCSObjectWrapper:getPosition() return self.dcsRepresentation:getPosition() end function SkynetIADSAbstractDCSObjectWrapper:isExist() if self.dcsRepresentation then return self.dcsRepresentation:isExist() else return false end end function SkynetIADSAbstractDCSObjectWrapper:insertToTableIfNotAlreadyAdded(tbl, object) local isAdded = false for i = 1, #tbl do local child = tbl[i] if child == object then isAdded = true end end if isAdded == false then table.insert(tbl, object) end return not isAdded end -- helper code for class inheritance function inheritsFrom( baseClass ) local new_class = {} local class_mt = { __index = new_class } function new_class:create() local newinst = {} setmetatable( newinst, class_mt ) return newinst end if nil ~= baseClass then setmetatable( new_class, { __index = baseClass } ) end -- Implementation of additional OO properties starts here -- -- Return the class object of the instance function new_class:class() return new_class end -- Return the super class object of the instance function new_class:superClass() return baseClass end -- Return true if the caller is an instance of theClass function new_class:isa( theClass ) local b_isa = false local cur_class = new_class while ( nil ~= cur_class ) and ( false == b_isa ) do if cur_class == theClass then b_isa = true else cur_class = cur_class:superClass() end end return b_isa end return new_class end end ================================================ FILE: skynet-iads-source/skynet-iads-abstract-element.lua ================================================ do SkynetIADSAbstractElement = {} SkynetIADSAbstractElement = inheritsFrom(SkynetIADSAbstractDCSObjectWrapper) function SkynetIADSAbstractElement:create(dcsRepresentation, iads) local instance = self:superClass():create(dcsRepresentation) setmetatable(instance, self) self.__index = self instance.connectionNodes = {} instance.powerSources = {} instance.iads = iads instance.natoName = "UNKNOWN" world.addEventHandler(instance) return instance end function SkynetIADSAbstractElement:removeEventHandlers() world.removeEventHandler(self) end function SkynetIADSAbstractElement:cleanUp() self:removeEventHandlers() end function SkynetIADSAbstractElement:isDestroyed() return self:getDCSRepresentation():isExist() == false end function SkynetIADSAbstractElement:addPowerSource(powerSource) table.insert(self.powerSources, powerSource) self:informChildrenOfStateChange() return self end function SkynetIADSAbstractElement:getPowerSources() return self.powerSources end function SkynetIADSAbstractElement:addConnectionNode(connectionNode) table.insert(self.connectionNodes, connectionNode) self:informChildrenOfStateChange() return self end function SkynetIADSAbstractElement:getConnectionNodes() return self.connectionNodes end function SkynetIADSAbstractElement:hasActiveConnectionNode() local connectionNode = self:genericCheckOneObjectIsAlive(self.connectionNodes) if connectionNode == false and self.iads:getDebugSettings().samNoConnection then self.iads:printOutput(self:getDescription().." no connection to Command Center") end return connectionNode end function SkynetIADSAbstractElement:hasWorkingPowerSource() local power = self:genericCheckOneObjectIsAlive(self.powerSources) if power == false and self.iads:getDebugSettings().hasNoPower then self.iads:printOutput(self:getDescription().." has no power") end return power end function SkynetIADSAbstractElement:getDCSName() return self.dcsName end -- generic function to theck if power plants, command centers, connection nodes are still alive function SkynetIADSAbstractElement:genericCheckOneObjectIsAlive(objects) local isAlive = (#objects == 0) for i = 1, #objects do local object = objects[i] --if we find one object that is not fully destroyed we assume the IADS is still working if object:isExist() then isAlive = true break end end return isAlive end function SkynetIADSAbstractElement:getNatoName() return self.natoName end function SkynetIADSAbstractElement:getDescription() return "IADS ELEMENT: "..self:getDCSName().." | Type: "..tostring(self:getNatoName()) end function SkynetIADSAbstractElement:onEvent(event) --if a unit is destroyed we check to see if its a power plant powering the unit or a connection node if event.id == world.event.S_EVENT_DEAD then if self:hasWorkingPowerSource() == false or self:isDestroyed() then self:goDark() self:informChildrenOfStateChange() end if self:hasActiveConnectionNode() == false then self:informChildrenOfStateChange() end end if event.id == world.event.S_EVENT_SHOT then self:weaponFired(event) end end --placeholder method, can be implemented by subclasses function SkynetIADSAbstractElement:weaponFired(event) end --placeholder method, can be implemented by subclasses function SkynetIADSAbstractElement:goDark() end --placeholder method, can be implemented by subclasses function SkynetIADSAbstractElement:goAutonomous() end --placeholder method, can be implemented by subclasses function SkynetIADSAbstractElement:setToCorrectAutonomousState() end --placeholder method, can be implemented by subclasses function SkynetIADSAbstractElement:informChildrenOfStateChange() end end ================================================ FILE: skynet-iads-source/skynet-iads-abstract-radar-element.lua ================================================ do SkynetIADSAbstractRadarElement = {} SkynetIADSAbstractRadarElement = inheritsFrom(SkynetIADSAbstractElement) SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DCS_AI = 1 SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK = 2 SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE = 1 SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE = 2 SkynetIADSAbstractRadarElement.HARM_TO_SAM_ASPECT = 15 SkynetIADSAbstractRadarElement.HARM_LOOKAHEAD_NM = 20 function SkynetIADSAbstractRadarElement:create(dcsElementWithRadar, iads) local instance = self:superClass():create(dcsElementWithRadar, iads) setmetatable(instance, self) self.__index = self instance.aiState = false instance.harmScanID = nil instance.harmSilenceID = nil instance.lastJammerUpdate = 0 instance.objectsIdentifiedAsHarms = {} instance.objectsIdentifiedAsHarmsMaxTargetAge = 60 instance.launchers = {} instance.trackingRadars = {} instance.searchRadars = {} instance.parentRadars = {} instance.childRadars = {} instance.missilesInFlight = {} instance.pointDefences = {} instance.harmDecoys = {} instance.autonomousBehaviour = SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DCS_AI instance.goLiveRange = SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE instance.isAutonomous = true instance.harmDetectionChance = 0 instance.minHarmShutdownTime = 0 instance.maxHarmShutDownTime = 0 instance.minHarmPresetShutdownTime = 30 instance.maxHarmPresetShutdownTime = 180 instance.harmShutdownTime = 0 instance.firingRangePercent = 100 instance.actAsEW = false instance.cachedTargets = {} instance.cachedTargetsMaxAge = 1 instance.cachedTargetsCurrentAge = 0 instance.goLiveTime = 0 instance.engageAirWeapons = false instance.isAPointDefence = false instance.canEngageHARM = false instance.dataBaseSupportedTypesCanEngageHARM = false -- 5 seconds seems to be a good value for the sam site to find the target with its organic radar instance.noCacheActiveForSecondsAfterGoLive = 5 return instance end --TODO: this method could be updated to only return Radar weapons fired, this way a SAM firing an IR weapon could go dark faster in the goDark() method function SkynetIADSAbstractRadarElement:weaponFired(event) if event.id == world.event.S_EVENT_SHOT then local weapon = event.weapon local launcherFired = event.initiator for i = 1, #self.launchers do local launcher = self.launchers[i] if launcher:getDCSRepresentation() == launcherFired then table.insert(self.missilesInFlight, weapon) end end end end function SkynetIADSAbstractRadarElement:setCachedTargetsMaxAge(maxAge) self.cachedTargetsMaxAge = maxAge end function SkynetIADSAbstractRadarElement:cleanUp() for i = 1, #self.pointDefences do local pointDefence = self.pointDefences[i] pointDefence:cleanUp() end mist.removeFunction(self.harmScanID) mist.removeFunction(self.harmSilenceID) --call method from super class self:removeEventHandlers() end function SkynetIADSAbstractRadarElement:setIsAPointDefence(state) if (state == true or state == false) then self.isAPointDefence = state end end function SkynetIADSAbstractRadarElement:getIsAPointDefence() return self.isAPointDefence end function SkynetIADSAbstractRadarElement:addPointDefence(pointDefence) table.insert(self.pointDefences, pointDefence) pointDefence:setIsAPointDefence(true) return self end function SkynetIADSAbstractRadarElement:getPointDefences() return self.pointDefences end function SkynetIADSAbstractRadarElement:addHARMDecoy(harmDecoy) table.insert(self.harmDecoys, harmDecoy) end function SkynetIADSAbstractRadarElement:addParentRadar(parentRadar) self:insertToTableIfNotAlreadyAdded(self.parentRadars, parentRadar) self:informChildrenOfStateChange() end function SkynetIADSAbstractRadarElement:getParentRadars() return self.parentRadars end function SkynetIADSAbstractRadarElement:clearParentRadars() self.parentRadars = {} end function SkynetIADSAbstractRadarElement:addChildRadar(childRadar) self:insertToTableIfNotAlreadyAdded(self.childRadars, childRadar) end function SkynetIADSAbstractRadarElement:getChildRadars() return self.childRadars end function SkynetIADSAbstractRadarElement:clearChildRadars() self.childRadars = {} end --TODO: unit test this method function SkynetIADSAbstractRadarElement:getUsableChildRadars() local usableRadars = {} for i = 1, #self.childRadars do local childRadar = self.childRadars[i] if childRadar:hasWorkingPowerSource() and childRadar:hasActiveConnectionNode() then table.insert(usableRadars, childRadar) end end return usableRadars end function SkynetIADSAbstractRadarElement:informChildrenOfStateChange() self:setToCorrectAutonomousState() local children = self:getChildRadars() for i = 1, #children do local childRadar = children[i] childRadar:setToCorrectAutonomousState() end self.iads:getMooseConnector():update() end function SkynetIADSAbstractRadarElement:setToCorrectAutonomousState() local parents = self:getParentRadars() for i = 1, #parents do local parent = parents[i] --of one parent exists that still is connected to the IADS, the SAM site does not have to go autonomous --instead of isDestroyed() write method, hasWorkingSearchRadars() if self:hasActiveConnectionNode() and self.iads:isCommandCenterUsable() and parent:hasWorkingPowerSource() and parent:hasActiveConnectionNode() and parent:getActAsEW() == true and parent:isDestroyed() == false then self:resetAutonomousState() return end end self:goAutonomous() end function SkynetIADSAbstractRadarElement:setAutonomousBehaviour(mode) if mode ~= nil then self.autonomousBehaviour = mode end return self end function SkynetIADSAbstractRadarElement:getAutonomousBehaviour() return self.autonomousBehaviour end function SkynetIADSAbstractRadarElement:resetAutonomousState() self.isAutonomous = false self:goDark() end function SkynetIADSAbstractRadarElement:goAutonomous() self.isAutonomous = true if self.autonomousBehaviour == SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK then self:goDark() else self:goLive() end end function SkynetIADSAbstractRadarElement:getAutonomousState() return self.isAutonomous end function SkynetIADSAbstractRadarElement:pointDefencesHaveRemainingAmmo(minNumberOfMissiles) local remainingMissiles = 0 for i = 1, #self.pointDefences do local pointDefence = self.pointDefences[i] remainingMissiles = remainingMissiles + pointDefence:getRemainingNumberOfMissiles() end return self:hasRequiredNumberOfMissiles(minNumberOfMissiles, remainingMissiles) end function SkynetIADSAbstractRadarElement:hasRequiredNumberOfMissiles(minNumberOfMissiles, remainingMissiles) local returnValue = false if ( remainingMissiles > 0 and remainingMissiles >= minNumberOfMissiles ) then returnValue = true end return returnValue end function SkynetIADSAbstractRadarElement:hasRemainingAmmoToEngageMissiles(minNumberOfMissiles) local remainingMissiles = self:getRemainingNumberOfMissiles() return self:hasRequiredNumberOfMissiles(minNumberOfMissiles, remainingMissiles) end -- this method needs to be refactored so that it works for ew radars that don't have launchers, or that it is only called by sam sites function SkynetIADSAbstractRadarElement:hasEnoughLaunchersToEngageMissiles(minNumberOfLaunchers) local launchers = self:getLaunchers() if(launchers ~= nil) then launchers = #self:getLaunchers() else launchers = 0 end return self:hasRequiredNumberOfMissiles(minNumberOfLaunchers, launchers) end function SkynetIADSAbstractRadarElement:pointDefencesHaveEnoughLaunchers(minNumberOfLaunchers) local numOfLaunchers = 0 for i = 1, #self.pointDefences do local pointDefence = self.pointDefences[i] numOfLaunchers = numOfLaunchers + #pointDefence:getLaunchers() end return self:hasRequiredNumberOfMissiles(minNumberOfLaunchers, numOfLaunchers) end function SkynetIADSAbstractRadarElement:setIgnoreHARMSWhilePointDefencesHaveAmmo(state) self.iads:printOutputToLog("DEPRECATED: setIgnoreHARMSWhilePointDefencesHaveAmmo SAM Site will stay live automaticall as long as itself or it's point defences can defend against a HARM") return self end function SkynetIADSAbstractRadarElement:hasMissilesInFlight() return #self.missilesInFlight > 0 end function SkynetIADSAbstractRadarElement:getNumberOfMissilesInFlight() return #self.missilesInFlight end -- DCS does not send an event, when a missile is destroyed, so this method needs to be polled so that the missiles in flight are current, polling is done in the HARM Search call: evaluateIfTargetsContainHARMs function SkynetIADSAbstractRadarElement:updateMissilesInFlight() local missilesInFlight = {} for i = 1, #self.missilesInFlight do local missile = self.missilesInFlight[i] if missile:isExist() then table.insert(missilesInFlight, missile) end end self.missilesInFlight = missilesInFlight self:goDarkIfOutOfAmmo() end function SkynetIADSAbstractRadarElement:goDarkIfOutOfAmmo() if self:hasRemainingAmmo() == false and self:getActAsEW() == false then self:goDark() end end function SkynetIADSAbstractRadarElement:getActAsEW() return self.actAsEW end function SkynetIADSAbstractRadarElement:setActAsEW(ewState) if ewState == true or ewState == false then local stateChange = false if ewState ~= self.actAsEW then stateChange = true end self.actAsEW = ewState if stateChange then self:informChildrenOfStateChange() end end if self.actAsEW == true then self:goLive() else self:goDark() end return self end function SkynetIADSAbstractRadarElement:getUnitsToAnalyse() local units = {} table.insert(units, self:getDCSRepresentation()) if getmetatable(self:getDCSRepresentation()) == Group then units = self:getDCSRepresentation():getUnits() end return units end function SkynetIADSAbstractRadarElement:getRemainingNumberOfMissiles() local remainingNumberOfMissiles = 0 for i = 1, #self.launchers do local launcher = self.launchers[i] remainingNumberOfMissiles = remainingNumberOfMissiles + launcher:getRemainingNumberOfMissiles() end return remainingNumberOfMissiles end function SkynetIADSAbstractRadarElement:getInitialNumberOfMissiles() local initalNumberOfMissiles = 0 for i = 1, #self.launchers do local launcher = self.launchers[i] initalNumberOfMissiles = launcher:getInitialNumberOfMissiles() + initalNumberOfMissiles end return initalNumberOfMissiles end function SkynetIADSAbstractRadarElement:getRemainingNumberOfShells() local remainingNumberOfShells = 0 for i = 1, #self.launchers do local launcher = self.launchers[i] remainingNumberOfShells = remainingNumberOfShells + launcher:getRemainingNumberOfShells() end return remainingNumberOfShells end function SkynetIADSAbstractRadarElement:getInitialNumberOfShells() local initialNumberOfShells = 0 for i = 1, #self.launchers do local launcher = self.launchers[i] initialNumberOfShells = initialNumberOfShells + launcher:getInitialNumberOfShells() end return initialNumberOfShells end function SkynetIADSAbstractRadarElement:hasRemainingAmmo() --the launcher check is due to ew radars they have no launcher and no ammo and therefore are never out of ammo return ( #self.launchers == 0 ) or ((self:getRemainingNumberOfMissiles() > 0 ) or ( self:getRemainingNumberOfShells() > 0 ) ) end function SkynetIADSAbstractRadarElement:getHARMDetectionChance() return self.harmDetectionChance end function SkynetIADSAbstractRadarElement:setHARMDetectionChance(chance) if chance and chance >= 0 and chance <= 100 then self.harmDetectionChance = chance end return self end function SkynetIADSAbstractRadarElement:setupElements() local numUnits = #self:getUnitsToAnalyse() for typeName, dataType in pairs(SkynetIADS.database) do local hasSearchRadar = false local hasTrackingRadar = false local hasLauncher = false self.searchRadars = {} self.trackingRadars = {} self.launchers = {} for entry, unitData in pairs(dataType) do if entry == 'searchRadar' then self:analyseAndAddUnit(SkynetIADSSAMSearchRadar, self.searchRadars, unitData) hasSearchRadar = true end if entry == 'launchers' then self:analyseAndAddUnit(SkynetIADSSAMLauncher, self.launchers, unitData) hasLauncher = true end if entry == 'trackingRadar' then self:analyseAndAddUnit(SkynetIADSSAMTrackingRadar, self.trackingRadars, unitData) hasTrackingRadar = true end end --this check ensures a unit or group has all required elements for the specific sam or ew type: if (hasLauncher and hasSearchRadar and hasTrackingRadar and #self.launchers > 0 and #self.searchRadars > 0 and #self.trackingRadars > 0 ) or (hasSearchRadar and hasLauncher and #self.searchRadars > 0 and #self.launchers > 0) then self:setHARMDetectionChance(dataType['harm_detection_chance']) self.dataBaseSupportedTypesCanEngageHARM = dataType['can_engage_harm'] self:setCanEngageHARM(self.dataBaseSupportedTypesCanEngageHARM) local natoName = dataType['name']['NATO'] self:buildNatoName(natoName) break end end end function SkynetIADSAbstractRadarElement:setCanEngageHARM(canEngage) if canEngage == true or canEngage == false then self.canEngageHARM = canEngage if ( canEngage == true and self:getCanEngageAirWeapons() == false ) then self:setCanEngageAirWeapons(true) end end return self end function SkynetIADSAbstractRadarElement:getCanEngageHARM() return self.canEngageHARM end function SkynetIADSAbstractRadarElement:setCanEngageAirWeapons(engageAirWeapons) if self:isDestroyed() == false then local controller = self:getDCSRepresentation():getController() if ( engageAirWeapons == true ) then controller:setOption(AI.Option.Ground.id.ENGAGE_AIR_WEAPONS, true) --its important that we set var to true here, to prevent recursion in setCanEngageHARM self.engageAirWeapons = true --we set the original value we got when loading info about the SAM site self:setCanEngageHARM(self.dataBaseSupportedTypesCanEngageHARM) else controller:setOption(AI.Option.Ground.id.ENGAGE_AIR_WEAPONS, false) self:setCanEngageHARM(false) self.engageAirWeapons = false end end return self end function SkynetIADSAbstractRadarElement:getCanEngageAirWeapons() return self.engageAirWeapons end function SkynetIADSAbstractRadarElement:buildNatoName(natoName) --we shorten the SA-XX names and don't return their code names eg goa, gainful.. local pos = natoName:find(" ") local prefix = natoName:sub(1, 2) if string.lower(prefix) == 'sa' and pos ~= nil then self.natoName = natoName:sub(1, (pos-1)) else self.natoName = natoName end end function SkynetIADSAbstractRadarElement:analyseAndAddUnit(class, tableToAdd, unitData) local units = self:getUnitsToAnalyse() for i = 1, #units do local unit = units[i] self:buildSingleUnit(unit, class, tableToAdd, unitData) end end function SkynetIADSAbstractRadarElement:buildSingleUnit(unit, class, tableToAdd, unitData) local unitTypeName = unit:getTypeName() for unitName, unitPerformanceData in pairs(unitData) do if unitName == unitTypeName then samElement = class:create(unit) samElement:setupRangeData() table.insert(tableToAdd, samElement) end end end function SkynetIADSAbstractRadarElement:getController() local dcsRepresentation = self:getDCSRepresentation() if dcsRepresentation:isExist() then return dcsRepresentation:getController() else return nil end end function SkynetIADSAbstractRadarElement:getLaunchers() return self.launchers end function SkynetIADSAbstractRadarElement:getSearchRadars() return self.searchRadars end function SkynetIADSAbstractRadarElement:getTrackingRadars() return self.trackingRadars end function SkynetIADSAbstractRadarElement:getRadars() local radarUnits = {} for i = 1, #self.searchRadars do table.insert(radarUnits, self.searchRadars[i]) end for i = 1, #self.trackingRadars do table.insert(radarUnits, self.trackingRadars[i]) end return radarUnits end function SkynetIADSAbstractRadarElement:setGoLiveRangeInPercent(percent) if percent ~= nil then self.firingRangePercent = percent for i = 1, #self.launchers do local launcher = self.launchers[i] launcher:setFiringRangePercent(self.firingRangePercent) end for i = 1, #self.searchRadars do local radar = self.searchRadars[i] radar:setFiringRangePercent(self.firingRangePercent) end end return self end function SkynetIADSAbstractRadarElement:getGoLiveRangeInPercent() return self.firingRangePercent end function SkynetIADSAbstractRadarElement:setEngagementZone(engagementZone) if engagementZone == SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE then self.goLiveRange = engagementZone elseif engagementZone == SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE then self.goLiveRange = engagementZone end return self end function SkynetIADSAbstractRadarElement:getEngagementZone() return self.goLiveRange end function SkynetIADSAbstractRadarElement:goLive() if ( self.aiState == false and self:hasWorkingPowerSource() and self.harmSilenceID == nil) and (self:hasRemainingAmmo() == true ) then if self:isDestroyed() == false then local cont = self:getController() cont:setOnOff(true) cont:setOption(AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.RED) cont:setOption(AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_FREE) self:getDCSRepresentation():enableEmission(true) self.goLiveTime = timer.getTime() self.aiState = true end self:pointDefencesStopActingAsEW() if self.iads:getDebugSettings().radarWentLive then self.iads:printOutputToLog("GOING LIVE: "..self:getDescription()) end self:scanForHarms() end end function SkynetIADSAbstractRadarElement:pointDefencesStopActingAsEW() for i = 1, #self.pointDefences do local pointDefence = self.pointDefences[i] pointDefence:setActAsEW(false) end end function SkynetIADSAbstractRadarElement:goDark() if (self:hasWorkingPowerSource() == false) or ( self.aiState == true ) and (self.harmSilenceID ~= nil or ( self.harmSilenceID == nil and #self:getDetectedTargets() == 0 and self:hasMissilesInFlight() == false) or ( self.harmSilenceID == nil and #self:getDetectedTargets() > 0 and self:hasMissilesInFlight() == false and self:hasRemainingAmmo() == false ) ) then if self:isDestroyed() == false then self:getDCSRepresentation():enableEmission(false) end -- point defence will only go live if the Radar Emitting site it is protecting goes dark and this is due to a it defending against a HARM if (self.harmSilenceID ~= nil) then self:pointDefencesGoLive() if self:isDestroyed() == false then --if site goes dark due to HARM we turn off AI, this is due to a bug in DCS multiplayer where the harm will find its way to the radar emitter if just setEmissions is set to false local controller = self:getController() controller:setOnOff(false) end end self.aiState = false self:stopScanningForHARMs() self.cachedTargets = {} if self.iads:getDebugSettings().radarWentDark then self.iads:printOutputToLog("GOING DARK: "..self:getDescription()) end end end function SkynetIADSAbstractRadarElement:pointDefencesGoLive() local setActive = false for i = 1, #self.pointDefences do local pointDefence = self.pointDefences[i] if ( pointDefence:getActAsEW() == false ) then setActive = true pointDefence:setActAsEW(true) end end return setActive end function SkynetIADSAbstractRadarElement:isActive() return self.aiState end function SkynetIADSAbstractRadarElement:isTargetInRange(target) local isSearchRadarInRange = false local isTrackingRadarInRange = false local isLauncherInRange = false local isSearchRadarInRange = ( #self.searchRadars == 0 ) for i = 1, #self.searchRadars do local searchRadar = self.searchRadars[i] if searchRadar:isInRange(target) then isSearchRadarInRange = true break end end if self.goLiveRange == SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE then isLauncherInRange = ( #self.launchers == 0 ) for i = 1, #self.launchers do local launcher = self.launchers[i] if launcher:isInRange(target) then isLauncherInRange = true break end end isTrackingRadarInRange = ( #self.trackingRadars == 0 ) for i = 1, #self.trackingRadars do local trackingRadar = self.trackingRadars[i] if trackingRadar:isInRange(target) then isTrackingRadarInRange = true break end end else isLauncherInRange = true isTrackingRadarInRange = true end return (isSearchRadarInRange and isTrackingRadarInRange and isLauncherInRange ) end function SkynetIADSAbstractRadarElement:isInRadarDetectionRangeOf(abstractRadarElement) local radars = self:getRadars() local abstractRadarElementRadars = abstractRadarElement:getRadars() for i = 1, #radars do local radar = radars[i] for j = 1, #abstractRadarElementRadars do local abstractRadarElementRadar = abstractRadarElementRadars[j] if abstractRadarElementRadar:isExist() and radar:isExist() then local distance = self:getDistanceToUnit(radar:getDCSRepresentation():getPosition().p, abstractRadarElementRadar:getDCSRepresentation():getPosition().p) if abstractRadarElementRadar:getMaxRangeFindingTarget() >= distance then return true end end end end return false end function SkynetIADSAbstractRadarElement:getDistanceToUnit(unitPosA, unitPosB) return mist.utils.round(mist.utils.get2DDist(unitPosA, unitPosB, 0)) end function SkynetIADSAbstractRadarElement:hasWorkingRadar() local radars = self:getRadars() for i = 1, #radars do local radar = radars[i] if radar:isRadarWorking() == true then return true end end return false end function SkynetIADSAbstractRadarElement:jam(successProbability) if self:isDestroyed() == false then local controller = self:getController() local probability = math.random(1, 100) if self.iads:getDebugSettings().jammerProbability then self.iads:printOutputToLog("JAMMER: "..self:getDescription()..": Probability: "..successProbability) end if successProbability > probability then controller:setOption(AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD) if self.iads:getDebugSettings().jammerProbability then self.iads:printOutputToLog("JAMMER: "..self:getDescription()..": jammed, setting to weapon hold") end else controller:setOption(AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_FREE) if self.iads:getDebugSettings().jammerProbability then self.iads:printOutputToLog("JAMMER: "..self:getDescription()..": jammed, setting to weapon free") end end self.lastJammerUpdate = timer:getTime() end end function SkynetIADSAbstractRadarElement:scanForHarms() self:stopScanningForHARMs() self.harmScanID = mist.scheduleFunction(SkynetIADSAbstractRadarElement.evaluateIfTargetsContainHARMs, {self}, 1, 2) end function SkynetIADSAbstractRadarElement:isScanningForHARMs() return self.harmScanID ~= nil end function SkynetIADSAbstractRadarElement:isDefendingHARM() return self.harmSilenceID ~= nil end function SkynetIADSAbstractRadarElement:stopScanningForHARMs() mist.removeFunction(self.harmScanID) self.harmScanID = nil end function SkynetIADSAbstractRadarElement:goSilentToEvadeHARM(timeToImpact) self:finishHarmDefence(self) if ( timeToImpact == nil ) then timeToImpact = 0 end self.minHarmShutdownTime = self:calculateMinimalShutdownTimeInSeconds(timeToImpact) self.maxHarmShutDownTime = self:calculateMaximalShutdownTimeInSeconds(self.minHarmShutdownTime) self.harmShutdownTime = self:calculateHARMShutdownTime() if self.iads:getDebugSettings().harmDefence then self.iads:printOutputToLog("HARM DEFENCE SHUTTING DOWN: "..self:getDCSName().." | FOR: "..self.harmShutdownTime.." seconds | TTI: "..timeToImpact) end self.harmSilenceID = mist.scheduleFunction(SkynetIADSAbstractRadarElement.finishHarmDefence, {self}, timer.getTime() + self.harmShutdownTime, 1) self:goDark() end function SkynetIADSAbstractRadarElement:getHARMShutdownTime() return self.harmShutdownTime end function SkynetIADSAbstractRadarElement:calculateHARMShutdownTime() local shutDownTime = math.random(self.minHarmShutdownTime, self.maxHarmShutDownTime) return shutDownTime end function SkynetIADSAbstractRadarElement.finishHarmDefence(self) mist.removeFunction(self.harmSilenceID) self.harmSilenceID = nil self.harmShutdownTime = 0 if ( self:getAutonomousState() == true ) then self:goAutonomous() end end function SkynetIADSAbstractRadarElement:getDetectedTargets() if ( timer.getTime() - self.cachedTargetsCurrentAge > self.cachedTargetsMaxAge ) or ( timer.getTime() - self.goLiveTime < self.noCacheActiveForSecondsAfterGoLive ) then self.cachedTargets = {} self.cachedTargetsCurrentAge = timer.getTime() if self:hasWorkingPowerSource() and self:isDestroyed() == false then local targets = self:getController():getDetectedTargets(Controller.Detection.RADAR) for i = 1, #targets do local target = targets[i] -- there are cases when a destroyed object is still visible as a target to the radar, don't add it, will cause errors everywhere the dcs object is accessed if target.object then local iadsTarget = SkynetIADSContact:create(target, self) iadsTarget:refresh() if self:isTargetInRange(iadsTarget) then table.insert(self.cachedTargets, iadsTarget) end end end end end return self.cachedTargets end function SkynetIADSAbstractRadarElement:getSecondsToImpact(distanceNM, speedKT) local tti = 0 if speedKT > 0 then tti = mist.utils.round((distanceNM / speedKT) * 3600, 0) if tti < 0 then tti = 0 end end return tti end function SkynetIADSAbstractRadarElement:getDistanceInMetersToContact(radarUnit, point) return mist.utils.round(mist.utils.get3DDist(radarUnit:getPosition().p, point), 0) end function SkynetIADSAbstractRadarElement:calculateMinimalShutdownTimeInSeconds(timeToImpact) return timeToImpact + self.minHarmPresetShutdownTime end function SkynetIADSAbstractRadarElement:calculateMaximalShutdownTimeInSeconds(minShutdownTime) return minShutdownTime + mist.random(1, self.maxHarmPresetShutdownTime) end function SkynetIADSAbstractRadarElement:calculateImpactPoint(target, distanceInMeters) -- distance needs to be incremented by a certain value for ip calculation to work, check why presumably due to rounding errors in the previous distance calculation return land.getIP(target:getPosition().p, target:getPosition().x, distanceInMeters + 50) end function SkynetIADSAbstractRadarElement:shallReactToHARM() return self.harmDetectionChance >= math.random(1, 100) end -- will only check for missiles, if DCS ads AAA than can engage HARMs then this code must be updated: function SkynetIADSAbstractRadarElement:shallIgnoreHARMShutdown() local numOfHarms = self:getNumberOfObjectsItentifiedAsHARMS() --[[ self.iads:printOutputToLog("Self enough launchers: "..tostring(self:hasEnoughLaunchersToEngageMissiles(numOfHarms))) self.iads:printOutputToLog("Self enough missiles: "..tostring(self:hasRemainingAmmoToEngageMissiles(numOfHarms))) self.iads:printOutputToLog("PD enough missiles: "..tostring(self:pointDefencesHaveRemainingAmmo(numOfHarms))) self.iads:printOutputToLog("PD enough launchers: "..tostring(self:pointDefencesHaveEnoughLaunchers(numOfHarms))) --]] return ( ((self:hasEnoughLaunchersToEngageMissiles(numOfHarms) and self:hasRemainingAmmoToEngageMissiles(numOfHarms) and self:getCanEngageHARM()) or (self:pointDefencesHaveRemainingAmmo(numOfHarms) and self:pointDefencesHaveEnoughLaunchers(numOfHarms)))) end function SkynetIADSAbstractRadarElement:informOfHARM(harmContact) local radars = self:getRadars() for j = 1, #radars do local radar = radars[j] if radar:isExist() then local distanceNM = mist.utils.metersToNM(self:getDistanceInMetersToContact(radar, harmContact:getPosition().p)) local harmToSAMHeading = mist.utils.toDegree(mist.utils.getHeadingPoints(harmContact:getPosition().p, radar:getPosition().p)) local harmToSAMAspect = self:calculateAspectInDegrees(harmContact:getMagneticHeading(), harmToSAMHeading) local speedKT = harmContact:getGroundSpeedInKnots(0) local secondsToImpact = self:getSecondsToImpact(distanceNM, speedKT) --TODO: use tti instead of distanceNM? -- when iterating through the radars, store shortest tti and work with that value?? if ( harmToSAMAspect < SkynetIADSAbstractRadarElement.HARM_TO_SAM_ASPECT and distanceNM < SkynetIADSAbstractRadarElement.HARM_LOOKAHEAD_NM ) then self:addObjectIdentifiedAsHARM(harmContact) if ( #self:getPointDefences() > 0 and self:pointDefencesGoLive() == true and self.iads:getDebugSettings().harmDefence ) then self.iads:printOutputToLog("POINT DEFENCES GOING LIVE FOR: "..self:getDCSName().." | TTI: "..secondsToImpact) end --self.iads:printOutputToLog("Ignore HARM shutdown: "..tostring(self:shallIgnoreHARMShutdown())) if ( self:getIsAPointDefence() == false and ( self:isDefendingHARM() == false or ( self:getHARMShutdownTime() < secondsToImpact ) ) and self:shallIgnoreHARMShutdown() == false) then self:goSilentToEvadeHARM(secondsToImpact) break end end end end end function SkynetIADSAbstractElement:addObjectIdentifiedAsHARM(harmContact) self:insertToTableIfNotAlreadyAdded(self.objectsIdentifiedAsHarms, harmContact) end function SkynetIADSAbstractRadarElement:calculateAspectInDegrees(harmHeading, harmToSAMHeading) local aspect = harmHeading - harmToSAMHeading if ( aspect < 0 ) then aspect = -1 * aspect end if aspect > 180 then aspect = 360 - aspect end return mist.utils.round(aspect) end function SkynetIADSAbstractRadarElement:getNumberOfObjectsItentifiedAsHARMS() return #self.objectsIdentifiedAsHarms end function SkynetIADSAbstractRadarElement:cleanUpOldObjectsIdentifiedAsHARMS() local newHARMS = {} for i = 1, #self.objectsIdentifiedAsHarms do local harmContact = self.objectsIdentifiedAsHarms[i] if harmContact:getAge() < self.objectsIdentifiedAsHarmsMaxTargetAge then table.insert(newHARMS, harmContact) end end --stop point defences acting as ew (always on), will occur if activated via evaluateIfTargetsContainHARMs() --if in this iteration all harms where cleared we turn of the point defence. But in any other cases we dont turn of point defences, that interferes with other parts of the iads -- when setting up the iads (letting pds go to read state) if (#newHARMS == 0 and self:getNumberOfObjectsItentifiedAsHARMS() > 0 ) then self:pointDefencesStopActingAsEW() end self.objectsIdentifiedAsHarms = newHARMS end function SkynetIADSAbstractRadarElement.evaluateIfTargetsContainHARMs(self) --if an emitter dies the SAM site being jammed will revert back to normal operation: if self.lastJammerUpdate > 0 and ( timer:getTime() - self.lastJammerUpdate ) > 10 then self:jam(0) self.lastJammerUpdate = 0 end --we use the regular interval of this method to update to other states: self:updateMissilesInFlight() self:cleanUpOldObjectsIdentifiedAsHARMS() end end ================================================ FILE: skynet-iads-source/skynet-iads-awacs-radar.lua ================================================ do --this class is currently used for AWACS and Ships, at a latter date a separate class for ships could be created, currently not needed SkynetIADSAWACSRadar = {} SkynetIADSAWACSRadar = inheritsFrom(SkynetIADSAbstractRadarElement) function SkynetIADSAWACSRadar:create(radarUnit, iads) local instance = self:superClass():create(radarUnit, iads) setmetatable(instance, self) self.__index = self instance.lastUpdatePosition = nil instance.natoName = radarUnit:getTypeName() return instance end function SkynetIADSAWACSRadar:setupElements() local unit = self:getDCSRepresentation() local radar = SkynetIADSSAMSearchRadar:create(unit) radar:setupRangeData() table.insert(self.searchRadars, radar) end -- AWACs will not scan for HARMS function SkynetIADSAWACSRadar:scanForHarms() end function SkynetIADSAWACSRadar:getMaxAllowedMovementForAutonomousUpdateInNM() --local radarRange = mist.utils.metersToNM(self.searchRadars[1]:getMaxRangeFindingTarget()) --return mist.utils.round(radarRange / 10) --fixed to 10 nm miles to better fit small SAM sites return 10 end function SkynetIADSAWACSRadar:isUpdateOfAutonomousStateOfSAMSitesRequired() local isUpdateRequired = self:getDistanceTraveledSinceLastUpdate() > self:getMaxAllowedMovementForAutonomousUpdateInNM() if isUpdateRequired then self.lastUpdatePosition = nil end return isUpdateRequired end function SkynetIADSAWACSRadar:getDistanceTraveledSinceLastUpdate() local currentPosition = nil if self.lastUpdatePosition == nil and self:getDCSRepresentation():isExist() then self.lastUpdatePosition = self:getDCSRepresentation():getPosition().p end if self:getDCSRepresentation():isExist() then currentPosition = self:getDCSRepresentation():getPosition().p end return mist.utils.round(mist.utils.metersToNM(self:getDistanceToUnit(self.lastUpdatePosition, currentPosition))) end end ================================================ FILE: skynet-iads-source/skynet-iads-command-center.lua ================================================ do SkynetIADSCommandCenter = {} SkynetIADSCommandCenter = inheritsFrom(SkynetIADSAbstractRadarElement) function SkynetIADSCommandCenter:create(commandCenter, iads) local instance = self:superClass():create(commandCenter, iads) setmetatable(instance, self) self.__index = self instance.natoName = "COMMAND CENTER" return instance end function SkynetIADSCommandCenter:goDark() end function SkynetIADSCommandCenter:goLive() end end ================================================ FILE: skynet-iads-source/skynet-iads-contact.lua ================================================ do SkynetIADSContact = {} SkynetIADSContact = inheritsFrom(SkynetIADSAbstractDCSObjectWrapper) SkynetIADSContact.CLIMB = "CLIMB" SkynetIADSContact.DESCEND = "DESCEND" SkynetIADSContact.HARM = "HARM" SkynetIADSContact.NOT_HARM = "NOT_HARM" SkynetIADSContact.HARM_UNKNOWN = "HARM_UNKNOWN" function SkynetIADSContact:create(dcsRadarTarget, abstractRadarElementDetected) local instance = self:superClass():create(dcsRadarTarget.object) setmetatable(instance, self) self.__index = self instance.abstractRadarElementsDetected = {} table.insert(instance.abstractRadarElementsDetected, abstractRadarElementDetected) instance.firstContactTime = timer.getAbsTime() instance.lastTimeSeen = 0 instance.dcsRadarTarget = dcsRadarTarget instance.position = instance:getDCSRepresentation():getPosition() instance.numOfTimesRefreshed = 0 instance.speed = 0 instance.harmState = SkynetIADSContact.HARM_UNKNOWN instance.simpleAltitudeProfile = {} return instance end function SkynetIADSContact:setHARMState(state) self.harmState = state end function SkynetIADSContact:getHARMState() return self.harmState end function SkynetIADSContact:isIdentifiedAsHARM() return self.harmState == SkynetIADSContact.HARM end function SkynetIADSContact:isHARMStateUnknown() return self.harmState == SkynetIADSContact.HARM_UNKNOWN end function SkynetIADSContact:getMagneticHeading() if ( self:isExist() ) then return mist.utils.round(mist.utils.toDegree(mist.getHeading(self:getDCSRepresentation()))) else return -1 end end function SkynetIADSContact:getAbstractRadarElementsDetected() return self.abstractRadarElementsDetected end function SkynetIADSContact:addAbstractRadarElementDetected(radar) self:insertToTableIfNotAlreadyAdded(self.abstractRadarElementsDetected, radar) end function SkynetIADSContact:isTypeKnown() return self.dcsRadarTarget.type end function SkynetIADSContact:isDistanceKnown() return self.dcsRadarTarget.distance end function SkynetIADSContact:getTypeName() if self:isIdentifiedAsHARM() then return SkynetIADSContact.HARM end if self:getDCSRepresentation() ~= nil then local category = self:getDCSRepresentation():getCategory() if category == Object.Category.UNIT then return self.typeName end end return "UNKNOWN" end function SkynetIADSContact:getPosition() return self.position end function SkynetIADSContact:getGroundSpeedInKnots(decimals) if decimals == nil then decimals = 2 end return mist.utils.round(self.speed, decimals) end function SkynetIADSContact:getHeightInFeetMSL() if self:isExist() then return mist.utils.round(mist.utils.metersToFeet(self:getDCSRepresentation():getPosition().p.y), 0) else return 0 end end function SkynetIADSContact:getDesc() if self:isExist() then return self:getDCSRepresentation():getDesc() else return {} end end function SkynetIADSContact:getNumberOfTimesHitByRadar() return self.numOfTimesRefreshed end function SkynetIADSContact:refresh() if self:isExist() then local timeDelta = (timer.getAbsTime() - self.lastTimeSeen) if timeDelta > 0 then self.numOfTimesRefreshed = self.numOfTimesRefreshed + 1 local distance = mist.utils.metersToNM(mist.utils.get2DDist(self.position.p, self:getDCSRepresentation():getPosition().p)) local hours = timeDelta / 3600 self.speed = (distance / hours) self:updateSimpleAltitudeProfile() self.position = self:getDCSRepresentation():getPosition() end end self.lastTimeSeen = timer.getAbsTime() end function SkynetIADSContact:updateSimpleAltitudeProfile() local currentAltitude = self:getDCSRepresentation():getPosition().p.y local previousPath = "" if #self.simpleAltitudeProfile > 0 then previousPath = self.simpleAltitudeProfile[#self.simpleAltitudeProfile] end if self.position.p.y > currentAltitude and previousPath ~= SkynetIADSContact.DESCEND then table.insert(self.simpleAltitudeProfile, SkynetIADSContact.DESCEND) elseif self.position.p.y < currentAltitude and previousPath ~= SkynetIADSContact.CLIMB then table.insert(self.simpleAltitudeProfile, SkynetIADSContact.CLIMB) end end function SkynetIADSContact:getSimpleAltitudeProfile() return self.simpleAltitudeProfile end function SkynetIADSContact:getAge() return mist.utils.round(timer.getAbsTime() - self.lastTimeSeen) end end ================================================ FILE: skynet-iads-source/skynet-iads-early-warning-radar.lua ================================================ do SkynetIADSEWRadar = {} SkynetIADSEWRadar = inheritsFrom(SkynetIADSAbstractRadarElement) function SkynetIADSEWRadar:create(radarUnit, iads) local instance = self:superClass():create(radarUnit, iads) setmetatable(instance, self) self.__index = self instance.autonomousBehaviour = SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK return instance end function SkynetIADSEWRadar:setupElements() local unit = self:getDCSRepresentation() local unitType = unit:getTypeName() for typeName, dataType in pairs(SkynetIADS.database) do for entry, unitData in pairs(dataType) do if entry == 'searchRadar' then --buildSingleUnit checks to make sure the EW radar is defined in the Skynet database. If it is not, self.searchRadars will be 0 so no ew radar will be added self:buildSingleUnit(unit, SkynetIADSSAMSearchRadar, self.searchRadars, unitData) if #self.searchRadars > 0 then local harmDetection = dataType['harm_detection_chance'] self:setHARMDetectionChance(harmDetection) if unitData[unitType]['name'] then local natoName = unitData[unitType]['name']['NATO'] self:buildNatoName(natoName) end return end end end end end --an Early Warning Radar has simplified check to determine if its autonomous or not function SkynetIADSEWRadar:setToCorrectAutonomousState() if self:hasActiveConnectionNode() and self:hasWorkingPowerSource() and self.iads:isCommandCenterUsable() then self:resetAutonomousState() self:goLive() end if self:hasActiveConnectionNode() == false or self.iads:isCommandCenterUsable() == false then self:goAutonomous() end end end ================================================ FILE: skynet-iads-source/skynet-iads-harm-detection.lua ================================================ do SkynetIADSHARMDetection = {} SkynetIADSHARMDetection.__index = SkynetIADSHARMDetection SkynetIADSHARMDetection.HARM_THRESHOLD_SPEED_KTS = 800 function SkynetIADSHARMDetection:create(iads) local harmDetection = {} setmetatable(harmDetection, self) harmDetection.contacts = {} harmDetection.iads = iads harmDetection.contactRadarsEvaluated = {} return harmDetection end function SkynetIADSHARMDetection:setContacts(contacts) self.contacts = contacts end function SkynetIADSHARMDetection:evaluateContacts() self:cleanAgedContacts() for i = 1, #self.contacts do local contact = self.contacts[i] local groundSpeed = contact:getGroundSpeedInKnots(0) --if a contact has only been hit by a radar once it's speed is 0 if groundSpeed == 0 then return end local simpleAltitudeProfile = contact:getSimpleAltitudeProfile() local newRadarsToEvaluate = self:getNewRadarsThatHaveDetectedContact(contact) --self.iads:printOutputToLog(contact:getName().." new Radars to evaluate: "..#newRadarsToEvaluate) --self.iads:printOutputToLog(contact:getName().." ground speed: "..groundSpeed) if ( #newRadarsToEvaluate > 0 and contact:isIdentifiedAsHARM() == false and ( groundSpeed > SkynetIADSHARMDetection.HARM_THRESHOLD_SPEED_KTS and #simpleAltitudeProfile <= 2 ) ) then local detectionProbability = self:getDetectionProbability(newRadarsToEvaluate) --self.iads:printOutputToLog("DETECTION PROB: "..detectionProbability) if ( self:shallReactToHARM(detectionProbability) ) then contact:setHARMState(SkynetIADSContact.HARM) if (self.iads:getDebugSettings().harmDefence ) then self.iads:printOutputToLog("HARM IDENTIFIED: "..contact:getTypeName().." | DETECTION PROBABILITY WAS: "..detectionProbability.."%") end else contact:setHARMState(SkynetIADSContact.NOT_HARM) if (self.iads:getDebugSettings().harmDefence ) then self.iads:printOutputToLog("HARM NOT IDENTIFIED: "..contact:getTypeName().." | DETECTION PROBABILITY WAS: "..detectionProbability.."%") end end end if ( #simpleAltitudeProfile > 2 and contact:isIdentifiedAsHARM() ) then contact:setHARMState(SkynetIADSContact.HARM_UNKNOWN) if (self.iads:getDebugSettings().harmDefence ) then self.iads:printOutputToLog("CORRECTING HARM STATE: CONTACT IS NOT A HARM: "..contact:getName()) end end if ( contact:isIdentifiedAsHARM() ) then self:informRadarsOfHARM(contact) end end end function SkynetIADSHARMDetection:cleanAgedContacts() local activeContactRadars = {} for contact, radars in pairs (self.contactRadarsEvaluated) do if contact:getAge() < 32 then activeContactRadars[contact] = radars end end self.contactRadarsEvaluated = activeContactRadars end function SkynetIADSHARMDetection:getNewRadarsThatHaveDetectedContact(contact) local radarsFromContact = contact:getAbstractRadarElementsDetected() local evaluatedRadars = self.contactRadarsEvaluated[contact] local newRadars = {} if evaluatedRadars == nil then evaluatedRadars = {} self.contactRadarsEvaluated[contact] = evaluatedRadars end for i = 1, #radarsFromContact do local contactRadar = radarsFromContact[i] if self:isElementInTable(evaluatedRadars, contactRadar) == false then table.insert(evaluatedRadars, contactRadar) table.insert(newRadars, contactRadar) end end return newRadars end function SkynetIADSHARMDetection:isElementInTable(tbl, element) for i = 1, #tbl do local tblElement = tbl[i] if tblElement == element then return true end end return false end function SkynetIADSHARMDetection:informRadarsOfHARM(contact) local samSites = self.iads:getUsableSAMSites() self:updateRadarsOfSites(samSites, contact) local ewRadars = self.iads:getUsableEarlyWarningRadars() self:updateRadarsOfSites(ewRadars, contact) end function SkynetIADSHARMDetection:updateRadarsOfSites(sites, contact) for i = 1, #sites do local site = sites[i] site:informOfHARM(contact) end end function SkynetIADSHARMDetection:shallReactToHARM(chance) return chance >= math.random(1, 100) end function SkynetIADSHARMDetection:getDetectionProbability(radars) local detectionChance = 0 local missChance = 100 local detection = 0 for i = 1, #radars do detection = radars[i]:getHARMDetectionChance() if ( detectionChance == 0 ) then detectionChance = detection else detectionChance = detectionChance + (detection * (missChance / 100)) end missChance = 100 - detection end return detectionChance end end ================================================ FILE: skynet-iads-source/skynet-iads-jammer.lua ================================================ do SkynetIADSJammer = {} SkynetIADSJammer.__index = SkynetIADSJammer function SkynetIADSJammer:create(emitter, iads) local jammer = {} setmetatable(jammer, SkynetIADSJammer) jammer.radioMenu = nil jammer.emitter = emitter jammer.jammerTaskID = nil jammer.iads = {iads} jammer.maximumEffectiveDistanceNM = 200 --jammer probability settings are stored here, visualisation, see: https://docs.google.com/spreadsheets/d/16rnaU49ZpOczPEsdGJ6nfD0SLPxYLEYKmmo4i2Vfoe0/edit#gid=0 jammer.jammerTable = { ['SA-2'] = { ['function'] = function(distanceNauticalMiles) return ( 1.4 ^ distanceNauticalMiles ) + 90 end, ['canjam'] = true, }, ['SA-3'] = { ['function'] = function(distanceNauticalMiles) return ( 1.4 ^ distanceNauticalMiles ) + 80 end, ['canjam'] = true, }, ['SA-6'] = { ['function'] = function(distanceNauticalMiles) return ( 1.4 ^ distanceNauticalMiles ) + 23 end, ['canjam'] = true, }, ['SA-8'] = { ['function'] = function(distanceNauticalMiles) return ( 1.35 ^ distanceNauticalMiles ) + 30 end, ['canjam'] = true, }, ['SA-10'] = { ['function'] = function(distanceNauticalMiles) return ( 1.07 ^ (distanceNauticalMiles / 1.13) ) + 5 end, ['canjam'] = true, }, ['SA-11'] = { ['function'] = function(distanceNauticalMiles) return ( 1.25 ^ distanceNauticalMiles ) + 15 end, ['canjam'] = true, }, ['SA-15'] = { ['function'] = function(distanceNauticalMiles) return ( 1.15 ^ distanceNauticalMiles ) + 5 end, ['canjam'] = true, }, } return jammer end function SkynetIADSJammer:masterArmOn() self:masterArmSafe() self.jammerTaskID = mist.scheduleFunction(SkynetIADSJammer.runCycle, {self}, 1, 10) end function SkynetIADSJammer:addFunction(natoName, jammerFunction) self.jammerTable[natoName] = { ['function'] = jammerFunction, ['canjam'] = true } end function SkynetIADSJammer:setMaximumEffectiveDistance(distance) self.maximumEffectiveDistanceNM = distance end function SkynetIADSJammer:disableFor(natoName) self.jammerTable[natoName]['canjam'] = false end function SkynetIADSJammer:isKnownRadarEmitter(natoName) local isActive = false for unitName, unit in pairs(self.jammerTable) do if unitName == natoName and unit['canjam'] == true then isActive = true end end return isActive end function SkynetIADSJammer:addIADS(iads) table.insert(self.iads, iads) end function SkynetIADSJammer:getSuccessProbability(distanceNauticalMiles, natoName) local probability = 0 local jammerSettings = self.jammerTable[natoName] if jammerSettings ~= nil then probability = jammerSettings['function'](distanceNauticalMiles) end return probability end function SkynetIADSJammer:getDistanceNMToRadarUnit(radarUnit) return mist.utils.metersToNM(mist.utils.get3DDist(self.emitter:getPosition().p, radarUnit:getPosition().p)) end function SkynetIADSJammer.runCycle(self) if self.emitter:isExist() == false then self:masterArmSafe() return end for i = 1, #self.iads do local iads = self.iads[i] local samSites = iads:getActiveSAMSites() for j = 1, #samSites do local samSite = samSites[j] local radars = samSite:getRadars() local hasLOS = false local distance = 0 local natoName = samSite:getNatoName() for l = 1, #radars do local radar = radars[l] distance = self:getDistanceNMToRadarUnit(radar) -- I try to emulate the system as it would work in real life, so a jammer can only jam a SAM site if has line of sight to at least one radar in the group if self:isKnownRadarEmitter(natoName) and self:hasLineOfSightToRadar(radar) and distance <= self.maximumEffectiveDistanceNM then if iads:getDebugSettings().jammerProbability then iads:printOutput("JAMMER: Distance: "..distance) end samSite:jam(self:getSuccessProbability(distance, natoName)) end end end end end function SkynetIADSJammer:hasLineOfSightToRadar(radar) local radarPos = radar:getPosition().p --lift the radar 30 meters off the ground, some 3d models are dug in to the ground, creating issues in calculating LOS radarPos.y = radarPos.y + 30 return land.isVisible(radarPos, self.emitter:getPosition().p) end function SkynetIADSJammer:masterArmSafe() mist.removeFunction(self.jammerTaskID) end --TODO: Remove Menu when emitter dies: function SkynetIADSJammer:addRadioMenu() self.radioMenu = missionCommands.addSubMenu('Jammer: '..self.emitter:getName()) missionCommands.addCommand('Master Arm On', self.radioMenu, SkynetIADSJammer.updateMasterArm, {self = self, option = 'masterArmOn'}) missionCommands.addCommand('Master Arm Safe', self.radioMenu, SkynetIADSJammer.updateMasterArm, {self = self, option = 'masterArmSafe'}) end function SkynetIADSJammer.updateMasterArm(params) local option = params.option local self = params.self if option == 'masterArmOn' then self:masterArmOn() elseif option == 'masterArmSafe' then self:masterArmSafe() end end function SkynetIADSJammer:removeRadioMenu() missionCommands.removeItem(self.radioMenu) end end ================================================ FILE: skynet-iads-source/skynet-iads-logger.lua ================================================ do SkynetIADSLogger = {} SkynetIADSLogger.__index = SkynetIADSLogger function SkynetIADSLogger:create(iads) local logger = {} setmetatable(logger, SkynetIADSLogger) logger.debugOutput = {} logger.debugOutput.IADSStatus = false logger.debugOutput.samWentDark = false logger.debugOutput.contacts = false logger.debugOutput.radarWentLive = false logger.debugOutput.jammerProbability = false logger.debugOutput.addedEWRadar = false logger.debugOutput.addedSAMSite = false logger.debugOutput.warnings = true logger.debugOutput.harmDefence = false logger.debugOutput.samSiteStatusEnvOutput = false logger.debugOutput.earlyWarningRadarStatusEnvOutput = false logger.debugOutput.commandCenterStatusEnvOutput = false logger.iads = iads return logger end function SkynetIADSLogger:getDebugSettings() return self.debugOutput end function SkynetIADSLogger:printOutput(output, typeWarning) if typeWarning == true and self:getDebugSettings().warnings or typeWarning == nil then if typeWarning == true then output = "WARNING: "..output end trigger.action.outText(output, 4) end end function SkynetIADSLogger:printOutputToLog(output) env.info("SKYNET: "..output, 4) end function SkynetIADSLogger:printEarlyWarningRadarStatus() local ewRadars = self.iads:getEarlyWarningRadars() self:printOutputToLog("------------------------------------------ EW RADAR STATUS: "..self.iads:getCoalitionString().." -------------------------------") for i = 1, #ewRadars do local ewRadar = ewRadars[i] local numConnectionNodes = #ewRadar:getConnectionNodes() local numPowerSources = #ewRadar:getPowerSources() local isActive = ewRadar:isActive() local connectionNodes = ewRadar:getConnectionNodes() local firstRadar = nil local radars = ewRadar:getRadars() --get the first existing radar to prevent issues in calculating the distance later on: for i = 1, #radars do if radars[i]:isExist() then firstRadar = radars[i] break end end local numDamagedConnectionNodes = 0 for j = 1, #connectionNodes do local connectionNode = connectionNodes[j] if connectionNode:isExist() == false then numDamagedConnectionNodes = numDamagedConnectionNodes + 1 end end local intactConnectionNodes = numConnectionNodes - numDamagedConnectionNodes local powerSources = ewRadar:getPowerSources() local numDamagedPowerSources = 0 for j = 1, #powerSources do local powerSource = powerSources[j] if powerSource:isExist() == false then numDamagedPowerSources = numDamagedPowerSources + 1 end end local intactPowerSources = numPowerSources - numDamagedPowerSources local detectedTargets = ewRadar:getDetectedTargets() local samSitesInCoveredArea = ewRadar:getChildRadars() local unitName = "DESTROYED" if ewRadar:getDCSRepresentation():isExist() then unitName = ewRadar:getDCSName() end self:printOutputToLog("UNIT: "..unitName.." | TYPE: "..ewRadar:getNatoName()) self:printOutputToLog("ACTIVE: "..tostring(isActive).."| DETECTED TARGETS: "..#detectedTargets.." | DEFENDING HARM: "..tostring(ewRadar:isDefendingHARM())) if numConnectionNodes > 0 then self:printOutputToLog("CONNECTION NODES: "..numConnectionNodes.." | DAMAGED: "..numDamagedConnectionNodes.." | INTACT: "..intactConnectionNodes) else self:printOutputToLog("NO CONNECTION NODES SET") end if numPowerSources > 0 then self:printOutputToLog("POWER SOURCES : "..numPowerSources.." | DAMAGED:"..numDamagedPowerSources.." | INTACT: "..intactPowerSources) else self:printOutputToLog("NO POWER SOURCES SET") end self:printOutputToLog("SAM SITES IN COVERED AREA: "..#samSitesInCoveredArea) for j = 1, #samSitesInCoveredArea do local samSiteCovered = samSitesInCoveredArea[j] self:printOutputToLog(samSiteCovered:getDCSName()) end for j = 1, #detectedTargets do local contact = detectedTargets[j] if firstRadar ~= nil and firstRadar:isExist() then local distance = mist.utils.round(mist.utils.metersToNM(ewRadar:getDistanceInMetersToContact(firstRadar:getDCSRepresentation(), contact:getPosition().p)), 2) self:printOutputToLog("CONTACT: "..contact:getName().." | TYPE: "..contact:getTypeName().." | DISTANCE NM: "..distance) end end self:printOutputToLog("---------------------------------------------------") end end function SkynetIADSLogger:getMetaInfo(abstractElementSupport) local info = {} info.numSources = #abstractElementSupport info.numDamagedSources = 0 info.numIntactSources = 0 for j = 1, #abstractElementSupport do local source = abstractElementSupport[j] if source:isExist() == false then info.numDamagedSources = info.numDamagedSources + 1 end end info.numIntactSources = info.numSources - info.numDamagedSources return info end function SkynetIADSLogger:printSAMSiteStatus() local samSites = self.iads:getSAMSites() self:printOutputToLog("------------------------------------------ SAM STATUS: "..self.iads:getCoalitionString().." -------------------------------") for i = 1, #samSites do local samSite = samSites[i] local numConnectionNodes = #samSite:getConnectionNodes() local numPowerSources = #samSite:getPowerSources() local isAutonomous = samSite:getAutonomousState() local isActive = samSite:isActive() local connectionNodes = samSite:getConnectionNodes() local firstRadar = samSite:getRadars()[1] local numDamagedConnectionNodes = 0 for j = 1, #connectionNodes do local connectionNode = connectionNodes[j] if connectionNode:isExist() == false then numDamagedConnectionNodes = numDamagedConnectionNodes + 1 end end local intactConnectionNodes = numConnectionNodes - numDamagedConnectionNodes local powerSources = samSite:getPowerSources() local numDamagedPowerSources = 0 for j = 1, #powerSources do local powerSource = powerSources[j] if powerSource:isExist() == false then numDamagedPowerSources = numDamagedPowerSources + 1 end end local intactPowerSources = numPowerSources - numDamagedPowerSources local detectedTargets = samSite:getDetectedTargets() local samSitesInCoveredArea = samSite:getChildRadars() local engageAirWeapons = samSite:getCanEngageAirWeapons() local engageHARMS = samSite:getCanEngageHARM() local hasAmmo = samSite:hasRemainingAmmo() self:printOutputToLog("GROUP: "..samSite:getDCSName().." | TYPE: "..samSite:getNatoName()) self:printOutputToLog("ACTIVE: "..tostring(isActive).." | AUTONOMOUS: "..tostring(isAutonomous).." | IS ACTING AS EW: "..tostring(samSite:getActAsEW()).." | CAN ENGAGE AIR WEAPONS : "..tostring(engageAirWeapons).." | CAN ENGAGE HARMS : "..tostring(engageHARMS).." | HAS AMMO: "..tostring(hasAmmo).." | DETECTED TARGETS: "..#detectedTargets.." | DEFENDING HARM: "..tostring(samSite:isDefendingHARM()).." | MISSILES IN FLIGHT: "..tostring(samSite:getNumberOfMissilesInFlight())) if numConnectionNodes > 0 then self:printOutputToLog("CONNECTION NODES: "..numConnectionNodes.." | DAMAGED: "..numDamagedConnectionNodes.." | INTACT: "..intactConnectionNodes) else self:printOutputToLog("NO CONNECTION NODES SET") end if numPowerSources > 0 then self:printOutputToLog("POWER SOURCES : "..numPowerSources.." | DAMAGED:"..numDamagedPowerSources.." | INTACT: "..intactPowerSources) else self:printOutputToLog("NO POWER SOURCES SET") end self:printOutputToLog("SAM SITES IN COVERED AREA: "..#samSitesInCoveredArea) for j = 1, #samSitesInCoveredArea do local samSiteCovered = samSitesInCoveredArea[j] self:printOutputToLog(samSiteCovered:getDCSName()) end for j = 1, #detectedTargets do local contact = detectedTargets[j] if firstRadar ~= nil and firstRadar:isExist() then local distance = mist.utils.round(mist.utils.metersToNM(samSite:getDistanceInMetersToContact(firstRadar:getDCSRepresentation(), contact:getPosition().p)), 2) self:printOutputToLog("CONTACT: "..contact:getName().." | TYPE: "..contact:getTypeName().." | DISTANCE NM: "..distance) end end self:printOutputToLog("---------------------------------------------------") end end function SkynetIADSLogger:printCommandCenterStatus() local commandCenters = self.iads:getCommandCenters() self:printOutputToLog("------------------------------------------ COMMAND CENTER STATUS: "..self.iads:getCoalitionString().." -------------------------------") for i = 1, #commandCenters do local commandCenter = commandCenters[i] local numConnectionNodes = #commandCenter:getConnectionNodes() local powerSourceInfo = self:getMetaInfo(commandCenter:getPowerSources()) local connectionNodeInfo = self:getMetaInfo(commandCenter:getConnectionNodes()) self:printOutputToLog("GROUP: "..commandCenter:getDCSName().." | TYPE: "..commandCenter:getNatoName()) if connectionNodeInfo.numSources > 0 then self:printOutputToLog("CONNECTION NODES: "..connectionNodeInfo.numSources.." | DAMAGED: "..connectionNodeInfo.numDamagedSources.." | INTACT: "..connectionNodeInfo.numIntactSources) else self:printOutputToLog("NO CONNECTION NODES SET") end if powerSourceInfo.numSources > 0 then self:printOutputToLog("POWER SOURCES : "..powerSourceInfo.numSources.." | DAMAGED: "..powerSourceInfo.numDamagedSources.." | INTACT: "..powerSourceInfo.numIntactSources) else self:printOutputToLog("NO POWER SOURCES SET") end self:printOutputToLog("---------------------------------------------------") end end function SkynetIADSLogger:printSystemStatus() if self:getDebugSettings().IADSStatus or self:getDebugSettings().contacts then local coalitionStr = self.iads:getCoalitionString() self:printOutput("---- IADS: "..coalitionStr.." ------") end if self:getDebugSettings().IADSStatus then local commandCenters = self.iads:getCommandCenters() local numComCenters = #commandCenters local numDestroyedComCenters = 0 local numComCentersNoPower = 0 local numComCentersNoConnectionNode = 0 local numIntactComCenters = 0 for i = 1, #commandCenters do local commandCenter = commandCenters[i] if commandCenter:hasWorkingPowerSource() == false then numComCentersNoPower = numComCentersNoPower + 1 end if commandCenter:hasActiveConnectionNode() == false then numComCentersNoConnectionNode = numComCentersNoConnectionNode + 1 end if commandCenter:isDestroyed() == false then numIntactComCenters = numIntactComCenters + 1 end end numDestroyedComCenters = numComCenters - numIntactComCenters self:printOutput("COMMAND CENTERS: "..numComCenters.." | Destroyed: "..numDestroyedComCenters.." | NoPowr: "..numComCentersNoPower.." | NoCon: "..numComCentersNoConnectionNode) local ewNoPower = 0 local earlyWarningRadars = self.iads:getEarlyWarningRadars() local ewTotal = #earlyWarningRadars local ewNoConnectionNode = 0 local ewActive = 0 local ewRadarsInactive = 0 for i = 1, #earlyWarningRadars do local ewRadar = earlyWarningRadars[i] if ewRadar:hasWorkingPowerSource() == false then ewNoPower = ewNoPower + 1 end if ewRadar:hasActiveConnectionNode() == false then ewNoConnectionNode = ewNoConnectionNode + 1 end if ewRadar:isActive() then ewActive = ewActive + 1 end end ewRadarsInactive = ewTotal - ewActive local numEWRadarsDestroyed = #self.iads:getDestroyedEarlyWarningRadars() self:printOutput("EW: "..ewTotal.." | On: "..ewActive.." | Off: "..ewRadarsInactive.." | Destroyed: "..numEWRadarsDestroyed.." | NoPowr: "..ewNoPower.." | NoCon: "..ewNoConnectionNode) local samSitesInactive = 0 local samSitesActive = 0 local samSites = self.iads:getSAMSites() local samSitesTotal = #samSites local samSitesNoPower = 0 local samSitesNoConnectionNode = 0 local samSitesOutOfAmmo = 0 local samSiteAutonomous = 0 local samSiteRadarDestroyed = 0 for i = 1, #samSites do local samSite = samSites[i] if samSite:hasWorkingPowerSource() == false then samSitesNoPower = samSitesNoPower + 1 end if samSite:hasActiveConnectionNode() == false then samSitesNoConnectionNode = samSitesNoConnectionNode + 1 end if samSite:isActive() then samSitesActive = samSitesActive + 1 end if samSite:hasRemainingAmmo() == false then samSitesOutOfAmmo = samSitesOutOfAmmo + 1 end if samSite:getAutonomousState() == true then samSiteAutonomous = samSiteAutonomous + 1 end if samSite:hasWorkingRadar() == false then samSiteRadarDestroyed = samSiteRadarDestroyed + 1 end end samSitesInactive = samSitesTotal - samSitesActive self:printOutput("SAM: "..samSitesTotal.." | On: "..samSitesActive.." | Off: "..samSitesInactive.." | Autonm: "..samSiteAutonomous.." | Raddest: "..samSiteRadarDestroyed.." | NoPowr: "..samSitesNoPower.." | NoCon: "..samSitesNoConnectionNode.." | NoAmmo: "..samSitesOutOfAmmo) end if self:getDebugSettings().contacts then local contacts = self.iads:getContacts() if contacts then for i = 1, #contacts do local contact = contacts[i] self:printOutput("CONTACT: "..contact:getName().." | TYPE: "..contact:getTypeName().." | GS: "..tostring(contact:getGroundSpeedInKnots()).." | LAST SEEN: "..contact:getAge()) end end end if self:getDebugSettings().commandCenterStatusEnvOutput then self:printCommandCenterStatus() end if self:getDebugSettings().earlyWarningRadarStatusEnvOutput then self:printEarlyWarningRadarStatus() end if self:getDebugSettings().samSiteStatusEnvOutput then self:printSAMSiteStatus() end end end ================================================ FILE: skynet-iads-source/skynet-iads-sam-search-radar.lua ================================================ do SkynetIADSSAMSearchRadar = {} SkynetIADSSAMSearchRadar = inheritsFrom(SkynetIADSAbstractDCSObjectWrapper) function SkynetIADSSAMSearchRadar:create(unit) local instance = self:superClass():create(unit) setmetatable(instance, self) self.__index = self instance.firingRangePercent = 100 instance.maximumRange = 0 instance.initialNumberOfMissiles = 0 instance.remainingNumberOfMissiles = 0 instance.initialNumberOfShells = 0 instance.remainingNumberOfShells = 0 instance.triedSensors = 0 return instance end --override in subclasses to match different datastructure of getSensors() function SkynetIADSSAMSearchRadar:setupRangeData() if self:isExist() then local data = self:getDCSRepresentation():getSensors() if data == nil then --this is to prevent infinite calls between launcher and search radar self.triedSensors = self.triedSensors + 1 --the SA-13 does not have any sensor data, but is has launcher data, so we use the stuff from the launcher for the radar range. SkynetIADSSAMLauncher.setupRangeData(self) return end for i = 1, #data do local subEntries = data[i] for j = 1, #subEntries do local sensorInformation = subEntries[j] -- some sam sites have IR and passive EWR detection, we are just interested in the radar data -- investigate if upperHemisphere and headOn is ok, I guess it will work for most detection cases if sensorInformation.type == Unit.SensorType.RADAR and sensorInformation['detectionDistanceAir'] then local upperHemisphere = sensorInformation['detectionDistanceAir']['upperHemisphere']['headOn'] local lowerHemisphere = sensorInformation['detectionDistanceAir']['lowerHemisphere']['headOn'] self.maximumRange = upperHemisphere if lowerHemisphere > upperHemisphere then self.maximumRange = lowerHemisphere end end end end end end function SkynetIADSSAMSearchRadar:getMaxRangeFindingTarget() return self.maximumRange end function SkynetIADSSAMSearchRadar:isRadarWorking() -- the ammo check is for the SA-13 which does not return any sensor data: return (self:isExist() == true and ( self:getDCSRepresentation():getSensors() ~= nil or self:getDCSRepresentation():getAmmo() ~= nil ) ) end function SkynetIADSSAMSearchRadar:setFiringRangePercent(percent) self.firingRangePercent = percent end function SkynetIADSSAMSearchRadar:getDistance(target) return mist.utils.get2DDist(target:getPosition().p, self:getDCSRepresentation():getPosition().p) end function SkynetIADSSAMSearchRadar:getHeight(target) local radarElevation = self:getDCSRepresentation():getPosition().p.y local targetElevation = target:getPosition().p.y return math.abs(targetElevation - radarElevation) end function SkynetIADSSAMSearchRadar:isInHorizontalRange(target) return (self:getMaxRangeFindingTarget() / 100 * self.firingRangePercent) >= self:getDistance(target) end function SkynetIADSSAMSearchRadar:isInRange(target) if self:isExist() == false then return false end return self:isInHorizontalRange(target) end end ================================================ FILE: skynet-iads-source/skynet-iads-sam-site.lua ================================================ do SkynetIADSSamSite = {} SkynetIADSSamSite = inheritsFrom(SkynetIADSAbstractRadarElement) function SkynetIADSSamSite:create(samGroup, iads) local sam = self:superClass():create(samGroup, iads) setmetatable(sam, self) self.__index = self sam.targetsInRange = false sam.goLiveConstraints = {} return sam end function SkynetIADSSamSite:addGoLiveConstraint(constraintName, constraint) self.goLiveConstraints[constraintName] = constraint end function SkynetIADSAbstractRadarElement:areGoLiveConstraintsSatisfied(contact) for constraintName, constraint in pairs(self.goLiveConstraints) do if ( constraint(contact) ~= true ) then return false end end return true end function SkynetIADSAbstractRadarElement:removeGoLiveConstraint(constraintName) local constraints = {} for cName, constraint in pairs(self.goLiveConstraints) do if cName ~= constraintName then constraints[cName] = constraint end end self.goLiveConstraints = constraints end function SkynetIADSAbstractRadarElement:getGoLiveConstraints() return self.goLiveConstraints end function SkynetIADSSamSite:isDestroyed() local isDestroyed = true for i = 1, #self.launchers do local launcher = self.launchers[i] if launcher:isExist() == true then isDestroyed = false end end local radars = self:getRadars() for i = 1, #radars do local radar = radars[i] if radar:isExist() == true then isDestroyed = false end end return isDestroyed end function SkynetIADSSamSite:targetCycleUpdateStart() self.targetsInRange = false end function SkynetIADSSamSite:targetCycleUpdateEnd() if self.targetsInRange == false and self.actAsEW == false and self:getAutonomousState() == false and self:getAutonomousBehaviour() == SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DCS_AI then self:goDark() end end function SkynetIADSSamSite:informOfContact(contact) -- we make sure isTargetInRange (expensive call) is only triggered if no previous calls to this method resulted in targets in range if ( self.targetsInRange == false and self:areGoLiveConstraintsSatisfied(contact) == true and self:isTargetInRange(contact) and ( contact:isIdentifiedAsHARM() == false or ( contact:isIdentifiedAsHARM() == true and self:getCanEngageHARM() == true ) ) ) then self:goLive() self.targetsInRange = true end end end ================================================ FILE: skynet-iads-source/skynet-iads-sam-tracking-radar.lua ================================================ do SkynetIADSSAMTrackingRadar = {} SkynetIADSSAMTrackingRadar = inheritsFrom(SkynetIADSSAMSearchRadar) function SkynetIADSSAMTrackingRadar:create(unit) local instance = self:superClass():create(unit) setmetatable(instance, self) self.__index = self return instance end end ================================================ FILE: skynet-iads-source/skynet-iads-supported-types.lua ================================================ do --this file contains the required units per sam type samTypesDB = { ['S-200'] = { ['type'] = 'complex', ['searchRadar'] = { ['RLS_19J6'] = { ['name'] = { ['NATO'] = 'Tin Shield', }, }, ['p-19 s-125 sr'] = { ['name'] = { ['NATO'] = 'Flat Face', }, }, }, ['EWR P-37 BAR LOCK'] = { ['Name'] = { ['NATO'] = "Bar lock", }, }, ['trackingRadar'] = { ['RPC_5N62V'] = { }, }, ['launchers'] = { ['S-200_Launcher'] = { }, }, ['name'] = { ['NATO'] = 'SA-5 Gammon', }, ['harm_detection_chance'] = 60 }, ['S-300'] = { ['type'] = 'complex', ['searchRadar'] = { ['S-300PS 40B6MD sr'] = { ['name'] = { ['NATO'] = 'Clam Shell', }, }, ['S-300PS 64H6E sr'] = { ['name'] = { ['NATO'] = 'Big Bird', }, }, ['S-300PS 40B6MD sr_19J6'] = { ['name'] = { ['NATO'] = 'Tin Shield', }, } }, ['trackingRadar'] = { ['S-300PS 40B6M tr'] = { }, ['S-300PS 5H63C 30H6_tr'] = { }, }, ['launchers'] = { ['S-300PS 5P85D ln'] = { }, ['S-300PS 5P85C ln'] = { }, }, ['misc'] = { ['S-300PS 54K6 cp'] = { ['required'] = true, }, }, ['name'] = { ['NATO'] = 'SA-10 Grumble', }, ['harm_detection_chance'] = 90, ['can_engage_harm'] = true }, ['Buk'] = { ['type'] = 'complex', ['searchRadar'] = { ['SA-11 Buk SR 9S18M1'] = { ['name'] = { ['NATO'] = 'Snow Drift', }, }, }, ['launchers'] = { ['SA-11 Buk LN 9A310M1'] = { }, }, ['misc'] = { ['SA-11 Buk CC 9S470M1'] = { ['required'] = true, }, }, ['name'] = { ['NATO'] = 'SA-11 Gadfly', }, ['harm_detection_chance'] = 70 }, ['S-125'] = { ['type'] = 'complex', ['searchRadar'] = { ['p-19 s-125 sr'] = { ['name'] = { ['NATO'] = 'Flat Face', }, }, }, ['trackingRadar'] = { ['snr s-125 tr'] = { }, }, ['launchers'] = { ['5p73 s-125 ln'] = { }, }, ['name'] = { ['NATO'] = 'SA-3 Goa', }, ['harm_detection_chance'] = 30 }, ['S-75'] = { ['type'] = 'complex', ['searchRadar'] = { ['p-19 s-125 sr'] = { ['name'] = { ['NATO'] = 'Flat Face', }, }, }, ['trackingRadar'] = { ['SNR_75V'] = { }, }, ['launchers'] = { ['S_75M_Volhov'] = { }, }, ['name'] = { ['NATO'] = 'SA-2 Guideline', }, ['harm_detection_chance'] = 30 }, ['Kub'] = { ['type'] = 'complex', ['searchRadar'] = { ['Kub 1S91 str'] = { ['name'] = { ['NATO'] = 'Straight Flush', }, }, }, ['launchers'] = { ['Kub 2P25 ln'] = { }, }, ['name'] = { ['NATO'] = 'SA-6 Gainful', }, ['harm_detection_chance'] = 40 }, ['Patriot'] = { ['type'] = 'complex', ['searchRadar'] = { ['Patriot str'] = { ['name'] = { ['NATO'] = 'Patriot str', }, }, }, ['launchers'] = { ['Patriot ln'] = { }, }, ['misc'] = { ['Patriot cp'] = { ['required'] = false, }, ['Patriot EPP'] = { ['required'] = false, }, ['Patriot ECS'] = { ['required'] = true, }, ['Patriot AMG'] = { ['required'] = false, }, }, ['name'] = { ['NATO'] = 'Patriot', }, ['harm_detection_chance'] = 90, ['can_engage_harm'] = true }, ['Hawk'] = { ['type'] = 'complex', ['searchRadar'] = { ['Hawk sr'] = { ['name'] = { ['NATO'] = 'Hawk str', }, }, }, ['trackingRadar'] = { ['Hawk tr'] = { }, }, ['launchers'] = { ['Hawk ln'] = { }, }, ['name'] = { ['NATO'] = 'Hawk', }, ['harm_detection_chance'] = 40 }, ['Roland ADS'] = { ['type'] = 'complex', ['searchRadar'] = { ['Roland Radar'] = { ['name'] = { ['NATO'] = 'Roland EWR', }, }, }, ['launchers'] = { ['Roland ADS'] = { }, }, ['name'] = { ['NATO'] = 'Roland ADS', }, ['harm_detection_chance'] = 60 }, ['NASAMS'] = { ['type'] = 'complex', ['searchRadar'] = { ['NASAMS_Radar_MPQ64F1'] = { }, }, ['launchers'] = { ['NASAMS_LN_B'] = { }, ['NASAMS_LN_C'] = { }, }, ['name'] = { ['NATO'] = 'NASAMS', }, ['misc'] = { ['NASAMS_Command_Post'] = { ['required'] = false, }, }, ['can_engage_harm'] = true, ['harm_detection_chance'] = 90 }, ['2S6 Tunguska'] = { ['type'] = 'single', ['searchRadar'] = { ['2S6 Tunguska'] = { }, }, ['launchers'] = { ['2S6 Tunguska'] = { }, }, ['name'] = { ['NATO'] = 'SA-19 Grison', }, }, ['Osa'] = { ['type'] = 'single', ['searchRadar'] = { ['Osa 9A33 ln'] = { }, }, ['launchers'] = { ['Osa 9A33 ln'] = { }, }, ['name'] = { ['NATO'] = 'SA-8 Gecko', }, ['harm_detection_chance'] = 20 }, ['Strela-10M3'] = { ['type'] = 'single', ['searchRadar'] = { ['Strela-10M3'] = { ['trackingRadar'] = true, }, }, ['launchers'] = { ['Strela-10M3'] = { }, }, ['name'] = { ['NATO'] = 'SA-13 Gopher', }, }, ['Strela-1 9P31'] = { ['type'] = 'single', ['searchRadar'] = { ['Strela-1 9P31'] = { }, }, ['launchers'] = { ['Strela-1 9P31'] = { }, }, ['name'] = { ['NATO'] = 'SA-9 Gaskin', }, ['harm_detection_chance'] = 20 }, ['Tor'] = { ['type'] = 'single', ['searchRadar'] = { ['Tor 9A331'] = { }, }, ['launchers'] = { ['Tor 9A331'] = { }, }, ['name'] = { ['NATO'] = 'SA-15 Gauntlet', }, ['harm_detection_chance'] = 90, ['can_engage_harm'] = true }, ['Gepard'] = { ['type'] = 'single', ['searchRadar'] = { ['Gepard'] = { }, }, ['launchers'] = { ['Gepard'] = { }, }, ['name'] = { ['NATO'] = 'Gepard', }, ['harm_detection_chance'] = 10 }, ['Rapier'] = { ['searchRadar'] = { ['rapier_fsa_blindfire_radar'] = { }, }, ['launchers'] = { ['rapier_fsa_launcher'] = { ['trackingRadar'] = true, }, }, ['misc'] = { ['rapier_fsa_optical_tracker_unit'] = { ['required'] = true, }, }, ['name'] = { ['NATO'] = 'Rapier', }, ['harm_detection_chance'] = 10 }, ['ZSU-23-4 Shilka'] = { ['type'] = 'single', ['searchRadar'] = { ['ZSU-23-4 Shilka'] = { }, }, ['launchers'] = { ['ZSU-23-4 Shilka'] = { }, }, ['name'] = { ['NATO'] = 'Zues', }, ['harm_detection_chance'] = 10 }, ['HQ-7'] = { ['searchRadar'] = { ['HQ-7_STR_SP'] = { ['name'] = { ['NATO'] = 'CSA-4', }, }, }, ['launchers'] = { ['HQ-7_LN_SP'] = { }, }, ['name'] = { ['NATO'] = 'CSA-4', }, ['harm_detection_chance'] = 30 }, ['Phalanx'] = { ['type'] = 'single', ['searchRadar'] = { ['HEMTT_C-RAM_Phalanx'] = { }, }, ['launchers'] = { ['HEMTT_C-RAM_Phalanx'] = { }, }, ['name'] = { ['NATO'] = 'Phalanx', }, ['harm_detection_chance'] = 10 }, -- Start of RED EW radars: ['1L13 EWR'] = { ['type'] = 'ewr', ['searchRadar'] = { ['1L13 EWR'] = { ['name'] = { ['NATO'] = 'Box Spring', }, }, }, ['harm_detection_chance'] = 60 }, ['55G6 EWR'] = { ['type'] = 'ewr', ['searchRadar'] = { ['55G6 EWR'] = { ['name'] = { ['NATO'] = 'Tall Rack', }, }, }, ['harm_detection_chance'] = 60 }, ['Dog Ear'] = { ['type'] = 'ewr', ['searchRadar'] = { ['Dog Ear radar'] = { ['name'] = { ['NATO'] = 'Dog Ear', }, }, }, ['harm_detection_chance'] = 20 }, -- Start of BLUE EW radars: ['FPS-117 Dome'] = { ['type'] = 'ewr', ['searchRadar'] = { ['FPS-117 Dome'] = { ['name'] = { ['NATO'] = 'FPS-117 Dome', }, }, }, ['harm_detection_chance'] = 80 }, ['FPS-117'] = { ['type'] = 'ewr', ['searchRadar'] = { ['FPS-117'] = { ['name'] = { ['NATO'] = 'FPS-117', }, }, }, ['harm_detection_chance'] = 80 } } end ================================================ FILE: skynet-iads-source/skynet-iads-table-delegator.lua ================================================ do SkynetIADSTableDelegator = {} function SkynetIADSTableDelegator:create() local instance = {} local forwarder = {} forwarder.__index = function(tbl, name) tbl[name] = function(self, ...) for i = 1, #self do self[i][name](self[i], ...) end return self end return tbl[name] end setmetatable(instance, forwarder) instance.__index = forwarder return instance end end ================================================ FILE: skynet-iads-source/skynet-iads.lua ================================================ do SkynetIADS = {} SkynetIADS.__index = SkynetIADS SkynetIADS.database = samTypesDB function SkynetIADS:create(name) local iads = {} setmetatable(iads, SkynetIADS) iads.radioMenu = nil iads.earlyWarningRadars = {} iads.samSites = {} iads.commandCenters = {} iads.ewRadarScanMistTaskID = nil iads.coalition = nil iads.contacts = {} iads.maxTargetAge = 32 iads.name = name iads.harmDetection = SkynetIADSHARMDetection:create(iads) iads.logger = SkynetIADSLogger:create(iads) if iads.name == nil then iads.name = "" end iads.contactUpdateInterval = 5 world.addEventHandler(iads) return iads end function SkynetIADS:onEvent(event) if (event.id == world.event.S_EVENT_BIRTH ) then env.info("New Object Spawned") -- self:addSAMSite(event.initiator:getGroup():getName()); end end function SkynetIADS:setUpdateInterval(interval) self.contactUpdateInterval = interval end function SkynetIADS:setCoalition(item) if item then local coalitionID = item:getCoalition() if self.coalitionID == nil then self.coalitionID = coalitionID end if self.coalitionID ~= coalitionID then self:printOutputToLog("element: "..item:getName().." has a different coalition than the IADS", true) end end end function SkynetIADS:addJammer(jammer) table.insert(self.jammers, jammer) end function SkynetIADS:getCoalition() return self.coalitionID end function SkynetIADS:getDestroyedEarlyWarningRadars() local destroyedSites = {} for i = 1, #self.earlyWarningRadars do local ewSite = self.earlyWarningRadars[i] if ewSite:isDestroyed() then table.insert(destroyedSites, ewSite) end end return destroyedSites end function SkynetIADS:getUsableAbstractRadarElemtentsOfTable(abstractRadarTable) local usable = {} for i = 1, #abstractRadarTable do local abstractRadarElement = abstractRadarTable[i] if abstractRadarElement:hasActiveConnectionNode() and abstractRadarElement:hasWorkingPowerSource() and abstractRadarElement:isDestroyed() == false then table.insert(usable, abstractRadarElement) end end return usable end function SkynetIADS:getUsableEarlyWarningRadars() return self:getUsableAbstractRadarElemtentsOfTable(self.earlyWarningRadars) end function SkynetIADS:createTableDelegator(units) local sites = SkynetIADSTableDelegator:create() for i = 1, #units do local site = units[i] table.insert(sites, site) end return sites end function SkynetIADS:addEarlyWarningRadarsByPrefix(prefix) self:deactivateEarlyWarningRadars() self.earlyWarningRadars = {} for unitName, unit in pairs(mist.DBs.unitsByName) do local pos = self:findSubString(unitName, prefix) --somehow the MIST unit db contains StaticObject, we check to see we only add Units local unit = Unit.getByName(unitName) if pos and pos == 1 and unit then self:addEarlyWarningRadar(unitName) end end return self:createTableDelegator(self.earlyWarningRadars) end function SkynetIADS:addEarlyWarningRadar(earlyWarningRadarUnitName) local earlyWarningRadarUnit = Unit.getByName(earlyWarningRadarUnitName) if earlyWarningRadarUnit == nil then self:printOutputToLog("you have added an EW Radar that does not exist, check name of Unit in Setup and Mission editor: "..earlyWarningRadarUnitName, true) return end self:setCoalition(earlyWarningRadarUnit) local ewRadar = nil local category = earlyWarningRadarUnit:getDesc().category if category == Unit.Category.AIRPLANE or category == Unit.Category.SHIP then ewRadar = SkynetIADSAWACSRadar:create(earlyWarningRadarUnit, self) else ewRadar = SkynetIADSEWRadar:create(earlyWarningRadarUnit, self) end ewRadar:setupElements() ewRadar:setCachedTargetsMaxAge(self:getCachedTargetsMaxAge()) -- for performance improvement, if iads is not scanning no update coverage update needs to be done, will be executed once when iads activates if self.ewRadarScanMistTaskID ~= nil then self:buildRadarCoverageForEarlyWarningRadar(ewRadar) end ewRadar:setActAsEW(true) ewRadar:setToCorrectAutonomousState() ewRadar:goLive() table.insert(self.earlyWarningRadars, ewRadar) if self:getDebugSettings().addedEWRadar then self:printOutputToLog("ADDED: "..ewRadar:getDescription()) end return ewRadar end function SkynetIADS:getCachedTargetsMaxAge() return self.contactUpdateInterval end function SkynetIADS:getEarlyWarningRadars() return self:createTableDelegator(self.earlyWarningRadars) end function SkynetIADS:getEarlyWarningRadarByUnitName(unitName) for i = 1, #self.earlyWarningRadars do local ewRadar = self.earlyWarningRadars[i] if ewRadar:getDCSName() == unitName then return ewRadar end end end function SkynetIADS:findSubString(haystack, needle) return string.find(haystack, needle, 1, true) end function SkynetIADS:addSAMSitesByPrefix(prefix) self:deativateSAMSites() self.samSites = {} for groupName, groupData in pairs(mist.DBs.groupsByName) do local pos = self:findSubString(groupName, prefix) if pos and pos == 1 then --mist returns groups, units and, StaticObjects local dcsObject = Group.getByName(groupName) if dcsObject and dcsObject:getUnits()[1]:isActive() then self:addSAMSite(groupName) end end end return self:createTableDelegator(self.samSites) end function SkynetIADS:getSAMSitesByPrefix(prefix) local returnSams = {} for i = 1, #self.samSites do local samSite = self.samSites[i] local groupName = samSite:getDCSName() local pos = self:findSubString(groupName, prefix) if pos and pos == 1 then table.insert(returnSams, samSite) end end return self:createTableDelegator(returnSams) end function SkynetIADS:addSAMSite(samSiteName) local samSiteDCS = Group.getByName(samSiteName) if samSiteDCS == nil then self:printOutputToLog("you have added an SAM Site that does not exist, check name of Group in Setup and Mission editor: "..tostring(samSiteName), true) return end self:setCoalition(samSiteDCS) local samSite = SkynetIADSSamSite:create(samSiteDCS, self) samSite:setupElements() samSite:setCanEngageAirWeapons(true) samSite:goLive() samSite:setCachedTargetsMaxAge(self:getCachedTargetsMaxAge()) if samSite:getNatoName() == "UNKNOWN" then self:printOutputToLog("you have added an SAM site that Skynet IADS can not handle: "..samSite:getDCSName(), true) samSite:cleanUp() else samSite:goDark() table.insert(self.samSites, samSite) if self:getDebugSettings().addedSAMSite then self:printOutputToLog("ADDED: "..samSite:getDescription()) end -- for performance improvement, if iads is not scanning no update coverage update needs to be done, will be executed once when iads activates if self.ewRadarScanMistTaskID ~= nil then self:buildRadarCoverageForSAMSite(samSite) end return samSite end end function SkynetIADS:getUsableSAMSites() return self:getUsableAbstractRadarElemtentsOfTable(self.samSites) end function SkynetIADS:getDestroyedSAMSites() local destroyedSites = {} for i = 1, #self.samSites do local samSite = self.samSites[i] if samSite:isDestroyed() then table.insert(destroyedSites, samSite) end end return destroyedSites end function SkynetIADS:getSAMSites() return self:createTableDelegator(self.samSites) end function SkynetIADS:getActiveSAMSites() local activeSAMSites = {} for i = 1, #self.samSites do if self.samSites[i]:isActive() then table.insert(activeSAMSites, self.samSites[i]) end end return activeSAMSites end function SkynetIADS:getSAMSiteByGroupName(groupName) for i = 1, #self.samSites do local samSite = self.samSites[i] if samSite:getDCSName() == groupName then return samSite end end end function SkynetIADS:getSAMSitesByNatoName(natoName) local selectedSAMSites = SkynetIADSTableDelegator:create() for i = 1, #self.samSites do local samSite = self.samSites[i] if samSite:getNatoName() == natoName then table.insert(selectedSAMSites, samSite) end end return selectedSAMSites end function SkynetIADS:addCommandCenter(commandCenter) self:setCoalition(commandCenter) local comCenter = SkynetIADSCommandCenter:create(commandCenter, self) table.insert(self.commandCenters, comCenter) -- when IADS is active the radars will be added to the new command center. If it not active this will happen when radar coverage is built if self.ewRadarScanMistTaskID ~= nil then self:addRadarsToCommandCenters() end return comCenter end function SkynetIADS:isCommandCenterUsable() if #self:getCommandCenters() == 0 then return true end local usableComCenters = self:getUsableAbstractRadarElemtentsOfTable(self:getCommandCenters()) return (#usableComCenters > 0) end function SkynetIADS:getCommandCenters() return self.commandCenters end function SkynetIADS.evaluateContacts(self) local ewRadars = self:getUsableEarlyWarningRadars() local samSites = self:getUsableSAMSites() --will add SAM Sites acting as EW Rardars to the ewRadars array: for i = 1, #samSites do local samSite = samSites[i] --We inform SAM sites that a target update is about to happen. If they have no targets in range after the cycle they go dark samSite:targetCycleUpdateStart() if samSite:getActAsEW() then table.insert(ewRadars, samSite) end --if the sam site is not in ew mode and active we grab the detected targets right here if samSite:isActive() and samSite:getActAsEW() == false then local contacts = samSite:getDetectedTargets() for j = 1, #contacts do local contact = contacts[j] self:mergeContact(contact) end end end local samSitesToTrigger = {} for i = 1, #ewRadars do local ewRadar = ewRadars[i] --call go live in case ewRadar had to shut down (HARM attack) ewRadar:goLive() -- if an awacs has traveled more than a predeterminded distance we update the autonomous state of the SAMs if getmetatable(ewRadar) == SkynetIADSAWACSRadar and ewRadar:isUpdateOfAutonomousStateOfSAMSitesRequired() then self:buildRadarCoverageForEarlyWarningRadar(ewRadar) end local ewContacts = ewRadar:getDetectedTargets() if #ewContacts > 0 then local samSitesUnderCoverage = ewRadar:getUsableChildRadars() for j = 1, #samSitesUnderCoverage do local samSiteUnterCoverage = samSitesUnderCoverage[j] -- only if a SAM site is not active we add it to the hash of SAM sites to be iterated later on if samSiteUnterCoverage:isActive() == false then --we add them to a hash to make sure each SAM site is in the collection only once, reducing the number of loops we conduct later on samSitesToTrigger[samSiteUnterCoverage:getDCSName()] = samSiteUnterCoverage end end for j = 1, #ewContacts do local contact = ewContacts[j] self:mergeContact(contact) end end end self:cleanAgedTargets() for samName, samToTrigger in pairs(samSitesToTrigger) do for j = 1, #self.contacts do local contact = self.contacts[j] -- the DCS Radar only returns enemy aircraft, if that should change a coalition check will be required -- currently every type of object in the air is handed of to the SAM site, including missiles local description = contact:getDesc() local category = description.category if category and category ~= Unit.Category.GROUND_UNIT and category ~= Unit.Category.SHIP and category ~= Unit.Category.STRUCTURE then samToTrigger:informOfContact(contact) end end end for i = 1, #samSites do local samSite = samSites[i] samSite:targetCycleUpdateEnd() end self.harmDetection:setContacts(self:getContacts()) self.harmDetection:evaluateContacts() self.logger:printSystemStatus() end function SkynetIADS:cleanAgedTargets() local contactsToKeep = {} for i = 1, #self.contacts do local contact = self.contacts[i] if contact:getAge() < self.maxTargetAge then table.insert(contactsToKeep, contact) end end self.contacts = contactsToKeep end --TODO unit test this method: function SkynetIADS:getAbstracRadarElements() local abstractRadarElements = {} local ewRadars = self:getEarlyWarningRadars() local samSites = self:getSAMSites() for i = 1, #ewRadars do local ewRadar = ewRadars[i] table.insert(abstractRadarElements, ewRadar) end for i = 1, #samSites do local samSite = samSites[i] table.insert(abstractRadarElements, samSite) end return abstractRadarElements end function SkynetIADS:addRadarsToCommandCenters() --we clear any existing radars that may have been added earlier local comCenters = self:getCommandCenters() for i = 1, #comCenters do local comCenter = comCenters[i] comCenter:clearChildRadars() end -- then we add child radars to the command centers local abstractRadarElements = self:getAbstracRadarElements() for i = 1, #abstractRadarElements do local abstractRadar = abstractRadarElements[i] self:addSingleRadarToCommandCenters(abstractRadar) end end function SkynetIADS:addSingleRadarToCommandCenters(abstractRadarElement) local comCenters = self:getCommandCenters() for i = 1, #comCenters do local comCenter = comCenters[i] comCenter:addChildRadar(abstractRadarElement) end end -- this method rebuilds the radar coverage of the IADS, a complete rebuild is only required the first time the IADS is activated -- during runtime it is sufficient to call buildRadarCoverageForSAMSite or buildRadarCoverageForEarlyWarningRadar method that just updates the IADS for one unit, this saves script execution time function SkynetIADS:buildRadarCoverage() --to build the basic radar coverage we use all SAM sites. Checks if SAM site has power or a connection node is done when using the SAM site later on local samSites = self:getSAMSites() --first we clear all child and parent radars that may have been added previously for i = 1, #samSites do local samSite = samSites[i] samSite:clearChildRadars() samSite:clearParentRadars() end local ewRadars = self:getEarlyWarningRadars() for i = 1, #ewRadars do local ewRadar = ewRadars[i] ewRadar:clearChildRadars() end --then we rebuild the radar coverage local abstractRadarElements = self:getAbstracRadarElements() for i = 1, #abstractRadarElements do local abstract = abstractRadarElements[i] self:buildRadarCoverageForAbstractRadarElement(abstract) end self:addRadarsToCommandCenters() --we call this once on all sam sites, to make sure autonomous sites go live when IADS activates for i = 1, #samSites do local samSite = samSites[i] samSite:informChildrenOfStateChange() end end function SkynetIADS:buildRadarCoverageForAbstractRadarElement(abstractRadarElement) local abstractRadarElements = self:getAbstracRadarElements() for i = 1, #abstractRadarElements do local aElementToCompare = abstractRadarElements[i] if aElementToCompare ~= abstractRadarElement then if abstractRadarElement:isInRadarDetectionRangeOf(aElementToCompare) then self:buildRadarAssociation(aElementToCompare, abstractRadarElement) end if aElementToCompare:isInRadarDetectionRangeOf(abstractRadarElement) then self:buildRadarAssociation(abstractRadarElement, aElementToCompare) end end end end function SkynetIADS:buildRadarAssociation(parent, child) --chilren should only be SAM sites not EW radars if ( getmetatable(child) == SkynetIADSSamSite ) then parent:addChildRadar(child) end --Only SAM Sites should have parent Radars, not EW Radars if ( getmetatable(child) == SkynetIADSSamSite ) then child:addParentRadar(parent) end end function SkynetIADS:buildRadarCoverageForSAMSite(samSite) self:buildRadarCoverageForAbstractRadarElement(samSite) self:addSingleRadarToCommandCenters(samSite) end function SkynetIADS:buildRadarCoverageForEarlyWarningRadar(ewRadar) self:buildRadarCoverageForAbstractRadarElement(ewRadar) self:addSingleRadarToCommandCenters(ewRadar) end function SkynetIADS:mergeContact(contact) local existingContact = false for i = 1, #self.contacts do local iadsContact = self.contacts[i] if iadsContact:getName() == contact:getName() then iadsContact:refresh() --these contacts are used in the logger we set a kown harm state of a contact coming from a SAM site. So the logger will show them als HARMs contact:setHARMState(iadsContact:getHARMState()) local radars = contact:getAbstractRadarElementsDetected() for j = 1, #radars do local radar = radars[j] iadsContact:addAbstractRadarElementDetected(radar) end existingContact = true end end if existingContact == false then table.insert(self.contacts, contact) end end function SkynetIADS:getContacts() return self.contacts end function SkynetIADS:getDebugSettings() return self.logger.debugOutput end function SkynetIADS:printOutput(output, typeWarning) self.logger:printOutput(output, typeWarning) end function SkynetIADS:printOutputToLog(output) self.logger:printOutputToLog(output) end -- will start going through the Early Warning Radars and SAM sites to check what targets they have detected function SkynetIADS.activate(self) mist.removeFunction(self.ewRadarScanMistTaskID) self.ewRadarScanMistTaskID = mist.scheduleFunction(SkynetIADS.evaluateContacts, {self}, 1, self.contactUpdateInterval) self:buildRadarCoverage() end function SkynetIADS:setupSAMSitesAndThenActivate(setupTime) self:activate() self.logger:printOutputToLog("DEPRECATED: setupSAMSitesAndThenActivate, no longer needed since using enableEmission instead of AI on / off allows for the Ground units to setup with their radars turned off") end function SkynetIADS:deactivate() mist.removeFunction(self.ewRadarScanMistTaskID) mist.removeFunction(self.samSetupMistTaskID) self:deativateSAMSites() self:deactivateEarlyWarningRadars() self:deactivateCommandCenters() end function SkynetIADS:deactivateCommandCenters() for i = 1, #self.commandCenters do local comCenter = self.commandCenters[i] comCenter:cleanUp() end end function SkynetIADS:deativateSAMSites() for i = 1, #self.samSites do local samSite = self.samSites[i] samSite:cleanUp() end end function SkynetIADS:deactivateEarlyWarningRadars() for i = 1, #self.earlyWarningRadars do local ewRadar = self.earlyWarningRadars[i] ewRadar:cleanUp() end end function SkynetIADS:addRadioMenu() self.radioMenu = missionCommands.addSubMenu('SKYNET IADS '..self:getCoalitionString()) local displayIADSStatus = missionCommands.addCommand('show IADS Status', self.radioMenu, SkynetIADS.updateDisplay, {self = self, value = true, option = 'IADSStatus'}) local displayIADSStatus = missionCommands.addCommand('hide IADS Status', self.radioMenu, SkynetIADS.updateDisplay, {self = self, value = false, option = 'IADSStatus'}) local displayIADSStatus = missionCommands.addCommand('show contacts', self.radioMenu, SkynetIADS.updateDisplay, {self = self, value = true, option = 'contacts'}) local displayIADSStatus = missionCommands.addCommand('hide contacts', self.radioMenu, SkynetIADS.updateDisplay, {self = self, value = false, option = 'contacts'}) end function SkynetIADS:removeRadioMenu() missionCommands.removeItem(self.radioMenu) end function SkynetIADS.updateDisplay(params) local option = params.option local self = params.self local value = params.value if option == 'IADSStatus' then self:getDebugSettings()[option] = value elseif option == 'contacts' then self:getDebugSettings()[option] = value end end function SkynetIADS:getCoalitionString() local coalitionStr = "RED" if self.coalitionID == coalition.side.BLUE then coalitionStr = "BLUE" elseif self.coalitionID == coalition.side.NEUTRAL then coalitionStr = "NEUTRAL" end if self.name then coalitionStr = "COALITION: "..coalitionStr.." | NAME: "..self.name end return coalitionStr end function SkynetIADS:getMooseConnector() if self.mooseConnector == nil then self.mooseConnector = SkynetMooseA2ADispatcherConnector:create(self) end return self.mooseConnector end function SkynetIADS:addMooseSetGroup(mooseSetGroup) self:getMooseConnector():addMooseSetGroup(mooseSetGroup) end end ================================================ FILE: skynet-iads-source/skynet-mooose-a2a-dispatcher-connector.lua ================================================ do SkynetMooseA2ADispatcherConnector = {} function SkynetMooseA2ADispatcherConnector:create(iads) local instance = {} setmetatable(instance, self) self.__index = self instance.iadsCollection = {} instance.mooseGroups = {} instance.ewRadarGroupNames = {} instance.samSiteGroupNames = {} table.insert(instance.iadsCollection, iads) return instance end function SkynetMooseA2ADispatcherConnector:addIADS(iads) table.insert(self.iadsCollection, iads) end function SkynetMooseA2ADispatcherConnector:addMooseSetGroup(mooseSetGroup) table.insert(self.mooseGroups, mooseSetGroup) self:update() end function SkynetMooseA2ADispatcherConnector:getEarlyWarningRadarGroupNames() self.ewRadarGroupNames = {} for i = 1, #self.iadsCollection do local ewRadars = self.iadsCollection[i]:getUsableEarlyWarningRadars() for j = 1, #ewRadars do local ewRadar = ewRadars[j] table.insert(self.ewRadarGroupNames, ewRadar:getDCSRepresentation():getGroup():getName()) end end return self.ewRadarGroupNames end function SkynetMooseA2ADispatcherConnector:getSAMSiteGroupNames() self.samSiteGroupNames = {} for i = 1, #self.iadsCollection do local samSites = self.iadsCollection[i]:getUsableSAMSites() for j = 1, #samSites do local samSite = samSites[j] table.insert(self.samSiteGroupNames, samSite:getDCSName()) end end return self.samSiteGroupNames end function SkynetMooseA2ADispatcherConnector:update() --mooseGroup elements are type of: --https://flightcontrol-master.github.io/MOOSE_DOCS_DEVELOP/Documentation/Core.Set.html##(SET_GROUP) --remove previously set group names: for i = 1, #self.mooseGroups do local mooseGroup = self.mooseGroups[i] mooseGroup:RemoveGroupsByName(self.ewRadarGroupNames) mooseGroup:RemoveGroupsByName(self.samSiteGroupNames) end --add group names of IADS radars that are currently usable by the IADS: for i = 1, #self.mooseGroups do local mooseGroup = self.mooseGroups[i] mooseGroup:AddGroupsByName(self:getEarlyWarningRadarGroupNames()) mooseGroup:AddGroupsByName(self:getSAMSiteGroupNames()) end end end ================================================ FILE: skynet-iads-source/syknet-iads-sam-launcher.lua ================================================ do SkynetIADSSAMLauncher = {} SkynetIADSSAMLauncher = inheritsFrom(SkynetIADSSAMSearchRadar) function SkynetIADSSAMLauncher:create(unit) local instance = self:superClass():create(unit) setmetatable(instance, self) self.__index = self instance.maximumFiringAltitude = 0 return instance end function SkynetIADSSAMLauncher:setupRangeData() self.remainingNumberOfMissiles = 0 self.remainingNumberOfShells = 0 if self:isExist() then local data = self:getDCSRepresentation():getAmmo() local initialNumberOfMissiles = 0 local initialNumberOfShells = 0 --data becomes nil, when all missiles are fired if data then for i = 1, #data do local ammo = data[i] --we ignore checks on radar guidance types, since we are not interested in how exactly the missile is guided by the SAM site. if ammo.desc.category == Weapon.Category.MISSILE then --TODO: see what the difference is between Max and Min values, SA-3 has higher Min value than Max?, most likely it has to do with the box parameters supplied by launcher --to simplyfy we just use the larger value, sam sites need a few seconds of tracking time to fire, by that time contact has most likely closed in on the SAM site. local altMin = ammo.desc.rangeMaxAltMin local altMax = ammo.desc.rangeMaxAltMax self.maximumRange = altMin if altMin < altMax then self.maximumRange = altMax end self.maximumFiringAltitude = ammo.desc.altMax self.remainingNumberOfMissiles = self.remainingNumberOfMissiles + ammo.count initialNumberOfMissiles = self.remainingNumberOfMissiles end if ammo.desc.category == Weapon.Category.SHELL then self.remainingNumberOfShells = self.remainingNumberOfShells + ammo.count initialNumberOfShells = self.remainingNumberOfShells end --if no distance was detected we run the code for the search radar. This happens when all in one units are passed like the shilka if self.maximumRange == 0 then --this is to prevent infinite calls between launcher and search radar if self.triedSensors <= 2 then SkynetIADSSAMSearchRadar.setupRangeData(self) end end end -- conditions here are because setupRangeData() is called multiple times in the code to update ammo status, we set initial values only the first time the method is called if self.initialNumberOfMissiles == 0 then self.initialNumberOfMissiles = initialNumberOfMissiles end if self.initialNumberOfShells == 0 then self.initialNumberOfShells = initialNumberOfShells end end end end function SkynetIADSSAMLauncher:getInitialNumberOfShells() return self.initialNumberOfShells end function SkynetIADSSAMLauncher:getRemainingNumberOfShells() self:setupRangeData() return self.remainingNumberOfShells end function SkynetIADSSAMLauncher:getInitialNumberOfMissiles() return self.initialNumberOfMissiles end function SkynetIADSSAMLauncher:getRemainingNumberOfMissiles() self:setupRangeData() return self.remainingNumberOfMissiles end function SkynetIADSSAMLauncher:getRange() return self.maximumRange end function SkynetIADSSAMLauncher:getMaximumFiringAltitude() return self.maximumFiringAltitude end function SkynetIADSSAMLauncher:isWithinFiringHeight(target) -- if no max firing height is set (radar quided AAA) then we use the vertical range, bit of a hack but probably ok for AAA if self:getMaximumFiringAltitude() > 0 then return self:getMaximumFiringAltitude() >= self:getHeight(target) else return self:getRange() >= self:getHeight(target) end end function SkynetIADSSAMLauncher:isInRange(target) if self:isExist() == false then return false end return self:isWithinFiringHeight(target) and self:isInHorizontalRange(target) end end --[[ SA-2 Launcher: { count=1, desc={ Nmax=17, RCS=0.39669999480247, _origin="", altMax=25000, altMin=100, box={ max={x=4.7303376197815, y=0.84564626216888, z=0.84564626216888}, min={x=-5.8387970924377, y=-0.84564626216888, z=-0.84564626216888} }, category=1, displayName="SA2V755", fuseDist=20, guidance=4, life=2, missileCategory=2, rangeMaxAltMax=30000, rangeMaxAltMin=40000, rangeMin=7000, typeName="SA2V755", warhead={caliber=500, explosiveMass=196, mass=196, type=1} } } } --]] ================================================ FILE: unit-tests/highdigitsams/skynet-high-digit-sams-unit-test-setup.lua ================================================ do local units = Group.getByName('SAM-SA-20B'):getUnits() for i = 1, #units do local unit = units[i] env.info(unit:getTypeName()) end lu.LuaUnit.run() --activate IADS redIADS = SkynetIADS:create("Red IADS") local iadsDebug = redIADS:getDebugSettings() iadsDebug.IADSStatus = true iadsDebug.radarWentDark = true iadsDebug.contacts = true iadsDebug.radarWentLive = true iadsDebug.jammerProbability = true iadsDebug.addedEWRadar = true iadsDebug.addedSAMSite = true iadsDebug.harmDefence = true iadsDebug.commandCenterStatusEnvOutput = true iadsDebug.samSiteStatusEnvOutput = true redIADS:addSAMSitesByPrefix('SAM') redIADS:activate() end ================================================ FILE: unit-tests/highdigitsams/test-skynet-high-digit-sam-sites.lua ================================================ do TestSyknetIADSHighDigitSAMSites = {} function TestSyknetIADSHighDigitSAMSites:setUp() if self.samSiteName then self.skynetIADS = SkynetIADS:create() local samSite = Group.getByName(self.samSiteName) self.samSite = SkynetIADSSamSite:create(samSite, self.skynetIADS) self.samSite:setupElements() end if self.ewName then self.skynetIADS = SkynetIADS:create() local ewRadar = Unit.getByName(self.ewName) self.ewRadar = SkynetIADSEWRadar:create(ewRadar, self.skynetIADS) self.ewRadar:setupElements() end end function TestSyknetIADSHighDigitSAMSites:tearDown() if self.samSite then self.samSite:cleanUp() end if self.ewRadar then self.ewRadar:cleanUp() end end function TestSyknetIADSHighDigitSAMSites:testSA20AGargoyle() self.samSiteName = "SAM-SA-20A" self:setUp() lu.assertEquals(self.samSite:getNatoName(), "SA-20A") local launchers = self.samSite:getLaunchers() lu.assertEquals(#launchers, 2) local launcher1 = launchers[1] lu.assertEquals(launcher1:getTypeName(), "S-300PMU1 5P85CE ln") lu.assertEquals(launcher1:getRange(), 150000) lu.assertEquals(launcher1:getMaximumFiringAltitude(), 27000) lu.assertEquals(launcher1:getInitialNumberOfMissiles(), 4) local launcher2 = launchers[2] lu.assertEquals(launcher2:getTypeName(), "S-300PMU1 5P85DE ln") lu.assertEquals(launcher2:getRange(), 150000) lu.assertEquals(launcher2:getMaximumFiringAltitude(), 27000) lu.assertEquals(launcher1:getInitialNumberOfMissiles(), 4) local searchRadars = self.samSite:getSearchRadars() lu.assertEquals(#searchRadars, 2) local searchRadars1 = searchRadars[1] lu.assertEquals(searchRadars1:getTypeName(), "S-300PMU1 40B6MD sr") lu.assertEquals(searchRadars1:getMaxRangeFindingTarget(), 106998.453125) local searchRadars2 = searchRadars[2] lu.assertEquals(searchRadars2:getTypeName(), "S-300PMU1 64N6E sr") lu.assertEquals(searchRadars2:getMaxRangeFindingTarget(), 106998.453125) local trackingRadars = self.samSite:getTrackingRadars() lu.assertEquals(#trackingRadars, 2) local trackingRadar1 = trackingRadars[1] lu.assertEquals(trackingRadar1:getTypeName(), "S-300PMU1 40B6M tr") lu.assertEquals(trackingRadar1:getMaxRangeFindingTarget(), 106998.453125) local trackingRadar2 = trackingRadars[2] lu.assertEquals(trackingRadar2:getTypeName(), "S-300PMU1 30N6E tr") lu.assertEquals(trackingRadar2:getMaxRangeFindingTarget(), 106998.453125) lu.assertEquals(self.samSite:getHARMDetectionChance(), 90) lu.assertEquals(self.samSite:getCanEngageHARM(), true) --output sensor data to dcs.log: --lu.assertEquals(launcher1:getDCSRepresentation():getSensors(), "00") end function TestSyknetIADSHighDigitSAMSites:testBigBird() self.ewName = "Big-Bird" self:setUp() lu.assertEquals(self.ewRadar:getNatoName(), "Big Bird") end function TestSyknetIADSHighDigitSAMSites:testClamShell() self.ewName = "Clam-Shell" self:setUp() lu.assertEquals(self.ewRadar:getNatoName(), "Clam Shell") end function TestSyknetIADSHighDigitSAMSites:testBillBoardC() self.ewName = "Bill-Board-C" self:setUp() lu.assertEquals(self.ewRadar:getNatoName(), "Bill Board-C") end function TestSyknetIADSHighDigitSAMSites:testHighScreenB() self.ewName = "High-Screen-B" self:setUp() lu.assertEquals(self.ewRadar:getNatoName(), "High Screen-B") end function TestSyknetIADSHighDigitSAMSites:testClamShell2() self.ewName = "Clam-Shell-2" self:setUp() lu.assertEquals(self.ewRadar:getNatoName(), "Clam Shell") end function TestSyknetIADSHighDigitSAMSites:testSnowDrift() self.ewName = "Snow-Drift" self:setUp() lu.assertEquals(self.ewRadar:getNatoName(), "Snow Drift") end function TestSyknetIADSHighDigitSAMSites:testUnnamedRadar() self.ewName = "unnamed-radar" self:setUp() lu.assertEquals(self.ewRadar:getNatoName(), "UNKNOWN") lu.assertEquals(self.ewRadar:getHARMDetectionChance(), 90) end function TestSyknetIADSHighDigitSAMSites:testSA23GladiatorOrGiant() self.samSiteName = "SAM-SA-23" self:setUp() lu.assertEquals(self.samSite:getNatoName(), "SA-23") local launchers = self.samSite:getLaunchers() lu.assertEquals(#launchers, 2) local launcher1 = launchers[1] lu.assertEquals(launcher1:getTypeName(), "S-300VM 9A83ME ln") lu.assertEquals(launcher1:getRange(), 100000) lu.assertEquals(launcher1:getMaximumFiringAltitude(), 30000) lu.assertEquals(launcher1:getInitialNumberOfMissiles(), 4) local launcher1 = launchers[2] lu.assertEquals(launcher1:getTypeName(), "S-300VM 9A82ME ln") lu.assertEquals(launcher1:getRange(), 200000) lu.assertEquals(launcher1:getMaximumFiringAltitude(), 37000) lu.assertEquals(launcher1:getInitialNumberOfMissiles(), 2) local searchRadars = self.samSite:getSearchRadars() lu.assertEquals(#searchRadars, 2) local searchRadars1 = searchRadars[1] lu.assertEquals(searchRadars1:getTypeName(), "S-300VM 9S15M2 sr") lu.assertEquals(searchRadars1:getMaxRangeFindingTarget(), 213996.90625) local searchRadars1 = searchRadars[2] lu.assertEquals(searchRadars1:getTypeName(), "S-300VM 9S19M2 sr") lu.assertEquals(searchRadars1:getMaxRangeFindingTarget(), 213996.90625) local trackingRadars = self.samSite:getTrackingRadars() lu.assertEquals(#trackingRadars, 1) local trackingRadar1 = trackingRadars[1] lu.assertEquals(trackingRadar1:getTypeName(), "S-300VM 9S32ME tr") lu.assertEquals(trackingRadar1:getMaxRangeFindingTarget(), 213996.90625) lu.assertEquals(self.samSite:getHARMDetectionChance(), 90) lu.assertEquals(self.samSite:getCanEngageHARM(), true) end function TestSyknetIADSHighDigitSAMSites:testSA10BGrumble() self.samSiteName = "SAM-SA-10B" self:setUp() lu.assertEquals(self.samSite:getNatoName(), "SA-10B") local launchers = self.samSite:getLaunchers() lu.assertEquals(#launchers, 2) local launcher1 = launchers[1] lu.assertEquals(launcher1:getTypeName(), "S-300PS 5P85SE_mod ln") lu.assertEquals(launcher1:getRange(), 75000) lu.assertEquals(launcher1:getMaximumFiringAltitude(), 30000) lu.assertEquals(launcher1:getInitialNumberOfMissiles(), 4) local launcher1 = launchers[2] lu.assertEquals(launcher1:getTypeName(), "S-300PS 5P85SU_mod ln") lu.assertEquals(launcher1:getRange(), 75000) lu.assertEquals(launcher1:getMaximumFiringAltitude(), 30000) lu.assertEquals(launcher1:getInitialNumberOfMissiles(), 4) local searchRadars = self.samSite:getSearchRadars() lu.assertEquals(#searchRadars, 2) local searchRadars1 = searchRadars[1] lu.assertEquals(searchRadars1:getTypeName(), "S-300PS SA-10B 40B6MD MAST sr") lu.assertEquals(searchRadars1:getMaxRangeFindingTarget(), 80248.84375) local searchRadars1 = searchRadars[2] lu.assertEquals(searchRadars1:getTypeName(), "S-300PS 64H6E TRAILER sr") lu.assertEquals(searchRadars1:getMaxRangeFindingTarget(), 80248.84375) local trackingRadars = self.samSite:getTrackingRadars() lu.assertEquals(#trackingRadars, 2) local trackingRadar1 = trackingRadars[1] lu.assertEquals(trackingRadar1:getTypeName(), "S-300PS 30N6 TRAILER tr") lu.assertEquals(trackingRadar1:getMaxRangeFindingTarget(), 80248.84375) local trackingRadar1 = trackingRadars[2] lu.assertEquals(trackingRadar1:getTypeName(), "S-300PS SA-10B 40B6M MAST tr") lu.assertEquals(trackingRadar1:getMaxRangeFindingTarget(), 80248.84375) lu.assertEquals(self.samSite:getHARMDetectionChance(), 90) lu.assertEquals(self.samSite:getCanEngageHARM(), true) end function TestSyknetIADSHighDigitSAMSites:testEDDefaultSA10GrubleWith55VRUD() self.samSiteName = "SAM-SA-10C-5V55RUD" self:setUp() lu.assertEquals(self.samSite:getNatoName(), "SA-10") local launchers = self.samSite:getLaunchers() lu.assertEquals(#launchers, 2) local launcher1 = launchers[1] lu.assertEquals(launcher1:getTypeName(), "S-300PS 5P85DE ln") lu.assertEquals(launcher1:getRange(), 90000) lu.assertEquals(launcher1:getMaximumFiringAltitude(), 25000) lu.assertEquals(launcher1:getInitialNumberOfMissiles(), 4) end function TestSyknetIADSHighDigitSAMSites:testSA10BGrumbleWith55VRUD() self.samSiteName = "SAM-SA-10B-5V55RUD" self:setUp() lu.assertEquals(self.samSite:getNatoName(), "SA-10B") local launchers = self.samSite:getLaunchers() lu.assertEquals(#launchers, 2) end function TestSyknetIADSHighDigitSAMSites:testSA20AGargoyleWith55VRUD() self.samSiteName = "SAM-SA-20A-5V55RUD" self:setUp() lu.assertEquals(self.samSite:getNatoName(), "SA-20A") local launchers = self.samSite:getLaunchers() lu.assertEquals(#launchers, 2) end function TestSyknetIADSHighDigitSAMSites:testSA17Grizzly() self.samSiteName = "SAM-SA-17" self:setUp() lu.assertEquals(self.samSite:getNatoName(), "SA-17") local launchers = self.samSite:getLaunchers() lu.assertEquals(#launchers, 1) local launcher1 = launchers[1] lu.assertEquals(launcher1:getTypeName(), "SA-17 Buk M1-2 LN 9A310M1-2") lu.assertEquals(launcher1:getRange(), 50000) lu.assertEquals(launcher1:getMaximumFiringAltitude(), 50000) lu.assertEquals(launcher1:getInitialNumberOfMissiles(), 4) end function TestSyknetIADSHighDigitSAMSites:testSA2GuidelineWithV7595V23() self.samSiteName = "SAM-SA-2-V-759-5V23" self:setUp() lu.assertEquals(self.samSite:getNatoName(), "SA-2") local launchers = self.samSite:getLaunchers() lu.assertEquals(#launchers, 1) local launcher1 = launchers[1] lu.assertEquals(launcher1:getTypeName(), "S_75M_Volhov_V759") lu.assertEquals(launcher1:getRange(), 56000) lu.assertEquals(launcher1:getMaximumFiringAltitude(), 30000) lu.assertEquals(launcher1:getInitialNumberOfMissiles(), 1) end function TestSyknetIADSHighDigitSAMSites:testSA3GoaWithV601P5V27() self.samSiteName = "SAM-SA-3-V-601P-5V27" self:setUp() lu.assertEquals(self.samSite:getNatoName(), "SA-3") local launchers = self.samSite:getLaunchers() lu.assertEquals(#launchers, 1) local launcher1 = launchers[1] lu.assertEquals(launcher1:getTypeName(), "5p73 V-601P ln") lu.assertEquals(launcher1:getRange(), 25000) lu.assertEquals(launcher1:getMaximumFiringAltitude(), 18000) lu.assertEquals(launcher1:getInitialNumberOfMissiles(), 4) end function TestSyknetIADSHighDigitSAMSites:testSA2GuidelineWithHQ2() self.samSiteName = "SAM-SA-2HQ-2" self:setUp() lu.assertEquals(self.samSite:getNatoName(), "SA-2") local launchers = self.samSite:getLaunchers() lu.assertEquals(#launchers, 1) local launcher1 = launchers[1] lu.assertEquals(launcher1:getTypeName(), "HQ_2_Guideline_LN") lu.assertEquals(launcher1:getRange(), 50000) lu.assertEquals(launcher1:getMaximumFiringAltitude(), 30000) lu.assertEquals(launcher1:getInitialNumberOfMissiles(), 1) end function TestSyknetIADSHighDigitSAMSites:testSA12GladiatorGiant() self.samSiteName = "SAM-SA-12-S300V" self:setUp() lu.assertEquals(self.samSite:getNatoName(), "SA-12") local launchers = self.samSite:getLaunchers() lu.assertEquals(#launchers, 2) local searchRadars = self.samSite:getSearchRadars() lu.assertEquals(#searchRadars, 2) local searchRadar1 = searchRadars[1] lu.assertEquals(searchRadar1:getTypeName(), "S-300V 9S15 sr") lu.assertEquals(searchRadar1:getMaxRangeFindingTarget(), 160497.6875) local searchRadar2 = searchRadars[2] lu.assertEquals(searchRadar2:getTypeName(), "S-300V 9S19 sr") lu.assertEquals(searchRadar2:getMaxRangeFindingTarget(), 160497.6875) local trackingRadars = self.samSite:getTrackingRadars() lu.assertEquals(#trackingRadars, 1) local trackingRadar1 = trackingRadars[1] lu.assertEquals(trackingRadar1:getTypeName(), "S-300V 9S32 tr") lu.assertEquals(trackingRadar1:getMaxRangeFindingTarget(), 160497.6875) local launcher1 = launchers[1] lu.assertEquals(launcher1:getTypeName(), "S-300V 9A83 ln") lu.assertEquals(launcher1:getRange(), 75000) lu.assertEquals(launcher1:getMaximumFiringAltitude(), 25000) lu.assertEquals(launcher1:getInitialNumberOfMissiles(), 4) local launcher2 = launchers[2] lu.assertEquals(launcher2:getTypeName(), "S-300V 9A82 ln") lu.assertEquals(launcher2:getRange(), 100000) lu.assertEquals(launcher2:getMaximumFiringAltitude(), 30000) lu.assertEquals(launcher2:getInitialNumberOfMissiles(), 2) lu.assertEquals(self.samSite:getHARMDetectionChance(), 90) lu.assertEquals(self.samSite:getCanEngageHARM(), true) end function TestSyknetIADSHighDigitSAMSites:testSA20BGargoyle() self.samSiteName = "SAM-SA-20B" self:setUp() lu.assertEquals(self.samSite:getNatoName(), "SA-20B") local searchRadars = self.samSite:getSearchRadars() lu.assertEquals(#searchRadars, 1) local searchRadar1 = searchRadars[1] lu.assertEquals(searchRadar1:getTypeName(), "S-300PMU2 64H6E2 sr") lu.assertEquals(searchRadar1:getMaxRangeFindingTarget(), 220684.3125) local trackingRadars = self.samSite:getTrackingRadars() lu.assertEquals(#trackingRadars, 1) local trackingRadar1 = trackingRadars[1] lu.assertEquals(trackingRadar1:getTypeName(), "S-300PMU2 92H6E tr") lu.assertEquals(trackingRadar1:getMaxRangeFindingTarget(), 220684.3125) local launchers = self.samSite:getLaunchers() lu.assertEquals(#launchers, 1) local launcher1 = launchers[1] lu.assertEquals(launcher1:getTypeName(), "S-300PMU2 5P85SE2 ln") lu.assertEquals(launcher1:getRange(), 200000) lu.assertEquals(launcher1:getMaximumFiringAltitude(), 27000) lu.assertEquals(launcher1:getInitialNumberOfMissiles(), 4) lu.assertEquals(self.samSite:getHARMDetectionChance(), 90) lu.assertEquals(self.samSite:getCanEngageHARM(), true) end end ================================================ FILE: unit-tests/luaunit.lua ================================================ --[[ luaunit.lua Description: A unit testing framework Homepage: https://github.com/bluebird75/luaunit Development by Philippe Fremy Based on initial work of Ryu, Gwang (http://www.gpgstudy.com/gpgiki/LuaUnit) License: BSD License, see LICENSE.txt ]]-- --require("math") local M={} --- update for DCS ioWrapper = {} ioWrapper.stdout = {} function ioWrapper.stdout:write(str) if self.string == nil then self.string = str else self.string = self.string..str end end function ioWrapper.stdout:flush() env.info(self.string) self.string = nil end --- end update for DCS -- private exported functions (for testing) M.private = {} M.VERSION='3.4-dev' M._VERSION=M.VERSION -- For LuaUnit v2 compatibility -- a version which distinguish between regular Lua and LuaJit M._LUAVERSION = (jit and jit.version) or _VERSION --[[ Some people like assertEquals( actual, expected ) and some people prefer assertEquals( expected, actual ). ]]-- M.ORDER_ACTUAL_EXPECTED = true M.PRINT_TABLE_REF_IN_ERROR_MSG = false M.LINE_LENGTH = 80 M.TABLE_DIFF_ANALYSIS_THRESHOLD = 10 -- display deep analysis for more than 10 items M.LIST_DIFF_ANALYSIS_THRESHOLD = 10 -- display deep analysis for more than 10 items --[[ EPS is meant to help with Lua's floating point math in simple corner cases like almostEquals(1.1-0.1, 1), which may not work as-is (e.g. on numbers with rational binary representation) if the user doesn't provide some explicit error margin. The default margin used by almostEquals() in such cases is EPS; and since Lua may be compiled with different numeric precisions (single vs. double), we try to select a useful default for it dynamically. Note: If the initial value is not acceptable, it can be changed by the user to better suit specific needs. See also: https://en.wikipedia.org/wiki/Machine_epsilon ]] M.EPS = 2^-52 -- = machine epsilon for "double", ~2.22E-16 if math.abs(1.1 - 1 - 0.1) > M.EPS then -- rounding error is above EPS, assume single precision M.EPS = 2^-23 -- = machine epsilon for "float", ~1.19E-07 end -- set this to false to debug luaunit local STRIP_LUAUNIT_FROM_STACKTRACE = true M.VERBOSITY_DEFAULT = 10 M.VERBOSITY_LOW = 1 M.VERBOSITY_QUIET = 0 M.VERBOSITY_VERBOSE = 20 M.DEFAULT_DEEP_ANALYSIS = nil M.FORCE_DEEP_ANALYSIS = true M.DISABLE_DEEP_ANALYSIS = false -- set EXPORT_ASSERT_TO_GLOBALS to have all asserts visible as global values -- EXPORT_ASSERT_TO_GLOBALS = true -- we need to keep a copy of the script args before it is overriden local cmdline_argv = rawget(_G, "arg") M.FAILURE_PREFIX = 'LuaUnit test FAILURE: ' -- prefix string for failed tests M.SUCCESS_PREFIX = 'LuaUnit test SUCCESS: ' -- prefix string for successful tests finished early M.SKIP_PREFIX = 'LuaUnit test SKIP: ' -- prefix string for skipped tests M.USAGE=[[Usage: lua [options] [testname1 [testname2] ... ] Options: -h, --help: Print this help --version: Print version information -v, --verbose: Increase verbosity -q, --quiet: Set verbosity to minimum -e, --error: Stop on first error -f, --failure: Stop on first failure or error -s, --shuffle: Shuffle tests before running them -o, --output OUTPUT: Set output type to OUTPUT Possible values: text, tap, junit, nil -n, --name NAME: For junit only, mandatory name of xml file -r, --repeat NUM: Execute all tests NUM times, e.g. to trig the JIT -p, --pattern PATTERN: Execute all test names matching the Lua PATTERN May be repeated to include several patterns Make sure you escape magic chars like +? with % -x, --exclude PATTERN: Exclude all test names matching the Lua PATTERN May be repeated to exclude several patterns Make sure you escape magic chars like +? with % testname1, testname2, ... : tests to run in the form of testFunction, TestClass or TestClass.testMethod ]] local is_equal -- defined here to allow calling from mismatchFormattingPureList ---------------------------------------------------------------- -- -- general utility functions -- ---------------------------------------------------------------- local function pcall_or_abort(func, ...) -- unpack is a global function for Lua 5.1, otherwise use table.unpack local unpack = rawget(_G, "unpack") or table.unpack local result = {pcall(func, ...)} if not result[1] then -- an error occurred env.info(result[2]) -- error message env.info() env.info(M.USAGE) -- os.exit(-1) end return unpack(result, 2) end local crossTypeOrdering = { number = 1, boolean = 2, string = 3, table = 4, other = 5 } local crossTypeComparison = { number = function(a, b) return a < b end, string = function(a, b) return a < b end, other = function(a, b) return tostring(a) < tostring(b) end, } local function crossTypeSort(a, b) local type_a, type_b = type(a), type(b) if type_a == type_b then local func = crossTypeComparison[type_a] or crossTypeComparison.other return func(a, b) end type_a = crossTypeOrdering[type_a] or crossTypeOrdering.other type_b = crossTypeOrdering[type_b] or crossTypeOrdering.other return type_a < type_b end local function __genSortedIndex( t ) -- Returns a sequence consisting of t's keys, sorted. local sortedIndex = {} for key,_ in pairs(t) do table.insert(sortedIndex, key) end table.sort(sortedIndex, crossTypeSort) return sortedIndex end M.private.__genSortedIndex = __genSortedIndex local function sortedNext(state, control) -- Equivalent of the next() function of table iteration, but returns the -- keys in sorted order (see __genSortedIndex and crossTypeSort). -- The state is a temporary variable during iteration and contains the -- sorted key table (state.sortedIdx). It also stores the last index (into -- the keys) used by the iteration, to find the next one quickly. local key --env.info("sortedNext: control = "..tostring(control) ) if control == nil then -- start of iteration state.count = #state.sortedIdx state.lastIdx = 1 key = state.sortedIdx[1] return key, state.t[key] end -- normally, we expect the control variable to match the last key used if control ~= state.sortedIdx[state.lastIdx] then -- strange, we have to find the next value by ourselves -- the key table is sorted in crossTypeSort() order! -> use bisection local lower, upper = 1, state.count repeat state.lastIdx = math.modf((lower + upper) / 2) key = state.sortedIdx[state.lastIdx] if key == control then break -- key found (and thus prev index) end if crossTypeSort(key, control) then -- key < control, continue search "right" (towards upper bound) lower = state.lastIdx + 1 else -- key > control, continue search "left" (towards lower bound) upper = state.lastIdx - 1 end until lower > upper if lower > upper then -- only true if the key wasn't found, ... state.lastIdx = state.count -- ... so ensure no match in code below end end -- proceed by retrieving the next value (or nil) from the sorted keys state.lastIdx = state.lastIdx + 1 key = state.sortedIdx[state.lastIdx] if key then return key, state.t[key] end -- getting here means returning `nil`, which will end the iteration end local function sortedPairs(tbl) -- Equivalent of the pairs() function on tables. Allows to iterate in -- sorted order. As required by "generic for" loops, this will return the -- iterator (function), an "invariant state", and the initial control value. -- (see http://www.lua.org/pil/7.2.html) return sortedNext, {t = tbl, sortedIdx = __genSortedIndex(tbl)}, nil end M.private.sortedPairs = sortedPairs -- seed the random with a strongly varying seed --math.randomseed(os.clock()*1E11) local function randomizeTable( t ) -- randomize the item orders of the table t for i = #t, 2, -1 do local j = math.random(i) if i ~= j then t[i], t[j] = t[j], t[i] end end end M.private.randomizeTable = randomizeTable local function strsplit(delimiter, text) -- Split text into a list consisting of the strings in text, separated -- by strings matching delimiter (which may _NOT_ be a pattern). -- Example: strsplit(", ", "Anna, Bob, Charlie, Dolores") if delimiter == "" or delimiter == nil then -- this would result in endless loops error("delimiter is nil or empty string!") end if text == nil then return nil end local list, pos, first, last = {}, 1 while true do first, last = text:find(delimiter, pos, true) if first then -- found? table.insert(list, text:sub(pos, first - 1)) pos = last + 1 else table.insert(list, text:sub(pos)) break end end return list end M.private.strsplit = strsplit local function hasNewLine( s ) -- return true if s has a newline return (string.find(s, '\n', 1, true) ~= nil) end M.private.hasNewLine = hasNewLine local function prefixString( prefix, s ) -- Prefix all the lines of s with prefix return prefix .. string.gsub(s, '\n', '\n' .. prefix) end M.private.prefixString = prefixString local function strMatch(s, pattern, start, final ) -- return true if s matches completely the pattern from index start to index end -- return false in every other cases -- if start is nil, matches from the beginning of the string -- if final is nil, matches to the end of the string start = start or 1 final = final or string.len(s) local foundStart, foundEnd = string.find(s, pattern, start, false) return foundStart == start and foundEnd == final end M.private.strMatch = strMatch local function patternFilter(patterns, expr) -- Run `expr` through the inclusion and exclusion rules defined in patterns -- and return true if expr shall be included, false for excluded. -- Inclusion pattern are defined as normal patterns, exclusions -- patterns start with `!` and are followed by a normal pattern -- result: nil = UNKNOWN (not matched yet), true = ACCEPT, false = REJECT -- default: true if no explicit "include" is found, set to false otherwise local default, result = true, nil if patterns ~= nil then for _, pattern in ipairs(patterns) do local exclude = pattern:sub(1,1) == '!' if exclude then pattern = pattern:sub(2) else -- at least one include pattern specified, a match is required default = false end -- env.info('pattern: ',pattern) -- env.info('exclude: ',exclude) -- env.info('default: ',default) if string.find(expr, pattern) then -- set result to false when excluding, true otherwise result = not exclude end end end if result ~= nil then return result end return default end M.private.patternFilter = patternFilter local function xmlEscape( s ) -- Return s escaped for XML attributes -- escapes table: -- " " -- ' ' -- < < -- > > -- & & return string.gsub( s, '.', { ['&'] = "&", ['"'] = """, ["'"] = "'", ['<'] = "<", ['>'] = ">", } ) end M.private.xmlEscape = xmlEscape local function xmlCDataEscape( s ) -- Return s escaped for CData section, escapes: "]]>" return string.gsub( s, ']]>', ']]>' ) end M.private.xmlCDataEscape = xmlCDataEscape local function stripLuaunitTrace( stackTrace ) --[[ -- Example of a traceback: < [C]: in function 'xpcall' ./luaunit.lua:1449: in function 'protectedCall' ./luaunit.lua:1508: in function 'execOneFunction' ./luaunit.lua:1596: in function 'runSuiteByInstances' ./luaunit.lua:1660: in function 'runSuiteByNames' ./luaunit.lua:1736: in function 'runSuite' example_with_luaunit.lua:140: in main chunk [C]: in ?>> Other example: < [C]: in function 'xpcall' ./luaunit.lua:1517: in function 'protectedCall' ./luaunit.lua:1578: in function 'execOneFunction' ./luaunit.lua:1677: in function 'runSuiteByInstances' ./luaunit.lua:1730: in function 'runSuiteByNames' ./luaunit.lua:1806: in function 'runSuite' example_with_luaunit.lua:140: in main chunk [C]: in ?>> < [C]: in function 'xpcall' luaunit2/luaunit.lua:1532: in function 'protectedCall' luaunit2/luaunit.lua:1591: in function 'execOneFunction' luaunit2/luaunit.lua:1679: in function 'runSuiteByInstances' luaunit2/luaunit.lua:1743: in function 'runSuiteByNames' luaunit2/luaunit.lua:1819: in function 'runSuite' luaunit2/example_with_luaunit.lua:140: in main chunk [C]: in ?>> -- first line is "stack traceback": KEEP -- next line may be luaunit line: REMOVE -- next lines are call in the program under testOk: REMOVE -- next lines are calls from luaunit to call the program under test: KEEP -- Strategy: -- keep first line -- remove lines that are part of luaunit -- kepp lines until we hit a luaunit line ]] local function isLuaunitInternalLine( s ) -- return true if line of stack trace comes from inside luaunit return s:find('[/\\]luaunit%.lua:%d+: ') ~= nil end -- env.info( '<<'..stackTrace..'>>' ) local t = strsplit( '\n', stackTrace ) -- env.info( prettystr(t) ) local idx = 2 -- remove lines that are still part of luaunit while t[idx] and isLuaunitInternalLine( t[idx] ) do -- env.info('Removing : '..t[idx] ) table.remove(t, idx) end -- keep lines until we hit luaunit again while t[idx] and (not isLuaunitInternalLine(t[idx])) do -- env.info('Keeping : '..t[idx] ) idx = idx + 1 end -- remove remaining luaunit lines while t[idx] do -- env.info('Removing : '..t[idx] ) table.remove(t, idx) end -- env.info( prettystr(t) ) return table.concat( t, '\n') end M.private.stripLuaunitTrace = stripLuaunitTrace local function prettystr_sub(v, indentLevel, printTableRefs, cycleDetectTable ) local type_v = type(v) if "string" == type_v then -- use clever delimiters according to content: -- enclose with single quotes if string contains ", but no ' if v:find('"', 1, true) and not v:find("'", 1, true) then return "'" .. v .. "'" end -- use double quotes otherwise, escape embedded " return '"' .. v:gsub('"', '\\"') .. '"' elseif "table" == type_v then --if v.__class__ then -- return string.gsub( tostring(v), 'table', v.__class__ ) --end return M.private._table_tostring(v, indentLevel, printTableRefs, cycleDetectTable) elseif "number" == type_v then -- eliminate differences in formatting between various Lua versions if v ~= v then return "#NaN" -- "not a number" end if v == math.huge then return "#Inf" -- "infinite" end if v == -math.huge then return "-#Inf" end if _VERSION == "Lua 5.3" then local i = math.tointeger(v) if i then return tostring(i) end end end return tostring(v) end local function prettystr( v ) --[[ Pretty string conversion, to display the full content of a variable of any type. * string are enclosed with " by default, or with ' if string contains a " * tables are expanded to show their full content, with indentation in case of nested tables ]]-- local cycleDetectTable = {} local s = prettystr_sub(v, 1, M.PRINT_TABLE_REF_IN_ERROR_MSG, cycleDetectTable) if cycleDetectTable.detected and not M.PRINT_TABLE_REF_IN_ERROR_MSG then -- some table contain recursive references, -- so we must recompute the value by including all table references -- else the result looks like crap cycleDetectTable = {} s = prettystr_sub(v, 1, true, cycleDetectTable) end return s end M.prettystr = prettystr function M.adjust_err_msg_with_iter( err_msg, iter_msg ) --[[ Adjust the error message err_msg: trim the FAILURE_PREFIX or SUCCESS_PREFIX information if needed, add the iteration message if any and return the result. err_msg: string, error message captured with pcall iter_msg: a string describing the current iteration ("iteration N") or nil if there is no iteration in this test. Returns: (new_err_msg, test_status) new_err_msg: string, adjusted error message, or nil in case of success test_status: M.NodeStatus.FAIL, SUCCESS or ERROR according to the information contained in the error message. ]] if iter_msg then iter_msg = iter_msg..', ' else iter_msg = '' end local RE_FILE_LINE = '.*:%d+: ' -- error message is not necessarily a string, -- so convert the value to string with prettystr() if type( err_msg ) ~= 'string' then err_msg = prettystr( err_msg ) end if (err_msg:find( M.SUCCESS_PREFIX ) == 1) or err_msg:match( '('..RE_FILE_LINE..')' .. M.SUCCESS_PREFIX .. ".*" ) then -- test finished early with success() return nil, M.NodeStatus.SUCCESS end if (err_msg:find( M.SKIP_PREFIX ) == 1) or (err_msg:match( '('..RE_FILE_LINE..')' .. M.SKIP_PREFIX .. ".*" ) ~= nil) then -- substitute prefix by iteration message err_msg = err_msg:gsub('.*'..M.SKIP_PREFIX, iter_msg, 1) -- env.info("failure detected") return err_msg, M.NodeStatus.SKIP end if (err_msg:find( M.FAILURE_PREFIX ) == 1) or (err_msg:match( '('..RE_FILE_LINE..')' .. M.FAILURE_PREFIX .. ".*" ) ~= nil) then -- substitute prefix by iteration message err_msg = err_msg:gsub(M.FAILURE_PREFIX, iter_msg, 1) -- env.info("failure detected") return err_msg, M.NodeStatus.FAIL end -- env.info("error detected") -- regular error, not a failure if iter_msg then local match -- "./test\\test_luaunit.lua:2241: some error msg match = err_msg:match( '(.*:%d+: ).*' ) if match then err_msg = err_msg:gsub( match, match .. iter_msg ) else -- no file:line: infromation, just add the iteration info at the beginning of the line err_msg = iter_msg .. err_msg end end return err_msg, M.NodeStatus.ERROR end local function tryMismatchFormatting( table_a, table_b, doDeepAnalysis ) --[[ Prepares a nice error message when comparing tables, performing a deeper analysis. Arguments: * table_a, table_b: tables to be compared * doDeepAnalysis: M.DEFAULT_DEEP_ANALYSIS: (the default if not specified) perform deep analysis only for big lists and big dictionnaries M.FORCE_DEEP_ANALYSIS : always perform deep analysis M.DISABLE_DEEP_ANALYSIS: never perform deep analysis Returns: {success, result} * success: false if deep analysis could not be performed in this case, just use standard assertion message * result: if success is true, a multi-line string with deep analysis of the two lists ]] -- check if table_a & table_b are suitable for deep analysis if type(table_a) ~= 'table' or type(table_b) ~= 'table' then return false end if doDeepAnalysis == M.DISABLE_DEEP_ANALYSIS then return false end local len_a, len_b, isPureList = #table_a, #table_b, true for k1, v1 in pairs(table_a) do if type(k1) ~= 'number' or k1 > len_a then -- this table a mapping isPureList = false break end end if isPureList then for k2, v2 in pairs(table_b) do if type(k2) ~= 'number' or k2 > len_b then -- this table a mapping isPureList = false break end end end if isPureList and math.min(len_a, len_b) < M.LIST_DIFF_ANALYSIS_THRESHOLD then if not (doDeepAnalysis == M.FORCE_DEEP_ANALYSIS) then return false end end if isPureList then return M.private.mismatchFormattingPureList( table_a, table_b ) else -- only work on mapping for the moment -- return M.private.mismatchFormattingMapping( table_a, table_b, doDeepAnalysis ) return false end end M.private.tryMismatchFormatting = tryMismatchFormatting local function getTaTbDescr() if not M.ORDER_ACTUAL_EXPECTED then return 'expected', 'actual' end return 'actual', 'expected' end local function extendWithStrFmt( res, ... ) table.insert( res, string.format( ... ) ) end local function mismatchFormattingMapping( table_a, table_b, doDeepAnalysis ) --[[ Prepares a nice error message when comparing tables which are not pure lists, performing a deeper analysis. Returns: {success, result} * success: false if deep analysis could not be performed in this case, just use standard assertion message * result: if success is true, a multi-line string with deep analysis of the two lists ]] -- disable for the moment --[[ local result = {} local descrTa, descrTb = getTaTbDescr() local keysCommon = {} local keysOnlyTa = {} local keysOnlyTb = {} local keysDiffTaTb = {} local k, v for k,v in pairs( table_a ) do if is_equal( v, table_b[k] ) then table.insert( keysCommon, k ) else if table_b[k] == nil then table.insert( keysOnlyTa, k ) else table.insert( keysDiffTaTb, k ) end end end for k,v in pairs( table_b ) do if not is_equal( v, table_a[k] ) and table_a[k] == nil then table.insert( keysOnlyTb, k ) end end local len_a = #keysCommon + #keysDiffTaTb + #keysOnlyTa local len_b = #keysCommon + #keysDiffTaTb + #keysOnlyTb local limited_display = (len_a < 5 or len_b < 5) if math.min(len_a, len_b) < M.TABLE_DIFF_ANALYSIS_THRESHOLD then return false end if not limited_display then if len_a == len_b then extendWithStrFmt( result, 'Table A (%s) and B (%s) both have %d items', descrTa, descrTb, len_a ) else extendWithStrFmt( result, 'Table A (%s) has %d items and table B (%s) has %d items', descrTa, len_a, descrTb, len_b ) end if #keysCommon == 0 and #keysDiffTaTb == 0 then table.insert( result, 'Table A and B have no keys in common, they are totally different') else local s_other = 'other ' if #keysCommon then extendWithStrFmt( result, 'Table A and B have %d identical items', #keysCommon ) else table.insert( result, 'Table A and B have no identical items' ) s_other = '' end if #keysDiffTaTb ~= 0 then result[#result] = string.format( '%s and %d items differing present in both tables', result[#result], #keysDiffTaTb) else result[#result] = string.format( '%s and no %sitems differing present in both tables', result[#result], s_other, #keysDiffTaTb) end end extendWithStrFmt( result, 'Table A has %d keys not present in table B and table B has %d keys not present in table A', #keysOnlyTa, #keysOnlyTb ) end local function keytostring(k) if "string" == type(k) and k:match("^[_%a][_%w]*$") then return k end return prettystr(k) end if #keysDiffTaTb ~= 0 then table.insert( result, 'Items differing in A and B:') for k,v in sortedPairs( keysDiffTaTb ) do extendWithStrFmt( result, ' - A[%s]: %s', keytostring(v), prettystr(table_a[v]) ) extendWithStrFmt( result, ' + B[%s]: %s', keytostring(v), prettystr(table_b[v]) ) end end if #keysOnlyTa ~= 0 then table.insert( result, 'Items only in table A:' ) for k,v in sortedPairs( keysOnlyTa ) do extendWithStrFmt( result, ' - A[%s]: %s', keytostring(v), prettystr(table_a[v]) ) end end if #keysOnlyTb ~= 0 then table.insert( result, 'Items only in table B:' ) for k,v in sortedPairs( keysOnlyTb ) do extendWithStrFmt( result, ' + B[%s]: %s', keytostring(v), prettystr(table_b[v]) ) end end if #keysCommon ~= 0 then table.insert( result, 'Items common to A and B:') for k,v in sortedPairs( keysCommon ) do extendWithStrFmt( result, ' = A and B [%s]: %s', keytostring(v), prettystr(table_a[v]) ) end end return true, table.concat( result, '\n') ]] end M.private.mismatchFormattingMapping = mismatchFormattingMapping local function mismatchFormattingPureList( table_a, table_b ) --[[ Prepares a nice error message when comparing tables which are lists, performing a deeper analysis. Returns: {success, result} * success: false if deep analysis could not be performed in this case, just use standard assertion message * result: if success is true, a multi-line string with deep analysis of the two lists ]] local result, descrTa, descrTb = {}, getTaTbDescr() local len_a, len_b, refa, refb = #table_a, #table_b, '', '' if M.PRINT_TABLE_REF_IN_ERROR_MSG then refa, refb = string.format( '<%s> ', M.private.table_ref(table_a)), string.format('<%s> ', M.private.table_ref(table_b) ) end local longest, shortest = math.max(len_a, len_b), math.min(len_a, len_b) local deltalv = longest - shortest local commonUntil = shortest for i = 1, shortest do if not is_equal(table_a[i], table_b[i]) then commonUntil = i - 1 break end end local commonBackTo = shortest - 1 for i = 0, shortest - 1 do if not is_equal(table_a[len_a-i], table_b[len_b-i]) then commonBackTo = i - 1 break end end table.insert( result, 'List difference analysis:' ) if len_a == len_b then -- TODO: handle expected/actual naming extendWithStrFmt( result, '* lists %sA (%s) and %sB (%s) have the same size', refa, descrTa, refb, descrTb ) else extendWithStrFmt( result, '* list sizes differ: list %sA (%s) has %d items, list %sB (%s) has %d items', refa, descrTa, len_a, refb, descrTb, len_b ) end extendWithStrFmt( result, '* lists A and B start differing at index %d', commonUntil+1 ) if commonBackTo >= 0 then if deltalv > 0 then extendWithStrFmt( result, '* lists A and B are equal again from index %d for A, %d for B', len_a-commonBackTo, len_b-commonBackTo ) else extendWithStrFmt( result, '* lists A and B are equal again from index %d', len_a-commonBackTo ) end end local function insertABValue(ai, bi) bi = bi or ai if is_equal( table_a[ai], table_b[bi]) then return extendWithStrFmt( result, ' = A[%d], B[%d]: %s', ai, bi, prettystr(table_a[ai]) ) else extendWithStrFmt( result, ' - A[%d]: %s', ai, prettystr(table_a[ai])) extendWithStrFmt( result, ' + B[%d]: %s', bi, prettystr(table_b[bi])) end end -- common parts to list A & B, at the beginning if commonUntil > 0 then table.insert( result, '* Common parts:' ) for i = 1, commonUntil do insertABValue( i ) end end -- diffing parts to list A & B if commonUntil < shortest - commonBackTo - 1 then table.insert( result, '* Differing parts:' ) for i = commonUntil + 1, shortest - commonBackTo - 1 do insertABValue( i ) end end -- display indexes of one list, with no match on other list if shortest - commonBackTo <= longest - commonBackTo - 1 then table.insert( result, '* Present only in one list:' ) for i = shortest - commonBackTo, longest - commonBackTo - 1 do if len_a > len_b then extendWithStrFmt( result, ' - A[%d]: %s', i, prettystr(table_a[i]) ) -- table.insert( result, '+ (no matching B index)') else -- table.insert( result, '- no matching A index') extendWithStrFmt( result, ' + B[%d]: %s', i, prettystr(table_b[i]) ) end end end -- common parts to list A & B, at the end if commonBackTo >= 0 then table.insert( result, '* Common parts at the end of the lists' ) for i = longest - commonBackTo, longest do if len_a > len_b then insertABValue( i, i-deltalv ) else insertABValue( i-deltalv, i ) end end end return true, table.concat( result, '\n') end M.private.mismatchFormattingPureList = mismatchFormattingPureList local function prettystrPairs(value1, value2, suffix_a, suffix_b) --[[ This function helps with the recurring task of constructing the "expected vs. actual" error messages. It takes two arbitrary values and formats corresponding strings with prettystr(). To keep the (possibly complex) output more readable in case the resulting strings contain line breaks, they get automatically prefixed with additional newlines. Both suffixes are optional (default to empty strings), and get appended to the "value1" string. "suffix_a" is used if line breaks were encountered, "suffix_b" otherwise. Returns the two formatted strings (including padding/newlines). ]] local str1, str2 = prettystr(value1), prettystr(value2) if hasNewLine(str1) or hasNewLine(str2) then -- line break(s) detected, add padding return "\n" .. str1 .. (suffix_a or ""), "\n" .. str2 end return str1 .. (suffix_b or ""), str2 end M.private.prettystrPairs = prettystrPairs local UNKNOWN_REF = 'table 00-unknown ref' local ref_generator = { value=1, [UNKNOWN_REF]=0 } local function table_ref( t ) -- return the default tostring() for tables, with the table ID, even if the table has a metatable -- with the __tostring converter local ref = '' local mt = getmetatable( t ) if mt == nil then ref = tostring(t) else local success, result success, result = pcall(setmetatable, t, nil) if not success then -- protected table, if __tostring is defined, we can -- not get the reference. And we can not know in advance. ref = tostring(t) if not ref:match( 'table: 0?x?[%x]+' ) then return UNKNOWN_REF end else ref = tostring(t) setmetatable( t, mt ) end end -- strip the "table: " part ref = ref:sub(8) if ref ~= UNKNOWN_REF and ref_generator[ref] == nil then -- Create a new reference number ref_generator[ref] = ref_generator.value ref_generator.value = ref_generator.value+1 end if M.PRINT_TABLE_REF_IN_ERROR_MSG then return string.format('table %02d-%s', ref_generator[ref], ref) else return string.format('table %02d', ref_generator[ref]) end end M.private.table_ref = table_ref local TABLE_TOSTRING_SEP = ", " local TABLE_TOSTRING_SEP_LEN = string.len(TABLE_TOSTRING_SEP) local function _table_tostring( tbl, indentLevel, printTableRefs, cycleDetectTable ) printTableRefs = printTableRefs or M.PRINT_TABLE_REF_IN_ERROR_MSG cycleDetectTable = cycleDetectTable or {} cycleDetectTable[tbl] = true local result, dispOnMultLines = {}, false -- like prettystr but do not enclose with "" if the string is just alphanumerical -- this is better for displaying table keys who are often simple strings local function keytostring(k) if "string" == type(k) and k:match("^[_%a][_%w]*$") then return k end return prettystr_sub(k, indentLevel+1, printTableRefs, cycleDetectTable) end local mt = getmetatable( tbl ) if mt and mt.__tostring then -- if table has a __tostring() function in its metatable, use it to display the table -- else, compute a regular table result = tostring(tbl) if type(result) ~= 'string' then return string.format( '', prettystr(result) ) end result = strsplit( '\n', result ) return M.private._table_tostring_format_multiline_string( result, indentLevel ) else -- no metatable, compute the table representation local entry, count, seq_index = nil, 0, 1 for k, v in sortedPairs( tbl ) do -- key part if k == seq_index then -- for the sequential part of tables, we'll skip the "=" output entry = '' seq_index = seq_index + 1 elseif cycleDetectTable[k] then -- recursion in the key detected cycleDetectTable.detected = true entry = "<"..table_ref(k)..">=" else entry = keytostring(k) .. "=" end -- value part if cycleDetectTable[v] then -- recursion in the value detected! cycleDetectTable.detected = true entry = entry .. "<"..table_ref(v)..">" else entry = entry .. prettystr_sub( v, indentLevel+1, printTableRefs, cycleDetectTable ) end count = count + 1 result[count] = entry end return M.private._table_tostring_format_result( tbl, result, indentLevel, printTableRefs ) end end M.private._table_tostring = _table_tostring -- prettystr_sub() needs it local function _table_tostring_format_multiline_string( tbl_str, indentLevel ) local indentString = '\n'..string.rep(" ", indentLevel - 1) return table.concat( tbl_str, indentString ) end M.private._table_tostring_format_multiline_string = _table_tostring_format_multiline_string local function _table_tostring_format_result( tbl, result, indentLevel, printTableRefs ) -- final function called in _table_to_string() to format the resulting list of -- string describing the table. local dispOnMultLines = false -- set dispOnMultLines to true if the maximum LINE_LENGTH would be exceeded with the values local totalLength = 0 for k, v in ipairs( result ) do totalLength = totalLength + string.len( v ) if totalLength >= M.LINE_LENGTH then dispOnMultLines = true break end end -- set dispOnMultLines to true if the max LINE_LENGTH would be exceeded -- with the values and the separators. if not dispOnMultLines then -- adjust with length of separator(s): -- two items need 1 sep, three items two seps, ... plus len of '{}' if #result > 0 then totalLength = totalLength + TABLE_TOSTRING_SEP_LEN * (#result - 1) end dispOnMultLines = (totalLength + 2 >= M.LINE_LENGTH) end -- now reformat the result table (currently holding element strings) if dispOnMultLines then local indentString = string.rep(" ", indentLevel - 1) result = { "{\n ", indentString, table.concat(result, ",\n " .. indentString), "\n", indentString, "}" } else result = {"{", table.concat(result, TABLE_TOSTRING_SEP), "}"} end if printTableRefs then table.insert(result, 1, "<"..table_ref(tbl).."> ") -- prepend table ref end return table.concat(result) end M.private._table_tostring_format_result = _table_tostring_format_result -- prettystr_sub() needs it local function table_findkeyof(t, element) -- Return the key k of the given element in table t, so that t[k] == element -- (or `nil` if element is not present within t). Note that we use our -- 'general' is_equal comparison for matching, so this function should -- handle table-type elements gracefully and consistently. if type(t) == "table" then for k, v in pairs(t) do if is_equal(v, element) then return k end end end return nil end local function _is_table_items_equals(actual, expected ) local type_a, type_e = type(actual), type(expected) if type_a ~= type_e then return false elseif (type_a == 'table') --[[and (type_e == 'table')]] then for k, v in pairs(actual) do if table_findkeyof(expected, v) == nil then return false -- v not contained in expected end end for k, v in pairs(expected) do if table_findkeyof(actual, v) == nil then return false -- v not contained in actual end end return true elseif actual ~= expected then return false end return true end --[[ This is a specialized metatable to help with the bookkeeping of recursions in _is_table_equals(). It provides an __index table that implements utility functions for easier management of the table. The "cached" method queries the state of a specific (actual,expected) pair; and the "store" method sets this state to the given value. The state of pairs not "seen" / visited is assumed to be `nil`. ]] local _recursion_cache_MT = { __index = { -- Return the cached value for an (actual,expected) pair (or `nil`) cached = function(t, actual, expected) local subtable = t[actual] or {} return subtable[expected] end, -- Store cached value for a specific (actual,expected) pair. -- Returns the value, so it's easy to use for a "tailcall" (return ...). store = function(t, actual, expected, value, asymmetric) local subtable = t[actual] if not subtable then subtable = {} t[actual] = subtable end subtable[expected] = value -- Unless explicitly marked "asymmetric": Consider the recursion -- on (expected,actual) to be equivalent to (actual,expected) by -- default, and thus cache the value for both. if not asymmetric then t:store(expected, actual, value, true) end return value end } } local function _is_table_equals(actual, expected, cycleDetectTable) local type_a, type_e = type(actual), type(expected) if type_a ~= type_e then return false -- different types won't match end if type_a ~= 'table' then -- other typtes compare directly return actual == expected end -- env.info('_is_table_equals( \n '..prettystr(actual)..'\n , '..prettystr(expected)..'\n , '..prettystr(recursions)..' \n )') cycleDetectTable = cycleDetectTable or { actual={}, expected={} } if cycleDetectTable.actual[ actual ] then -- oh, we hit a cycle in actual if cycleDetectTable.expected[ expected ] then -- uh, we hit a cycle at the same time in expected -- so the two tables have similar structure return true end -- cycle was hit only in actual, the structure differs from expected return false end if cycleDetectTable.expected[ expected ] then -- no cycle in actual, but cycle in expected -- the structure differ return false end -- at this point, no table cycle detected, we are -- seeing this table for the first time -- mark the cycle detection cycleDetectTable.actual[ actual ] = true cycleDetectTable.expected[ expected ] = true local actualKeysMatched = {} for k, v in pairs(actual) do actualKeysMatched[k] = true -- Keep track of matched keys if not _is_table_equals(v, expected[k], cycleDetectTable) then -- table differs on this key -- clear the cycle detection before returning cycleDetectTable.actual[ actual ] = nil cycleDetectTable.expected[ expected ] = nil return false end end for k, v in pairs(expected) do if not actualKeysMatched[k] then -- Found a key that we did not see in "actual" -> mismatch -- clear the cycle detection before returning cycleDetectTable.actual[ actual ] = nil cycleDetectTable.expected[ expected ] = nil return false end -- Otherwise actual[k] was already matched against v = expected[k]. end -- all key match, we have a match ! cycleDetectTable.actual[ actual ] = nil cycleDetectTable.expected[ expected ] = nil return true end M.private._is_table_equals = _is_table_equals is_equal = _is_table_equals local function failure(main_msg, extra_msg_or_nil, level) -- raise an error indicating a test failure -- for error() compatibility we adjust "level" here (by +1), to report the -- calling context local msg if type(extra_msg_or_nil) == 'string' and extra_msg_or_nil:len() > 0 then msg = extra_msg_or_nil .. '\n' .. main_msg else msg = main_msg end error(M.FAILURE_PREFIX .. msg, (level or 1) + 1) end local function fail_fmt(level, extra_msg_or_nil, ...) -- failure with printf-style formatted message and given error level failure(string.format(...), extra_msg_or_nil, (level or 1) + 1) end M.private.fail_fmt = fail_fmt local function error_fmt(level, ...) -- printf-style error() error(string.format(...), (level or 1) + 1) end ---------------------------------------------------------------- -- -- assertions -- ---------------------------------------------------------------- local function errorMsgEquality(actual, expected, doDeepAnalysis) if not M.ORDER_ACTUAL_EXPECTED then expected, actual = actual, expected end if type(expected) == 'string' or type(expected) == 'table' then local strExpected, strActual = prettystrPairs(expected, actual) local result = string.format("expected: %s\nactual: %s", strExpected, strActual) -- extend with mismatch analysis if possible: local success, mismatchResult success, mismatchResult = tryMismatchFormatting( actual, expected, doDeepAnalysis ) if success then result = table.concat( { result, mismatchResult }, '\n' ) end return result end return string.format("expected: %s, actual: %s", prettystr(expected), prettystr(actual)) end function M.assertError(f, ...) -- assert that calling f with the arguments will raise an error -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error if pcall( f, ... ) then failure( "Expected an error when calling function but no error generated", nil, 2 ) end end function M.fail( msg ) -- stops a test due to a failure failure( msg, nil, 2 ) end function M.failIf( cond, msg ) -- Fails a test with "msg" if condition is true if cond then failure( msg, nil, 2 ) end end function M.skip(msg) -- skip a running test error(M.SKIP_PREFIX .. msg, 2) end function M.skipIf( cond, msg ) -- skip a running test if condition is met if cond then error(M.SKIP_PREFIX .. msg, 2) end end function M.runOnlyIf( cond, msg ) -- continue a running test if condition is met, else skip it if not cond then error(M.SKIP_PREFIX .. prettystr(msg), 2) end end function M.success() -- stops a test with a success error(M.SUCCESS_PREFIX, 2) end function M.successIf( cond ) -- stops a test with a success if condition is met if cond then error(M.SUCCESS_PREFIX, 2) end end ------------------------------------------------------------------ -- Equality assertions ------------------------------------------------------------------ function M.assertEquals(actual, expected, extra_msg_or_nil, doDeepAnalysis) if type(actual) == 'table' and type(expected) == 'table' then if not _is_table_equals(actual, expected) then failure( errorMsgEquality(actual, expected, doDeepAnalysis), extra_msg_or_nil, 2 ) end elseif type(actual) ~= type(expected) then failure( errorMsgEquality(actual, expected), extra_msg_or_nil, 2 ) elseif actual ~= expected then failure( errorMsgEquality(actual, expected), extra_msg_or_nil, 2 ) end end function M.almostEquals( actual, expected, margin ) if type(actual) ~= 'number' or type(expected) ~= 'number' or type(margin) ~= 'number' then error_fmt(3, 'almostEquals: must supply only number arguments.\nArguments supplied: %s, %s, %s', prettystr(actual), prettystr(expected), prettystr(margin)) end if margin < 0 then error('almostEquals: margin must not be negative, current value is ' .. margin, 3) end return math.abs(expected - actual) <= margin end function M.assertAlmostEquals( actual, expected, margin, extra_msg_or_nil ) -- check that two floats are close by margin margin = margin or M.EPS if not M.almostEquals(actual, expected, margin) then if not M.ORDER_ACTUAL_EXPECTED then expected, actual = actual, expected end local delta = math.abs(actual - expected) fail_fmt(2, extra_msg_or_nil, 'Values are not almost equal\n' .. 'Actual: %s, expected: %s, delta %s above margin of %s', actual, expected, delta, margin) end end function M.assertNotEquals(actual, expected, extra_msg_or_nil) if type(actual) ~= type(expected) then return end if type(actual) == 'table' and type(expected) == 'table' then if not _is_table_equals(actual, expected) then return end elseif actual ~= expected then return end fail_fmt(2, extra_msg_or_nil, 'Received the not expected value: %s', prettystr(actual)) end function M.assertNotAlmostEquals( actual, expected, margin, extra_msg_or_nil ) -- check that two floats are not close by margin margin = margin or M.EPS if M.almostEquals(actual, expected, margin) then if not M.ORDER_ACTUAL_EXPECTED then expected, actual = actual, expected end local delta = math.abs(actual - expected) fail_fmt(2, extra_msg_or_nil, 'Values are almost equal\nActual: %s, expected: %s' .. ', delta %s below margin of %s', actual, expected, delta, margin) end end function M.assertItemsEquals(actual, expected, extra_msg_or_nil) -- checks that the items of table expected -- are contained in table actual. Warning, this function -- is at least O(n^2) if not _is_table_items_equals(actual, expected ) then expected, actual = prettystrPairs(expected, actual) fail_fmt(2, extra_msg_or_nil, 'Content of the tables are not identical:\nExpected: %s\nActual: %s', expected, actual) end end ------------------------------------------------------------------ -- String assertion ------------------------------------------------------------------ function M.assertStrContains( str, sub, isPattern, extra_msg_or_nil ) -- this relies on lua string.find function -- a string always contains the empty string -- assert( type(str) == 'string', 'Argument 1 of assertStrContains() should be a string.' ) ) -- assert( type(sub) == 'string', 'Argument 2 of assertStrContains() should be a string.' ) ) if not string.find(str, sub, 1, not isPattern) then sub, str = prettystrPairs(sub, str, '\n') fail_fmt(2, extra_msg_or_nil, 'Could not find %s %s in string %s', isPattern and 'pattern' or 'substring', sub, str) end end function M.assertStrIContains( str, sub, extra_msg_or_nil ) -- this relies on lua string.find function -- a string always contains the empty string if not string.find(str:lower(), sub:lower(), 1, true) then sub, str = prettystrPairs(sub, str, '\n') fail_fmt(2, extra_msg_or_nil, 'Could not find (case insensitively) substring %s in string %s', sub, str) end end function M.assertNotStrContains( str, sub, isPattern, extra_msg_or_nil ) -- this relies on lua string.find function -- a string always contains the empty string if string.find(str, sub, 1, not isPattern) then sub, str = prettystrPairs(sub, str, '\n') fail_fmt(2, extra_msg_or_nil, 'Found the not expected %s %s in string %s', isPattern and 'pattern' or 'substring', sub, str) end end function M.assertNotStrIContains( str, sub, extra_msg_or_nil ) -- this relies on lua string.find function -- a string always contains the empty string if string.find(str:lower(), sub:lower(), 1, true) then sub, str = prettystrPairs(sub, str, '\n') fail_fmt(2, extra_msg_or_nil, 'Found (case insensitively) the not expected substring %s in string %s', sub, str) end end function M.assertStrMatches( str, pattern, start, final, extra_msg_or_nil ) -- Verify a full match for the string if not strMatch( str, pattern, start, final ) then pattern, str = prettystrPairs(pattern, str, '\n') fail_fmt(2, extra_msg_or_nil, 'Could not match pattern %s with string %s', pattern, str) end end local function _assertErrorMsgEquals( stripFileAndLine, expectedMsg, func, ... ) local no_error, error_msg = pcall( func, ... ) if no_error then failure( 'No error generated when calling function but expected error: '..M.prettystr(expectedMsg), nil, 3 ) end if type(expectedMsg) == "string" and type(error_msg) ~= "string" then -- table are converted to string automatically error_msg = tostring(error_msg) end local differ = false if stripFileAndLine then if error_msg:gsub("^.+:%d+: ", "") ~= expectedMsg then differ = true end else if error_msg ~= expectedMsg then local tr = type(error_msg) local te = type(expectedMsg) if te == 'table' then if tr ~= 'table' then differ = true else local ok = pcall(M.assertItemsEquals, error_msg, expectedMsg) if not ok then differ = true end end else differ = true end end end if differ then error_msg, expectedMsg = prettystrPairs(error_msg, expectedMsg) fail_fmt(3, nil, 'Error message expected: %s\nError message received: %s\n', expectedMsg, error_msg) end end function M.assertErrorMsgEquals( expectedMsg, func, ... ) -- assert that calling f with the arguments will raise an error -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error _assertErrorMsgEquals(false, expectedMsg, func, ...) end function M.assertErrorMsgContentEquals(expectedMsg, func, ...) _assertErrorMsgEquals(true, expectedMsg, func, ...) end function M.assertErrorMsgContains( partialMsg, func, ... ) -- assert that calling f with the arguments will raise an error -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error local no_error, error_msg = pcall( func, ... ) if no_error then failure( 'No error generated when calling function but expected error containing: '..prettystr(partialMsg), nil, 2 ) end if type(error_msg) ~= "string" then error_msg = tostring(error_msg) end if not string.find( error_msg, partialMsg, nil, true ) then error_msg, partialMsg = prettystrPairs(error_msg, partialMsg) fail_fmt(2, nil, 'Error message does not contain: %s\nError message received: %s\n', partialMsg, error_msg) end end function M.assertErrorMsgMatches( expectedMsg, func, ... ) -- assert that calling f with the arguments will raise an error -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error local no_error, error_msg = pcall( func, ... ) if no_error then failure( 'No error generated when calling function but expected error matching: "'..expectedMsg..'"', nil, 2 ) end if type(error_msg) ~= "string" then error_msg = tostring(error_msg) end if not strMatch( error_msg, expectedMsg ) then expectedMsg, error_msg = prettystrPairs(expectedMsg, error_msg) fail_fmt(2, nil, 'Error message does not match pattern: %s\nError message received: %s\n', expectedMsg, error_msg) end end ------------------------------------------------------------------ -- Type assertions ------------------------------------------------------------------ function M.assertEvalToTrue(value, extra_msg_or_nil) if not value then failure("expected: a value evaluating to true, actual: " ..prettystr(value), extra_msg_or_nil, 2) end end function M.assertEvalToFalse(value, extra_msg_or_nil) if value then failure("expected: false or nil, actual: " ..prettystr(value), extra_msg_or_nil, 2) end end function M.assertIsTrue(value, extra_msg_or_nil) if value ~= true then failure("expected: true, actual: " ..prettystr(value), extra_msg_or_nil, 2) end end function M.assertNotIsTrue(value, extra_msg_or_nil) if value == true then failure("expected: not true, actual: " ..prettystr(value), extra_msg_or_nil, 2) end end function M.assertIsFalse(value, extra_msg_or_nil) if value ~= false then failure("expected: false, actual: " ..prettystr(value), extra_msg_or_nil, 2) end end function M.assertNotIsFalse(value, extra_msg_or_nil) if value == false then failure("expected: not false, actual: " ..prettystr(value), extra_msg_or_nil, 2) end end function M.assertIsNil(value, extra_msg_or_nil) if value ~= nil then failure("expected: nil, actual: " ..prettystr(value), extra_msg_or_nil, 2) end end function M.assertNotIsNil(value, extra_msg_or_nil) if value == nil then failure("expected: not nil, actual: nil", extra_msg_or_nil, 2) end end --[[ Add type assertion functions to the module table M. Each of these functions takes a single parameter "value", and checks that its Lua type matches the expected string (derived from the function name): M.assertIsXxx(value) -> ensure that type(value) conforms to "xxx" ]] for _, funcName in ipairs( {'assertIsNumber', 'assertIsString', 'assertIsTable', 'assertIsBoolean', 'assertIsFunction', 'assertIsUserdata', 'assertIsThread'} ) do local typeExpected = funcName:match("^assertIs([A-Z]%a*)$") -- Lua type() always returns lowercase, also make sure the match() succeeded typeExpected = typeExpected and typeExpected:lower() or error("bad function name '"..funcName.."' for type assertion") M[funcName] = function(value, extra_msg_or_nil) if type(value) ~= typeExpected then if type(value) == 'nil' then fail_fmt(2, extra_msg_or_nil, 'expected: a %s value, actual: nil', typeExpected, type(value), prettystrPairs(value)) else fail_fmt(2, extra_msg_or_nil, 'expected: a %s value, actual: type %s, value %s', typeExpected, type(value), prettystrPairs(value)) end end end end --[[ Add shortcuts for verifying type of a variable, without failure (luaunit v2 compatibility) M.isXxx(value) -> returns true if type(value) conforms to "xxx" ]] for _, typeExpected in ipairs( {'Number', 'String', 'Table', 'Boolean', 'Function', 'Userdata', 'Thread', 'Nil' } ) do local typeExpectedLower = typeExpected:lower() local isType = function(value) return (type(value) == typeExpectedLower) end M['is'..typeExpected] = isType M['is_'..typeExpectedLower] = isType end --[[ Add non-type assertion functions to the module table M. Each of these functions takes a single parameter "value", and checks that its Lua type differs from the expected string (derived from the function name): M.assertNotIsXxx(value) -> ensure that type(value) is not "xxx" ]] for _, funcName in ipairs( {'assertNotIsNumber', 'assertNotIsString', 'assertNotIsTable', 'assertNotIsBoolean', 'assertNotIsFunction', 'assertNotIsUserdata', 'assertNotIsThread'} ) do local typeUnexpected = funcName:match("^assertNotIs([A-Z]%a*)$") -- Lua type() always returns lowercase, also make sure the match() succeeded typeUnexpected = typeUnexpected and typeUnexpected:lower() or error("bad function name '"..funcName.."' for type assertion") M[funcName] = function(value, extra_msg_or_nil) if type(value) == typeUnexpected then fail_fmt(2, extra_msg_or_nil, 'expected: not a %s type, actual: value %s', typeUnexpected, prettystrPairs(value)) end end end function M.assertIs(actual, expected, extra_msg_or_nil) if actual ~= expected then if not M.ORDER_ACTUAL_EXPECTED then actual, expected = expected, actual end local old_print_table_ref_in_error_msg = M.PRINT_TABLE_REF_IN_ERROR_MSG M.PRINT_TABLE_REF_IN_ERROR_MSG = true expected, actual = prettystrPairs(expected, actual, '\n', '') M.PRINT_TABLE_REF_IN_ERROR_MSG = old_print_table_ref_in_error_msg fail_fmt(2, extra_msg_or_nil, 'expected and actual object should not be different\nExpected: %s\nReceived: %s', expected, actual) end end function M.assertNotIs(actual, expected, extra_msg_or_nil) if actual == expected then local old_print_table_ref_in_error_msg = M.PRINT_TABLE_REF_IN_ERROR_MSG M.PRINT_TABLE_REF_IN_ERROR_MSG = true local s_expected if not M.ORDER_ACTUAL_EXPECTED then s_expected = prettystrPairs(actual) else s_expected = prettystrPairs(expected) end M.PRINT_TABLE_REF_IN_ERROR_MSG = old_print_table_ref_in_error_msg fail_fmt(2, extra_msg_or_nil, 'expected and actual object should be different: %s', s_expected ) end end ------------------------------------------------------------------ -- Scientific assertions ------------------------------------------------------------------ function M.assertIsNaN(value, extra_msg_or_nil) if type(value) ~= "number" or value == value then failure("expected: NaN, actual: " ..prettystr(value), extra_msg_or_nil, 2) end end function M.assertNotIsNaN(value, extra_msg_or_nil) if type(value) == "number" and value ~= value then failure("expected: not NaN, actual: NaN", extra_msg_or_nil, 2) end end function M.assertIsInf(value, extra_msg_or_nil) if type(value) ~= "number" or math.abs(value) ~= math.huge then failure("expected: #Inf, actual: " ..prettystr(value), extra_msg_or_nil, 2) end end function M.assertIsPlusInf(value, extra_msg_or_nil) if type(value) ~= "number" or value ~= math.huge then failure("expected: #Inf, actual: " ..prettystr(value), extra_msg_or_nil, 2) end end function M.assertIsMinusInf(value, extra_msg_or_nil) if type(value) ~= "number" or value ~= -math.huge then failure("expected: -#Inf, actual: " ..prettystr(value), extra_msg_or_nil, 2) end end function M.assertNotIsPlusInf(value, extra_msg_or_nil) if type(value) == "number" and value == math.huge then failure("expected: not #Inf, actual: #Inf", extra_msg_or_nil, 2) end end function M.assertNotIsMinusInf(value, extra_msg_or_nil) if type(value) == "number" and value == -math.huge then failure("expected: not -#Inf, actual: -#Inf", extra_msg_or_nil, 2) end end function M.assertNotIsInf(value, extra_msg_or_nil) if type(value) == "number" and math.abs(value) == math.huge then failure("expected: not infinity, actual: " .. prettystr(value), extra_msg_or_nil, 2) end end function M.assertIsPlusZero(value, extra_msg_or_nil) if type(value) ~= 'number' or value ~= 0 then failure("expected: +0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2) else if (1/value == -math.huge) then -- more precise error diagnosis failure("expected: +0.0, actual: -0.0", extra_msg_or_nil, 2) else if (1/value ~= math.huge) then -- strange, case should have already been covered failure("expected: +0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2) end end end end function M.assertIsMinusZero(value, extra_msg_or_nil) if type(value) ~= 'number' or value ~= 0 then failure("expected: -0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2) else if (1/value == math.huge) then -- more precise error diagnosis failure("expected: -0.0, actual: +0.0", extra_msg_or_nil, 2) else if (1/value ~= -math.huge) then -- strange, case should have already been covered failure("expected: -0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2) end end end end function M.assertNotIsPlusZero(value, extra_msg_or_nil) if type(value) == 'number' and (1/value == math.huge) then failure("expected: not +0.0, actual: +0.0", extra_msg_or_nil, 2) end end function M.assertNotIsMinusZero(value, extra_msg_or_nil) if type(value) == 'number' and (1/value == -math.huge) then failure("expected: not -0.0, actual: -0.0", extra_msg_or_nil, 2) end end function M.assertTableContains(t, expected) -- checks that table t contains the expected element if table_findkeyof(t, expected) == nil then t, expected = prettystrPairs(t, expected) fail_fmt(2, 'Table %s does NOT contain the expected element %s', t, expected) end end function M.assertNotTableContains(t, expected) -- checks that table t doesn't contain the expected element local k = table_findkeyof(t, expected) if k ~= nil then t, expected = prettystrPairs(t, expected) fail_fmt(2, 'Table %s DOES contain the unwanted element %s (at key %s)', t, expected, prettystr(k)) end end ---------------------------------------------------------------- -- Compatibility layer ---------------------------------------------------------------- -- for compatibility with LuaUnit v2.x function M.wrapFunctions() -- In LuaUnit version <= 2.1 , this function was necessary to include -- a test function inside the global test suite. Nowadays, the functions -- are simply run directly as part of the test discovery process. -- so just do nothing ! ioWrapper.stderr:write[[Use of WrapFunctions() is no longer needed. Just prefix your test function names with "test" or "Test" and they will be picked up and run by LuaUnit. ]] end local list_of_funcs = { -- { official function name , alias } -- general assertions { 'assertEquals' , 'assert_equals' }, { 'assertItemsEquals' , 'assert_items_equals' }, { 'assertNotEquals' , 'assert_not_equals' }, { 'assertAlmostEquals' , 'assert_almost_equals' }, { 'assertNotAlmostEquals' , 'assert_not_almost_equals' }, { 'assertEvalToTrue' , 'assert_eval_to_true' }, { 'assertEvalToFalse' , 'assert_eval_to_false' }, { 'assertStrContains' , 'assert_str_contains' }, { 'assertStrIContains' , 'assert_str_icontains' }, { 'assertNotStrContains' , 'assert_not_str_contains' }, { 'assertNotStrIContains' , 'assert_not_str_icontains' }, { 'assertStrMatches' , 'assert_str_matches' }, { 'assertError' , 'assert_error' }, { 'assertErrorMsgEquals' , 'assert_error_msg_equals' }, { 'assertErrorMsgContains' , 'assert_error_msg_contains' }, { 'assertErrorMsgMatches' , 'assert_error_msg_matches' }, { 'assertErrorMsgContentEquals', 'assert_error_msg_content_equals' }, { 'assertIs' , 'assert_is' }, { 'assertNotIs' , 'assert_not_is' }, { 'assertTableContains' , 'assert_table_contains' }, { 'assertNotTableContains' , 'assert_not_table_contains' }, { 'wrapFunctions' , 'WrapFunctions' }, { 'wrapFunctions' , 'wrap_functions' }, -- type assertions: assertIsXXX -> assert_is_xxx { 'assertIsNumber' , 'assert_is_number' }, { 'assertIsString' , 'assert_is_string' }, { 'assertIsTable' , 'assert_is_table' }, { 'assertIsBoolean' , 'assert_is_boolean' }, { 'assertIsNil' , 'assert_is_nil' }, { 'assertIsTrue' , 'assert_is_true' }, { 'assertIsFalse' , 'assert_is_false' }, { 'assertIsNaN' , 'assert_is_nan' }, { 'assertIsInf' , 'assert_is_inf' }, { 'assertIsPlusInf' , 'assert_is_plus_inf' }, { 'assertIsMinusInf' , 'assert_is_minus_inf' }, { 'assertIsPlusZero' , 'assert_is_plus_zero' }, { 'assertIsMinusZero' , 'assert_is_minus_zero' }, { 'assertIsFunction' , 'assert_is_function' }, { 'assertIsThread' , 'assert_is_thread' }, { 'assertIsUserdata' , 'assert_is_userdata' }, -- type assertions: assertIsXXX -> assertXxx { 'assertIsNumber' , 'assertNumber' }, { 'assertIsString' , 'assertString' }, { 'assertIsTable' , 'assertTable' }, { 'assertIsBoolean' , 'assertBoolean' }, { 'assertIsNil' , 'assertNil' }, { 'assertIsTrue' , 'assertTrue' }, { 'assertIsFalse' , 'assertFalse' }, { 'assertIsNaN' , 'assertNaN' }, { 'assertIsInf' , 'assertInf' }, { 'assertIsPlusInf' , 'assertPlusInf' }, { 'assertIsMinusInf' , 'assertMinusInf' }, { 'assertIsPlusZero' , 'assertPlusZero' }, { 'assertIsMinusZero' , 'assertMinusZero'}, { 'assertIsFunction' , 'assertFunction' }, { 'assertIsThread' , 'assertThread' }, { 'assertIsUserdata' , 'assertUserdata' }, -- type assertions: assertIsXXX -> assert_xxx (luaunit v2 compat) { 'assertIsNumber' , 'assert_number' }, { 'assertIsString' , 'assert_string' }, { 'assertIsTable' , 'assert_table' }, { 'assertIsBoolean' , 'assert_boolean' }, { 'assertIsNil' , 'assert_nil' }, { 'assertIsTrue' , 'assert_true' }, { 'assertIsFalse' , 'assert_false' }, { 'assertIsNaN' , 'assert_nan' }, { 'assertIsInf' , 'assert_inf' }, { 'assertIsPlusInf' , 'assert_plus_inf' }, { 'assertIsMinusInf' , 'assert_minus_inf' }, { 'assertIsPlusZero' , 'assert_plus_zero' }, { 'assertIsMinusZero' , 'assert_minus_zero' }, { 'assertIsFunction' , 'assert_function' }, { 'assertIsThread' , 'assert_thread' }, { 'assertIsUserdata' , 'assert_userdata' }, -- type assertions: assertNotIsXXX -> assert_not_is_xxx { 'assertNotIsNumber' , 'assert_not_is_number' }, { 'assertNotIsString' , 'assert_not_is_string' }, { 'assertNotIsTable' , 'assert_not_is_table' }, { 'assertNotIsBoolean' , 'assert_not_is_boolean' }, { 'assertNotIsNil' , 'assert_not_is_nil' }, { 'assertNotIsTrue' , 'assert_not_is_true' }, { 'assertNotIsFalse' , 'assert_not_is_false' }, { 'assertNotIsNaN' , 'assert_not_is_nan' }, { 'assertNotIsInf' , 'assert_not_is_inf' }, { 'assertNotIsPlusInf' , 'assert_not_plus_inf' }, { 'assertNotIsMinusInf' , 'assert_not_minus_inf' }, { 'assertNotIsPlusZero' , 'assert_not_plus_zero' }, { 'assertNotIsMinusZero' , 'assert_not_minus_zero' }, { 'assertNotIsFunction' , 'assert_not_is_function' }, { 'assertNotIsThread' , 'assert_not_is_thread' }, { 'assertNotIsUserdata' , 'assert_not_is_userdata' }, -- type assertions: assertNotIsXXX -> assertNotXxx (luaunit v2 compat) { 'assertNotIsNumber' , 'assertNotNumber' }, { 'assertNotIsString' , 'assertNotString' }, { 'assertNotIsTable' , 'assertNotTable' }, { 'assertNotIsBoolean' , 'assertNotBoolean' }, { 'assertNotIsNil' , 'assertNotNil' }, { 'assertNotIsTrue' , 'assertNotTrue' }, { 'assertNotIsFalse' , 'assertNotFalse' }, { 'assertNotIsNaN' , 'assertNotNaN' }, { 'assertNotIsInf' , 'assertNotInf' }, { 'assertNotIsPlusInf' , 'assertNotPlusInf' }, { 'assertNotIsMinusInf' , 'assertNotMinusInf' }, { 'assertNotIsPlusZero' , 'assertNotPlusZero' }, { 'assertNotIsMinusZero' , 'assertNotMinusZero' }, { 'assertNotIsFunction' , 'assertNotFunction' }, { 'assertNotIsThread' , 'assertNotThread' }, { 'assertNotIsUserdata' , 'assertNotUserdata' }, -- type assertions: assertNotIsXXX -> assert_not_xxx { 'assertNotIsNumber' , 'assert_not_number' }, { 'assertNotIsString' , 'assert_not_string' }, { 'assertNotIsTable' , 'assert_not_table' }, { 'assertNotIsBoolean' , 'assert_not_boolean' }, { 'assertNotIsNil' , 'assert_not_nil' }, { 'assertNotIsTrue' , 'assert_not_true' }, { 'assertNotIsFalse' , 'assert_not_false' }, { 'assertNotIsNaN' , 'assert_not_nan' }, { 'assertNotIsInf' , 'assert_not_inf' }, { 'assertNotIsPlusInf' , 'assert_not_plus_inf' }, { 'assertNotIsMinusInf' , 'assert_not_minus_inf' }, { 'assertNotIsPlusZero' , 'assert_not_plus_zero' }, { 'assertNotIsMinusZero' , 'assert_not_minus_zero' }, { 'assertNotIsFunction' , 'assert_not_function' }, { 'assertNotIsThread' , 'assert_not_thread' }, { 'assertNotIsUserdata' , 'assert_not_userdata' }, -- all assertions with Coroutine duplicate Thread assertions { 'assertIsThread' , 'assertIsCoroutine' }, { 'assertIsThread' , 'assertCoroutine' }, { 'assertIsThread' , 'assert_is_coroutine' }, { 'assertIsThread' , 'assert_coroutine' }, { 'assertNotIsThread' , 'assertNotIsCoroutine' }, { 'assertNotIsThread' , 'assertNotCoroutine' }, { 'assertNotIsThread' , 'assert_not_is_coroutine' }, { 'assertNotIsThread' , 'assert_not_coroutine' }, } -- Create all aliases in M for _,v in ipairs( list_of_funcs ) do local funcname, alias = v[1], v[2] M[alias] = M[funcname] if EXPORT_ASSERT_TO_GLOBALS then _G[funcname] = M[funcname] _G[alias] = M[funcname] end end ---------------------------------------------------------------- -- -- Outputters -- ---------------------------------------------------------------- -- A common "base" class for outputters -- For concepts involved (class inheritance) see http://www.lua.org/pil/16.2.html local genericOutput = { __class__ = 'genericOutput' } -- class local genericOutput_MT = { __index = genericOutput } -- metatable M.genericOutput = genericOutput -- publish, so that custom classes may derive from it function genericOutput.new(runner, default_verbosity) -- runner is the "parent" object controlling the output, usually a LuaUnit instance local t = { runner = runner } if runner then t.result = runner.result t.verbosity = runner.verbosity or default_verbosity t.fname = runner.fname else t.verbosity = default_verbosity end return setmetatable( t, genericOutput_MT) end -- abstract ("empty") methods function genericOutput:startSuite() -- Called once, when the suite is started end function genericOutput:startClass(className) -- Called each time a new test class is started end function genericOutput:startTest(testName) -- called each time a new test is started, right before the setUp() -- the current test status node is already created and available in: self.result.currentNode end function genericOutput:updateStatus(node) -- called with status failed or error as soon as the error/failure is encountered -- this method is NOT called for a successful test because a test is marked as successful by default -- and does not need to be updated end function genericOutput:endTest(node) -- called when the test is finished, after the tearDown() method end function genericOutput:endClass() -- called when executing the class is finished, before moving on to the next class of at the end of the test execution end function genericOutput:endSuite() -- called at the end of the test suite execution end ---------------------------------------------------------------- -- class TapOutput ---------------------------------------------------------------- local TapOutput = genericOutput.new() -- derived class local TapOutput_MT = { __index = TapOutput } -- metatable TapOutput.__class__ = 'TapOutput' -- For a good reference for TAP format, check: http://testanything.org/tap-specification.html function TapOutput.new(runner) local t = genericOutput.new(runner, M.VERBOSITY_LOW) return setmetatable( t, TapOutput_MT) end function TapOutput:startSuite() env.info("1.."..self.result.selectedCount) env.info('# Started on '..self.result.startDate) end function TapOutput:startClass(className) if className ~= '[TestFunctions]' then env.info('# Starting class: '..className) end end function TapOutput:updateStatus( node ) if node:isSkipped() then ioWrapper.stdout:write("ok "..self.result.currentTestNumber.."\t# SKIP "..node.msg.."\n" ) return end ioWrapper.stdout:write("not ok "..self.result.currentTestNumber.."\t"..node.testName.."\n") if self.verbosity > M.VERBOSITY_LOW then env.info( prefixString( '# ', node.msg ) ) end if (node:isFailure() or node:isError()) and self.verbosity > M.VERBOSITY_DEFAULT then env.info( prefixString( '# ', node.stackTrace ) ) end end function TapOutput:endTest( node ) if node:isSuccess() then ioWrapper.stdout:write("ok "..self.result.currentTestNumber.."\t"..node.testName.."\n") end end function TapOutput:endSuite() env.info( '# '..M.LuaUnit.statusLine( self.result ) ) return self.result.notSuccessCount end -- class TapOutput end ---------------------------------------------------------------- -- class JUnitOutput ---------------------------------------------------------------- -- See directory junitxml for more information about the junit format local JUnitOutput = genericOutput.new() -- derived class local JUnitOutput_MT = { __index = JUnitOutput } -- metatable JUnitOutput.__class__ = 'JUnitOutput' function JUnitOutput.new(runner) local t = genericOutput.new(runner, M.VERBOSITY_LOW) t.testList = {} return setmetatable( t, JUnitOutput_MT ) end function JUnitOutput:startSuite() -- open xml file early to deal with errors if self.fname == nil then error('With Junit, an output filename must be supplied with --name!') end if string.sub(self.fname,-4) ~= '.xml' then self.fname = self.fname..'.xml' end self.fd = ioWrapper.open(self.fname, "w") if self.fd == nil then error("Could not open file for writing: "..self.fname) end env.info('# XML output to '..self.fname) env.info('# Started on '..self.result.startDate) end function JUnitOutput:startClass(className) if className ~= '[TestFunctions]' then env.info('# Starting class: '..className) end end function JUnitOutput:startTest(testName) env.info('# Starting test: '..testName) end function JUnitOutput:updateStatus( node ) if node:isFailure() then env.info( '# Failure: ' .. prefixString( '# ', node.msg ):sub(4, nil) ) -- env.info('# ' .. node.stackTrace) elseif node:isError() then env.info( '# Error: ' .. prefixString( '# ' , node.msg ):sub(4, nil) ) -- env.info('# ' .. node.stackTrace) end end function JUnitOutput:endSuite() env.info( '# '..M.LuaUnit.statusLine(self.result)) -- XML file writing self.fd:write('\n') self.fd:write('\n') self.fd:write(string.format( ' \n', self.result.runCount, self.result.startIsodate, self.result.duration, self.result.errorCount, self.result.failureCount, self.result.skippedCount )) self.fd:write(" \n") self.fd:write(string.format(' \n', _VERSION ) ) self.fd:write(string.format(' \n', M.VERSION) ) -- XXX please include system name and version if possible self.fd:write(" \n") for i,node in ipairs(self.result.allTests) do self.fd:write(string.format(' \n', node.className, node.testName, node.duration ) ) if node:isNotSuccess() then self.fd:write(node:statusXML()) end self.fd:write(' \n') end -- Next two lines are needed to validate junit ANT xsd, but really not useful in general: self.fd:write(' \n') self.fd:write(' \n') self.fd:write(' \n') self.fd:write('\n') self.fd:close() return self.result.notSuccessCount end -- class TapOutput end ---------------------------------------------------------------- -- class TextOutput ---------------------------------------------------------------- --[[ Example of other unit-tests suite text output -- Python Non verbose: For each test: . or F or E If some failed tests: ============== ERROR / FAILURE: TestName (testfile.testclass) --------- Stack trace then -------------- then "Ran x tests in 0.000s" then OK or FAILED (failures=1, error=1) -- Python Verbose: testname (filename.classname) ... ok testname (filename.classname) ... FAIL testname (filename.classname) ... ERROR then -------------- then "Ran x tests in 0.000s" then OK or FAILED (failures=1, error=1) -- Ruby: Started . Finished in 0.002695 seconds. 1 tests, 2 assertions, 0 failures, 0 errors -- Ruby: >> ruby tc_simple_number2.rb Loaded suite tc_simple_number2 Started F.. Finished in 0.038617 seconds. 1) Failure: test_failure(TestSimpleNumber) [tc_simple_number2.rb:16]: Adding doesn't work. <3> expected but was <4>. 3 tests, 4 assertions, 1 failures, 0 errors -- Java Junit .......F. Time: 0,003 There was 1 failure: 1) testCapacity(junit.samples.VectorTest)junit.framework.AssertionFailedError at junit.samples.VectorTest.testCapacity(VectorTest.java:87) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) FAILURES!!! Tests run: 8, Failures: 1, Errors: 0 -- Maven # mvn test ------------------------------------------------------- T E S T S ------------------------------------------------------- Running math.AdditionTest Tests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.03 sec <<< FAILURE! Results : Failed tests: testLireSymbole(math.AdditionTest) Tests run: 2, Failures: 1, Errors: 0, Skipped: 0 -- LuaUnit ---- non verbose * display . or F or E when running tests ---- verbose * display test name + ok/fail ---- * blank line * number) ERROR or FAILURE: TestName Stack trace * blank line * number) ERROR or FAILURE: TestName Stack trace then -------------- then "Ran x tests in 0.000s (%d not selected, %d skipped)" then OK or FAILED (failures=1, error=1) ]] local TextOutput = genericOutput.new() -- derived class local TextOutput_MT = { __index = TextOutput } -- metatable TextOutput.__class__ = 'TextOutput' function TextOutput.new(runner) local t = genericOutput.new(runner, M.VERBOSITY_DEFAULT) t.errorList = {} return setmetatable( t, TextOutput_MT ) end function TextOutput:startSuite() if self.verbosity > M.VERBOSITY_DEFAULT then env.info( 'Started on '.. self.result.startDate ) end end function TextOutput:startTest(testName) if self.verbosity > M.VERBOSITY_DEFAULT then ioWrapper.stdout:write( " "..self.result.currentNode.testName.." ... " ) end end function TextOutput:endTest( node ) if node:isSuccess() then if self.verbosity > M.VERBOSITY_DEFAULT then ioWrapper.stdout:write("Ok\n") ioWrapper.stdout:flush() else ioWrapper.stdout:write(".") ioWrapper.stdout:flush() end else if self.verbosity > M.VERBOSITY_DEFAULT then env.info( node.status ) env.info( node.msg ) --[[ -- find out when to do this: if self.verbosity > M.VERBOSITY_DEFAULT then env.info( node.stackTrace ) end ]] else -- write only the first character of status E, F or S ioWrapper.stdout:write(string.sub(node.status, 1, 1)) ioWrapper.stdout:flush() end end end function TextOutput:displayOneFailedTest( index, fail ) env.info(index..") "..fail.testName ) env.info( fail.msg ) env.info( fail.stackTrace ) env.info() end function TextOutput:displayErroredTests() if #self.result.errorTests ~= 0 then env.info("Tests with errors:") env.info("------------------") for i, v in ipairs(self.result.errorTests) do self:displayOneFailedTest(i, v) end end end function TextOutput:displayFailedTests() if #self.result.failedTests ~= 0 then env.info("Failed tests:") env.info("-------------") for i, v in ipairs(self.result.failedTests) do self:displayOneFailedTest(i, v) end end end function TextOutput:endSuite() if self.verbosity > M.VERBOSITY_DEFAULT then env.info("=========================================================") else env.info() end self:displayErroredTests() self:displayFailedTests() env.info( M.LuaUnit.statusLine( self.result ) ) if self.result.notSuccessCount == 0 then env.info('OK') end end -- class TextOutput end ---------------------------------------------------------------- -- class NilOutput ---------------------------------------------------------------- local function nopCallable() --env.info(42) return nopCallable end local NilOutput = { __class__ = 'NilOuptut' } -- class local NilOutput_MT = { __index = nopCallable } -- metatable function NilOutput.new(runner) return setmetatable( { __class__ = 'NilOutput' }, NilOutput_MT ) end ---------------------------------------------------------------- -- -- class LuaUnit -- ---------------------------------------------------------------- M.LuaUnit = { outputType = TextOutput, verbosity = M.VERBOSITY_DEFAULT, __class__ = 'LuaUnit' } local LuaUnit_MT = { __index = M.LuaUnit } if EXPORT_ASSERT_TO_GLOBALS then LuaUnit = M.LuaUnit end function M.LuaUnit.new() return setmetatable( {}, LuaUnit_MT ) end -----------------[[ Utility methods ]]--------------------- function M.LuaUnit.asFunction(aObject) -- return "aObject" if it is a function, and nil otherwise if 'function' == type(aObject) then return aObject end end function M.LuaUnit.splitClassMethod(someName) --[[ Return a pair of className, methodName strings for a name in the form "class.method". If no class part (or separator) is found, will return nil, someName instead (the latter being unchanged). This convention thus also replaces the older isClassMethod() test: You just have to check for a non-nil className (return) value. ]] local separator = string.find(someName, '.', 1, true) if separator then return someName:sub(1, separator - 1), someName:sub(separator + 1) end return nil, someName end function M.LuaUnit.isMethodTestName( s ) -- return true is the name matches the name of a test method -- default rule is that is starts with 'Test' or with 'test' return string.sub(s, 1, 4):lower() == 'test' end function M.LuaUnit.isTestName( s ) -- return true is the name matches the name of a test -- default rule is that is starts with 'Test' or with 'test' return string.sub(s, 1, 4):lower() == 'test' end function M.LuaUnit.collectTests() -- return a list of all test names in the global namespace -- that match LuaUnit.isTestName local testNames = {} for k, _ in pairs(_G) do if type(k) == "string" and M.LuaUnit.isTestName( k ) then table.insert( testNames , k ) end end table.sort( testNames ) return testNames end function M.LuaUnit.parseCmdLine( cmdLine ) -- parse the command line -- Supported command line parameters: -- --verbose, -v: increase verbosity -- --quiet, -q: silence output -- --error, -e: treat errors as fatal (quit program) -- --output, -o, + name: select output type -- --pattern, -p, + pattern: run test matching pattern, may be repeated -- --exclude, -x, + pattern: run test not matching pattern, may be repeated -- --shuffle, -s, : shuffle tests before reunning them -- --name, -n, + fname: name of output file for junit, default to stdout -- --repeat, -r, + num: number of times to execute each test -- [testnames, ...]: run selected test names -- -- Returns a table with the following fields: -- verbosity: nil, M.VERBOSITY_DEFAULT, M.VERBOSITY_QUIET, M.VERBOSITY_VERBOSE -- output: nil, 'tap', 'junit', 'text', 'nil' -- testNames: nil or a list of test names to run -- exeRepeat: num or 1 -- pattern: nil or a list of patterns -- exclude: nil or a list of patterns local result, state = {}, nil local SET_OUTPUT = 1 local SET_PATTERN = 2 local SET_EXCLUDE = 3 local SET_FNAME = 4 local SET_REPEAT = 5 if cmdLine == nil then return result end local function parseOption( option ) if option == '--help' or option == '-h' then result['help'] = true return elseif option == '--version' then result['version'] = true return elseif option == '--verbose' or option == '-v' then result['verbosity'] = M.VERBOSITY_VERBOSE return elseif option == '--quiet' or option == '-q' then result['verbosity'] = M.VERBOSITY_QUIET return elseif option == '--error' or option == '-e' then result['quitOnError'] = true return elseif option == '--failure' or option == '-f' then result['quitOnFailure'] = true return elseif option == '--shuffle' or option == '-s' then result['shuffle'] = true return elseif option == '--output' or option == '-o' then state = SET_OUTPUT return state elseif option == '--name' or option == '-n' then state = SET_FNAME return state elseif option == '--repeat' or option == '-r' then state = SET_REPEAT return state elseif option == '--pattern' or option == '-p' then state = SET_PATTERN return state elseif option == '--exclude' or option == '-x' then state = SET_EXCLUDE return state end error('Unknown option: '..option,3) end local function setArg( cmdArg, state ) if state == SET_OUTPUT then result['output'] = cmdArg return elseif state == SET_FNAME then result['fname'] = cmdArg return elseif state == SET_REPEAT then result['exeRepeat'] = tonumber(cmdArg) or error('Malformed -r argument: '..cmdArg) return elseif state == SET_PATTERN then if result['pattern'] then table.insert( result['pattern'], cmdArg ) else result['pattern'] = { cmdArg } end return elseif state == SET_EXCLUDE then local notArg = '!'..cmdArg if result['pattern'] then table.insert( result['pattern'], notArg ) else result['pattern'] = { notArg } end return end error('Unknown parse state: '.. state) end for i, cmdArg in ipairs(cmdLine) do if state ~= nil then setArg( cmdArg, state, result ) state = nil else if cmdArg:sub(1,1) == '-' then state = parseOption( cmdArg ) else if result['testNames'] then table.insert( result['testNames'], cmdArg ) else result['testNames'] = { cmdArg } end end end end if result['help'] then M.LuaUnit.help() end if result['version'] then M.LuaUnit.version() end if state ~= nil then error('Missing argument after '..cmdLine[ #cmdLine ],2 ) end return result end function M.LuaUnit.help() env.info(M.USAGE) -- os.exit(0) end function M.LuaUnit.version() env.info('LuaUnit v'..M.VERSION..' by Philippe Fremy ') -- os.exit(0) end ---------------------------------------------------------------- -- class NodeStatus ---------------------------------------------------------------- local NodeStatus = { __class__ = 'NodeStatus' } -- class local NodeStatus_MT = { __index = NodeStatus } -- metatable M.NodeStatus = NodeStatus -- values of status NodeStatus.SUCCESS = 'SUCCESS' NodeStatus.SKIP = 'SKIP' NodeStatus.FAIL = 'FAIL' NodeStatus.ERROR = 'ERROR' function NodeStatus.new( number, testName, className ) -- default constructor, test are PASS by default local t = { number = number, testName = testName, className = className } setmetatable( t, NodeStatus_MT ) t:success() return t end function NodeStatus:success() self.status = self.SUCCESS -- useless because lua does this for us, but it helps me remembering the relevant field names self.msg = nil self.stackTrace = nil end function NodeStatus:skip(msg) self.status = self.SKIP self.msg = msg self.stackTrace = nil end function NodeStatus:fail(msg, stackTrace) self.status = self.FAIL self.msg = msg self.stackTrace = stackTrace end function NodeStatus:error(msg, stackTrace) self.status = self.ERROR self.msg = msg self.stackTrace = stackTrace end function NodeStatus:isSuccess() return self.status == NodeStatus.SUCCESS end function NodeStatus:isNotSuccess() -- Return true if node is either failure or error or skip return (self.status == NodeStatus.FAIL or self.status == NodeStatus.ERROR or self.status == NodeStatus.SKIP) end function NodeStatus:isSkipped() return self.status == NodeStatus.SKIP end function NodeStatus:isFailure() return self.status == NodeStatus.FAIL end function NodeStatus:isError() return self.status == NodeStatus.ERROR end function NodeStatus:statusXML() if self:isError() then return table.concat( {' \n', ' \n'}) elseif self:isFailure() then return table.concat( {' \n', ' \n'}) elseif self:isSkipped() then return table.concat({' ', xmlEscape(self.msg),'\n' } ) end return ' \n' -- (not XSD-compliant! normally shouldn't get here) end --------------[[ Output methods ]]------------------------- local function conditional_plural(number, singular) -- returns a grammatically well-formed string "%d " local suffix = '' if number ~= 1 then -- use plural suffix = (singular:sub(-2) == 'ss') and 'es' or 's' end return string.format('%d %s%s', number, singular, suffix) end function M.LuaUnit.statusLine(result) -- return status line string according to results local s = { string.format('Ran %d tests in %0.3f seconds', result.runCount, result.duration), conditional_plural(result.successCount, 'success'), } if result.notSuccessCount > 0 then if result.failureCount > 0 then table.insert(s, conditional_plural(result.failureCount, 'failure')) end if result.errorCount > 0 then table.insert(s, conditional_plural(result.errorCount, 'error')) end else table.insert(s, '0 failures') end if result.skippedCount > 0 then table.insert(s, string.format("%d skipped", result.skippedCount)) end if result.nonSelectedCount > 0 then table.insert(s, string.format("%d non-selected", result.nonSelectedCount)) end return table.concat(s, ', ') end function M.LuaUnit:startSuite(selectedCount, nonSelectedCount) self.result = { selectedCount = selectedCount, nonSelectedCount = nonSelectedCount, successCount = 0, runCount = 0, currentTestNumber = 0, currentClassName = "", currentNode = nil, suiteStarted = true, startTime = timer.getTime0(), ---os.clock(), startDate = 'Date',--os.date(os.getenv('LUAUNIT_DATEFMT')), startIsodate = 'Date',--os.date('%Y-%m-%dT%H:%M:%S'), patternIncludeFilter = self.patternIncludeFilter, -- list of test node status allTests = {}, failedTests = {}, errorTests = {}, skippedTests = {}, failureCount = 0, errorCount = 0, notSuccessCount = 0, skippedCount = 0, } self.outputType = self.outputType or TextOutput self.output = self.outputType.new(self) self.output:startSuite() end function M.LuaUnit:startClass( className ) self.result.currentClassName = className self.output:startClass( className ) end function M.LuaUnit:startTest( testName ) self.result.currentTestNumber = self.result.currentTestNumber + 1 self.result.runCount = self.result.runCount + 1 self.result.currentNode = NodeStatus.new( self.result.currentTestNumber, testName, self.result.currentClassName ) self.result.currentNode.startTime = timer.getTime() --os.clock() table.insert( self.result.allTests, self.result.currentNode ) self.output:startTest( testName ) end function M.LuaUnit:updateStatus( err ) -- "err" is expected to be a table / result from protectedCall() if err.status == NodeStatus.SUCCESS then return end local node = self.result.currentNode --[[ As a first approach, we will report only one error or one failure for one test. However, we can have the case where the test is in failure, and the teardown is in error. In such case, it's a good idea to report both a failure and an error in the test suite. This is what Python unittest does for example. However, it mixes up counts so need to be handled carefully: for example, there could be more (failures + errors) count that tests. What happens to the current node ? We will do this more intelligent version later. ]] -- if the node is already in failure/error, just don't report the new error (see above) if node.status ~= NodeStatus.SUCCESS then return end if err.status == NodeStatus.FAIL then node:fail( err.msg, err.trace ) table.insert( self.result.failedTests, node ) elseif err.status == NodeStatus.ERROR then node:error( err.msg, err.trace ) table.insert( self.result.errorTests, node ) elseif err.status == NodeStatus.SKIP then node:skip( err.msg ) table.insert( self.result.skippedTests, node ) else error('No such status: ' .. prettystr(err.status)) end self.output:updateStatus( node ) end function M.LuaUnit:endTest() local node = self.result.currentNode -- env.info( 'endTest() '..prettystr(node)) -- env.info( 'endTest() '..prettystr(node:isNotSuccess())) node.duration = timer.getTime() --os.clock() - node.startTime node.startTime = nil self.output:endTest( node ) if node:isSuccess() then self.result.successCount = self.result.successCount + 1 elseif node:isError() then if self.quitOnError or self.quitOnFailure then -- Runtime error - abort test execution as requested by -- "--error" option. This is done by setting a special -- flag that gets handled in runSuiteByInstances(). env.info("\nERROR during LuaUnit test execution:\n" .. node.msg) self.result.aborted = true end elseif node:isFailure() then if self.quitOnFailure then -- Failure - abort test execution as requested by -- "--failure" option. This is done by setting a special -- flag that gets handled in runSuiteByInstances(). env.info("\nFailure during LuaUnit test execution:\n" .. node.msg) self.result.aborted = true end elseif node:isSkipped() then self.result.runCount = self.result.runCount - 1 else error('No such node status: ' .. prettystr(node.status)) end self.result.currentNode = nil end function M.LuaUnit:endClass() self.output:endClass() end function M.LuaUnit:endSuite() if self.result.suiteStarted == false then error('LuaUnit:endSuite() -- suite was already ended' ) end self.result.duration = timer.getTime() --os.clock()-self.result.startTime self.result.suiteStarted = false -- Expose test counts for outputter's endSuite(). This could be managed -- internally instead by using the length of the lists of failed tests -- but unit tests rely on these fields being present. self.result.failureCount = #self.result.failedTests self.result.errorCount = #self.result.errorTests self.result.notSuccessCount = self.result.failureCount + self.result.errorCount self.result.skippedCount = #self.result.skippedTests self.output:endSuite() end function M.LuaUnit:setOutputType(outputType, fname) -- Configures LuaUnit runner output -- outputType is one of: NIL, TAP, JUNIT, TEXT -- when outputType is junit, the additional argument fname is used to set the name of junit output file -- for other formats, fname is ignored if outputType:upper() == "NIL" then self.outputType = NilOutput return end if outputType:upper() == "TAP" then self.outputType = TapOutput return end if outputType:upper() == "JUNIT" then self.outputType = JUnitOutput if fname then self.fname = fname end return end if outputType:upper() == "TEXT" then self.outputType = TextOutput return end error( 'No such format: '..outputType,2) end --------------[[ Runner ]]----------------- function M.LuaUnit:protectedCall(classInstance, methodInstance, prettyFuncName) -- if classInstance is nil, this is just a function call -- else, it's method of a class being called. local function err_handler(e) -- transform error into a table, adding the traceback information return { status = NodeStatus.ERROR, msg = e, trace = string.sub(debug.traceback("", 3), 2) } end local ok, err if classInstance then -- stupid Lua < 5.2 does not allow xpcall with arguments so let's use a workaround ok, err = xpcall( function () methodInstance(classInstance) end, err_handler ) else ok, err = xpcall( function () methodInstance() end, err_handler ) end if ok then return {status = NodeStatus.SUCCESS} end local iter_msg iter_msg = self.exeRepeat and 'iteration '..self.currentCount err.msg, err.status = M.adjust_err_msg_with_iter( err.msg, iter_msg ) if err.status == NodeStatus.SUCCESS or err.status == NodeStatus.SKIP then err.trace = nil return err end -- reformat / improve the stack trace if prettyFuncName then -- we do have the real method name err.trace = err.trace:gsub("in (%a+) 'methodInstance'", "in %1 '"..prettyFuncName.."'") end if STRIP_LUAUNIT_FROM_STACKTRACE then err.trace = stripLuaunitTrace(err.trace) end return err -- return the error "object" (table) end function M.LuaUnit:execOneFunction(className, methodName, classInstance, methodInstance) -- When executing a test function, className and classInstance must be nil -- When executing a class method, all parameters must be set if type(methodInstance) ~= 'function' then error( tostring(methodName)..' must be a function, not '..type(methodInstance)) end local prettyFuncName if className == nil then className = '[TestFunctions]' prettyFuncName = methodName else prettyFuncName = className..'.'..methodName end if self.lastClassName ~= className then if self.lastClassName ~= nil then self:endClass() end self:startClass( className ) self.lastClassName = className end self:startTest(prettyFuncName) local node = self.result.currentNode for iter_n = 1, self.exeRepeat or 1 do if node:isNotSuccess() then break end self.currentCount = iter_n -- run setUp first (if any) if classInstance then local func = self.asFunction( classInstance.setUp ) or self.asFunction( classInstance.Setup ) or self.asFunction( classInstance.setup ) or self.asFunction( classInstance.SetUp ) if func then self:updateStatus(self:protectedCall(classInstance, func, className..'.setUp')) end end -- run testMethod() if node:isSuccess() then self:updateStatus(self:protectedCall(classInstance, methodInstance, prettyFuncName)) end -- lastly, run tearDown (if any) if classInstance then local func = self.asFunction( classInstance.tearDown ) or self.asFunction( classInstance.TearDown ) or self.asFunction( classInstance.teardown ) or self.asFunction( classInstance.Teardown ) if func then self:updateStatus(self:protectedCall(classInstance, func, className..'.tearDown')) end end end self:endTest() end function M.LuaUnit.expandOneClass( result, className, classInstance ) --[[ Input: a list of { name, instance }, a class name, a class instance Ouptut: modify result to add all test method instance in the form: { className.methodName, classInstance } ]] for methodName, methodInstance in sortedPairs(classInstance) do if M.LuaUnit.asFunction(methodInstance) and M.LuaUnit.isMethodTestName( methodName ) then table.insert( result, { className..'.'..methodName, classInstance } ) end end end function M.LuaUnit.expandClasses( listOfNameAndInst ) --[[ -- expand all classes (provided as {className, classInstance}) to a list of {className.methodName, classInstance} -- functions and methods remain untouched Input: a list of { name, instance } Output: * { function name, function instance } : do nothing * { class.method name, class instance }: do nothing * { class name, class instance } : add all method names in the form of (className.methodName, classInstance) ]] local result = {} for i,v in ipairs( listOfNameAndInst ) do local name, instance = v[1], v[2] if M.LuaUnit.asFunction(instance) then table.insert( result, { name, instance } ) else if type(instance) ~= 'table' then error( 'Instance must be a table or a function, not a '..type(instance)..' with value '..prettystr(instance)) end local className, methodName = M.LuaUnit.splitClassMethod( name ) if className then local methodInstance = instance[methodName] if methodInstance == nil then error( "Could not find method in class "..tostring(className).." for method "..tostring(methodName) ) end table.insert( result, { name, instance } ) else M.LuaUnit.expandOneClass( result, name, instance ) end end end return result end function M.LuaUnit.applyPatternFilter( patternIncFilter, listOfNameAndInst ) local included, excluded = {}, {} for i, v in ipairs( listOfNameAndInst ) do -- local name, instance = v[1], v[2] if patternFilter( patternIncFilter, v[1] ) then table.insert( included, v ) else table.insert( excluded, v ) end end return included, excluded end function M.LuaUnit:runSuiteByInstances( listOfNameAndInst ) --[[ Run an explicit list of tests. Each item of the list must be one of: * { function name, function instance } * { class name, class instance } * { class.method name, class instance } ]] local expandedList = self.expandClasses( listOfNameAndInst ) if self.shuffle then randomizeTable( expandedList ) end local filteredList, filteredOutList = self.applyPatternFilter( self.patternIncludeFilter, expandedList ) self:startSuite( #filteredList, #filteredOutList ) for i,v in ipairs( filteredList ) do local name, instance = v[1], v[2] if M.LuaUnit.asFunction(instance) then self:execOneFunction( nil, name, nil, instance ) else -- expandClasses() should have already taken care of sanitizing the input assert( type(instance) == 'table' ) local className, methodName = M.LuaUnit.splitClassMethod( name ) assert( className ~= nil ) local methodInstance = instance[methodName] assert(methodInstance ~= nil) self:execOneFunction( className, methodName, instance, methodInstance ) end if self.result.aborted then break -- "--error" or "--failure" option triggered end end if self.lastClassName ~= nil then self:endClass() end self:endSuite() if self.result.aborted then env.info("LuaUnit ABORTED (as requested by --error or --failure option)") os.exit(-2) end end function M.LuaUnit:runSuiteByNames( listOfName ) --[[ Run LuaUnit with a list of generic names, coming either from command-line or from global namespace analysis. Convert the list into a list of (name, valid instances (table or function)) and calls runSuiteByInstances. ]] local instanceName, instance local listOfNameAndInst = {} for i,name in ipairs( listOfName ) do local className, methodName = M.LuaUnit.splitClassMethod( name ) if className then instanceName = className instance = _G[instanceName] if instance == nil then error( "No such name in global space: "..instanceName ) end if type(instance) ~= 'table' then error( 'Instance of '..instanceName..' must be a table, not '..type(instance)) end local methodInstance = instance[methodName] if methodInstance == nil then error( "Could not find method in class "..tostring(className).." for method "..tostring(methodName) ) end else -- for functions and classes instanceName = name instance = _G[instanceName] end if instance == nil then error( "No such name in global space: "..instanceName ) end if (type(instance) ~= 'table' and type(instance) ~= 'function') then error( 'Name must match a function or a table: '..instanceName ) end table.insert( listOfNameAndInst, { name, instance } ) end self:runSuiteByInstances( listOfNameAndInst ) end function M.LuaUnit.run(...) -- Run some specific test classes. -- If no arguments are passed, run the class names specified on the -- command line. If no class name is specified on the command line -- run all classes whose name starts with 'Test' -- -- If arguments are passed, they must be strings of the class names -- that you want to run or generic command line arguments (-o, -p, -v, ...) local runner = M.LuaUnit.new() return runner:runSuite(...) end function M.LuaUnit:runSuite( ... ) local args = {...} if type(args[1]) == 'table' and args[1].__class__ == 'LuaUnit' then -- run was called with the syntax M.LuaUnit:runSuite() -- we support both M.LuaUnit.run() and M.LuaUnit:run() -- strip out the first argument table.remove(args,1) end if #args == 0 then args = cmdline_argv end local options = pcall_or_abort( M.LuaUnit.parseCmdLine, args ) -- We expect these option fields to be either `nil` or contain -- valid values, so it's safe to always copy them directly. self.verbosity = M.VERBOSITY_VERBOSE self.quitOnError = options.quitOnError self.quitOnFailure = options.quitOnFailure self.exeRepeat = options.exeRepeat self.patternIncludeFilter = options.pattern self.shuffle = options.shuffle if options.output then if options.output:lower() == 'junit' and options.fname == nil then env.info('With junit output, a filename must be supplied with -n or --name') os.exit(-1) end pcall_or_abort(self.setOutputType, self, options.output, options.fname) end self:runSuiteByNames( options.testNames or M.LuaUnit.collectTests() ) return self.result.notSuccessCount end -- class LuaUnit -- For compatbility with LuaUnit v2 M.run = M.LuaUnit.run M.Run = M.LuaUnit.run function M:setVerbosity( verbosity ) M.LuaUnit.verbosity = verbosity end M.set_verbosity = M.setVerbosity M.SetVerbosity = M.setVerbosity lu = {} lu = M return ================================================ FILE: unit-tests/skynet-unit-test-iads-setup.lua ================================================ do --- create an iads so the mission can be played, the ones in the unit tests, are cleaned once the tests are finished redIADS = SkynetIADS:create("Red IADS") local iadsDebug = redIADS:getDebugSettings() iadsDebug.IADSStatus = true iadsDebug.contacts = true iadsDebug.harmDefence = true --[[ iadsDebug.radarWentDark = true iadsDebug.radarWentLive = true iadsDebug.jammerProbability = true iadsDebug.addedEWRadar = true iadsDebug.addedSAMSite = true iadsDebug.commandCenterStatusEnvOutput = true iadsDebug.samSiteStatusEnvOutput = true iadsDebug.earlyWarningRadarStatusEnvOutput = true --]] local comCenter = Unit.getByName('connection-node-ew') local power = StaticObject.getByName('Command Center Power') local connection = Unit.getByName('connection-node-ew') redIADS:addCommandCenter(comCenter):addPowerSource(power):addConnectionNode(connection) local comCenter2 = StaticObject.getByName('Command Center') redIADS:addCommandCenter(comCenter2) redIADS:addEarlyWarningRadarsByPrefix('EW') redIADS:addSAMSitesByPrefix('SAM'):setHARMDetectionChance(100) ewConnectionNode = Unit.getByName('connection-node-ew') redIADS:getEarlyWarningRadarByUnitName('EW-west2'):setHARMDetectionChance(100):addConnectionNode(ewConnectionNode) local sa15 = redIADS:getSAMSiteByGroupName('SAM-SA-15-1') redIADS:getSAMSiteByGroupName('SAM-SA-10'):setActAsEW(true):setHARMDetectionChance(100):addPointDefence(sa15) redIADS:getSAMSiteByGroupName('SAM-HQ-7'):setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE) local connectioNode = StaticObject.getByName('Unused Connection Node') redIADS:getSAMSiteByGroupName('SAM-SA-6-2'):addConnectionNode(connectioNode):setGoLiveRangeInPercent(120):setHARMDetectionChance(100) redIADS:getEarlyWarningRadarByUnitName('EW-SR-P19'):addPointDefence(redIADS:getSAMSiteByGroupName('SAM-SA-15-P19')) redIADS:addRadioMenu() redIADS:activate() blueIADS = SkynetIADS:create("UAE") blueIADS:addSAMSitesByPrefix('BLUE-SAM') blueIADS:addEarlyWarningRadarsByPrefix('BLUE-EW') blueIADS:getSAMSitesByNatoName('Rapier'):setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE) blueIADS:getSAMSitesByNatoName('Roland ADS'):setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE) blueIADS:addRadioMenu() blueIADS:activate() --[[ local iadsDebug = blueIADS:getDebugSettings() iadsDebug.IADSStatus = true iadsDebug.radarWentDark = true iadsDebug.contacts = true iadsDebug.radarWentLive = true --]] local jammer = SkynetIADSJammer:create(Unit.getByName('jammer-source'), redIADS) jammer:addRadioMenu() posCounter = 0 initialPosition = nil secondPoisition = nil calculatedPosition = nil function Vec3CalculationSpike() if posCounter == 1 then initialPosition = Unit.getByName('Hornet SA-6 Attack'):getPosition().p env.info("Initial Position X:"..initialPosition.x.." Y:"..initialPosition.y.." Z:"..initialPosition.z) end if posCounter == 2 then secondPoisition = Unit.getByName('Hornet SA-6 Attack'):getPosition().p env.info("Second Position X:"..secondPoisition.x.." Y:"..secondPoisition.y.." Z:"..secondPoisition.z) end if posCounter >= 2 then local deltaX = (secondPoisition.x - initialPosition.x) --y represents altitude in implementation don't increment this value it may skyrocket or go below 0 local deltaY = (secondPoisition.y - initialPosition.y) local deltaZ = (secondPoisition.z - initialPosition.z) env.info("deltas X:"..deltaX.." Y:"..deltaY.." Z:"..deltaZ) env.info("------------------------------------------------") if calculatedPosition == nil then calculatedPosition = {} calculatedPosition.x = initialPosition.x calculatedPosition.y = initialPosition.y calculatedPosition.z = initialPosition.z end calculatedPosition.x = calculatedPosition.x + deltaX calculatedPosition.y = calculatedPosition.y + deltaY calculatedPosition.z = calculatedPosition.z + deltaZ local currentPosition = Unit.getByName('Hornet SA-6 Attack'):getPosition().p env.info("Calculated Position X:"..calculatedPosition.x.." Y:"..calculatedPosition.y.." Z:"..calculatedPosition.z) env.info("Current Position X:"..currentPosition.x.." Y:"..currentPosition.y.." Z:"..currentPosition.z) local difX = currentPosition.x - calculatedPosition.x local difY = currentPosition.y - calculatedPosition.y local difZ = currentPosition.z - calculatedPosition.z env.info("Difference X:"..difX.." Y:"..difY.." Z:"..difZ) env.info("------------------------------------------------") end posCounter = posCounter + 1 end --mist.scheduleFunction(Vec3CalculationSpike, {}, 1, 1) end ================================================ FILE: unit-tests/skynet-unit-tests.lua ================================================ do ---IADS Unit Tests SKYNET_UNIT_TESTS_NUM_EW_SITES_RED = 17 SKYNET_UNIT_TESTS_NUM_SAM_SITES_RED = 17 --factory method used in multiple unit tests function IADSContactFactory(unitName) local contact = Unit.getByName(unitName) local radarContact = {} radarContact.object = contact local iadsContact = SkynetIADSContact:create(radarContact) iadsContact:refresh() return iadsContact end function createDeadEvent() local event = {} event.id = world.event.S_EVENT_DEAD return event end lu.LuaUnit.run() --clean mist left over scheduled tasks form unit tests, check there are no left over tasks in the IADS local i = 0 while i < 10000 do local id = mist.removeFunction(i) i = i + 1 if id then env.info("WARNING: IADS left over Tasks") end end end ================================================ FILE: unit-tests/test-skynet-iads-abstract-dcs-object-wrapper.lua ================================================ do TestSkynetIADSAbstractDCSObjectWrapper = {} function TestSkynetIADSAbstractDCSObjectWrapper:setUp() self.abstractObjectWrapper = SkynetIADSAbstractDCSObjectWrapper:create(Unit.getByName('EW-SA-6')) end function TestSkynetIADSAbstractDCSObjectWrapper:tearDown() end function TestSkynetIADSAbstractDCSObjectWrapper:testGetName() lu.assertEquals(self.abstractObjectWrapper:getName(), 'EW-SA-6') self.abstractObjectWrapper.dcsRepresentation = nil --test to see if name is still returned after object wrapped is nil lu.assertEquals(self.abstractObjectWrapper:getName(), 'EW-SA-6') end function TestSkynetIADSAbstractDCSObjectWrapper:testGetTypeName() lu.assertEquals(self.abstractObjectWrapper:getTypeName(), 'Kub 1S91 str') self.abstractObjectWrapper.dcsRepresentation = nil lu.assertEquals(self.abstractObjectWrapper:getTypeName(), 'Kub 1S91 str') end function TestSkynetIADSAbstractDCSObjectWrapper:testIsExist() lu.assertEquals(self.abstractObjectWrapper:isExist(), true) self.abstractObjectWrapper.dcsRepresentation = nil lu.assertEquals(self.abstractObjectWrapper:isExist(), false) end function TestSkynetIADSAbstractDCSObjectWrapper:testGetDCSRepresentation() lu.assertEquals(self.abstractObjectWrapper:getDCSRepresentation(), Unit.getByName('EW-SA-6')) end function TestSkynetIADSAbstractDCSObjectWrapper:testInsertToTableIfNotAlreadyAdded() local tbl = {} local mock = {} table.insert(tbl, mock) local result = self.abstractObjectWrapper:insertToTableIfNotAlreadyAdded(tbl, mock) lu.assertEquals(#tbl, 1) lu.assertEquals(result, false) local mock2 = {} local result2 = self.abstractObjectWrapper:insertToTableIfNotAlreadyAdded(tbl, mock2) lu.assertEquals(#tbl, 2) lu.assertEquals(result2, true) end end ================================================ FILE: unit-tests/test-skynet-iads-abstract-element.lua ================================================ do TestSkynetIADSAbstractElement = {} function TestSkynetIADSAbstractElement:setUp() self.iads = SkynetIADS:create() self.abstractElement = SkynetIADSAbstractElement:create(Group.getByName("SAM-SA-6-2"), self.iads) --mock this fucntion we test it once in testCheckOneGenericObjectAliveForUnitWorks function self.abstractElement:setToCorrectAutonomousState() end end function TestSkynetIADSAbstractElement:tearDown() self.abstractElement:cleanUp() end -- by default an abstractElement will return true if no power source or connection node ist set function TestSkynetIADSAbstractElement:testHasActiveConnectionNodeByDefaultIfNoneIsSet() lu.assertEquals(self.abstractElement:genericCheckOneObjectIsAlive({}), true) lu.assertEquals(self.abstractElement:hasActiveConnectionNode(), true) lu.assertEquals(self.abstractElement:hasWorkingPowerSource(), true) end function TestSkynetIADSAbstractElement:testCheckOneGenericObjectAliveForUnitWorks() local unit = Unit.getByName('SAM-SA-6-2-connection-node-unit') local called = false function self.abstractElement:informChildrenOfStateChange() called = true end self.abstractElement:addConnectionNode(unit) lu.assertEquals(called, true) lu.assertEquals(self.abstractElement:genericCheckOneObjectIsAlive(self.abstractElement.connectionNodes), true) lu.assertEquals(self.abstractElement:hasActiveConnectionNode(), true) trigger.action.explosion(unit:getPosition().p, 1000) lu.assertEquals(self.abstractElement:genericCheckOneObjectIsAlive(self.abstractElement.connectionNodes), false) lu.assertEquals(self.abstractElement:hasActiveConnectionNode(), false) end function TestSkynetIADSAbstractElement:testCheckOneGenericObjectAliveForStaticObjectsWorks() local static = StaticObject.getByName('SAM-SA-6-2-coonection-node-static') self.abstractElement:addConnectionNode(static) lu.assertEquals(self.abstractElement:genericCheckOneObjectIsAlive(self.abstractElement.connectionNodes), true) lu.assertEquals(self.abstractElement:hasActiveConnectionNode(), true) trigger.action.explosion(static:getPosition().p, 1000) lu.assertEquals(self.abstractElement:genericCheckOneObjectIsAlive(self.abstractElement.connectionNodes), false) lu.assertEquals(self.abstractElement:hasActiveConnectionNode(), false) end function TestSkynetIADSAbstractElement:testPowerSourceAndConnectionNodeStaticObjectAndDestrutionSuccessful() local powerSource = StaticObject.getByName("test-ground-vehicle-power-source") local connectionNode = StaticObject.getByName("test-ground-vehicle-connection-node") self.abstractElement:addPowerSource(powerSource) self.abstractElement:addConnectionNode(connectionNode) lu.assertEquals(self.abstractElement:hasWorkingPowerSource(), true) lu.assertEquals(self.abstractElement:hasActiveConnectionNode(), true) trigger.action.explosion(powerSource:getPosition().p, 100) trigger.action.explosion(connectionNode:getPosition().p, 500) lu.assertEquals(self.abstractElement:hasWorkingPowerSource(), false) lu.assertEquals(self.abstractElement:hasActiveConnectionNode(), false) end function TestSkynetIADSAbstractElement:testGetNatoName() lu.assertEquals(self.abstractElement:getNatoName(), "UNKNOWN") end function TestSkynetIADSAbstractElement:testGetDescription() lu.assertEquals(self.abstractElement:getDescription(), "IADS ELEMENT: SAM-SA-6-2 | Type: UNKNOWN") end function TestSkynetIADSAbstractElement:testGetDCSRepresentation() lu.assertEquals(self.abstractElement:getDCSRepresentation(), Group.getByName("SAM-SA-6-2")) end function TestSkynetIADSAbstractElement:testGetDCSName() lu.assertEquals(self.abstractElement:getDCSName(), "SAM-SA-6-2") --overwrite the DCSRepresentation to test caching on the DCS unit / group name function self.abstractElement:getDCSRepresentation() return nil end lu.assertEquals(self.abstractElement:getDCSName(), "SAM-SA-6-2") end end ================================================ FILE: unit-tests/test-skynet-iads-abstract-radar-element.lua ================================================ do TestSkynetIADSAbstractRadarElement = {} function TestSkynetIADSAbstractRadarElement:setUp() if self.samSiteName then self.skynetIADS = SkynetIADS:create() local samSite = Group.getByName(self.samSiteName) self.samSite = SkynetIADSSamSite:create(samSite, self.skynetIADS) -- we overrite this method since it returns radar contacts in the DCS world which mess up the tests. function self.samSite:getDetectedTargets() return {} end self.samSite:setupElements() self.samSite:goLive() end end function TestSkynetIADSAbstractRadarElement:tearDown() if self.samSite then self.samSite:goDark() self.samSite:cleanUp() end if self.skynetIADS then self.skynetIADS:deactivate() end self.samSite = nil self.samSiteName = nil end --TODO: test other calls in the GoDark Method function TestSkynetIADSAbstractRadarElement:testGoDark() self.samSiteName = "SAM-SA-6-2" self:setUp() local mockRepresentation = {} local emissionState = nil function mockRepresentation:enableEmission(state) emissionState = false end function mockRepresentation:isExist() return true end function self.samSite:getDCSRepresentation() return mockRepresentation end local mockController = {} function mockController:setOption(option) end function mockRepresentation:getController() return mockController end table.insert(self.samSite.cachedTargets,{"Mock1"}) self.samSite:goDark() lu.assertEquals(self.samSite:isActive(), false) lu.assertEquals(emissionState, false) lu.assertEquals(#self.samSite.cachedTargets, 0) end --TODO: test other calls in the GoLive Method function TestSkynetIADSAbstractRadarElement:testGoLive() self.samSiteName = "SAM-SA-6-2" self:setUp() self.samSite:goDark() local mockRepresentation = {} local emissionState = nil function mockRepresentation:enableEmission(state) emissionState = true end function mockRepresentation:isExist() return true end function self.samSite:getDCSRepresentation() return mockRepresentation end local mockController = {} function mockController:setOption(option) end function mockRepresentation:getController() return mockController end --test so see if controller is called when setting site live: call = 0 function mockController:setOnOff(state) lu.assertEquals(state, true) call = 1 end self.samSite:goLive() lu.assertEquals(call, 1) lu.assertEquals(self.samSite:isActive(), true) lu.assertEquals(emissionState, true) end function TestSkynetIADSAbstractRadarElement:testGoDarkDueToHARMTestIfAIisOff() self.samSiteName = "SAM-SA-2" self:setUp() local mockController = {} local call = 0 function mockController:setOnOff(state) lu.assertEquals(state, false) call = 1 end function self.samSite:getController() return mockController end self.samSite:goSilentToEvadeHARM(10) lu.assertEquals(self.samSite:isActive(), false) lu.assertEquals(call, 1) --test so no controller call is made if sam site is destroyed: self.samSiteName = "SAM-SA-2" self:setUp() local mockController = {} call = 0 function mockController:setOnOff(state) call = call + 1 end function self.samSite:getController() return mockController end function self.samSite:isDestroyed() return true end self.samSite:goSilentToEvadeHARM(10) lu.assertEquals(self.samSite:isActive(), false) lu.assertEquals(call, 0) end function TestSkynetIADSAbstractRadarElement:testCanEngageAirWeapons() self.samSiteName = "SAM-SA-6-2" self:setUp() local called = false local mockController = {} function mockController:setOption(option, value) lu.assertEquals(option, AI.Option.Ground.id.ENGAGE_AIR_WEAPONS) lu.assertEquals(value, true) called = true end local mockDCSRepresenation = {} function mockDCSRepresenation:getController() return mockController end function self.samSite:getDCSRepresentation() return mockDCSRepresenation end function self.samSite:getController() return mockController end --by default SAM site is not set to engage air weapons in Skynet: lu.assertEquals(self.samSite:getCanEngageAirWeapons(), false) lu.assertEquals(self.samSite:setCanEngageAirWeapons(true), self.samSite) lu.assertEquals(self.samSite:getCanEngageAirWeapons(), true) lu.assertEquals(called, true) --we test that calling setEngageAirWeapons with true on a SAM site that can by default engage harms also sets canEngageHarm to true function mockController:setOption(option, value) end self.samSite.dataBaseSupportedTypesCanEngageHARM = true self.samSite:setCanEngageAirWeapons(false) self.samSite:setCanEngageAirWeapons(true) lu.assertEquals(self.samSite:getCanEngageHARM(), true) self.samSite = nil end function TestSkynetIADSAbstractRadarElement:testCanEngageHARM() self.samSiteName = "SAM-SA-6-2" self:setUp() local called = false function self.samSite:setCanEngageAirWeapons(state) lu.assertEquals(state, true) called = true end lu.assertEquals(self.samSite:setCanEngageHARM(true), self.samSite) lu.assertEquals(self.samSite:getCanEngageHARM(), true) lu.assertEquals(called, true) local called = false self.samSite:setCanEngageHARM(false) lu.assertEquals(self.samSite:getCanEngageHARM(), false) lu.assertEquals(called, false) end function TestSkynetIADSAbstractRadarElement:testAddParentRadarAndClearParentRadars() self.samSiteName = "SAM-SA-6-2" self:setUp() local called = false function self.samSite:setToCorrectAutonomousState() called = true end lu.assertEquals(#self.samSite:getParentRadars(), 0) local parentRad1 = {} self.samSite:addParentRadar(parentRad1) lu.assertEquals(#self.samSite:getParentRadars(), 1) --try adding the same radar again, make sure its not added: self.samSite:addParentRadar(parentRad1) lu.assertEquals(#self.samSite:getParentRadars(), 1) local parentRad2 = {} self.samSite:addParentRadar(parentRad2) lu.assertEquals(#self.samSite:getParentRadars(), 2) lu.assertEquals(self.samSite:getParentRadars()[1], parentRad2) lu.assertEquals(self.samSite:getParentRadars()[2], parentRad1) lu.assertEquals(called, true) --reset array to prevent teardown issues with mock objects self.samSite:clearParentRadars() lu.assertEquals(#self.samSite:getParentRadars(), 0) end function TestSkynetIADSAbstractRadarElement:testAddChildRadarAndClearChildRadars() self.samSiteName = "SAM-SA-6-2" self:setUp() lu.assertEquals(#self.samSite:getChildRadars(), 0) local childRad1 = {} self.samSite:addChildRadar(childRad1) lu.assertEquals(#self.samSite:getChildRadars(), 1) --try adding the same radar again, make sure its not added: self.samSite:addChildRadar(childRad1) lu.assertEquals(#self.samSite:getChildRadars(), 1) local childRad2 = {} self.samSite:addChildRadar(childRad2) lu.assertEquals(#self.samSite:getChildRadars(), 2) lu.assertEquals(self.samSite:getChildRadars()[1], childRad1) lu.assertEquals(self.samSite:getChildRadars()[2], childRad2) --reset array to prevent teardown issues with mock objects self.samSite:clearChildRadars() lu.assertEquals(#self.samSite:getChildRadars(), 0) end function TestSkynetIADSAbstractRadarElement:testGetUsableChildRadars() self.samSiteName = "SAM-SA-6-2" self:setUp() function self.samSite:setToCorrectAutonomousState() end local childRad1 = {} function childRad1:hasWorkingPowerSource() return false end function childRad1:hasActiveConnectionNode() return true end self.samSite:addChildRadar(childRad1) lu.assertEquals(#self.samSite:getUsableChildRadars(), 0) function childRad1:hasWorkingPowerSource() return true end function childRad1:hasActiveConnectionNode() return false end lu.assertEquals(#self.samSite:getUsableChildRadars(), 0) function childRad1:hasWorkingPowerSource() return true end function childRad1:hasActiveConnectionNode() return true end lu.assertEquals(#self.samSite:getUsableChildRadars(), 1) --reset array to prevent teardown issues with mock objects self.samSite.childRadars = {} end function TestSkynetIADSAbstractRadarElement:testInformChildrenOfStateChange() self.samSiteName = "SAM-SA-6-2" self:setUp() --we ensure the moose connector is updated if a state of an IADS radar changes local updateCalled = false local mockMoose = {} function mockMoose:update() updateCalled = true end function self.samSite.iads:getMooseConnector() return mockMoose end local calls = 0 local childRad1 = {} function childRad1:setToCorrectAutonomousState() calls = calls + 1 end self.samSite:addChildRadar(childRad1) local childRad2 = {} function childRad2:setToCorrectAutonomousState() calls = calls + 1 end self.samSite:addChildRadar(childRad2) self.samSite:informChildrenOfStateChange() lu.assertEquals(updateCalled, true) lu.assertEquals(calls, 2) end --this test is related to testInformChildrenOfStateChange it tests, if SAM site go to their correct state depending on destruction of connection nodes and power sources -- TODO: remove SkynetIADS variable to reduce test cupling its not needed for this test, sam and ew site could just be instantiated by ther own. function TestSkynetIADSAbstractRadarElement:testSAMSiteAndEWRadarLoosesConnectionAndPowerSourceThenAddANewOneAgain() self:tearDown() self.testIADS = SkynetIADS:create() local connectionNode = StaticObject.getByName('SA-6 Connection Node-autonomous-test') local nonAutonomousSAM = self.testIADS:addSAMSite('SAM-SA-6'):addConnectionNode(connectionNode) self.testIADS:addEarlyWarningRadar('EW-west2') self.testIADS:buildRadarCoverage() lu.assertEquals(nonAutonomousSAM:getAutonomousState(), false) trigger.action.explosion(connectionNode:getPosition().p, 500) --we simulate a call to the event, since in game will be triggered to late to for later checks in this unit test nonAutonomousSAM:onEvent(createDeadEvent()) lu.assertEquals(nonAutonomousSAM:getAutonomousState(), true) local connectionNodeReAdd = StaticObject.getByName('SA-6 Connection Node-autonomous-readd') nonAutonomousSAM:addConnectionNode(connectionNodeReAdd) lu.assertEquals(nonAutonomousSAM:getAutonomousState(), false) local ewRadar = self.testIADS:getEarlyWarningRadarByUnitName('EW-west2') ewRadar:addConnectionNode(StaticObject.getByName('ew-west-connection-node-test')) trigger.action.explosion(StaticObject.getByName('ew-west-connection-node-test'):getPosition().p, 500) --we simulate a call to the event, since in game will be triggered to late to for later checks in this unit test ewRadar:onEvent(createDeadEvent()) lu.assertEquals(ewRadar:hasActiveConnectionNode(), false) lu.assertEquals(ewRadar:getAutonomousState(), true) lu.assertEquals(nonAutonomousSAM:getAutonomousState(), true) ewRadar:addConnectionNode(Unit.getByName('connection-node-ew')) lu.assertEquals(nonAutonomousSAM:getAutonomousState(), false) ewRadar:addPowerSource(StaticObject.getByName('ew-power-source')) trigger.action.explosion(StaticObject.getByName('ew-power-source'):getPosition().p, 500) --we simulate a call to the event, since in game will be triggered to late to for later checks in this unit test ewRadar:onEvent(createDeadEvent()) lu.assertEquals(ewRadar:hasWorkingPowerSource(), false) lu.assertEquals(nonAutonomousSAM:getAutonomousState(), true) ewRadar:addPowerSource(StaticObject.getByName('ew-power-source-2')) lu.assertEquals(ewRadar:isActive(), true) lu.assertEquals(nonAutonomousSAM:getAutonomousState(), false) --test if a SAM site will stay active if it's in EW mode and it's parent EW radar becomes inoperable as long as SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK is set this will work nonAutonomousSAM:setActAsEW(true) trigger.action.explosion(StaticObject.getByName('ew-power-source-2'):getPosition().p, 500) --we simulate a call to the event, since in game will be triggered to late to for later checks in this unit test ewRadar:onEvent(createDeadEvent()) lu.assertEquals(ewRadar:isActive(), false) lu.assertEquals(ewRadar:hasWorkingPowerSource(), false) lu.assertEquals(nonAutonomousSAM:isActive(), true) lu.assertEquals(nonAutonomousSAM:getAutonomousState(), true) --test if command center is destroyed all SAM sites and EW radars should go autonomous: ewRadar.powerSources = {} nonAutonomousSAM.powerSources = {} nonAutonomousSAM:setActAsEW(false) ewRadar:setToCorrectAutonomousState() ewRadar:informChildrenOfStateChange() lu.assertEquals(ewRadar:isActive(), true) lu.assertEquals(nonAutonomousSAM:isActive(), false) lu.assertEquals(nonAutonomousSAM:getAutonomousState(), false) local commandCenter = StaticObject.getByName('command-center-unit-test') local comCenter = self.testIADS:addCommandCenter(commandCenter) self.testIADS:buildRadarCoverage() lu.assertEquals(#comCenter:getChildRadars(), 2) trigger.action.explosion(commandCenter:getPosition().p, 5000) --we simulate a call to the event, since in game will be triggered to late to for later checks in this unit test comCenter:onEvent(createDeadEvent()) lu.assertEquals(self.testIADS:isCommandCenterUsable(), false) lu.assertEquals(ewRadar:getAutonomousState(), true) lu.assertEquals(nonAutonomousSAM:getAutonomousState(), true) self.testIADS:deactivate() end --TODO: add tests for more check true / false combiations connectionnode power source etc. function TestSkynetIADSAbstractRadarElement:testSetToCorrectAutonomousState() self.samSiteName = "SAM-SA-6-2" self:setUp() self.samSite:goAutonomous() lu.assertEquals(self.samSite:getAutonomousState(), true) function self.samSite:hasActiveConnectionNode() return true end local parentRad = {} function parentRad:hasWorkingPowerSource() return true end function parentRad:hasActiveConnectionNode() return true end function parentRad:getActAsEW() return true end function parentRad:isDestroyed() return false end self.samSite:addParentRadar(parentRad) self.samSite:setToCorrectAutonomousState() lu.assertEquals(self.samSite:getAutonomousState(), false) --check when SAM site does not have active connection node self.samSite:goAutonomous() lu.assertEquals(self.samSite:getAutonomousState(), true) function self.samSite:hasActiveConnectionNode() return false end local parentRad = {} function parentRad:hasWorkingPowerSource() return true end function parentRad:hasActiveConnectionNode() return true end function parentRad:getActAsEW() return true end function parentRad:isDestroyed() return false end self.samSite:addParentRadar(parentRad) self.samSite:setToCorrectAutonomousState() lu.assertEquals(self.samSite:getAutonomousState(), true) end function TestSkynetIADSAbstractRadarElement:testWillGoLiveWhenAutonomousAndHARMDefenceFinished() self.samSiteName = "SAM-SA-6-2" self:setUp() self.samSite:setAutonomousBehaviour(SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DCS_AI) self.samSite:goSilentToEvadeHARM(1) self.samSite:finishHarmDefence() lu.assertEquals(self.samSite:isActive(), true) end -- TODO: write test for updateMissilesInFlight in AbstractRadarElement function TestSkynetIADSAbstractRadarElement:testUpdateMissilesInFlight() self.samSiteName = "SAM-SA-6-2" self:setUp() local mockMissile1 = {} function mockMissile1:isExist() return false end local mockMissile2 = {} function mockMissile2:isExist() return true end self.samSite.missilesInFlight = {mockMissile1, mockMissile2} lu.assertEquals(#self.samSite.missilesInFlight, 2) lu.assertEquals(self.samSite:hasMissilesInFlight(), true) self.samSite:updateMissilesInFlight() lu.assertEquals(#self.samSite.missilesInFlight, 1) lu.assertEquals(self.samSite:hasMissilesInFlight(), true) self.samSite.missilesInFlight = {mockMissile1} lu.assertEquals(#self.samSite.missilesInFlight, 1) lu.assertEquals(self.samSite:hasMissilesInFlight(), true) self.samSite:updateMissilesInFlight() lu.assertEquals(#self.samSite.missilesInFlight, 0) lu.assertEquals(self.samSite:hasMissilesInFlight(), false) end function TestSkynetIADSAbstractRadarElement:testShutDownShilkaWhenOutOfAmmo() local launcherData = { { count=503, desc={ _origin="", box={ max={x=2.2344591617584, y=0.12504191696644, z=0.12113922089338}, min={x=-6.61008644104, y=-0.12504199147224, z=-0.12113920599222} }, category=0, displayName="23mm AP", life=2, typeName="weapons.shells.2A7_23_AP", warhead={caliber=23, explosiveMass=0, mass=0.189, type=0} } }, { count=1501, desc={ _origin="", box={ max={x=2.2344591617584, y=0.12504191696644, z=0.12113922089338}, min={x=-6.61008644104, y=-0.12504199147224, z=-0.12113920599222} }, category=0, displayName="23mm HE", life=2, typeName="weapons.shells.2A7_23_HE", warhead={caliber=23, explosiveMass=0.189, mass=0.189, type=1} } } } self.samSiteName = "SAM-Shilka" self:setUp() local launcher = self.samSite:getLaunchers()[1] local mockDCSObjcect = {} function mockDCSObjcect:getAmmo() launcherData[1].count = 300 launcherData[2].count = 200 return launcherData end ---simulate firing of 1 missile function launcher:getDCSRepresentation() return mockDCSObjcect end lu.assertEquals(launcher:getInitialNumberOfShells(), 2004) lu.assertEquals(launcher:getRemainingNumberOfShells(), 500) lu.assertEquals(self.samSite:getInitialNumberOfShells(), 2004) lu.assertEquals(self.samSite:getRemainingNumberOfShells(), 500) lu.assertEquals(self.samSite:hasRemainingAmmo(), true) lu.assertEquals(self.samSite:isActive(), true) self.samSite:goDarkIfOutOfAmmo() lu.assertEquals(self.samSite:isActive(), true) local mockDCSObjcect = {} function mockDCSObjcect:getAmmo() launcherData[1].count = 0 launcherData[2].count = 0 return launcherData end ---simulate firing of 1 missile function launcher:getDCSRepresentation() return mockDCSObjcect end lu.assertEquals(launcher:getInitialNumberOfShells(), 2004) lu.assertEquals(launcher:getRemainingNumberOfShells(), 0) lu.assertEquals(self.samSite:getInitialNumberOfShells(), 2004) lu.assertEquals(self.samSite:getRemainingNumberOfShells(), 0) lu.assertEquals(self.samSite:hasRemainingAmmo(), false) lu.assertEquals(self.samSite:isActive(), true) self.samSite:goDarkIfOutOfAmmo() lu.assertEquals(self.samSite:isActive(), false) end function TestSkynetIADSAbstractRadarElement:testCreateSamSiteFromInvalidGroup() self.samSiteName = "Invalid-for-sam" self:setUp() lu.assertStrMatches(self.samSite:getNatoName(), "UNKNOWN") lu.assertEquals(#self.samSite:getRadars(), 0) lu.assertEquals(#self.samSite:getLaunchers(), 0) lu.assertEquals(#self.samSite:getSearchRadars(), 0) lu.assertEquals(#self.samSite:getTrackingRadars(), 0) end function TestSkynetIADSAbstractRadarElement:testSamSiteGroupContainingOfOneUnitOnlySA8() self.samSiteName = "SAM-SA-8" self:setUp() lu.assertEquals(#self.samSite:getRadars(), 1) lu.assertEquals(#self.samSite:getLaunchers(), 1) lu.assertEquals(#self.samSite:getSearchRadars(), 1) lu.assertEquals(#self.samSite:getTrackingRadars(), 0) lu.assertEquals(self.samSite:getNatoName(), "SA-8") end function TestSkynetIADSAbstractRadarElement:testHARMDefenceStates() self.samSiteName = "SAM-SA-6" self:setUp() lu.assertEquals(self.samSite:isActive(), true) lu.assertEquals(self.samSite:isScanningForHARMs(), true) self.samSite:goSilentToEvadeHARM() lu.assertEquals(self.samSite:isScanningForHARMs(), false) lu.assertEquals(self.samSite:isActive(), false) end function TestSkynetIADSAbstractRadarElement:testGoLiveFailsWhenInHARMDefenceMode() self.samSiteName = "SAM-SA-6" self:setUp() lu.assertEquals(self.samSite:isActive(), true) lu.assertEquals(self.samSite:isScanningForHARMs(), true) self.samSite:goSilentToEvadeHARM() lu.assertEquals(self.samSite:isActive(), false) self.samSite:goLive() lu.assertEquals(self.samSite:isActive(), false) end function TestSkynetIADSAbstractRadarElement:testHARMTimeToImpactCalculation() self.samSiteName = "SAM-SA-6" self:setUp() lu.assertEquals(self.samSite:getSecondsToImpact(100, 10), 36000) lu.assertEquals(self.samSite:getSecondsToImpact(10, 400), 90) lu.assertEquals(self.samSite:getSecondsToImpact(0, 400), 0) lu.assertEquals(self.samSite:getSecondsToImpact(400, 0), 0) end function TestSkynetIADSAbstractRadarElement:testSlantRangeCalculationForHARMDefence() self.samSiteName = "SAM-SA-6-2" self:setUp() local iadsContact = IADSContactFactory("test-distance-calculation") local radarUnit = self.samSite:getRadars()[1] local distanceSlantRange = self.samSite:getDistanceInMetersToContact(iadsContact, radarUnit:getPosition().p) local straightLine = mist.utils.round(mist.utils.get2DDist(radarUnit:getPosition().p, iadsContact:getPosition().p), 0) lu.assertEquals(distanceSlantRange > straightLine, true) end function TestSkynetIADSAbstractRadarElement:testFinishHARMDefence() self.samSiteName = "SAM-SA-6-2" self:setUp() self.samSite:goSilentToEvadeHARM(10) lu.assertEquals(self.samSite:isActive(), false) self.samSite:finishHarmDefence() lu.assertEquals(self.samSite.harmShutdownTime, 0) self.samSite:goLive() lu.assertEquals(self.samSite:isActive(), true) end function TestSkynetIADSAbstractRadarElement:testShutDownWhenOutOfMissiles() self.samSiteName = "SAM-SA-6-2" self:setUp() local launcher = self.samSite:getLaunchers()[1] lu.assertEquals(launcher:getInitialNumberOfMissiles(), 3) lu.assertEquals(launcher:getRemainingNumberOfMissiles(), 3) lu.assertEquals(self.samSite:getInitialNumberOfMissiles(), 3) lu.assertEquals(self.samSite:getRemainingNumberOfMissiles(), 3) local launcherData = { { count=3, desc={ Nmax=16, RCS=0.1059999987483, _origin="", altMax=8000, altMin=30, box={ max={x=2.9061908721924, y=0.43574807047844, z=0.4395649433136}, min={x=-2.9048342704773, y=-0.43574807047844, z=-0.4395649433136}, }, category=1, displayName="3M9M Kub (SA-6 Gainful)", fuseDist=12, guidance=4, life=2, missileCategory=2, rangeMaxAltMax=25000, rangeMaxAltMin=25000, rangeMin=4000, typeName="SA3M9M", warhead={caliber=330, explosiveMass=59, mass=59, type=1}, } } } local mockDCSObjcect = {} function mockDCSObjcect:getAmmo() launcherData[1].count = 2 return launcherData end ---simulate firing of 1 missile function launcher:getDCSRepresentation() return mockDCSObjcect end lu.assertEquals(launcher:getInitialNumberOfMissiles(), 3) lu.assertEquals(launcher:getRemainingNumberOfMissiles(), 2) lu.assertEquals(self.samSite:getInitialNumberOfMissiles(), 3) lu.assertEquals(self.samSite:getRemainingNumberOfMissiles(), 2) lu.assertEquals(self.samSite:hasRemainingAmmo(), true) self.samSite:goDarkIfOutOfAmmo() lu.assertEquals(self.samSite:isActive(), true ) function mockDCSObjcect:getAmmo() launcherData[1].count = 1 return launcherData end lu.assertEquals(launcher:getInitialNumberOfMissiles(), 3) lu.assertEquals(launcher:getRemainingNumberOfMissiles(), 1) lu.assertEquals(self.samSite:getInitialNumberOfMissiles(), 3) lu.assertEquals(self.samSite:getRemainingNumberOfMissiles(), 1) lu.assertEquals(self.samSite:hasRemainingAmmo(), true) self.samSite:goDarkIfOutOfAmmo() lu.assertEquals(self.samSite:isActive(), true ) --DCS missile info is nil when no ammo is remaining function mockDCSObjcect:getAmmo() return nil end lu.assertEquals(launcher:getInitialNumberOfMissiles(), 3) lu.assertEquals(launcher:getRemainingNumberOfMissiles(), 0) lu.assertEquals(self.samSite:getInitialNumberOfMissiles(), 3) lu.assertEquals(self.samSite:getRemainingNumberOfMissiles(), 0) lu.assertEquals(self.samSite:hasRemainingAmmo(), false) self.samSite:goDarkIfOutOfAmmo() lu.assertEquals(self.samSite:isActive(), false ) self.samSite:goLive() lu.assertEquals(self.samSite:isActive(), false ) end function TestSkynetIADSAbstractRadarElement:testActAsEarlyWarningRadar() self.samSiteName = "SAM-SA-6" self:setUp() self.samSite:goDark() lu.assertEquals(self.samSite:isActive(), false) self.samSite:setActAsEW(true) lu.assertEquals(self.samSite:isActive(), true) self.samSite:targetCycleUpdateEnd() -- SAM Site should not shut down when out of ammo and in EW Mode function self.samSite:getRemainingNumberOfMissiles() return 0 end self.samSite:goDarkIfOutOfAmmo() lu.assertEquals(self.samSite:isActive(), true) lu.assertEquals(self.samSite:isActive(), true) -- test when stopping EW mode the child SAM site should go dark local samSA62 = SkynetIADSSamSite:create(Group.getByName('SAM-SA-6-2'), self.skynetIADS) samSA62:setAutonomousBehaviour(SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK) samSA62:setupElements() samSA62:goLive() self.samSite:addChildRadar(samSA62) samSA62:addParentRadar(self.samSite) self.samSite:informChildrenOfStateChange() lu.assertEquals(samSA62:getAutonomousState(), false) self.samSite:setActAsEW(false) lu.assertEquals(self.samSite:isActive(), false) lu.assertEquals(samSA62:getAutonomousState(), true) lu.assertEquals(samSA62:isActive(), false) samSA62:cleanUp() end function TestSkynetIADSAbstractRadarElement:testInformOfContactInRangeWhenEarlyWaringRadar() self.samSiteName = "SAM-SA-6" self:setUp() self.samSite:setActAsEW(true) local mockContact = {} function self.samSite:isTargetInRange(target) lu.assertIs(target, mockContact) return false end self.samSite:targetCycleUpdateStart() lu.assertEquals(self.samSite:isActive(), true) self.samSite:informOfContact(mockContact) lu.assertEquals(self.samSite:isActive(), true) self.samSite:targetCycleUpdateEnd() lu.assertEquals(self.samSite:isActive(), true) end function TestSkynetIADSAbstractRadarElement:testSA2InformOfContactTargetInRangeMethod() self.samSiteName = "SAM-SA-2" self:setUp() --DCS AI radar instantly detects contact in test, so Site will not go dark, therefore we overwrite the method in this test function self.samSite:getDetectedTargets() return {} end self.samSite:goDark() lu.assertEquals(self.samSite:isActive(), false) local target = IADSContactFactory('test-in-firing-range-of-sa-2') self.samSite:informOfContact(target) local searchRadar = self.samSite:getSearchRadars()[1] lu.assertEquals(searchRadar:getTypeName(), 'p-19 s-125 sr') local sensors = Unit.getByName('Unit #005'):getSensors() lu.assertEquals(searchRadar:getMaxRangeFindingTarget(), 53499.2265625) local launcher = self.samSite:getLaunchers()[1] lu.assertEquals(launcher:getRange(), 40000) local trackingRadar = self.samSite:getTrackingRadars()[1] --in its current implementation the SA-2 tracking radar returns the values of the search radar, I presume its only a placeholder in DCS lu.assertEquals(trackingRadar:getMaxRangeFindingTarget(), 53499.2265625) lu.assertEquals(self.samSite:isActive(), true) lu.assertEquals(self.samSite:isTargetInRange(target), true) end function TestSkynetIADSAbstractRadarElement:testSA2WillNotGoDarkIfTargetIsInRange() self.samSiteName = "SAM-SA-2" self:setUp() local target = IADSContactFactory('test-in-firing-range-of-sa-2') --we return a detected target, to pervent SAM site going dark function self.samSite:getDetectedTargets() local targets = {} table.insert(targets, target) return targets end self.samSite:informOfContact(target) self.samSite:goDark() lu.assertEquals(self.samSite:isActive(), true) end function TestSkynetIADSAbstractRadarElement:testSA2WillNotGoDarkIfOutOfMisslesAndMissilesAreStillInFlight() self.samSiteName = "SAM-SA-2" self:setUp() lu.assertEquals(self.samSite:hasMissilesInFlight(), false) local mockMissileInFlight = {} function mockMissileInFlight:isExist() return true end local missiles = {} table.insert(missiles, mockMissileInFlight) self.samSite.missilesInFlight = missiles lu.assertEquals(self.samSite:hasMissilesInFlight(), true) lu.assertEquals(#self.samSite:getDetectedTargets(), 0) lu.assertEquals(self.samSite:isActive(), true) self.samSite:goDark() lu.assertEquals(self.samSite:isActive(), true) end function TestSkynetIADSAbstractRadarElement:testSA2WillGoDarkWithTargetsInRangeAndHARMDetected() self.samSiteName = "SAM-SA-2" self:setUp() local target = IADSContactFactory('test-in-firing-range-of-sa-2') function self.samSite:getDetectedTargets() local targets = {} table.insert(targets, target) return targets end self.samSite:informOfContact(target) self.samSite:goSilentToEvadeHARM(5) lu.assertEquals(self.samSite:isActive(), false) end function TestSkynetIADSAbstractRadarElement:testSA2WillgoDarkIfOutOfAmmoNoMissilesAreInFlightAndTargetStillInRange() self.samSiteName = "SAM-SA-2" self:setUp() local target = IADSContactFactory('test-in-firing-range-of-sa-2') function self.samSite:getDetectedTargets() local targets = {} table.insert(targets, target) return targets end function self.samSite:getRemainingNumberOfMissiles() return 0 end local mockMissileInFlight = {} function mockMissileInFlight:isExist() return false end local missiles = {} lu.assertEquals(self.samSite:hasMissilesInFlight(), false) lu.assertEquals(self.samSite:getRemainingNumberOfMissiles(), 0) lu.assertEquals(self.samSite:isActive(), true) self.samSite:goDark() lu.assertEquals(self.samSite:isActive(), false) end --TODO: write test case: SAM is out of missiles, is currently dark, is informed of a target in range has not detected it with its own radar is not in harm defence mode function TestSkynetIADSAbstractRadarElement:testSA2OutOfMissilesNoMissilesInFlightIsInformedOfTargetByIADSHasNotDetectedTargetWithOwnRadar() self.samSiteName = "SAM-SA-2" self:setUp() function self.samSite:getDetectedTargets() return {} end function self.samSite:getRemainingNumberOfMissiles() return 0 end self.samSite:goDark() lu.assertEquals(self.samSite:hasMissilesInFlight(), false) lu.assertEquals(self.samSite:isActive(), false) local target = IADSContactFactory('test-in-firing-range-of-sa-2') self.samSite:informOfContact(target) lu.assertEquals(self.samSite:isActive(), false) end --[[ This test is no longer required with setEmission available in dcs 2.7 function TestSkynetIADSAbstractRadarElement:testControllerNotDisabledWhenGoingDarkAndOutOfAmmo() self.samSiteName = "test-SAM-SA-2-test" self:setUp() local stateCalled = false local mockController = {} function mockController:setOnOff(state) lu.assertEquals(state, false) stateCalled = true end local optionCalled = false function mockController:setOption(opt, val) optionCalled = true end function self.samSite:getController() return mockController end function self.samSite:hasRemainingAmmo() return false end self.samSite:goDark() lu.assertEquals(stateCalled, false) lu.assertEquals(optionCalled, true) end --]] --[[ This test is no longer required with setEmission available in dcs 2.7 function TestSkynetIADSAbstractRadarElement:testControllerDisabledWhenGoingDarkAndHasRemainingAmmo() self.samSiteName = "test-SAM-SA-2-test" self:setUp() local stateCalled = false local mockController = {} function mockController:setOnOff(state) lu.assertEquals(state, false) stateCalled = true end local optionCalled = false function mockController:setOption(opt, val) optionCalled = true end function self.samSite:getController() return mockController end function self.samSite:hasRemainingAmmo() return true end self.samSite:goDark() lu.assertEquals(stateCalled, true) lu.assertEquals(optionCalled, false) end --]] function TestSkynetIADSAbstractRadarElement:testSA2GoLiveRangeInPercentInKillZone() self.samSiteName = "SAM-SA-2" self:setUp() lu.assertIs(self.samSite:getEngagementZone(), SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE) self.samSite:setGoLiveRangeInPercent(60) local target = IADSContactFactory('test-in-firing-range-of-sa-2') local launcher = self.samSite:getLaunchers()[1] lu.assertEquals(launcher:isInRange(target), false) lu.assertEquals(self.samSite:isTargetInRange(target), false) end function TestSkynetIADSAbstractRadarElement:testSA2GoLiveRangeInPercentSearchRange() self.samSiteName = "test-SAM-SA-2-test-2" self:setUp() self.samSite:setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE) self.samSite:setGoLiveRangeInPercent(80) local target = IADSContactFactory('test-outer-search-range') local radars = self.samSite:getSearchRadars() for i = 1, #radars do local radar = radars[i] lu.assertEquals(radar:isInRange(target), false) end lu.assertEquals(self.samSite:isTargetInRange(target), false) end function TestSkynetIADSAbstractRadarElement:testSA8GoLiveRangeInPercent() self.samSiteName = 'SAM-SA-8' self:setUp() self.samSite:goDark() local target = IADSContactFactory('test-sa-8-will-go-active') self.samSite:informOfContact(target) lu.assertEquals(self.samSite:isActive(), true) local launcher = self.samSite:getLaunchers()[1] self.samSite:setGoLiveRangeInPercent(20) self.samSite:goDark() self.samSite:informOfContact(target) lu.assertEquals(launcher:isInRange(target), false) lu.assertEquals(self.samSite:isActive(), false) end function TestSkynetIADSAbstractRadarElement:testShutDownTimes() self.samSiteName = "SAM-SA-6" self:setUp() lu.assertEquals(self.samSite:calculateMinimalShutdownTimeInSeconds(30), 60) local saveRandom = mist.random function mist.random(low, high) return 10 end lu.assertEquals(self.samSite:calculateMaximalShutdownTimeInSeconds(20), 30) mist.random = saveRandom end function TestSkynetIADSAbstractRadarElement:testDaisychainSAMOptions() self.samSiteName = "SAM-SA-11" self:setUp() local powerSource = StaticObject.getByName('SA-11-power-source') local connectionNode = StaticObject.getByName('SA-11-connection-node') local returnValue = self.samSite:setActAsEW(true):addPowerSource(powerSource):addConnectionNode(connectionNode):setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE):setGoLiveRangeInPercent(90):setAutonomousBehaviour(SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK) lu.assertIs(self.samSite, returnValue) lu.assertEquals(self.samSite:getActAsEW(), true) lu.assertEquals(self.samSite:getEngagementZone(), SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE) lu.assertEquals(self.samSite:getGoLiveRangeInPercent(), 90) lu.assertEquals(self.samSite:getAutonomousBehaviour(), SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK) lu.assertIs(self.samSite:getConnectionNodes()[1], connectionNode) lu.assertIs(self.samSite:getPowerSources()[1], powerSource) end function TestSkynetIADSAbstractRadarElement:testWillSAMShutDownWhenItLoosesPowerAndAMissileIsInFlight() self.samSiteName = "SAM-SA-11" self:setUp() local powerSource = StaticObject.getByName('SA-11-power-source') self.samSite:addPowerSource(powerSource) self.samSite:goLive() lu.assertEquals(self.samSite:hasWorkingPowerSource(), true) lu.assertEquals(self.samSite:isActive(), true) -- simulate that the SAM site has a missile in flight function self.samSite:hasMissilesInFlight() return true end --trigger the explosion of the power source, this should shut down the SAM site trigger.action.explosion(powerSource:getPosition().p, 100) --we simulate a call to the event, since in game will be triggered to late to for later checks in this unit test self.samSite:onEvent(createDeadEvent()) lu.assertEquals(self.samSite:hasWorkingPowerSource(), false) lu.assertEquals(self.samSite:isActive(), false) end function TestSkynetIADSAbstractRadarElement:testSetPointDefence() self.samSiteName = "SAM-SA-10" self:setUp() local pd = self.skynetIADS:addSAMSite("SAM-SA-15-1") lu.assertEquals(pd:getIsAPointDefence(), false) self.samSite:addPointDefence(pd) lu.assertEquals(pd:getIsAPointDefence(), true) end function TestSkynetIADSAbstractRadarElement:testPointDefencesGoLive() local stateSet = false local mockPD1 = {} function mockPD1:getActAsEW() return false end function mockPD1:setIsAPointDefence(state) end function mockPD1:setActAsEW(state) stateSet = true end function mockPD1:cleanUp() end self.samSiteName = "SAM-SA-10" self:setUp() self.samSite:addPointDefence(mockPD1) lu.assertEquals(self.samSite:pointDefencesGoLive(), true) lu.assertEquals(stateSet, true) self:tearDown() local stateSet = false local mockPD1 = {} function mockPD1:getActAsEW() return true end function mockPD1:setActAsEW(state) stateSet = true end function mockPD1:setIsAPointDefence(state) end function mockPD1:cleanUp() end self.samSiteName = "SAM-SA-10" self:setUp() self.samSite:addPointDefence(mockPD1) lu.assertEquals(self.samSite:pointDefencesGoLive(), false) lu.assertEquals(stateSet, false) end function TestSkynetIADSAbstractRadarElement:testPointDefenceActiveWhenSAMGoesDarkDueToHARMDefence() self.samSiteName = "SAM-SA-10" self:setUp() self.samSite:setActAsEW(true) --in this group there are two SA-15 units: local sa15 = Group.getByName("SAM-SA-15-1") local pointDefence = SkynetIADSSamSite:create(sa15, self.skynetIADS) pointDefence:setupElements() pointDefence:goLive() pointDefence:goDark() lu.assertEquals(self.samSite:addPointDefence(pointDefence), self.samSite) lu.assertEquals(#self.samSite:getPointDefences(), 1) self.samSite:goSilentToEvadeHARM() lu.assertEquals(self.samSite:isActive(), false) lu.assertEquals(pointDefence:isActive(), true) self.samSite:finishHarmDefence() self.samSite:goLive() lu.assertEquals(pointDefence:getActAsEW(), false) end function TestSkynetIADSAbstractRadarElement:testCleanUpOldObjectsIdentifiedAsHARMS() self.samSiteName = "SAM-SA-10" self:setUp() local mockContact = {} function mockContact:getAge() return 10 end table.insert(self.samSite.objectsIdentifiedAsHarms, mockContact) lu.assertEquals(self.samSite:getNumberOfObjectsItentifiedAsHARMS(), 1) end --TODO:write Unit test function TestSkynetIADSAbstractRadarElement:testPointDefenceWhenOnlyOneEWRadarIsActiveAndAmmoIsStillAvailable() end function TestSkynetIADSAbstractRadarElement:testPointDefencesAreNotActivatedWhenNoHARMSRemoved() self.samSiteName = "SAM-SA-10" self:setUp() local sa15 = Group.getByName("SAM-SA-15-1") local pointDefence = SkynetIADSSamSite:create(sa15, self.skynetIADS) self.samSite:addPointDefence(pointDefence) local calledStopPointDefence = false function self.samSite:pointDefencesStopActingAsEW() calledStopPointDefence = true end self.samSite:evaluateIfTargetsContainHARMs() lu.assertEquals(calledStopPointDefence, false) end function TestSkynetIADSAbstractRadarElement:testPointDefenceWillGoDarkWhenSAMItIsProtectingGoesDark() self.samSiteName = "SAM-SA-10" self:setUp() local sa15 = Group.getByName("SAM-SA-15-1") local pointDefence = SkynetIADSSamSite:create(sa15, self.skynetIADS) self.samSite:addPointDefence(pointDefence) pointDefence:setActAsEW(true) self.samSite:goDark() lu.assertEquals(pointDefence:isActive(), false) end --[[ function TestSkynetIADSAbstractRadarElement:testCallMethodOnTableElements() local test = {} function test:theMethod(value) env.info("call there: "..value) return {} end function test:theOtherMethod(value) env.info("call here: "..value) return {} end test.__index = test setmetatable(test, test) local testContainer = {} local handler = {} handler.__index = function(tbl, name) tbl[name] = function(self, ...) for i = 1, #self do self[i][name](self[i], ...) end return self end return tbl[name] end setmetatable(testContainer, handler) local tast = {} setmetatable(tast, test) tast.__index = test table.insert(testContainer, test) table.insert(testContainer, tast) tast['theOtherMethod'](tast, '101') lu.assertIs(testContainer:theMethod("99"), testContainer) lu.assertIs(testContainer:theOtherMethod("100"), testContainer) end --]] --[[ this test ensures that targets are cached in the sam site, calls to the getDetectedTargets function of the controller are cpu intensive multiple calls are made within miliseconds on the same SAM or EW site when updating the IADS status, therefore only the first call ist to the acutall controller after that results are cached for a few seconds (default IADS setting is for one update cycle, e.g. 5 seconds). --]] function TestSkynetIADSAbstractRadarElement:testCacheDetectedTargets() self.skynetIADS:addSAMSitesByPrefix('SAM') self.samSite = self.skynetIADS:getSAMSiteByGroupName('SAM-SA-10') self.samSite:goDark() self.samSite:goLive() -- deactivate no cache after goLive self.samSite.noCacheActiveForSecondsAfterGoLive = 0 lu.assertEquals(self.samSite:getDetectedTargets() == self.samSite:getDetectedTargets(), true) self.samSite.cachedTargetsMaxAge = -1 lu.assertEquals(self.samSite:getDetectedTargets() == self.samSite:getDetectedTargets(), false) end --[[ the IADS turns controllers of a SAM or EW site on and off. This has the advantage that a SAM site will react faster after beeing waken up by the IADS the down side is that the first call to getDetectedTarget() on a controller after a goLive returns no targets, this result is cached causing the SAM site to misbehave in the IADS. Therefore for the first seconds after goLive the cache of getDetectedTargets is bypassed, ensuring targets are stored and the SAM site behaves correctly. --]] function TestSkynetIADSAbstractRadarElement:testCacheInvalidatedFirstfewSecondsAfterControllerIsActivated() self.skynetIADS:addSAMSitesByPrefix('SAM') self.samSite = self.skynetIADS:getSAMSiteByGroupName('SAM-SA-10') self.samSite:goDark() self.samSite:goLive() lu.assertEquals(self.samSite:getDetectedTargets() == self.samSite:getDetectedTargets(), false) end function TestSkynetIADSAbstractRadarElement:testCalculateAspectInDegrees() self.samSite = self.skynetIADS:getSAMSiteByGroupName('SAM-SA-10') lu.assertEquals(self.samSite:calculateAspectInDegrees(0, 90), 90) lu.assertEquals(self.samSite:calculateAspectInDegrees(300, 90), 150) lu.assertEquals(self.samSite:calculateAspectInDegrees(010, 280), 90) lu.assertEquals(self.samSite:calculateAspectInDegrees(190, 350), 160) lu.assertEquals(self.samSite:calculateAspectInDegrees(090, 270), 180) lu.assertEquals(self.samSite:calculateAspectInDegrees(010, 170), 160) end function TestSkynetIADSAbstractRadarElement:testShallIgnoreHARMShutdown() self.samSiteName = "SAM-SA-10" self:setUp() --older sam site that can not engage HARMs (air weapons) function self.samSite:hasEnoughLaunchersToEngageMissiles(value) return true end function self.samSite:hasRemainingAmmoToEngageMissiles(value) return true end function self.samSite:getCanEngageHARM() return false end function self.samSite:pointDefencesHaveRemainingAmmo(value) return false end function self.samSite:pointDefencesHaveEnoughLaunchers(value) return false end lu.assertEquals(self.samSite:shallIgnoreHARMShutdown(), false) function self.samSite:hasEnoughLaunchersToEngageMissiles(value) return false end function self.samSite:hasRemainingAmmoToEngageMissiles(value) return false end function self.samSite:getCanEngageHARM() return true end function self.samSite:pointDefencesHaveRemainingAmmo(value) return false end function self.samSite:pointDefencesHaveEnoughLaunchers(value) return false end lu.assertEquals(self.samSite:shallIgnoreHARMShutdown(), false) function self.samSite:hasEnoughLaunchersToEngageMissiles(value) return false end function self.samSite:hasRemainingAmmoToEngageMissiles(value) return true end function self.samSite:getCanEngageHARM() return true end function self.samSite:pointDefencesHaveRemainingAmmo(value) return false end function self.samSite:pointDefencesHaveEnoughLaunchers(value) return true end lu.assertEquals(self.samSite:shallIgnoreHARMShutdown(), false) function self.samSite:hasEnoughLaunchersToEngageMissiles(value) return true end function self.samSite:hasRemainingAmmoToEngageMissiles(value) return false end function self.samSite:getCanEngageHARM() return true end function self.samSite:pointDefencesHaveRemainingAmmo(value) return true end function self.samSite:pointDefencesHaveEnoughLaunchers(value) return false end lu.assertEquals(self.samSite:shallIgnoreHARMShutdown(), false) function self.samSite:hasEnoughLaunchersToEngageMissiles(value) return true end function self.samSite:hasRemainingAmmoToEngageMissiles(value) return true end function self.samSite:getCanEngageHARM() return true end function self.samSite:pointDefencesHaveRemainingAmmo(value) return false end function self.samSite:pointDefencesHaveEnoughLaunchers(value) return false end lu.assertEquals(self.samSite:shallIgnoreHARMShutdown(), true) function self.samSite:hasEnoughLaunchersToEngageMissiles(value) return true end function self.samSite:hasRemainingAmmoToEngageMissiles(value) return true end function self.samSite:getCanEngageHARM() return true end function self.samSite:pointDefencesHaveRemainingAmmo(value) return true end function self.samSite:pointDefencesHaveEnoughLaunchers(value) return true end lu.assertEquals(self.samSite:shallIgnoreHARMShutdown(), true) function self.samSite:hasEnoughLaunchersToEngageMissiles(value) return true end function self.samSite:hasRemainingAmmoToEngageMissiles(value) return false end function self.samSite:getCanEngageHARM() return true end function self.samSite:pointDefencesHaveRemainingAmmo(value) return true end function self.samSite:pointDefencesHaveEnoughLaunchers(value) return true end lu.assertEquals(self.samSite:shallIgnoreHARMShutdown(), true) function self.samSite:hasEnoughLaunchersToEngageMissiles(value) return true end function self.samSite:hasRemainingAmmoToEngageMissiles(value) return true end function self.samSite:getCanEngageHARM() return true end function self.samSite:pointDefencesHaveRemainingAmmo(value) return false end function self.samSite:pointDefencesHaveEnoughLaunchers(value) return true end lu.assertEquals(self.samSite:shallIgnoreHARMShutdown(), true) function self.samSite:hasEnoughLaunchersToEngageMissiles(value) return true end function self.samSite:hasRemainingAmmoToEngageMissiles(value) return true end function self.samSite:getCanEngageHARM() return true end function self.samSite:pointDefencesHaveRemainingAmmo(value) return true end function self.samSite:pointDefencesHaveEnoughLaunchers(value) return false end lu.assertEquals(self.samSite:shallIgnoreHARMShutdown(), true) end end ================================================ FILE: unit-tests/test-skynet-iads-blue-sam-sites-and-ew-radars.lua ================================================ do TestSkynetIADSBLUESAMSitesAndEWRadars = {} function TestSkynetIADSBLUESAMSitesAndEWRadars:setUp() if self.samSiteName then self.skynetIADS = SkynetIADS:create() local samSite = Group.getByName(self.samSiteName) self.samSite = SkynetIADSSamSite:create(samSite, self.skynetIADS) -- we overrite this method since it returns radar contacts in the DCS world which mess up the tests. function self.samSite:getDetectedTargets() return {} end self.samSite:setupElements() self.samSite:goLive() end if self.ewRadarName then self.iads = SkynetIADS:create() self.iads:addEarlyWarningRadarsByPrefix('BLUE-EW') self.ewRadar = self.iads:getEarlyWarningRadarByUnitName(self.ewRadarName) end end function TestSkynetIADSBLUESAMSitesAndEWRadars:tearDown() if self.samSite then self.samSite:goDark() self.samSite:cleanUp() end if self.ewRadar then self.ewRadar:cleanUp() end if self.iads then self.iads:deactivate() end self.iads = nil self.ewRadar = nil self.ewRadarName = nil self.samSite = nil self.samSiteName = nil end function TestSkynetIADSBLUESAMSitesAndEWRadars:testHawkSTR() self.ewRadarName = "BLUE-EW-Hawk" self:setUp() lu.assertEquals(self.ewRadar:getNatoName(), "Hawk str") lu.assertEquals(self.ewRadar:hasWorkingRadar(), true) end function TestSkynetIADSBLUESAMSitesAndEWRadars:testPatriotSTR() self.ewRadarName = "BLUE-EW" self:setUp() lu.assertEquals(self.ewRadar:getNatoName(), "Patriot str") lu.assertEquals(self.ewRadar:hasWorkingRadar(), true) end function TestSkynetIADSBLUESAMSitesAndEWRadars:testRolandEWR() self.ewRadarName = "BLUE-EW-Roland" self:setUp() lu.assertEquals(self.ewRadar:getNatoName(), "Roland EWR") lu.assertEquals(self.ewRadar:hasWorkingRadar(), true) end function TestSkynetIADSBLUESAMSitesAndEWRadars:testRolandLauncherAndRadar() --[[ Roland: Radar: { 0={ {opticType=0, type=0, typeName="generic SAM search visir"}, {opticType=2, type=0, typeName="generic SAM IR search visir"} }, { { detectionDistanceAir={ lowerHemisphere={headOn=8024.8837890625, tailOn=8024.8837890625}, upperHemisphere={headOn=8024.8837890625, tailOn=8024.8837890625} }, type=1, typeName="Roland ADS" } } } Launcher: { { count=10, desc={ Nmax=14, RCS=0.019600000232458, _origin="", altMax=6000, altMin=10, box={ max={x=1.2142661809921, y=0.17386008799076, z=0.1697566062212}, min={x=-1.212909579277, y=-0.1738600730896, z=-0.1697566062212} }, category=1, displayName="XMIM-115 Roland", fuseDist=5, guidance=4, life=2, missileCategory=2, rangeMaxAltMax=8000, rangeMaxAltMin=8000, rangeMin=500, typeName="ROLAND_R", warhead={caliber=150, explosiveMass=6.5, mass=6.5, type=1} } } } --]] self.samSiteName = "BLUE-SAM-ROLAND" self:setUp() lu.assertEquals(self.samSite:getNatoName(), "Roland ADS") lu.assertEquals(#self.samSite:getLaunchers(), 1) lu.assertEquals(#self.samSite:getSearchRadars(), 1) lu.assertEquals(self.samSite:getSearchRadars()[1]:getMaxRangeFindingTarget(), 23405.912109375) lu.assertEquals(self.samSite:getLaunchers()[1]:getRange(), 8000) lu.assertEquals(self.samSite:getLaunchers()[1]:getInitialNumberOfMissiles(), 10) end function TestSkynetIADSBLUESAMSitesAndEWRadars:testNASAMS() self.samSiteName = "BLUE-SAM-NASAMS" self:setUp() lu.assertEquals(self.samSite:getNatoName(), "NASAMS") lu.assertEquals(self.samSite:getHARMDetectionChance(),90) lu.assertEquals(self.samSite:getCanEngageHARM(), true) lu.assertEquals(self.samSite:getRadars()[1]:getMaxRangeFindingTarget(), 26749.61328125) lu.assertEquals(self.samSite:getLaunchers()[1]:getRange(), 57000) lu.assertEquals(self.samSite:getLaunchers()[1]:getInitialNumberOfMissiles(), 6) lu.assertEquals(self.samSite:getLaunchers()[2]:getRange(), 61000) lu.assertEquals(self.samSite:getLaunchers()[2]:getInitialNumberOfMissiles(), 6) end function TestSkynetIADSBLUESAMSitesAndEWRadars:testRapierLauncherAndRadar() --[[ Rapier: Radar: (for some reason the typeName is Tor?) { { { detectionDistanceAir={ lowerHemisphere={headOn=16718.5078125, tailOn=16718.5078125}, upperHemisphere={headOn=16718.5078125, tailOn=16718.5078125} }, type=1, typeName="Tor 9A331" } } } Launcher: { { count=4, desc={ Nmax=14, RCS=0.079999998211861, _origin="", altMax=3000, altMin=50, box={ max={x=1.4030002355576, y=0.13611803948879, z=0.13611821830273}, min={x=-0.84999942779541, y=-0.13611836731434, z=-0.1361181885004} }, category=1, displayName="Rapier", fuseDist=0, guidance=8, life=2, missileCategory=2, rangeMaxAltMax=6800, rangeMaxAltMin=6800, rangeMin=400, typeName="Rapier", warhead={caliber=133, explosiveMass=1.3999999761581, mass=1.3999999761581, type=1} } } } --]] self.samSiteName = "BLUE-SAM-RAPIER" self:setUp() lu.assertEquals(self.samSite:getNatoName(), "Rapier") lu.assertEquals(self.samSite:getRadars()[1]:getMaxRangeFindingTarget(), 16718.5078125) lu.assertEquals(self.samSite:getLaunchers()[1]:getRange(), 6800) lu.assertEquals(self.samSite:getLaunchers()[1]:getInitialNumberOfMissiles(), 4) local units = Group.getByName(self.samSiteName):getUnits() for i = 1, #units do local unit = units[i] if unit:getTypeName() == 'rapier_fsa_optical_tracker_unit' then -- lu.assertEquals(unit:getSensors(), true) end end end function TestSkynetIADSBLUESAMSitesAndEWRadars:testPatriotLauncherAndRadar() --[[ Patriot: Radar: { { count=4, desc={ Nmax=25, RCS=0.10660000145435, _origin="", altMax=24240, altMin=45, box={ max={x=2.5578553676605, y=0.33423712849617, z=0.32681864500046}, min={x=-2.5578553676605, y=-0.33423712849617, z=-0.32681867480278} }, category=1, displayName="MIM-104 Patriot", fuseDist=13, guidance=4, life=2, missileCategory=2, rangeMaxAltMax=120000, rangeMaxAltMin=30000, rangeMin=3000, typeName="MIM_104", warhead={caliber=410, explosiveMass=73, mass=73, type=1} } } } Search Radar: { { { detectionDistanceAir={ lowerHemisphere={headOn=173872.484375, tailOn=173872.484375}, upperHemisphere={headOn=173872.484375, tailOn=173872.484375} }, type=1, typeName="Patriot str" } } } --]] self.samSiteName = "BLUE-SAM-PATRIOT" self:setUp() lu.assertEquals(self.samSite:getNatoName(), "Patriot") lu.assertEquals(self.samSite:getHARMDetectionChance(), 90) lu.assertEquals(self.samSite:getCanEngageHARM(), true) local radar = self.samSite:getSearchRadars()[1] lu.assertEquals(radar:getMaxRangeFindingTarget(), 173872.484375) local launcher = self.samSite:getLaunchers()[1] lu.assertEquals(launcher:getInitialNumberOfMissiles(), 4) lu.assertEquals(launcher:getRange(), 120000) end function TestSkynetIADSBLUESAMSitesAndEWRadars:testLPWSCRAM() --"HEMTT_C-RAM_Phalanx" --[[ { { detectionDistanceAir={ lowerHemisphere={headOn=13374.806640625, tailOn=13374.806640625}, upperHemisphere={headOn=13374.806640625, tailOn=13374.806640625} }, type=1, typeName="C_RAM_Phalanx" } } { count=1550, desc={ _origin="", box={ max={x=2.2344591617584, y=0.12504191696644, z=0.12113922089338}, min={x=-6.61008644104, y=-0.12504199147224, z=-0.12113920599222} }, category=0, displayName="M246_20_HE", life=2, typeName="weapons.shells.M246_20_HE_gr", warhead={caliber=20, explosiveMass=0.1, mass=0.1, type=1} } } } --]] self.samSiteName = "BLUE-SAM-LPWS-C-RAM" self:setUp() lu.assertEquals(#self.samSite:getRadars(),1) lu.assertEquals(#self.samSite:getLaunchers(), 1) lu.assertEquals(#self.samSite:getTrackingRadars(), 0) local searchRadar = self.samSite:getSearchRadars()[1] local launcher = self.samSite:getLaunchers()[1] lu.assertEquals(searchRadar:getMaxRangeFindingTarget(), 13374.806640625) lu.assertEquals(launcher:getRange(), 13374.806640625) end function TestSkynetIADSBLUESAMSitesAndEWRadars:testEWRANFPS117Domed() --[[ { { detectionDistanceAir={ lowerHemisphere={headOn=309626.78125, tailOn=309626.78125}, upperHemisphere={headOn=309626.78125, tailOn=309626.78125} }, type=1, typeName="FPS-117" } } } --]] self.ewRadarName = "BLUE-EW-FPS-117-DOMED" self:setUp() --lu.assertEquals(Unit.getByName(self.ewRadarName):getTypeName(), nil) lu.assertEquals(self.ewRadar:getNatoName(), "FPS-117 Dome") lu.assertEquals(self.ewRadar:getHARMDetectionChance(), 80) local radar = self.ewRadar:getSearchRadars()[1] lu.assertEquals(radar:getMaxRangeFindingTarget(), 309626.78125) end function TestSkynetIADSBLUESAMSitesAndEWRadars:testEWRANFPS117() --[[ { { detectionDistanceAir={ lowerHemisphere={headOn=309626.78125, tailOn=309626.78125}, upperHemisphere={headOn=309626.78125, tailOn=309626.78125} }, type=1, typeName="FPS-117" } } } --]] self.ewRadarName = "BLUE-EW-FPS-117" self:setUp() --lu.assertEquals(Unit.getByName(self.ewRadarName):getTypeName(), nil) lu.assertEquals(self.ewRadar:getNatoName(), "FPS-117") lu.assertEquals(self.ewRadar:getHARMDetectionChance(), 80) local radar = self.ewRadar:getSearchRadars()[1] lu.assertEquals(radar:getMaxRangeFindingTarget(), 309626.78125) end --TODO: this test can only be finished once the perry class has radar data: function TestSkynetIADSBLUESAMSitesAndEWRadars:testOliverHazzardPerryClassShip() --[[ Oliver Hazzard: Launchers: { { count=2016, desc={ _origin="", box={ max={x=2.2344591617584, y=0.12504191696644, z=0.12113922089338}, min={x=-6.61008644104, y=-0.12504199147224, z=-0.12113920599222} }, category=0, displayName="12.7mm", life=2, typeName="weapons.shells.M2_12_7_T", warhead={caliber=12.7, explosiveMass=0, mass=0.046, type=0} } }, { count=460, desc={ _origin="", box={ max={x=2.2344591617584, y=0.12504191696644, z=0.12113922089338}, min={x=-6.61008644104, y=-0.12504199147224, z=-0.12113920599222} }, category=0, displayName="25mm HE", life=2, typeName="weapons.shells.M242_25_HE_M792", warhead={caliber=25, explosiveMass=0.185, mass=0.185, type=1} } }, { count=142, desc={ _origin="", box={ max={x=2.2344591617584, y=0.12504191696644, z=0.12113922089338}, min={x=-6.61008644104, y=-0.12504199147224, z=-0.12113920599222} }, category=0, displayName="25mm AP", life=2, typeName="weapons.shells.M242_25_AP_M791", warhead={caliber=25, explosiveMass=0, mass=0.155, type=0} } }, { count=775, desc={ _origin="", box={ max={x=2.2344591617584, y=0.12504191696644, z=0.12113922089338}, min={x=-6.61008644104, y=-0.12504199147224, z=-0.12113920599222} }, category=0, displayName="20mm AP", life=2, typeName="weapons.shells.M61_20_AP", warhead={caliber=20, explosiveMass=0, mass=0.1, type=0} } }, { count=775, desc={ _origin="", box={ max={x=2.2344591617584, y=0.12504191696644, z=0.12113922089338}, min={x=-6.61008644104, y=-0.12504199147224, z=-0.12113920599222} }, category=0, displayName="20mm HE", life=2, typeName="weapons.shells.M61_20_HE", warhead={caliber=20, explosiveMass=0.1, mass=0.1, type=1} } }, { count=24, desc={ Nmax=25, RCS=0.1765999943018, _origin="", altMax=24400, altMin=10, box={ max={x=2.9796471595764, y=0.39923620223999, z=0.39878171682358}, min={x=-1.5204827785492, y=-0.38143759965897, z=-0.39878168702126} }, category=1, displayName="SM-2", fuseDist=15, guidance=4, life=2, missileCategory=2, rangeMaxAltMax=100000, rangeMaxAltMin=40000, rangeMin=4000, typeName="SM_2", warhead={caliber=340, explosiveMass=98, mass=98, type=1} } }, { count=16, desc={ Nmax=18, RCS=0.10580000281334, _origin="", altMax=10000, altMin=-1, box={ max={x=2.2758972644806, y=0.13610155880451, z=0.28847914934158}, min={x=-1.6704962253571, y=-0.4600305557251, z=-0.28847911953926} }, category=1, displayName="AGM-84S Harpoon", fuseDist=0, guidance=1, life=2, missileCategory=4, rangeMaxAltMax=241401, rangeMaxAltMin=95000, rangeMin=3000, typeName="AGM_84S", warhead={caliber=343, explosiveMass=90, mass=90, type=1} } }, { count=180, desc={ _origin="", box={ SENSOR: { 0={{opticType=0, type=0, typeName="long-range naval optics"}}, { { detectionDistanceAir={ lowerHemisphere={headOn=173872.484375, tailOn=173872.484375}, upperHemisphere={headOn=173872.484375, tailOn=173872.484375} }, type=1, typeName="Patriot str" }, {detectionDistanceRBM=336.19998168945, type=1, typeName="perry search radar"} } } --]] self.ewRadarName = "BLUE-EW-Oliver-Hazzard" self:setUp() lu.assertEquals(self.ewRadar:getNatoName(), "PERRY") --as long as we don't use the PERRY as a SAM site the distance returned is irrelevant, because it's radar wil be on all the time lu.assertEquals(self.ewRadar:getRadars()[1]:getMaxRangeFindingTarget(), 173872.484375) end end ================================================ FILE: unit-tests/test-skynet-iads-contact.lua ================================================ do TestSyknetIADSContact = {} function TestSyknetIADSContact:setUp() local radarTarget = {} radarTarget.object = Unit.getByName('test-outer-search-range') self.contact = SkynetIADSContact:create(radarTarget) end function TestSyknetIADSContact:testGetNumberOfTimesHitByRadar() lu.assertEquals(self.contact:getNumberOfTimesHitByRadar(), 0) self.contact:refresh() lu.assertEquals(self.contact:getNumberOfTimesHitByRadar(), 1) end function TestSyknetIADSContact:testRefresh() local called = 0 function self.contact:updateSimpleAltitudeProfile() called = 1 end self.contact:refresh() lu.assertEquals(called, 1) function self.contact:getDCSRepresentation() return Unit.getByName('test-not-in-firing-range-of-sa-2') end --we set time in the past, to simulate distance traveled self.contact.lastTimeSeen = timer.getAbsTime() - 1000 lu.assertEquals(self.contact:getAge(), 1000) self.contact:refresh() lu.assertEquals(self.contact:getGroundSpeedInKnots(0), 989) end function TestSyknetIADSContact:testGetHeightInFeetMSL() lu.assertEquals(self.contact:getHeightInFeetMSL(), 5015) end function TestSyknetIADSContact:testUpdateSimpleAltitudeProfile() local mockDCSObject = {} function mockDCSObject:getPosition() local p = {} p.y = 100 local ret = {} ret.p = p return ret end self.contact.position.p.y = 200 function self.contact:getDCSRepresentation() return mockDCSObject end self.contact:updateSimpleAltitudeProfile() local altProfile = self.contact:getSimpleAltitudeProfile() lu.assertEquals(altProfile[1], SkynetIADSContact.DESCEND) lu.assertEquals(#altProfile, 1) function mockDCSObject:getPosition() local p = {} p.y = 200 local ret = {} ret.p = p return ret end self.contact.position.p.y = 200 function self.contact:getDCSRepresentation() return mockDCSObject end self.contact:updateSimpleAltitudeProfile() local altProfile = self.contact:getSimpleAltitudeProfile() lu.assertEquals(altProfile[1], SkynetIADSContact.DESCEND) lu.assertEquals(#altProfile, 1) function mockDCSObject:getPosition() local p = {} p.y = 200 local ret = {} ret.p = p return ret end self.contact.position.p.y = 100 self.contact:updateSimpleAltitudeProfile() local altProfile = self.contact:getSimpleAltitudeProfile() lu.assertEquals(altProfile[2], SkynetIADSContact.CLIMB) lu.assertEquals(#altProfile, 2) function mockDCSObject:getPosition() local p = {} p.y = 200 local ret = {} ret.p = p return ret end self.contact.position.p.y = 100 self.contact:updateSimpleAltitudeProfile() local altProfile = self.contact:getSimpleAltitudeProfile() lu.assertEquals(altProfile[2], SkynetIADSContact.CLIMB) lu.assertEquals(#altProfile, 2) end function TestSyknetIADSContact:testSetIsHARM() lu.assertEquals(self.contact.harmState, SkynetIADSContact.HARM_UNKNOWN) self.contact:setHARMState(SkynetIADSContact.HARM) lu.assertEquals(self.contact.harmState, SkynetIADSContact.HARM) end function TestSyknetIADSContact:testGetMagneticHeading() lu.assertEquals(self.contact:getMagneticHeading(), 347) function self.contact:isExist() return false end lu.assertEquals(self.contact:getMagneticHeading(), -1) end function TestSyknetIADSContact:testIsIdentifiedAsHARM() lu.assertEquals(self.contact:isIdentifiedAsHARM(), false) self.contact:setHARMState(SkynetIADSContact.HARM) lu.assertEquals(self.contact:isIdentifiedAsHARM(), true) end function TestSyknetIADSContact:testIsHARMStateUnknown() lu.assertEquals(self.contact:isHARMStateUnknown(), true) self.contact:setHARMState(SkynetIADSContact.NOT_HARM) lu.assertEquals(self.contact:isHARMStateUnknown(), false) end function TestSyknetIADSContact:testAddAbstractRadarElementDetected() local radar = {} self.contact:addAbstractRadarElementDetected(radar) lu.assertEquals(#self.contact:getAbstractRadarElementsDetected(), 1) --adding the same radar again, shall not result in it being added: self.contact:addAbstractRadarElementDetected(radar) lu.assertEquals(#self.contact:getAbstractRadarElementsDetected(), 1) local radar2 = {} self.contact:addAbstractRadarElementDetected(radar2) lu.assertEquals(#self.contact:getAbstractRadarElementsDetected(), 2) end function TestSyknetIADSContact:testGetTypeNameUNKNOWN() function self.contact:getDCSRepresentation() return nil end lu.assertEquals(self.contact:getTypeName(), "UNKNOWN") end function TestSyknetIADSContact:testGetTypeNameisHARM() self.contact:setHARMState(SkynetIADSContact.HARM) lu.assertEquals(self.contact:getTypeName(), SkynetIADSContact.HARM) end function TestSyknetIADSContact:testGetTypeNameisUnit() lu.assertEquals(self.contact:getTypeName(), "AH-1W") end end ================================================ FILE: unit-tests/test-skynet-iads-harm-detection.lua ================================================ do TestSkynetIADSHARMDetection = {} function TestSkynetIADSHARMDetection:setUp() local iads = SkynetIADS:create() self.harmDetection = SkynetIADSHARMDetection:create(iads) end function TestSkynetIADSHARMDetection:testContact0GroundSpeed() local mockContact = {} function mockContact:getGroundSpeedInKnots(round) return 0 end local calledProfileInfo = false function mockContact:getSimpleAltitudeProfile() calledProfileInfo = true end self.harmDetection:setContacts({mockContact}) self.harmDetection:evaluateContacts() lu.assertEquals(calledProfileInfo, false) end function TestSkynetIADSHARMDetection:testEvaluateContactsContactIsHARMInClimb() --test with a contact that shall be identified as a HARM local mockContactHARM = {} function mockContactHARM:getGroundSpeedInKnots(round) return 1500 end function mockContactHARM:isHARMStateUnknown() return true end function mockContactHARM:getSimpleAltitudeProfile() return {SkynetIADSContact.CLIMB} end local harmStateCalled = false function mockContactHARM:setHARMState(state) harmStateCalled = true lu.assertEquals(state, SkynetIADSContact.HARM) end local calls = 0 function mockContactHARM:isIdentifiedAsHARM() calls = calls + 1 if ( calls == 2 ) then return true else return false end end local mockRadar = {} function mockRadar:getHARMDetectionChance() return 50 end function mockContactHARM:getAbstractRadarElementsDetected() return {mockRadar} end local probCalled = false function self.harmDetection:shallReactToHARM(prob) lu.assertEquals(prob, 50) probCalled = true return true end local contactInform = false function self.harmDetection:informRadarsOfHARM(contact) lu.assertEquals(mockContactHARM, contact) contactInform = true end self.harmDetection:setContacts({mockContactHARM}) local calledCleanedAgedTargets = false function self.harmDetection:cleanAgedContacts() calledCleanedAgedTargets = true end self.harmDetection:evaluateContacts() lu.assertEquals(calledCleanedAgedTargets, true) lu.assertEquals(harmStateCalled, true) lu.assertEquals(probCalled, true) lu.assertEquals(contactInform, true) end function TestSkynetIADSHARMDetection:testEvaluateContactsContactDetectedAsHARMHas3rdAltitudeChangeRecorded() --a contact previously identified as a HARM has a 3rd altitude change recorded, this means it's an aircraft previously falsely detected as HARM local mockContactHARM = {} function mockContactHARM:getGroundSpeedInKnots(round) return 1000 end function mockContactHARM:isHARMStateUnknown() return false end function mockContactHARM:getSimpleAltitudeProfile() return {SkynetIADSContact.DESCEND, SkynetIADSContact.CLIMB, SkynetIADSContact.DESCEND } end local harmStateCalled = false function mockContactHARM:setHARMState(state) harmStateCalled = true lu.assertEquals(state, SkynetIADSContact.HARM_UNKNOWN) end local calls = 0 function mockContactHARM:isIdentifiedAsHARM() calls = calls + 1 if ( calls == 2 ) then return true else return false end end local contactInform = false function self.harmDetection:informRadarsOfHARM(contact) contactInform = true end function self.harmDetection:getNewRadarsThatHaveDetectedContact(contact) return {"MockRadar"} end self.harmDetection:setContacts({mockContactHARM}) self.harmDetection:evaluateContacts() lu.assertEquals(harmStateCalled, true) lu.assertEquals(contactInform, false) end function TestSkynetIADSHARMDetection:testGetDetectionProbability() local mockSAM1 = {} function mockSAM1:getHARMDetectionChance() return 60 end local mockSam2 = {} function mockSam2:getHARMDetectionChance() return 30 end local mockNewRadarsDetected = {mockSAM1, mockSam2} lu.assertEquals(self.harmDetection:getDetectionProbability(mockNewRadarsDetected), 72) function mockSAM1:getHARMDetectionChance() return 20 end function mockSam2:getHARMDetectionChance() return 90 end lu.assertEquals(self.harmDetection:getDetectionProbability(mockNewRadarsDetected), 92) end function TestSkynetIADSHARMDetection:testGetNewRadarsThatHaveDetectedContact() local mockContact = {} local mockRadar1 = {"MockRadar1"} local mockRadar2 = {"MockRadar2"} local detectedRadars = {mockRadar1, mockRadar2} function mockContact:getAbstractRadarElementsDetected() return detectedRadars end local result = self.harmDetection:getNewRadarsThatHaveDetectedContact(mockContact) lu.assertEquals(result, {mockRadar1, mockRadar2}) lu.assertEquals(self.harmDetection.contactRadarsEvaluated[mockContact], {mockRadar1, mockRadar2}) local result2 = self.harmDetection:getNewRadarsThatHaveDetectedContact(mockContact) lu.assertEquals(result2, {}) lu.assertEquals(self.harmDetection.contactRadarsEvaluated[mockContact], {mockRadar1, mockRadar2}) local mockRadar3 = {"MockRadar3"} table.insert(detectedRadars, mockRadar3) lu.assertEquals(#mockContact:getAbstractRadarElementsDetected(), 3) local result3 = self.harmDetection:getNewRadarsThatHaveDetectedContact(mockContact) lu.assertEquals(result3, {mockRadar3}) lu.assertEquals(self.harmDetection.contactRadarsEvaluated[mockContact], {mockRadar1, mockRadar2, mockRadar3}) local mockRadar4 = {"MockRadar4"} table.insert(detectedRadars, mockRadar4) local result4 = self.harmDetection:getNewRadarsThatHaveDetectedContact(mockContact) lu.assertEquals(result4, {mockRadar4}) lu.assertEquals(self.harmDetection.contactRadarsEvaluated[mockContact], {mockRadar1, mockRadar2, mockRadar3, mockRadar4}) end function TestSkynetIADSHARMDetection:testCleanAgedContacts() local mockContact1 = {} function mockContact1:getAge() return 1 end local mockContact2 = {} function mockContact2:getAge() return 33 end local contactRadars = {} contactRadars[mockContact1] = "keep" contactRadars[mockContact2] = "delete" self.harmDetection.contactRadarsEvaluated = contactRadars self.harmDetection:cleanAgedContacts() local count = 0 for key, value in pairs(self.harmDetection.contactRadarsEvaluated) do count = count + 1 end lu.assertEquals(count, 1) lu.assertEquals(self.harmDetection.contactRadarsEvaluated[mockContact1], "keep") end end ================================================ FILE: unit-tests/test-skynet-iads-jammer.lua ================================================ do TestSkynetIADSJammer = {} function TestSkynetIADSJammer:setUp() self.emitter = Unit.getByName('jammer-source') self.mockIADS = {} function self.mockIADS:getDebugSettings() return {} end self.jammer = SkynetIADSJammer:create(self.emitter, self.mockIADS) end function TestSkynetIADSJammer:tearDown() self.jammer:masterArmSafe() end function TestSkynetIADSJammer:testSetJammerDistance() self.jammer:setMaximumEffectiveDistance(20) lu.assertEquals(self.jammer.maximumEffectiveDistanceNM, 20) end function TestSkynetIADSJammer:testSetupJammerAndRunCycle() lu.assertEquals(self.jammer.jammerTaskID, nil) self.jammer:masterArmOn() lu.assertNotIs(self.jammer.jammerTaskID, nil) local mockRadar = {} local mockSAM = {} local calledJam = false function mockSAM:getRadars() return {mockRadar} end function mockSAM:getNatoName() return "SA-2" end function mockSAM:jam(prob) calledJam = true end function self.mockIADS:getActiveSAMSites() return {mockSAM} end function self.jammer:getDistanceNMToRadarUnit(radarUnit) return 50 end function self.jammer:hasLineOfSightToRadar(radar) return true end self.jammer.runCycle(self.jammer) lu.assertEquals(calledJam, true) end function TestSkynetIADSJammer:testIsActiveForUnknownType() lu.assertEquals(self.jammer:isKnownRadarEmitter('ABC-Test'), false) end function TestSkynetIADSJammer:testIsActiveForKnownType() lu.assertEquals(self.jammer:isKnownRadarEmitter('SA-2'), true) end function TestSkynetIADSJammer:testCleanUpJammer() self.jammer:masterArmOn() local alive = false local i = 0 while i < 10000 do local id = mist.removeFunction(i) i = i + 1 if id then alive = true end end lu.assertEquals(alive, true) self.jammer:masterArmSafe() i = 0 alive = false while i < 10000 do local id = mist.removeFunction(i) i = i + 1 if id then alive = true end end lu.assertEquals(alive, false) end function TestSkynetIADSJammer:testAddJammerFunction() local function f(distanceNM) return 2 * distanceNM end self.jammer:addFunction('SA-99', f) lu.assertEquals(self.jammer:getSuccessProbability(20, 'SA-99'), 40) lu.assertEquals(self.jammer:isKnownRadarEmitter('SA-99'), true) self.jammer:disableFor('SA-99') lu.assertEquals(self.jammer:isKnownRadarEmitter('SA-99'), false) end function TestSkynetIADSJammer:testDestroyEmitter() self:tearDown() self.emitter = Unit.getByName("jammer-source-unit-test") local iads = SkynetIADS:create() self.jammer = SkynetIADSJammer:create(self.emitter, iads) self.jammer:masterArmOn() trigger.action.explosion(Unit.getByName("jammer-source-unit-test"):getPosition().p, 500) self.jammer.runCycle(self.jammer) local i = 0 local alive = false while i < 10000 do local id = mist.removeFunction(i) i = i + 1 if id then alive = true end end lu.assertEquals(alive, false) end end ================================================ FILE: unit-tests/test-skynet-iads-red-sam-sites-and-ew-radars.lua ================================================ do TestSkynetIADSREDSAMSitesAndEWRadars = {} function TestSkynetIADSREDSAMSitesAndEWRadars:setUp() self.skynetIADS = SkynetIADS:create() if self.samSiteName then local samSite = Group.getByName(self.samSiteName) self.samSite = SkynetIADSSamSite:create(samSite, self.skynetIADS) -- we overrite this method since it returns radar contacts in the DCS world which mess up the tests. function self.samSite:getDetectedTargets() return {} end self.samSite:setupElements() self.samSite:goLive() end if self.ewRadarName then self.skynetIADS:addEarlyWarningRadarsByPrefix('EW') self.ewRadar = self.skynetIADS:getEarlyWarningRadarByUnitName(self.ewRadarName) end end function TestSkynetIADSREDSAMSitesAndEWRadars:tearDown() if self.samSite then self.samSite:goDark() self.samSite:cleanUp() end if self.ewRadar then self.ewRadar:cleanUp() end if self.skynetIADS then self.skynetIADS:deactivate() end self.ewRadarName = nil self.samSiteName = nil end function TestSkynetIADSREDSAMSitesAndEWRadars:testCheckSA6GroupNumberOfLaunchersAndRangeValuesAndSearchRadarsAndNatoName() --[[ DCS properties SA-6 (Kub / Gainful) Radar: { { { detectionDistanceAir={ lowerHemisphere={headOn=46811.82421875, tailOn=46811.82421875}, upperHemisphere={headOn=46811.82421875, tailOn=46811.82421875} upperHemisphere={headOn=46811.82421875, tailOn=46811.82421875} }, type=1, typeName="Kub 1S91 str" } } } Launcher: { count=3, desc={ Nmax=16, RCS=0.1059999987483, _origin="", altMax=8000, altMin=30, box={ max={x=2.9061908721924, y=0.43574807047844, z=0.4395649433136}, min={x=-2.9048342704773, y=-0.43574807047844, z=-0.4395649433136} }, category=1, displayName="3M9M Kub (SA-6 Gainful)", fuseDist=12, guidance=4, life=2, missileCategory=2, rangeMaxAltMax=25000, rangeMaxAltMin=25000, rangeMin=4000, typeName="SA3M9M", warhead={caliber=330, explosiveMass=59, mass=59, type=1} } } } --]] self.samSiteName = "SAM-SA-6-2" self:setUp() lu.assertEquals(#self.samSite:getLaunchers(), 1) lu.assertEquals(#self.samSite:getSearchRadars(), 1) local searchRadar = self.samSite:getSearchRadars()[1] lu.assertEquals(searchRadar:getMaxRangeFindingTarget(), 46811.82421875) lu.assertEquals(self.samSite:getNatoName(), "SA-6") local launcher = self.samSite:getLaunchers()[1] lu.assertEquals(launcher:getRange(), 25000) lu.assertEquals(self.samSite:getRemainingNumberOfMissiles(), 3) end function TestSkynetIADSREDSAMSitesAndEWRadars:testCheckSA10GroupNumberOfLaunchersAndSearchRadarsAndNatoName() --[[ DCS properties SA-10 (S-300 / SA-10 Grumble) Launcher: { { count=4, desc={ Nmax=25, RCS=0.17800000309944, _origin="", altMax=30000, altMin=25, box={ max={x=3.6516976356506, y=0.81190091371536, z=0.81109911203384}, min={x=-3.6131811141968, y=-0.80982387065887, z=-0.81062549352646} }, category=1, displayName="5V55 S-300PS (SA-10B Grumble)", fuseDist=20, guidance=4, life=2, missileCategory=2, rangeMaxAltMax=75000, rangeMaxAltMin=40000, rangeMin=5000, typeName="SA5B55", warhead={caliber=508, explosiveMass=133, mass=133, type=1} } } } --]] self.samSiteName = "SAM-SA-10" self:setUp() lu.assertEquals(#self.samSite:getLaunchers(), 2) lu.assertEquals(#self.samSite:getSearchRadars(), 3) lu.assertEquals(#self.samSite:getTrackingRadars(), 2) lu.assertEquals(#self.samSite:getRadars(), 5) lu.assertEquals(self.samSite:getNatoName(), "SA-10") lu.assertEquals(self.samSite:getCanEngageHARM(), true) local launchers = self.samSite:getLaunchers() local numLoops = 0 -- seems like currently both launcher types of the SA-10 have the same range values for i = 1, #launchers do local launcher = launchers[i] lu.assertEquals(launcher:getInitialNumberOfMissiles(), 4) lu.assertEquals(launcher:getRange(), 75000) lu.assertEquals(launcher:getMaximumFiringAltitude(), 25000) numLoops = numLoops + 1 end lu.assertEquals(numLoops, 2) local radars = self.samSite:getRadars() --for some strange reason the s300 does not have any range values in getSensors(), all the data there is empty local tr = self.samSite:getTrackingRadars()[1] lu.assertEquals(tr:getMaxRangeFindingTarget(),0) local tr = self.samSite:getTrackingRadars()[2] lu.assertEquals(tr:getMaxRangeFindingTarget(),0) local sr = self.samSite:getSearchRadars()[1] lu.assertEquals(sr:getMaxRangeFindingTarget(), 0) local sr = self.samSite:getSearchRadars()[2] lu.assertEquals(sr:getMaxRangeFindingTarget(), 0) local sr = self.samSite:getSearchRadars()[3] lu.assertEquals(sr:getMaxRangeFindingTarget(), 0) end function TestSkynetIADSREDSAMSitesAndEWRadars:testCheckSA11GroupNumberOfLaunchersAndSearchRadarsAndNatoName() --[[ DCS properties SA-10 (S-300 / SA-10 Grumble) Radar: { { { detectionDistanceAir={ lowerHemisphere={headOn=53499.2265625, tailOn=53499.2265625}, upperHemisphere={headOn=53499.2265625, tailOn=53499.2265625} }, type=1, typeName="S-300PS 40B6M tr" } } } Launcher: { { count=4, desc={ Nmax=25, RCS=0.17800000309944, _origin="", altMax=30000, altMin=25, box={ max={x=3.6516976356506, y=0.81190091371536, z=0.81109911203384}, min={x=-3.6131811141968, y=-0.80982387065887, z=-0.81062549352646} }, category=1, displayName="5V55 S-300PS (SA-10B Grumble)", fuseDist=20, guidance=4, life=2, missileCategory=2, rangeMaxAltMax=75000, rangeMaxAltMin=40000, rangeMin=5000, typeName="SA5B55", warhead={caliber=508, explosiveMass=133, mass=133, type=1} } } } --]] self.samSiteName = "SAM-SA-11" self:setUp() lu.assertEquals(#self.samSite:getLaunchers(), 1) lu.assertEquals(#self.samSite:getSearchRadars(), 1) lu.assertEquals(#self.samSite:getTrackingRadars(), 0) lu.assertEquals(#self.samSite:getRadars(), 1) lu.assertEquals(self.samSite:getNatoName(), "SA-11") local launchers = self.samSite:getLaunchers() local launcher = launchers[1] lu.assertEquals(launcher:getInitialNumberOfMissiles(), 4) lu.assertEquals(launcher:getRange(), 35000) lu.assertEquals(launcher:getMaximumFiringAltitude(), 22000) local radars = self.samSite:getRadars() local radar = radars[1] lu.assertEquals(radar:getMaxRangeFindingTarget(), 66874.03125) end function TestSkynetIADSREDSAMSitesAndEWRadars:testCheckSA3GroupNumberOfLaunchersAndRangeValuesAndSearchRadarsAndNatoName() --[[ DCS properties SA-3 (s-125 / SA-3 Goa) Radar: { { detectionDistanceAir={ lowerHemisphere={headOn=53499.2265625, tailOn=53499.2265625}, upperHemisphere={headOn=53499.2265625, tailOn=53499.2265625} }, type=1, typeName="p-19 s-125 sr" } } Launcher: { { count=4, desc={ Nmax=16, RCS=0.1676000058651, _origin="", altMax=18000, altMin=20, box={ max={x=3.7270171642303, y=0.94484841823578, z=0.95312494039536}, min={x=-2.6432721614838, y=-0.94484841823578, z=-0.95312494039536} }, category=1, displayName="5V27 S-125 Neva (SA-3 Goa)", fuseDist=14, guidance=4, life=2, missileCategory=2, rangeMaxAltMax=25000, rangeMaxAltMin=11000, rangeMin=3500, typeName="SA5B27", warhead={caliber=400, explosiveMass=60, mass=60, type=1} } } } --]] self.samSiteName = "test-SA-3" self:setUp() local array = {} local unitData = { ['p-19 s-125 sr'] = { }, } self.samSite:analyseAndAddUnit(SkynetIADSSAMSearchRadar, array, unitData) local searchRadar = array[1] lu.assertEquals(#array, 1) lu.assertEquals(searchRadar:getMaxRangeFindingTarget(), 53499.2265625) array = {} unitData = { ['5p73 s-125 ln'] = { }, } self.samSite:analyseAndAddUnit(SkynetIADSSAMLauncher, array, unitData) local launcher = array[1] lu.assertEquals(launcher:getRange(), 25000) lu.assertEquals(launcher:getMaximumFiringAltitude(), 18000) array = {} unitData = { ['snr s-125 tr'] = { }, } self.samSite:analyseAndAddUnit(SkynetIADSSAMTrackingRadar, array, unitData) local searchRadar = array[1] lu.assertEquals(searchRadar:getMaxRangeFindingTarget(), 53499.2265625) lu.assertEquals(#self.samSite:getLaunchers(), 1) lu.assertEquals(#self.samSite:getSearchRadars(), 1) lu.assertEquals(#self.samSite:getTrackingRadars(), 1) lu.assertEquals(#self.samSite:getRadars(), 2) lu.assertEquals(self.samSite:getHARMDetectionChance(), 30) lu.assertEquals(self.samSite:setHARMDetectionChance(100), self.samSite) lu.assertEquals(self.samSite:getNatoName(), "SA-3") end function TestSkynetIADSREDSAMSitesAndEWRadars:testShilkaGroupLaunchersSearchRadarRangesAndHARMDefenceChance() --[[ DCS Properties Shilka / Zues: Radar: { { detectionDistanceAir={ lowerHemisphere={headOn=5015.552734375, tailOn=5015.552734375}, upperHemisphere={headOn=5015.552734375, tailOn=5015.552734375} }, type=1, typeName="ZSU-23-4 Shilka" } } Launcher: { { count=503, desc={ _origin="", box={ max={x=2.2344591617584, y=0.12504191696644, z=0.12113922089338}, min={x=-6.61008644104, y=-0.12504199147224, z=-0.12113920599222} }, category=0, displayName="23mm AP", life=2, typeName="weapons.shells.2A7_23_AP", warhead={caliber=23, explosiveMass=0, mass=0.189, type=0} } }, { count=1501, desc={ _origin="", box={ max={x=2.2344591617584, y=0.12504191696644, z=0.12113922089338}, min={x=-6.61008644104, y=-0.12504199147224, z=-0.12113920599222} }, category=0, displayName="23mm HE", life=2, typeName="weapons.shells.2A7_23_HE", warhead={caliber=23, explosiveMass=0.189, mass=0.189, type=1} } } } --]] self.samSiteName = "SAM-Shilka" self:setUp() lu.assertEquals(self.samSite:getHARMDetectionChance(), 10) lu.assertEquals(#self.samSite:getRadars(),1) lu.assertEquals(#self.samSite:getLaunchers(), 1) lu.assertEquals(#self.samSite:getTrackingRadars(), 0) local searchRadar = self.samSite:getSearchRadars()[1] lu.assertEquals(searchRadar:getMaxRangeFindingTarget(), 5015.552734375) local target = IADSContactFactory("Harrier Pilot") local launcher = self.samSite:getLaunchers()[1] --shilka has no missiles lu.assertEquals(launcher:getInitialNumberOfMissiles(), 0) lu.assertEquals(launcher:getRemainingNumberOfMissiles(), 0) lu.assertEquals(#launcher:getDCSRepresentation():getAmmo(), 2) lu.assertEquals(launcher:getInitialNumberOfShells(), 2004) lu.assertEquals(launcher:getRemainingNumberOfShells(), 2004) lu.assertEquals(launcher:getRange(), 5015.552734375) --dcs has no maximum height data for AAA lu.assertEquals(launcher:getMaximumFiringAltitude(), 0) lu.assertEquals(launcher:isWithinFiringHeight(target), true) lu.assertEquals(mist.utils.round(launcher:getHeight(target)), 1909) --this target is at 25k feet local target = IADSContactFactory("test-not-in-firing-range-of-sa-2") lu.assertEquals(launcher:isWithinFiringHeight(target), false) end function TestSkynetIADSREDSAMSitesAndEWRadars:testSA15LaunchersSearchRadarRangeAndHARMDefenceChance() --[[ DCS SA-15: properties: Launcher { count=8, desc={ Nmax=30, RCS=0.03070000000298, _origin="", altMax=6000, altMin=10, box={ max={x=1.8263295888901, y=0.26701140403748, z=0.26600670814514}, min={x=-1.678077340126, y=-0.26701140403748, z=-0.26600670814514} }, category=1, displayName="9M330 Tor (SA-15 Gauntlet)", fuseDist=7, guidance=4, life=2, missileCategory=2, rangeMaxAltMax=12000, rangeMaxAltMin=12000, rangeMin=1500, typeName="SA9M330", warhead={caliber=220, explosiveMass=14.5, mass=14.5, type=1} } } Radar: { 0={{opticType=0, type=0, typeName="generic SAM search visir"}}, { { detectionDistanceAir={ lowerHemisphere={headOn=16718.5078125, tailOn=16718.5078125}, upperHemisphere={headOn=16718.5078125, tailOn=16718.5078125} }, type=1, typeName="Tor 9A331" } } } --]] self.samSiteName = "SAM-SA-15" self:setUp() lu.assertEquals(self.samSite:getNatoName(),'SA-15') lu.assertEquals(self.samSite:getHARMDetectionChance(), 90) lu.assertEquals(#self.samSite:getRadars(),1) lu.assertEquals(self.samSite:getCanEngageHARM(), true) local target = IADSContactFactory("Harrier Pilot") local searchRadar = self.samSite:getSearchRadars()[1] lu.assertEquals(searchRadar:getMaxRangeFindingTarget(), 16718.5078125) lu.assertEquals(searchRadar:isInRange(target), false) lu.assertEquals(#self.samSite:getSearchRadars(), 1) lu.assertEquals(#self.samSite:getTrackingRadars(), 0) lu.assertEquals(#self.samSite:getLaunchers(), 1) local launcher = self.samSite:getLaunchers()[1] lu.assertEquals(launcher:getRange(), 12000) lu.assertEquals(launcher:getMaximumFiringAltitude(), 6000) lu.assertEquals(launcher:isInRange(target), false) lu.assertEquals(mist.utils.round(launcher:getHeight(target)), 1930) lu.assertEquals(launcher:getMaximumFiringAltitude(), 6000) lu.assertEquals(launcher:isWithinFiringHeight(target), true) lu.assertEquals(launcher:getRemainingNumberOfMissiles(), 8) launcher.maximumFiringAltitude = 400 lu.assertEquals(launcher:isWithinFiringHeight(target), false) end function TestSkynetIADSREDSAMSitesAndEWRadars:testSA13LaunchersSearchRadarRangeAndHARMDefence() --[[ DCS SA-13 Properties (Strela-10M3 / Gopher): { { count=8, desc={ Nmax=16, RCS=0.050000000745058, _origin="", altMax=3500, altMin=25, box={ max={x=1.1227556467056, y=0.13098473846912, z=0.13213211297989}, min={x=-1.1213990449905, y=-0.13098473846912, z=-0.13213211297989} }, category=1, displayName="9M333 Strela-10 (SA-13 Gopher)", fuseDist=3, guidance=2, life=2, missileCategory=2, rangeMaxAltMax=5000, rangeMaxAltMin=5000, rangeMin=800, typeName="SA9M333", warhead={caliber=120, explosiveMass=3.5, mass=3.5, type=1} } }, { count=1009, desc={ _origin="", box={ max={x=2.2344591617584, y=0.12504191696644, z=0.12113922089338}, min={x=-6.61008644104, y=-0.12504199147224, z=-0.12113920599222} }, category=0, displayName="7.62mm", life=2, typeName="weapons.shells.7_62x54", warhead={caliber=7.62, explosiveMass=0, mass=0.0119, type=0} } } Does not have any Radar Properties in DCS --]] self.samSiteName = "SAM-SA-13" self:setUp() lu.assertEquals(#self.samSite:getRadars(), 1) lu.assertEquals(#self.samSite:getSearchRadars(), 1) lu.assertEquals(#self.samSite:getTrackingRadars(), 0) lu.assertEquals(#self.samSite:getLaunchers(), 1) lu.assertEquals(self.samSite:getCanEngageHARM(), false) local searchRadar = self.samSite:getSearchRadars()[1] --this asset has no radar sensor information, we load the launcher data instead, to keep interface consistent: lu.assertEquals(searchRadar:getDCSRepresentation():getSensors(), nil) lu.assertEquals(searchRadar:getMaxRangeFindingTarget(), 5000) local launcher = self.samSite:getLaunchers()[1] lu.assertEquals(launcher:getRange(), 5000) lu.assertEquals(launcher:getMaximumFiringAltitude(), 3500) lu.assertEquals(launcher:getRemainingNumberOfMissiles(), 8) end function TestSkynetIADSREDSAMSitesAndEWRadars:testHQ7LauncherAndRadar() --[[ HQ-7: Radar: { 0={ {opticType=0, type=0, typeName="TKN-3B day"}, {opticType=2, type=0, typeName="TKN-3B night"}, {opticType=0, type=0, typeName="Tunguska optic sight"} }, { { detectionDistanceAir={ lowerHemisphere={headOn=10090.756835938, tailOn=6727.1713867188}, upperHemisphere={headOn=8408.9638671875, tailOn=6727.1713867188} }, type=1, typeName="HQ-7 TR" } } } Launcher: { { count=4, desc={ Nmax=18, RCS=0.0099999997764826, _origin="", altMax=5500, altMin=14.5, box={ max={x=1.245908498764, y=0.20055842399597, z=0.20074887573719}, min={x=-1.754227399826, y=-0.20056092739105, z=-0.20036999881268} }, category=1, displayName="HQ-7", fuseDist=7, guidance=4, life=2, missileCategory=2, rangeMaxAltMax=12000, rangeMaxAltMin=12000, rangeMin=500, typeName="HQ-7", warhead={caliber=156, explosiveMass=15, mass=15, type=1} } } } --]] self.samSiteName = "SAM-HQ-7" self:setUp() local group = Group.getByName(self.samSiteName) --[[ local units = group:getUnits() for i = 1, #units do local unit = units[i] if unit:getAmmo() then -- lu.assertEquals(unit:getAmmo(), false) end end --]] lu.assertEquals(self.samSite:getNatoName(), "CSA-4") lu.assertEquals(self.samSite:getLaunchers()[1]:getRange(), 12000) lu.assertEquals(mist.utils.round(self.samSite:getRadars()[1]:getMaxRangeFindingTarget()), 12613) end function TestSkynetIADSREDSAMSitesAndEWRadars:testSA5() self.samSiteName = "SAM-SA-5" self:setUp() lu.assertEquals(self.samSite:getNatoName(), "SA-5") local searchRadar = self.samSite:getSearchRadars()[1] lu.assertEquals(searchRadar:getMaxRangeFindingTarget(), 100311.046875) local trackingRadar = self.samSite:getTrackingRadars()[1] lu.assertEquals(trackingRadar:getMaxRangeFindingTarget(), 100311.046875) lu.assertEquals(self.samSite:getLaunchers()[1]:getRange(), 240000) lu.assertEquals(self.samSite:getLaunchers()[1]:getInitialNumberOfMissiles(), 1) end function TestSkynetIADSREDSAMSitesAndEWRadars:testSA5P19() self.samSiteName = "SAM-SA-5-p-19" self:setUp() lu.assertEquals(self.samSite:getNatoName(), "SA-5") local searchRadar = self.samSite:getSearchRadars()[1] lu.assertEquals(searchRadar:getMaxRangeFindingTarget(), 53499.2265625) local trackingRadar = self.samSite:getTrackingRadars()[1] lu.assertEquals(trackingRadar:getMaxRangeFindingTarget(), 53499.2265625) lu.assertEquals(self.samSite:getLaunchers()[1]:getRange(), 240000) lu.assertEquals(self.samSite:getLaunchers()[1]:getInitialNumberOfMissiles(), 1) end function TestSkynetIADSREDSAMSitesAndEWRadars:test1L13EWRBoxSpring() self.ewRadarName = "EW-west2" self:setUp() lu.assertEquals(self.ewRadar:getNatoName(), "Box Spring") lu.assertEquals(self.ewRadar:hasWorkingRadar(), true) end function TestSkynetIADSREDSAMSitesAndEWRadars:testCP9S80M1SborkaDogEar() self.ewRadarName = "EW-Dog Ear" self:setUp() lu.assertEquals(self.ewRadar:getNatoName(), "Dog Ear") lu.assertEquals(self.ewRadar:hasWorkingRadar(), true) end function TestSkynetIADSREDSAMSitesAndEWRadars:test55G6EWRTalRack() self.ewRadarName = "EW-west8" self:setUp() lu.assertEquals(self.ewRadar:getNatoName(), "Tall Rack") lu.assertEquals(self.ewRadar:hasWorkingRadar(), true) end function TestSkynetIADSREDSAMSitesAndEWRadars:testEWP19FlatFace() self.ewRadarName = "EW-SR-P19" self:setUp() lu.assertEquals(self.ewRadar:getNatoName(), "Flat Face") lu.assertEquals(self.ewRadar:hasWorkingRadar(), true) lu.assertEquals(self.ewRadar:getHARMDetectionChance(), 30) end function TestSkynetIADSREDSAMSitesAndEWRadars:testSA10BigBird() self.ewRadarName = "EW-SA-10" self:setUp() lu.assertEquals(self.ewRadar:getNatoName(), "Big Bird") lu.assertEquals(self.ewRadar:hasWorkingRadar(), true) end function TestSkynetIADSREDSAMSitesAndEWRadars:testSA10ClamShell() self.ewRadarName = "EW-SA-10-2" self:setUp() lu.assertEquals(self.ewRadar:getNatoName(), "Clam Shell") lu.assertEquals(self.ewRadar:hasWorkingRadar(), true) end function TestSkynetIADSREDSAMSitesAndEWRadars:testSA11SnowDrift() self.ewRadarName = "EW-SA-11" self:setUp() lu.assertEquals(self.ewRadar:getNatoName(), "Snow Drift") lu.assertEquals(self.ewRadar:hasWorkingRadar(), true) end function TestSkynetIADSREDSAMSitesAndEWRadars:testSA6StraightFlush() self.ewRadarName = "EW-SA-6" self:setUp() lu.assertEquals(self.ewRadar:getNatoName(), "Straight Flush") lu.assertEquals(self.ewRadar:hasWorkingRadar(), true) end function TestSkynetIADSREDSAMSitesAndEWRadars:testHQ7() self.ewRadarName = "EW-HQ7-STR" self:setUp() lu.assertEquals(self.ewRadar:getNatoName(), "CSA-4") lu.assertEquals(self.ewRadar:hasWorkingRadar(), true) end function TestSkynetIADSREDSAMSitesAndEWRadars:testA50AWACSAsEWRadar() --[[ DCS A-50 properties: Radar: { { detectionDistanceAir={ lowerHemisphere={headOn=204461.796875, tailOn=204461.796875}, upperHemisphere={headOn=204461.796875, tailOn=204461.796875} }, detectionDistanceRBM=2500, type=1, typeName="Shmel" } }, 3={{type=3, typeName="Abstract RWR"}} } --]] self.ewRadarName = "EW-AWACS-A-50" self:setUp() local unit = Unit.getByName(self.ewRadarName) lu.assertEquals(unit:getDesc().category, Unit.Category.AIRPLANE) lu.assertEquals(self.ewRadar:getNatoName(), 'A-50') local searchRadar = self.ewRadar:getSearchRadars()[1] lu.assertEquals(searchRadar:getMaxRangeFindingTarget(), 204461.796875) end function TestSkynetIADSREDSAMSitesAndEWRadars:testKJ2000AWACSAsEWRadar() --[[ DCS KJ-2000 properties: Radar: { { { detectionDistanceAir={ lowerHemisphere={headOn=268356.125, tailOn=268356.125}, upperHemisphere={headOn=268356.125, tailOn=268356.125} }, detectionDistanceRBM=3500, type=1, typeName="AESA_KJ2000" } }, 3={{type=3, typeName="Abstract RWR"}} } --]] self.ewRadarName = "EW-AWACS-KJ-2000" self:setUp() local unit = Unit.getByName('EW-AWACS-KJ-2000') local searchRadar = self.ewRadar:getSearchRadars()[1] lu.assertEquals(self.ewRadar:getNatoName(), 'KJ-2000') lu.assertEquals(searchRadar:getMaxRangeFindingTarget(), 268356.125) end end ================================================ FILE: unit-tests/test-skynet-iads-sam-site.lua ================================================ do TestSkynetIADSSAMSite = {} function TestSkynetIADSSAMSite:setUp() self.skynetIADS = SkynetIADS:create() if self.samSiteName then local samSite = Group.getByName(self.samSiteName) self.samSite = SkynetIADSSamSite:create(samSite, self.skynetIADS) -- we overrite this method since it returns radar contacts in the DCS world which mess up the tests. function self.samSite:getDetectedTargets() return {} end self.samSite:setupElements() self.samSite:goLive() end end function TestSkynetIADSSAMSite:tearDown() if self.samSite then self.samSite:goDark() self.samSite:cleanUp() end if self.skynetIADS then self.skynetIADS:deactivate() end self.samSite = nil self.samSiteName = nil end function TestSkynetIADSSAMSite:testCompleteDestructionOfSamSiteAndLoadDestroyedSAMSiteInToIADS() local samSite = SkynetIADSSamSite:create(Group.getByName("Destruction-test-sam"), self.skynetIADS):setActAsEW(true) samSite:setupElements() local samSite2 = SkynetIADSSamSite:create(Group.getByName('prefixtest-sam'), self.skynetIADS) samSite2:setupElements() samSite:addChildRadar(samSite2) samSite2:addParentRadar(samSite) lu.assertEquals(samSite2:getAutonomousState(), false) lu.assertEquals(samSite:isDestroyed(), false) lu.assertEquals(samSite:hasWorkingRadar(), true) local radars = samSite:getRadars() for i = 1, #radars do local radar = radars[i] trigger.action.explosion(radar:getDCSRepresentation():getPosition().p, 500) --we simulate a call to the event, since in game will be triggered to late to for later checks in this unit test samSite:onEvent(createDeadEvent()) end local launchers = samSite:getLaunchers() for i = 1, #launchers do local launcher = launchers[i] trigger.action.explosion(launcher:getDCSRepresentation():getPosition().p, 900) --we simulate a call to the event, since in game will be triggered to late to for later checks in this unit test samSite:onEvent(createDeadEvent()) end lu.assertEquals(samSite:isActive(), false) lu.assertEquals(samSite:isDestroyed(), true) lu.assertEquals(samSite:hasWorkingRadar(), false) lu.assertEquals(samSite:getRemainingNumberOfMissiles(), 0) lu.assertEquals(samSite:getInitialNumberOfMissiles(), 6) lu.assertEquals(samSite:hasRemainingAmmo(), false) --after destruction of samSite acting as EW samSite2 must be autonomous: lu.assertEquals(samSite2:getAutonomousState(), true) --test build SAM with destroyed elements samSite:cleanUp() local samSite = SkynetIADSSamSite:create(Group.getByName("Destruction-test-sam"), self.skynetIADS) samSite:setupElements() lu.assertEquals(samSite:getNatoName(), "UNKNOWN") lu.assertEquals(#samSite:getRadars(), 0) lu.assertEquals(#samSite:getLaunchers(), 0) samSite:cleanUp() samSite2:cleanUp() end function TestSkynetIADSSAMSite:testInformOfContactInRange() self.samSiteName = "SAM-SA-6" self:setUp() local mockContact = {} function mockContact:isIdentifiedAsHARM() return false end function self.samSite:isTargetInRange(target) lu.assertIs(target, mockContact) return true end self.samSite:goDark() self.samSite:targetCycleUpdateStart() lu.assertEquals(self.samSite:isActive(), false) self.samSite:informOfContact(mockContact) lu.assertEquals(self.samSite:isActive(), true) self.samSite:targetCycleUpdateEnd() lu.assertEquals(self.samSite:isActive(), true) end function TestSkynetIADSSAMSite:testInformOfContactNotInRange() self.samSiteName = "SAM-SA-6" self:setUp() local mockContact = {} function self.samSite:isTargetInRange(target) lu.assertIs(target, mockContact) return false end self.samSite:goDark() self.samSite:targetCycleUpdateStart() lu.assertEquals(self.samSite:isActive(), false) self.samSite:informOfContact(mockContact) lu.assertEquals(self.samSite:isActive(), false) self.samSite:targetCycleUpdateEnd() lu.assertEquals(self.samSite:isActive(), false) end function TestSkynetIADSSAMSite:testInformOfHARMContactSAMCanEngageHARM() self.samSiteName = "test-SAM-SA-2-test" self:setUp() function self.samSite:isTargetInRange(contact) return true end local mockTarget = {} function mockTarget:isIdentifiedAsHARM() return true end self.samSite:goDark() lu.assertEquals(self.samSite:isActive(), false) self.samSite:setCanEngageHARM(true) self.samSite:informOfContact(mockTarget) lu.assertEquals(self.samSite:isActive(), true) end function TestSkynetIADSSAMSite:testInformOfHARMContactSAMCanNotEngageHARM() self.samSiteName = "test-SAM-SA-2-test" self:setUp() function self.samSite:isTargetInRange(contact) return true end local mockTarget = {} function mockTarget:isIdentifiedAsHARM() return true end self.samSite:goDark() lu.assertEquals(self.samSite:isActive(), false) self.samSite:setCanEngageHARM(false) self.samSite:informOfContact(mockTarget) lu.assertEquals(self.samSite:isActive(), false) end function TestSkynetIADSSAMSite:testSA2InformOfContactTargetNotInRange() self.samSiteName = "test-SAM-SA-2-test" self:setUp() self.samSite:goDark() local target = IADSContactFactory('test-not-in-firing-range-of-sa-2') self.samSite:informOfContact(target) lu.assertEquals(self.samSite:isTargetInRange(target), false) lu.assertEquals(self.samSite:isActive(), false) end function TestSkynetIADSSAMSite:testSA2InforOfContactInSearchRangeSAMSiteGoLiveWhenSetToSearchRange() self.samSiteName = "test-SAM-SA-2-test" self:setUp() self.samSite:goDark() lu.assertEquals(self.samSite:isActive(), false) self.samSite:setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE) lu.assertIs(self.samSite:getEngagementZone(), SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE) local target = IADSContactFactory('test-not-in-firing-range-of-sa-2') self.samSite:informOfContact(target) lu.assertEquals(self.samSite:isActive(), true) end function TestSkynetIADSSAMSite:testInformOfContactMultipleTimesOnlyOneIsTargetInRangeCall() self.samSiteName = "SAM-SA-6" self:setUp() local mockContact = {} function mockContact:isIdentifiedAsHARM() return false end local numTimesCalledTargetInRange = 0 function self.samSite:isTargetInRange(target) numTimesCalledTargetInRange = numTimesCalledTargetInRange + 1 lu.assertIs(target, mockContact) return true end self.samSite:targetCycleUpdateStart() self.samSite:informOfContact(mockContact) self.samSite:informOfContact(mockContact) lu.assertEquals(numTimesCalledTargetInRange, 1) end function TestSkynetIADSSAMSite:testSAMStaysActiveWhenInAutonomousMode() self.samSiteName = "test-SAM-SA-2-test" self:setUp() lu.assertEquals(self.samSite:isActive(), true) lu.assertEquals(self.samSite:getAutonomousState(), true) self.samSite:targetCycleUpdateEnd() lu.assertEquals(self.samSite:isActive(), true) end function TestSkynetIADSSAMSite:testGoLiveConstraint() self.samSiteName = "SAM-SA-2" self:setUp() local contact = IADSContactFactory('test-in-firing-range-of-sa-2') local function goLiveConstraint(contact) return ( contact:getHeightInFeetMSL() > 4000 ) end lu.assertEquals(goLiveConstraint(contact), true) lu.assertEquals(self.samSite:areGoLiveConstraintsSatisfied(contact), true) self.samSite:addGoLiveConstraint('helicopter', goLiveConstraint) lu.assertEquals(self.samSite:areGoLiveConstraintsSatisfied(contact), true) --TODO: finish test to check return false if constraint is false local function goLiveConstraintFalse(contact) return ( contact:getHeightInFeetMSL() < 4000 ) end self.samSite:addGoLiveConstraint('helicopter', goLiveConstraintFalse) lu.assertEquals(self.samSite:areGoLiveConstraintsSatisfied(contact), false) end function TestSkynetIADSSAMSite:testRemoveGoLiveConstraint() self.samSiteName = "SAM-SA-2" self:setUp() self.samSite:addGoLiveConstraint("constraint", {}) --this marker funtion is to test if after removing the first function this one will still exist function testMarkerFunction(contact) return 3 end self.samSite:addGoLiveConstraint("test", testMarkerFunction) local count = 0 for constraintName, constraint in pairs(self.samSite:getGoLiveConstraints()) do count = count + 1 end lu.assertEquals(count, 2) count = 0 self.samSite:removeGoLiveConstraint("constraint") for constraintName, constraint in pairs(self.samSite:getGoLiveConstraints()) do count = count + 1 end lu.assertEquals(count, 1) lu.assertEquals(self.samSite:getGoLiveConstraints()["test"](contact), 3) end function TestSkynetIADSSAMSite:testSAMSiteWillNotGoLiveIfConstraintFailesAndContactIsInRange() self.samSiteName = "SAM-SA-2" self:setUp() local contact = IADSContactFactory('test-in-firing-range-of-sa-2') local function goLiveConstraintFalse(contact) return ( contact:getHeightInFeetMSL() < 4000 ) end self.samSite:addGoLiveConstraint('helicopter', goLiveConstraintFalse) self.samSite:goDark() self.samSite:targetCycleUpdateStart() self.samSite:informOfContact(contact) lu.assertEquals(self.samSite:isActive(), false) end end ================================================ FILE: unit-tests/test-skynet-iads.lua ================================================ do TestSkynetIADS = {} function TestSkynetIADS:setUp() self.numSAMSites = SKYNET_UNIT_TESTS_NUM_SAM_SITES_RED self.numEWSites = SKYNET_UNIT_TESTS_NUM_EW_SITES_RED self.testIADS = SkynetIADS:create() self.testIADS:addEarlyWarningRadarsByPrefix('EW') self.testIADS:addSAMSitesByPrefix('SAM') end function TestSkynetIADS:tearDown() if self.testIADS then self.testIADS:deactivate() end self.testIADS = nil end -- this function checks constants in DCS that the IADS relies on. A change to them might indicate that functionallity is broken. -- In the code constants are refereed to with their constant name calue, not the values the represent. function TestSkynetIADS:testDCSContstantsHaveNotChanged() lu.assertEquals(Weapon.Category.MISSILE, 1) lu.assertEquals(Weapon.Category.SHELL, 0) lu.assertEquals(world.event.S_EVENT_SHOT, 1) lu.assertEquals(world.event.S_EVENT_DEAD, 8) lu.assertEquals(Unit.Category.AIRPLANE, 0) end function TestSkynetIADS:testCaclulateNumberOfSamSitesAndEWRadars() self:tearDown() self.testIADS = SkynetIADS:create() lu.assertEquals(#self.testIADS:getSAMSites(), 0) lu.assertEquals(#self.testIADS:getEarlyWarningRadars(), 0) self.testIADS:addEarlyWarningRadarsByPrefix('EW') self.testIADS:addSAMSitesByPrefix('SAM') lu.assertEquals(#self.testIADS:getSAMSites(), self.numSAMSites) lu.assertEquals(#self.testIADS:getEarlyWarningRadars(), self.numEWSites) end function TestSkynetIADS:testCaclulateNumberOfSamSitesAndEWRadarsWhenAddMethodsCalledTwice() self:tearDown() self.testIADS = SkynetIADS:create() lu.assertEquals(#self.testIADS:getSAMSites(), 0) lu.assertEquals(#self.testIADS:getEarlyWarningRadars(), 0) self.testIADS:addEarlyWarningRadarsByPrefix('EW') self.testIADS:addEarlyWarningRadarsByPrefix('EW') self.testIADS:addSAMSitesByPrefix('SAM') self.testIADS:addSAMSitesByPrefix('SAM') lu.assertEquals(#self.testIADS:getSAMSites(), self.numSAMSites) lu.assertEquals(#self.testIADS:getEarlyWarningRadars(), self.numEWSites) end function TestSkynetIADS:testWrongCaseStringWillNotLoadSAMGroup() self:tearDown() self.testIADS = SkynetIADS:create() self.testIADS:addSAMSitesByPrefix('sam') lu.assertEquals(#self.testIADS:getSAMSites(), 0) end function TestSkynetIADS:testWrongCaseStringWillNotLoadEWRadars() self:tearDown() self.testIADS = SkynetIADS:create() self.testIADS:addEarlyWarningRadarsByPrefix('ew') lu.assertEquals(#self.testIADS:getEarlyWarningRadars(), 0) end function TestSkynetIADS:testEvaluateContacts1EWAnd1SAMSiteWithContactInRange() self:tearDown() local iads = SkynetIADS:create() local ewRadar = iads:addEarlyWarningRadar('EW-west23') function ewRadar:getDetectedTargets() return {IADSContactFactory('test-in-firing-range-of-sa-2')} end local samSite = iads:addSAMSite('SAM-SA-2') function samSite:getDetectedTargets() return {} end samSite:goDark() lu.assertEquals(samSite:isInRadarDetectionRangeOf(ewRadar), true) iads:activate() iads:evaluateContacts() lu.assertEquals(#iads:getContacts(), 1) lu.assertEquals(samSite:isActive(), true) -- we remove the target to test if the sam site will now go dark, was added for the performance optimised code function ewRadar:getDetectedTargets() return {} end iads:evaluateContacts() lu.assertEquals(samSite:isActive(), false) iads:deactivate() end function TestSkynetIADS:testEarlyWarningRadarHasWorkingPowerSourceByDefault() local ewRadar = self.testIADS:getEarlyWarningRadarByUnitName('EW-west') lu.assertEquals(ewRadar:hasWorkingPowerSource(), true) end function TestSkynetIADS:testAWACSHasMovedAndThereforeRebuildAutonomousStatesOfSAMSites() local iads = SkynetIADS:create() local awacs = iads:addEarlyWarningRadar('EW-AWACS-A-50') local updateCalls = 0 function iads:buildRadarCoverageForEarlyWarningRadar(ewRadar) SkynetIADS.buildRadarCoverageForEarlyWarningRadar(self, ewRadar) updateCalls = updateCalls + 1 end lu.assertEquals(awacs:getDistanceTraveledSinceLastUpdate(), 0) lu.assertEquals(getmetatable(awacs), SkynetIADSAWACSRadar) lu.assertEquals(awacs:getMaxAllowedMovementForAutonomousUpdateInNM(), 10) lu.assertEquals(awacs:isUpdateOfAutonomousStateOfSAMSitesRequired(), false) iads:evaluateContacts() lu.assertEquals(updateCalls, 0) --test distance calculation by giving the awacs a different position: local firstPos = Unit.getByName('EW-AWACS-KJ-2000'):getPosition().p awacs.lastUpdatePosition = firstPos lu.assertEquals(awacs:getDistanceTraveledSinceLastUpdate(), 763) lu.assertEquals(awacs:isUpdateOfAutonomousStateOfSAMSitesRequired(), true) -- a second imediate call shall result in false lu.assertEquals(awacs:getDistanceTraveledSinceLastUpdate(), 0) lu.assertEquals(awacs:isUpdateOfAutonomousStateOfSAMSitesRequired(), false) --we reset lastUpdatePosition to firstPos to test call in the IADS code -- TODO: when refactoring move this test to te AWACS Radar and use mock objects for integration tests in the IADS awacs.lastUpdatePosition = firstPos iads:evaluateContacts() lu.assertEquals(updateCalls, 1) iads:deactivate() end function TestSkynetIADS:testSAMSiteLoosesPower() local powerSource = StaticObject.getByName('SA-6 Power') local samSite = self.testIADS:getSAMSiteByGroupName('SAM-SA-6'):addPowerSource(powerSource) lu.assertEquals(#self.testIADS:getUsableSAMSites(), self.numSAMSites) samSite:goLive() lu.assertEquals(samSite:isActive(), true) trigger.action.explosion(powerSource:getPosition().p, 100) --we simulate a call to the event, since in game will be triggered to late to for later checks in this unit test samSite:onEvent(createDeadEvent()) lu.assertEquals(#self.testIADS:getUsableSAMSites(), self.numSAMSites-1) lu.assertEquals(samSite:isActive(), false) end function TestSkynetIADS:testSAMSiteSA6LostConnectionNodeAutonomusStateDCSAI() local sa6ConnectionNode = StaticObject.getByName('SA-6 Connection Node') self.testIADS:getSAMSiteByGroupName('SAM-SA-6'):addConnectionNode(sa6ConnectionNode) lu.assertEquals(#self.testIADS:getSAMSites(), self.numSAMSites) lu.assertEquals(#self.testIADS:getUsableSAMSites(), self.numSAMSites) trigger.action.explosion(sa6ConnectionNode:getPosition().p, 100) lu.assertEquals(#self.testIADS:getUsableSAMSites(), self.numSAMSites-1) lu.assertEquals(#self.testIADS:getUsableSAMSites(), self.numSAMSites-1) lu.assertEquals(#self.testIADS:getSAMSites(), self.numSAMSites) local samSite = self.testIADS:getSAMSiteByGroupName('SAM-SA-6') lu.assertEquals(samSite:isActive(), true) lu.assertEquals(samSite:getAutonomousState(), true) lu.assertEquals(samSite:isActive(), true) end function TestSkynetIADS:testAddRadarsToCommandCenter() local comCenter = StaticObject.getByName('command-center-3') self.testIADS:addCommandCenter(comCenter) local comC = self.testIADS:getCommandCenters()[1] local called = false function comC:clearChildRadars() called = true end --as long as IADS is not active addCommandCenter will not trigger addRadarsToCommandCenters when called: self.testIADS:addRadarsToCommandCenters() lu.assertEquals(called, true) lu.assertEquals(#comC:getChildRadars(), (self.numEWSites + self.numSAMSites)) end function TestSkynetIADS:testAddCommandCenter() local called = false function self.testIADS:addRadarsToCommandCenters() called = true end local comCenter = StaticObject.getByName('command-center-3') self.testIADS:addCommandCenter(comCenter) lu.assertEquals(called, false) self.testIADS:activate() self.testIADS:addCommandCenter(comCenter) lu.assertEquals(called, true) self.testIADS:deactivate() end function TestSkynetIADS:testOneCommandCenterHasNoConnectionNode() local commandCenter2 = StaticObject.getByName("Command Center2") local commandCenter2ConnectionNode = StaticObject.getByName("command-center-2-connection-node") local comCenter = self.testIADS:addCommandCenter(commandCenter2):addConnectionNode(commandCenter2ConnectionNode) lu.assertEquals(#comCenter:getConnectionNodes(), 1) lu.assertEquals(self.testIADS:isCommandCenterUsable(), true) local samSites = self.testIADS:getSAMSites() lu.assertEquals(#samSites, SKYNET_UNIT_TESTS_NUM_SAM_SITES_RED) local ewRadars = self.testIADS:getEarlyWarningRadars() lu.assertEquals(#ewRadars, SKYNET_UNIT_TESTS_NUM_EW_SITES_RED) self.testIADS:activate() trigger.action.explosion(commandCenter2ConnectionNode:getPosition().p, 500) --we simulate a call to the event, since in game will be triggered to late to for later checks in this unit test comCenter:onEvent(createDeadEvent()) lu.assertEquals(self.testIADS:isCommandCenterUsable(), false) --after the command center is no longer reachable we check to see if all SAM and EW radars are in their expected autonomous state: for i = 1, #samSites do local sam = samSites[i] lu.assertEquals(sam:getAutonomousState(), true) end for i = 1, #ewRadars do local ewRad = ewRadars[i] lu.assertEquals(ewRad:getAutonomousState(), true) end end function TestSkynetIADS:testOneCommandCenterLoosesPower() local commandCenter2Power = StaticObject.getByName("Command Center2 Power") local commandCenter2 = StaticObject.getByName("Command Center2") lu.assertEquals(#self.testIADS:getCommandCenters(), 0) lu.assertEquals(self.testIADS:isCommandCenterUsable(), true) local comCenter = self.testIADS:addCommandCenter(commandCenter2):addPowerSource(commandCenter2Power) lu.assertEquals(#comCenter:getPowerSources(), 1) lu.assertEquals(#self.testIADS:getCommandCenters(), 1) lu.assertEquals(self.testIADS:isCommandCenterUsable(), true) trigger.action.explosion(commandCenter2Power:getPosition().p, 10000) lu.assertEquals(#self.testIADS:getCommandCenters(), 1) lu.assertEquals(self.testIADS:isCommandCenterUsable(), false) end function TestSkynetIADS:testOneCommandCenterIsDestroyed() local commandCenter1 = StaticObject.getByName("Command Center") lu.assertEquals(#self.testIADS:getCommandCenters(), 0) self.testIADS:addCommandCenter(commandCenter1) lu.assertEquals(#self.testIADS:getCommandCenters(), 1) lu.assertEquals(self.testIADS:isCommandCenterUsable(), true) trigger.action.explosion(commandCenter1:getPosition().p, 10000) lu.assertEquals(#self.testIADS:getCommandCenters(), 1) lu.assertEquals(self.testIADS:isCommandCenterUsable(), false) end function TestSkynetIADS:testSetOptionsForSAMSiteType() local powerSource = StaticObject.getByName('SA-11-power-source') local connectionNode = StaticObject.getByName('SA-11-connection-node') lu.assertEquals(#self.testIADS:getSAMSitesByNatoName('SA-6'), 2) --lu.assertIs(getmetatable(self.testIADS:getSAMSitesByNatoName('SA-6')), SkynetIADSTableForwarder) local samSites = self.testIADS:getSAMSitesByNatoName('SA-6'):setActAsEW(true):addPowerSource(powerSource):addConnectionNode(connectionNode):setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE):setGoLiveRangeInPercent(90):setAutonomousBehaviour(SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK) lu.assertEquals(#samSites, 2) for i = 1, #samSites do local samSite = samSites[i] lu.assertEquals(samSite:getActAsEW(), true) lu.assertEquals(samSite:getEngagementZone(), SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE) lu.assertEquals(samSite:getGoLiveRangeInPercent(), 90) lu.assertEquals(samSite:getAutonomousBehaviour(), SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK) lu.assertIs(samSite:getConnectionNodes()[1], connectionNode) lu.assertIs(samSite:getPowerSources()[1], powerSource) end end function TestSkynetIADS:testSetOptionsForAllAddedSamSitesByPrefix() self:tearDown() self.testIADS = SkynetIADS:create() local samSites = self.testIADS:addSAMSitesByPrefix('SAM'):setActAsEW(true):addPowerSource(powerSource):addConnectionNode(connectionNode):setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE):setGoLiveRangeInPercent(90):setAutonomousBehaviour(SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK) lu.assertEquals(#samSites, self.numSAMSites) for i = 1, #samSites do local samSite = samSites[i] lu.assertEquals(samSite:getActAsEW(), true) lu.assertEquals(samSite:getEngagementZone(), SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE) lu.assertEquals(samSite:getGoLiveRangeInPercent(), 90) lu.assertEquals(samSite:getAutonomousBehaviour(), SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK) lu.assertIs(samSite:getConnectionNodes()[1], connectionNode) lu.assertIs(samSite:getPowerSources()[1], powerSource) end end function TestSkynetIADS:testSetOptionsForAllAddedSAMSites() local samSites = self.testIADS:getSAMSites():setActAsEW(true):addPowerSource(powerSource):addConnectionNode(connectionNode):setEngagementZone(SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE):setGoLiveRangeInPercent(90):setAutonomousBehaviour(SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK) lu.assertEquals(#samSites, self.numSAMSites) for i = 1, #samSites do local samSite = samSites[i] lu.assertEquals(samSite:getActAsEW(), true) lu.assertEquals(samSite:getEngagementZone(), SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE) lu.assertEquals(samSite:getGoLiveRangeInPercent(), 90) lu.assertEquals(samSite:getAutonomousBehaviour(), SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK) lu.assertIs(samSite:getConnectionNodes()[1], connectionNode) lu.assertIs(samSite:getPowerSources()[1], powerSource) end end function TestSkynetIADS:testSetOptionsForAllAddedEWSitesByPrefix() self:tearDown() self.testIADS = SkynetIADS:create() local ewSites = self.testIADS:addEarlyWarningRadarsByPrefix('EW'):addPowerSource(powerSource):addConnectionNode(connectionNode) lu.assertEquals(#ewSites, self.numEWSites) for i = 1, #ewSites do local ewSite = ewSites[i] lu.assertIs(ewSite:getConnectionNodes()[1], connectionNode) lu.assertIs(ewSite:getPowerSources()[1], powerSource) end end function TestSkynetIADS:testSetOptionsForAllAddedEWSites() local ewSites = self.testIADS:getEarlyWarningRadars() lu.assertEquals(#ewSites, self.numEWSites) for i = 1, #ewSites do local ewSite = ewSites[i] lu.assertIs(ewSite:getConnectionNodes()[1], connectionNode) lu.assertIs(ewSite:getPowerSources()[1], powerSource) end end function TestSkynetIADS:testMergeContacts() lu.assertEquals(#self.testIADS:getContacts(), 0) self.testIADS:mergeContact(IADSContactFactory('Harrier Pilot')) lu.assertEquals(#self.testIADS:getContacts(), 1) local contact = IADSContactFactory('Harrier Pilot') local mockRadar = {} function contact:getAbstractRadarElementsDetected() return {mockRadar} end self.testIADS:mergeContact(contact) lu.assertEquals(#self.testIADS:getContacts(), 1) local iadsContact = self.testIADS:getContacts()[1] lu.assertEquals(#iadsContact:getAbstractRadarElementsDetected(), 1) self.testIADS:mergeContact(IADSContactFactory('test-in-firing-range-of-sa-2')) lu.assertEquals(#self.testIADS:getContacts(), 2) end function TestSkynetIADS:testCleanAgedTargets() local iads = SkynetIADS:create() target1 = IADSContactFactory('test-in-firing-range-of-sa-2') function target1:getAge() return iads.maxTargetAge + 1 end target2 = IADSContactFactory('test-distance-calculation') function target2:getAge() return 1 end iads.contacts[1] = target1 iads.contacts[2] = target2 lu.assertEquals(#iads:getContacts(), 2) iads:cleanAgedTargets() lu.assertEquals(#iads:getContacts(), 1) iads:deactivate() end function TestSkynetIADS:testOnlyLoadGroupsWithPrefixForSAMSiteNotOtherUnitsOrStaticObjectsWithSamePrefix() self:tearDown() self.testIADS = SkynetIADS:create() local calledPrint = false function self.testIADS:printOutput(str, isWarning) calledPrint = true end self.testIADS:addSAMSitesByPrefix('prefixtest') lu.assertEquals(#self.testIADS:getSAMSites(), 1) lu.assertEquals(calledPrint, false) end function TestSkynetIADS:testOnlyLoadGroupsWithPrefixForSAMSiteNotOtherUnitsOrStaticObjectsWithSamePrefix2() self:tearDown() self.testIADS = SkynetIADS:create() local calledPrint = false function self.testIADS:printOutput(str, isWarning) calledPrint = true end --happened when the string.find method was not set to plain special characters messed up the regex pattern self.testIADS:addSAMSitesByPrefix('IADS-EW') lu.assertEquals(#self.testIADS:getSAMSites(), 1) lu.assertEquals(calledPrint, false) end function TestSkynetIADS:testOnlyLoadUnitsWithPrefixForEWSiteNotStaticObjectssWithSamePrefix() self:tearDown() self.testIADS = SkynetIADS:create() local calledPrint = false function self.testIADS:printOutput(str, isWarning) calledPrint = true end self.testIADS:addEarlyWarningRadarsByPrefix('prefixewtest') lu.assertEquals(#self.testIADS:getEarlyWarningRadars(), 1) lu.assertEquals(calledPrint, false) end --TODO rework this test for new evaluateContacts code: --[[ function TestSkynetIADS:testDontPassShipsGroundUnitsAndStructuresToSAMSites() -- make sure we don't get any targets in the test mission local ewRadars = self.testIADS:getEarlyWarningRadars() for i = 1, #ewRadars do local ewRadar = ewRadars[i] function ewRadar:getDetectedTargets() return {} end end local samSites = self.testIADS:getSAMSites() for i = 1, #samSites do local samSite = samSites[i] function samSite:getDetectedTargets() return {} end end self.testIADS:evaluateContacts() -- verifies we have a clean test setup lu.assertEquals(#self.testIADS.contacts, 0) -- ground units should not be passed to the SAM local mockContactGroundUnit = {} function mockContactGroundUnit:getDesc() return {category = Unit.Category.GROUND_UNIT} end function mockContactGroundUnit:getAge() return 0 end table.insert(self.testIADS.contacts, mockContactGroundUnit) local correlatedCalled = false function self.testIADS:informOfContact(contact) correlatedCalled = true end self.testIADS:evaluateContacts() lu.assertEquals(correlatedCalled, false) lu.assertEquals(#self.testIADS.contacts, 1) self.testIADS.contacts = {} -- ships should not be passed to the SAM local mockContactShip = {} function mockContactShip:getDesc() return {category = Unit.Category.SHIP} end function mockContactShip:getAge() return 0 end table.insert(self.testIADS.contacts, mockContactShip) correlatedCalled = false function self.testIADS:informOfContact(contact) correlatedCalled = true end self.testIADS:evaluateContacts() lu.assertEquals(correlatedCalled, false) lu.assertEquals(#self.testIADS.contacts, 1) self.testIADS.contacts = {} -- aircraft should be passed to the SAM local mockContactAirplane = {} function mockContactAirplane:getDesc() return {category = Unit.Category.AIRPLANE} end function mockContactAirplane:getAge() return 0 end table.insert(self.testIADS.contacts, mockContactAirplane) correlatedCalled = false function self.testIADS:informOfContact(contact) correlatedCalled = true end self.testIADS:evaluateContacts() --TODO: FIX TEST lu.assertEquals(correlatedCalled, true) lu.assertEquals(#self.testIADS.contacts, 1) self.testIADS.contacts = {} end --]] function TestSkynetIADS:testAddMooseSetGroup() local mockMooseSetGroup = {} local mockMooseConnector = {} local setGroupCalled = false function mockMooseConnector:addMooseSetGroup(group) setGroupCalled = true lu.assertEquals(mockMooseSetGroup, group) end function self.testIADS:getMooseConnector() return mockMooseConnector end self.testIADS:addMooseSetGroup(mockMooseSetGroup) lu.assertEquals(setGroupCalled, true) end --TODO: add more comparisons in this test, this test also tests buildRadarCoverageForAbstractRadarElement function TestSkynetIADS:testBuildRadarCoverage() self:tearDown() self.testIADS = SkynetIADS:create() local ewWest2 = self.testIADS:addEarlyWarningRadar('EW-west2') local samSA6 = self.testIADS:addSAMSite('SAM-SA-6') local samSA62 = self.testIADS:addSAMSite('SAM-SA-6-2') local samSA2 = self.testIADS:addSAMSite('SAM-SA-2') self.testIADS:buildRadarCoverage() local ewWestChildren = ewWest2:getChildRadars() lu.assertEquals(#ewWestChildren, 3) local containsSa6 = false local containsSA62 = false local containsSA2 = false for i = 1, #ewWestChildren do local radar = ewWestChildren[i] if radar == samSA6 then containsSa6 = true end if radar == samSA2 then containsSA2 = true end if radar == samSA62 then containsSA62 = true end end lu.assertEquals(containsSA2, true) lu.assertEquals(containsSA62, true) lu.assertEquals(containsSa6, true) --further tests to verify the exact content of the parent radars could be done with these: lu.assertEquals(#samSA6:getParentRadars(), 2) lu.assertEquals(#samSA6:getChildRadars(), 1) lu.assertEquals(#samSA62:getParentRadars(), 2) lu.assertEquals(#samSA62:getChildRadars(), 1) lu.assertEquals(#samSA2:getParentRadars(), 1) end --this test adds an EW Radar to an existing IADS, SAM site under coverage must then be adopted by the new EW radar function TestSkynetIADS:testBuildRadarCoverageForSingleEarlyWarningRadar() self:tearDown() self.testIADS = SkynetIADS:create() self.testIADS:addCommandCenter(StaticObject.getByName("Command Center")) local ewRadar = self.testIADS:getEarlyWarningRadarByUnitName('EW-west2') local sam2 = self.testIADS:addSAMSite('SAM-SA-6') local sam1 = self.testIADS:addSAMSite('SAM-SA-6-2') self.testIADS:buildRadarCoverage() lu.assertEquals(#sam1:getParentRadars(), 1) lu.assertEquals(#sam2:getParentRadars(), 1) lu.assertEquals(#self.testIADS:getCommandCenters()[1]:getChildRadars(), 2) local ewWest2 = self.testIADS:addEarlyWarningRadar('EW-west2') self.testIADS:buildRadarCoverageForEarlyWarningRadar(ewWest2) lu.assertEquals(#sam1:getParentRadars(), 2) lu.assertEquals(#sam2:getParentRadars(), 2) lu.assertEquals(#ewWest2:getChildRadars(), 2) lu.assertEquals(ewWest2:getAutonomousState(), false) lu.assertEquals(#self.testIADS:getCommandCenters()[1]:getChildRadars(), 3) end --this tet adds a SAM site to a IADS network, SAM site under coverage must then be adopted by the new SAM site also EW radars must be added as parents function TestSkynetIADS:testBuildRadarCoverageForSingleSAMSite() self:tearDown() self.testIADS = SkynetIADS:create() local sam1 = self.testIADS:addSAMSite('SAM-SA-6-2') local ewWest2 = self.testIADS:addEarlyWarningRadar('EW-west2') self.testIADS:buildRadarCoverage() lu.assertEquals(#sam1:getParentRadars(), 1) lu.assertEquals(#ewWest2:getChildRadars(), 1) local sam2 = self.testIADS:addSAMSite('SAM-SA-6') lu.assertEquals(sam2:getAutonomousState(), true) self.testIADS:buildRadarCoverageForSAMSite(sam2) lu.assertEquals(sam2:getAutonomousState(), false) lu.assertEquals(#sam2:getParentRadars(), 2) lu.assertEquals(#sam1:getParentRadars(), 2) lu.assertEquals(#ewWest2:getChildRadars(), 2) end function TestSkynetIADS:testGetSAMSitesByPrefix() local samSites = self.testIADS:getSAMSitesByPrefix('SAM-SA-15') lu.assertEquals(#samSites, 3) end function TestSkynetIADS:testSetMaxAgeOfCachedTargets() local iads = SkynetIADS:create() -- test default value lu.assertEquals(iads.contactUpdateInterval, 5) iads:setUpdateInterval(10) lu.assertEquals(iads.contactUpdateInterval, 10) lu.assertEquals(iads:getCachedTargetsMaxAge(), 10) local ewRadar = iads:addEarlyWarningRadar('EW-west') local samSite = iads:addSAMSite('SAM-SA-15-1') lu.assertEquals(ewRadar.cachedTargetsMaxAge, 10) lu.assertEquals(samSite.cachedTargetsMaxAge, 10) iads:deactivate() end function TestSkynetIADS:testAddSingleEWRadarAndSAMSiteWhenIADSIsActiveWillTriggerCorrectRadarCoverageUpdates() local iads = SkynetIADS:create() local calledSAMUpdate = 0 local calledEWUpdate = 0 function iads:buildRadarCoverageForSAMSite(samSite) calledSAMUpdate = calledSAMUpdate + 1 end function iads:buildRadarCoverageForEarlyWarningRadar(ewRadar) calledEWUpdate = calledEWUpdate + 1 end local ewRadar = iads:addEarlyWarningRadar('EW-west') lu.assertEquals(calledEWUpdate, 0) local samSite = iads:addSAMSite('SAM-SA-6-2') lu.assertEquals(calledSAMUpdate, 0) --simulate an active IADS: iads.ewRadarScanMistTaskID = 1 local ewRadar = iads:addEarlyWarningRadar('EW-west') lu.assertEquals(calledEWUpdate, 1) local samSite = iads:addSAMSite('SAM-SA-6-2') lu.assertEquals(calledSAMUpdate, 1) iads:deactivate() end function TestSkynetIADS:testBuildIADSWithAutonomousSAMS() local iads = SkynetIADS:create() local samSite = iads:addSAMSite('SAM-SA-10') iads:activate() lu.assertEquals(samSite:isActive(), true) iads:deactivate() end end ================================================ FILE: unit-tests/test-skynet-moose-a2a-dispatcher-connector.lua ================================================ do TestMooseA2ADispatcherConnector = {} function TestMooseA2ADispatcherConnector:setUp() self.iads = SkynetIADS:create() self.iads:addEarlyWarningRadarsByPrefix("EW") self.iads:addSAMSitesByPrefix("SAM") self.connector = SkynetMooseA2ADispatcherConnector:create(self.iads) end function TestMooseA2ADispatcherConnector:tearDown() self.iads:deactivate() end function TestMooseA2ADispatcherConnector:testGetEarlyWarningRadarGroupNames() local ewRadarNames = self.connector:getEarlyWarningRadarGroupNames() ---we iterate through the EW radars of the IADS, to check the table in the connector contains all the names of the EW radars local usableEWRadars = self.iads:getUsableEarlyWarningRadars() local numRadars = 0 for i = 1, #ewRadarNames do local ewName = ewRadarNames[i] local ewFound = false for j = 1, #usableEWRadars do local ewNameInIADS = usableEWRadars[j]:getDCSRepresentation():getGroup():getName() if ewName == ewNameInIADS then ewFound = true end end lu.assertEquals(ewFound, true) numRadars = numRadars + 1 end lu.assertEquals(numRadars, SKYNET_UNIT_TESTS_NUM_EW_SITES_RED) end function TestMooseA2ADispatcherConnector:testGetSAMSitesGroupNames() local samSiteGroupNames = self.connector:getSAMSiteGroupNames() ---we iterate through the SAM sites of the IADS, to check the table in the connector contains all the names of the SAM sites local usableSAMSites = self.iads:getUsableSAMSites() local numSams = 0 for i = 1, #samSiteGroupNames do local samSiteName = samSiteGroupNames[i] local samFound = false for j = 1, #usableSAMSites do local samNameInIADS = usableSAMSites[j]:getDCSName() if samSiteName == samNameInIADS then samFound = true end end lu.assertEquals(samFound, true) numSams = numSams + 1 end lu.assertEquals(numSams, SKYNET_UNIT_TESTS_NUM_SAM_SITES_RED) end function TestMooseA2ADispatcherConnector:testAddMooseSetGroupAndUpdate() local mockMooseSetGroup = {} mockMooseSetGroup.connector = self.connector local numRemoveCalls = 0 function mockMooseSetGroup:RemoveGroupsByName(groupNames) numRemoveCalls = numRemoveCalls + 1 if numRemoveCalls == 1 then lu.assertEquals(groupNames, self.connector.ewRadarGroupNames) end if numRemoveCalls == 2 then lu.assertEquals(groupNames, self.connector.samSiteGroupNames) end end local samGroups = {} function self.connector:getSAMSiteGroupNames() return samGroups end local ewGroups = {} function self.connector:getEarlyWarningRadarGroupNames() return ewGroups end local numAddCalls = 0 function mockMooseSetGroup:AddGroupsByName(groupNames) if numAddCalls == 0 then lu.assertEquals(groupNames, samGroups) end if numAddCalls == 1 then lu.assertEquals(groupNames, ewGroups) end numAddCalls = numAddCalls + 1 end self.connector:addMooseSetGroup(mockMooseSetGroup) lu.assertEquals(numRemoveCalls, 2) lu.assertEquals(numAddCalls, 2) end end ================================================ FILE: unit-tests/test-syknet-early-warning-radar.lua ================================================ do TestSkynetIADSEWRadar = {} function TestSkynetIADSEWRadar:setUp() self.numEWSites = SKYNET_UNIT_TESTS_NUM_EW_SITES_RED if self.blue == nil then self.blue = "" end if self.ewRadarName then self.iads = SkynetIADS:create() self.iads:addEarlyWarningRadarsByPrefix(self.blue..'EW') self.ewRadar = self.iads:getEarlyWarningRadarByUnitName(self.ewRadarName) end end function TestSkynetIADSEWRadar:tearDown() if self.ewRadar then self.ewRadar:cleanUp() end if self.iads then self.iads:deactivate() end self.iads = nil self.ewRadar = nil self.ewRadarName = nil self.blue = "" end function TestSkynetIADSEWRadar:testCompleteDestructionOfEarlyWarningRadar() local ewRadar = SkynetIADSAWACSRadar:create(Unit.getByName('EW-west22-destroy'), SkynetIADS:create('test')) ewRadar:setupElements() ewRadar:setActAsEW(true) ewRadar:goLive() local sa61 = SkynetIADSSamSite:create(Group.getByName('SAM-SA-6'), SkynetIADS:create('test')) local sa62 = SkynetIADSSamSite:create(Group.getByName('SAM-SA-6-2'), SkynetIADS:create('test')) --build radar association ewRadar:addChildRadar(sa61) sa61:addParentRadar(ewRadar) ewRadar:addChildRadar(sa62) sa62:addParentRadar(ewRadar) sa61:setToCorrectAutonomousState() sa62:setToCorrectAutonomousState() lu.assertEquals(ewRadar:hasRemainingAmmo(), true) lu.assertEquals(ewRadar:isActive(), true) lu.assertEquals(ewRadar:getDCSRepresentation():isExist(), true) lu.assertEquals(sa61:getAutonomousState(), false) lu.assertEquals(sa62:getAutonomousState(), false) trigger.action.explosion(ewRadar:getDCSRepresentation():getPosition().p, 500) --we simulate a call to the event, since in game will be triggered to late to for later checks in this unit test ewRadar:onEvent(createDeadEvent()) lu.assertEquals(ewRadar:getDCSRepresentation():isExist(), false) lu.assertEquals(ewRadar:isActive(), false) lu.assertEquals(sa61:getAutonomousState(), true) lu.assertEquals(sa62:getAutonomousState(), true) sa61:cleanUp() sa62:cleanUp() ewRadar:cleanUp() end function TestSkynetIADSEWRadar:testFinishHARMDefence() self.ewRadarName = "EW-west2" self:setUp() lu.assertEquals(self.ewRadar:isActive(), true) lu.assertEquals(self.ewRadar:hasRemainingAmmo(), true) self.ewRadar:goSilentToEvadeHARM() lu.assertEquals(self.ewRadar:isActive(), false) self.ewRadar.finishHarmDefence(self.ewRadar) lu.assertEquals(self.ewRadar.harmSilenceID, nil) self.iads.evaluateContacts(self.iads) lu.assertEquals(self.ewRadar:isActive(), true) end function TestSkynetIADSEWRadar:testGoDarkWhenAutonomousByDefault() self.ewRadarName = "EW-west2" self:setUp() lu.assertEquals(self.ewRadar:isActive(), true) function self.ewRadar:hasActiveConnectionNode() return false end self.ewRadar:goAutonomous() lu.assertEquals(self.ewRadar:isActive(), false) end end