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 0 0 - 1209 + 1280 26 + + + Map Filter + + + + + + + + + + + Gametype Filter + + + + + + + + + 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