RotorOps/Generator/MissionGenerator.py
spencershepard bca47d63d6
BHD2 and infantry update (#48)
* BHD2 and infantry update

- default to easy comms
- modify ctld for better infantry models
- cleaner logging
- warn user if no resources for fat cow FARPs
- condition for fat cow added; not available if enemies too close

* Update README.md
2023-02-11 20:54:37 -08:00

1024 lines
41 KiB
Python

import json
import yaml
import sys
import os
import RotorOpsMission as ROps
import RotorOpsUnits
import version
import user
import logging
import requests
from packaging import version as ver
from PyQt5.QtWidgets import (
QApplication, QDialog, QMainWindow, QMessageBox, QCheckBox, QSpinBox, QSplashScreen, QFileDialog, QRadioButton,
QInputDialog, QDialogButtonBox, QVBoxLayout, QLabel, QComboBox
)
from PyQt5 import QtGui
from PyQt5.QtGui import QPixmap, QFont
from PyQt5.QtCore import QObject, QEvent, Qt, QUrl
import resources # pyqt resource file
from MissionGeneratorUI import Ui_MainWindow
import qtmodern.styles
import qtmodern.windows
modules_version = 2
modules_url = 'https://dcs-helicopters.com/user-files/modules/'
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']
version.version_url = 'https://dcs-helicopters.com/app-updates/versioncheck.yaml'
#Setup logfile and exception handler
logger = logging.getLogger(__name__)
logging.basicConfig(filename='generator.log', encoding='utf-8', level=logging.DEBUG, format='%(asctime)s %(levelname)-8s %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
handler = logging.StreamHandler(stream=sys.stdout)
logger.addHandler(handler)
class directories:
home_dir = scenarios = forces = scripts = sound = output = assets = imports = user_datafile_path = scenarios_downloaded = scenarios_user = default_config = None
@classmethod
def find(cls):
current_dir = os.getcwd()
if os.path.basename(os.getcwd()) == "Generator":
os.chdir("..")
cls.home_dir = os.getcwd()
cls.scenarios = cls.home_dir + "\\templates\\Scenarios"
cls.forces_downloaded = cls.home_dir + "\\templates\\Forces\\downloaded"
cls.forces_user = cls.home_dir + "\\templates\\Forces\\user"
cls.scripts = cls.home_dir + "\\scripts"
cls.sound = cls.home_dir + "\\sound\\embedded"
cls.output = cls.home_dir + "\\MissionOutput"
cls.assets = cls.home_dir + "\\assets"
cls.imports_downloaded = cls.home_dir + "\\templates\\Imports\\downloaded"
cls.imports_user = cls.home_dir + "\\templates\\Imports\\user"
cls.user_datafile_path = cls.home_dir + "\\config\\user-data.yaml"
cls.scenarios_downloaded = cls.scenarios + "\\downloaded"
cls.scenarios_user = cls.scenarios + "\\user"
cls.default_config = cls.home_dir + '\\config\\default-config.yaml'
os.chdir(current_dir)
@classmethod
def createDirectories(cls):
required_dirs = [cls.scenarios_user, cls.scenarios_downloaded, cls.imports_user, cls.imports_downloaded, cls.forces_user, cls.forces_downloaded, cls.output]
for path in required_dirs:
if not os.path.exists(path):
os.makedirs(path)
directories.find()
directories.createDirectories()
import MissionGeneratorTemplates
def handle_exception(exc_type, exc_value, exc_traceback):
if issubclass(exc_type, KeyboardInterrupt): #example of handling error subclasses
sys.__excepthook__(exc_type, exc_value, exc_traceback)
return
QApplication.restoreOverrideCursor()
logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))
msg = QMessageBox()
msg.setWindowTitle("Uncaught exception")
msg.setText("Oops, there was a problem. Please check the log file for more details or post it in the RotorOps discord where some helpful people will have a look. \n\n" + str(exc_value))
x = msg.exec_()
sys.excepthook = handle_exception
defenders_text = "Defending Forces:"
attackers_text = "Attacking Forces:"
ratings_json = None
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.version_string
windll.shell32.SetCurrentProcessExplicitAppUserModelID(appid)
except ImportError:
pass
class Window(QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
super().__init__(parent)
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
logger.info('running in a PyInstaller bundle')
qtmodern.styles._STYLESHEET = directories.assets + '/style.qss'
qtmodern.windows._FL_STYLESHEET = directories.assets + '/frameless.qss'
else:
logger.info('running in a normal Python process')
self.userid = None
self.scenarios_list = []
self.scenario = None
self.player_slots = []
self.user_output_dir = None
self.user_data = None
self.forces_list = []
self.imports_list = []
self.user_data = self.loadUserData()
self.setupUi(self)
self.connectSignalsSlots()
self.populateScenarios()
self.populateForces()
self.populateSlotSelection()
self.getImports()
# self.blue_forces_label.setText(attackers_text)
# self.red_forces_label.setText(defenders_text)
self.background_label.setPixmap(QtGui.QPixmap(directories.assets + "/rotorops-dkgray.png"))
self.statusbar = self.statusBar()
self.statusbar.setStyleSheet(
"QStatusBar{padding-left:5px;}")
self.version_label.setText("Version " + version.version_string)
self.scenarioChanged()
self.time_comboBox.addItem("Default Time")
self.time_comboBox.addItem("Day")
self.time_comboBox.addItem("Night")
self.time_comboBox.addItem("Dusk")
self.time_comboBox.addItem("Dawn")
self.time_comboBox.addItem("Noon")
self.time_comboBox.addItem("Random")
def connectSignalsSlots(self):
self.action_generateMission.triggered.connect(self.generateMissionAction)
self.action_scenarioSelected.triggered.connect(self.scenarioChanged)
self.action_defensiveModeChanged.triggered.connect(self.defensiveModeChanged)
self.action_nextScenario.triggered.connect(self.nextScenario)
self.action_prevScenario.triggered.connect(self.prevScenario)
self.actionSave_Directory.triggered.connect(self.chooseSaveDir)
self.action_slotChanged.triggered.connect(self.slotChanged)
self.actionCaucasus.triggered.connect(self.filterMenuTouched)
self.actionPersian_Gulf.triggered.connect(self.filterMenuTouched)
self.actionMarianas.triggered.connect(self.filterMenuTouched)
self.actionNevada.triggered.connect(self.filterMenuTouched)
self.actionSyria.triggered.connect(self.filterMenuTouched)
self.actionMultiplayer.triggered.connect(self.filterMenuTouched)
self.actionSingle_Player.triggered.connect(self.filterMenuTouched)
self.actionCo_Op.triggered.connect(self.filterMenuTouched)
self.action_rateButton1.triggered.connect(self.rateButtonActionOne)
self.action_rateButton2.triggered.connect(self.rateButtonActionTwo)
self.action_rateButton3.triggered.connect(self.rateButtonActionThree)
self.action_rateButton4.triggered.connect(self.rateButtonActionFour)
self.action_rateButton5.triggered.connect(self.rateButtonActionFive)
# Find the selected dropdown menu options and make a list of tags to filter for
def tagsFromMenuOptions(self):
tags = []
maps = []
if self.actionCaucasus.isChecked():
maps.append('Caucasus')
if self.actionPersian_Gulf.isChecked():
maps.append('PersianGulf')
if self.actionMarianas.isChecked():
maps.append('Marianas')
if self.actionNevada.isChecked():
maps.append('Nevada')
if self.actionSyria.isChecked():
maps.append('Syria')
if self.actionMultiplayer.isChecked():
tags.append('MultiPlayer')
if self.actionSingle_Player.isChecked():
tags.append('SinglePlayer')
if self.actionCo_Op.isChecked():
tags.append('CoOp')
return maps, tags
def populateScenarios(self):
QApplication.setOverrideCursor(Qt.WaitCursor)
self.scenario_comboBox.clear()
scenarios = []
for path in [directories.scenarios_downloaded, directories.scenarios_user]:
logger.info("Looking for mission files in " + path)
os.chdir(path)
module_folders = next(os.walk('.'))[1]
for folder in module_folders:
for filename in os.listdir(folder):
if filename.endswith(".miz"):
basename = filename.removesuffix('.miz')
mizpath = os.path.join(path, folder, filename)
# create scenario object
s = MissionGeneratorTemplates.Scenario(mizpath, basename)
#apply some properties if found in the downloads directory
if path == directories.scenarios_downloaded:
package_name = folder
s.downloadable = True
s.packageID = folder
if ratings_json:
for module in ratings_json:
if module['package'] == folder:
s.rating = module["avg_rating"]
s.rating_qty = module["rating_count"]
config_file_path = os.path.join(path, folder, basename + '.yaml')
if os.path.exists(config_file_path):
config = self.loadScenarioConfig(config_file_path)
if config:
s.applyConfig(config)
# all the scenarios we can find
scenarios.append(s)
#remove scenarios if they don't match filter criteria
filter_maps, filter_tags = self.tagsFromMenuOptions()
# remove scenarios if map not selected in menu
for s in scenarios:
if s.map_name and not s.map_name in filter_maps:
scenarios.remove(s)
# add scenarios if tags match
if len(filter_tags) > 0:
t_scenarios = []
for s in scenarios:
if s.tags: #if the config file has tags set
for tag in filter_tags:
if tag in s.tags:
t_scenarios.append(s)
else: #add if no tags set
t_scenarios.append(s)
scenarios = t_scenarios.copy()
self.scenarios_list = sorted(scenarios, key=lambda x: x.name, reverse=False)
for s in self.scenarios_list:
self.scenario_comboBox.addItem(s.name)
QApplication.restoreOverrideCursor()
def filterMenuTouched(self):
self.populateScenarios()
# self.scenarioChanged() haven't tried yet
def populateForces(self):
self.forces_list = []
for path in [directories.forces_downloaded, directories.forces_user]:
logger.info("Looking for forces files in " + path)
os.chdir(path)
module_folders = next(os.walk('.'))[1]
for folder in module_folders:
for filename in os.listdir(folder):
if filename.endswith(".miz"):
basename = filename.removesuffix('.miz')
mizpath = os.path.join(path, folder, filename)
config_file_path = os.path.join(path, folder, basename + '.yaml')
if os.path.exists(config_file_path):
# create forces object with config
try:
config = yaml.safe_load(open(config_file_path))
f = MissionGeneratorTemplates.Forces(mizpath, filename, config)
self.forces_list.append(f)
except:
logger.error("Error in " + config_file_path)
else:
# create forces object without config
f = MissionGeneratorTemplates.Forces(mizpath, basename)
self.forces_list.append(f)
self.forces_list = sorted(self.forces_list, key=lambda x: x.name, reverse=False)
for forces in self.forces_list:
self.redforces_comboBox.addItem(forces.name)
self.blueforces_comboBox.addItem(forces.name)
def getImports(self):
self.imports_list = []
for path in [directories.imports_downloaded, directories.imports_user]:
logger.info("Looking for imports files in " + path)
os.chdir(path)
module_folders = next(os.walk('.'))[1]
for folder in module_folders:
for filename in os.listdir(folder):
if filename.endswith(".miz"):
basename = filename.removesuffix('.miz')
mizpath = os.path.join(path, folder, filename)
config_file_path = os.path.join(path, folder, basename + '.yaml')
if os.path.exists(config_file_path):
# create imports object with config
try:
config = yaml.safe_load(config_file_path)
f = MissionGeneratorTemplates.Import(mizpath, filename, config)
self.imports_list.append(f)
except:
logger.error("Error in " + config_file_path)
else:
# create imports object without config
f = MissionGeneratorTemplates.Import(mizpath, filename)
self.imports_list.append(f)
def populateSlotSelection(self):
self.slot_template_comboBox.addItem("Multiple Slots")
for type in RotorOpsUnits.player_helos:
self.slot_template_comboBox.addItem(type.id)
self.slot_template_comboBox.addItem("None")
def slotChanged(self):
if self.slot_template_comboBox.currentIndex() == 0:
sd = self.slotDialog(self)
sd.exec_()
if sd.helicopter_types:
self.user_data["player_slots"] = sd.helicopter_types
self.player_slots = sd.helicopter_types
self.saveUserData()
def defensiveModeChanged(self):
print("defensive checkbox changed")
def lockedSlot(self):
return self.slot_template_comboBox.findText("Locked to Scenario")
def loadScenarioConfig(self, filename):
try:
j = open(filename)
config = yaml.safe_load(j)
j.close()
return config
except yaml.parser.ParserError as e:
logger.error("Unable to load configuration file. Invalid yaml: " + filename)
return None
except OSError as e:
logger.error(e)
return None
def applyScenarioConfig(self, config):
# reset some UI elements
self.defense_checkBox.setEnabled(True)
self.slot_template_comboBox.removeItem(self.lockedSlot())
self.slot_template_comboBox.setEnabled(True)
self.slot_template_comboBox.setCurrentIndex(0)
try:
if 'player_spawn' in config and config['player_spawn'] == "fixed":
self.slot_template_comboBox.addItem("Locked to Scenario")
self.slot_template_comboBox.setCurrentIndex(self.lockedSlot())
self.slot_template_comboBox.setEnabled(False)
if 'checkboxes' in config:
for box in config['checkboxes']:
qobj = QObject.findChild(self, QCheckBox, box)
if qobj:
qobj.setChecked(config['checkboxes'][box])
for box in QObject.findChildren(self, QCheckBox):
if 'disable_checkboxes' in config and config['disable_checkboxes'] is not None and box.objectName() in config['disable_checkboxes']:
box.setEnabled(False)
else:
box.setEnabled(True)
if 'spinboxes' in config:
for box in config['spinboxes']:
qobj = QObject.findChild(self, QSpinBox, box)
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)
for button in QObject.findChildren(self, QRadioButton):
if 'disable_radiobuttons' in config and config['disable_radiobuttons'] is not None and button.objectName() in config['disable_radiobuttons']:
button.setEnabled(False)
else:
button.setEnabled(True)
if 'blue_forces' in config:
for template in self.forces_list:
if template.basename == config['blue_forces']:
self.blueforces_comboBox.setCurrentIndex(self.blueforces_comboBox.findText(template.name))
if 'red_forces' in config:
for template in self.forces_list:
if template.basename == config['red_forces']:
self.redforces_comboBox.setCurrentIndex(self.redforces_comboBox.findText(template.name))
except Exception as e:
logger.error("Error loading config file: " + str(e))
def loadUserData(self):
prefs = {}
if os.path.exists(directories.user_datafile_path):
try:
with open(directories.user_datafile_path, 'r') as pfile:
prefs = yaml.safe_load(pfile)
if "save_directory" in prefs:
self.user_output_dir = prefs["save_directory"]
if "player_slots" in prefs:
self.player_slots = prefs["player_slots"]
if "ratings" in prefs:
self.user_ratings = prefs["ratings"]
except:
logger.error("Could not load prefs.yaml")
if not prefs:
prefs = {}
return prefs
def saveUserData(self):
with open(directories.user_datafile_path, 'w') as pfile:
yaml.dump(self.user_data, pfile)
def chooseSaveDir(self):
dlg = QFileDialog()
dlg.setFileMode(QFileDialog.Directory)
if "save_directory" in self.user_data:
dlg.setDirectory(self.user_data["save_directory"])
if dlg.exec_():
path = dlg.directory().absolutePath()
if path:
self.user_data["save_directory"] = path
self.user_output_dir = path
self.saveUserData()
def scenarioChanged(self):
if len(self.scenarios_list) <= 0:
return
QApplication.setOverrideCursor(Qt.WaitCursor)
self.slot_template_comboBox.setCurrentIndex(0)
self.scenario = self.scenarios_list[self.scenario_comboBox.currentIndex()]
# reset generator options to default
default_config = self.loadScenarioConfig(directories.default_config)
self.applyScenarioConfig(default_config)
if self.scenario.config:
self.applyScenarioConfig(self.scenario.config)
path = self.scenario.path.removesuffix(".miz") + ".jpg"
if os.path.isfile(path):
self.missionImage.setPixmap(QtGui.QPixmap(path))
else:
self.missionImage.setPixmap(QtGui.QPixmap(directories.assets + "/briefing1.png"))
self.scenario.evaluateMiz()
self.description_textBrowser.setText(self.scenario.description)
QApplication.restoreOverrideCursor()
rate_buttons = [
self.rateButton1,
self.rateButton2,
self.rateButton3,
self.rateButton4,
self.rateButton5,
]
# Star rating buttons
star_full_ss = "border-image:url(:/images/star_full);"
star_empty_ss = "border-image:url(:/images/star_empty);"
for button in rate_buttons:
button.setStyleSheet(star_empty_ss)
if self.user_data and 'local_ratings' in self.user_data and self.scenario.path in self.user_data["local_ratings"]:
user_rating = self.user_data['local_ratings'][self.scenario.path]
for i in range(user_rating):
rate_buttons[i].setStyleSheet(star_full_ss)
def generateMissionAction(self):
QApplication.setOverrideCursor(Qt.WaitCursor)
red_forces = self.forces_list[self.redforces_comboBox.currentIndex()]
blue_forces = self.forces_list[self.blueforces_comboBox.currentIndex()]
scenario_name = self.scenario.name
scenario_path = self.scenario.path
credits = ("'" + scenario_name + "' mission template by " + self.scenario.author + "\n" +
"'" + red_forces.name + "' by " + red_forces.author + "\n" +
"'" + blue_forces.name + "' by " + blue_forces.author + "\n"
)
objects = {
"imports": self.imports_list,
}
# holds our generator options. We'll pull from the UI or the scenario config file
data = {
"objects": objects,
"credits": credits,
"scenario_file": scenario_path,
"scenario_name": scenario_name,
"red_forces_path": red_forces.path,
"blue_forces_path": blue_forces.path,
"red_quantity": self.redqty_spinBox.value(),
"blue_quantity": self.blueqty_spinBox.value(),
"inf_spawn_qty": self.inf_spawn_spinBox.value(),
"apc_spawns_inf": self.apcs_spawn_checkBox.isChecked(),
"e_attack_helos": self.e_attack_helos_spinBox.value(),
"e_attack_planes": self.e_attack_planes_spinBox.value(),
"crates": self.logistics_crates_checkBox.isChecked(),
"f_awacs": self.awacs_checkBox.isChecked(),
"f_tankers": self.tankers_checkBox.isChecked(),
"voiceovers": self.voiceovers_checkBox.isChecked(),
"force_offroad": self.scenario.getConfigValue("force_offroad", default=False),
"game_display": self.game_status_checkBox.isChecked(),
"defending": self.defense_checkBox.isChecked(),
"slots": self.slot_template_comboBox.currentText(),
"zone_farps": self.farp_buttonGroup.checkedButton().objectName(),
"e_transport_helos": self.e_transport_helos_spinBox.value(),
"transport_drop_qty": self.troop_drop_spinBox.value(),
"smoke_pickup_zones": self.smoke_pickup_zone_checkBox.isChecked(),
"player_slots": self.player_slots,
"player_hotstart": self.hotstart_checkBox.isChecked(),
"random_weather": self.random_weather_checkBox.isChecked(),
"time": self.time_comboBox.currentText(),
"start_trigger": self.scenario.getConfigValue("start_trigger", default=True),
"end_trigger": self.scenario.getConfigValue("end_trigger", default=True),
"farp_spawns": self.farp_spawn_checkBox.isChecked(),
"staging_logistics_file": self.scenario.getConfigValue("staging_logistics_file", default=None),
"zone_farp_file": self.scenario.getConfigValue("zone_farp_file", default=None),
"defensive_farp_file": self.scenario.getConfigValue("defensive_farp_file", default=None),
"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),
"perks": self.perks_checkBox.isChecked(),
"easy_comms": self.scenario.getConfigValue("easy_comms", default=True)
}
logger.info("Generating mission with options:")
logger.info(str(data))
n = ROps.RotorOpsMission()
result = n.generateMission(self, data)
QApplication.restoreOverrideCursor()
#display results
if result["success"]:
logger.info(result["filename"] + "' successfully generated in " + result["directory"])
self.statusbar.showMessage(result["filename"] + "' successfully generated in " + result["directory"], 10000)
msg = QMessageBox()
msg.setWindowTitle("Mission Generated")
msg.setText("Awesome, your mission is ready! It's located in this directory: \n" +
result["directory"] + "\n" +
"\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" +
"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" +
"Have fun! \n"
)
x = msg.exec_()
elif not result["success"]:
logger.warning(result["failure_msg"])
msg = QMessageBox()
msg.setWindowTitle("Error")
msg.setText(result["failure_msg"])
x = msg.exec_()
def prevScenario(self):
self.scenario_comboBox.setCurrentIndex((self.scenario_comboBox.currentIndex() - 1))
def nextScenario(self):
self.scenario_comboBox.setCurrentIndex((self.scenario_comboBox.currentIndex() + 1))
# 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 slotDialog(QDialog):
def __init__(self, window, parent=None):
QDialog.__init__(self, parent)
self.setWindowTitle("Multiplayer Slots")
self.layout = QVBoxLayout()
self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) # remove help button
message = QLabel("Add your desired multiplayer slots here. \nIt is recommended to check placement in the \nMission Editor before flying your mission.\n")
self.layout.addWidget(message)
self.helicopter_types = None
self.slot_qty = len(window.player_slots)
self.window = window
#self.populateBoxes()
QBtn = QDialogButtonBox.Ok
self.buttonBox = QDialogButtonBox(QBtn)
self.addBtn = self.buttonBox.addButton("+", QDialogButtonBox.ActionRole)
self.removeBtn = self.buttonBox.addButton("-", QDialogButtonBox.ActionRole)
self.layout.addWidget(self.buttonBox)
self.buttonBox.accepted.connect(self.accepted)
self.buttonBox.rejected.connect(self.close)
self.addBtn.clicked.connect(self.addSlotBox)
self.removeBtn.clicked.connect(self.removeSlotBox)
self.slot_boxes = []
if "player_slots" in window.user_data:
for index in range(0, len(window.user_data["player_slots"])):
self.addSlotBox()
self.setLayout(self.layout)
def populateBoxes(self):
for index in range(0, self.slot_qty):
self.slot_boxes.append(QComboBox())
for type in RotorOpsUnits.player_helos:
self.slot_boxes[index].addItem(type.id)
for index in range(0, self.slot_qty):
self.layout.addWidget(self.slot_boxes[index])
#self.slot_boxes[index].setCurrentIndex(self.slot_boxes[index].findText(self.window.user_data["player_slots"][index]))
def addSlotBox(self):
new_slot = QComboBox()
self.slot_boxes.append(new_slot)
self.layout.addWidget(new_slot)
for helo_type in RotorOpsUnits.player_helos:
new_slot.addItem(helo_type.id)
slot_index = len(self.slot_boxes) - 1
if "player_slots" not in self.window.user_data:
new_slot.setCurrentIndex(0)
elif slot_index < len(self.window.user_data["player_slots"]):
new_slot.setCurrentIndex(new_slot.findText(self.window.user_data["player_slots"][slot_index]))
return new_slot
def removeSlotBox(self):
last_index = len(self.slot_boxes) - 1
self.layout.removeWidget(self.slot_boxes[last_index])
self.slot_boxes.pop(last_index)
def accepted(self):
heli_types = []
for box in self.slot_boxes:
heli_types.append(box.currentText())
self.helicopter_types = heli_types
self.close()
def rateScenario(self, rating):
if "local_ratings" not in self.user_data:
self.user_data["local_ratings"] = {}
self.user_data["local_ratings"][self.scenario.path] = rating
self.saveUserData()
self.scenarioChanged()
if not self.scenario.downloadable:
return
params = {}
params["userid"] = self.userid
params["package"] = self.scenario.packageID
params["rating"] = rating
QApplication.setOverrideCursor(Qt.WaitCursor)
r = requests.get(ratings_url, allow_redirects=False, timeout=7, params=params)
QApplication.restoreOverrideCursor()
if r.status_code == 200:
logger.info("Rating successfully submitted for " + self.scenario.packageID)
msg = QMessageBox()
msg.setWindowTitle("Success")
msg.setText("Thank you for submitting a " + str(rating) + " star review for " + self.scenario.name + ". If you have previously submitted a rating for this mission, it will be updated.")
msg.setIcon(QMessageBox.Icon.Information)
x = msg.exec_()
def rateButtonActionOne(self):
self.rateScenario(1)
def rateButtonActionTwo(self):
self.rateScenario(2)
def rateButtonActionThree(self):
self.rateScenario(3)
def rateButtonActionFour(self):
self.rateScenario(4)
def rateButtonActionFive(self):
self.rateScenario(5)
def checkVersion(splashscreen):
try:
r = requests.get(version.version_url, allow_redirects=False, timeout=7)
v = yaml.safe_load(r.content)
avail_build = v["version"]
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"])
msg.setText(v["description"])
msg.setIcon(QMessageBox.Icon.Information)
x = msg.exec_()
else:
logger.info("Version check complete: running the latest version. (micro version ignored)")
except:
logger.error("Online version check failed.")
def loadModules(splashscreen):
msg = QMessageBox()
msg.setWindowTitle("Unable to connect to server")
msg.setText(
"We were unable to connect to the RotorOps server to download content. This is a temporary problem, so please try again later. If the problem persists, please get in touch via Discord.")
try:
r = requests.get(modules_map_url, allow_redirects=False, timeout=7)
if not r.status_code == 200:
logger.error("Could not retrieve the modules map.")
x = msg.exec_()
return
except:
logger.error("Failed to retrieve module map.")
x = msg.exec_()
return
module_list = yaml.safe_load(r.content)
files_success = []
files_failed = []
new_modules = []
updated_modules = []
outversioned_modules = []
# Download scenarios files
if module_list:
for module in module_list:
should_download = False
new_module = False
# only allow predefined paths
dp = module_list[module]["path"]
if dp not in allowed_paths:
logger.warning("Invalid path for module: " + module)
continue
# check if local version already exists
package_file_path = os.path.join(directories.home_dir, module_list[module]["path"], module, "package.yaml")
if os.path.exists(package_file_path):
pkg_file = yaml.safe_load(open(package_file_path))
else:
pkg_file = None
# compare required generator version and actual version
if 'requires' in module_list[module]:
if module_list[module]['requires'] > modules_version:
name = 'unknown module'
if 'name' in module_list[module]:
name = module_list[module]['name']
outversioned_modules.append(name)
continue
# compare local and remote versions
if pkg_file and 'version' in pkg_file:
local_version = pkg_file['version']
if module_list[module]['version'] > local_version:
should_download = True
else: # package file not found
should_download = True
new_module = True
# delete modules with 'remove' dist property
if 'dist' in module_list[module] and module_list[module]['dist'] == 'remove':
for filename in module_list[module]["files"]:
module_dir = os.path.join(directories.home_dir, module_list[module]["path"], module)
file_path = os.path.join(module_dir, filename)
if os.path.exists(file_path):
try:
os.remove(file_path)
print("Removed module file: " + filename)
except:
logger.error("Error while trying to remove " + filename)
continue
# download files
if should_download:
logger.info("Updating module: " + module)
module_dir = os.path.join(directories.home_dir, module_list[module]["path"], module)
# download files in remote package
for filename in module_list[module]["files"]:
broken_file = False
type_path = module_list[module]["type"]
splash.showMessage("Downloading " + filename + " ...", Qt.AlignHCenter | Qt.AlignTop, Qt.white)
app.processEvents()
url = modules_url + type_path + "/" + module + "/" + filename
try:
r = requests.get(url, allow_redirects=False, timeout=10)
except:
logger("Request for " + url + " failed.")
broken_file = True
break
if r and r.status_code == 200:
os.makedirs(module_dir, exist_ok=True)
file_path = os.path.join(module_dir, filename)
open(file_path, 'wb+').write(r.content)
files_success.append(filename)
# do some stuff for the dialog popup
if filename.endswith('.miz') and "name" in module_list[module]:
if new_module:
new_modules.append(module_list[module]["name"])
else:
updated_modules.append(module_list[module]["name"])
else:
broken_file = True
files_failed.append(filename)
logger.error("Download failed: " + filename)
# create the local package file
if not broken_file:
logger.info("Creating local package file for module " + module)
package = {}
package['version'] = module_list[module]['version']
with open(package_file_path, 'w+') as pfile:
yaml.dump(package, pfile)
else:
logger.error("Problem encountered with modules map.")
# show a popup if we downloaded any packages
if len(files_success) > 0 or len(files_failed) > 0 or len(outversioned_modules) > 0:
if len(files_failed) > 0:
fs = ""
for filename in files_failed:
fs = fs + filename + ','
logger.error("Failed to add new files: " + fs)
msg = QMessageBox()
msg.setWindowTitle("Downloaded Files")
message = ""
if len(new_modules) > 0:
message = message + "New modules added: \n\n"
for name in new_modules:
message = message + name + "\n"
if len(updated_modules) > 0:
message = message + "\nModules updated: \n"
for name in updated_modules:
message = message + name + "\n"
if len(files_failed) > 0:
message = message + "\n\n" + str(len(files_failed)) + " files failed."
if len(outversioned_modules) > 0:
message = message + "\n\n" + str(len(outversioned_modules)) + " modules did not download because you need an required update."
msg.setText(message)
x = msg.exec_()
else:
logger.info("All packages up to date.")
def getRatings(splashscreen):
try:
r = requests.get(ratings_url, allow_redirects=False, timeout=7)
j = json.loads(r.text)
# for entry in j:
# print(entry["package"])
# print(entry["avg_rating"])
logger.info("Retrieved online package info.")
return j
except TimeoutError:
logger.error("Online package info failed: connection timed out.")
except ConnectionError:
logger.error("Online package info failed: connection error.")
except:
logger.error("Online package info failed.")
class StatusTipFilter(QObject):
def eventFilter(self, watched: QObject, event: QEvent) -> bool:
if isinstance(event, QtGui.QStatusTipEvent):
return True
return super().eventFilter(watched, event)
if __name__ == "__main__":
# os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1"
app = QApplication(sys.argv)
# Splash Screen and loading
pixmap = QPixmap(directories.assets + "/splash.jpg")
splash = QSplashScreen(pixmap)
splash.show()
font = splash.font()
font.setPixelSize(14)
splash.setFont(font)
splash.showMessage("Checking registry...", Qt.AlignHCenter | Qt.AlignTop, Qt.white)
userid = user.createUserKey()
app.processEvents()
splash.showMessage("Checking version...", Qt.AlignHCenter | Qt.AlignTop, Qt.white)
checkVersion(splash)
app.processEvents()
splash.showMessage("Getting package info...", Qt.AlignHCenter | Qt.AlignTop, Qt.white)
ratings_json = getRatings(splash)
app.processEvents()
splash.showMessage("Getting content...", Qt.AlignHCenter | Qt.AlignTop, Qt.white)
loadModules(splash)
app.processEvents()
app.setWindowIcon(QtGui.QIcon(directories.assets + '/icon.ico'))
# QCoreApplication.setAttribute(QtCore.Qt.AA_DisableHighDpiScaling)
win = Window()
win.userid = userid
splash.finish(win)
win.generateButton.installEventFilter(StatusTipFilter(win)) #prevent button statustip from obscuring other messages
qtmodern.styles.dark(app)
mw = qtmodern.windows.ModernWindow(win)
mw.show()
# wv = win.myWebView(win)
# wv.exec_()
sys.exit(app.exec())