diff --git a/.change-monitored b/.change-monitored
new file mode 100644
index 0000000..a003611
--- /dev/null
+++ b/.change-monitored
@@ -0,0 +1,4 @@
+/assets/*
+/scripts/*
+/sound/*
+MissionGenerator.exe
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index c6ca7b9..17a1e0a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,7 +8,6 @@ Generator/build
Generator/venv
Generator/dist
Generator/generator.log
-MissionGenerator.exe
incoming templates/
Generator/utils/extract units/source.miz
Generator/utils/extract units/units.txt
@@ -20,9 +19,10 @@ templates/Imports/downloaded
templates/Forces/user
templates/Forces/downloaded
config/user-data.yaml
-*.exe
config/user-data.yaml
server/user-files/modules/mapscript-sql.py
distribution/
MissionOutput/
-
+.idea
+*.env
+RotorOps_setup.exe
diff --git a/Generator/MissionGenerator.py b/Generator/MissionGenerator.py
index 89ab13e..92cb22e 100644
--- a/Generator/MissionGenerator.py
+++ b/Generator/MissionGenerator.py
@@ -2,15 +2,16 @@ import json
import yaml
import sys
import os
-import operator
import RotorOpsMission as ROps
import RotorOpsUnits
+import version
import user
import logging
import requests
-from packaging import version
+from packaging import version as ver
+
from PyQt5.QtWidgets import (
QApplication, QDialog, QMainWindow, QMessageBox, QCheckBox, QSpinBox, QSplashScreen, QFileDialog, QRadioButton,
@@ -19,7 +20,6 @@ from PyQt5.QtWidgets import (
from PyQt5 import QtGui
from PyQt5.QtGui import QPixmap, QFont
from PyQt5.QtCore import QObject, QEvent, Qt, QUrl
-from PyQt5.QtWebEngineWidgets import QWebEngineView
import resources # pyqt resource file
from MissionGeneratorUI import Ui_MainWindow
@@ -27,20 +27,15 @@ from MissionGeneratorUI import Ui_MainWindow
import qtmodern.styles
import qtmodern.windows
-# UPDATE BUILD VERSION
-maj_version = 1
-minor_version = 2
-patch_version = 0
modules_version = 2
modules_url = 'https://dcs-helicopters.com/user-files/modules/'
-version_url = 'https://dcs-helicopters.com/app-updates/versions.yaml'
modules_map_url = 'https://dcs-helicopters.com/user-files/modules/module-map-v2.yaml'
ratings_url = 'https://dcs-helicopters.com/user-files/ratings.php'
allowed_paths = ['templates\\Scenarios\\downloaded', 'templates\\Forces\\downloaded', 'templates\\Imports\\downloaded']
-user_files_url = 'https://dcs-helicopters.com/user-files/'
-version_url = 'https://dcs-helicopters.com/app-updates/versioncheck.yaml'
+version.version_url = 'https://dcs-helicopters.com/app-updates/versioncheck.yaml'
+
#Setup logfile and exception handler
logger = logging.getLogger(__name__)
@@ -102,17 +97,16 @@ def handle_exception(exc_type, exc_value, exc_traceback):
sys.excepthook = handle_exception
-version_string = str(maj_version) + "." + str(minor_version) + "." + str(patch_version)
defenders_text = "Defending Forces:"
attackers_text = "Attacking Forces:"
ratings_json = None
-logger.info("RotorOps v" + version_string)
+logger.info("RotorOps v" + version.version_string)
# Try to set windows app ID to display taskbar icon properly
try:
from ctypes import windll
- appid = 'RotorOps.MissionGenerator.' + version_string
+ appid = 'RotorOps.MissionGenerator.' + version.version_string
windll.shell32.SetCurrentProcessExplicitAppUserModelID(appid)
except ImportError:
pass
@@ -156,7 +150,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.statusbar = self.statusBar()
self.statusbar.setStyleSheet(
"QStatusBar{padding-left:5px;}")
- self.version_label.setText("Version " + version_string)
+ self.version_label.setText("Version " + version.version_string)
self.scenarioChanged()
@@ -421,6 +415,12 @@ class Window(QMainWindow, Ui_MainWindow):
if qobj:
qobj.setValue(config['spinboxes'][box])
+ for box in QObject.findChildren(self, QSpinBox):
+ if 'disable_spinboxes' in config and config['disable_spinboxes'] is not None and box.objectName() in config['disable_spinboxes']:
+ box.setEnabled(False)
+ else:
+ box.setEnabled(True)
+
for button in QObject.findChildren(self, QRadioButton):
if 'radiobuttons' in config and button.objectName() in config['radiobuttons']:
button.setChecked(True)
@@ -570,7 +570,6 @@ class Window(QMainWindow, Ui_MainWindow):
"game_display": self.game_status_checkBox.isChecked(),
"defending": self.defense_checkBox.isChecked(),
"slots": self.slot_template_comboBox.currentText(),
- "zone_protect_sams": self.zone_sams_checkBox.isChecked(),
"zone_farps": self.farp_buttonGroup.checkedButton().objectName(),
"e_transport_helos": self.e_transport_helos_spinBox.value(),
"transport_drop_qty": self.troop_drop_spinBox.value(),
@@ -588,6 +587,10 @@ class Window(QMainWindow, Ui_MainWindow):
"logistics_farp_file": self.scenario.getConfigValue("logistics_farp_file", default=None),
"zone_protect_file": self.scenario.getConfigValue("zone_protect_file", default=None),
"script": self.scenario.getConfigValue("script", default=None),
+ "advanced_defenses": self.advanced_defenses_checkBox.isChecked(),
+ "red_cap": self.scenario.getConfigValue("red_cap", default=True),
+ "blue_cap": self.scenario.getConfigValue("blue_cap", default=True),
+ "rotorops_server": self.scenario.getConfigValue("rotorops_server", default=False),
}
logger.info("Generating mission with options:")
@@ -606,11 +609,9 @@ class Window(QMainWindow, Ui_MainWindow):
msg.setText("Awesome, your mission is ready! It's located in this directory: \n" +
result["directory"] + "\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" +
+ "You MUST use the DCS Mission Editor to open the mission, or else it may not work correctly. Save the mission or launch it directly from the editor.\n" +
"\n" +
- "There are no hidden script changes, everything is visible in the ME. Triggers have been created to help you to add your own actions based on active zone and game status. \n" +
- "\n" +
- "Units can be changed or moved without issue. Player slots can be changed or moved without issue (one per group though!) \n" +
+ "It's also highly recommended to fine-tune ground unit placements.\n" +
"\n" +
"Don't forget, you can also create your own templates that can include any mission options, objects, or even scripts. \n" +
"\n" +
@@ -632,20 +633,20 @@ class Window(QMainWindow, Ui_MainWindow):
# works fine but no use for this currently
- class myWebView(QDialog):
- def __init__(self, window, parent=None):
- QDialog.__init__(self, parent)
- vbox = QVBoxLayout(self)
-
- self.webEngineView = QWebEngineView()
- self.webEngineView.load(QUrl('https://dcs-helicopters.com'))
-
- vbox.addWidget(self.webEngineView)
-
- self.setLayout(vbox)
-
- self.setGeometry(600, 600, 700, 500)
- self.setWindowTitle('QWebEngineView')
+ # class myWebView(QDialog):
+ # def __init__(self, window, parent=None):
+ # QDialog.__init__(self, parent)
+ # vbox = QVBoxLayout(self)
+ #
+ # self.webEngineView = QWebEngineView()
+ # self.webEngineView.load(QUrl('https://dcs-helicopters.com'))
+ #
+ # vbox.addWidget(self.webEngineView)
+ #
+ # self.setLayout(vbox)
+ #
+ # self.setGeometry(600, 600, 700, 500)
+ # self.setWindowTitle('QWebEngineView')
class slotDialog(QDialog):
def __init__(self, window, parent=None):
@@ -770,10 +771,14 @@ def checkVersion(splashscreen):
try:
- r = requests.get(version_url, allow_redirects=False, timeout=7)
+ r = requests.get(version.version_url, allow_redirects=False, timeout=7)
v = yaml.safe_load(r.content)
avail_build = v["version"]
- if version.parse(avail_build) > version.parse(version_string):
+ avail_version = ver.parse(avail_build)
+ current_version = ver.parse(version.version_string)
+ current_maj_min = ver.parse(str(current_version.major) + "." + str(current_version.minor))
+ avail_maj_min = ver.parse(str(avail_version.major) + "." + str(avail_version.minor))
+ if avail_maj_min > current_maj_min:
logger.warning("New version available. Please update to available version " + v["version"])
msg = QMessageBox()
msg.setWindowTitle(v["title"])
@@ -781,7 +786,7 @@ def checkVersion(splashscreen):
msg.setIcon(QMessageBox.Icon.Information)
x = msg.exec_()
else:
- logger.info("Version check complete: running the latest version.")
+ logger.info("Version check complete: running the latest version. (micro version ignored)")
except:
logger.error("Online version check failed.")
diff --git a/Generator/MissionGeneratorUI.py b/Generator/MissionGeneratorUI.py
index 2a2dacf..6d7a52e 100644
--- a/Generator/MissionGeneratorUI.py
+++ b/Generator/MissionGeneratorUI.py
@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file 'MissionGeneratorUI.ui'
#
-# Created by: PyQt5 UI code generator 5.15.4
+# Created by: PyQt5 UI code generator 5.15.7
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
@@ -34,20 +34,20 @@ class Ui_MainWindow(object):
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.logistics_crates_checkBox = QtWidgets.QCheckBox(self.centralwidget)
- self.logistics_crates_checkBox.setGeometry(QtCore.QRect(980, 211, 251, 28))
+ self.logistics_crates_checkBox.setGeometry(QtCore.QRect(980, 231, 251, 28))
font = QtGui.QFont()
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(980, 320, 241, 28))
+ self.advanced_defenses_checkBox = QtWidgets.QCheckBox(self.centralwidget)
+ self.advanced_defenses_checkBox.setGeometry(QtCore.QRect(510, 350, 241, 28))
font = QtGui.QFont()
font.setPointSize(10)
font.setBold(False)
- self.zone_sams_checkBox.setFont(font)
- self.zone_sams_checkBox.setObjectName("zone_sams_checkBox")
+ self.advanced_defenses_checkBox.setFont(font)
+ self.advanced_defenses_checkBox.setObjectName("advanced_defenses_checkBox")
self.red_forces_label = QtWidgets.QLabel(self.centralwidget)
self.red_forces_label.setGeometry(QtCore.QRect(470, 80, 171, 27))
font = QtGui.QFont()
@@ -79,7 +79,7 @@ class Ui_MainWindow(object):
self.description_textBrowser.setObjectName("description_textBrowser")
self.defense_checkBox = QtWidgets.QCheckBox(self.centralwidget)
self.defense_checkBox.setEnabled(True)
- self.defense_checkBox.setGeometry(QtCore.QRect(470, 130, 156, 28))
+ self.defense_checkBox.setGeometry(QtCore.QRect(980, 140, 156, 28))
font = QtGui.QFont()
font.setPointSize(11)
font.setBold(False)
@@ -109,7 +109,7 @@ class Ui_MainWindow(object):
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))
+ self.scenario_label_8.setGeometry(QtCore.QRect(570, 180, 271, 24))
font = QtGui.QFont()
font.setPointSize(10)
font.setBold(False)
@@ -161,18 +161,21 @@ class Ui_MainWindow(object):
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.setGeometry(QtCore.QRect(1070, 650, 181, 20))
+ font = QtGui.QFont()
+ font.setPointSize(8)
+ self.version_label.setFont(font)
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))
+ self.scenario_label_10.setGeometry(QtCore.QRect(570, 220, 271, 24))
font = QtGui.QFont()
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))
+ self.e_transport_helos_spinBox.setGeometry(QtCore.QRect(510, 220, 51, 31))
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@@ -187,7 +190,7 @@ class Ui_MainWindow(object):
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))
+ self.e_attack_planes_spinBox.setGeometry(QtCore.QRect(510, 180, 51, 31))
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@@ -202,7 +205,7 @@ class Ui_MainWindow(object):
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(510, 180, 51, 31))
+ self.e_attack_helos_spinBox.setGeometry(QtCore.QRect(510, 140, 51, 31))
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@@ -219,7 +222,7 @@ class Ui_MainWindow(object):
self.e_attack_helos_spinBox.setProperty("value", 1)
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(570, 180, 271, 24))
+ self.scenario_label_7.setGeometry(QtCore.QRect(570, 140, 271, 24))
font = QtGui.QFont()
font.setPointSize(10)
font.setBold(False)
@@ -239,7 +242,7 @@ class Ui_MainWindow(object):
self.scenario_label_9.setFont(font)
self.scenario_label_9.setObjectName("scenario_label_9")
self.awacs_checkBox = QtWidgets.QCheckBox(self.centralwidget)
- self.awacs_checkBox.setGeometry(QtCore.QRect(980, 246, 241, 28))
+ self.awacs_checkBox.setGeometry(QtCore.QRect(980, 266, 241, 28))
font = QtGui.QFont()
font.setPointSize(10)
font.setBold(False)
@@ -247,7 +250,7 @@ class Ui_MainWindow(object):
self.awacs_checkBox.setChecked(True)
self.awacs_checkBox.setObjectName("awacs_checkBox")
self.tankers_checkBox = QtWidgets.QCheckBox(self.centralwidget)
- self.tankers_checkBox.setGeometry(QtCore.QRect(980, 282, 241, 28))
+ self.tankers_checkBox.setGeometry(QtCore.QRect(980, 302, 241, 28))
font = QtGui.QFont()
font.setPointSize(10)
font.setBold(False)
@@ -277,14 +280,14 @@ class Ui_MainWindow(object):
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, 340, 261, 23))
+ self.label.setGeometry(QtCore.QRect(570, 300, 261, 23))
font = QtGui.QFont()
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, 340, 51, 31))
+ self.inf_spawn_spinBox.setGeometry(QtCore.QRect(510, 300, 51, 31))
font = QtGui.QFont()
font.setPointSize(12)
self.inf_spawn_spinBox.setFont(font)
@@ -294,7 +297,7 @@ class Ui_MainWindow(object):
self.inf_spawn_spinBox.setProperty("value", 0)
self.inf_spawn_spinBox.setObjectName("inf_spawn_spinBox")
self.troop_drop_spinBox = QtWidgets.QSpinBox(self.centralwidget)
- self.troop_drop_spinBox.setGeometry(QtCore.QRect(510, 300, 51, 31))
+ self.troop_drop_spinBox.setGeometry(QtCore.QRect(510, 260, 51, 31))
font = QtGui.QFont()
font.setPointSize(12)
self.troop_drop_spinBox.setFont(font)
@@ -312,14 +315,14 @@ class Ui_MainWindow(object):
self.random_weather_checkBox.setTristate(False)
self.random_weather_checkBox.setObjectName("random_weather_checkBox")
self.label_3 = QtWidgets.QLabel(self.centralwidget)
- self.label_3.setGeometry(QtCore.QRect(570, 300, 281, 23))
+ self.label_3.setGeometry(QtCore.QRect(570, 260, 281, 23))
font = QtGui.QFont()
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(980, 180, 251, 27))
+ self.apcs_spawn_checkBox.setGeometry(QtCore.QRect(980, 200, 251, 27))
font = QtGui.QFont()
font.setPointSize(10)
font.setBold(False)
@@ -563,17 +566,17 @@ class Ui_MainWindow(object):
self.menubar.addAction(self.menuPreferences.menuAction())
self.retranslateUi(MainWindow)
- self.generateButton.clicked.connect(self.action_generateMission.trigger)
- self.prevScenario_pushButton.clicked.connect(self.action_prevScenario.trigger)
- self.defense_checkBox.clicked.connect(self.action_defensiveModeChanged.trigger)
- self.slot_template_comboBox.activated['int'].connect(self.action_slotChanged.trigger)
- self.scenario_comboBox.currentIndexChanged['int'].connect(self.action_scenarioSelected.trigger)
- self.nextScenario_pushButton.clicked.connect(self.action_nextScenario.trigger)
- self.rateButton1.clicked.connect(self.action_rateButton1.trigger)
- self.rateButton2.clicked.connect(self.action_rateButton2.trigger)
- self.rateButton3.clicked.connect(self.action_rateButton3.trigger)
- self.rateButton4.clicked.connect(self.action_rateButton4.trigger)
- self.rateButton5.clicked.connect(self.action_rateButton5.trigger)
+ self.generateButton.clicked.connect(self.action_generateMission.trigger) # type: ignore
+ self.prevScenario_pushButton.clicked.connect(self.action_prevScenario.trigger) # type: ignore
+ self.defense_checkBox.clicked.connect(self.action_defensiveModeChanged.trigger) # type: ignore
+ self.slot_template_comboBox.activated['int'].connect(self.action_slotChanged.trigger) # type: ignore
+ self.scenario_comboBox.currentIndexChanged['int'].connect(self.action_scenarioSelected.trigger) # type: ignore
+ self.nextScenario_pushButton.clicked.connect(self.action_nextScenario.trigger) # type: ignore
+ self.rateButton1.clicked.connect(self.action_rateButton1.trigger) # type: ignore
+ self.rateButton2.clicked.connect(self.action_rateButton2.trigger) # type: ignore
+ self.rateButton3.clicked.connect(self.action_rateButton3.trigger) # type: ignore
+ self.rateButton4.clicked.connect(self.action_rateButton4.trigger) # type: ignore
+ self.rateButton5.clicked.connect(self.action_rateButton5.trigger) # type: ignore
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
@@ -581,8 +584,8 @@ class Ui_MainWindow(object):
MainWindow.setWindowTitle(_translate("MainWindow", "RotorOps Mission Generator"))
self.logistics_crates_checkBox.setStatusTip(_translate("MainWindow", "Enable a base or FARP near the start position that can spawn CTLD crates for building ground units and air defenses. Sling load the logistics containers to create new logistics sites."))
self.logistics_crates_checkBox.setText(_translate("MainWindow", "Logistics Base"))
- 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. No effect if Blue on defense."))
- self.zone_sams_checkBox.setText(_translate("MainWindow", "Protect Inactive Zones"))
+ self.advanced_defenses_checkBox.setStatusTip(_translate("MainWindow", "Each enemy conflict zone spawns a template of AA defenses and radar units that may spawn fighter intercepts for detected aircraft. A good difficulty multiplier for multiplayer."))
+ self.advanced_defenses_checkBox.setText(_translate("MainWindow", "Enemy Advanced Defenses"))
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.description_textBrowser.setHtml(_translate("MainWindow", "\n"
@@ -624,7 +627,7 @@ class Ui_MainWindow(object):
self.game_status_checkBox.setText(_translate("MainWindow", "Game Status Display"))
self.label.setStatusTip(_translate("MainWindow", "Total number of infantry groups to spawn per game."))
self.label.setText(_translate("MainWindow", "Infantry Spawns"))
- self.inf_spawn_spinBox.setStatusTip(_translate("MainWindow", "This value is multiplied by the number of spawn zones in the mission template."))
+ self.inf_spawn_spinBox.setStatusTip(_translate("MainWindow", "Total number of infantry groups to spawn per game."))
self.troop_drop_spinBox.setStatusTip(_translate("MainWindow", "The number of troop drops per transport helicopter flight."))
self.random_weather_checkBox.setStatusTip(_translate("MainWindow", "Random weather preset will be applied."))
self.random_weather_checkBox.setText(_translate("MainWindow", "Random Weather"))
diff --git a/Generator/MissionGeneratorUI.ui b/Generator/MissionGeneratorUI.ui
index 9efcc07..e247fb5 100644
--- a/Generator/MissionGeneratorUI.ui
+++ b/Generator/MissionGeneratorUI.ui
@@ -54,7 +54,7 @@
980
- 211
+ 231
251
28
@@ -75,11 +75,11 @@
true
-
+
- 980
- 320
+ 510
+ 350
241
28
@@ -91,10 +91,10 @@
- Inactive conflict zones will be protected by SAMs. When a zone is cleared, SAMs at next active zone will be destroyed. No effect if Blue on defense.
+ Each enemy conflict zone spawns a template of AA defenses and radar units that may spawn fighter intercepts for detected aircraft. A good difficulty multiplier for multiplayer.
- Protect Inactive Zones
+ Enemy Advanced Defenses
@@ -190,8 +190,8 @@ p, li { white-space: pre-wrap; }
- 470
- 130
+ 980
+ 140
156
28
@@ -271,7 +271,7 @@ p, li { white-space: pre-wrap; }
570
- 220
+ 180
271
24
@@ -421,12 +421,17 @@ p, li { white-space: pre-wrap; }
- 1140
+ 1070
650
- 111
+ 181
20
+
+
+ 8
+
+
Version string
@@ -438,7 +443,7 @@ p, li { white-space: pre-wrap; }
570
- 260
+ 220
271
24
@@ -460,7 +465,7 @@ p, li { white-space: pre-wrap; }
510
- 260
+ 220
51
31
@@ -496,7 +501,7 @@ p, li { white-space: pre-wrap; }
510
- 220
+ 180
51
31
@@ -532,7 +537,7 @@ p, li { white-space: pre-wrap; }
510
- 180
+ 140
51
31
@@ -574,7 +579,7 @@ p, li { white-space: pre-wrap; }
570
- 180
+ 140
271
24
@@ -633,7 +638,7 @@ p, li { white-space: pre-wrap; }
980
- 246
+ 266
241
28
@@ -658,7 +663,7 @@ p, li { white-space: pre-wrap; }
980
- 282
+ 302
241
28
@@ -758,7 +763,7 @@ p, li { white-space: pre-wrap; }
570
- 340
+ 300
261
23
@@ -780,7 +785,7 @@ p, li { white-space: pre-wrap; }
510
- 340
+ 300
51
31
@@ -791,7 +796,7 @@ p, li { white-space: pre-wrap; }
- This value is multiplied by the number of spawn zones in the mission template.
+ Total number of infantry groups to spawn per game.
QAbstractSpinBox::PlusMinus
@@ -810,7 +815,7 @@ p, li { white-space: pre-wrap; }
510
- 300
+ 260
51
31
@@ -867,7 +872,7 @@ p, li { white-space: pre-wrap; }
570
- 300
+ 260
281
23
@@ -889,7 +894,7 @@ p, li { white-space: pre-wrap; }
980
- 180
+ 200
251
27
diff --git a/Generator/README.md b/Generator/README.md
new file mode 100644
index 0000000..b03a060
--- /dev/null
+++ b/Generator/README.md
@@ -0,0 +1,75 @@
+# Building the exe with build.bat
+
+**Use build.bat to compile the UI files and build the exe.**
+The steps are provided below for reference:
+
+## build UI files
+pyuic5 -x MissionGeneratorUI.ui -o MissionGeneratorUI.py
+
+## build resources
+pyrcc5 -o resources.py resources.qrc
+
+## build exe
+pyinstaller MissionGenerator.spec --distpath ..\ -i='assets\icon.ico'
+
+
+# Adding update to auto-installer
+
+**Merging into the main branch now triggers a deployment action that automatically performs the actions below when significant files are changed (defined in '.change-monitored').**
+
+**Significant files moved/deleted are not supported and may cause issues with the deployment script**
+
+**Version must be incremented in version.py. Only maj/min version is compared at app startup, so changes to supporting files can be made by incrementing the patch version.**
+
+
+Files currently live at https://dcs-helicopters.com/app-updates
+
+1) Add new files to /updates folder
+2) Update updatescript.ini:
+
+example:
+
+ releases{
+ 1.1.1
+ 1.1.2
+ 1.2.0
+ }
+
+ release:1.1.1{
+
+ }
+
+ release:1.1.2{
+ DownloadFile:MissionGenerator.exe
+ }
+
+ release:1.2.0{
+ DownloadFile:MissionGenerator.exe
+ DownloadFile:RotorOps.lua,scripts\
+ DownloadFile:Splash_Damage_2_0.lua,scripts\
+ }
+
+3) Update versioncheck.yaml
+
+example:
+ ---
+ title: "Update Available"
+ description: "UPDATE AVAILABLE: Please run the included updater utility (RotorOps_updater.exe) to get the latest version."
+ version: "1.2.0"
+
+# Building new RotorOps_setup.exe installer package
+
+Uses https://installforge.net/
+See install-config.ifp and installforge_constants.txt
+
+# Adding/updating downloadable modules
+
+** Templates now live in their own repo and there is a deployment action to automatically perform the steps below **
+
+Currently lives at https://dcs-helicopters.com/modules
+1) Add new folder to remote directory, ie modules/forces
+2) Trigger an update to templates by incrementing version in it's .yaml config file
+3) Run server/user-files/modules/mapscript.py
+
+
+
diff --git a/Generator/RotorOpsConflict.py b/Generator/RotorOpsConflict.py
index 55f1d86..4c4f3db 100644
--- a/Generator/RotorOpsConflict.py
+++ b/Generator/RotorOpsConflict.py
@@ -29,7 +29,9 @@ def triggerSetup(rops, options):
"RotorOps.zone_status_display = " + lb("game_display") + "\n\n" +
"RotorOps.inf_spawn_messages = true\n\n" +
"RotorOps.inf_spawns_total = " + lb("inf_spawn_qty") + "\n\n" +
- "RotorOps.apcs_spawn_infantry = " + lb("apc_spawns_inf") + " \n\n")
+ "RotorOps.apcs_spawn_infantry = " + lb("apc_spawns_inf") + " \n\n" +
+ "RotorOps.fighter_min_detection_alt = 609\n\n" +
+ "RotorOps.fighter_max_active = 2\n\n")
if not options["smoke_pickup_zones"]:
script = script + 'RotorOps.pickup_zone_smoke = "none"\n\n'
trig.actions.append(dcs.action.DoScript(dcs.action.String((script))))
@@ -58,6 +60,20 @@ def triggerSetup(rops, options):
trig.actions.append(dcs.action.DoScript(dcs.action.String("RotorOps.startConflict(100)")))
rops.m.triggerrules.triggers.append(trig)
+ if options["rotorops_server"]:
+
+ trig = dcs.triggers.TriggerOnce(comment="RotorOps Set Up Server")
+ trig.rules.append(dcs.condition.TimeAfter(4))
+ trig.actions.append(dcs.action.DoScriptFile(rops.scripts["RotorOpsServer.lua"]))
+ # Slot block the zone spawns if SSB is available
+ trig.actions.append(dcs.action.SetFlagValue('SSB', 100))
+ for c_zone in rops.conflict_zones:
+ for group in rops.all_zones[c_zone].player_helo_spawns:
+ trig.actions.append(dcs.action.SetFlagValue(group.name, 100))
+
+ rops.m.triggerrules.triggers.append(trig)
+
+
# Add generic zone-based triggers
for index, zone_name in enumerate(rops.conflict_zones):
z_active_trig = dcs.triggers.TriggerOnce(comment=zone_name + " Active")
@@ -74,14 +90,14 @@ def triggerSetup(rops, options):
# dcs.action.DoScript(dcs.action.String("ctld.createRadioBeaconAtZone('" + c_zone + "','blue', 1440,'" + c_zone + "')")))
# rops.m.triggerrules.triggers.append(trig)
- # Zone protection SAMs
- if options["zone_protect_sams"]:
- for index, zone_name in enumerate(rops.conflict_zones):
- z_sams_trig = dcs.triggers.TriggerOnce(comment="Deactivate " + zone_name + " SAMs")
- z_sams_trig.rules.append(dcs.condition.FlagEquals(game_flag, index + 1))
- z_sams_trig.actions.append(dcs.action.DoScript(
- dcs.action.String("Group.destroy(Group.getByName('Static " + zone_name + " Protection SAM'))")))
- rops.m.triggerrules.triggers.append(z_sams_trig)
+ # # Zone protection SAMs
+ # if options["zone_protect_sams"]:
+ # for index, zone_name in enumerate(rops.conflict_zones):
+ # z_sams_trig = dcs.triggers.TriggerOnce(comment="Deactivate " + zone_name + " SAMs")
+ # z_sams_trig.rules.append(dcs.condition.FlagEquals(game_flag, index + 1))
+ # z_sams_trig.actions.append(dcs.action.DoScript(
+ # dcs.action.String("Group.destroy(Group.getByName('" + zone_name + " Protect Static'))")))
+ # rops.m.triggerrules.triggers.append(z_sams_trig)
# Deactivate zone FARPs and player slots in defensive mode:
# this will also deactivate players already in the air.
@@ -107,11 +123,10 @@ def triggerSetup(rops, options):
z_farps_trig.rules.append(dcs.condition.FlagEquals(game_flag, index + 1))
z_farps_trig.actions.append(
dcs.action.ActivateGroup(rops.m.country(jtf_blue).find_group(previous_zone + " FARP Static").id))
- # Activate late-activated helicopters at FARPs. Doesn't work consistently
- # for group in rops.all_zones[previous_zone].player_helo_spawns:
- # z_farps_trig.actions.append(
- # dcs.action.ActivateGroup(
- # group.id))
+ # Activate late-activated helicopters at FARPs if SSB slot blocking script is available
+ for group in rops.all_zones[previous_zone].player_helo_spawns:
+ z_farps_trig.actions.append(
+ dcs.action.SetFlagValue(group.name, 0))
z_farps_trig.actions.append(dcs.action.DoScript(dcs.action.String(
"RotorOps.farpEstablished(" + str(index) + ", '" + previous_zone + "_FARP')")))
rops.m.triggerrules.triggers.append(z_farps_trig)
@@ -130,11 +145,10 @@ def triggerSetup(rops, options):
"--The 100 flag indicates which zone is active. The 111 flag value is the percentage of staged units remaining")))
z_farps_trig.actions.append(
dcs.action.ActivateGroup(rops.m.country(jtf_blue).find_group(previous_zone + " FARP Static").id))
- # Activate late-activated helicopters at FARPs. Doesn't work consistently
- # for group in rops.all_zones[previous_zone].player_helo_spawns:
- # z_farps_trig.actions.append(
- # dcs.action.ActivateGroup(
- # group.id))
+ # Activate late-activated helicopters at FARPs if SSB slot blocking script is available
+ for group in rops.all_zones[previous_zone].player_helo_spawns:
+ z_farps_trig.actions.append(
+ dcs.action.SetFlagValue(group.name, 0))
z_farps_trig.actions.append(dcs.action.DoScript(dcs.action.String(
"RotorOps.farpEstablished(" + str(index) + ", '" + previous_zone + "_FARP')")))
rops.m.triggerrules.triggers.append(z_farps_trig)
@@ -178,24 +192,53 @@ def triggerSetup(rops, options):
dcs.action.String("RotorOps.spawnTranspHelos(8," + str(options["transport_drop_qty"]) + ")")))
rops.m.triggerrules.triggers.append(z_weak_trig)
+ # Add enemy CAP spawn trigger
+ cap_trig = dcs.triggers.TriggerContinious(comment="Spawn Enemy CAP")
+ cap_trig.rules.append(dcs.condition.TimeAfter(10))
+ cap_trig.rules.append(dcs.condition.Predicate(dcs.action.String("return RotorOps.predSpawnRedCap()")))
+ cap_trig.actions.append(dcs.action.DoScript(dcs.action.String("RotorOps.deployFighters()")))
+ rops.m.triggerrules.triggers.append(cap_trig)
+
# Add game won/lost triggers
- # Add game won triggers
- trig = dcs.triggers.TriggerOnce(comment="RotorOps Conflict WON")
- trig.rules.append(dcs.condition.FlagEquals(game_flag, 99))
+ # Add game won triggers
+ mission_end_delay = 1200
+ trig = dcs.triggers.TriggerOnce(comment="RotorOps Conflict WON")
+ trig.rules.append(dcs.condition.FlagEquals(game_flag, 99))
+ trig.actions.append(
+ dcs.action.DoScript(dcs.action.String("---Add an action you want to happen when the game is WON")))
+ if options["end_trigger"] is not False:
trig.actions.append(
- dcs.action.DoScript(dcs.action.String("---Add an action you want to happen when the game is WON")))
- if options["end_trigger"] is not False:
- trig.actions.append(
- dcs.action.DoScript(dcs.action.String("RotorOps.gameMsg(RotorOps.gameMsgs.success)")))
- rops.m.triggerrules.triggers.append(trig)
+ dcs.action.DoScript(dcs.action.String("RotorOps.gameMsg(RotorOps.gameMsgs.success)")))
+ trig.actions.append(dcs.action.DoScript(dcs.action.String(
+ "timer.scheduleFunction(function()trigger.action.setUserFlag('mission_end', 2) end, {}, timer.getTime() + " + str(
+ mission_end_delay) + ")")))
+ trig.actions.append(dcs.action.MessageToAll(dcs.action.String("Time to RTB. Mission will end soon."), mission_end_delay))
- # Add game lost triggers
- trig = dcs.triggers.TriggerOnce(comment="RotorOps Conflict LOST")
- trig.rules.append(dcs.condition.FlagEquals(game_flag, 98))
+ mission_end_trigger = dcs.triggers.TriggerOnce(comment="End the mission")
+ mission_end_trigger.rules.append(dcs.condition.FlagEquals("mission_end", 2))
+ mission_end_trigger.actions.append(dcs.action.EndMission(text=dcs.action.String("Blue forces won!")))
+ rops.m.triggerrules.triggers.append(mission_end_trigger)
+
+
+ rops.m.triggerrules.triggers.append(trig)
+
+ # Add game lost triggers
+ trig = dcs.triggers.TriggerOnce(comment="RotorOps Conflict LOST")
+ trig.rules.append(dcs.condition.FlagEquals(game_flag, 98))
+ trig.actions.append(
+ dcs.action.DoScript(dcs.action.String("---Add an action you want to happen when the game is LOST")))
+ if options["end_trigger"] is not False:
+ trig.actions.append(dcs.action.DoScript(dcs.action.String("RotorOps.gameMsg(RotorOps.gameMsgs.failure)")))
+ trig.actions.append(dcs.action.DoScript(dcs.action.String(
+ "timer.scheduleFunction(function()trigger.action.setUserFlag('mission_end', 1) end, {}, timer.getTime() + " + str(
+ mission_end_delay) + ")")))
trig.actions.append(
- dcs.action.DoScript(dcs.action.String("---Add an action you want to happen when the game is LOST")))
- if options["end_trigger"] is not False:
- trig.actions.append(dcs.action.DoScript(dcs.action.String("RotorOps.gameMsg(RotorOps.gameMsgs.failure)")))
- rops.m.triggerrules.triggers.append(trig)
\ No newline at end of file
+ dcs.action.MessageToAll(dcs.action.String("Time to RTB. Mission will end soon."), mission_end_delay))
+ mission_end_trigger = dcs.triggers.TriggerOnce(comment="End the mission")
+ mission_end_trigger.rules.append(dcs.condition.FlagEquals("mission_end", 1))
+ mission_end_trigger.actions.append(dcs.action.EndMission(text=dcs.action.String("Red forces won!")))
+ rops.m.triggerrules.triggers.append(mission_end_trigger)
+
+ rops.m.triggerrules.triggers.append(trig)
\ No newline at end of file
diff --git a/Generator/RotorOpsImport.py b/Generator/RotorOpsImport.py
index b5c347d..76554fe 100644
--- a/Generator/RotorOpsImport.py
+++ b/Generator/RotorOpsImport.py
@@ -126,6 +126,7 @@ class ImportObjects:
group.units[0].heading)
# ng.units[0].livery_id = group.units[0].livery_id
+ ng.units[0].name = dest_name + " " + group.units[i].name
new_groups.append(ng)
else:
@@ -140,7 +141,7 @@ class ImportObjects:
return new_groups
def copyHelicopters(self, mission, dest_country_name, dest_name, dest_point, dest_heading=0,
- start_type=dcs.mission.StartType.Cold):
+ start_type=None):
logger.info("Copying " + str(len(self.helicopters)) + " helicopters as " + dest_name)
new_groups = []
@@ -176,9 +177,12 @@ class ImportObjects:
if start_type == dcs.mission.StartType.Warm:
ng.points[0].action = dcs.point.PointAction.FromGroundAreaHot
ng.points[0].type = "TakeOffGroundHot"
- else:
+ elif start_type == dcs.mission.StartType.Cold:
ng.points[0].action = dcs.point.PointAction.FromGroundArea
ng.points[0].type = "TakeOffGround"
+ else:
+ ng.points[0].action = group.points[0].action
+ ng.points[0].type = group.points[0].type
ng.units[0].heading = group.units[0].heading
ng.units[0].skill = group.units[0].skill
ng.units[0].livery_id = group.units[0].livery_id
@@ -211,6 +215,7 @@ class ImportObjects:
group.units[0].heading)
unit_count = unit_count + 1
# new_group.units[0].livery_id = group.units[0].livery_id
+ new_group.units[0].name = dest_name + " " + group.units[i].name
else:
diff --git a/Generator/RotorOpsMission.py b/Generator/RotorOpsMission.py
index 5b2f929..c5e2efc 100644
--- a/Generator/RotorOpsMission.py
+++ b/Generator/RotorOpsMission.py
@@ -32,6 +32,10 @@ class RotorOpsMission:
self.res_map = {}
self.config = None # not used
self.imports = None
+ self.red_zones = {}
+ self.blue_zones = {}
+ self.primary_e_airport = None
+
class RotorOpsZone:
def __init__(self, name: str, flag: int, position: dcs.point, size: int):
@@ -132,7 +136,22 @@ class RotorOpsMission:
window.statusBar().showMessage("Loading scenario mission", 10000)
- self.m.load_file(options["scenario_file"])
+ # self.m.load_file(options["scenario_file"])
+
+ # Bypass trig, triggrules, and triggers. Then load triggers
+ # manually. We want to get our zones from the template mission, but leave the existing trigger actions and
+ # conditions the same, since pydcs cannot yet handle some of them well.
+
+ self.m.load_file(options["scenario_file"], True)
+ self.m.triggers.load_from_dict(self.m.bypassed_triggers)
+
+ # Create some 'empty' triggerrules so that we can maintain indexing when we merge dictionaries on save
+ for rule in self.m.bypassed_trigrules:
+ trig = dcs.triggers.TriggerOnce(comment="Empty " + str(rule))
+ trig.rules.append(dcs.condition.TimeAfter(1))
+ trig.actions.append(dcs.action.DoScript(dcs.action.String("Filler " + str(rule))))
+ self.m.triggerrules.triggers.append(trig)
+
# Add countries if they're missing
if not self.m.country(jtf_red):
self.m.coalition.get("red").add_country(dcs.countries.CombinedJointTaskForcesRed())
@@ -200,11 +219,11 @@ class RotorOpsMission:
elif zone.name.rfind("SPAWN") >= 0:
self.addZone(self.spawn_zones, self.RotorOpsZone(zone.name, None, zone.position, zone.radius))
- blue_zones = self.staging_zones
- red_zones = self.conflict_zones
+ self.blue_zones = self.staging_zones
+ self.red_zones = self.conflict_zones
if options["defending"]:
- blue_zones = self.conflict_zones
- red_zones = self.staging_zones
+ self.blue_zones = self.conflict_zones
+ self.red_zones = self.staging_zones
# swap airport sides
self.swapSides(options)
@@ -214,119 +233,10 @@ class RotorOpsMission:
if options["player_hotstart"]:
start_type = dcs.mission.StartType.Warm
- # Adds vehicles as a single group (for easy late activation), and helicopters if enabled in settings
- # def addZoneFARP(_zone_name, country, file):
- #
- # farp_flag = self.m.find_group(_zone_name)
- #
- # if farp_flag:
- # farp_position = farp_flag.units[0].position
- # farp_heading = farp_flag.units[0].heading
- # else:
- # farp_position = self.all_zones[_zone_name].position
- # farp_heading = 0
- #
- # # Add the basic invisible farp object
- # farp = self.m.farp(self.m.country(country), _zone_name + " FARP", farp_position,
- # hidden=False, dead=False,
- # farp_type=dcs.unit.InvisibleFARP)
- #
- # # Use alternate template file if it has been defined in scenario config
- # if options["zone_farp_file"]:
- #
- # for i in imports:
- # if i.filename.removesuffix('.miz') == options["zone_farp_file"]:
- # file = i.path
- # # if multiple files found, we want the latest file to override the first
- #
- # i = ImportObjects(file)
- # i.anchorByGroupName("ANCHOR")
- # farp_group = i.copyVehiclesAsGroup(self.m, country, _zone_name + " FARP Static", farp_position,
- # farp_heading)
- # # Add client helicopters
- # if options["farp_spawns"]:
- # helicopter_groups = i.copyHelicopters(self.m, jtf_blue, "ZONE " + _zone_name + " EMPTY ", farp_position, farp_heading)
- # for group in helicopter_groups:
- # self.all_zones[_zone_name].player_helo_spawns.append(group)
- #
- # return farp_group
- # # Adds statics, vehicles, and helicopters. Late activation is not possible
- # def addLogisticsZone(_zone_name, country, file, config_name, helicopters=False):
- # flag = self.m.find_group(_zone_name)
- # if flag:
- # position = flag.units[0].position
- # heading = flag.units[0].heading
- # else:
- # position = self.all_zones[_zone_name].position
- # heading = 0
- #
- # # Use alternate template file if it has been defined in scenario config
- # if options[config_name]:
- #
- # for i in imports:
- # if i.filename.removesuffix('.miz') == options[config_name]:
- # file = i.path
- # # if multiple files found, we want the latest file to override the first
- #
- # # Import statics and vehicles
- # i = ImportObjects(file)
- # i.anchorByGroupName("ANCHOR")
- # i.copyStatics(self.m, country, _zone_name + " Logistics Zone",
- # position, heading)
- # i.copyVehicles(self.m, country, _zone_name + " Logistics Zone",
- # position, heading)
- #
- # # Add client helicopters
- # if helicopters:
- # helicopter_groups = i.copyHelicopters(self.m, jtf_blue, "ZONE " + _zone_name + " EMPTY ", position,
- # heading)
- # for group in helicopter_groups:
- # self.all_zones[_zone_name].player_helo_spawns.append(group)
-
- # Adds statics, vehicles, and helicopters (if enabled). Late activation is not possible.
- # def addDefensiveFARP(_zone_name, country, file):
- #
- # farp_flag = self.m.find_group(_zone_name)
- #
- # if farp_flag:
- # farp_position = farp_flag.units[0].position
- # farp_heading = farp_flag.units[0].heading
- # else:
- # farp_position = self.all_zones[_zone_name].position
- # farp_heading = 0
- #
- # # Add the basic invisible farp object
- # farp = self.m.farp(self.m.country(country), _zone_name + " FARP", farp_position,
- # hidden=False, dead=False,
- # farp_type=dcs.unit.InvisibleFARP)
- #
- # # Use alternate template file if it has been defined in scenario config
- # if options["defensive_farp_file"]:
- #
- # for i in imports:
- # if i.filename.removesuffix('.miz') == options["defensive_farp_file"]:
- # file = i.path
- # # if multiple files found, we want the latest file to override the first
- #
- # # Import statics and vehicles
- # i = ImportObjects(file)
- # i.anchorByGroupName("ANCHOR")
- # i.copyStatics(self.m, country, _zone_name + " Logistics Zone",
- # farp_position, farp_heading)
- # i.copyVehicles(self.m, country, _zone_name + " Logistics Zone",
- # farp_position, farp_heading)
- #
- # # Import player helicopters
- # if options["farp_spawns"]:
- # helicopter_groups = i.copyHelicopters(self.m, jtf_blue, "ZONE " + _zone_name + " EMPTY ", farp_position,
- # farp_heading)
- # for group in helicopter_groups:
- # self.all_zones[_zone_name].player_helo_spawns.append(group)
-
- for zone_name in red_zones:
+ for zone_name in self.red_zones:
if red_forces["vehicles"]:
- self.addGroundGroups(red_zones[zone_name], self.m.country(jtf_red), red_forces["vehicles"],
+ self.addGroundGroups(self.red_zones[zone_name], self.m.country(jtf_red), red_forces["vehicles"],
options["red_quantity"])
if options["zone_farps"] != "farp_never" and not options["defending"]:
@@ -340,8 +250,8 @@ class RotorOpsMission:
file=activated_farp,
config_name="zone_farp_file",
copy_helicopters=helicopters,
- helicopters_name="ZONE " + zone_name + " EMPTY",
- heli_start_type=dcs.mission.StartType.Cold,
+ helicopters_name="ZONE " + zone_name,
+ heli_start_type=None,
copy_vehicles=True,
vehicles_name=zone_name + " FARP Static",
copy_statics=False,
@@ -352,34 +262,21 @@ class RotorOpsMission:
)
vehicle_group.late_activation = True
- # For SAMs: Add vehicles as a single group (for easy late activation)
- if options["zone_protect_sams"]:
+
+ if options["advanced_defenses"]:
sam_group = self.addZoneBase(options, zone_name, jtf_red,
file=zone_protect,
config_name="zone_protect_file",
copy_vehicles=True,
- vehicles_name=zone_name + " Protect Static",
- vehicles_single_group=True
+ vehicles_name=zone_name + " Defense Static",
+ vehicles_single_group=False
)
- # farp_flag = self.m.find_group(zone_name)
- #
- # if farp_flag:
- # farp_position = farp_flag.units[0].position
- # farp_heading = farp_flag.units[0].heading
- # else:
- # farp_position = self.all_zones[zone_name].position
- # farp_heading = 0
- #
- # i = ImportObjects(zone_protect)
- # i.anchorByGroupName("ANCHOR")
- # farp_group = i.copyVehiclesAsGroup(self.m, jtf_red, "Static " + zone_name + " Protection SAM",
- # farp_position,
- # farp_heading)
+
# Populate Blue zones with ground units
- for i, zone_name in enumerate(blue_zones):
+ for i, zone_name in enumerate(self.blue_zones):
if blue_forces["vehicles"]:
- self.addGroundGroups(blue_zones[zone_name], self.m.country(jtf_blue), blue_forces["vehicles"],
+ self.addGroundGroups(self.blue_zones[zone_name], self.m.country(jtf_blue), blue_forces["vehicles"],
options["blue_quantity"])
# Add blue zone FARPS (not late activated) for defensive mode
@@ -389,7 +286,7 @@ class RotorOpsMission:
if options["farp_spawns"]:
helicopters = True
- if options["crates"] and i == len(blue_zones) - 1:
+ if options["crates"] and i == len(self.blue_zones) - 1:
# add a logistics zone to the last conflict zone
# addLogisticsZone(zone_name, jtf_blue, logistics_farp, "logistics_farp_file", helicopters)
self.addZoneBase(options, zone_name, jtf_blue,
@@ -397,7 +294,7 @@ class RotorOpsMission:
config_name="logistics_farp_file",
copy_helicopters=helicopters,
helicopters_name="ZONE " + zone_name + " LOGISTICS",
- heli_start_type=start_type,
+ heli_start_type=None,
copy_vehicles=True,
vehicles_name=zone_name + " Logistics FARP",
copy_statics=True,
@@ -413,7 +310,7 @@ class RotorOpsMission:
config_name="defensive_farp_file",
copy_helicopters=helicopters,
helicopters_name="ZONE " + zone_name + " EMPTY",
- heli_start_type=dcs.mission.StartType.Cold,
+ heli_start_type=None,
copy_vehicles=True,
vehicles_name=zone_name + " Defensive FARP",
copy_statics=True,
@@ -443,7 +340,9 @@ class RotorOpsMission:
# Add player slots
window.statusBar().showMessage("Adding flights to mission...", 10000)
- if options["slots"] != "Locked to Scenario" and options["slots"] != "None":
+ if options["slots"] == "Locked to Scenario" or options["slots"] == "None":
+ pass
+ else:
self.addPlayerHelos(options)
# Add AI Flights
@@ -482,11 +381,15 @@ class RotorOpsMission:
self.m.weather.wind_at_8000.direction = (wind_dir + random.randrange(-90, 90) - 180) % 360
self.m.weather.wind_at_8000.speed = wind_speed + random.randrange(-1, 10)
+ self.m.weather.halo.preset = dcs.weather.Halo.Preset.Auto
+ self.m.weather.halo.crystals = None
+
logger.info("Cloud preset = " + cloud_preset.ui_name + ", ground windspeed = " + str(
self.m.weather.wind_at_ground.speed))
if options["time"] != "Default Time":
self.m.random_daytime(options["time"].lower())
+ print("Time set to " + options["time"])
# Save the mission file
window.statusBar().showMessage("Saving mission...", 10000)
@@ -496,12 +399,22 @@ class RotorOpsMission:
output_dir = directories.output # default dir
os.chdir(output_dir)
output_filename = options["scenario_name"] + " " + time.strftime('%a%H%M%S') + '.miz'
+
+ # dcs.mission.save will use the bypassed trig, trigrules, and triggers. Our goal is to leave the trigrules and
+ # trig from the source mission untouched. See comments in self.m.load_file above
+
+ #merge dictionaries
+ self.m.bypassed_trig = self.m.triggerrules.trig() | self.m.bypassed_trig
+ self.m.bypassed_trigrules = self.m.triggerrules.trigrules() | self.m.bypassed_trigrules
+
+ self.m.bypassed_triggers = self.m.triggers.dict()
+
success = self.m.save(output_filename)
return {"success": success, "filename": output_filename, "directory": output_dir} # let the UI know the result
# Use the ImportObjects class to place farps and bases
def addZoneBase(self, options, _zone_name, country, file, config_name=None, copy_helicopters=False,
- helicopters_name="", heli_start_type=dcs.mission.StartType.Cold,
+ helicopters_name="", heli_start_type=None,
copy_vehicles=False, vehicles_name="", copy_statics=False, statics_names="",
vehicles_single_group=False, trigger_name=None, trigger_radius=110, farp=True):
@@ -598,7 +511,7 @@ class RotorOpsMission:
if len(airport.free_parking_slots(aircraft)) >= group_size:
if not (aircraft.id in dcs.planes.plane_map and (
- len(airport.runways) == 0 or airport.runways[0].ils is None)):
+ len(airport.runways) == 0 or not hasattr(airport.runways[0], "ils"))):
return airport
if alt_airports:
@@ -703,7 +616,7 @@ class RotorOpsMission:
for helicopter in dcs.helicopters.helicopter_map:
if helicopter == options["slots"]:
client_helos = [dcs.helicopters.helicopter_map[
- helicopter]] # if out ui slot option matches a specific helicopter type name
+ helicopter]] # if our ui slot option matches a specific helicopter type name
# get loadouts from miz file and put into a simple dict
default_loadouts = {}
@@ -757,6 +670,8 @@ class RotorOpsMission:
helotype = None
if helicopter_id in dcs.helicopters.helicopter_map:
helotype = dcs.helicopters.helicopter_map[helicopter_id]
+ elif helicopter_id in dcs.planes.plane_map:
+ helotype = dcs.planes.plane_map[helicopter_id]
else:
continue
if carrier:
@@ -846,7 +761,7 @@ class RotorOpsMission:
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)
+ enemy_heading - random.randrange(140, 220), 20000)
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], terrain), heading, race_dist
@@ -857,6 +772,7 @@ class RotorOpsMission:
friendly_airports, primary_f_airport = self.getCoalitionAirports("blue")
enemy_airports, primary_e_airport = self.getCoalitionAirports("red")
+
# find enemy carriers and farps
carrier = self.m.country(jtf_red).find_ship_group(name="HELO_CARRIER")
if not carrier:
@@ -876,6 +792,27 @@ class RotorOpsMission:
primary_f_airport.position.y
)
+ self.primary_e_airport = primary_e_airport
+ self.m.triggers.add_triggerzone(primary_e_airport.position, 1500, hidden=True, name="RED_AIRBASE")
+
+ if options["red_cap"]:
+ scenario_red_cap_spawn_zone = None
+ for zone in self.m.triggers.zones():
+ if zone.name == "RED_CAP_SPAWN":
+ scenario_red_cap_spawn_zone = True
+ if not scenario_red_cap_spawn_zone:
+ e_cap_spawn_point = primary_e_airport.position.point_from_heading(e_airport_heading, 100000)
+ self.m.triggers.add_triggerzone(e_cap_spawn_point, 30000, hidden=True, name="RED_CAP_SPAWN")
+
+ if options["blue_cap"]:
+ scenario_blue_cap_spawn_zone = None
+ for zone in self.m.triggers.zones():
+ if zone.name == "BLUE_CAP_SPAWN":
+ scenario_blue_cap_spawn_zone = True
+ if not scenario_blue_cap_spawn_zone:
+ f_cap_spawn_point = primary_f_airport.position.point_from_heading(e_airport_heading + 180, 100000)
+ self.m.triggers.add_triggerzone(f_cap_spawn_point, 30000, hidden=True, name="BLUE_CAP_SPAWN")
+
if options["f_awacs"]:
awacs_name = "AWACS"
awacs_freq = 266
@@ -962,6 +899,7 @@ class RotorOpsMission:
t1_freq) + ".00 " + t1_tac + "\n" + t2_name + " " + str(t2_freq) + ".00 " + t2_tac + "\n\n"
self.m.set_description_text(briefing)
+
def zone_attack(fg, airport):
fg.set_skill(dcs.unit.Skill.High)
fg.late_activation = True
@@ -1093,6 +1031,139 @@ class RotorOpsMission:
unit.pylons = source_helo.pylons
unit.livery_id = source_helo.livery_id
+ if False:
+ for i in range(1,4):
+ randzone = random.choice(list(self.red_zones))
+ pt2 = self.red_zones[randzone].position
+ source_plane = None
+ if red_forces["fighter_planes"]:
+ source_group = random.choice(red_forces["fighter_planes"])
+ source_plane = source_group.units[0]
+ plane_type = source_plane.unit_type
+ group_size = random.randrange(1, 2)
+
+ else:
+ group_size = random.randrange(1, 2)
+ plane_type = random.choice(RotorOpsUnits.e_attack_helos)
+
+ airport = self.getParking(primary_e_airport, plane_type, enemy_airports, group_size)
+
+ enemy_cap = self.m.patrol_flight(airport=airport,
+ name="Enemy CAP " + str(i),
+ country=combinedJointTaskForcesRed,
+ patrol_type=plane_type,
+ pos1=primary_e_airport.position,
+ pos2=pt2,
+ altitude=3000,
+ group_size=group_size,
+ max_engage_distance=40 * 1000
+ )
+
+ # enemy_cap.points[0].tasks[0] = dcs.task.EngageTargets(max_engage_distance, [dcs.task.Targets.All.Air.Planes])
+
+ for unit in enemy_cap.units:
+ unit.skill = dcs.unit.Skill.Random
+
+ if source_plane:
+ for unit in enemy_cap.units:
+ unit.pylons = source_plane.pylons
+ unit.livery_id = source_plane.livery_id
+
+ if options["red_cap"]:
+
+ if red_forces["fighter_planes"]:
+ for fighter_plane_group in red_forces["fighter_planes"]:
+ source_group = random.choice(red_forces["fighter_planes"])
+ source_plane = source_group.units[0]
+ plane_type = source_plane.unit_type
+
+ enemy_cap = self.m.flight_group(
+ country=combinedJointTaskForcesRed,
+ name="RED CAP",
+ aircraft_type=plane_type,
+ airport=None,
+ maintask=dcs.task.CAP,
+ group_size=1,
+ position=e_cap_spawn_point,
+ altitude=5000,
+ speed=300
+ )
+
+ enemy_cap.late_activation = True
+
+ for unit in enemy_cap.units:
+ unit.skill = dcs.unit.Skill.Random
+ unit.pylons = source_plane.pylons
+ unit.livery_id = source_plane.livery_id
+
+ else:
+ plane_type = random.choice(RotorOpsUnits.e_fighter_planes)
+
+ enemy_cap = self.m.flight_group(
+ country=combinedJointTaskForcesRed,
+ name="RED CAP",
+ aircraft_type=plane_type,
+ airport=None,
+ maintask=dcs.task.CAP,
+ group_size=1,
+ position=e_cap_spawn_point,
+ altitude=5000,
+ speed=300
+ )
+
+ enemy_cap.late_activation = True
+
+ for unit in enemy_cap.units:
+ unit.skill = dcs.unit.Skill.Random
+
+ if options["blue_cap"]:
+
+ if blue_forces["fighter_planes"]:
+ for fighter_plane_group in blue_forces["fighter_planes"]:
+ source_group = random.choice(blue_forces["fighter_planes"])
+ source_plane = source_group.units[0]
+ plane_type = source_plane.unit_type
+
+ friendly_cap = self.m.flight_group(
+ country=combinedJointTaskForcesBlue,
+ name="BLUE CAP",
+ aircraft_type=plane_type,
+ airport=None,
+ maintask=dcs.task.CAP,
+ group_size=1,
+ position=f_cap_spawn_point,
+ altitude=5000,
+ speed=300
+ )
+
+ friendly_cap.late_activation = True
+
+ for unit in friendly_cap.units:
+ unit.skill = dcs.unit.Skill.Random
+ unit.pylons = source_plane.pylons
+ unit.livery_id = source_plane.livery_id
+
+ else:
+ plane_type = random.choice(RotorOpsUnits.f_fighter_planes)
+
+ friendly_cap = self.m.flight_group(
+ country=combinedJointTaskForcesBlue,
+ name="BLUE CAP",
+ aircraft_type=plane_type,
+ airport=None,
+ maintask=dcs.task.CAP,
+ group_size=1,
+ position=f_cap_spawn_point,
+ altitude=5000,
+ speed=300
+ )
+
+ friendly_cap.late_activation = True
+
+ for unit in friendly_cap.units:
+ unit.skill = dcs.unit.Skill.Random
+
+
def importObjects(self, data):
imports = data["objects"]["imports"]
diff --git a/Generator/RotorOpsUnits.py b/Generator/RotorOpsUnits.py
index e0449f7..3b8116c 100644
--- a/Generator/RotorOpsUnits.py
+++ b/Generator/RotorOpsUnits.py
@@ -12,6 +12,7 @@ client_helos = [
player_helos = [
dcs.helicopters.AH_64D_BLK_II,
dcs.helicopters.Ka_50,
+ dcs.helicopters.Ka_50_3,
dcs.helicopters.Mi_8MT,
dcs.helicopters.Mi_24P,
dcs.helicopters.SA342M,
@@ -20,6 +21,7 @@ player_helos = [
dcs.helicopters.SA342Mistral,
dcs.helicopters.UH_1H,
aircraftMods.UH_60L,
+ dcs.planes.AV8BNA,
]
e_attack_helos = [
@@ -39,6 +41,14 @@ e_attack_planes = [
dcs.planes.A_10C,
]
+e_fighter_planes = [
+ dcs.planes.Su_27,
+]
+
+f_fighter_planes = [
+ dcs.planes.FA_18C_hornet,
+]
+
e_zone_sams = [
dcs.vehicles.AirDefence.Strela_10M3,
]
\ No newline at end of file
diff --git a/Generator/build command.txt b/Generator/build command.txt
deleted file mode 100644
index d966d57..0000000
--- a/Generator/build command.txt
+++ /dev/null
@@ -1,8 +0,0 @@
-#build UI files
-pyuic5 -x MissionGeneratorUI.ui -o MissionGeneratorUI.py
-
-#build resources
-pyrcc5 -o resources.py resources.qrc
-
-#build exe
-pyinstaller MissionGenerator.spec --distpath ..\ -i='assets\icon.ico'
\ No newline at end of file
diff --git a/Generator/build.bat b/Generator/build.bat
new file mode 100644
index 0000000..3e07e56
--- /dev/null
+++ b/Generator/build.bat
@@ -0,0 +1,13 @@
+call .\venv\Scripts\activate.bat
+echo activated python venv.
+
+pyuic5 -x MissionGeneratorUI.ui -o MissionGeneratorUI.py
+echo built MissionGenerator.py from MissionsGeneratorUI.ui
+
+pyrcc5 -o resources.py resources.qrc
+echo compiled ui resource files.
+
+echo building exe with pyinstaller...
+pyinstaller MissionGenerator.spec --distpath ..\ --clean
+
+pause >nul
\ No newline at end of file
diff --git a/Generator/requirements.txt b/Generator/requirements.txt
index 43c09aa..973a365 100644
Binary files a/Generator/requirements.txt and b/Generator/requirements.txt differ
diff --git a/Generator/resources.py b/Generator/resources.py
index aeec8e3..5af7b36 100644
--- a/Generator/resources.py
+++ b/Generator/resources.py
@@ -1175,9 +1175,9 @@ qt_resource_struct_v2 = b"\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x02\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x12\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
-\x00\x00\x01\x80\x16\x14\x6f\x4d\
+\x00\x00\x01\x80\x25\xe1\xff\x48\
\x00\x00\x00\x2c\x00\x00\x00\x00\x00\x01\x00\x00\x22\x0d\
-\x00\x00\x01\x80\x16\x13\x53\x5c\
+\x00\x00\x01\x80\x25\xe1\xff\x48\
"
qt_version = [int(v) for v in QtCore.qVersion().split('.')]
diff --git a/Generator/version.py b/Generator/version.py
new file mode 100644
index 0000000..1c0b54b
--- /dev/null
+++ b/Generator/version.py
@@ -0,0 +1,8 @@
+# ROTOROPS VERSION
+maj_version = 1
+minor_version = 3
+patch_version = 2
+
+version_url = 'https://dcs-helicopters.com/app-updates/versioncheck.yaml'
+
+version_string = str(maj_version) + "." + str(minor_version) + "." + str(patch_version)
diff --git a/MissionGenerator.exe b/MissionGenerator.exe
new file mode 100644
index 0000000..5b59cec
Binary files /dev/null and b/MissionGenerator.exe differ
diff --git a/assets/rotorops mp brief.png b/assets/rotorops mp brief.png
new file mode 100644
index 0000000..8539f2d
Binary files /dev/null and b/assets/rotorops mp brief.png differ
diff --git a/config/blue_player_loadouts.miz b/config/blue_player_loadouts.miz
index db72de0..918a429 100644
Binary files a/config/blue_player_loadouts.miz and b/config/blue_player_loadouts.miz differ
diff --git a/release_script.py b/release_script.py
new file mode 100644
index 0000000..c08b308
--- /dev/null
+++ b/release_script.py
@@ -0,0 +1,145 @@
+import datetime
+import ftplib
+import os
+import sys
+import json
+import yaml
+from Generator import version
+
+if __name__ == "__main__":
+
+ # change working directory to the directory of this
+ os.chdir(os.path.dirname(os.path.abspath(__file__)))
+
+ #get unix epoch seconds string via datetime
+ timestamp = str(int(datetime.datetime.now().timestamp()))
+ updatescript_bkup = "updatescript.ini.bkup" + timestamp
+ versioncheck_bkup = "versioncheck.yaml.bkup" + timestamp
+
+ if not os.getenv("FTP_SERVER") or not os.getenv("FTP_USERNAME") or not os.getenv("FTP_PASSWORD"):
+ print("FTP_SERVER, FTP_USERNAME, and FTP_PASSWORD environment variables must be set.")
+ sys.exit(1)
+
+ # connect to the update server
+ ftp = ftplib.FTP(os.getenv("FTP_SERVER"))
+
+ # login to the update server
+ try:
+ ftp.login(os.getenv("FTP_USERNAME"), os.getenv("FTP_PASSWORD"))
+
+ except ftplib.error_perm:
+ print("Login failed. Check your username and password.")
+ sys.exit(1)
+
+ except ftplib.error_temp:
+ print("Login failed due to temp error. Check your connection settings.")
+ sys.exit(1)
+
+ # list files in the current directory on the update server
+ print("Files in the current remote directory: ")
+ files = ftp.nlst()
+ print(files)
+
+ #download the updatescript.ini file
+ with open("updatescript.ini", "wb") as f:
+ ftp.retrbinary("RETR updatescript.ini", f.write)
+
+ with open(updatescript_bkup, "wb") as f:
+ ftp.retrbinary("RETR updatescript.ini", f.write)
+
+
+ #download the versioncheck.yaml file
+ with open("versioncheck.yaml", "wb") as f:
+ ftp.retrbinary("RETR versioncheck.yaml", f.write)
+
+ with open(versioncheck_bkup, "wb") as f:
+ ftp.retrbinary("RETR versioncheck.yaml", f.write)
+
+
+
+
+ # load yaml file and check previous version
+
+ with open('versioncheck.yaml') as f:
+ data = yaml.load(f, Loader=yaml.FullLoader)
+ # get version from yaml file
+ version_from_file = data['version']
+
+ if version_from_file == version.version_string:
+ # throw error
+ print("Version is the same as the current version. Increment the version number in version.py and try again.")
+ sys.exit(1)
+
+
+
+ changed_files = json.loads(os.getenv("changed_files"))
+ if changed_files:
+ print("Changed files: " + str(changed_files))
+
+ # open updatescript.ini for read/write
+
+ with open("updatescript.ini", "r") as f:
+ file_text = f.read()
+ # get the contents of the first block starting with 'releases{' and ending with '}'
+ releases_str = file_text[file_text.find('releases{') + 9:file_text.find('}')].strip()
+ # split the releases into a list of releases
+ # remove spaces
+ releases_str = releases_str.replace(" ", "")
+ releases = releases_str.split('\n')
+ for r in releases:
+ if r == version.version_string:
+ print("Version already exists in updatescript.ini")
+ sys.exit(1)
+
+ with open("updatescript.ini", "w") as f:
+
+ #add the newest release to the bottom of the list
+ releases.append(version.version_string)
+
+ # remove text before first '}'
+ file_text = file_text[file_text.find('}') + 1:]
+
+ # write the releases block back to the file
+ f.write('releases{\n ' + '\n '.join(releases) + '\n}\n')
+
+ #write the old releases back to the file
+ f.write(file_text)
+
+
+ # write the new release to the file
+ f.write("\nrelease:" + version.version_string + "{")
+ for file in changed_files:
+ remote_path = 'continuous/' + file
+ subdir = None
+ #if file has a subdir, get the subdir
+ if file.rfind("/") != -1:
+ subdir = file[:file.rfind("/")]
+ file = file[file.rfind("/") + 1:]
+ f.write("\n DownloadFile:" + remote_path)
+ f.write("," + subdir + "/")
+ else:
+ f.write("\n DownloadFile:" + remote_path)
+
+ f.write("\n}")
+ f.close()
+
+ # create new versioncheck.yaml file
+ with open('versioncheck.yaml', 'w') as f:
+ f.write("title: \"Update Available\"\n")
+ f.write("description: \"UPDATE AVAILABLE: Please run the included updater utility (RotorOps_updater.exe) to get the latest version.\"" + "\n")
+ f.write("version: \"" + version.version_string + "\"\n")
+
+
+ #upload the new files to the update server
+
+ files = [updatescript_bkup, versioncheck_bkup, 'updatescript.ini', 'versioncheck.yaml']
+
+
+
+ for file in files:
+ ftp.storbinary("STOR " + file, open(file, "rb"))
+ print("Uploaded " + file)
+
+ ftp.quit()
+
+
diff --git a/scripts/RotorOps.lua b/scripts/RotorOps.lua
index 642fa99..2de4501 100644
--- a/scripts/RotorOps.lua
+++ b/scripts/RotorOps.lua
@@ -1,5 +1,5 @@
RotorOps = {}
-RotorOps.version = "1.3.0"
+RotorOps.version = "1.3.3"
local debug = true
@@ -40,7 +40,7 @@ RotorOps.pickup_zone_smoke = "blue"
RotorOps.ai_task_by_name = true --allow tasking all groups that include key strings in their group names eg 'Patrol'
RotorOps.ai_task_by_name_scheduler = true --continually search active groups for key strings and ai tasking
RotorOps.patrol_task_string = 'patrol' --default string to search group names for the patrol task. requires ai_task_by_name
-RotorOps.aggressive_task_string = 'aggressive' --default string to search group names for the patrol task. requires ai_task_by_name
+RotorOps.aggressive_task_string = 'aggressive' --default string to search group names for the aggressive task. requires ai_task_by_name
RotorOps.move_to_active_task_string = "activezone" --default string to search group names for the move to active zone task. requires ai_task_by_name
RotorOps.shift_task_string = "shift"
RotorOps.guard_task_string = "guard"
@@ -49,6 +49,10 @@ RotorOps.guard_task_string = "guard"
RotorOps.defending_vehicles_behavior = "shift" --available options: 'none', 'patrol', 'shift'
RotorOps.farp_pickups = true --allow ctld troop pickup at FARPs
RotorOps.enable_staging_pickzones = true
+RotorOps.persistent_tasking = false --prevent the script from restasking in a loop --might help with odd movement patterns between zones
+
+--RotorOps settings that are safe to change only in the script config option in the scenario config file
+RotorOps.draw_conflict_zones = true
---[[END OF OPTIONS]]---
@@ -71,6 +75,13 @@ RotorOps.ai_tasks = {}
RotorOps.defending = false
RotorOps.staged_units_flag = 111 -- shows a percentage of the units found in the staging zone when the game starts. you can also use 'ROPS_ATTACKERS' for readability
+--fighter variables
+local fighters_by_detected_unitname = {}
+RotorOps.fighter_radar_unit_string = 'FIGHTER_DEPLOYMENT' --any unit capable of detecting aircraft by radar can be used as a detection source to spawn intercept fighters, if this string is in the unit name
+RotorOps.fighter_min_detection_alt = 609 --aircraft below this agl altitude (meters) will not be 'detected' by radar units.
+RotorOps.fighter_max_detection_dist = 7000 --default max range from radar to target in order for intercept fighters to spawn (you can also set range for individual radar sources via unit name)
+RotorOps.fighter_max_active = 2 --total maximum active deployed fighters, shared between red/blue
+
trigger.action.outText("ROTOR OPS STARTED: "..RotorOps.version, 5)
env.info("ROTOR OPS STARTED: "..RotorOps.version)
@@ -88,6 +99,7 @@ local cooldown = {
["attack_helo_msg"] = 0,
["attack_plane_msg"] = 0,
["trans_helo_msg"] = 0,
+ ["e_fighters_inbound_msg"] = 0,
}
local zone_defenders_flags = {
'ROPS_A_DEFENDERS',
@@ -202,6 +214,9 @@ RotorOps.gameMsgs = {
transp_helos_toff = {
{'ENEMY TRANSPORT HELICOPTERS INBOUND!', 'enemy_chopper_inbound.ogg'},
},
+ enemy_fighters_inbound = {
+ {'ENEMY FIGHTERS INBOUND!', 'enemy_fighters_inbound.ogg'},
+ },
}
@@ -289,16 +304,19 @@ ctld.addCallback(function(_args)
local unit = _args.unit
local picked_troops = _args.onboard
local dropped_troops = _args.unloaded
- --trigger.action.outText("dbg: ".. mist.utils.tableShow(_args), 5)
+ --env.info("ctld callback: ".. mist.utils.tableShow(_args))
+
+
if action == "load_troops" or action == "extract_troops" then
trigger.action.outSoundForGroup(unit:getGroup():getID() , sound_effects.troop_pickup[math.random(1, #sound_effects.troop_pickup)])
elseif action == "unload_troops_zone" or action == "dropped_troops" then
trigger.action.outSoundForGroup(unit:getGroup():getID() , sound_effects.troop_dropoff[math.random(1, #sound_effects.troop_dropoff)])
if RotorOps.isUnitInZone(unit, RotorOps.active_zone) then
local id = timer.scheduleFunction(RotorOps.gameMsgHandler, RotorOps.gameMsgs.friendly_troops_dropped, timer.getTime() + 6) --allow some extra time so we don't step on the player's troop/unload sound effects
+
end
if dropped_troops.jtac == true then
- local id = timer.scheduleFunction(RotorOps.gameMsgHandler, RotorOps.gameMsgs.jtac, timer.getTime() + 6) --allow some extra time so we don't step on the player's troop/unload sound effects
+ local id = timer.scheduleFunction(RotorOps.gameMsgHandler, RotorOps.gameMsgs.jtac, timer.getTime() + 12) --allow some extra time so we don't step on the player's troop/unload sound effects
end
end
@@ -529,9 +547,9 @@ function RotorOps.deployTroops(quantity, target_group, announce)
debugMsg("DeployTroops on group: "..target_group_obj:getName())
local valid_unit = RotorOps.getValidUnitFromGroup(target_group_obj)
if not valid_unit then return end
- local coalition = valid_unit:getCoalition()
+ local coal = valid_unit:getCoalition()
local side = "red"
- if coalition == 2 then side = "blue" end
+ if coal == 2 then side = "blue" end
local point = valid_unit:getPoint()
ctld.spawnGroupAtPoint(side, quantity, point, 1000)
@@ -831,11 +849,11 @@ function RotorOps.shiftPosition(vars)
local search_radius = vars.radius or 100
local inner_radius = 50 --minimum distance to move for randpointincircle
local first_valid_unit
- if grp:isExist() ~= true then return end
+ if grp and grp:isExist() ~= true then return end
local start_point = vars.point
if not start_point then
- env.info("RotorOps: No point provided, getting current position.")
+ --env.info("RotorOps: No point provided, getting current position.")
for index, unit in pairs(grp:getUnits()) do
if unit:isExist() == true then
first_valid_unit = unit
@@ -865,7 +883,7 @@ function RotorOps.shiftPosition(vars)
if mist.isTerrainValid(rand_point, {'LAND', 'ROAD'}) == true then
path[#path + 1] = mist.ground.buildWP(rand_point, formation, 5)
- env.info("point is valid, adding as waypoint with formation: " .. formation)
+ --env.info("point is valid, adding as waypoint with formation: " .. formation)
break
end
@@ -881,11 +899,11 @@ function RotorOps.guardPosition(vars)
local grp = vars.grp
local search_radius = vars.radius or 100
local first_valid_unit
- if grp:isExist() ~= true then return end
+ if not grp or grp:isExist() ~= true then return end
local start_point = vars.point
if not start_point then
- env.info("RotorOps: No point provided, getting current position.")
+ --env.info("RotorOps: No point provided, getting current position.")
for index, unit in pairs(grp:getUnits()) do
if unit:isExist() == true then
first_valid_unit = unit
@@ -897,13 +915,12 @@ function RotorOps.guardPosition(vars)
start_point = first_valid_unit:getPoint()
end
local object_vol_thresh = 0
- local max_waypoints = 1
local foundUnits = {}
local volS = {
id = world.VolumeType.SPHERE,
params = {
- point = grp:getUnit(1):getPoint(), --check if exists, maybe itterate through grp
+ point = start_point,
radius = search_radius
}
}
@@ -927,9 +944,10 @@ function RotorOps.guardPosition(vars)
--world.searchObjects(Object.Category.BASE, volS, ifFound)
if #foundUnits > 0 then
local path = {}
- path[1] = mist.ground.buildWP(start_point, '', 5)
+ --path[1] = mist.ground.buildWP(RotorOps.getValidUnitFromGroup(grp):getPoint(), '', 2)
+ path[1] = mist.ground.buildWP(RotorOps.getValidUnitFromGroup(grp):getPoint(), '', 2)
local rand_index = math.random(1,#foundUnits)
- path[#path + 1] = mist.ground.buildWP(foundUnits[rand_index]:getPoint(), '', 3)
+ path[#path + 1] = mist.ground.buildWP(foundUnits[rand_index]:getPoint(), '', 2)
mist.goRoute(grp, path)
end
end
@@ -963,6 +981,9 @@ function RotorOps.aiExecute(vars)
local last_task = vars.last_task
local last_zone = vars.last_zone
local group_name = vars.group_name
+ if not vars.group_name or not tableHasKey(RotorOps.ai_tasks, group_name) then
+ return
+ end
local task = RotorOps.ai_tasks[group_name].ai_task
local zone = RotorOps.ai_tasks[group_name].zone
local point = RotorOps.ai_tasks[group_name].point
@@ -999,17 +1020,13 @@ function RotorOps.aiExecute(vars)
local should_update = true
--- if task == last_task then
--- should_update = false
--- end
---
--- if same_zone then
--- should_update = false
--- end
---
--- if task == "patrol" then
--- should_update = true
--- end
+ if RotorOps.persistent_tasking and task == last_task then
+ if task == "move_to_active_zone" or task == "move_to_zone" then
+ if same_zone then
+ should_update = false
+ end
+ end
+ end
if should_update then --check to make sure we don't have the same task
@@ -1225,7 +1242,6 @@ function RotorOps.assessUnitsInZone(var)
percent_staged_remain = math.floor((#staged_units_remaining / #RotorOps.staged_units) * 100)
trigger.action.setUserFlag(RotorOps.staged_units_flag, percent_staged_remain)
trigger.action.setUserFlag('ROPS_ATTACKERS', percent_staged_remain)
- debugMsg("Staged units remaining percent: "..percent_staged_remain.."%")
--is the game finished?
@@ -1316,7 +1332,7 @@ function RotorOps.assessUnitsInZone(var)
end
RotorOps.inf_spawns_avail = RotorOps.inf_spawns_avail - 1
- env.info("ROTOR OPS: Spawned infantry. "..RotorOps.inf_spawns_avail.." spawns remaining in "..zone)
+ env.info("ROTOR OPS: Attempting to spawn infantry. "..RotorOps.inf_spawns_avail.." spawns remaining in "..zone)
end
end
@@ -1377,7 +1393,7 @@ function RotorOps.drawZones() --this could use a lot of work, we should use tri
do
local point = trigger.misc.getZone(zone.name).point
local radius = trigger.misc.getZone(zone.name).radius
- local coalition = -1
+ local coal = -1
local id = index --this must be UNIQUE!
local color = {1, 1, 1, 0.5}
local fill_color = {1, 1, 1, 0.1}
@@ -1392,11 +1408,14 @@ function RotorOps.drawZones() --this could use a lot of work, we should use tri
fill_color = {1, 0, 0, 0.05}
end
if previous_point ~= nill then
- --trigger.action.lineToAll(coalition, id + 200, point, previous_point, color, line_type)
+ --trigger.action.lineToAll(coal, id + 200, point, previous_point, color, line_type)
end
previous_point = point
- trigger.action.circleToAll(coalition, id, point, radius, color, fill_color, line_type)
- trigger.action.textToAll(coalition, id + 100, point, color, text_fill_color, font_size, read_only, text)
+
+ if RotorOps.draw_conflict_zones == true then
+ trigger.action.circleToAll(coal, id, point, radius, color, fill_color, line_type)
+ trigger.action.textToAll(coal, id + 100, point, color, text_fill_color, font_size, read_only, text)
+ end
end
for index, cpz in pairs(ctld.pickupZones) do
@@ -1407,14 +1426,14 @@ function RotorOps.drawZones() --this could use a lot of work, we should use tri
local ctld_zone_status = cpz[4]
local point = pickup_zone.point
local radius = pickup_zone.radius
- local coalition = -1
+ local coal = -1
local id = index + 150 --this must be UNIQUE!
local color = {1, 1, 1, 0.5}
local fill_color = {0, 0.8, 0, 0.1}
local line_type = 5 --1 Solid 2 Dashed 3 Dotted 4 Dot Dash 5 Long Dash 6 Two Dash
if ctld_zone_status == 'yes' or ctld_zone_status == 1 then
env.info("pickup zone is active, drawing it to the map")
- trigger.action.circleToAll(coalition, id, point, radius, color, fill_color, line_type)
+ trigger.action.circleToAll(coal, id, point, radius, color, fill_color, line_type)
end
end
end
@@ -1429,14 +1448,14 @@ function RotorOps.drawZones() --this could use a lot of work, we should use tri
-- local ctld_zone_status = c_zone[4]
-- local point = trigger.misc.getZone(pickup_zone).point
-- local radius = trigger.misc.getZone(pickup_zone).radius
- -- local coalition = -1
+ -- local coal = -1
-- local id = index + 150 --this must be UNIQUE!
-- local color = {1, 1, 1, 0.5}
-- local fill_color = {0, 0.8, 0, 0.1}
-- local line_type = 5 --1 Solid 2 Dashed 3 Dotted 4 Dot Dash 5 Long Dash 6 Two Dash
-- if ctld_zone_status == 'yes' or ctld_zone_status == 1 then
-- --debugMsg("draw the pickup zone")
- -- trigger.action.circleToAll(coalition, id, point, radius, color, fill_color, line_type)
+ -- trigger.action.circleToAll(coal, id, point, radius, color, fill_color, line_type)
-- end
-- end
-- end
@@ -1548,8 +1567,8 @@ function RotorOps.setupCTLD()
ctld.pickupZones[#ctld.pickupZones + 1] = { "BRAVO_FARP", RotorOps.pickup_zone_smoke, -1, "no", 0 }
ctld.pickupZones[#ctld.pickupZones + 1] = { "CHARLIE_FARP", RotorOps.pickup_zone_smoke, -1, "no", 0 }
ctld.pickupZones[#ctld.pickupZones + 1] = { "DELTA_FARP", RotorOps.pickup_zone_smoke, -1, "no", 0 }
- ctld.pickupZones[#ctld.pickupZones + 1] = { "HELO_CARRIER", "none", -1, "no", 0 }
- ctld.pickupZones[#ctld.pickupZones + 1] = { "HELO_CARRIER_1", "none", -1, "no", 0 }
+ ctld.pickupZones[#ctld.pickupZones + 1] = { "HELO_CARRIER", "none", -1, "yes", 0 }
+ ctld.pickupZones[#ctld.pickupZones + 1] = { "HELO_CARRIER_1", "none", -1, "yes", 0 }
ctld.pickupZones[#ctld.pickupZones + 1] = { "troops1", RotorOps.pickup_zone_smoke, -1, "yes", 0 }
ctld.pickupZones[#ctld.pickupZones + 1] = { "troops2", RotorOps.pickup_zone_smoke, -1, "yes", 0 }
ctld.pickupZones[#ctld.pickupZones + 1] = { "troops3", RotorOps.pickup_zone_smoke, -1, "yes", 0 }
@@ -1742,7 +1761,7 @@ function RotorOps.taskByName()
end
end
if RotorOps.ai_task_by_name_scheduler then
- local timer_id = timer.scheduleFunction(RotorOps.taskByName, nil, timer.getTime() + 120)
+ local timer_id = timer.scheduleFunction(RotorOps.taskByName, nil, timer.getTime() + 500)
end
end
@@ -1874,7 +1893,7 @@ function RotorOps.spawnTranspHelos(troops, max_drops)
gp.route.points[#gp.route.points + 1] = mist.heli.buildWP(initial_point, 'flyover', 100, 400, 'agl')
gp.clone = true
local new_group_data = mist.dynAdd(gp) --returns a mist group data table
- debugTable(new_group_data)
+ --debugTable(new_group_data)
-- local new_group = Group.getByName(new_group_data.groupName)
-- local grp_controller = new_group:getController() --controller for aircraft can be group or unit level
-- grp_controller:setOption(AI.Option.Air.id.REACTION_ON_THREAT , AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE)
@@ -1885,6 +1904,273 @@ function RotorOps.spawnTranspHelos(troops, max_drops)
end
+function RotorOps.spawnCapToZone(_target_zone, _spawn_zone, coal)
+ local target_zone = _target_zone
+ if not target_zone then
+ target_zone = RotorOps.getEnemyZones()[math.random(1, #RotorOps.getEnemyZones())]
+ end
+ local zone_point = trigger.misc.getZone(target_zone).point
+ RotorOps.spawnCap(zone_point, _spawn_zone, coal)
+end
+
+RotorOps.fighter_red_source_string = "RED CAP"
+RotorOps.fighter_blue_source_string = "BLUE CAP"
+RotorOps.fighter_engagement_dist = 20
+
+function RotorOps.spawnCap(destination_point, _spawn_zone, coal)
+ local red_zone_string = "RED_CAP_SPAWN"
+ local blue_zone_string = "BLUE_CAP_SPAWN"
+
+ local coal_zone_string = nil
+ if not coal or coal == 0 then
+ return
+ end
+ if coal == 1 then
+ coal_zone_string = red_zone_string
+ source_group_string = RotorOps.fighter_red_source_string
+ end
+ if coal == 2 then
+ coal_zone_string = blue_zone_string
+ source_group_string = RotorOps.fighter_blue_source_string
+ end
+
+ local spawn_zone = _spawn_zone
+ if not _spawn_zone then
+ local spawn_zones = {}
+ for zone, zoneobj in pairs(mist.DBs.zonesByName) do
+ if string.find(zone, coal_zone_string) then
+ spawn_zones[#spawn_zones + 1] = zone
+ --env.info("found cap spawn zone: " .. zone)
+ end
+ end
+ if #spawn_zones < 1 then
+ return
+ end
+ spawn_zone = spawn_zones[math.random(1, #spawn_zones)]
+ end
+
+ local spawn_point = mist.getRandomPointInZone(spawn_zone)
+
+
+ local altitude = math.random(2000,6000)
+ local speed = 300
+
+
+ --pick a template group at random for the source
+ fighter_groups = {} --stores group names of template groups
+ for uName, uData in pairs(mist.DBs.groupsByName) do
+ if string.find(uName, source_group_string) then
+ fighter_groups[#fighter_groups + 1] = uName
+ end
+ end
+
+ if #fighter_groups < 1 then
+ return
+ end
+
+ fighter_group_name = fighter_groups[math.random(1, #fighter_groups)]
+ local group = Group.getByName(fighter_group_name)
+
+ if not group then
+ return
+ end
+
+ local gp = mist.getGroupData(fighter_group_name)
+ --debugTable(gp)
+
+ gp.units[1].alt = altitude
+ gp.units[1].speed = speed
+ gp.units[1].x = spawn_point.x
+ gp.units[1].y = spawn_point.y
+ gp.units[1].heading = mist.utils.getHeadingPoints(spawn_point, destination_point)
+
+
+
+ local engage = {
+ id = 'EngageTargets',
+ params = {
+ maxDist = RotorOps.fighter_engagement_dist,
+ maxDistEnabled = true,
+ targetTypes = { [1] = "Air" },
+ }
+ }
+
+ local orbit = {
+ id = 'Orbit',
+ params = {
+ pattern = 'Race-Track',
+ }
+ }
+
+
+ gp.route = {points = {}}
+ -- gp.route[1] = mist.fixedWing.buildWP(random_airbase:getPoint())
+ -- gp.route[1].type = "TakeOffParking"
+ -- gp.route[1].action = "From Parking Area"
+ -- gp.route[1].airdromeId = airbase_id
+
+ gp.route.points[1] = mist.fixedWing.buildWP(spawn_point, 'turning point', speed, altitude, 'baro')
+
+ gp.route.points[1].task = {}
+ gp.route.points[1].task.id = 'ComboTask'
+ gp.route.points[1].task.params = {}
+ gp.route.points[1].task.params.tasks = {}
+ gp.route.points[1].task.params.tasks[1] = {number = 1, id = 'ControlledTask', enabled = true, params = {task = engage}}
+ gp.route.points[1].task.params.tasks[2] = {number = 2, id = 'ControlledTask', enabled = true, params = {task = orbit}}
+
+ gp.route.points[2] = mist.fixedWing.buildWP(destination_point, 'turning point', speed, altitude, 'baro')
+
+ gp.clone = true
+ local new_group_data = mist.dynAdd(gp) --returns a mist group data table
+ --debugTable(new_group_data)
+ local new_group = Group.getByName(new_group_data.name)
+ if new_group then
+ env.info("RotorOps spawned CAP: "..new_group_data.name)
+ else
+ env.error("RotorOps tried to spawn CAP but something went wrong.")
+ return
+ end
+
+ local grp_controller = new_group:getController() --controller for aircraft can be group or unit level
+ grp_controller:setOption(AI.Option.Air.id.REACTION_ON_THREAT , AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE)
+ grp_controller:setOption(AI.Option.Air.id.FLARE_USING , AI.Option.Air.val.FLARE_USING.WHEN_FLYING_NEAR_ENEMIES)
+ grp_controller:setOption(AI.Option.Air.id.ROE , AI.Option.Air.val.ROE.OPEN_FIRE_WEAPON_FREE)
+ grp_controller:setOption(AI.Option.Air.id.RADAR_USING, AI.Option.Air.val.RADAR_USING.FOR_SEARCH_IF_REQUIRED)
+
+ return new_group_data.name
+
+end
+
+
+
+function RotorOps.deployFighters()
+ local function spawn(dest_point, target_unit, coal)
+ fighter = RotorOps.spawnCap(dest_point, nil, coal)
+
+ if fighter and #fighters_by_detected_unitname < RotorOps.fighter_max_active then
+ _spawn_time = RotorOps.getTime()
+ fighters_by_detected_unitname[target_unit] = {
+ name = fighter,
+ spawn_time = _spawn_time,
+ rtb_time = math.random(_spawn_time + (15 * 60), _spawn_time + (25 * 60)),
+ respawn_time = math.random(_spawn_time + (5 * 60), _spawn_time + (15 * 60)),
+ }
+ if ((RotorOps.getTime() - cooldown["e_fighters_inbound_msg"]) > 90) then
+ RotorOps.gameMsg(RotorOps.gameMsgs.enemy_fighters_inbound)
+ cooldown["e_fighters_inbound_msg"] = RotorOps.getTime()
+ end
+ --debugTable(fighters_by_detected_unitname)
+ env.info(target_unit .. " was detected and we spawned a new fighter group: " .. fighter)
+ end
+ end
+
+ local function rtb(group_name)
+
+ local grp = Group.getByName(group_name)
+ if grp then
+ local coal_airbases = coalition.getAirbases(grp:getCoalition())
+ --debugTable(coal_airbases)
+ random_airbase = coal_airbases[math.random(1, #coal_airbases)]
+
+ local airbase_pos = mist.utils.makeVec2(random_airbase:getPoint())
+ local airbase_id = random_airbase:getID()
+ local rtb = {
+ id = 'Mission',
+ params = {
+ route = {
+ points = {
+ [1] = {
+ alt = 2000,
+ alt_type = "RADIO",
+ speed = 300,
+ x = airbase_pos.x,
+ y = airbase_pos.y,
+ aerodromeId = airbase_id,
+ type = "Land",
+ action = "Landing",
+ }
+ }
+ }
+ }
+ }
+
+ grp:getController():setTask(rtb)
+ env.info(group_name .. " is RTB to ".. random_airbase:getName())
+ end
+ end
+
+
+ --fighter respawning and rtb
+ for target_name, fighter_group_data in pairs(fighters_by_detected_unitname) do
+ local group = Group.getByName(fighter_group_data.name)
+ if group then --if group alive
+ if fighter_group_data.rtb_time < RotorOps.getTime() then
+ env.info(fighter_group_data.name .. " is RTB. Removing from table.")
+ rtb(fighter_group_data.name)
+ fighters_by_detected_unitname[target_name] = nil
+ end
+ else --if group dead
+ if fighter_group_data.respawn_time < RotorOps.getTime() then
+ env.info(fighter_group_data.name .. " has hit respawn_time limit. Removing from table to allow another group to spawn.")
+ fighters_by_detected_unitname[target_name] = nil
+ end
+ end
+ end
+
+
+
+
+ for uName, uData in pairs(mist.DBs.unitsByName) do
+ local str_index = string.find(uName, RotorOps.fighter_radar_unit_string)
+ if str_index then
+ --trigger.action.outText("Found radar unit: " .. uData.unitName, 2)
+ local radar_unit = Unit.getByName(uData.unitName)
+ local max_distance = RotorOps.fighter_max_detection_dist
+ local dist_str = string.sub(uName, str_index + #RotorOps.fighter_radar_unit_string + 1)
+ if #dist_str > 3 then
+ --env.info("RotorOps: Radar unit name has the max detection distance property:".. dist_str)
+ local dist = tonumber(dist_str)
+ if dist and dist > 0 then
+ max_distance = dist
+ end
+ end
+
+ if radar_unit and radar_unit:getLife() > 0 then
+ --trigger.action.outText(uData.unitName .. " is searching for targets. life=" .. radar_unit:getLife(), 2)
+
+ raw_detected_units = radar_unit:getController():getDetectedTargets(Controller.Detection.RADAR)
+ if raw_detected_units then
+ for i, target in pairs(raw_detected_units) do
+ --debugTable(target)
+ if target.object and target.object:getCategory() == Object.Category.UNIT then
+ local detected_unitname = target.object:getName()
+ local target_pos = target.object:getPosition().p
+ local target_distance = mist.utils.get2DDist(radar_unit:getPosition().p, target_pos)
+ local terrain_height = land.getHeight({x = target_pos.x, y = target_pos.z})
+ local target_agl = target_pos.y - terrain_height
+
+ env.info(uData.unitName .. "detected " .. detected_unitname .. " at " .. target_distance .. " agl:" .. target_agl)
+
+ if target_distance <= max_distance and target_agl >= RotorOps.fighter_min_detection_alt then
+ env.info('RotorOps: ' .. uData.unitName .. " has detected "..detected_unitname .. "at agl=" .. target_agl .. " distance=" .. target_distance)
+
+ if tableHasKey(fighters_by_detected_unitname, detected_unitname) then
+ --trigger.action.outText(detected_unitname .. " already in table with " .. fighters_by_detected_unitname[detected_unitname], 2)
+
+ else
+ spawn(target_pos, detected_unitname, radar_unit:getCoalition())
+ end
+
+ end
+ end --end if target.object
+ end --end of raw_detected targets loop
+ end
+
+ end --end of radar_unit
+ end
+ end --end of all units by name loop
+
+end
--- USEFUL PUBLIC 'LUA PREDICATE' FUNCTIONS FOR MISSION EDITOR TRIGGERS (don't forget that DCS lua predicate functions should 'return' these function calls)
@@ -1928,3 +2214,8 @@ function RotorOps.predPlayerInZone(zone_name)
end
end
+--determine if enemy CAP is needed
+function RotorOps.predSpawnRedCap()
+ return true
+end
+
diff --git a/scripts/RotorOpsServer.lua b/scripts/RotorOpsServer.lua
new file mode 100644
index 0000000..efda6a1
--- /dev/null
+++ b/scripts/RotorOpsServer.lua
@@ -0,0 +1,68 @@
+RotorOpsServer = {}
+RotorOpsServer.version = "0.3"
+trigger.action.outText("ROTOROPS SERVER SCRIPT: "..RotorOpsServer.version, 5)
+env.info("ROTOROPS SERVER SCRIPT STARTED: "..RotorOpsServer.version)
+
+--For SpecialK's DCSServerBot
+RotorOpsServer.dcsbot = {}
+RotorOpsServer.dcsbot.enabled = true
+RotorOpsServer.dcsbot.points = {}
+RotorOpsServer.dcsbot.points.troop_drop = 6
+RotorOpsServer.dcsbot.points.unpack = 5
+RotorOpsServer.dcsbot.points.rearm_repair = 3
+
+--Mission Ending
+RotorOpsServer.time_to_end = 600
+
+function RotorOpsServer.endMission(secs)
+ if secs then
+ RotorOpsServer.time_to_end = secs
+ end
+
+ local function countdown()
+ local minutes = math.floor(RotorOpsServer.time_to_end / 60)
+ local seconds = RotorOpsServer.time_to_end - (minutes * 60) --handle as string
+ if seconds < 10 then
+ seconds = "0" .. seconds
+ end
+ trigger.action.outText("RTB now. Mission will end in "..minutes..":"..seconds, 2, true)
+ RotorOpsServer.time_to_end = RotorOpsServer.time_to_end - 1
+ if RotorOpsServer.time_to_end <= 0 then
+ trigger.action.setUserFlag('mission_end', 2)
+ else
+ timer.scheduleFunction(countdown, {}, timer.getTime() + 1)
+ end
+ end
+ countdown()
+end
+
+function RotorOpsServer.registerCtldCallbacks()
+ ctld.addCallback(function(_args)
+ local action = _args.action
+ local unit = _args.unit
+ local picked_troops = _args.onboard
+ local dropped_troops = _args.unloaded
+ --env.info("ctld callback: ".. mist.utils.tableShow(_args))
+
+ local playername = unit:getPlayerName()
+ if RotorOpsServer.dcsbot.enabled and dcsbot and playername then
+ if action == "unload_troops_zone" or action == "dropped_troops" then
+ if RotorOps.isUnitInZone(unit, RotorOps.active_zone) then
+ env.info('RotorOpsServer: adding points (unload troops in active zone) for ' ..playername)
+ net.send_chat(playername .. " dropped troops into the active zone. [" .. RotorOpsServer.dcsbot.points.troop_drop .. " points]")
+ dcsbot.addUserPoints(playername, RotorOpsServer.dcsbot.points.troop_drop)
+ end
+ elseif action == "rearm" or action == "repair" then
+ env.info('RotorOpsServer: adding points (rearm/repair) for ' ..playername)
+ net.send_chat(playername .. " repaired/rearmed our defenses. [" .. RotorOpsServer.dcsbot.points.rearm_repair .. " points]")
+ dcsbot.addUserPoints(playername, RotorOpsServer.dcsbot.points.rearm_repair)
+ elseif action == "unpack" then
+ env.info('RotorOpsServer: adding points (unpack) for ' ..playername)
+ net.send_chat(playername .. " unpacked ground units. [" .. RotorOpsServer.dcsbot.points.unpack .. " points]")
+ dcsbot.addUserPoints(playername, RotorOpsServer.dcsbot.points.unpack)
+ end
+ end
+ end)
+end
+
+RotorOpsServer.registerCtldCallbacks()
diff --git a/server/user-files/modules/mapscript.py b/server/user-files/modules/mapscript.py
deleted file mode 100644
index fe174d0..0000000
--- a/server/user-files/modules/mapscript.py
+++ /dev/null
@@ -1,66 +0,0 @@
-# A script for creating the modules map file
-
-import os
-import yaml
-
-print("Current dir: " + os.getcwd())
-modules = []
-module_folders = next(os.walk('.'))[1]
-for folder in module_folders:
-
- valid_module = False
- module_filenames = []
- module = {}
- print("searching folder: " + folder)
-
- for filename in os.listdir(folder):
- module_filenames.append(filename)
-
- # assume the yaml file is our scenario configuration file
- if filename.endswith(".yaml"):
- #print("found config file: " + filename)
- stream = file(os.path.join(folder, filename), 'r')
- config = yaml.load(stream)
- #print("Config file yaml: " + str(config))
-
- if 'name' in config:
- print("Config file has name: " + config['name'])
- valid_module = True
- module['name'] = config['name']
-
- if valid_module:
- print("Populating module attributes for " + folder)
- module['id'] = folder
- module['dist'] = 'add'
- module['path'] = 'templates\Scenarios\downloaded'
- module['files'] = module_filenames
-
- if 'version' in config:
- module['version'] = config['version']
- else:
- module['version'] = 1
-
- if 'requires' in config:
- module['requires'] = config['requires']
- else:
- module['requires'] = 1
-
- modules.append(module)
-
-print("Found modules: " + str(len(modules)))
-
-if len(modules) > 0:
- modulemap = {}
- #print(str(modules))
- for m in modules:
- print("adding module: " + m["id"])
- modulemap[m['id']] = m
-
- with open('module-map.yaml', 'w') as mapfile:
- print("Creating map file...")
- yaml.dump(modulemap, mapfile)
- print("Success.")
-
-
-
-
diff --git a/voiceovers/rotorops template.aup3 b/voiceovers/rotorops template.aup3
new file mode 100644
index 0000000..33a8d58
Binary files /dev/null and b/voiceovers/rotorops template.aup3 differ
diff --git a/voiceovers/rotorops_radio_effect.txt b/voiceovers/rotorops_radio_effect.txt
new file mode 100644
index 0000000..b6c6644
--- /dev/null
+++ b/voiceovers/rotorops_radio_effect.txt
@@ -0,0 +1,2 @@
+GraphicEq:f0="20" f1="20.798237" f10="29.579803" f100="1001.5735" f101="1041.5482" f102="1083.1183" f103="1126.3476" f104="1171.3022" f105="1218.051" f106="1266.6657" f107="1317.2207" f108="1369.7934" f109="1424.4644" f11="30.760388" f110="1481.3174" f111="1540.4395" f112="1601.9213" f113="1665.857" f114="1732.3444" f115="1801.4855" f116="1873.3861" f117="1948.1564" f118="2025.911" f119="2106.7688" f12="31.988092" f120="2190.8539" f121="2278.2949" f122="2369.2259" f123="2463.7861" f124="2562.1204" f125="2664.3793" f126="2770.7196" f127="2881.3042" f128="2996.3024" f129="3115.8904" f13="33.264796" f130="3240.2513" f131="3369.5758" f132="3504.0618" f133="3643.9154" f134="3789.3508" f135="3940.5908" f136="4097.8671" f137="4261.4206" f138="4431.5018" f139="4608.3712" f14="34.592456" f140="4792.2998" f141="4983.5694" f142="5182.4729" f143="5389.315" f144="5604.4126" f145="5828.095" f146="6060.7051" f147="6302.5991" f148="6554.1475" f149="6815.7357" f15="35.973105" f150="7087.7643" f151="7370.6501" f152="7664.8264" f153="7970.7439" f154="8288.871" f155="8619.6952" f156="8963.7232" f157="9321.482" f158="9693.5197" f159="10080.406" f16="37.408858" f160="10482.734" f161="10901.119" f162="11336.203" f163="11788.652" f164="12259.159" f165="12748.444" f166="13257.258" f167="13786.38" f168="14336.62" f169="14908.821" f17="38.901915" f170="15503.86" f171="16122.648" f172="16766.133" f173="17435.3" f174="18131.175" f175="18854.824" f176="19607.355" f177="20389.921" f178="21203.72" f179="22050" f18="40.454562" f19="42.069179" f2="21.628333" f20="43.748238" f21="45.494311" f22="47.310073" f23="49.198306" f24="51.161902" f25="53.203868" f26="55.327333" f27="57.535549" f28="59.8319" f29="62.219902" f3="22.49156" f30="64.703213" f31="67.285639" f32="69.971133" f33="72.763811" f34="75.667949" f35="78.687997" f36="81.828581" f37="85.094511" f38="88.490791" f39="92.022623" f4="23.38924" f40="95.695416" f41="99.514797" f42="103.48662" f43="107.61696" f44="111.91215" f45="116.37877" f46="121.02367" f47="125.85395" f48="130.87701" f49="136.10055" f5="24.322748" f50="141.53258" f51="147.18141" f52="153.05569" f53="159.16443" f54="165.51697" f55="172.12306" f56="178.99281" f57="186.13675" f58="193.56581" f59="201.29138" f6="25.293514" f60="209.32529" f61="217.67985" f62="226.36786" f63="235.40262" f64="244.79797" f65="254.56831" f66="264.72861" f67="275.29442" f68="286.28193" f69="297.70797" f7="26.303025" f70="309.59005" f71="321.94636" f72="334.79583" f73="348.15816" f74="362.05379" f75="376.50403" f76="391.53101" f77="407.15773" f78="423.40815" f79="440.30716" f8="27.352827" f80="457.88063" f81="476.1555" f82="495.15975" f83="514.92249" f84="535.474" f85="556.84576" f86="579.07051" f87="602.18228" f88="626.21649" f89="651.20995" f9="28.444529" f90="677.20095" f91="704.2293" f92="732.33639" f93="761.56529" f94="791.96078" f95="823.5694" f96="856.43958" f97="890.62167" f98="926.16803" f99="963.13312" FilterLength="8191" InterpolateLin="0" InterpolationMethod="B-spline" v0="-17.5" v1="-18.946249" v10="-20" v100="3.9716162" v101="3.4356755" v102="2.8990024" v103="2.3628477" v104="1.8728232" v105="1.4518665" v106="1.1103325" v107="0.84965403" v108="0.64540986" v109="0.49694949" v11="-20" v110="0.37942455" v111="0.28013427" v112="0.19884427" v113="0.13048421" v114="0.084440866" v115="0.060392583" v116="0.045452917" v117="0.032927052" v118="0.022814989" v119="0.015116727" v12="-20" v120="0.0098322676" v121="0.0067422748" v122="0.0044079518" v123="0.0025677994" v124="0.0012456946" v125="0.00040470987" v126="2.4407598e-05" v127="0" v128="0" v129="0" v13="-20" v130="0" v131="0" v132="0" v133="0" v134="0" v135="0" v136="0" v137="0" v138="0" v139="0" v14="-20" v140="0" v141="0" v142="0" v143="0" v144="0" v145="0.06651144" v146="0.27630236" v147="0.62932393" v148="1.1073901" v149="1.719646" v15="-20" v150="2.4660915" v151="3.211126" v152="3.7953877" v153="4.2186218" v154="4.4921214" v155="4.5833302" v156="4.4903595" v157="4.3491495" v158="4.269459" v159="4.251288" v16="-20" v160="4.2946366" v161="4.3995048" v162="4.5638535" v163="4.7639394" v164="4.9929578" v165="5.2377811" v166="5.494583" v167="5.7750252" v168="6.076484" v169="6.3676321" v17="-20" v170="6.6393407" v171="6.8966357" v172="7.1526226" v173="7.3848182" v174="7.5777281" v175="7.5415697" v176="7.2201181" v177="6.6133733" v178="5.7213353" v179="4.5440041" v18="-20" v19="-20" v2="-19.777302" v20="-20" v21="-20" v22="-20" v23="-20" v24="-20" v25="-20" v26="-20" v27="-20" v28="-20" v29="-20" v3="-20" v30="-20" v31="-20" v32="-20" v33="-20" v34="-20" v35="-20" v36="-20" v37="-20" v38="-20" v39="-20" v4="-20" v40="-20" v41="-20" v42="-20" v43="-20" v44="-20" v45="-20" v46="-20" v47="-20" v48="-20" v49="-20" v5="-20" v50="-20" v51="-20" v52="-20" v53="-20" v54="-20" v55="-20" v56="-20" v57="-20" v58="-20" v59="-20" v6="-20" v60="-20" v61="-20" v62="-20" v63="-20" v64="-20" v65="-20" v66="-20" v67="-20" v68="-19.981896" v69="-19.841306" v7="-20" v70="-19.561522" v71="-19.15139" v72="-18.616033" v73="-17.950401" v74="-17.15336" v75="-16.218252" v76="-15.143991" v77="-13.889471" v78="-12.431425" v79="-10.813891" v8="-20" v80="-9.076468" v81="-7.3805989" v82="-5.7435465" v83="-4.2054572" v84="-2.7359738" v85="-1.3213212" v86="0.018400267" v87="1.2415582" v88="2.3468769" v89="3.3087249" v9="-20" v90="4.1550404" v91="4.8910671" v92="5.4918504" v93="5.9048598" v94="6.1284157" v95="6.1590838" v96="5.9749911" v97="5.5737599" v98="5.0413005" v99="4.5068245"
+Distortion:DC_Block="0" Noise_Floor="-70" Parameter_1="59" Parameter_2="50" Repeats="1" Threshold_dB="-6" Type="Hard Overdrive"