diff --git a/Generator/Forces/red/RED Default Armor, Infantry & Artillery (MED).miz b/Generator/Forces/red/RED Default Armor, Infantry & Artillery (MED).miz
index b2eec21..f7a5175 100644
Binary files a/Generator/Forces/red/RED Default Armor, Infantry & Artillery (MED).miz and b/Generator/Forces/red/RED Default Armor, Infantry & Artillery (MED).miz differ
diff --git a/Generator/MissionGenerator.py b/Generator/MissionGenerator.py
index 42bb74d..4d4b30c 100644
--- a/Generator/MissionGenerator.py
+++ b/Generator/MissionGenerator.py
@@ -2,18 +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__)
@@ -21,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)
@@ -36,8 +68,8 @@ def handle_exception(exc_type, exc_value, exc_traceback):
sys.excepthook = handle_exception
-maj_version = 0
-minor_version = 6
+maj_version = 1
+minor_version = 1
version_string = str(maj_version) + "." + str(minor_version)
scenarios = []
red_forces_files = []
@@ -55,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')
@@ -69,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;}")
@@ -82,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)
@@ -95,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)
@@ -113,15 +147,16 @@ class Window(QMainWindow, Ui_MainWindow):
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)
@@ -165,7 +200,7 @@ class Window(QMainWindow, Ui_MainWindow):
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)
@@ -228,12 +263,22 @@ class Window(QMainWindow, Ui_MainWindow):
+ source_mission.description_text()
)
+ 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,
@@ -258,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:")
@@ -274,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" +
@@ -294,13 +339,74 @@ 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 loadOnlineContent(self):
+ url = user_files_url + 'directory.yaml'
+ r = requests.get(url, allow_redirects=False)
+ user_files = yaml.safe_load(r.content)
+ count = 0
+ #todo: try/catch/fail here
+
+ # 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()
+
+
+ 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..caee74d 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(True)
+ 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 31aecfe..19c853e 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,200 +961,121 @@ p, li { white-space: pre-wrap; }
false
-
-
- true
-
+
- 60
- 90
- 181
- 31
-
-
-
-
- 11
-
-
-
- Defensive Mode
-
-
- true
-
-
-
-
-
- 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
@@ -806,14 +1092,15 @@ p, li { white-space: pre-wrap; }
- 950
- 530
- 221
- 21
+ 510
+ 509
+ 261
+ 24
+ Arial
9
@@ -830,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);
+
@@ -1075,6 +1279,71 @@ p, li { white-space: pre-wrap; }
_defensiveModeChanged
+
+
+ _nextScenario
+
+
+
+
+ _prevScenario
+
+
+
+
+ Caucasus
+
+
+
+
+ Persian Gulf
+
+
+
+
+ Marianas
+
+
+
+
+ Nevada
+
+
+
+
+ Syria
+
+
+
+
+ true
+
+
+ true
+
+
+ All
+
+
+
+
+ true
+
+
+ Multiplayer
+
+
+
+
+ true
+
+
+ true
+
+
+ All
+
+
@@ -1085,8 +1354,8 @@ p, li { white-space: pre-wrap; }
trigger()
- 993
- 591
+ 1030
+ 616
-1
@@ -1117,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
index 2b83529..1cd8c18 100644
--- a/Generator/RotorOpsConflict.py
+++ b/Generator/RotorOpsConflict.py
@@ -14,7 +14,8 @@ def triggerSetup(rops, options):
# 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_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"]))
diff --git a/Generator/RotorOpsImport.py b/Generator/RotorOpsImport.py
index 91c9060..af8aadb 100644
--- a/Generator/RotorOpsImport.py
+++ b/Generator/RotorOpsImport.py
@@ -5,16 +5,13 @@ from MissionGenerator import logger
class ImportObjects:
- def __init__(self, mizfile, source_point=None, source_heading=0):
+ 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 = 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"])
+ self.source_heading = None
+ self.source_point = None
self.statics = []
self.vehicles = []
self.helicopters = []
diff --git a/Generator/RotorOpsMission.py b/Generator/RotorOpsMission.py
index d5bf153..c00b536 100644
--- a/Generator/RotorOpsMission.py
+++ b/Generator/RotorOpsMission.py
@@ -4,6 +4,7 @@ import dcs
import os
import random
+
import RotorOpsGroups
import RotorOpsUnits
import RotorOpsUtils
@@ -11,6 +12,7 @@ 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"
@@ -19,15 +21,15 @@ 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"
- self.imports_dir = self.home_dir + "\Generator\Imports"
+ # 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 = {}
@@ -89,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()
@@ -124,9 +126,11 @@ 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"])
self.importObjects()
@@ -135,7 +139,7 @@ class RotorOpsMission:
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 " + self.scenarios_dir
+ 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")
@@ -147,8 +151,8 @@ class RotorOpsMission:
# 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
@@ -207,7 +211,7 @@ class RotorOpsMission:
hidden=False, dead=False,
farp_type=dcs.unit.InvisibleFARP)
- os.chdir(self.imports_dir)
+ os.chdir(directories.imports)
if self.config and self.config["zone_farp_file"]:
filename = self.config["zone_farp_file"]
else:
@@ -249,7 +253,7 @@ class RotorOpsMission:
# RotorOpsGroups.VehicleTemplate.CombinedJointTaskForcesBlue.logistics_site(self.m, self.m.country(jtf_blue),
# blue_zones[zone_name].position,
# 180, zone_name)
- os.chdir(self.imports_dir)
+ os.chdir(directories.imports)
staging_flag = self.m.find_group(zone_name)
if staging_flag:
staging_position = staging_flag.units[0].position
@@ -293,15 +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.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):
@@ -344,8 +348,10 @@ 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
+
+
if alt_airports:
for airport in alt_airports:
if len(airport.free_parking_slots(aircraft)) >= group_size:
@@ -483,7 +489,7 @@ class RotorOpsMission:
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(jtf_blue), primary_f_airport.name + " " + helotype.id, helotype,
@@ -508,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)
@@ -543,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,
@@ -589,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,
@@ -605,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,
@@ -763,7 +769,7 @@ class RotorOpsMission:
def importObjects(self):
- os.chdir(self.imports_dir)
+ os.chdir(directories.imports)
logger.info("Looking for import .miz files in '" + os.getcwd())
for side in "red", "blue", "neutrals":
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/Scenarios/Mariana Conflict - Rota Landing (Mr Nobody).miz b/Generator/Scenarios/Mariana Conflict - Rota Landing (Mr Nobody).miz
index 5f411b0..337d415 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/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/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 d75205a..33082c2 100644
Binary files a/MissionGenerator.exe and b/MissionGenerator.exe differ
diff --git a/RotorOps.lua b/RotorOps.lua
index f76174c..4546b9c 100644
--- a/RotorOps.lua
+++ b/RotorOps.lua
@@ -747,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
@@ -1499,9 +1513,9 @@ function RotorOps.spawnTranspHelos(troops, max_drops)
end
---- USEFUL PUBLIC 'LUA PREDICATE' FUNCTIONS FOR MISSION EDITOR TRIGGERS
+--- 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 players are above a defined ceiling above ground level. If 'above' parameter is false, function will return true if no players above ceiling
+--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
@@ -1525,7 +1539,7 @@ function RotorOps.predPlayerMaxAGL(max_agl, above)
end
---determine if any players are in a zone (not currently working)
+--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
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