diff --git a/Generator/Forces/blue/BLUE Default Armor.miz b/Generator/Forces/blue/BLUE Default US Armor.miz
similarity index 100%
rename from Generator/Forces/blue/BLUE Default Armor.miz
rename to Generator/Forces/blue/BLUE Default US Armor.miz
diff --git a/Generator/Forces/blue/BLUE Greece Armor (Mr Nobody).miz b/Generator/Forces/blue/BLUE Greece Armor (Mr Nobody).miz
new file mode 100644
index 0000000..e3a6485
Binary files /dev/null and b/Generator/Forces/blue/BLUE Greece Armor (Mr Nobody).miz differ
diff --git a/Generator/Forces/blue/BLUE Iran Armor (Mr Nobody).miz b/Generator/Forces/blue/BLUE Iran Armor (Mr Nobody).miz
new file mode 100644
index 0000000..d9da8ed
Binary files /dev/null and b/Generator/Forces/blue/BLUE Iran Armor (Mr Nobody).miz differ
diff --git a/Generator/Forces/blue/BLUE Turkey Armor (Mr Nobody).miz b/Generator/Forces/blue/BLUE Turkey Armor (Mr Nobody).miz
new file mode 100644
index 0000000..8f2ee4d
Binary files /dev/null and b/Generator/Forces/blue/BLUE Turkey Armor (Mr Nobody).miz differ
diff --git a/Generator/Forces/blue/BLUE UK Armor (Mr Nobody).miz b/Generator/Forces/blue/BLUE UK Armor (Mr Nobody).miz
new file mode 100644
index 0000000..5a88e7d
Binary files /dev/null and b/Generator/Forces/blue/BLUE UK Armor (Mr Nobody).miz differ
diff --git a/Generator/Forces/blue/BLUE US 1970s Armor & Infantry (Mr Nobody).miz b/Generator/Forces/blue/BLUE US 1970s Armor & Infantry (Mr Nobody).miz
new file mode 100644
index 0000000..c5fe727
Binary files /dev/null and b/Generator/Forces/blue/BLUE US 1970s Armor & Infantry (Mr Nobody).miz differ
diff --git a/Generator/Forces/red/RED Armor, Infantry & Artillery (Med).miz b/Generator/Forces/red/RED Armor, Infantry & Artillery (Med).miz
deleted file mode 100644
index b2eec21..0000000
Binary files a/Generator/Forces/red/RED Armor, Infantry & Artillery (Med).miz and /dev/null differ
diff --git a/Generator/Forces/red/RED Armor (Hard).miz b/Generator/Forces/red/RED Default Armor (HARD).miz
similarity index 100%
rename from Generator/Forces/red/RED Armor (Hard).miz
rename to Generator/Forces/red/RED Default Armor (HARD).miz
diff --git a/Generator/Forces/red/RED Default Armor, Infantry & Artillery (MED).miz b/Generator/Forces/red/RED Default Armor, Infantry & Artillery (MED).miz
new file mode 100644
index 0000000..f7a5175
Binary files /dev/null and b/Generator/Forces/red/RED Default Armor, Infantry & Artillery (MED).miz differ
diff --git a/Generator/Forces/red/RED Trucks & Infantry (Easy).miz b/Generator/Forces/red/RED Default Trucks & Infantry (EASY).miz
similarity index 100%
rename from Generator/Forces/red/RED Trucks & Infantry (Easy).miz
rename to Generator/Forces/red/RED Default Trucks & Infantry (EASY).miz
diff --git a/Generator/Forces/red/RED Greece Armor (Mr Nobody).miz b/Generator/Forces/red/RED Greece Armor (Mr Nobody).miz
new file mode 100644
index 0000000..7fa5bbb
Binary files /dev/null and b/Generator/Forces/red/RED Greece Armor (Mr Nobody).miz differ
diff --git a/Generator/Forces/red/RED Iran Armor & Infantry (Mr Nobody).miz b/Generator/Forces/red/RED Iran Armor & Infantry (Mr Nobody).miz
new file mode 100644
index 0000000..301eba9
Binary files /dev/null and b/Generator/Forces/red/RED Iran Armor & Infantry (Mr Nobody).miz differ
diff --git a/Generator/Imports/FARP_DEFAULT_ZONE.miz b/Generator/Imports/FARP_DEFAULT_ZONE.miz
new file mode 100644
index 0000000..8ba07ee
Binary files /dev/null and b/Generator/Imports/FARP_DEFAULT_ZONE.miz differ
diff --git a/Generator/Imports/FARP_MINIMUM_ROADSIDE_INVULNERABLE.miz b/Generator/Imports/FARP_MINIMUM_ROADSIDE_INVULNERABLE.miz
new file mode 100644
index 0000000..b69db01
Binary files /dev/null and b/Generator/Imports/FARP_MINIMUM_ROADSIDE_INVULNERABLE.miz differ
diff --git a/Generator/Imports/FARP_MINIMUM_ROADSIDE_STATICS.miz b/Generator/Imports/FARP_MINIMUM_ROADSIDE_STATICS.miz
new file mode 100644
index 0000000..b40d8b2
Binary files /dev/null and b/Generator/Imports/FARP_MINIMUM_ROADSIDE_STATICS.miz differ
diff --git a/Generator/Imports/FARP_MOBILE_ROADSIDE_INVULNERABLE.miz b/Generator/Imports/FARP_MOBILE_ROADSIDE_INVULNERABLE.miz
new file mode 100644
index 0000000..86ced3a
Binary files /dev/null and b/Generator/Imports/FARP_MOBILE_ROADSIDE_INVULNERABLE.miz differ
diff --git a/Generator/Imports/FARP_MOBILE_ROADSIDE_STATICS.miz b/Generator/Imports/FARP_MOBILE_ROADSIDE_STATICS.miz
new file mode 100644
index 0000000..dfae3b3
Binary files /dev/null and b/Generator/Imports/FARP_MOBILE_ROADSIDE_STATICS.miz differ
diff --git a/Generator/Imports/FOB_16_SPWN_WIDE.miz b/Generator/Imports/FOB_16_SPWN_WIDE.miz
new file mode 100644
index 0000000..f00aa0f
Binary files /dev/null and b/Generator/Imports/FOB_16_SPWN_WIDE.miz differ
diff --git a/Generator/Imports/FOB_8_SPWN.miz b/Generator/Imports/FOB_8_SPWN.miz
new file mode 100644
index 0000000..be862f6
Binary files /dev/null and b/Generator/Imports/FOB_8_SPWN.miz differ
diff --git a/Generator/Imports/How to use imports.txt b/Generator/Imports/How to use imports.txt
new file mode 100644
index 0000000..5634805
--- /dev/null
+++ b/Generator/Imports/How to use imports.txt
@@ -0,0 +1,17 @@
+You can put .miz files in this folder to be copied into the generated mission at marker points. This feature is currently very 'alpha' and may produce errors. Currently, this doesn't work for ship groups or plane groups.
+
+1) Make an empty mission on Cauacasus.
+
+2) Place units/objects on the map.
+
+3) Make one unit group name: 'ANCHOR' This will represent the point of insertion into the target mission.
+
+4) In a Scenario template, place a static object (flag, etc) and call it "IMPORT-[filename of .miz created in first step]" Country should be CJTF Red, CJTF Blue, or UN Peacekeepers.
+
+5) Change the unit name of the object created in the previous step. This unit name might be used for spawn names, so you should call the unit name something like "North Base" so players know where they'll be spawning when choosing a slot.
+
+
+Tips:
+-You can change the heading of the imported group by changing the heading of the insertion object.
+-For multiple imports of the same template, the import object group name should end with '-01' or '-whatever'.
+
diff --git a/Generator/Imports/MARKET_PLACE.miz b/Generator/Imports/MARKET_PLACE.miz
new file mode 100644
index 0000000..28ee14c
Binary files /dev/null and b/Generator/Imports/MARKET_PLACE.miz differ
diff --git a/Generator/Imports/STAGING_LOGISTIC_HUB.miz b/Generator/Imports/STAGING_LOGISTIC_HUB.miz
new file mode 100644
index 0000000..b0497b9
Binary files /dev/null and b/Generator/Imports/STAGING_LOGISTIC_HUB.miz differ
diff --git a/Generator/Imports/VILLA_GRIMM.miz b/Generator/Imports/VILLA_GRIMM.miz
new file mode 100644
index 0000000..ab6d00d
Binary files /dev/null and b/Generator/Imports/VILLA_GRIMM.miz differ
diff --git a/Generator/MissionGenerator.py b/Generator/MissionGenerator.py
index ccb4384..81e0d70 100644
--- a/Generator/MissionGenerator.py
+++ b/Generator/MissionGenerator.py
@@ -2,17 +2,26 @@ import math
import sys
import os
import dcs
+from PyQt5.QtCore import QCoreApplication
+from PyQt5.uic.properties import QtCore
+
import RotorOpsMission as ROps
import RotorOpsUtils
import RotorOpsUnits
import logging
+import json
+import yaml
+import requests
from PyQt5.QtWidgets import (
QApplication, QDialog, QMainWindow, QMessageBox
)
from PyQt5 import QtGui
+from PyQt5 import Qt, QtCore
from MissionGeneratorUI import Ui_MainWindow
+import qtmodern.styles
+import qtmodern.windows
#Setup logfile and exception handler
logger = logging.getLogger(__name__)
@@ -20,6 +29,30 @@ logging.basicConfig(filename='generator.log', encoding='utf-8', level=logging.DE
handler = logging.StreamHandler(stream=sys.stdout)
logger.addHandler(handler)
+user_files_url = 'https://dcs-helicopters.com/user-files/'
+
+class directories:
+ home_dir = scenarios = forces = scripts = sound = output = assets = imports = None
+
+ @classmethod
+ def find(cls):
+ current_dir = os.getcwd()
+ if os.path.basename(os.getcwd()) == "Generator":
+ os.chdir("..")
+ cls.home_dir = os.getcwd()
+ cls.scenarios = cls.home_dir + "\Generator\Scenarios"
+ cls.forces = cls.home_dir + "\Generator\Forces"
+ cls.scripts = cls.home_dir
+ cls.sound = cls.home_dir + "\sound\embedded"
+ cls.output = cls.home_dir + "\Generator\Output"
+ cls.assets = cls.home_dir + "\Generator/assets"
+ cls.imports = cls.home_dir + "\Generator\Imports"
+ os.chdir(current_dir)
+
+
+directories.find()
+
+
def handle_exception(exc_type, exc_value, exc_traceback):
if issubclass(exc_type, KeyboardInterrupt): #example of handling error subclasses
sys.__excepthook__(exc_type, exc_value, exc_traceback)
@@ -28,15 +61,15 @@ def handle_exception(exc_type, exc_value, exc_traceback):
logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))
msg = QMessageBox()
msg.setWindowTitle("Uncaught exception")
- msg.setText("Oops, there was a problem. Please check the log file or post it in the RotorOps discord where some helpful people will have a look.")
+ msg.setText("Oops, there was a problem. Please check the log file for more details or post it in the RotorOps discord where some helpful people will have a look. \n\n" + str(exc_value))
x = msg.exec_()
sys.excepthook = handle_exception
-
-maj_version = 0
-minor_version = 4
+build = 1
+maj_version = 1
+minor_version = 1
version_string = str(maj_version) + "." + str(minor_version)
scenarios = []
red_forces_files = []
@@ -54,8 +87,8 @@ class Window(QMainWindow, Ui_MainWindow):
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
logger.info('running in a PyInstaller bundle')
- home_dir = os.getcwd()
- os.chdir(home_dir + "/Generator")
+ qtmodern.styles._STYLESHEET = directories.assets + '/style.qss'
+ qtmodern.windows._FL_STYLESHEET = directories.assets + '/frameless.qss'
else:
logger.info('running in a normal Python process')
@@ -68,9 +101,9 @@ class Window(QMainWindow, Ui_MainWindow):
self.populateForces("blue", self.blueforces_comboBox, blue_forces_files)
self.populateSlotSelection()
- self.blue_forces_label.setText(attackers_text)
- self.red_forces_label.setText(defenders_text)
- self.background_label.setPixmap(QtGui.QPixmap(self.m.assets_dir + "/background.PNG"))
+ # self.blue_forces_label.setText(attackers_text)
+ # self.red_forces_label.setText(defenders_text)
+ self.background_label.setPixmap(QtGui.QPixmap(directories.assets + "/rotorops-dkgray.png"))
self.statusbar.setStyleSheet(
"QStatusBar{padding-left:5px;color:black;font-weight:bold;}")
@@ -81,9 +114,11 @@ class Window(QMainWindow, Ui_MainWindow):
self.action_generateMission.triggered.connect(self.generateMissionAction)
self.action_scenarioSelected.triggered.connect(self.scenarioChanged)
self.action_defensiveModeChanged.triggered.connect(self.defensiveModeChanged)
+ self.action_nextScenario.triggered.connect(self.nextScenario)
+ self.action_prevScenario.triggered.connect(self.prevScenario)
def populateScenarios(self):
- os.chdir(self.m.scenarios_dir)
+ os.chdir(directories.scenarios)
path = os.getcwd()
dir_list = os.listdir(path)
logger.info("Looking for mission files in " + path)
@@ -94,8 +129,8 @@ class Window(QMainWindow, Ui_MainWindow):
self.scenario_comboBox.addItem(filename.removesuffix('.miz'))
def populateForces(self, side, combobox, files_list):
- os.chdir(self.m.home_dir)
- os.chdir(self.m.forces_dir + "/" + side)
+ os.chdir(directories.home_dir)
+ os.chdir(directories.forces + "/" + side)
path = os.getcwd()
dir_list = os.listdir(path)
logger.info("Looking for " + side + " Forces files in '" + path)
@@ -109,18 +144,63 @@ class Window(QMainWindow, Ui_MainWindow):
self.slot_template_comboBox.addItem("Multiple Slots")
for type in RotorOpsUnits.client_helos:
self.slot_template_comboBox.addItem(type.id)
+ self.slot_template_comboBox.addItem("None")
def defensiveModeChanged(self):
- if self.defense_checkBox.isChecked():
- self.red_forces_label.setText(attackers_text)
- self.blue_forces_label.setText(defenders_text)
- else:
- self.red_forces_label.setText(defenders_text)
- self.blue_forces_label.setText(attackers_text)
+ # if self.defense_checkBox.isChecked():
+ # self.red_forces_label.setText(attackers_text)
+ # self.blue_forces_label.setText(defenders_text)
+ # else:
+ # self.red_forces_label.setText(defenders_text)
+ # self.blue_forces_label.setText(attackers_text)
+
+ self.applyScenarioConfig()
+
+
+ def loadScenarioConfig(self, filename):
+ try:
+ j = open(filename)
+ config = json.load(j)
+ j.close()
+ return config
+ except:
+ return None
+
+ def lockedSlot(self):
+ return self.slot_template_comboBox.findText("Locked to Scenario")
+
+ def clearScenarioConfig(self):
+ # reset default states
+ self.defense_checkBox.setEnabled(True)
+ if self.lockedSlot():
+ self.slot_template_comboBox.removeItem(self.lockedSlot())
+
+ self.slot_template_comboBox.setEnabled(True)
+ self.slot_template_comboBox.setCurrentIndex(0)
+
+ def applyScenarioConfig(self):
+
+ if not self.config:
+ return
+
+ if self.config['defense']['allowed'] == False:
+ self.defense_checkBox.setChecked(False)
+ self.defense_checkBox.setEnabled(False)
+ elif self.config['offense']['allowed'] == False:
+ self.defense_checkBox.setChecked(True)
+ self.defense_checkBox.setEnabled(False)
+
+ if self.config['defense']['player_spawn'] == "fixed":
+ self.slot_template_comboBox.addItem("Locked to Scenario")
+ self.slot_template_comboBox.setCurrentIndex(self.lockedSlot())
+ self.slot_template_comboBox.setEnabled(False)
+
+
+
def scenarioChanged(self):
- os.chdir(self.m.scenarios_dir)
+ os.chdir(directories.scenarios)
filename = scenarios[self.scenario_comboBox.currentIndex()]
source_mission = dcs.mission.Mission()
source_mission.load_file(filename)
@@ -136,6 +216,14 @@ class Window(QMainWindow, Ui_MainWindow):
friendly_airports = True
enemy_airports = True
+ self.clearScenarioConfig()
+ config_filename = filename.removesuffix(".miz") + ".json"
+ self.config = self.loadScenarioConfig(config_filename)
+ if self.config:
+ self.applyScenarioConfig()
+ self.m.setConfig(self.config)
+
+
for zone in zones:
if zone.name == "STAGING":
staging_zones += 1
@@ -174,14 +262,23 @@ class Window(QMainWindow, Ui_MainWindow):
+ "\n== BRIEFING ==\n\n"
+ source_mission.description_text()
)
- #self.description_textBrowser.setText("File error occured.")
+
+ path = directories.scenarios + "/" + filename.removesuffix(".miz") + ".jpg"
+ if os.path.isfile(path):
+ self.missionImage.setPixmap(QtGui.QPixmap(path))
+ else:
+ self.missionImage.setPixmap(QtGui.QPixmap(directories.assets + "/briefing1.png"))
+
+
def generateMissionAction(self):
red_forces_filename = red_forces_files[self.redforces_comboBox.currentIndex()]
blue_forces_filename = blue_forces_files[self.blueforces_comboBox.currentIndex()]
scenario_filename = scenarios[self.scenario_comboBox.currentIndex()]
+ source = "offline"
data = {
+ "source": source,
"scenario_filename": scenario_filename,
"red_forces_filename": red_forces_filename,
"blue_forces_filename": blue_forces_filename,
@@ -206,7 +303,7 @@ class Window(QMainWindow, Ui_MainWindow):
"transport_drop_qty": self.troop_drop_spinBox.value(),
"smoke_pickup_zones": self.smoke_pickup_zone_checkBox.isChecked(),
}
- os.chdir(self.m.home_dir + '/Generator')
+ os.chdir(directories.home_dir + '/Generator')
n = ROps.RotorOpsMission()
result = n.generateMission(data)
logger.info("Generating mission with options:")
@@ -222,7 +319,7 @@ class Window(QMainWindow, Ui_MainWindow):
msg = QMessageBox()
msg.setWindowTitle("Mission Generated")
msg.setText("Awesome, your mission is ready! It's located in this directory: \n" +
- self.m.output_dir + "\n" +
+ directories.output + "\n" +
"\n" +
"Next, you should use the DCS Mission Editor to fine tune unit placements. Don't be afraid to edit the missions that this generator produces. \n" +
"\n" +
@@ -242,13 +339,90 @@ class Window(QMainWindow, Ui_MainWindow):
msg.setText(result["failure_msg"])
x = msg.exec_()
+ def prevScenario(self):
+ self.scenario_comboBox.setCurrentIndex((self.scenario_comboBox.currentIndex() - 1))
+
+ def nextScenario(self):
+ self.scenario_comboBox.setCurrentIndex((self.scenario_comboBox.currentIndex() + 1))
+
+ def checkVersion(self):
+ try:
+ url = user_files_url + 'versions.yaml'
+ r = requests.get(url, allow_redirects=False)
+ v = yaml.safe_load(r.content)
+ print(v["build"])
+ avail_build = v["build"]
+ if avail_build > build:
+ msg = QMessageBox()
+ msg.setWindowTitle("Update Available")
+ msg.setText(v["description"])
+ x = msg.exec_()
+ except:
+ logger.error("Online version check failed.")
+
+
+ def loadOnlineContent(self):
+ url = user_files_url + 'directory.yaml'
+ r = requests.get(url, allow_redirects=False)
+ user_files = yaml.safe_load(r.content)
+ count = 0
+
+ # Download scenarios files
+ os.chdir(directories.scenarios)
+ if user_files["scenarios"]["files"]:
+ for filename in user_files["scenarios"]["files"]:
+ url = user_files_url + user_files["scenarios"]["dir"] + '/' + filename
+ r = requests.get(url, allow_redirects=False)
+ open(filename, 'wb').write(r.content)
+ count = count + 1
+
+
+ # Download blue forces files
+ os.chdir(directories.forces + '/blue')
+ if user_files["forces_blue"]["files"]:
+ for filename in user_files["forces_blue"]["files"]:
+ url = user_files_url + user_files["forces_blue"]["dir"] + '/' + filename
+ r = requests.get(url, allow_redirects=False)
+ open(filename, 'wb').write(r.content)
+ count = count + 1
+
+ # Download red forces files
+ os.chdir(directories.forces + '/red')
+ if user_files["forces_red"]["files"]:
+ for filename in user_files["forces_red"]["files"]:
+ url = user_files_url + user_files["forces_red"]["dir"] + '/' + filename
+ r = requests.get(url, allow_redirects=False)
+ open(filename, 'wb').write(r.content)
+ count = count + 1
+
+ # Download imports files
+ os.chdir(directories.imports)
+ if user_files["imports"]["files"]:
+ for filename in user_files["imports"]["files"]:
+ url = user_files_url + user_files["imports"]["dir"] + '/' + filename
+ r = requests.get(url, allow_redirects=False)
+ open(filename, 'wb').write(r.content)
+ count = count + 1
+
+ msg = QMessageBox()
+ msg.setWindowTitle("Downloaded Files")
+ msg.setText("We've downloaded " + str(count) + " new files!")
+ x = msg.exec_()
if __name__ == "__main__":
+ # os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1"
+
app = QApplication(sys.argv)
+ # QCoreApplication.setAttribute(QtCore.Qt.AA_DisableHighDpiScaling)
win = Window()
- win.show()
+ # win.show()
+ # win.loadOnlineContent()
+ win.checkVersion()
+
+
+ qtmodern.styles.dark(app)
+ mw = qtmodern.windows.ModernWindow(win)
+ mw.show()
sys.exit(app.exec())
-
-
diff --git a/Generator/MissionGenerator.spec b/Generator/MissionGenerator.spec
index 3c8f487..bb0df10 100644
--- a/Generator/MissionGenerator.spec
+++ b/Generator/MissionGenerator.spec
@@ -39,3 +39,4 @@ exe = EXE(pyz,
target_arch=None,
codesign_identity=None,
entitlements_file=None )
+
diff --git a/Generator/MissionGeneratorUI.py b/Generator/MissionGeneratorUI.py
index fafade2..7e34bbb 100644
--- a/Generator/MissionGeneratorUI.py
+++ b/Generator/MissionGeneratorUI.py
@@ -14,7 +14,14 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
- MainWindow.resize(1209, 900)
+ MainWindow.resize(1280, 720)
+ sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(MainWindow.sizePolicy().hasHeightForWidth())
+ MainWindow.setSizePolicy(sizePolicy)
+ MainWindow.setMinimumSize(QtCore.QSize(1280, 720))
+ MainWindow.setMaximumSize(QtCore.QSize(1280, 720))
font = QtGui.QFont()
font.setPointSize(10)
MainWindow.setFont(font)
@@ -23,353 +30,479 @@ class Ui_MainWindow(object):
MainWindow.setWindowIcon(icon)
MainWindow.setWindowOpacity(4.0)
MainWindow.setAutoFillBackground(False)
- MainWindow.setStyleSheet("background-color: white;")
+ MainWindow.setStyleSheet("/*-----QScrollBar-----*/\n"
+"QScrollBar:horizontal \n"
+"{\n"
+" background-color: transparent;\n"
+" height: 8px;\n"
+" margin: 0px;\n"
+" padding: 0px;\n"
+"\n"
+"}\n"
+"\n"
+"\n"
+"QScrollBar::handle:horizontal \n"
+"{\n"
+" border: none;\n"
+" min-width: 100px;\n"
+" background-color: #9b9b9b;\n"
+"\n"
+"}\n"
+"\n"
+"\n"
+"QScrollBar::add-line:horizontal, \n"
+"QScrollBar::sub-line:horizontal,\n"
+"QScrollBar::add-page:horizontal, \n"
+"QScrollBar::sub-page:horizontal \n"
+"{\n"
+" width: 0px;\n"
+" background-color: transparent;\n"
+"\n"
+"}\n"
+"\n"
+"\n"
+"QScrollBar:vertical \n"
+"{\n"
+" background-color: transparent;\n"
+" width: 8px;\n"
+" margin: 0;\n"
+"\n"
+"}\n"
+"\n"
+"\n"
+"QScrollBar::handle:vertical \n"
+"{\n"
+" border: none;\n"
+" min-height: 100px;\n"
+" background-color: #9b9b9b;\n"
+"\n"
+"}\n"
+"\n"
+"\n"
+"QScrollBar::add-line:vertical, \n"
+"QScrollBar::sub-line:vertical,\n"
+"QScrollBar::add-page:vertical, \n"
+"QScrollBar::sub-page:vertical \n"
+"{\n"
+" height: 0px;\n"
+" background-color: transparent;\n"
+"\n"
+"}")
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
+ self.logistics_crates_checkBox = QtWidgets.QCheckBox(self.centralwidget)
+ self.logistics_crates_checkBox.setGeometry(QtCore.QRect(990, 211, 251, 28))
+ font = QtGui.QFont()
+ font.setFamily("Arial")
+ font.setPointSize(10)
+ font.setBold(False)
+ self.logistics_crates_checkBox.setFont(font)
+ self.logistics_crates_checkBox.setChecked(True)
+ self.logistics_crates_checkBox.setObjectName("logistics_crates_checkBox")
+ self.zone_sams_checkBox = QtWidgets.QCheckBox(self.centralwidget)
+ self.zone_sams_checkBox.setGeometry(QtCore.QRect(990, 320, 241, 28))
+ font = QtGui.QFont()
+ font.setFamily("Arial")
+ font.setPointSize(10)
+ font.setBold(False)
+ self.zone_sams_checkBox.setFont(font)
+ self.zone_sams_checkBox.setObjectName("zone_sams_checkBox")
+ self.red_forces_label = QtWidgets.QLabel(self.centralwidget)
+ self.red_forces_label.setGeometry(QtCore.QRect(470, 80, 171, 27))
+ font = QtGui.QFont()
+ font.setFamily("Arial")
+ font.setPointSize(10)
+ font.setBold(False)
+ self.red_forces_label.setFont(font)
+ self.red_forces_label.setObjectName("red_forces_label")
self.scenario_comboBox = QtWidgets.QComboBox(self.centralwidget)
- self.scenario_comboBox.setGeometry(QtCore.QRect(270, 40, 361, 31))
+ self.scenario_comboBox.setGeometry(QtCore.QRect(30, 20, 371, 29))
+ font = QtGui.QFont()
+ font.setFamily("Arial")
+ font.setPointSize(8)
+ font.setBold(True)
+ self.scenario_comboBox.setFont(font)
self.scenario_comboBox.setToolTip("")
self.scenario_comboBox.setToolTipDuration(-1)
self.scenario_comboBox.setWhatsThis("")
+ self.scenario_comboBox.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContentsOnFirstShow)
+ self.scenario_comboBox.setFrame(True)
self.scenario_comboBox.setObjectName("scenario_comboBox")
- self.scenario_label = QtWidgets.QLabel(self.centralwidget)
- self.scenario_label.setGeometry(QtCore.QRect(60, 30, 181, 41))
- font = QtGui.QFont()
- font.setPointSize(12)
- self.scenario_label.setFont(font)
- self.scenario_label.setObjectName("scenario_label")
- self.generateButton = QtWidgets.QPushButton(self.centralwidget)
- self.generateButton.setGeometry(QtCore.QRect(1020, 790, 141, 41))
- self.generateButton.setStyleSheet("background-color: white;\n"
-"border-style: outset;\n"
-"border-width: 2px;\n"
-"border-radius: 15px;\n"
-"border-color: black;\n"
-"padding: 4px;")
- self.generateButton.setObjectName("generateButton")
self.description_textBrowser = QtWidgets.QTextBrowser(self.centralwidget)
- self.description_textBrowser.setGeometry(QtCore.QRect(670, 30, 501, 131))
+ self.description_textBrowser.setGeometry(QtCore.QRect(40, 410, 361, 251))
font = QtGui.QFont()
+ font.setFamily("Arial")
font.setPointSize(9)
self.description_textBrowser.setFont(font)
- self.description_textBrowser.setStyleSheet("border-radius: 5px; color: gray")
+ self.description_textBrowser.setStyleSheet("padding: 5px;")
+ self.description_textBrowser.setFrameShape(QtWidgets.QFrame.StyledPanel)
+ self.description_textBrowser.setFrameShadow(QtWidgets.QFrame.Plain)
+ self.description_textBrowser.setLineWidth(1)
self.description_textBrowser.setObjectName("description_textBrowser")
- self.blueforces_comboBox = QtWidgets.QComboBox(self.centralwidget)
- self.blueforces_comboBox.setGeometry(QtCore.QRect(790, 230, 291, 31))
- self.blueforces_comboBox.setObjectName("blueforces_comboBox")
- self.blue_forces_label = QtWidgets.QLabel(self.centralwidget)
- self.blue_forces_label.setGeometry(QtCore.QRect(690, 180, 241, 31))
+ self.defense_checkBox = QtWidgets.QCheckBox(self.centralwidget)
+ self.defense_checkBox.setEnabled(True)
+ self.defense_checkBox.setGeometry(QtCore.QRect(470, 120, 156, 28))
font = QtGui.QFont()
- font.setPointSize(12)
- self.blue_forces_label.setFont(font)
- self.blue_forces_label.setObjectName("blue_forces_label")
- self.red_forces_label = QtWidgets.QLabel(self.centralwidget)
- self.red_forces_label.setGeometry(QtCore.QRect(60, 180, 261, 31))
- font = QtGui.QFont()
- font.setPointSize(12)
- self.red_forces_label.setFont(font)
- self.red_forces_label.setObjectName("red_forces_label")
- self.redforces_comboBox = QtWidgets.QComboBox(self.centralwidget)
- self.redforces_comboBox.setGeometry(QtCore.QRect(170, 230, 291, 31))
- self.redforces_comboBox.setObjectName("redforces_comboBox")
- self.background_label = QtWidgets.QLabel(self.centralwidget)
- self.background_label.setGeometry(QtCore.QRect(-40, 490, 801, 371))
- self.background_label.setAutoFillBackground(False)
- self.background_label.setStyleSheet("")
- self.background_label.setText("")
- self.background_label.setPixmap(QtGui.QPixmap("assets/background.PNG"))
- self.background_label.setObjectName("background_label")
- self.scenario_hint_label = QtWidgets.QLabel(self.centralwidget)
- self.scenario_hint_label.setGeometry(QtCore.QRect(250, 80, 381, 16))
- self.scenario_hint_label.setAlignment(QtCore.Qt.AlignCenter)
- self.scenario_hint_label.setObjectName("scenario_hint_label")
- self.forces_hint_label = QtWidgets.QLabel(self.centralwidget)
- self.forces_hint_label.setGeometry(QtCore.QRect(130, 270, 381, 16))
- self.forces_hint_label.setAlignment(QtCore.Qt.AlignCenter)
- self.forces_hint_label.setObjectName("forces_hint_label")
- self.blueqty_spinBox = QtWidgets.QSpinBox(self.centralwidget)
- self.blueqty_spinBox.setGeometry(QtCore.QRect(690, 230, 71, 31))
- font = QtGui.QFont()
- font.setPointSize(12)
- self.blueqty_spinBox.setFont(font)
- self.blueqty_spinBox.setMinimum(0)
- self.blueqty_spinBox.setMaximum(8)
- self.blueqty_spinBox.setProperty("value", 3)
- self.blueqty_spinBox.setObjectName("blueqty_spinBox")
+ font.setFamily("Arial")
+ font.setPointSize(10)
+ font.setBold(False)
+ self.defense_checkBox.setFont(font)
+ self.defense_checkBox.setCheckable(True)
+ self.defense_checkBox.setObjectName("defense_checkBox")
self.redqty_spinBox = QtWidgets.QSpinBox(self.centralwidget)
- self.redqty_spinBox.setGeometry(QtCore.QRect(70, 230, 71, 31))
+ self.redqty_spinBox.setGeometry(QtCore.QRect(1070, 80, 51, 31))
font = QtGui.QFont()
font.setPointSize(12)
self.redqty_spinBox.setFont(font)
+ self.redqty_spinBox.setButtonSymbols(QtWidgets.QAbstractSpinBox.PlusMinus)
self.redqty_spinBox.setMinimum(0)
self.redqty_spinBox.setMaximum(8)
self.redqty_spinBox.setProperty("value", 2)
self.redqty_spinBox.setObjectName("redqty_spinBox")
- self.scenario_label_4 = QtWidgets.QLabel(self.centralwidget)
- self.scenario_label_4.setGeometry(QtCore.QRect(670, 260, 101, 31))
- font = QtGui.QFont()
- font.setPointSize(8)
- self.scenario_label_4.setFont(font)
- self.scenario_label_4.setAlignment(QtCore.Qt.AlignCenter)
- self.scenario_label_4.setObjectName("scenario_label_4")
- self.game_status_checkBox = QtWidgets.QCheckBox(self.centralwidget)
- self.game_status_checkBox.setGeometry(QtCore.QRect(810, 760, 191, 16))
+ self.redforces_comboBox = QtWidgets.QComboBox(self.centralwidget)
+ self.redforces_comboBox.setGeometry(QtCore.QRect(660, 80, 391, 33))
+ sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.redforces_comboBox.sizePolicy().hasHeightForWidth())
+ self.redforces_comboBox.setSizePolicy(sizePolicy)
font = QtGui.QFont()
+ font.setFamily("Arial")
font.setPointSize(9)
- self.game_status_checkBox.setFont(font)
- self.game_status_checkBox.setChecked(True)
- self.game_status_checkBox.setTristate(False)
- self.game_status_checkBox.setObjectName("game_status_checkBox")
- self.voiceovers_checkBox = QtWidgets.QCheckBox(self.centralwidget)
- self.voiceovers_checkBox.setGeometry(QtCore.QRect(810, 790, 191, 16))
- font = QtGui.QFont()
- font.setPointSize(9)
- self.voiceovers_checkBox.setFont(font)
- self.voiceovers_checkBox.setChecked(True)
- self.voiceovers_checkBox.setObjectName("voiceovers_checkBox")
- self.logistics_crates_checkBox = QtWidgets.QCheckBox(self.centralwidget)
- self.logistics_crates_checkBox.setGeometry(QtCore.QRect(920, 320, 251, 31))
- font = QtGui.QFont()
- font.setPointSize(11)
- self.logistics_crates_checkBox.setFont(font)
- self.logistics_crates_checkBox.setChecked(True)
- self.logistics_crates_checkBox.setObjectName("logistics_crates_checkBox")
- self.awacs_checkBox = QtWidgets.QCheckBox(self.centralwidget)
- self.awacs_checkBox.setGeometry(QtCore.QRect(920, 350, 251, 31))
- font = QtGui.QFont()
- font.setPointSize(11)
- self.awacs_checkBox.setFont(font)
- self.awacs_checkBox.setStatusTip("")
- self.awacs_checkBox.setChecked(True)
- self.awacs_checkBox.setObjectName("awacs_checkBox")
- self.tankers_checkBox = QtWidgets.QCheckBox(self.centralwidget)
- self.tankers_checkBox.setGeometry(QtCore.QRect(920, 380, 251, 31))
- font = QtGui.QFont()
- font.setPointSize(11)
- self.tankers_checkBox.setFont(font)
- self.tankers_checkBox.setChecked(True)
- self.tankers_checkBox.setObjectName("tankers_checkBox")
- self.apcs_spawn_checkBox = QtWidgets.QCheckBox(self.centralwidget)
- self.apcs_spawn_checkBox.setGeometry(QtCore.QRect(450, 420, 251, 31))
+ font.setBold(False)
+ self.redforces_comboBox.setFont(font)
+ self.redforces_comboBox.setObjectName("redforces_comboBox")
+ self.scenario_label_8 = QtWidgets.QLabel(self.centralwidget)
+ self.scenario_label_8.setGeometry(QtCore.QRect(570, 220, 271, 24))
font = QtGui.QFont()
+ font.setFamily("Arial")
font.setPointSize(10)
- self.apcs_spawn_checkBox.setFont(font)
- self.apcs_spawn_checkBox.setChecked(True)
- self.apcs_spawn_checkBox.setObjectName("apcs_spawn_checkBox")
- self.inf_spawn_spinBox = QtWidgets.QSpinBox(self.centralwidget)
- self.inf_spawn_spinBox.setGeometry(QtCore.QRect(670, 340, 51, 31))
+ font.setBold(False)
+ self.scenario_label_8.setFont(font)
+ self.scenario_label_8.setObjectName("scenario_label_8")
+ self.slot_template_comboBox = QtWidgets.QComboBox(self.centralwidget)
+ self.slot_template_comboBox.setGeometry(QtCore.QRect(960, 384, 271, 33))
font = QtGui.QFont()
- font.setPointSize(12)
- self.inf_spawn_spinBox.setFont(font)
- self.inf_spawn_spinBox.setMinimum(0)
- self.inf_spawn_spinBox.setMaximum(20)
- self.inf_spawn_spinBox.setProperty("value", 2)
- self.inf_spawn_spinBox.setObjectName("inf_spawn_spinBox")
+ font.setFamily("Arial")
+ font.setPointSize(10)
+ font.setBold(False)
+ self.slot_template_comboBox.setFont(font)
+ self.slot_template_comboBox.setObjectName("slot_template_comboBox")
self.scenario_label_5 = QtWidgets.QLabel(self.centralwidget)
- self.scenario_label_5.setGeometry(QtCore.QRect(50, 260, 101, 31))
+ self.scenario_label_5.setGeometry(QtCore.QRect(1130, 40, 131, 18))
font = QtGui.QFont()
font.setPointSize(8)
self.scenario_label_5.setFont(font)
self.scenario_label_5.setAlignment(QtCore.Qt.AlignCenter)
self.scenario_label_5.setObjectName("scenario_label_5")
- self.forces_hint_label_2 = QtWidgets.QLabel(self.centralwidget)
- self.forces_hint_label_2.setGeometry(QtCore.QRect(790, 270, 311, 20))
- self.forces_hint_label_2.setAlignment(QtCore.Qt.AlignCenter)
- self.forces_hint_label_2.setObjectName("forces_hint_label_2")
- self.label = QtWidgets.QLabel(self.centralwidget)
- self.label.setGeometry(QtCore.QRect(450, 340, 211, 21))
+ self.blue_forces_label = QtWidgets.QLabel(self.centralwidget)
+ self.blue_forces_label.setGeometry(QtCore.QRect(470, 30, 161, 27))
font = QtGui.QFont()
+ font.setFamily("Arial")
font.setPointSize(10)
- self.label.setFont(font)
- self.label.setObjectName("label")
- self.slot_template_comboBox = QtWidgets.QComboBox(self.centralwidget)
- self.slot_template_comboBox.setGeometry(QtCore.QRect(870, 640, 291, 31))
- self.slot_template_comboBox.setObjectName("slot_template_comboBox")
- self.label_2 = QtWidgets.QLabel(self.centralwidget)
- self.label_2.setGeometry(QtCore.QRect(750, 640, 111, 31))
+ font.setBold(False)
+ self.blue_forces_label.setFont(font)
+ self.blue_forces_label.setObjectName("blue_forces_label")
+ self.blueqty_spinBox = QtWidgets.QSpinBox(self.centralwidget)
+ self.blueqty_spinBox.setGeometry(QtCore.QRect(1070, 30, 51, 31))
font = QtGui.QFont()
- font.setPointSize(11)
- self.label_2.setFont(font)
- self.label_2.setObjectName("label_2")
- self.force_offroad_checkBox = QtWidgets.QCheckBox(self.centralwidget)
- self.force_offroad_checkBox.setGeometry(QtCore.QRect(810, 820, 191, 16))
+ font.setPointSize(12)
+ self.blueqty_spinBox.setFont(font)
+ self.blueqty_spinBox.setButtonSymbols(QtWidgets.QAbstractSpinBox.PlusMinus)
+ self.blueqty_spinBox.setMinimum(0)
+ self.blueqty_spinBox.setMaximum(8)
+ self.blueqty_spinBox.setProperty("value", 3)
+ self.blueqty_spinBox.setObjectName("blueqty_spinBox")
+ self.blueforces_comboBox = QtWidgets.QComboBox(self.centralwidget)
+ self.blueforces_comboBox.setGeometry(QtCore.QRect(660, 30, 391, 33))
font = QtGui.QFont()
+ font.setFamily("Arial")
font.setPointSize(9)
- self.force_offroad_checkBox.setFont(font)
- self.force_offroad_checkBox.setChecked(False)
- self.force_offroad_checkBox.setTristate(False)
- self.force_offroad_checkBox.setObjectName("force_offroad_checkBox")
- self.defense_checkBox = QtWidgets.QCheckBox(self.centralwidget)
- self.defense_checkBox.setGeometry(QtCore.QRect(60, 90, 181, 31))
+ font.setBold(False)
+ self.blueforces_comboBox.setFont(font)
+ self.blueforces_comboBox.setObjectName("blueforces_comboBox")
+ self.scenario_label_4 = QtWidgets.QLabel(self.centralwidget)
+ self.scenario_label_4.setGeometry(QtCore.QRect(1130, 90, 131, 18))
font = QtGui.QFont()
- font.setPointSize(11)
- self.defense_checkBox.setFont(font)
- self.defense_checkBox.setObjectName("defense_checkBox")
+ font.setPointSize(8)
+ self.scenario_label_4.setFont(font)
+ self.scenario_label_4.setAlignment(QtCore.Qt.AlignCenter)
+ self.scenario_label_4.setObjectName("scenario_label_4")
+ self.version_label = QtWidgets.QLabel(self.centralwidget)
+ self.version_label.setGeometry(QtCore.QRect(1140, 650, 111, 20))
+ self.version_label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
+ self.version_label.setObjectName("version_label")
+ self.scenario_label_10 = QtWidgets.QLabel(self.centralwidget)
+ self.scenario_label_10.setGeometry(QtCore.QRect(570, 260, 271, 24))
+ font = QtGui.QFont()
+ font.setFamily("Arial")
+ font.setPointSize(10)
+ font.setBold(False)
+ self.scenario_label_10.setFont(font)
+ self.scenario_label_10.setObjectName("scenario_label_10")
+ self.e_transport_helos_spinBox = QtWidgets.QSpinBox(self.centralwidget)
+ self.e_transport_helos_spinBox.setGeometry(QtCore.QRect(510, 260, 51, 31))
+ sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.e_transport_helos_spinBox.sizePolicy().hasHeightForWidth())
+ self.e_transport_helos_spinBox.setSizePolicy(sizePolicy)
+ font = QtGui.QFont()
+ font.setPointSize(12)
+ self.e_transport_helos_spinBox.setFont(font)
+ self.e_transport_helos_spinBox.setButtonSymbols(QtWidgets.QAbstractSpinBox.PlusMinus)
+ self.e_transport_helos_spinBox.setMinimum(0)
+ self.e_transport_helos_spinBox.setMaximum(8)
+ self.e_transport_helos_spinBox.setProperty("value", 1)
+ self.e_transport_helos_spinBox.setObjectName("e_transport_helos_spinBox")
+ self.e_attack_planes_spinBox = QtWidgets.QSpinBox(self.centralwidget)
+ self.e_attack_planes_spinBox.setGeometry(QtCore.QRect(510, 220, 51, 31))
+ sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.e_attack_planes_spinBox.sizePolicy().hasHeightForWidth())
+ self.e_attack_planes_spinBox.setSizePolicy(sizePolicy)
+ font = QtGui.QFont()
+ font.setPointSize(12)
+ self.e_attack_planes_spinBox.setFont(font)
+ self.e_attack_planes_spinBox.setButtonSymbols(QtWidgets.QAbstractSpinBox.PlusMinus)
+ self.e_attack_planes_spinBox.setMinimum(0)
+ self.e_attack_planes_spinBox.setMaximum(8)
+ self.e_attack_planes_spinBox.setProperty("value", 1)
+ self.e_attack_planes_spinBox.setObjectName("e_attack_planes_spinBox")
self.e_attack_helos_spinBox = QtWidgets.QSpinBox(self.centralwidget)
- self.e_attack_helos_spinBox.setGeometry(QtCore.QRect(70, 330, 51, 31))
+ self.e_attack_helos_spinBox.setGeometry(QtCore.QRect(510, 180, 51, 31))
+ sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.e_attack_helos_spinBox.sizePolicy().hasHeightForWidth())
+ self.e_attack_helos_spinBox.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setPointSize(12)
self.e_attack_helos_spinBox.setFont(font)
+ self.e_attack_helos_spinBox.setReadOnly(False)
+ self.e_attack_helos_spinBox.setButtonSymbols(QtWidgets.QAbstractSpinBox.PlusMinus)
+ self.e_attack_helos_spinBox.setKeyboardTracking(True)
self.e_attack_helos_spinBox.setMinimum(0)
self.e_attack_helos_spinBox.setMaximum(8)
self.e_attack_helos_spinBox.setProperty("value", 2)
self.e_attack_helos_spinBox.setObjectName("e_attack_helos_spinBox")
self.scenario_label_7 = QtWidgets.QLabel(self.centralwidget)
- self.scenario_label_7.setGeometry(QtCore.QRect(140, 330, 211, 31))
+ self.scenario_label_7.setGeometry(QtCore.QRect(570, 180, 271, 24))
font = QtGui.QFont()
- font.setPointSize(11)
+ font.setFamily("Arial")
+ font.setPointSize(10)
+ font.setBold(False)
self.scenario_label_7.setFont(font)
self.scenario_label_7.setObjectName("scenario_label_7")
- self.scenario_label_8 = QtWidgets.QLabel(self.centralwidget)
- self.scenario_label_8.setGeometry(QtCore.QRect(140, 370, 201, 31))
+ self.label_2 = QtWidgets.QLabel(self.centralwidget)
+ self.label_2.setGeometry(QtCore.QRect(840, 390, 111, 24))
font = QtGui.QFont()
- font.setPointSize(11)
- self.scenario_label_8.setFont(font)
- self.scenario_label_8.setObjectName("scenario_label_8")
- self.e_attack_planes_spinBox = QtWidgets.QSpinBox(self.centralwidget)
- self.e_attack_planes_spinBox.setGeometry(QtCore.QRect(70, 370, 51, 31))
- font = QtGui.QFont()
- font.setPointSize(12)
- self.e_attack_planes_spinBox.setFont(font)
- self.e_attack_planes_spinBox.setMinimum(0)
- self.e_attack_planes_spinBox.setMaximum(8)
- self.e_attack_planes_spinBox.setProperty("value", 1)
- self.e_attack_planes_spinBox.setObjectName("e_attack_planes_spinBox")
- self.zone_sams_checkBox = QtWidgets.QCheckBox(self.centralwidget)
- self.zone_sams_checkBox.setGeometry(QtCore.QRect(920, 410, 201, 31))
- font = QtGui.QFont()
- font.setPointSize(11)
- self.zone_sams_checkBox.setFont(font)
- self.zone_sams_checkBox.setObjectName("zone_sams_checkBox")
+ font.setFamily("Arial")
+ font.setPointSize(10)
+ font.setBold(False)
+ self.label_2.setFont(font)
+ self.label_2.setObjectName("label_2")
self.scenario_label_9 = QtWidgets.QLabel(self.centralwidget)
- self.scenario_label_9.setGeometry(QtCore.QRect(810, 450, 171, 31))
+ self.scenario_label_9.setGeometry(QtCore.QRect(490, 450, 251, 23))
font = QtGui.QFont()
+ font.setFamily("Arial")
font.setPointSize(10)
self.scenario_label_9.setFont(font)
self.scenario_label_9.setObjectName("scenario_label_9")
- self.inf_spawn_voiceovers_checkBox = QtWidgets.QCheckBox(self.centralwidget)
- self.inf_spawn_voiceovers_checkBox.setGeometry(QtCore.QRect(810, 720, 251, 31))
+ self.awacs_checkBox = QtWidgets.QCheckBox(self.centralwidget)
+ self.awacs_checkBox.setGeometry(QtCore.QRect(990, 246, 241, 28))
font = QtGui.QFont()
+ font.setFamily("Arial")
+ font.setPointSize(10)
+ font.setBold(False)
+ self.awacs_checkBox.setFont(font)
+ self.awacs_checkBox.setStatusTip("")
+ self.awacs_checkBox.setChecked(True)
+ self.awacs_checkBox.setObjectName("awacs_checkBox")
+ self.tankers_checkBox = QtWidgets.QCheckBox(self.centralwidget)
+ self.tankers_checkBox.setGeometry(QtCore.QRect(990, 282, 241, 28))
+ font = QtGui.QFont()
+ font.setFamily("Arial")
+ font.setPointSize(10)
+ font.setBold(False)
+ self.tankers_checkBox.setFont(font)
+ self.tankers_checkBox.setChecked(True)
+ self.tankers_checkBox.setObjectName("tankers_checkBox")
+ self.inf_spawn_voiceovers_checkBox = QtWidgets.QCheckBox(self.centralwidget)
+ self.inf_spawn_voiceovers_checkBox.setGeometry(QtCore.QRect(960, 455, 271, 24))
+ font = QtGui.QFont()
+ font.setFamily("Arial")
font.setPointSize(9)
self.inf_spawn_voiceovers_checkBox.setFont(font)
self.inf_spawn_voiceovers_checkBox.setChecked(True)
self.inf_spawn_voiceovers_checkBox.setObjectName("inf_spawn_voiceovers_checkBox")
- self.farp_never = QtWidgets.QRadioButton(self.centralwidget)
- self.farp_never.setGeometry(QtCore.QRect(950, 500, 95, 20))
+ self.voiceovers_checkBox = QtWidgets.QCheckBox(self.centralwidget)
+ self.voiceovers_checkBox.setGeometry(QtCore.QRect(960, 517, 171, 24))
font = QtGui.QFont()
+ font.setFamily("Arial")
+ font.setPointSize(9)
+ self.voiceovers_checkBox.setFont(font)
+ self.voiceovers_checkBox.setChecked(True)
+ self.voiceovers_checkBox.setObjectName("voiceovers_checkBox")
+ self.smoke_pickup_zone_checkBox = QtWidgets.QCheckBox(self.centralwidget)
+ self.smoke_pickup_zone_checkBox.setGeometry(QtCore.QRect(960, 424, 271, 24))
+ font = QtGui.QFont()
+ font.setFamily("Arial")
+ font.setPointSize(9)
+ self.smoke_pickup_zone_checkBox.setFont(font)
+ self.smoke_pickup_zone_checkBox.setChecked(False)
+ self.smoke_pickup_zone_checkBox.setObjectName("smoke_pickup_zone_checkBox")
+ self.game_status_checkBox = QtWidgets.QCheckBox(self.centralwidget)
+ self.game_status_checkBox.setGeometry(QtCore.QRect(960, 486, 271, 24))
+ font = QtGui.QFont()
+ font.setFamily("Arial")
+ font.setPointSize(9)
+ self.game_status_checkBox.setFont(font)
+ self.game_status_checkBox.setChecked(True)
+ self.game_status_checkBox.setTristate(False)
+ self.game_status_checkBox.setObjectName("game_status_checkBox")
+ self.label = QtWidgets.QLabel(self.centralwidget)
+ self.label.setGeometry(QtCore.QRect(570, 380, 261, 23))
+ font = QtGui.QFont()
+ font.setFamily("Arial")
+ font.setPointSize(10)
+ font.setBold(False)
+ self.label.setFont(font)
+ self.label.setObjectName("label")
+ self.inf_spawn_spinBox = QtWidgets.QSpinBox(self.centralwidget)
+ self.inf_spawn_spinBox.setGeometry(QtCore.QRect(510, 380, 47, 31))
+ font = QtGui.QFont()
+ font.setPointSize(12)
+ self.inf_spawn_spinBox.setFont(font)
+ self.inf_spawn_spinBox.setButtonSymbols(QtWidgets.QAbstractSpinBox.PlusMinus)
+ self.inf_spawn_spinBox.setMinimum(0)
+ self.inf_spawn_spinBox.setMaximum(20)
+ self.inf_spawn_spinBox.setProperty("value", 2)
+ self.inf_spawn_spinBox.setObjectName("inf_spawn_spinBox")
+ self.troop_drop_spinBox = QtWidgets.QSpinBox(self.centralwidget)
+ self.troop_drop_spinBox.setGeometry(QtCore.QRect(510, 330, 47, 31))
+ font = QtGui.QFont()
+ font.setPointSize(12)
+ self.troop_drop_spinBox.setFont(font)
+ self.troop_drop_spinBox.setButtonSymbols(QtWidgets.QAbstractSpinBox.PlusMinus)
+ self.troop_drop_spinBox.setMinimum(0)
+ self.troop_drop_spinBox.setMaximum(10)
+ self.troop_drop_spinBox.setProperty("value", 4)
+ self.troop_drop_spinBox.setObjectName("troop_drop_spinBox")
+ self.force_offroad_checkBox = QtWidgets.QCheckBox(self.centralwidget)
+ self.force_offroad_checkBox.setGeometry(QtCore.QRect(960, 548, 161, 24))
+ font = QtGui.QFont()
+ font.setFamily("Arial")
+ font.setPointSize(9)
+ self.force_offroad_checkBox.setFont(font)
+ self.force_offroad_checkBox.setChecked(False)
+ self.force_offroad_checkBox.setTristate(False)
+ self.force_offroad_checkBox.setObjectName("force_offroad_checkBox")
+ self.label_3 = QtWidgets.QLabel(self.centralwidget)
+ self.label_3.setGeometry(QtCore.QRect(570, 330, 281, 23))
+ font = QtGui.QFont()
+ font.setFamily("Arial")
+ font.setPointSize(10)
+ font.setBold(False)
+ self.label_3.setFont(font)
+ self.label_3.setObjectName("label_3")
+ self.apcs_spawn_checkBox = QtWidgets.QCheckBox(self.centralwidget)
+ self.apcs_spawn_checkBox.setGeometry(QtCore.QRect(990, 180, 251, 27))
+ font = QtGui.QFont()
+ font.setFamily("Arial")
+ font.setPointSize(10)
+ font.setBold(False)
+ self.apcs_spawn_checkBox.setFont(font)
+ self.apcs_spawn_checkBox.setChecked(True)
+ self.apcs_spawn_checkBox.setObjectName("apcs_spawn_checkBox")
+ self.generateButton = QtWidgets.QPushButton(self.centralwidget)
+ self.generateButton.setGeometry(QtCore.QRect(710, 600, 231, 51))
+ font = QtGui.QFont()
+ font.setFamily("Arial")
+ font.setPointSize(8)
+ font.setBold(True)
+ self.generateButton.setFont(font)
+ self.generateButton.setStyleSheet("background-color: gray;\n"
+"color: rgb(255, 255, 255);\n"
+"border-style: outset;\n"
+"border-width: 1px;\n"
+"border-radius: 5px;\n"
+"border-color: black;\n"
+"padding: 4px;")
+ self.generateButton.setObjectName("generateButton")
+ self.farp_always = QtWidgets.QRadioButton(self.centralwidget)
+ self.farp_always.setGeometry(QtCore.QRect(510, 480, 261, 24))
+ font = QtGui.QFont()
+ font.setFamily("Arial")
+ font.setPointSize(9)
+ self.farp_always.setFont(font)
+ self.farp_always.setObjectName("farp_always")
+ self.farp_buttonGroup = QtWidgets.QButtonGroup(MainWindow)
+ self.farp_buttonGroup.setObjectName("farp_buttonGroup")
+ self.farp_buttonGroup.addButton(self.farp_always)
+ self.farp_never = QtWidgets.QRadioButton(self.centralwidget)
+ self.farp_never.setGeometry(QtCore.QRect(510, 540, 271, 24))
+ font = QtGui.QFont()
+ font.setFamily("Arial")
font.setPointSize(9)
self.farp_never.setFont(font)
self.farp_never.setObjectName("farp_never")
- self.farp_buttonGroup = QtWidgets.QButtonGroup(MainWindow)
- self.farp_buttonGroup.setObjectName("farp_buttonGroup")
self.farp_buttonGroup.addButton(self.farp_never)
self.farp_gunits = QtWidgets.QRadioButton(self.centralwidget)
- self.farp_gunits.setGeometry(QtCore.QRect(950, 530, 221, 21))
+ self.farp_gunits.setGeometry(QtCore.QRect(510, 509, 261, 24))
font = QtGui.QFont()
+ font.setFamily("Arial")
font.setPointSize(9)
self.farp_gunits.setFont(font)
self.farp_gunits.setChecked(True)
self.farp_gunits.setObjectName("farp_gunits")
self.farp_buttonGroup.addButton(self.farp_gunits)
- self.farp_always = QtWidgets.QRadioButton(self.centralwidget)
- self.farp_always.setGeometry(QtCore.QRect(950, 560, 221, 21))
- font = QtGui.QFont()
- font.setPointSize(9)
- self.farp_always.setFont(font)
- self.farp_always.setObjectName("farp_always")
- self.farp_buttonGroup.addButton(self.farp_always)
- self.version_label = QtWidgets.QLabel(self.centralwidget)
- self.version_label.setGeometry(QtCore.QRect(920, 840, 241, 21))
- self.version_label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
- self.version_label.setObjectName("version_label")
- self.scenario_label_10 = QtWidgets.QLabel(self.centralwidget)
- self.scenario_label_10.setGeometry(QtCore.QRect(140, 410, 241, 31))
- font = QtGui.QFont()
- font.setPointSize(11)
- self.scenario_label_10.setFont(font)
- self.scenario_label_10.setObjectName("scenario_label_10")
- self.e_transport_helos_spinBox = QtWidgets.QSpinBox(self.centralwidget)
- self.e_transport_helos_spinBox.setGeometry(QtCore.QRect(70, 410, 51, 31))
- font = QtGui.QFont()
- font.setPointSize(12)
- self.e_transport_helos_spinBox.setFont(font)
- self.e_transport_helos_spinBox.setMinimum(0)
- self.e_transport_helos_spinBox.setMaximum(8)
- self.e_transport_helos_spinBox.setProperty("value", 1)
- self.e_transport_helos_spinBox.setObjectName("e_transport_helos_spinBox")
- self.label_3 = QtWidgets.QLabel(self.centralwidget)
- self.label_3.setGeometry(QtCore.QRect(450, 380, 191, 31))
- font = QtGui.QFont()
- font.setPointSize(10)
- self.label_3.setFont(font)
- self.label_3.setObjectName("label_3")
- self.troop_drop_spinBox = QtWidgets.QSpinBox(self.centralwidget)
- self.troop_drop_spinBox.setGeometry(QtCore.QRect(670, 380, 51, 31))
- font = QtGui.QFont()
- font.setPointSize(12)
- self.troop_drop_spinBox.setFont(font)
- self.troop_drop_spinBox.setMinimum(0)
- self.troop_drop_spinBox.setMaximum(10)
- self.troop_drop_spinBox.setProperty("value", 4)
- self.troop_drop_spinBox.setObjectName("troop_drop_spinBox")
- self.smoke_pickup_zone_checkBox = QtWidgets.QCheckBox(self.centralwidget)
- self.smoke_pickup_zone_checkBox.setGeometry(QtCore.QRect(810, 690, 251, 31))
- font = QtGui.QFont()
- font.setPointSize(9)
- self.smoke_pickup_zone_checkBox.setFont(font)
- self.smoke_pickup_zone_checkBox.setChecked(True)
- self.smoke_pickup_zone_checkBox.setObjectName("smoke_pickup_zone_checkBox")
- self.background_label.raise_()
- self.scenario_comboBox.raise_()
- self.scenario_label.raise_()
- self.generateButton.raise_()
- self.description_textBrowser.raise_()
- self.blueforces_comboBox.raise_()
- self.blue_forces_label.raise_()
- self.red_forces_label.raise_()
- self.redforces_comboBox.raise_()
- self.scenario_hint_label.raise_()
- self.forces_hint_label.raise_()
- self.blueqty_spinBox.raise_()
- self.redqty_spinBox.raise_()
- self.scenario_label_4.raise_()
- self.game_status_checkBox.raise_()
- self.voiceovers_checkBox.raise_()
- self.logistics_crates_checkBox.raise_()
- self.awacs_checkBox.raise_()
- self.tankers_checkBox.raise_()
- self.apcs_spawn_checkBox.raise_()
- self.inf_spawn_spinBox.raise_()
- self.scenario_label_5.raise_()
- self.forces_hint_label_2.raise_()
- self.label.raise_()
- self.slot_template_comboBox.raise_()
- self.label_2.raise_()
- self.force_offroad_checkBox.raise_()
- self.defense_checkBox.raise_()
- self.e_attack_helos_spinBox.raise_()
- self.scenario_label_7.raise_()
- self.scenario_label_8.raise_()
- self.e_attack_planes_spinBox.raise_()
- self.zone_sams_checkBox.raise_()
- self.scenario_label_9.raise_()
- self.inf_spawn_voiceovers_checkBox.raise_()
- self.farp_never.raise_()
- self.farp_gunits.raise_()
- self.farp_always.raise_()
- self.version_label.raise_()
- self.scenario_label_10.raise_()
- self.e_transport_helos_spinBox.raise_()
- self.label_3.raise_()
- self.troop_drop_spinBox.raise_()
- self.smoke_pickup_zone_checkBox.raise_()
+ self.missionImage = QtWidgets.QLabel(self.centralwidget)
+ self.missionImage.setEnabled(True)
+ self.missionImage.setGeometry(QtCore.QRect(60, 80, 300, 300))
+ sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.missionImage.sizePolicy().hasHeightForWidth())
+ self.missionImage.setSizePolicy(sizePolicy)
+ self.missionImage.setMinimumSize(QtCore.QSize(300, 300))
+ self.missionImage.setMaximumSize(QtCore.QSize(16777215, 16777215))
+ self.missionImage.setStyleSheet("")
+ self.missionImage.setText("")
+ self.missionImage.setPixmap(QtGui.QPixmap("assets/briefing1.png"))
+ self.missionImage.setScaledContents(True)
+ self.missionImage.setWordWrap(False)
+ self.missionImage.setObjectName("missionImage")
+ self.nextScenario_pushButton = QtWidgets.QPushButton(self.centralwidget)
+ self.nextScenario_pushButton.setGeometry(QtCore.QRect(370, 210, 31, 51))
+ self.nextScenario_pushButton.setObjectName("nextScenario_pushButton")
+ self.prevScenario_pushButton = QtWidgets.QPushButton(self.centralwidget)
+ self.prevScenario_pushButton.setGeometry(QtCore.QRect(20, 210, 31, 51))
+ self.prevScenario_pushButton.setObjectName("prevScenario_pushButton")
+ self.background_label = QtWidgets.QLabel(self.centralwidget)
+ self.background_label.setGeometry(QtCore.QRect(1020, 600, 241, 51))
+ self.background_label.setText("")
+ self.background_label.setPixmap(QtGui.QPixmap("assets/rotorops-dkgray.png"))
+ self.background_label.setScaledContents(True)
+ self.background_label.setObjectName("background_label")
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
- self.menubar.setGeometry(QtCore.QRect(0, 0, 1209, 26))
+ self.menubar.setGeometry(QtCore.QRect(0, 0, 1280, 26))
self.menubar.setObjectName("menubar")
+ self.menuMap = QtWidgets.QMenu(self.menubar)
+ self.menuMap.setObjectName("menuMap")
+ self.menuGametype_Filter = QtWidgets.QMenu(self.menubar)
+ self.menuGametype_Filter.setObjectName("menuGametype_Filter")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
+ font = QtGui.QFont()
+ font.setFamily("Arial")
+ font.setPointSize(9)
+ font.setBold(False)
+ self.statusbar.setFont(font)
self.statusbar.setAcceptDrops(False)
+ self.statusbar.setStyleSheet("color: rgb(255, 255, 255);")
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.action_generateMission = QtWidgets.QAction(MainWindow)
@@ -382,84 +515,131 @@ class Ui_MainWindow(object):
self.action_redforcesSelected.setObjectName("action_redforcesSelected")
self.action_defensiveModeChanged = QtWidgets.QAction(MainWindow)
self.action_defensiveModeChanged.setObjectName("action_defensiveModeChanged")
+ self.action_nextScenario = QtWidgets.QAction(MainWindow)
+ self.action_nextScenario.setObjectName("action_nextScenario")
+ self.action_prevScenario = QtWidgets.QAction(MainWindow)
+ self.action_prevScenario.setObjectName("action_prevScenario")
+ self.actionCaucasus = QtWidgets.QAction(MainWindow)
+ self.actionCaucasus.setObjectName("actionCaucasus")
+ self.actionPersian_Gulf = QtWidgets.QAction(MainWindow)
+ self.actionPersian_Gulf.setObjectName("actionPersian_Gulf")
+ self.actionMarianas = QtWidgets.QAction(MainWindow)
+ self.actionMarianas.setObjectName("actionMarianas")
+ self.actionNevada = QtWidgets.QAction(MainWindow)
+ self.actionNevada.setObjectName("actionNevada")
+ self.actionSyria = QtWidgets.QAction(MainWindow)
+ self.actionSyria.setObjectName("actionSyria")
+ self.actionAll = QtWidgets.QAction(MainWindow)
+ self.actionAll.setCheckable(True)
+ self.actionAll.setChecked(True)
+ self.actionAll.setObjectName("actionAll")
+ self.actionMultiplayer = QtWidgets.QAction(MainWindow)
+ self.actionMultiplayer.setCheckable(False)
+ self.actionMultiplayer.setObjectName("actionMultiplayer")
+ self.actionAll_2 = QtWidgets.QAction(MainWindow)
+ self.actionAll_2.setCheckable(True)
+ self.actionAll_2.setChecked(True)
+ self.actionAll_2.setObjectName("actionAll_2")
+ self.menuMap.addAction(self.actionAll_2)
+ self.menuMap.addAction(self.actionCaucasus)
+ self.menuMap.addAction(self.actionPersian_Gulf)
+ self.menuMap.addAction(self.actionMarianas)
+ self.menuMap.addAction(self.actionNevada)
+ self.menuMap.addAction(self.actionSyria)
+ self.menuGametype_Filter.addAction(self.actionAll)
+ self.menuGametype_Filter.addAction(self.actionMultiplayer)
+ self.menubar.addAction(self.menuMap.menuAction())
+ self.menubar.addAction(self.menuGametype_Filter.menuAction())
self.retranslateUi(MainWindow)
self.generateButton.clicked.connect(self.action_generateMission.trigger)
self.scenario_comboBox.currentIndexChanged['int'].connect(self.action_scenarioSelected.trigger)
self.defense_checkBox.stateChanged['int'].connect(self.action_defensiveModeChanged.trigger)
+ self.nextScenario_pushButton.clicked.connect(self.action_nextScenario.trigger)
+ self.prevScenario_pushButton.clicked.connect(self.action_prevScenario.trigger)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "RotorOps Mission Generator"))
+ self.logistics_crates_checkBox.setStatusTip(_translate("MainWindow", "Enable CTLD logistics crates for building ground units and air defenses. Pickup logistics containers to create new logistics sites."))
+ self.logistics_crates_checkBox.setText(_translate("MainWindow", "Logistics"))
+ self.zone_sams_checkBox.setStatusTip(_translate("MainWindow", "Inactive conflict zones will be protected by SAMs. When a zone is cleared, SAMs at next active zone will be destroyed."))
+ self.zone_sams_checkBox.setText(_translate("MainWindow", "Inactive Zone SAMs"))
+ self.red_forces_label.setText(_translate("MainWindow", "Red Forces:"))
self.scenario_comboBox.setStatusTip(_translate("MainWindow", "Tip: You can create your own templates that include mission options like kneeboards, briefings, weather, static units, triggers, scripts, etc."))
- self.scenario_label.setText(_translate("MainWindow", "Scenario Template:"))
- self.generateButton.setText(_translate("MainWindow", "Generate Mission"))
self.description_textBrowser.setHtml(_translate("MainWindow", "\n"
"
\n"
+"\n"
"Provide close air support for our convoys as we take back Las Vegas from the enemy!
"))
- self.blueforces_comboBox.setStatusTip(_translate("MainWindow", "Tip: You can create your own custom ground forces groups to be automatically generated."))
- self.blue_forces_label.setText(_translate("MainWindow", "Friendly Forces:"))
- self.red_forces_label.setText(_translate("MainWindow", "Enemy Forces:"))
- self.redforces_comboBox.setStatusTip(_translate("MainWindow", "Tip: You can create your own custom ground forces groups to be automatically generated."))
- self.scenario_hint_label.setText(_translate("MainWindow", "Scenario templates are .miz files in \'Generator/Scenarios\'"))
- self.forces_hint_label.setText(_translate("MainWindow", "Forces templates are .miz files in \'Generator/Forces\'"))
- self.blueqty_spinBox.setStatusTip(_translate("MainWindow", "How many groups should we generate?"))
+ self.defense_checkBox.setText(_translate("MainWindow", "Blue on Defense"))
self.redqty_spinBox.setStatusTip(_translate("MainWindow", "How many groups should we generate?"))
- self.scenario_label_4.setText(_translate("MainWindow", "Groups Per Zone"))
- self.game_status_checkBox.setStatusTip(_translate("MainWindow", "Enable an onscreen zone status display. This helps keep focus on the active conflict zone."))
- self.game_status_checkBox.setText(_translate("MainWindow", "Game Status Display"))
- self.voiceovers_checkBox.setStatusTip(_translate("MainWindow", "Voiceovers from the ground commander. Helps keep focus on the active zone."))
- self.voiceovers_checkBox.setText(_translate("MainWindow", "Voiceovers"))
- self.logistics_crates_checkBox.setStatusTip(_translate("MainWindow", "Enable CTLD logistics crates for building ground units and air defenses. Pickup logistics containers to create new logistics sites."))
- self.logistics_crates_checkBox.setText(_translate("MainWindow", "Logistics"))
- self.awacs_checkBox.setText(_translate("MainWindow", "Friendly AWACS"))
- self.tankers_checkBox.setText(_translate("MainWindow", "Friendly Tankers"))
- self.apcs_spawn_checkBox.setStatusTip(_translate("MainWindow", "Friendly/enemy APCs will drop infantry when reaching a new conflict zone. Disables infinite troop pickups from conflict zones (you must pick up existing troops)."))
- self.apcs_spawn_checkBox.setText(_translate("MainWindow", "APCs Spawn Infantry"))
- self.inf_spawn_spinBox.setStatusTip(_translate("MainWindow", "This value is multiplied by the number of spawn zones in the mission template."))
- self.scenario_label_5.setText(_translate("MainWindow", "Groups Per Zone"))
- self.forces_hint_label_2.setText(_translate("MainWindow", "Forces templates are .miz files in \'Generator/Forces\'"))
- self.label.setStatusTip(_translate("MainWindow", "This value is multiplied by the number of spawn zones in the mission template."))
- self.label.setText(_translate("MainWindow", "Infantry Spawns per zone:"))
- self.slot_template_comboBox.setStatusTip(_translate("MainWindow", "Default player/client spawn locations at a friendly airport."))
- self.label_2.setText(_translate("MainWindow", "Player Slots"))
- self.force_offroad_checkBox.setStatusTip(_translate("MainWindow", "May help prevent long travel times or pathfinding issues. "))
- self.force_offroad_checkBox.setText(_translate("MainWindow", "Force Offroad"))
- self.defense_checkBox.setText(_translate("MainWindow", "Defensive Mode"))
- self.e_attack_helos_spinBox.setStatusTip(_translate("MainWindow", "Approximate number of enemy attack helicopter group spawns."))
- self.scenario_label_7.setStatusTip(_translate("MainWindow", "Approximate number of enemy attack helicopter group spawns."))
- self.scenario_label_7.setText(_translate("MainWindow", "Enemy Attack Helicopters"))
+ self.redforces_comboBox.setStatusTip(_translate("MainWindow", "Tip: You can create your own custom ground forces groups to be automatically generated."))
self.scenario_label_8.setStatusTip(_translate("MainWindow", "Approximate number of enemy attack plane group spawns."))
self.scenario_label_8.setText(_translate("MainWindow", "Enemy Attack Planes"))
- self.e_attack_planes_spinBox.setStatusTip(_translate("MainWindow", "Approximate number of enemy attack plane group spawns."))
- self.zone_sams_checkBox.setStatusTip(_translate("MainWindow", "Inactive conflict zones will be protected by SAMs. When a zone is cleared, SAMs at next active zone will be destroyed."))
- self.zone_sams_checkBox.setText(_translate("MainWindow", "Inactive Zone SAMs"))
- self.scenario_label_9.setText(_translate("MainWindow", "Zone FARP Conditions:"))
- self.inf_spawn_voiceovers_checkBox.setStatusTip(_translate("MainWindow", "Friendly/enemy APCs will drop infantry when reaching a new conflict zone."))
- self.inf_spawn_voiceovers_checkBox.setText(_translate("MainWindow", "Voiceovers on Infantry Spawn"))
- self.farp_never.setStatusTip(_translate("MainWindow", "Never spawn FARPs in defeated conflict zones."))
- self.farp_never.setText(_translate("MainWindow", "Never"))
- self.farp_gunits.setStatusTip(_translate("MainWindow", "Only spawn FARPs in defeated conflict zones if we have sufficient ground units remaining."))
- self.farp_gunits.setText(_translate("MainWindow", "20% Ground Units Remaining"))
- self.farp_always.setStatusTip(_translate("MainWindow", "Always spawn a FARP in defeated conflict zones."))
- self.farp_always.setText(_translate("MainWindow", "Always"))
+ self.slot_template_comboBox.setStatusTip(_translate("MainWindow", "Default player/client spawn locations at a friendly airport."))
+ self.scenario_label_5.setText(_translate("MainWindow", "Groups Per Zone"))
+ self.blue_forces_label.setText(_translate("MainWindow", "Blue Forces:"))
+ self.blueqty_spinBox.setStatusTip(_translate("MainWindow", "How many groups should we generate?"))
+ self.blueforces_comboBox.setStatusTip(_translate("MainWindow", "Tip: You can create your own custom ground forces groups to be automatically generated."))
+ self.scenario_label_4.setText(_translate("MainWindow", "Groups Per Zone"))
self.version_label.setText(_translate("MainWindow", "Version string"))
self.scenario_label_10.setStatusTip(_translate("MainWindow", "Approximate number of enemy transport helicopter spawns."))
self.scenario_label_10.setText(_translate("MainWindow", "Enemy Transport Helicopters"))
self.e_transport_helos_spinBox.setStatusTip(_translate("MainWindow", "Approximate number of enemy transport helicopter spawns."))
- self.label_3.setStatusTip(_translate("MainWindow", "The number of troop drops per transport helicopter flight."))
- self.label_3.setText(_translate("MainWindow", "Transport Drop Points:"))
- self.troop_drop_spinBox.setStatusTip(_translate("MainWindow", "The number of troop drops per transport helicopter flight."))
+ self.e_attack_planes_spinBox.setStatusTip(_translate("MainWindow", "Approximate number of enemy attack plane group spawns."))
+ self.e_attack_helos_spinBox.setStatusTip(_translate("MainWindow", "Approximate number of enemy attack helicopter group spawns."))
+ self.scenario_label_7.setStatusTip(_translate("MainWindow", "Approximate number of enemy attack helicopter group spawns."))
+ self.scenario_label_7.setText(_translate("MainWindow", "Enemy Attack Helicopters"))
+ self.label_2.setText(_translate("MainWindow", "Player Slots:"))
+ self.scenario_label_9.setText(_translate("MainWindow", "Zone FARP Conditions:"))
+ self.awacs_checkBox.setText(_translate("MainWindow", "Friendly AWACS"))
+ self.tankers_checkBox.setText(_translate("MainWindow", "Friendly Tankers"))
+ self.inf_spawn_voiceovers_checkBox.setStatusTip(_translate("MainWindow", "Friendly/enemy APCs will drop infantry when reaching a new conflict zone."))
+ self.inf_spawn_voiceovers_checkBox.setText(_translate("MainWindow", "Voiceovers on Infantry Spawn"))
+ self.voiceovers_checkBox.setStatusTip(_translate("MainWindow", "Voiceovers from the ground commander. Helps keep focus on the active zone."))
+ self.voiceovers_checkBox.setText(_translate("MainWindow", "Voiceovers"))
self.smoke_pickup_zone_checkBox.setStatusTip(_translate("MainWindow", "Infinite troop pickup zones will be marked with blue smoke."))
self.smoke_pickup_zone_checkBox.setText(_translate("MainWindow", "Smoke at Troop Pickup Zones"))
+ self.game_status_checkBox.setStatusTip(_translate("MainWindow", "Enable an onscreen zone status display. This helps keep focus on the active conflict zone."))
+ self.game_status_checkBox.setText(_translate("MainWindow", "Game Status Display"))
+ self.label.setStatusTip(_translate("MainWindow", "This value is multiplied by the number of spawn zones in the mission template."))
+ self.label.setText(_translate("MainWindow", "Infantry Spawns per zone"))
+ self.inf_spawn_spinBox.setStatusTip(_translate("MainWindow", "This value is multiplied by the number of spawn zones in the mission template."))
+ self.troop_drop_spinBox.setStatusTip(_translate("MainWindow", "The number of troop drops per transport helicopter flight."))
+ self.force_offroad_checkBox.setStatusTip(_translate("MainWindow", "May help prevent long travel times or pathfinding issues. "))
+ self.force_offroad_checkBox.setText(_translate("MainWindow", "Force Offroad"))
+ self.label_3.setStatusTip(_translate("MainWindow", "The number of troop drops per transport helicopter flight."))
+ self.label_3.setText(_translate("MainWindow", "Transport Drop Points"))
+ self.apcs_spawn_checkBox.setStatusTip(_translate("MainWindow", "Friendly/enemy APCs will drop infantry when reaching a new conflict zone. Disables infinite troop pickups from conflict zones (you must pick up existing troops)."))
+ self.apcs_spawn_checkBox.setText(_translate("MainWindow", "APCs Spawn Infantry"))
+ self.generateButton.setText(_translate("MainWindow", "GENERATE MISSION"))
+ self.farp_always.setStatusTip(_translate("MainWindow", "Always spawn a FARP in defeated conflict zones."))
+ self.farp_always.setText(_translate("MainWindow", "Always"))
+ self.farp_never.setStatusTip(_translate("MainWindow", "Never spawn FARPs in defeated conflict zones."))
+ self.farp_never.setText(_translate("MainWindow", "Never"))
+ self.farp_gunits.setStatusTip(_translate("MainWindow", "Only spawn FARPs in defeated conflict zones if we have sufficient ground units remaining."))
+ self.farp_gunits.setText(_translate("MainWindow", "20% Ground Units Remaining"))
+ self.nextScenario_pushButton.setText(_translate("MainWindow", ">"))
+ self.prevScenario_pushButton.setText(_translate("MainWindow", "<"))
+ self.menuMap.setTitle(_translate("MainWindow", "Map Filter"))
+ self.menuGametype_Filter.setTitle(_translate("MainWindow", "Gametype Filter"))
self.action_generateMission.setText(_translate("MainWindow", "_generateMission"))
self.action_scenarioSelected.setText(_translate("MainWindow", "_scenarioSelected"))
self.action_blueforcesSelected.setText(_translate("MainWindow", "_blueforcesSelected"))
self.action_redforcesSelected.setText(_translate("MainWindow", "_redforcesSelected"))
self.action_defensiveModeChanged.setText(_translate("MainWindow", "_defensiveModeChanged"))
+ self.action_nextScenario.setText(_translate("MainWindow", "_nextScenario"))
+ self.action_prevScenario.setText(_translate("MainWindow", "_prevScenario"))
+ self.actionCaucasus.setText(_translate("MainWindow", "Caucasus"))
+ self.actionPersian_Gulf.setText(_translate("MainWindow", "Persian Gulf"))
+ self.actionMarianas.setText(_translate("MainWindow", "Marianas"))
+ self.actionNevada.setText(_translate("MainWindow", "Nevada"))
+ self.actionSyria.setText(_translate("MainWindow", "Syria"))
+ self.actionAll.setText(_translate("MainWindow", "All"))
+ self.actionMultiplayer.setText(_translate("MainWindow", "Multiplayer"))
+ self.actionAll_2.setText(_translate("MainWindow", "All"))
if __name__ == "__main__":
diff --git a/Generator/MissionGeneratorUI.ui b/Generator/MissionGeneratorUI.ui
index 1898384..5d174ea 100644
--- a/Generator/MissionGeneratorUI.ui
+++ b/Generator/MissionGeneratorUI.ui
@@ -6,10 +6,28 @@
0
0
- 1209
- 900
+ 1280
+ 720
+
+
+ 0
+ 0
+
+
+
+
+ 1280
+ 720
+
+
+
+
+ 1280
+ 720
+
+
10
@@ -29,18 +47,151 @@
false
- background-color: white;
+ /*-----QScrollBar-----*/
+QScrollBar:horizontal
+{
+ background-color: transparent;
+ height: 8px;
+ margin: 0px;
+ padding: 0px;
+
+}
+
+
+QScrollBar::handle:horizontal
+{
+ border: none;
+ min-width: 100px;
+ background-color: #9b9b9b;
+
+}
+
+
+QScrollBar::add-line:horizontal,
+QScrollBar::sub-line:horizontal,
+QScrollBar::add-page:horizontal,
+QScrollBar::sub-page:horizontal
+{
+ width: 0px;
+ background-color: transparent;
+
+}
+
+
+QScrollBar:vertical
+{
+ background-color: transparent;
+ width: 8px;
+ margin: 0;
+
+}
+
+
+QScrollBar::handle:vertical
+{
+ border: none;
+ min-height: 100px;
+ background-color: #9b9b9b;
+
+}
+
+
+QScrollBar::add-line:vertical,
+QScrollBar::sub-line:vertical,
+QScrollBar::add-page:vertical,
+QScrollBar::sub-page:vertical
+{
+ height: 0px;
+ background-color: transparent;
+
+}
+
+
+
+ 990
+ 211
+ 251
+ 28
+
+
+
+
+ Arial
+ 10
+ false
+
+
+
+ Enable CTLD logistics crates for building ground units and air defenses. Pickup logistics containers to create new logistics sites.
+
+
+ Logistics
+
+
+ true
+
+
+
+
+
+ 990
+ 320
+ 241
+ 28
+
+
+
+
+ Arial
+ 10
+ false
+
+
+
+ Inactive conflict zones will be protected by SAMs. When a zone is cleared, SAMs at next active zone will be destroyed.
+
+
+ Inactive Zone SAMs
+
+
+
+
+
+ 470
+ 80
+ 171
+ 27
+
+
+
+
+ Arial
+ 10
+ false
+
+
+
+ Red Forces:
+
+
- 270
- 40
- 361
- 31
+ 30
+ 20
+ 371
+ 29
+
+
+ Arial
+ 8
+ true
+
+
@@ -53,220 +204,80 @@
-
-
-
-
- 60
- 30
- 181
- 41
-
+
+ QComboBox::AdjustToContentsOnFirstShow
-
-
- 12
-
-
-
- Scenario Template:
-
-
-
-
-
- 1020
- 790
- 141
- 41
-
-
-
- background-color: white;
-border-style: outset;
-border-width: 2px;
-border-radius: 15px;
-border-color: black;
-padding: 4px;
-
-
- Generate Mission
+
+ true
- 670
- 30
- 501
- 131
+ 40
+ 410
+ 361
+ 251
+ Arial
9
- border-radius: 5px; color: gray
+ padding: 5px;
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Plain
+
+
+ 1
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
<html><head><meta name="qrichtext" content="1" /><meta charset="utf-8" /><style type="text/css">
p, li { white-space: pre-wrap; }
-</style></head><body style=" font-family:'Segoe UI'; font-size:9pt; font-weight:400; font-style:normal;">
+</style></head><body style=" font-family:'Arial'; font-size:9pt; font-weight:400; font-style:normal;">
<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:10pt;">Provide close air support for our convoys as we take back Las Vegas from the enemy!</span></p></body></html>
-
+
+
+ true
+
- 790
- 230
- 291
- 31
-
-
-
- Tip: You can create your own custom ground forces groups to be automatically generated.
-
-
-
-
-
- 690
- 180
- 241
- 31
+ 470
+ 120
+ 156
+ 28
- 12
+ Arial
+ 10
+ false
- Friendly Forces:
+ Blue on Defense
-
-
-
-
- 60
- 180
- 261
- 31
-
-
-
-
- 12
-
-
-
- Enemy Forces:
-
-
-
-
-
- 170
- 230
- 291
- 31
-
-
-
- Tip: You can create your own custom ground forces groups to be automatically generated.
-
-
-
-
-
- -40
- 490
- 801
- 371
-
-
-
- false
-
-
-
-
-
-
-
-
- assets/background.PNG
-
-
-
-
-
- 250
- 80
- 381
- 16
-
-
-
- Scenario templates are .miz files in 'Generator/Scenarios'
-
-
- Qt::AlignCenter
-
-
-
-
-
- 130
- 270
- 381
- 16
-
-
-
- Forces templates are .miz files in 'Generator/Forces'
-
-
- Qt::AlignCenter
-
-
-
-
-
- 690
- 230
- 71
- 31
-
-
-
-
- 12
-
-
-
- How many groups should we generate?
-
-
- 0
-
-
- 8
-
-
- 3
+
+ true
- 70
- 230
- 71
+ 1070
+ 80
+ 51
31
@@ -278,6 +289,9 @@ p, li { white-space: pre-wrap; }
How many groups should we generate?
+
+ QAbstractSpinBox::PlusMinus
+
0
@@ -288,13 +302,82 @@ p, li { white-space: pre-wrap; }
2
-
+
- 670
- 260
- 101
- 31
+ 660
+ 80
+ 391
+ 33
+
+
+
+
+ 0
+ 0
+
+
+
+
+ Arial
+ 9
+ false
+
+
+
+ Tip: You can create your own custom ground forces groups to be automatically generated.
+
+
+
+
+
+ 570
+ 220
+ 271
+ 24
+
+
+
+
+ Arial
+ 10
+ false
+
+
+
+ Approximate number of enemy attack plane group spawns.
+
+
+ Enemy Attack Planes
+
+
+
+
+
+ 960
+ 384
+ 271
+ 33
+
+
+
+
+ Arial
+ 10
+ false
+
+
+
+ Default player/client spawn locations at a friendly airport.
+
+
+
+
+
+ 1130
+ 40
+ 131
+ 18
@@ -309,17 +392,448 @@ p, li { white-space: pre-wrap; }
Qt::AlignCenter
-
+
- 810
- 760
- 191
- 16
+ 470
+ 30
+ 161
+ 27
+ Arial
+ 10
+ false
+
+
+
+ Blue Forces:
+
+
+
+
+
+ 1070
+ 30
+ 51
+ 31
+
+
+
+
+ 12
+
+
+
+ How many groups should we generate?
+
+
+ QAbstractSpinBox::PlusMinus
+
+
+ 0
+
+
+ 8
+
+
+ 3
+
+
+
+
+
+ 660
+ 30
+ 391
+ 33
+
+
+
+
+ Arial
+ 9
+ false
+
+
+
+ Tip: You can create your own custom ground forces groups to be automatically generated.
+
+
+
+
+
+ 1130
+ 90
+ 131
+ 18
+
+
+
+
+ 8
+
+
+
+ Groups Per Zone
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 1140
+ 650
+ 111
+ 20
+
+
+
+ Version string
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+
+
+ 570
+ 260
+ 271
+ 24
+
+
+
+
+ Arial
+ 10
+ false
+
+
+
+ Approximate number of enemy transport helicopter spawns.
+
+
+ Enemy Transport Helicopters
+
+
+
+
+
+ 510
+ 260
+ 51
+ 31
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 12
+
+
+
+ Approximate number of enemy transport helicopter spawns.
+
+
+ QAbstractSpinBox::PlusMinus
+
+
+ 0
+
+
+ 8
+
+
+ 1
+
+
+
+
+
+ 510
+ 220
+ 51
+ 31
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 12
+
+
+
+ Approximate number of enemy attack plane group spawns.
+
+
+ QAbstractSpinBox::PlusMinus
+
+
+ 0
+
+
+ 8
+
+
+ 1
+
+
+
+
+
+ 510
+ 180
+ 51
+ 31
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 12
+
+
+
+ Approximate number of enemy attack helicopter group spawns.
+
+
+ false
+
+
+ QAbstractSpinBox::PlusMinus
+
+
+ true
+
+
+ 0
+
+
+ 8
+
+
+ 2
+
+
+
+
+
+ 570
+ 180
+ 271
+ 24
+
+
+
+
+ Arial
+ 10
+ false
+
+
+
+ Approximate number of enemy attack helicopter group spawns.
+
+
+ Enemy Attack Helicopters
+
+
+
+
+
+ 840
+ 390
+ 111
+ 24
+
+
+
+
+ Arial
+ 10
+ false
+
+
+
+ Player Slots:
+
+
+
+
+
+ 490
+ 450
+ 251
+ 23
+
+
+
+
+ Arial
+ 10
+
+
+
+ Zone FARP Conditions:
+
+
+
+
+
+ 990
+ 246
+ 241
+ 28
+
+
+
+
+ Arial
+ 10
+ false
+
+
+
+
+
+
+ Friendly AWACS
+
+
+ true
+
+
+
+
+
+ 990
+ 282
+ 241
+ 28
+
+
+
+
+ Arial
+ 10
+ false
+
+
+
+ Friendly Tankers
+
+
+ true
+
+
+
+
+
+ 960
+ 455
+ 271
+ 24
+
+
+
+
+ Arial
+ 9
+
+
+
+ Friendly/enemy APCs will drop infantry when reaching a new conflict zone.
+
+
+ Voiceovers on Infantry Spawn
+
+
+ true
+
+
+
+
+
+ 960
+ 517
+ 171
+ 24
+
+
+
+
+ Arial
+ 9
+
+
+
+ Voiceovers from the ground commander. Helps keep focus on the active zone.
+
+
+ Voiceovers
+
+
+ true
+
+
+
+
+
+ 960
+ 424
+ 271
+ 24
+
+
+
+
+ Arial
+ 9
+
+
+
+ Infinite troop pickup zones will be marked with blue smoke.
+
+
+ Smoke at Troop Pickup Zones
+
+
+ false
+
+
+
+
+
+ 960
+ 486
+ 271
+ 24
+
+
+
+
+ Arial
9
@@ -336,129 +850,35 @@ p, li { white-space: pre-wrap; }
false
-
+
- 810
- 790
- 191
- 16
-
-
-
-
- 9
-
-
-
- Voiceovers from the ground commander. Helps keep focus on the active zone.
-
-
- Voiceovers
-
-
- true
-
-
-
-
-
- 920
- 320
- 251
- 31
-
-
-
-
- 11
-
-
-
- Enable CTLD logistics crates for building ground units and air defenses. Pickup logistics containers to create new logistics sites.
-
-
- Logistics
-
-
- true
-
-
-
-
-
- 920
- 350
- 251
- 31
-
-
-
-
- 11
-
-
-
-
-
-
- Friendly AWACS
-
-
- true
-
-
-
-
-
- 920
+ 570
380
- 251
- 31
-
-
-
-
- 11
-
-
-
- Friendly Tankers
-
-
- true
-
-
-
-
-
- 450
- 420
- 251
- 31
+ 261
+ 23
+ Arial
10
+ false
- Friendly/enemy APCs will drop infantry when reaching a new conflict zone. Disables infinite troop pickups from conflict zones (you must pick up existing troops).
+ This value is multiplied by the number of spawn zones in the mission template.
- APCs Spawn Infantry
-
-
- true
+ Infantry Spawns per zone
- 670
- 340
- 51
+ 510
+ 380
+ 47
31
@@ -470,6 +890,9 @@ p, li { white-space: pre-wrap; }
This value is multiplied by the number of spawn zones in the mission template.
+
+ QAbstractSpinBox::PlusMinus
+
0
@@ -480,106 +903,48 @@ p, li { white-space: pre-wrap; }
2
-
+
- 50
- 260
- 101
+ 510
+ 330
+ 47
31
- 8
-
-
-
- Groups Per Zone
-
-
- Qt::AlignCenter
-
-
-
-
-
- 790
- 270
- 311
- 20
-
-
-
- Forces templates are .miz files in 'Generator/Forces'
-
-
- Qt::AlignCenter
-
-
-
-
-
- 450
- 340
- 211
- 21
-
-
-
-
- 10
+ 12
- This value is multiplied by the number of spawn zones in the mission template.
+ The number of troop drops per transport helicopter flight.
-
- Infantry Spawns per zone:
+
+ QAbstractSpinBox::PlusMinus
-
-
-
-
- 870
- 640
- 291
- 31
-
+
+ 0
-
- Default player/client spawn locations at a friendly airport.
+
+ 10
-
-
-
-
- 750
- 640
- 111
- 31
-
-
-
-
- 11
-
-
-
- Player Slots
+
+ 4
- 810
- 820
- 191
- 16
+ 960
+ 548
+ 161
+ 24
+ Arial
9
@@ -596,194 +961,121 @@ p, li { white-space: pre-wrap; }
false
-
+
- 60
- 90
- 181
- 31
-
-
-
-
- 11
-
-
-
- Defensive Mode
-
-
-
-
-
- 70
+ 570
330
- 51
- 31
-
-
-
-
- 12
-
-
-
- Approximate number of enemy attack helicopter group spawns.
-
-
- 0
-
-
- 8
-
-
- 2
-
-
-
-
-
- 140
- 330
- 211
- 31
-
-
-
-
- 11
-
-
-
- Approximate number of enemy attack helicopter group spawns.
-
-
- Enemy Attack Helicopters
-
-
-
-
-
- 140
- 370
- 201
- 31
-
-
-
-
- 11
-
-
-
- Approximate number of enemy attack plane group spawns.
-
-
- Enemy Attack Planes
-
-
-
-
-
- 70
- 370
- 51
- 31
-
-
-
-
- 12
-
-
-
- Approximate number of enemy attack plane group spawns.
-
-
- 0
-
-
- 8
-
-
- 1
-
-
-
-
-
- 920
- 410
- 201
- 31
-
-
-
-
- 11
-
-
-
- Inactive conflict zones will be protected by SAMs. When a zone is cleared, SAMs at next active zone will be destroyed.
-
-
- Inactive Zone SAMs
-
-
-
-
-
- 810
- 450
- 171
- 31
+ 281
+ 23
+ Arial
10
+ false
+
+ The number of troop drops per transport helicopter flight.
+
- Zone FARP Conditions:
+ Transport Drop Points
-
+
- 810
- 720
+ 990
+ 180
251
- 31
+ 27
- 9
+ Arial
+ 10
+ false
- Friendly/enemy APCs will drop infantry when reaching a new conflict zone.
+ Friendly/enemy APCs will drop infantry when reaching a new conflict zone. Disables infinite troop pickups from conflict zones (you must pick up existing troops).
- Voiceovers on Infantry Spawn
+ APCs Spawn Infantry
true
-
+
- 950
- 500
- 95
- 20
+ 710
+ 600
+ 231
+ 51
+ Arial
+ 8
+ true
+
+
+
+ background-color: gray;
+color: rgb(255, 255, 255);
+border-style: outset;
+border-width: 1px;
+border-radius: 5px;
+border-color: black;
+padding: 4px;
+
+
+ GENERATE MISSION
+
+
+
+
+
+ 510
+ 480
+ 261
+ 24
+
+
+
+
+ Arial
+ 9
+
+
+
+ Always spawn a FARP in defeated conflict zones.
+
+
+ Always
+
+
+ farp_buttonGroup
+
+
+
+
+
+ 510
+ 540
+ 271
+ 24
+
+
+
+
+ Arial
9
@@ -800,14 +1092,15 @@ p, li { white-space: pre-wrap; }
- 950
- 530
- 221
- 21
+ 510
+ 509
+ 261
+ 24
+ Arial
9
@@ -824,225 +1117,142 @@ p, li { white-space: pre-wrap; }
farp_buttonGroup
-
+
+
+ true
+
- 950
- 560
- 221
- 21
+ 60
+ 80
+ 300
+ 300
-
-
- 9
-
+
+
+ 0
+ 0
+
-
- Always spawn a FARP in defeated conflict zones.
+
+
+ 300
+ 300
+
+
+
+
+ 16777215
+ 16777215
+
+
+
+
- Always
+
+
+
+ assets/briefing1.png
+
+
+ true
+
+
+ false
-
- farp_buttonGroup
-
-
+
- 920
- 840
+ 370
+ 210
+ 31
+ 51
+
+
+
+ >
+
+
+
+
+
+ 20
+ 210
+ 31
+ 51
+
+
+
+ <
+
+
+
+
+
+ 1020
+ 600
241
- 21
+ 51
- Version string
+
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+ assets/rotorops-dkgray.png
-
-
-
-
- 140
- 410
- 241
- 31
-
-
-
-
- 11
-
-
-
- Approximate number of enemy transport helicopter spawns.
-
-
- Enemy Transport Helicopters
-
-
-
-
-
- 70
- 410
- 51
- 31
-
-
-
-
- 12
-
-
-
- Approximate number of enemy transport helicopter spawns.
-
-
- 0
-
-
- 8
-
-
- 1
-
-
-
-
-
- 450
- 380
- 191
- 31
-
-
-
-
- 10
-
-
-
- The number of troop drops per transport helicopter flight.
-
-
- Transport Drop Points:
-
-
-
-
-
- 670
- 380
- 51
- 31
-
-
-
-
- 12
-
-
-
- The number of troop drops per transport helicopter flight.
-
-
- 0
-
-
- 10
-
-
- 4
-
-
-
-
-
- 810
- 690
- 251
- 31
-
-
-
-
- 9
-
-
-
- Infinite troop pickup zones will be marked with blue smoke.
-
-
- Smoke at Troop Pickup Zones
-
-
+
true
- background_label
- scenario_comboBox
- scenario_label
- generateButton
- description_textBrowser
- blueforces_comboBox
- blue_forces_label
- red_forces_label
- redforces_comboBox
- scenario_hint_label
- forces_hint_label
- blueqty_spinBox
- redqty_spinBox
- scenario_label_4
- game_status_checkBox
- voiceovers_checkBox
- logistics_crates_checkBox
- awacs_checkBox
- tankers_checkBox
- apcs_spawn_checkBox
- inf_spawn_spinBox
- scenario_label_5
- forces_hint_label_2
- label
- slot_template_comboBox
- label_2
- force_offroad_checkBox
- defense_checkBox
- e_attack_helos_spinBox
- scenario_label_7
- scenario_label_8
- e_attack_planes_spinBox
- zone_sams_checkBox
- scenario_label_9
- inf_spawn_voiceovers_checkBox
- farp_never
- farp_gunits
- farp_always
- version_label
- scenario_label_10
- e_transport_helos_spinBox
- label_3
- troop_drop_spinBox
- smoke_pickup_zone_checkBox
+
+
+ Arial
+ 9
+ false
+
+
false
+
+ color: rgb(255, 255, 255);
+
@@ -1069,6 +1279,71 @@ p, li { white-space: pre-wrap; }
_defensiveModeChanged
+
+
+ _nextScenario
+
+
+
+
+ _prevScenario
+
+
+
+
+ Caucasus
+
+
+
+
+ Persian Gulf
+
+
+
+
+ Marianas
+
+
+
+
+ Nevada
+
+
+
+
+ Syria
+
+
+
+
+ true
+
+
+ true
+
+
+ All
+
+
+
+
+ false
+
+
+ Multiplayer
+
+
+
+
+ true
+
+
+ true
+
+
+ All
+
+
@@ -1079,8 +1354,8 @@ p, li { white-space: pre-wrap; }
trigger()
- 993
- 591
+ 1030
+ 616
-1
@@ -1111,8 +1386,40 @@ p, li { white-space: pre-wrap; }
trigger()
- 150
- 131
+ 560
+ 173
+
+
+ -1
+ -1
+
+
+
+
+ nextScenario_pushButton
+ clicked()
+ action_nextScenario
+ trigger()
+
+
+ 389
+ 257
+
+
+ 372
+ 63
+
+
+
+
+ prevScenario_pushButton
+ clicked()
+ action_prevScenario
+ trigger()
+
+
+ 35
+ 261
-1
diff --git a/Generator/RotorOpsConflict.py b/Generator/RotorOpsConflict.py
new file mode 100644
index 0000000..4347449
--- /dev/null
+++ b/Generator/RotorOpsConflict.py
@@ -0,0 +1,159 @@
+import dcs
+import random
+
+jtf_red = "Combined Joint Task Forces Red"
+jtf_blue = "Combined Joint Task Forces Blue"
+
+
+def triggerSetup(rops, options):
+ # get the boolean value from ui option and convert to lua string
+ def lb(var):
+ return str(options[var]).lower()
+
+ game_flag = 100
+ # Add the first trigger
+ trig = dcs.triggers.TriggerOnce(comment="RotorOps Setup Scripts")
+ trig.rules.append(dcs.condition.TimeAfter(1))
+ #trig.actions.append(dcs.action.DoScriptFile(rops.scripts["mist_4_4_90.lua"]))
+ trig.actions.append(dcs.action.DoScriptFile(rops.scripts["mist_4_5_107_grimm.lua"]))
+ trig.actions.append(dcs.action.DoScriptFile(rops.scripts["Splash_Damage_2_0.lua"]))
+ trig.actions.append(dcs.action.DoScriptFile(rops.scripts["CTLD.lua"]))
+ trig.actions.append(dcs.action.DoScriptFile(rops.scripts["RotorOps.lua"]))
+ script = ""
+ script = ("--OPTIONS HERE!\n\n" +
+ "RotorOps.CTLD_crates = " + lb("crates") + "\n\n" +
+ "RotorOps.CTLD_sound_effects = true\n\n" +
+ "RotorOps.force_offroad = " + lb("force_offroad") + "\n\n" +
+ "RotorOps.voice_overs = " + lb("voiceovers") + "\n\n" +
+ "RotorOps.zone_status_display = " + lb("game_display") + "\n\n" +
+ "RotorOps.inf_spawn_messages = " + lb("inf_spawn_msgs") + "\n\n" +
+ "RotorOps.inf_spawns_per_zone = " + lb("inf_spawn_qty") + "\n\n" +
+ "RotorOps.apcs_spawn_infantry = " + lb("apc_spawns_inf") + " \n\n")
+ if not options["smoke_pickup_zones"]:
+ script = script + 'RotorOps.pickup_zone_smoke = "none"\n\n'
+ trig.actions.append(dcs.action.DoScript(dcs.action.String((script))))
+ rops.m.triggerrules.triggers.append(trig)
+
+ # Add the second trigger
+ trig = dcs.triggers.TriggerOnce(comment="RotorOps Setup Zones")
+ trig.rules.append(dcs.condition.TimeAfter(2))
+ for s_zone in rops.staging_zones:
+ trig.actions.append(dcs.action.DoScript(dcs.action.String("RotorOps.stagingZone('" + s_zone + "')")))
+ for c_zone in rops.conflict_zones:
+ zone_flag = rops.conflict_zones[c_zone].flag
+ trig.actions.append(
+ dcs.action.DoScript(dcs.action.String("RotorOps.addZone('" + c_zone + "'," + str(zone_flag) + ")")))
+
+ trig.actions.append(dcs.action.DoScript(dcs.action.String("RotorOps.setupConflict('" + str(game_flag) + "')")))
+
+ rops.m.triggerrules.triggers.append(trig)
+
+ # Add the third trigger
+ trig = dcs.triggers.TriggerOnce(comment="RotorOps Conflict Start")
+ trig.rules.append(dcs.condition.TimeAfter(10))
+ trig.actions.append(dcs.action.DoScript(dcs.action.String("RotorOps.startConflict(100)")))
+ rops.m.triggerrules.triggers.append(trig)
+
+ # Add generic zone-based triggers
+ for index, zone_name in enumerate(rops.conflict_zones):
+ z_active_trig = dcs.triggers.TriggerOnce(comment=zone_name + " Active")
+ z_active_trig.rules.append(dcs.condition.FlagEquals(game_flag, index + 1))
+ z_active_trig.actions.append(dcs.action.DoScript(dcs.action.String("--Add any action you want here!")))
+ rops.m.triggerrules.triggers.append(z_active_trig)
+
+ # Zone protection SAMs
+ if options["zone_protect_sams"]:
+ for index, zone_name in enumerate(rops.conflict_zones):
+ z_sams_trig = dcs.triggers.TriggerOnce(comment="Deactivate " + zone_name + " SAMs")
+ z_sams_trig.rules.append(dcs.condition.FlagEquals(game_flag, index + 1))
+ z_sams_trig.actions.append(dcs.action.DoScript(
+ dcs.action.String("Group.destroy(Group.getByName('Static " + zone_name + " Protection SAM'))")))
+ rops.m.triggerrules.triggers.append(z_sams_trig)
+
+ # Zone FARPS always
+ if options["zone_farps"] == "farp_always" and not options["defending"]:
+ for index, zone_name in enumerate(rops.conflict_zones):
+ if index > 0:
+ previous_zone = list(rops.conflict_zones)[index - 1]
+ if not rops.m.country(jtf_blue).find_group(previous_zone + " FARP Static"):
+ continue
+ z_farps_trig = dcs.triggers.TriggerOnce(comment="Activate " + previous_zone + " FARP")
+ z_farps_trig.rules.append(dcs.condition.FlagEquals(game_flag, index + 1))
+ z_farps_trig.actions.append(
+ dcs.action.ActivateGroup(rops.m.country(jtf_blue).find_group(previous_zone + " FARP Static").id))
+ # z_farps_trig.actions.append(dcs.action.SoundToAll(str(rops.res_map['forward_base_established.ogg'])))
+ z_farps_trig.actions.append(dcs.action.DoScript(dcs.action.String(
+ "RotorOps.farpEstablished(" + str(index) + ")")))
+ rops.m.triggerrules.triggers.append(z_farps_trig)
+
+ # Zone FARPS conditional on staged units remaining
+ if options["zone_farps"] == "farp_gunits" and not options["defending"]:
+ for index, zone_name in enumerate(rops.conflict_zones):
+ if index > 0:
+ previous_zone = list(rops.conflict_zones)[index - 1]
+ if not rops.m.country(jtf_blue).find_group(previous_zone + " FARP Static"):
+ continue
+ z_farps_trig = dcs.triggers.TriggerOnce(comment="Activate " + previous_zone + " FARP")
+ z_farps_trig.rules.append(dcs.condition.FlagEquals(game_flag, index + 1))
+ z_farps_trig.rules.append(dcs.condition.FlagIsMore(111, 20))
+ z_farps_trig.actions.append(dcs.action.DoScript(dcs.action.String(
+ "--The 100 flag indicates which zone is active. The 111 flag value is the percentage of staged units remaining")))
+ z_farps_trig.actions.append(
+ dcs.action.ActivateGroup(rops.m.country(jtf_blue).find_group(previous_zone + " FARP Static").id))
+ # z_farps_trig.actions.append(dcs.action.SoundToAll(str(rops.res_map['forward_base_established.ogg'])))
+ z_farps_trig.actions.append(dcs.action.DoScript(dcs.action.String(
+ "RotorOps.farpEstablished(" + str(index) + ")")))
+ rops.m.triggerrules.triggers.append(z_farps_trig)
+
+ # Add attack helos triggers
+ for index in range(options["e_attack_helos"]):
+ random_zone_obj = random.choice(list(rops.conflict_zones.items()))
+ zone = random_zone_obj[1]
+ z_weak_trig = dcs.triggers.TriggerOnce(comment=zone.name + " Attack Helo")
+ z_weak_trig.rules.append(dcs.condition.FlagIsMore(zone.flag, 1))
+ z_weak_trig.rules.append(dcs.condition.FlagIsLess(zone.flag, random.randrange(20, 90)))
+ z_weak_trig.actions.append(dcs.action.DoScript(dcs.action.String("---Flag " + str(
+ zone.flag) + " value represents the percentage of defending ground units remaining in zone. ")))
+ z_weak_trig.actions.append(dcs.action.DoScript(dcs.action.String("RotorOps.spawnAttackHelos()")))
+ rops.m.triggerrules.triggers.append(z_weak_trig)
+
+ # Add attack plane triggers
+ for index in range(options["e_attack_planes"]):
+ random_zone_obj = random.choice(list(rops.conflict_zones.items()))
+ zone = random_zone_obj[1]
+ z_weak_trig = dcs.triggers.TriggerOnce(comment=zone.name + " Attack Plane")
+ z_weak_trig.rules.append(dcs.condition.FlagIsMore(zone.flag, 1))
+ z_weak_trig.rules.append(dcs.condition.FlagIsLess(zone.flag, random.randrange(20, 90)))
+ z_weak_trig.actions.append(dcs.action.DoScript(dcs.action.String("---Flag " + str(
+ zone.flag) + " value represents the percentage of defending ground units remaining in zone. ")))
+ z_weak_trig.actions.append(dcs.action.DoScript(dcs.action.String("RotorOps.spawnAttackPlanes()")))
+ rops.m.triggerrules.triggers.append(z_weak_trig)
+
+ # Add transport helos triggers
+ for index in range(options["e_transport_helos"]):
+ random_zone_index = random.randrange(1, len(rops.conflict_zones))
+ random_zone_obj = list(rops.conflict_zones.items())[random_zone_index]
+ zone = random_zone_obj[1]
+ z_weak_trig = dcs.triggers.TriggerOnce(comment=zone.name + " Transport Helo")
+ z_weak_trig.rules.append(dcs.condition.FlagEquals(game_flag, random_zone_index + 1))
+ z_weak_trig.rules.append(dcs.condition.FlagIsLess(zone.flag, random.randrange(20, 100)))
+ z_weak_trig.actions.append(dcs.action.DoScript(dcs.action.String(
+ "---Flag " + str(game_flag) + " value represents the index of the active zone. ")))
+ z_weak_trig.actions.append(dcs.action.DoScript(dcs.action.String("---Flag " + str(
+ zone.flag) + " value represents the percentage of defending ground units remaining in zone. ")))
+ z_weak_trig.actions.append(dcs.action.DoScript(
+ dcs.action.String("RotorOps.spawnTranspHelos(8," + str(options["transport_drop_qty"]) + ")")))
+ rops.m.triggerrules.triggers.append(z_weak_trig)
+
+ # Add game won/lost triggers
+ trig = dcs.triggers.TriggerOnce(comment="RotorOps Conflict WON")
+ trig.rules.append(dcs.condition.FlagEquals(game_flag, 99))
+ trig.actions.append(
+ dcs.action.DoScript(dcs.action.String("---Add an action you want to happen when the game is WON")))
+ rops.m.triggerrules.triggers.append(trig)
+
+ trig = dcs.triggers.TriggerOnce(comment="RotorOps Conflict LOST")
+ trig.rules.append(dcs.condition.FlagEquals(game_flag, 98))
+ trig.actions.append(
+ dcs.action.DoScript(dcs.action.String("---Add an action you want to happen when the game is LOST")))
+ rops.m.triggerrules.triggers.append(trig)
\ No newline at end of file
diff --git a/Generator/RotorOpsGroups.py b/Generator/RotorOpsGroups.py
index 25bc383..26a225a 100644
--- a/Generator/RotorOpsGroups.py
+++ b/Generator/RotorOpsGroups.py
@@ -25,7 +25,7 @@ class VehicleTemplate:
dcs.vehicles.Unarmed.M_818,
dcs.vehicles.AirDefence.Vulcan,
dcs.vehicles.Unarmed.Ural_375,
- dcs.vehicles.Unarmed.M978_HEMTT_Tanker
+ dcs.vehicles.Unarmed.M978_HEMTT_Tanker,
],
position.point_from_heading(45, 7),
heading=random.randint(0, 359),
diff --git a/Generator/RotorOpsImport.py b/Generator/RotorOpsImport.py
new file mode 100644
index 0000000..af8aadb
--- /dev/null
+++ b/Generator/RotorOpsImport.py
@@ -0,0 +1,233 @@
+import math
+import dcs
+from MissionGenerator import logger
+
+
+class ImportObjects:
+
+ def __init__(self, mizfile):
+ self.pad_unit = True #todo: use this to hold a unit for helicopter placement on ships ie flight_group_from_unit
+ logger.info("Importing objects from " + mizfile)
+ self.source_mission = dcs.mission.Mission()
+ self.source_mission.load_file(mizfile)
+ self.source_heading = None
+ self.source_point = None
+ self.statics = []
+ self.vehicles = []
+ self.helicopters = []
+
+ self.extractUnits()
+
+ def getStatics(self):
+ return self.statics
+
+ def getVehicles(self):
+ return self.vehicles
+
+ def getHelicopters(self):
+ return self.helicopters
+
+ def copyAll(self, mission, dest_country_name, dest_name, dest_point=None, dest_heading=0):
+ return self.copyStatics(mission, dest_country_name, dest_name, dest_point, dest_heading), \
+ self.copyVehicles(mission, dest_country_name, dest_name, dest_point, dest_heading), \
+ self.copyHelicopters(mission, dest_country_name, dest_name, dest_point, dest_heading)
+
+
+ def anchorByGroupName(self, group_name):
+ group = self.source_mission.find_group(group_name)
+ if group:
+ self.source_point = group.units[0].position
+ self.source_heading = group.units[0].heading
+ else:
+ logger.warning("Unable to find group for anchor.")
+ raise Exception(
+ "Import template file error: " + self.mizfile + " does not contain a group called " + group_name)
+
+ def extractUnits(self):
+
+ for side in "red", "blue", "neutrals":
+ coalition = self.source_mission.coalition.get(side)
+ for country_name in coalition.countries:
+
+ group_types = [coalition.countries[country_name].static_group, coalition.countries[country_name].vehicle_group, coalition.countries[country_name].helicopter_group, coalition.countries[country_name].plane_group,
+ coalition.countries[country_name].ship_group]
+
+ for index, group_type in enumerate(group_types):
+ for group in group_type:
+
+ if index == 0: # Statics
+ self.statics.append(group)
+ elif index == 1: # Vehicles
+ self.vehicles.append(group)
+ elif index == 2: # Helicopters
+ self.helicopters.append(group)
+ elif index == 3:
+ logger.warn(group.name + ": Planes not available for import")
+ elif index == 4:
+ logger.warn(group.name + ": Ships not available for import")
+
+
+ def copyStatics(self, mission, dest_country_name, dest_name, dest_point=None, dest_heading=0):
+ logger.info("Copying " + str(len(self.statics)) + " static objects as " + dest_name)
+ new_groups = []
+
+ if not dest_point:
+ dest_point = dcs.Point(mission.terrain.bullseye_blue["x"], mission.terrain.bullseye_blue["y"])
+
+ #Statics
+ statics_copy = self.statics.copy()
+ for group in statics_copy:
+
+ self.groupToPoint(group, self.source_point, dest_point, self.source_heading, dest_heading)
+
+
+ class temp(dcs.unittype.StaticType):
+ id = group.units[0].type
+ name = group.units[0].name
+ shape_name = group.units[0].shape_name
+ rate = group.units[0].rate
+ can_cargo = group.units[0].can_cargo
+ mass = group.units[0].mass
+
+
+ ng = mission.static_group(mission.country(dest_country_name),
+ dest_name + " " + group.name,
+ temp,
+ group.units[0].position,
+ group.units[0].heading,
+ hidden=False)
+ ng.units[0].name = group.units[0].name
+ new_groups.append(ng)
+
+ # if ng.units[0].type == "Invisible FARP":
+ # self.pad_unit = ng
+
+ return new_groups
+
+
+
+
+ def copyVehicles(self, mission, dest_country_name, dest_name, dest_point=None, dest_heading=0):
+ logger.info("Copying " + str(len(self.vehicles)) + " vehicle groups as " + dest_name)
+ new_groups = []
+
+ if not dest_point:
+ dest_point = dcs.Point(mission.terrain.bullseye_blue["x"], mission.terrain.bullseye_blue["y"])
+
+ vehicles_copy = self.vehicles
+ for group in vehicles_copy:
+
+ self.groupToPoint(group, self.source_point, dest_point, self.source_heading, dest_heading)
+
+ for i, unit in enumerate(group.units):
+ if i == 0:
+ ng = mission.vehicle_group(mission.country(dest_country_name),
+ dest_name + " " + group.name,
+ dcs.vehicles.vehicle_map[group.units[0].type],
+ group.units[0].position,
+ group.units[0].heading)
+
+ new_groups.append(ng) # will this hold units we add later?
+
+ else:
+
+ u = mission.vehicle(dest_name + " " + group.units[i].name, dcs.vehicles.vehicle_map[group.units[i].type])
+ u.position = group.units[i].position
+ u.heading = group.units[i].heading
+ ng.add_unit(u)
+
+ return new_groups
+
+
+ def copyHelicopters(self, mission, dest_country_name, dest_name, dest_point=None, dest_heading=0):
+ logger.info("Copying " + str(len(self.helicopters)) + " helicopters as " + dest_name)
+ new_groups = []
+
+ if not dest_point:
+ dest_point = dcs.Point(mission.terrain.bullseye_blue["x"], mission.terrain.bullseye_blue["y"])
+
+ helicopters_copy = self.helicopters.copy()
+ for group in helicopters_copy:
+
+ self.groupToPoint(group, self.source_point, dest_point, self.source_heading, dest_heading)
+
+ if self.pad_unit:
+ if group.units[0].skill == dcs.unit.Skill.Client or group.units[0].skill == dcs.unit.Skill.Player:
+
+ # we'll create a new FARP for each helicopter. we've tried adding the flight group to an existing FARP, but they stack on top of each other
+ # trying to move the units into position after adding the flight group moves the 2D graphic of the helicopter, but the unit marker remains stacked on top
+ # of the unit marker in ME
+ # farp = mission.country(country_name).find_group(self.pad_unit.name)
+
+ farp = mission.farp(mission.country(dest_country_name), dest_name + " " + group.name + " Pad", group.units[0].position, hidden=True, dead=False,
+ farp_type=dcs.unit.InvisibleFARP)
+
+
+
+ ng = mission.flight_group_from_unit(mission.country(dest_country_name),
+ dest_name + " " + group.name,
+ dcs.helicopters.helicopter_map[group.units[0].type],
+ farp,
+ group_size=1)
+
+ ng.points[0].action = dcs.point.PointAction.FromGroundArea
+ ng.points[0].type = "TakeOffGround"
+ ng.units[0].heading = group.units[0].heading
+ ng.units[0].skill = group.units[0].skill
+ ng.units[0].livery_id = group.units[0].livery_id
+ ng.units[0].pylons = group.units[0].pylons
+
+ new_groups.append(ng)
+ else:
+ logger.warn("No pad unit (ie FARP, carrier) found, so can't add helicopters.")
+
+ return new_groups
+
+
+ def copyVehiclesAsGroup(self, mission, dest_country_name, dest_name, dest_point=None, dest_heading=0):
+ logger.info("Copying " + str(len(self.vehicles)) + " vehicle groups as single group name: " + dest_name)
+ new_group = None
+
+ if not dest_point:
+ dest_point = dcs.Point(mission.terrain.bullseye_blue["x"], mission.terrain.bullseye_blue["y"])
+
+ unit_count = 0
+ vehicles_copy = self.vehicles.copy()
+ for group in vehicles_copy:
+ self.groupToPoint(group, self.source_point, dest_point, self.source_heading, dest_heading)
+ for i, unit in enumerate(group.units):
+
+ if unit_count == 0:
+ print("Group:" + group.name)
+ new_group = mission.vehicle_group(mission.country(dest_country_name),
+ dest_name,
+ dcs.vehicles.vehicle_map[group.units[0].type],
+ group.units[0].position,
+ group.units[0].heading)
+ unit_count = unit_count + 1
+
+ else:
+
+ print("Unit:" + group.units[i].name)
+ u = mission.vehicle(dest_name + " " + group.units[i].name, dcs.vehicles.vehicle_map[group.units[i].type])
+ u.position = group.units[i].position
+ u.heading = group.units[i].heading
+ new_group.add_unit(u)
+
+ unit_count = unit_count + 1
+ print("Made a group with units: " + str(unit_count))
+ print("group actually has units: " + str(len(new_group.units)))
+
+ return new_group
+
+
+ @staticmethod
+ def groupToPoint(group, src_point, dest_point, src_heading=0, dest_heading=0):
+ for unit in group.units:
+ heading_to_unit = dcs.mapping.heading_between_points(src_point.x, src_point.y, unit.position.x,
+ unit.position.y)
+ new_heading_to_unit = dest_heading + heading_to_unit
+ unit_distance = src_point.distance_to_point(unit.position)
+ unit.position = dest_point.point_from_heading(new_heading_to_unit, unit_distance)
+ unit.heading = unit.heading + dest_heading
+ return group
\ No newline at end of file
diff --git a/Generator/RotorOpsMission.py b/Generator/RotorOpsMission.py
index c9fc812..2c45f46 100644
--- a/Generator/RotorOpsMission.py
+++ b/Generator/RotorOpsMission.py
@@ -4,31 +4,39 @@ import dcs
import os
import random
+
import RotorOpsGroups
import RotorOpsUnits
+import RotorOpsUtils
+import RotorOpsConflict
+from RotorOpsImport import ImportObjects
import time
from MissionGenerator import logger
+from MissionGenerator import directories
-
+jtf_red = "Combined Joint Task Forces Red"
+jtf_blue = "Combined Joint Task Forces Blue"
class RotorOpsMission:
def __init__(self):
self.m = dcs.mission.Mission()
- os.chdir("../")
- self.home_dir = os.getcwd()
- self.scenarios_dir = self.home_dir + "\Generator\Scenarios"
- self.forces_dir = self.home_dir + "\Generator\Forces"
- self.script_directory = self.home_dir
- self.sound_directory = self.home_dir + "\sound\embedded"
- self.output_dir = self.home_dir + "\Generator\Output"
- self.assets_dir = self.home_dir + "\Generator/assets"
+ # os.chdir("../")
+ # directories.home_dir = os.getcwd()
+ # directories.scenarios = directories.home_dir + "\Generator\Scenarios"
+ # directories.forces = directories.home_dir + "\Generator\Forces"
+ # directories.scripts = directories.home_dir
+ # directories.sound = directories.home_dir + "\sound\embedded"
+ # directories.output = directories.home_dir + "\Generator\Output"
+ # directories.assets = directories.home_dir + "\Generator/assets"
+ # directories.imports = directories.home_dir + "\Generator\Imports"
self.conflict_zones = {}
self.staging_zones = {}
self.spawn_zones = {}
self.scripts = {}
self.res_map = {}
+ self.config = None
class RotorOpsZone:
def __init__(self, name: str, flag: int, position: dcs.point, size: int):
@@ -37,9 +45,13 @@ class RotorOpsMission:
self.position = position
self.size = size
+
def getMission(self):
return self.m
+ def setConfig(self,config):
+ self.config = config
+
def addZone(self, zone_dict, zone: RotorOpsZone):
zone_dict[zone.name] = zone
@@ -79,8 +91,8 @@ class RotorOpsMission:
attack_planes = []
fighter_planes = []
- os.chdir(self.home_dir)
- os.chdir(self.forces_dir + "/" + side)
+ os.chdir(directories.home_dir)
+ os.chdir(directories.forces + "/" + side)
logger.info("Looking for " + side + " Forces files in '" + os.getcwd())
source_mission = dcs.mission.Mission()
@@ -114,13 +126,20 @@ class RotorOpsMission:
logger.error("Failed to load units from " + filename)
def generateMission(self, options):
- os.chdir(self.scenarios_dir)
+ os.chdir(directories.scenarios)
logger.info("Looking for mission files in " + os.getcwd())
+
+
self.m.load_file(options["scenario_filename"])
- if not self.m.country("Combined Joint Task Forces Red") or not self.m.country("Combined Joint Task Forces Blue"):
- failure_msg = "You must include a CombinedJointTaskForcesBlue and CombinedJointTaskForcesRed unit in the scenario template. See the instructions in " + self.scenarios_dir
+ self.importObjects()
+
+ #todo: test
+ self.m.coalition.get("neutrals").add_country(dcs.countries.UnitedNationsPeacekeepers())
+
+ if not self.m.country(jtf_red) or not self.m.country(jtf_blue) or not self.m.country(dcs.countries.UnitedNationsPeacekeepers.name):
+ failure_msg = "You must include a CombinedJointTaskForcesBlue and CombinedJointTaskForcesRed unit in the scenario template. See the instructions in " + directories.scenarios
return {"success": False, "failure_msg": failure_msg}
red_forces = self.getUnitsFromMiz(options["red_forces_filename"], "red")
@@ -129,10 +148,11 @@ class RotorOpsMission:
# Add coalitions (we may be able to add CJTF here instead of requiring templates to have objects of those coalitions)
self.m.coalition.get("red").add_country(dcs.countries.Russia())
self.m.coalition.get("blue").add_country(dcs.countries.USA())
+ # blue = self.m.coalition.get("blue")
+ # blue.add_country(dcs.countries.CombinedJointTaskForcesBlue())
- self.m.add_picture_blue(self.assets_dir + '/briefing1.png')
- self.m.add_picture_blue(self.assets_dir + '/briefing2.png')
-
+ self.m.add_picture_blue(directories.assets + '/briefing1.png')
+ self.m.add_picture_blue(directories.assets + '/briefing2.png')
# add zones to target mission
@@ -163,20 +183,49 @@ class RotorOpsMission:
#Populate Red zones with ground units
+
for zone_name in red_zones:
if red_forces["vehicles"]:
- self.addGroundGroups(red_zones[zone_name], self.m.country('Combined Joint Task Forces Red'), red_forces["vehicles"], options["red_quantity"])
+ self.addGroundGroups(red_zones[zone_name], self.m.country(jtf_red), red_forces["vehicles"], options["red_quantity"])
#Add red FARPS
+
if options["zone_farps"] != "farp_never" and not options["defending"]:
- RotorOpsGroups.VehicleTemplate.CombinedJointTaskForcesBlue.zone_farp(self.m, self.m.country('Combined Joint Task Forces Blue'),
- self.m.country('Combined Joint Task Forces Blue'),
- red_zones[zone_name].position,
- 180, zone_name + " FARP", late_activation=True)
+ # RotorOpsGroups.VehicleTemplate.CombinedJointTaskForcesBlue.zone_farp(self.m, self.m.country(jtf_blue),
+ # self.m.country(jtf_blue),
+ # red_zones[zone_name].position,
+ # 180, zone_name + " FARP", late_activation=True)
+
+ #new_statics, new_vehicles, new_helicopters = i.copyAll(self.m, dcs.countries.UnitedNationsPeacekeepers.name, zone_name, red_zones[zone_name].position)
+
+ farp_flag = self.m.find_group(zone_name)
+
+ if farp_flag:
+ farp_position = farp_flag.units[0].position
+ farp_heading = farp_flag.units[0].heading
+ else:
+ farp_position = red_zones[zone_name].position
+ farp_heading = 0
+
+ farp = self.m.farp(self.m.country(jtf_blue), zone_name + " FARP", farp_position,
+ hidden=False, dead=False,
+ farp_type=dcs.unit.InvisibleFARP)
+
+ os.chdir(directories.imports)
+ if self.config and self.config["zone_farp_file"]:
+ filename = self.config["zone_farp_file"]
+ else:
+ filename = "FARP_DEFAULT_ZONE.miz"
+ i = ImportObjects(filename)
+ i.anchorByGroupName("ANCHOR")
+ farp_group = i.copyVehiclesAsGroup(self.m, jtf_blue, zone_name + " FARP Static", farp_position, farp_heading)
+ farp_group.late_activation = True
+
+
if options["zone_protect_sams"]:
self.m.vehicle_group(
- self.m.country('Combined Joint Task Forces Red'),
+ self.m.country(jtf_red),
"Static " + zone_name + " Protection SAM",
random.choice(RotorOpsUnits.e_zone_sams),
red_zones[zone_name].position,
@@ -190,20 +239,35 @@ class RotorOpsMission:
#Populate Blue zones with ground units
for zone_name in blue_zones:
if blue_forces["vehicles"]:
- self.addGroundGroups(blue_zones[zone_name], self.m.country('Combined Joint Task Forces Blue'), blue_forces["vehicles"],
+ self.addGroundGroups(blue_zones[zone_name], self.m.country(jtf_blue), blue_forces["vehicles"],
options["blue_quantity"])
#Add blue FARPS
if options["zone_farps"] != "farp_never" and options["defending"]:
- RotorOpsGroups.VehicleTemplate.CombinedJointTaskForcesBlue.zone_farp(self.m, self.m.country('Combined Joint Task Forces Blue'),
- self.m.country('Combined Joint Task Forces Blue'),
+ RotorOpsGroups.VehicleTemplate.CombinedJointTaskForcesBlue.zone_farp(self.m, self.m.country(jtf_blue),
+ self.m.country(jtf_blue),
blue_zones[zone_name].position,
180, zone_name + " FARP", late_activation=False)
#add logistics sites
if options["crates"] and zone_name in self.staging_zones:
- RotorOpsGroups.VehicleTemplate.CombinedJointTaskForcesBlue.logistics_site(self.m, self.m.country('Combined Joint Task Forces Blue'),
- blue_zones[zone_name].position,
- 180, zone_name)
+ # RotorOpsGroups.VehicleTemplate.CombinedJointTaskForcesBlue.logistics_site(self.m, self.m.country(jtf_blue),
+ # blue_zones[zone_name].position,
+ # 180, zone_name)
+ os.chdir(directories.imports)
+ staging_flag = self.m.find_group(zone_name)
+ if staging_flag:
+ staging_position = staging_flag.units[0].position
+ staging_heading = staging_flag.units[0].heading
+ else:
+ staging_position = blue_zones[zone_name].position
+ staging_heading = 0
+ i = ImportObjects("STAGING_LOGISTIC_HUB.miz")
+ i.anchorByGroupName("ANCHOR")
+ i.copyAll(self.m, jtf_blue, "Staging Logistics Zone",
+ staging_position, staging_heading)
+
+
+
@@ -211,7 +275,7 @@ class RotorOpsMission:
if options["zone_protect_sams"] and options["defending"]:
vg = self.m.vehicle_group(
- self.m.country('Combined Joint Task Forces Blue'),
+ self.m.country(jtf_blue),
"Static " + zone_name + " Protection SAM",
random.choice(RotorOpsUnits.e_zone_sams),
blue_zones[zone_name].position,
@@ -222,7 +286,8 @@ class RotorOpsMission:
#Add player slots
- self.addPlayerHelos(options)
+ if options["slots"] != "Locked to Scenario" and options["slots"] != "None":
+ self.addPlayerHelos(options)
#Add AI Flights
self.addFlights(options, red_forces, blue_forces)
@@ -232,14 +297,15 @@ class RotorOpsMission:
self.m.map.zoom = 100000
#add files and triggers necessary for RotorOps.lua script
- self.addResources(self.sound_directory, self.script_directory)
- self.scriptTriggerSetup(options)
+ self.addResources(directories.sound, directories.scripts)
+ RotorOpsConflict.triggerSetup(self, options)
+
#Save the mission file
- os.chdir(self.output_dir)
+ os.chdir(directories.output)
output_filename = options["scenario_filename"].removesuffix('.miz') + " " + time.strftime('%a%H%M%S') + '.miz'
success = self.m.save(output_filename)
- return {"success": success, "filename": output_filename, "directory": self.output_dir} #let the UI know the result
+ return {"success": success, "filename": output_filename, "directory": directories.output} #let the UI know the result
def addGroundGroups(self, zone, _country, groups, quantity):
for a in range(0, quantity):
@@ -282,12 +348,15 @@ class RotorOpsMission:
def getParking(self, airport, aircraft, alt_airports=None, group_size=1):
if len(airport.free_parking_slots(aircraft)) >= group_size:
- if not (aircraft.id in dcs.planes.plane_map and len(airport.runways) == 0):
+ if not (aircraft.id in dcs.planes.plane_map and (len(airport.runways) == 0 or airport.runways[0].ils is None)):
return airport
- for airport in alt_airports:
- if len(airport.free_parking_slots(aircraft)) >= group_size:
- if not (aircraft.id in dcs.planes.plane_map and len(airport.runways) == 0):
- return airport
+
+
+ if alt_airports:
+ for airport in alt_airports:
+ if len(airport.free_parking_slots(aircraft)) >= group_size:
+ if not (aircraft.id in dcs.planes.plane_map and len(airport.runways) == 0):
+ return airport
logger.warn("No parking available for " + aircraft.id)
return None
@@ -309,8 +378,8 @@ class RotorOpsMission:
for airport in red_airports:
self.m.terrain.airports[airport.name].set_blue()
- combinedJointTaskForcesBlue = self.m.country("Combined Joint Task Forces Blue")
- combinedJointTaskForcesRed = self.m.country("Combined Joint Task Forces Red")
+ combinedJointTaskForcesBlue = self.m.country(jtf_blue)
+ combinedJointTaskForcesRed = self.m.country(jtf_red)
#Swap ships
@@ -393,13 +462,13 @@ class RotorOpsMission:
client_helos = [dcs.helicopters.helicopter_map[helicopter]]
#find friendly carriers and farps
- carrier = self.m.country("Combined Joint Task Forces Blue").find_ship_group(name="HELO_CARRIER")
+ carrier = self.m.country(jtf_blue).find_ship_group(name="HELO_CARRIER")
if not carrier:
- carrier = self.m.country("Combined Joint Task Forces Blue").find_ship_group(name="HELO_CARRIER_1")
+ carrier = self.m.country(jtf_blue).find_ship_group(name="HELO_CARRIER_1")
- farp = self.m.country("Combined Joint Task Forces Blue").find_static_group("HELO_FARP")
+ farp = self.m.country(jtf_blue).find_static_group("HELO_FARP")
if not farp:
- farp = self.m.country("Combined Joint Task Forces Blue").find_static_group("HELO_FARP_1")
+ farp = self.m.country(jtf_blue).find_static_group("HELO_FARP_1")
friendly_airports, primary_f_airport = self.getCoalitionAirports("blue")
@@ -410,20 +479,20 @@ class RotorOpsMission:
for helotype in client_helos:
if carrier:
- fg = self.m.flight_group_from_unit(self.m.country('Combined Joint Task Forces Blue'), "CARRIER " + helotype.id, helotype, carrier,
+ fg = self.m.flight_group_from_unit(self.m.country(jtf_blue), "CARRIER " + helotype.id, helotype, carrier,
dcs.task.CAS, group_size=group_size)
elif farp:
- fg = self.m.flight_group_from_unit(self.m.country('Combined Joint Task Forces Blue'), "FARP " + helotype.id, helotype, farp,
+ fg = self.m.flight_group_from_unit(self.m.country(jtf_blue), "FARP " + helotype.id, helotype, farp,
dcs.task.CAS, group_size=group_size)
#invisible farps need manual unit placement for multiple units
if farp.units[0].type == 'Invisible FARP':
fg.points[0].action = dcs.point.PointAction.FromGroundArea
fg.points[0].type = "TakeOffGround"
- fg.units[0].position = fg.units[0].position.point_from_heading(heading, 30)
+ fg.units[0].position = fg.units[0].position.point_from_heading(heading, 20)
heading += 90
else:
- fg = self.m.flight_group_from_airport(self.m.country('Combined Joint Task Forces Blue'), primary_f_airport.name + " " + helotype.id, helotype,
+ fg = self.m.flight_group_from_airport(self.m.country(jtf_blue), primary_f_airport.name + " " + helotype.id, helotype,
self.getParking(primary_f_airport, helotype), group_size=group_size)
fg.units[0].set_client()
fg.load_task_default_loadout(dcs.task.CAS)
@@ -445,12 +514,12 @@ class RotorOpsMission:
return dcs.mapping.Point(x1, y1), heading, race_dist
@staticmethod
- def perpRacetrack(enemy_heading, friendly_pt):
+ def perpRacetrack(enemy_heading, friendly_pt, terrain):
heading = enemy_heading + random.randrange(70,110)
race_dist = random.randrange(40 * 1000, 80 * 1000)
center_pt = dcs.mapping.point_from_heading(friendly_pt.x, friendly_pt.y, enemy_heading - random.randrange(140, 220), 10000)
pt1 = dcs.mapping.point_from_heading(center_pt[0], center_pt[1], enemy_heading - 90, random.randrange(20 * 1000, 40 * 1000))
- return dcs.mapping.Point(pt1[0], pt1[1]), heading, race_dist
+ return dcs.mapping.Point(pt1[0], pt1[1], terrain), heading, race_dist
def addFlights(self, options, red_forces, blue_forces):
combinedJointTaskForcesBlue = self.m.country(dcs.countries.CombinedJointTaskForcesBlue.name)
@@ -459,13 +528,13 @@ class RotorOpsMission:
enemy_airports, primary_e_airport = self.getCoalitionAirports("red")
#find enemy carriers and farps
- carrier = self.m.country("Combined Joint Task Forces Red").find_ship_group(name="HELO_CARRIER")
+ carrier = self.m.country(jtf_red).find_ship_group(name="HELO_CARRIER")
if not carrier:
- carrier = self.m.country("Combined Joint Task Forces Red").find_ship_group(name="HELO_CARRIER_1")
+ carrier = self.m.country(jtf_red).find_ship_group(name="HELO_CARRIER_1")
- farp = self.m.country("Combined Joint Task Forces Red").find_static_group("HELO_FARP")
+ farp = self.m.country(jtf_red).find_static_group("HELO_FARP")
if not farp:
- farp = self.m.country("Combined Joint Task Forces Red").find_static_group("HELO_FARP_1")
+ farp = self.m.country(jtf_red).find_static_group("HELO_FARP_1")
e_airport_heading = dcs.mapping.heading_between_points(
friendly_airports[0].position.x, friendly_airports[0].position.y, enemy_airports[0].position.x, primary_e_airport.position.y
@@ -480,7 +549,7 @@ class RotorOpsMission:
awacs_name = "AWACS"
awacs_freq = 266
#pos, heading, race_dist = self.TrainingScenario.random_orbit(orbit_rect)
- pos, heading, race_dist = self.TrainingScenario.perpRacetrack(e_airport_heading, primary_f_airport.position)
+ pos, heading, race_dist = self.TrainingScenario.perpRacetrack(e_airport_heading, primary_f_airport.position, self.m.terrain)
awacs = self.m.awacs_flight(
combinedJointTaskForcesBlue,
awacs_name,
@@ -502,7 +571,7 @@ class RotorOpsMission:
awacs_escort = self.m.escort_flight(
combinedJointTaskForcesBlue, "AWACS Escort",
plane_type,
- airport=self.getParking(primary_f_airport, plane_type, friendly_airports),
+ airport=self.getParking(primary_f_airport, plane_type, friendly_airports, group_size=2),
group_to_escort=awacs,
group_size=2)
@@ -526,7 +595,7 @@ class RotorOpsMission:
t2_freq = 256
t2_tac = "101Y"
#pos, heading, race_dist = self.TrainingScenario.random_orbit(orbit_rect)
- pos, heading, race_dist = self.TrainingScenario.perpRacetrack(e_airport_heading, primary_f_airport.position)
+ pos, heading, race_dist = self.TrainingScenario.perpRacetrack(e_airport_heading, primary_f_airport.position, self.m.terrain)
refuel_net = self.m.refuel_flight(
combinedJointTaskForcesBlue,
t1_name,
@@ -542,7 +611,7 @@ class RotorOpsMission:
tacanchannel=t1_tac)
#pos, heading, race_dist = self.TrainingScenario.random_orbit(orbit_rect)
- pos, heading, race_dist = self.TrainingScenario.perpRacetrack(e_airport_heading, primary_f_airport.position)
+ pos, heading, race_dist = self.TrainingScenario.perpRacetrack(e_airport_heading, primary_f_airport.position, self.m.terrain)
refuel_rod = self.m.refuel_flight(
combinedJointTaskForcesBlue,
t2_name,
@@ -695,149 +764,33 @@ class RotorOpsMission:
unit.pylons = source_helo.pylons
unit.livery_id = source_helo.livery_id
- def scriptTriggerSetup(self, options):
-
- #get the boolean value from ui option and convert to lua string
- def lb(var):
- return str(options[var]).lower()
-
- game_flag = 100
- #Add the first trigger
- trig = dcs.triggers.TriggerOnce(comment="RotorOps Setup Scripts")
- trig.rules.append(dcs.condition.TimeAfter(1))
- trig.actions.append(dcs.action.DoScriptFile(self.scripts["mist_4_4_90.lua"]))
- trig.actions.append(dcs.action.DoScriptFile(self.scripts["Splash_Damage_2_0.lua"]))
- trig.actions.append(dcs.action.DoScriptFile(self.scripts["CTLD.lua"]))
- trig.actions.append(dcs.action.DoScriptFile(self.scripts["RotorOps.lua"]))
- script = ""
- script = ("--OPTIONS HERE!\n\n" +
- "RotorOps.CTLD_crates = " + lb("crates") + "\n\n" +
- "RotorOps.CTLD_sound_effects = true\n\n" +
- "RotorOps.force_offroad = " + lb("force_offroad") + "\n\n" +
- "RotorOps.voice_overs = " + lb("voiceovers") + "\n\n" +
- "RotorOps.zone_status_display = " + lb("game_display") + "\n\n" +
- "RotorOps.inf_spawn_messages = " + lb("inf_spawn_msgs") + "\n\n" +
- "RotorOps.inf_spawns_per_zone = " + lb("inf_spawn_qty") + "\n\n" +
- "RotorOps.apcs_spawn_infantry = " + lb("apc_spawns_inf") + " \n\n")
- if not options["smoke_pickup_zones"]:
- script = script + 'RotorOps.pickup_zone_smoke = "none"\n\n'
- trig.actions.append(dcs.action.DoScript(dcs.action.String((script))))
- self.m.triggerrules.triggers.append(trig)
-
- #Add the second trigger
- trig = dcs.triggers.TriggerOnce(comment="RotorOps Setup Zones")
- trig.rules.append(dcs.condition.TimeAfter(2))
- for s_zone in self.staging_zones:
- trig.actions.append(dcs.action.DoScript(dcs.action.String("RotorOps.stagingZone('" + s_zone + "')")))
- for c_zone in self.conflict_zones:
- zone_flag = self.conflict_zones[c_zone].flag
- trig.actions.append(dcs.action.DoScript(dcs.action.String("RotorOps.addZone('" + c_zone + "'," + str(zone_flag) + ")")))
-
- trig.actions.append(dcs.action.DoScript(dcs.action.String("RotorOps.setupConflict('" + str(game_flag) + "')")))
-
- self.m.triggerrules.triggers.append(trig)
-
- #Add the third trigger
- trig = dcs.triggers.TriggerOnce(comment="RotorOps Conflict Start")
- trig.rules.append(dcs.condition.TimeAfter(10))
- trig.actions.append(dcs.action.DoScript(dcs.action.String("RotorOps.startConflict(100)")))
- self.m.triggerrules.triggers.append(trig)
-
- #Add generic zone-based triggers
- for index, zone_name in enumerate(self.conflict_zones):
- z_active_trig = dcs.triggers.TriggerOnce(comment= zone_name + " Active")
- z_active_trig.rules.append(dcs.condition.FlagEquals(game_flag, index + 1))
- z_active_trig.actions.append(dcs.action.DoScript(dcs.action.String("--Add any action you want here!")))
- self.m.triggerrules.triggers.append(z_active_trig)
-
- #Zone protection SAMs
- if options["zone_protect_sams"]:
- for index, zone_name in enumerate(self.conflict_zones):
- z_sams_trig = dcs.triggers.TriggerOnce(comment="Deactivate " + zone_name + " SAMs")
- z_sams_trig.actions.append(dcs.action.DoScript(dcs.action.String("Group.destroy(Group.getByName('" + zone_name + " Protection SAM'))")))
- self.m.triggerrules.triggers.append(z_sams_trig)
-
- #Zone FARPS always
- if options["zone_farps"] == "farp_always" and not options["defending"]:
- for index, zone_name in enumerate(self.conflict_zones):
- if index > 0:
- previous_zone = list(self.conflict_zones)[index - 1]
- if not self.m.country("Combined Joint Task Forces Blue").find_group(previous_zone + " FARP Static"):
- continue
- z_farps_trig = dcs.triggers.TriggerOnce(comment="Activate " + previous_zone + " FARP")
- z_farps_trig.rules.append(dcs.condition.FlagEquals(game_flag, index + 1))
- z_farps_trig.actions.append(dcs.action.ActivateGroup(self.m.country("Combined Joint Task Forces Blue").find_group(previous_zone + " FARP Static").id))
- #z_farps_trig.actions.append(dcs.action.SoundToAll(str(self.res_map['forward_base_established.ogg'])))
- z_farps_trig.actions.append(dcs.action.DoScript(dcs.action.String(
- "RotorOps.farpEstablished(" + str(index) + ")")))
- self.m.triggerrules.triggers.append(z_farps_trig)
- #Zone FARPS conditional on staged units remaining
- if options["zone_farps"] == "farp_gunits" and not options["defending"]:
- for index, zone_name in enumerate(self.conflict_zones):
- if index > 0:
- previous_zone = list(self.conflict_zones)[index - 1]
- if not self.m.country("Combined Joint Task Forces Blue").find_group(previous_zone + " FARP Static"):
- continue
- z_farps_trig = dcs.triggers.TriggerOnce(comment= "Activate " + previous_zone + " FARP")
- z_farps_trig.rules.append(dcs.condition.FlagEquals(game_flag, index + 1))
- z_farps_trig.rules.append(dcs.condition.FlagIsMore(111, 20))
- z_farps_trig.actions.append(dcs.action.DoScript(dcs.action.String("--The 100 flag indicates which zone is active. The 111 flag value is the percentage of staged units remaining")))
- z_farps_trig.actions.append(
- dcs.action.ActivateGroup(self.m.country("Combined Joint Task Forces Blue").find_group(previous_zone + " FARP Static").id))
- #z_farps_trig.actions.append(dcs.action.SoundToAll(str(self.res_map['forward_base_established.ogg'])))
- z_farps_trig.actions.append(dcs.action.DoScript(dcs.action.String(
- "RotorOps.farpEstablished(" + str(index) + ")")))
- self.m.triggerrules.triggers.append(z_farps_trig)
+
+ def importObjects(self):
+ os.chdir(directories.imports)
+ logger.info("Looking for import .miz files in '" + os.getcwd())
+ for side in "red", "blue", "neutrals":
+ coalition = self.m.coalition.get(side)
+ for country_name in coalition.countries:
+ for group in self.m.country(country_name).static_group:
+ prefix = "IMPORT-"
+ if group.name.find(prefix) == 0:
+ if group.units[0].name.find('IMPORT-') == 0:
+ logger.error(
+ group.units[0].name + " IMPORT group's unit name cannot start with 'IMPORT'. Check the scenario template.")
+ raise Exception("Scenario file error: " + group.units[0].name + " IMPORT group's unit name cannot start with 'IMPORT'")
+
+ # trim the groupname to our filename convention
+ filename = group.name.removeprefix(prefix)
+ i = filename.find('-')
+ if i > 8:
+ filename = filename[0:i]
+ print(filename)
-
- #Add attack helos triggers
- for index in range(options["e_attack_helos"]):
- random_zone_obj = random.choice(list(self.conflict_zones.items()))
- zone = random_zone_obj[1]
- z_weak_trig = dcs.triggers.TriggerOnce(comment=zone.name + " Attack Helo")
- z_weak_trig.rules.append(dcs.condition.FlagIsMore(zone.flag, 1))
- z_weak_trig.rules.append(dcs.condition.FlagIsLess(zone.flag, random.randrange(20, 90)))
- z_weak_trig.actions.append(dcs.action.DoScript(dcs.action.String("---Flag " + str(zone.flag) + " value represents the percentage of defending ground units remaining in zone. ")))
- z_weak_trig.actions.append(dcs.action.DoScript(dcs.action.String("RotorOps.spawnAttackHelos()")))
- self.m.triggerrules.triggers.append(z_weak_trig)
-
- #Add attack plane triggers
- for index in range(options["e_attack_planes"]):
- random_zone_obj = random.choice(list(self.conflict_zones.items()))
- zone = random_zone_obj[1]
- z_weak_trig = dcs.triggers.TriggerOnce(comment=zone.name + " Attack Plane")
- z_weak_trig.rules.append(dcs.condition.FlagIsMore(zone.flag, 1))
- z_weak_trig.rules.append(dcs.condition.FlagIsLess(zone.flag, random.randrange(20, 90)))
- z_weak_trig.actions.append(dcs.action.DoScript(dcs.action.String("---Flag " + str(zone.flag) + " value represents the percentage of defending ground units remaining in zone. ")))
- z_weak_trig.actions.append(dcs.action.DoScript(dcs.action.String("RotorOps.spawnAttackPlanes()")))
- self.m.triggerrules.triggers.append(z_weak_trig)
-
- #Add transport helos triggers
- for index in range(options["e_transport_helos"]):
- random_zone_index = random.randrange(1, len(self.conflict_zones))
- random_zone_obj = list(self.conflict_zones.items())[random_zone_index]
- zone = random_zone_obj[1]
- z_weak_trig = dcs.triggers.TriggerOnce(comment=zone.name + " Transport Helo")
- z_weak_trig.rules.append(dcs.condition.FlagEquals(game_flag, random_zone_index + 1))
- z_weak_trig.rules.append(dcs.condition.FlagIsLess(zone.flag, random.randrange(20, 100)))
- z_weak_trig.actions.append(dcs.action.DoScript(dcs.action.String(
- "---Flag " + str(game_flag) + " value represents the index of the active zone. ")))
- z_weak_trig.actions.append(dcs.action.DoScript(dcs.action.String("---Flag " + str(zone.flag) + " value represents the percentage of defending ground units remaining in zone. ")))
- z_weak_trig.actions.append(dcs.action.DoScript(dcs.action.String("RotorOps.spawnTranspHelos(8," + str(options["transport_drop_qty"]) + ")")))
- self.m.triggerrules.triggers.append(z_weak_trig)
-
- #Add game won/lost triggers
- trig = dcs.triggers.TriggerOnce(comment="RotorOps Conflict WON")
- trig.rules.append(dcs.condition.FlagEquals(game_flag, 99))
- trig.actions.append(dcs.action.DoScript(dcs.action.String("---Add an action you want to happen when the game is WON")))
- self.m.triggerrules.triggers.append(trig)
-
- trig = dcs.triggers.TriggerOnce(comment="RotorOps Conflict LOST")
- trig.rules.append(dcs.condition.FlagEquals(game_flag, 98))
- trig.actions.append(dcs.action.DoScript(dcs.action.String("---Add an action you want to happen when the game is LOST")))
- self.m.triggerrules.triggers.append(trig)
-
+ filename = filename + ".miz"
+ i = ImportObjects(filename)
+ i.anchorByGroupName("ANCHOR")
+ new_statics, new_vehicles, new_helicopters = i.copyAll(self.m, country_name, group.units[0].name, group.units[0].position, group.units[0].heading)
diff --git a/Generator/RotorOpsUnits.py b/Generator/RotorOpsUnits.py
index 4bfda12..476639a 100644
--- a/Generator/RotorOpsUnits.py
+++ b/Generator/RotorOpsUnits.py
@@ -2,7 +2,7 @@ import dcs
client_helos = [
dcs.helicopters.UH_1H,
- dcs.helicopters.Mi_8MT,
+ dcs.helicopters.AH_64D_BLK_II,
dcs.helicopters.Mi_24P,
dcs.helicopters.Ka_50,
]
diff --git a/Generator/RotorOpsUtils.py b/Generator/RotorOpsUtils.py
index 2df38f6..d78ee69 100644
--- a/Generator/RotorOpsUtils.py
+++ b/Generator/RotorOpsUtils.py
@@ -1,5 +1,6 @@
import math
import dcs
+from MissionGenerator import logger
def getDistance(point1=dcs.Point, point2=dcs.Point):
@@ -15,3 +16,167 @@ def getDistance(point1=dcs.Point, point2=dcs.Point):
def convertMeterToNM(meters=int):
nm = meters / 1852
return nm
+
+
+#
+#
+# class ImportObjects:
+#
+# def __init__(self, mizfile, source_point=None, source_heading=0):
+# self.pad_unit = None
+# logger.info("Importing objects from " + mizfile)
+# self.source_mission = dcs.mission.Mission()
+# self.source_mission.load_file(mizfile)
+# self.source_heading = source_heading
+# if source_point:
+# self.source_point = source_point
+# else:
+# self.source_point = dcs.Point(self.source_mission.terrain.bullseye_blue["x"], self.source_mission.terrain.bullseye_blue["y"])
+#
+#
+# def anchorByGroupName(self, group_name):
+# group = self.source_mission.find_group(group_name)
+# if group:
+# self.source_point = group.units[0].position
+# self.source_heading = group.units[0].heading
+# else:
+# logger.warning("Unable to find group for anchor.")
+#
+#
+# def copyTo(self, mission, dest_country_name, dest_name, dest_point=None, dest_heading=0):
+# logger.info("Copying objects as " + dest_name)
+#
+# if not dest_point:
+# dest_point = dcs.Point(mission.terrain.bullseye_blue["x"], mission.terrain.bullseye_blue["y"])
+#
+# for side in "red", "blue", "neutrals":
+# coalition = self.source_mission.coalition.get(side)
+# for country_name in coalition.countries:
+#
+# group_types = [coalition.countries[country_name].static_group, coalition.countries[country_name].vehicle_group, coalition.countries[country_name].helicopter_group, coalition.countries[country_name].plane_group,
+# coalition.countries[country_name].ship_group]
+#
+#
+#
+# for index, group_type in enumerate(group_types):
+# for group in group_type:
+# self.groupToPoint(group, self.source_point, dest_point, self.source_heading, dest_heading)
+#
+# # add the country to the destination mission if it doesn't exist already
+# # if not mission.country(country_name):
+# # print(country_name + " not found in destination mission")
+# # for index, c in enumerate(dcs.countries.country_dict):
+# # if dcs.countries.country_dict[c].name == country_name:
+# #
+# # mission.coalition.get(side).add_country(dcs.countries.country_dict[c]())
+# # print(country_name + " added to " + side)
+#
+#
+#
+#
+# if index == 0: # Statics
+# type_name = group.units[0].type
+# type_maps = [dcs.statics.cargo_map, dcs.statics.warehouse_map, dcs.statics.groundobject_map, dcs.statics.fortification_map]
+# classed = False
+# for type_map in type_maps:
+# if type_name in type_map:
+# classed = True
+# unit_type = type_map[type_name]
+# ng = mission.static_group(mission.country(dest_country_name),
+# group.name,
+# unit_type,
+# group.units[0].position,
+# group.units[0].heading,
+# hidden=False)
+#
+#
+#
+# if not classed:
+# print("No pydcs class for " + type_name)
+#
+#
+# class temp(dcs.unittype.StaticType):
+# id = group.units[0].type
+# name = group.units[0].name
+# shape_name = group.units[0].shape_name
+# rate = group.units[0].rate
+#
+#
+# ng = mission.static_group(mission.country(dest_country_name),
+# group.name,
+# temp,
+# group.units[0].position,
+# group.units[0].heading,
+# hidden=False)
+#
+# if ng.units[0].type == "Invisible FARP":
+# self.pad_unit = ng
+#
+# elif index == 1: # Vehicles
+#
+# for i, unit in enumerate(group.units):
+# if i == 0:
+# ng = mission.vehicle_group(mission.country(dest_country_name),
+# group.name,
+# dcs.vehicles.vehicle_map[group.units[0].type],
+# group.units[0].position,
+# group.units[0].heading)
+#
+#
+# else:
+#
+# u = mission.vehicle(group.units[i].name, dcs.vehicles.vehicle_map[group.units[i].type])
+# u.position = group.units[i].position
+# u.heading = group.units[i].heading
+# ng.add_unit(u)
+#
+# mission.country(dest_country_name).add_vehicle_group(ng)
+#
+#
+# elif index == 2: # Helicopters
+# if self.pad_unit:
+# if group.units[0].skill == dcs.unit.Skill.Client or group.units[0].skill == dcs.unit.Skill.Player:
+#
+# # we'll create a new FARP for each helicopter. we've tried adding the flight group to an existing FARP, but they stack on top of each other
+# # trying to move the units into position after adding the flight group moves the 2D graphic of the helicopter, but the unit marker remains stacked on top
+# # of the unit marker in ME
+# # farp = mission.country(country_name).find_group(self.pad_unit.name)
+#
+# farp = mission.farp(mission.country(country_name), dest_name + " " + group.name + " Pad", group.units[0].position, hidden=True, dead=False,
+# farp_type=dcs.unit.InvisibleFARP)
+#
+#
+#
+# ng = mission.flight_group_from_unit(mission.country(dest_country_name),
+# dest_name + " " + group.name,
+# dcs.helicopters.helicopter_map[group.units[0].type],
+# farp,
+# group_size=1)
+#
+# ng.points[0].action = dcs.point.PointAction.FromGroundArea
+# ng.points[0].type = "TakeOffGround"
+# ng.units[0].heading = group.units[0].heading
+# ng.units[0].skill = group.units[0].skill
+# ng.units[0].livery_id = group.units[0].livery_id
+# ng.units[0].pylons = group.units[0].pylons
+# else:
+# logger.warn("No pad unit (ie FARP, carrier) found, so can't add helicopters.")
+#
+# elif index == 3:
+# #mission.country(country).add_plane_group(group)
+# print("not yet avail")
+# elif index == 4:
+# #mission.country(country).add_ship_group(group)
+# print("not yet avail")
+#
+#
+# @staticmethod
+# def groupToPoint(group, src_point, dest_point, src_heading=0, dest_heading=0):
+# for unit in group.units:
+# heading_to_unit = dcs.mapping.heading_between_points(src_point.x, src_point.y, unit.position.x,
+# unit.position.y)
+# new_heading_to_unit = dest_heading + heading_to_unit
+# unit_distance = src_point.distance_to_point(unit.position)
+# unit.position = dest_point.point_from_heading(new_heading_to_unit, unit_distance)
+# unit.heading = unit.heading + dest_heading
+# return group
\ No newline at end of file
diff --git a/Generator/Scenarios/Caucasus Conflict - Batumi to Kobuleti (GRIMM).miz b/Generator/Scenarios/Caucasus Conflict - Batumi to Kobuleti (GRIMM).miz
index e3734cf..d653c7c 100644
Binary files a/Generator/Scenarios/Caucasus Conflict - Batumi to Kobuleti (GRIMM).miz and b/Generator/Scenarios/Caucasus Conflict - Batumi to Kobuleti (GRIMM).miz differ
diff --git a/Generator/Scenarios/Mariana Conflict - Anderson to Won Pat (GRIMM).miz b/Generator/Scenarios/Mariana Conflict - Anderson to Won Pat (GRIMM).miz
index 8f0b814..45bbf50 100644
Binary files a/Generator/Scenarios/Mariana Conflict - Anderson to Won Pat (GRIMM).miz and b/Generator/Scenarios/Mariana Conflict - Anderson to Won Pat (GRIMM).miz differ
diff --git a/Generator/Scenarios/Mariana Conflict - Rota Landing (Mr Nobody).miz b/Generator/Scenarios/Mariana Conflict - Rota Landing (Mr Nobody).miz
index 8d032c6..7f0c263 100644
Binary files a/Generator/Scenarios/Mariana Conflict - Rota Landing (Mr Nobody).miz and b/Generator/Scenarios/Mariana Conflict - Rota Landing (Mr Nobody).miz differ
diff --git a/Generator/Scenarios/PG Conflict - Dubai Tour (GRIMM).miz b/Generator/Scenarios/PG Conflict - Dubai Tour (GRIMM).miz
index 39550a1..440ef08 100644
Binary files a/Generator/Scenarios/PG Conflict - Dubai Tour (GRIMM).miz and b/Generator/Scenarios/PG Conflict - Dubai Tour (GRIMM).miz differ
diff --git a/Generator/Scenarios/PG Conflict - Musandam Valley (Mr Nobody).miz b/Generator/Scenarios/PG Conflict - Musandam Valley (Mr Nobody).miz
new file mode 100644
index 0000000..9ca861a
Binary files /dev/null and b/Generator/Scenarios/PG Conflict - Musandam Valley (Mr Nobody).miz differ
diff --git a/Generator/Scenarios/Syria Conflict - Aleppo Tour (GRIMM).miz b/Generator/Scenarios/Syria Conflict - Aleppo Tour (GRIMM).miz
index 4de763d..5a53187 100644
Binary files a/Generator/Scenarios/Syria Conflict - Aleppo Tour (GRIMM).miz and b/Generator/Scenarios/Syria Conflict - Aleppo Tour (GRIMM).miz differ
diff --git a/Generator/Scenarios/Syria Conflict - Mount Olympus (Mr Nobody).miz b/Generator/Scenarios/Syria Conflict - Mount Olympus (Mr Nobody).miz
new file mode 100644
index 0000000..861b46d
Binary files /dev/null and b/Generator/Scenarios/Syria Conflict - Mount Olympus (Mr Nobody).miz differ
diff --git a/Generator/Scenarios/_How to create your own scenarios.txt b/Generator/Scenarios/_How to create your own scenarios.txt
index fa6dbc3..c06af51 100644
--- a/Generator/Scenarios/_How to create your own scenarios.txt
+++ b/Generator/Scenarios/_How to create your own scenarios.txt
@@ -39,3 +39,8 @@ Tips:
-In "Defense" or with "Swap sides" option, USA and Russia ships, helicopters, planes, and ground units will swap countries. Static objects may not. Test it out.
-Turn off or limit civilian road traffic.
-Pay attention to rivers and bridges, as a far away bridge crossing may break routing if it's the only way across. See the testing notes above.
+
+v0.6:
+You can now control the FARP spawning location and heading by adding a static object (such as a mark flag) with group name 'ALPHA' etc. Same for staging area logistics site..but the group name should be 'STAGING'. Change the object heading to better align with roads or map objects. The flags for these must be CJTFB country.
+
+You can dynamically insert complex arangements of objects such as large bases with multiplayer spawns. See the 'Imports' folder for more details.
diff --git a/Generator/assets/frameless.qss b/Generator/assets/frameless.qss
new file mode 100644
index 0000000..0c29c8e
--- /dev/null
+++ b/Generator/assets/frameless.qss
@@ -0,0 +1,61 @@
+#windowFrame {
+ border-radius: 5px 5px 5px 5px;
+ background-color: palette(Window);
+}
+
+#titleBar {
+ border: 0px none palette(base);
+ border-top-left-radius: 5px;
+ border-top-right-radius: 5px;
+ background-color: palette(Window);
+ height: 24px;
+}
+
+#btnClose, #btnRestore, #btnMaximize, #btnMinimize {
+ min-width: 14px;
+ min-height: 14px;
+ max-width: 14px;
+ max-height: 14px;
+ border-radius: 7px;
+ margin: 4px;
+}
+
+#btnRestore, #btnMaximize {
+ background-color: hsv(123, 204, 198);
+}
+
+#btnRestore::hover, #btnMaximize::hover {
+ background-color: hsv(123, 204, 148);
+}
+
+#btnRestore::pressed, #btnMaximize::pressed {
+ background-color: hsv(123, 204, 98);
+}
+
+#btnMinimize {
+ background-color: hsv(38, 218, 253);
+}
+
+#btnMinimize::hover {
+ background-color: hsv(38, 218, 203);
+}
+
+#btnMinimize::pressed {
+ background-color: hsv(38, 218, 153);
+}
+
+#btnClose {
+ background-color: hsv(0, 182, 252);
+}
+
+#btnClose::hover {
+ background-color: hsv(0, 182, 202);
+}
+
+#btnClose::pressed {
+ background-color: hsv(0, 182, 152);
+}
+
+#btnClose::disabled, #btnRestore::disabled, #btnMaximize::disabled, #btnMinimize::disabled {
+ background-color: palette(midlight);
+}
diff --git a/Generator/assets/rotorops-dkgray.png b/Generator/assets/rotorops-dkgray.png
new file mode 100644
index 0000000..c3eb5ba
Binary files /dev/null and b/Generator/assets/rotorops-dkgray.png differ
diff --git a/Generator/assets/style.qss b/Generator/assets/style.qss
new file mode 100644
index 0000000..f343afa
--- /dev/null
+++ b/Generator/assets/style.qss
@@ -0,0 +1,148 @@
+/*
+ * QGroupBox
+ */
+
+QGroupBox {
+ background-color: palette(alternate-base);
+ border: 1px solid palette(midlight);
+ margin-top: 25px;
+}
+
+QGroupBox::title {
+ background-color: transparent;
+}
+
+/*
+ * QToolBar
+ */
+
+QToolBar {
+ border: none;
+}
+
+/*
+ * QTabBar
+ */
+
+QTabBar{
+ background-color: transparent;
+}
+
+QTabBar::tab{
+ padding: 4px 6px;
+ background-color: transparent;
+ border-bottom: 2px solid transparent;
+}
+
+QTabBar::tab:selected, QTabBar::tab:hover {
+ color: palette(text);
+ border-bottom: 2px solid palette(highlight);
+}
+
+QTabBar::tab:selected:disabled {
+ border-bottom: 2px solid palette(light);
+}
+
+/*
+ * QScrollBar
+ */
+
+QScrollBar:vertical {
+ background: palette(base);
+ border-top-right-radius: 2px;
+ border-bottom-right-radius: 2px;
+ width: 16px;
+ margin: 0px;
+}
+
+QScrollBar::handle:vertical {
+ background-color: palette(midlight);
+ border-radius: 2px;
+ min-height: 20px;
+ margin: 2px 4px 2px 4px;
+}
+
+QScrollBar::handle:vertical:hover, QScrollBar::handle:horizontal:hover, QScrollBar::handle:vertical:pressed, QScrollBar::handle:horizontal:pressed {
+ background-color:palette(highlight);
+}
+
+QScrollBar::add-line:vertical {
+ background: none;
+ height: 0px;
+ subcontrol-position: right;
+ subcontrol-origin: margin;
+}
+
+QScrollBar::sub-line:vertical {
+ background: none;
+ height: 0px;
+ subcontrol-position: left;
+ subcontrol-origin: margin;
+}
+
+QScrollBar:horizontal{
+ background: palette(base);
+ height: 16px;
+ margin: 0px;
+}
+
+QScrollBar::handle:horizontal {
+ background-color: palette(midlight);
+ border-radius: 2px;
+ min-width: 20px;
+ margin: 4px 2px 4px 2px;
+}
+
+
+QScrollBar::add-line:horizontal {
+ background: none;
+ width: 0px;
+ subcontrol-position: bottom;
+ subcontrol-origin: margin;
+}
+
+QScrollBar::sub-line:horizontal {
+ background: none;
+ width: 0px;
+ subcontrol-position: top;
+ subcontrol-origin: margin;
+}
+
+/*
+ * QScrollArea
+ */
+
+QScrollArea {
+ border-style: none;
+}
+
+QScrollArea > QWidget > QWidget {
+ background-color: palette(alternate-base);
+}
+
+/*
+ * QSlider
+ */
+
+QSlider::handle:horizontal {
+ border-radius: 5px;
+ background-color: palette(light);
+ max-height: 20px;
+}
+
+QSlider::add-page:horizontal {
+ background: palette(base);
+}
+
+QSlider::sub-page:horizontal {
+ background: palette(highlight);
+}
+
+QSlider::sub-page:horizontal:disabled {
+ background-color: palette(light);
+}
+
+QTableView {
+ background-color: palette(link-visited);
+ alternate-background-color: palette(midlight);
+}
diff --git a/Generator/requirements.txt b/Generator/requirements.txt
index dfe21b6..51314dd 100644
Binary files a/Generator/requirements.txt and b/Generator/requirements.txt differ
diff --git a/MissionGenerator.exe b/MissionGenerator.exe
index 0f309bf..2dd91ab 100644
Binary files a/MissionGenerator.exe and b/MissionGenerator.exe differ
diff --git a/README.md b/README.md
index efb79ef..8c05ced 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,5 @@
+
+
# What is RotorOps?
RotorOps is a mission generator and gameplay script for DCS: World. At its heart is a game type called Conflict, which requires helicopter operations to win battles on the ground. This is a territory-capture game that promotes focus on individual 'conflict zones'.
@@ -5,6 +7,7 @@ At the core of the RotorOps script are AI enhancements that provide a dynamic gr

+
# Key Features:
- Unique helicopter-focused gameplay.
@@ -58,26 +61,21 @@ Easily add your own templates for friendly/enemy ground units directly in the DC
Create your own scenarios for the RotorOps mission generator, using the DCS mission editor.
+***
+
+
+### Developers
+We welcome contributors to this new project! Please get in touch on Discord with new ideas or pickup/create an issue in this repo.
+
+
### RotorOps Mission Creator Guide:
For more detailed information on how the script works, see this wiki:
https://github.com/spencershepard/RotorOps/wiki/RotorOps:-Mission-Creator-Guide
***
-### Get in touch!
+### Thanks to
-https://discord.gg/HFqjrZV9xD
-
-### Support Development
-
-https://www.patreon.com/spencershepard
-
-### Developers
-We welcome contributors to this new project! Please get in touch on Discord with new ideas or pickup/create an issue in this repo.
-
-
-***
-### Much thanks to
RotorOps uses MIST and integrates CTLD:
https://github.com/mrSkortch/MissionScriptingTools
@@ -87,3 +85,17 @@ https://github.com/ciribob/DCS-CTLD
The mission generator would not be possible without PyDCS:
https://github.com/pydcs/dcs
+
+### Thanks to contributors
+
+Shagrat: For amazing templates and testing for our FARPs, FOBs, and other mission assets.
+
+Mr. Nobody: For awesome scenario and forces templates and helping to indroduce the DCS world to RotorOps.
+
+***
+
+# Join our Discord!
+
+Chat about anything RotorOps or join up to fly!
+
+https://discord.gg/HFqjrZV9xD
diff --git a/RotorOps.lua b/RotorOps.lua
index 6e2b9a2..4546b9c 100644
--- a/RotorOps.lua
+++ b/RotorOps.lua
@@ -1,5 +1,5 @@
RotorOps = {}
-RotorOps.version = "1.2.6"
+RotorOps.version = "1.2.7"
local debug = true
@@ -345,6 +345,7 @@ end
function RotorOps.isUnitInZone(unit, zone_name)
local zone = trigger.misc.getZone(zone_name)
local distance = getDistance(unit:getPoint(), zone.point)
+ --local distance = mist.utils.get2DDist(unit:getPoint(), zone.point)
if distance <= zone.radius then
return true
else
@@ -746,11 +747,25 @@ function RotorOps.aiExecute(vars)
-- if vars.zone then zone = vars.zone end
- if Group.isExist(Group.getByName(group_name)) ~= true or #Group.getByName(group_name):getUnits() < 1 then
+--error after Apache update
+-- if Group.isExist(Group.getByName(group_name)) ~= true or #Group.getByName(group_name):getUnits() < 1 then
+-- debugMsg(group_name.." no longer exists")
+-- RotorOps.ai_tasks[group_name] = nil
+-- return
+-- end
+
+ if Group.getByName(group_name) then
+ if Group.isExist(Group.getByName(group_name)) ~= true or #Group.getByName(group_name):getUnits() < 1 then
+ debugMsg(group_name.." no longer exists")
+ RotorOps.ai_tasks[group_name] = nil
+ return
+ end
+ else
debugMsg(group_name.." no longer exists")
RotorOps.ai_tasks[group_name] = nil
- return
- end
+ end
+
+
local same_zone = false
if zone ~= nil then
@@ -1497,3 +1512,46 @@ function RotorOps.spawnTranspHelos(troops, max_drops)
end
+
+--- USEFUL PUBLIC 'LUA PREDICATE' FUNCTIONS FOR MISSION EDITOR TRIGGERS (don't forget that DCS lua predicate functions should 'return' these function calls)
+
+--determine if any human players are above a defined ceiling above ground level. If 'above' parameter is false, function will return true if no players above ceiling
+function RotorOps.predPlayerMaxAGL(max_agl, above)
+ local players_above_ceiling = 0
+
+ for uName, uData in pairs(mist.DBs.humansByName) do
+ local player_unit = Unit.getByName(uData.unitName)
+ if player_unit then
+ local player_pos = player_unit:getPosition().p
+ local terrain_height = land.getHeight({x = player_pos.x, y = player_pos.z})
+ local player_agl = player_pos.y - terrain_height
+ if player_agl > max_agl then
+ players_above_ceiling = players_above_ceiling + 1
+ end
+ end
+ end
+
+ if players_above_ceiling > 0 then
+ return above
+ else
+ return not above
+ end
+
+end
+
+--determine if any human players are in a zone
+function RotorOps.predPlayerInZone(zone_name)
+ local players_in_zone = 0
+ for uName, uData in pairs(mist.DBs.humansByName) do
+ local player_unit = Unit.getByName(uData.unitName)
+ if player_unit and RotorOps.isUnitInZone(player_unit, zone_name) then
+ players_in_zone = players_in_zone + 1
+ end
+ end
+ if players_in_zone > 0 then
+ return true
+ else
+ return false
+ end
+end
+
diff --git a/mist_4_5_107_grimm.lua b/mist_4_5_107_grimm.lua
new file mode 100644
index 0000000..2c9c0b7
--- /dev/null
+++ b/mist_4_5_107_grimm.lua
@@ -0,0 +1,9084 @@
+--[[--
+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 and unit:isExist() 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 and unit:isExist() 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