diff --git a/.gitignore b/.gitignore index aea95bd..6c3d373 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,7 @@ incoming templates/ Generator/utils/extract units/source.miz Generator/utils/extract units/units.txt generator.log +templates/Scenarios/user +templates/Scenarios/downloaded +config/user-data.yaml +*.exe diff --git a/Generator/Forces/_How to add your own templates.txt b/Generator/Forces/_How to add your own templates.txt deleted file mode 100644 index 1838d7d..0000000 --- a/Generator/Forces/_How to add your own templates.txt +++ /dev/null @@ -1,18 +0,0 @@ -You can add your own unit templates in this directory and they will appear in the mission generator. - -1) Create an empty mission on Caucasus -2) Add ground unit groups. -3) Save the mission in this directory. - -Optional: -4) Add helicopters with "CAS" main task for attack helicopters. -5) Add helicopters with "Transport" main task for transport helicopters. -6) Add planes with "CAS" main task for attack planes. -7) Add planes with "CAP" main task for fighters. -8) Configure loadouts, liveries, and skill for aircraft. - -Tips: --Drop your templates in the RotorOps Discord if you'd like to have them added in a release for everyone. --The mission generator will only extract blue ground units from the template when selected from the "Blue Forces" menu, and vice versa. --Only unit types are used from ground units. Liveries or other attributes are able to be copied. --For aircraft, group size is currently capped at 2 units per group to help prevent issues with parking. Only the first unit in the group is used as a source. diff --git a/Generator/Forces/blue/BLUE Vietnam Armor (Mr Nobody).miz b/Generator/Forces/blue/BLUE Vietnam Armor (Mr Nobody).miz deleted file mode 100644 index 8d049d1..0000000 Binary files a/Generator/Forces/blue/BLUE Vietnam Armor (Mr Nobody).miz and /dev/null differ diff --git a/Generator/Forces/red/RED Vietnam Armor & Infantry (Mr Nobody).miz b/Generator/Forces/red/RED Vietnam Armor & Infantry (Mr Nobody).miz deleted file mode 100644 index b939702..0000000 Binary files a/Generator/Forces/red/RED Vietnam Armor & Infantry (Mr Nobody).miz and /dev/null differ diff --git a/Generator/Imports/FOB_16_SPWN_WIDE.miz b/Generator/Imports/FOB_16_SPWN_WIDE.miz deleted file mode 100644 index f00aa0f..0000000 Binary files a/Generator/Imports/FOB_16_SPWN_WIDE.miz and /dev/null differ diff --git a/Generator/Imports/How to use imports.txt b/Generator/Imports/How to use imports.txt deleted file mode 100644 index 5634805..0000000 --- a/Generator/Imports/How to use imports.txt +++ /dev/null @@ -1,17 +0,0 @@ -You can put .miz files in this folder to be copied into the generated mission at marker points. This feature is currently very 'alpha' and may produce errors. Currently, this doesn't work for ship groups or plane groups. - -1) Make an empty mission on Cauacasus. - -2) Place units/objects on the map. - -3) Make one unit group name: 'ANCHOR' This will represent the point of insertion into the target mission. - -4) In a Scenario template, place a static object (flag, etc) and call it "IMPORT-[filename of .miz created in first step]" Country should be CJTF Red, CJTF Blue, or UN Peacekeepers. - -5) Change the unit name of the object created in the previous step. This unit name might be used for spawn names, so you should call the unit name something like "North Base" so players know where they'll be spawning when choosing a slot. - - -Tips: --You can change the heading of the imported group by changing the heading of the insertion object. --For multiple imports of the same template, the import object group name should end with '-01' or '-whatever'. - diff --git a/Generator/MissionGenerator.py b/Generator/MissionGenerator.py index 81e0d70..1633467 100644 --- a/Generator/MissionGenerator.py +++ b/Generator/MissionGenerator.py @@ -1,38 +1,47 @@ -import math -import sys -import os -import dcs -from PyQt5.QtCore import QCoreApplication -from PyQt5.uic.properties import QtCore - -import RotorOpsMission as ROps -import RotorOpsUtils -import RotorOpsUnits -import logging import json import yaml +import sys +import os + +import RotorOpsMission as ROps +import RotorOpsUnits +import user +import logging + import requests +from packaging import version from PyQt5.QtWidgets import ( - QApplication, QDialog, QMainWindow, QMessageBox + QApplication, QDialog, QMainWindow, QMessageBox, QCheckBox, QSpinBox, QSplashScreen, QFileDialog, QRadioButton, + QInputDialog, QDialogButtonBox, QVBoxLayout, QLabel, QComboBox ) from PyQt5 import QtGui -from PyQt5 import Qt, QtCore +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 import qtmodern.styles import qtmodern.windows +# UPDATE BUILD VERSION +maj_version = 1 +minor_version = 1 +patch_version = 2 + +user_files_url = 'https://dcs-helicopters.com/user-files/' + #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) -user_files_url = 'https://dcs-helicopters.com/user-files/' class directories: - home_dir = scenarios = forces = scripts = sound = output = assets = imports = None + home_dir = scenarios = forces = scripts = sound = output = assets = imports = user_datafile_path = scenarios_downloaded = scenarios_user = default_config = None @classmethod def find(cls): @@ -40,18 +49,22 @@ class directories: if os.path.basename(os.getcwd()) == "Generator": os.chdir("..") cls.home_dir = os.getcwd() - cls.scenarios = cls.home_dir + "\Generator\Scenarios" - cls.forces = cls.home_dir + "\Generator\Forces" - cls.scripts = cls.home_dir - cls.sound = cls.home_dir + "\sound\embedded" - cls.output = cls.home_dir + "\Generator\Output" - cls.assets = cls.home_dir + "\Generator/assets" - cls.imports = cls.home_dir + "\Generator\Imports" + cls.scenarios = cls.home_dir + "\\templates\\Scenarios" + cls.forces = cls.home_dir + "\\templates\\Forces" + 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 = cls.home_dir + "\\templates\\Imports" + 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) - directories.find() +import MissionGeneratorScenario def handle_exception(exc_type, exc_value, exc_traceback): if issubclass(exc_type, KeyboardInterrupt): #example of handling error subclasses @@ -67,18 +80,27 @@ def handle_exception(exc_type, exc_value, exc_traceback): sys.excepthook = handle_exception -build = 1 -maj_version = 1 -minor_version = 1 -version_string = str(maj_version) + "." + str(minor_version) -scenarios = [] + +version_string = str(maj_version) + "." + str(minor_version) + "." + str(patch_version) +# scenarios = [] red_forces_files = [] blue_forces_files = [] defenders_text = "Defending Forces:" attackers_text = "Attacking Forces:" +ratings_json = None logger.info("RotorOps v" + version_string) +# Try to set windows app ID to display taskbar icon properly +try: + from ctypes import windll + appid = 'RotorOps.MissionGenerator.' + version_string + windll.shell32.SetCurrentProcessExplicitAppUserModelID(appid) +except ImportError: + pass + + + class Window(QMainWindow, Ui_MainWindow): @@ -92,6 +114,14 @@ class Window(QMainWindow, Ui_MainWindow): 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.user_data = self.loadUserData() self.m = ROps.RotorOpsMission() self.setupUi(self) @@ -104,33 +134,144 @@ class Window(QMainWindow, Ui_MainWindow): # 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;color:black;font-weight:bold;}") - + "QStatusBar{padding-left:5px;}") self.version_label.setText("Version " + version_string) + + + + + + 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): - os.chdir(directories.scenarios) - path = os.getcwd() - dir_list = os.listdir(path) - logger.info("Looking for mission files in " + path) - for filename in dir_list: - if filename.endswith(".miz"): - scenarios.append(filename) - self.scenario_comboBox.addItem(filename.removesuffix('.miz')) + 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 = MissionGeneratorScenario.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: + print(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.scenario_comboBox.addItem(s.name) + self.scenarios_list = scenarios.copy() + 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, side, combobox, files_list): os.chdir(directories.home_dir) - os.chdir(directories.forces + "/" + side) + # os.chdir(directories.forces + "/" + side) + os.chdir(directories.forces) path = os.getcwd() dir_list = os.listdir(path) logger.info("Looking for " + side + " Forces files in '" + path) @@ -142,35 +283,43 @@ class Window(QMainWindow, Ui_MainWindow): def populateSlotSelection(self): self.slot_template_comboBox.addItem("Multiple Slots") - for type in RotorOpsUnits.client_helos: + 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): - # if self.defense_checkBox.isChecked(): - # self.red_forces_label.setText(attackers_text) - # self.blue_forces_label.setText(defenders_text) - # else: - # self.red_forces_label.setText(defenders_text) - # self.blue_forces_label.setText(attackers_text) - - self.applyScenarioConfig() - - - def loadScenarioConfig(self, filename): - try: - j = open(filename) - config = json.load(j) - j.close() - return config - except: - return None + print("defensive checkbox changed") def lockedSlot(self): return self.slot_template_comboBox.findText("Locked to Scenario") - def clearScenarioConfig(self): - # reset default states + 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) if self.lockedSlot(): self.slot_template_comboBox.removeItem(self.lockedSlot()) @@ -178,108 +327,150 @@ class Window(QMainWindow, Ui_MainWindow): self.slot_template_comboBox.setEnabled(True) self.slot_template_comboBox.setCurrentIndex(0) - def applyScenarioConfig(self): + 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 not self.config: - return + if 'checkboxes' in config: + for box in config['checkboxes']: + qobj = QObject.findChild(self, QCheckBox, box) + if qobj: + qobj.setChecked(config['checkboxes'][box]) - if self.config['defense']['allowed'] == False: - self.defense_checkBox.setChecked(False) - self.defense_checkBox.setEnabled(False) - elif self.config['offense']['allowed'] == False: - self.defense_checkBox.setChecked(True) - self.defense_checkBox.setEnabled(False) + 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 self.config['defense']['player_spawn'] == "fixed": - self.slot_template_comboBox.addItem("Locked to Scenario") - self.slot_template_comboBox.setCurrentIndex(self.lockedSlot()) - self.slot_template_comboBox.setEnabled(False) + if 'spinboxes' in config: + for box in config['spinboxes']: + qobj = QObject.findChild(self, QSpinBox, box) + if qobj: + qobj.setValue(config['spinboxes'][box]) + 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: + self.blueforces_comboBox.setCurrentIndex(self.blueforces_comboBox.findText(config['blue_forces'])) + + if 'red_forces' in config: + if self.redforces_comboBox.findText(config['red_forces']) >= 0: + self.redforces_comboBox.setCurrentIndex(self.redforces_comboBox.findText(config['red_forces'])) + + 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): - os.chdir(directories.scenarios) - filename = scenarios[self.scenario_comboBox.currentIndex()] - source_mission = dcs.mission.Mission() - source_mission.load_file(filename) - zones = source_mission.triggers.zones() - conflict_zones = 0 - staging_zones = 0 - conflict_zone_size_sum = 0 - conflict_zone_distance_sum = 0 - spawn_zones = 0 - conflict_zone_positions = [] - #friendly_airports = source_mission.getCoalitionAirports("blue") - #enemy_airports = source_mission.getCoalitionAirports("red") - friendly_airports = True - enemy_airports = True + if len(self.scenarios_list) <= 0: + return - self.clearScenarioConfig() - config_filename = filename.removesuffix(".miz") + ".json" - self.config = self.loadScenarioConfig(config_filename) - if self.config: - self.applyScenarioConfig() - self.m.setConfig(self.config) + QApplication.setOverrideCursor(Qt.WaitCursor) + self.scenario = self.scenarios_list[self.scenario_comboBox.currentIndex()] - for zone in zones: - if zone.name == "STAGING": - staging_zones += 1 - if zone.name == "ALPHA" or zone.name == "BRAVO" or zone.name == "CHARLIE" or zone.name == "DELTA": - conflict_zones += 1 - conflict_zone_size_sum += zone.radius - conflict_zone_positions.append(zone.position) - if zone.name.rfind("_SPAWN") > 0: - spawn_zones += 1 - if conflict_zones > 1: - for index, position in enumerate(conflict_zone_positions): - if index > 0: - conflict_zone_distance_sum += RotorOpsUtils.getDistance(conflict_zone_positions[index], conflict_zone_positions[index - 1]) + if self.scenario.config: + self.applyScenarioConfig(self.scenario.config) + self.m.setConfig(self.scenario.config) + else: + default_config = self.loadScenarioConfig(directories.default_config) + self.applyScenarioConfig(default_config) + self.m.setConfig(default_config) - def validateTemplate(): - valid = True - if len(staging_zones) < 1: - valid = False - if len(conflict_zones) < 1: - valid = False - if not friendly_airports: - valid = False - if not enemy_airports: - valid = False - return valid - - if conflict_zones and staging_zones : - average_zone_size = conflict_zone_size_sum / conflict_zones - self.description_textBrowser.setText( - "Map: " + source_mission.terrain.name + "\n" + - "Conflict Zones: " + str(conflict_zones) + "\n" + - "Average Zone Size " + str(math.floor(average_zone_size)) + "m \n" + - "Infantry Spawn Zones: " + str(spawn_zones) + "\n" + - "Approx Distance: " + str(math.floor(RotorOpsUtils.convertMeterToNM(conflict_zone_distance_sum))) + "nm \n" - #"Validity Check:" + str(validateTemplate()) - + "\n== BRIEFING ==\n\n" - + source_mission.description_text() - ) - - path = directories.scenarios + "/" + filename.removesuffix(".miz") + ".jpg" + 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_filename = red_forces_files[self.redforces_comboBox.currentIndex()] blue_forces_filename = blue_forces_files[self.blueforces_comboBox.currentIndex()] - scenario_filename = scenarios[self.scenario_comboBox.currentIndex()] + scenario_name = self.scenario.name + scenario_path = self.scenario.path source = "offline" data = { "source": source, - "scenario_filename": scenario_filename, + "scenario_file": scenario_path, + "scenario_name": scenario_name, "red_forces_filename": red_forces_filename, "blue_forces_filename": blue_forces_filename, "red_quantity": self.redqty_spinBox.value(), @@ -298,19 +489,19 @@ class Window(QMainWindow, Ui_MainWindow): "slots": self.slot_template_comboBox.currentText(), "zone_protect_sams": self.zone_sams_checkBox.isChecked(), "zone_farps": self.farp_buttonGroup.checkedButton().objectName(), - "inf_spawn_msgs": self.inf_spawn_voiceovers_checkBox.isChecked(), "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(), } - os.chdir(directories.home_dir + '/Generator') - n = ROps.RotorOpsMission() - result = n.generateMission(data) + logger.info("Generating mission with options:") logger.info(str(data)) + n = ROps.RotorOpsMission() + result = n.generateMission(self, data) - # generate the mission - #result = self.m.generateMission(data) + QApplication.restoreOverrideCursor() #display results if result["success"]: @@ -319,13 +510,13 @@ class Window(QMainWindow, Ui_MainWindow): msg = QMessageBox() msg.setWindowTitle("Mission Generated") msg.setText("Awesome, your mission is ready! It's located in this directory: \n" + - directories.output + "\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" + "\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. \n" + + "Units can be changed or moved without issue. Player slots can be changed or moved without issue (one per group though!) \n" + "\n" + "Don't forget, you can also create your own templates that can include any mission options, objects, or even scripts. \n" + "\n" + @@ -345,84 +536,351 @@ class Window(QMainWindow, Ui_MainWindow): def nextScenario(self): self.scenario_comboBox.setCurrentIndex((self.scenario_comboBox.currentIndex() + 1)) - def checkVersion(self): - try: - url = user_files_url + 'versions.yaml' - r = requests.get(url, allow_redirects=False) - v = yaml.safe_load(r.content) - print(v["build"]) - avail_build = v["build"] - if avail_build > build: - msg = QMessageBox() - msg.setWindowTitle("Update Available") - msg.setText(v["description"]) - x = msg.exec_() - except: - logger.error("Online version check failed.") + + # 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 - def loadOnlineContent(self): - url = user_files_url + 'directory.yaml' - r = requests.get(url, allow_redirects=False) - user_files = yaml.safe_load(r.content) - count = 0 + self.slot_qty = len(window.player_slots) + self.window = window - # Download scenarios files - os.chdir(directories.scenarios) - if user_files["scenarios"]["files"]: - for filename in user_files["scenarios"]["files"]: - url = user_files_url + user_files["scenarios"]["dir"] + '/' + filename - r = requests.get(url, allow_redirects=False) - open(filename, 'wb').write(r.content) - count = count + 1 + #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) - # Download blue forces files - os.chdir(directories.forces + '/blue') - if user_files["forces_blue"]["files"]: - for filename in user_files["forces_blue"]["files"]: - url = user_files_url + user_files["forces_blue"]["dir"] + '/' + filename - r = requests.get(url, allow_redirects=False) - open(filename, 'wb').write(r.content) - count = count + 1 + self.buttonBox.accepted.connect(self.accepted) + self.buttonBox.rejected.connect(self.close) + self.addBtn.clicked.connect(self.addSlotBox) + self.removeBtn.clicked.connect(self.removeSlotBox) - # Download red forces files - os.chdir(directories.forces + '/red') - if user_files["forces_red"]["files"]: - for filename in user_files["forces_red"]["files"]: - url = user_files_url + user_files["forces_red"]["dir"] + '/' + filename - r = requests.get(url, allow_redirects=False) - open(filename, 'wb').write(r.content) - count = count + 1 + self.slot_boxes = [] - # Download imports files - os.chdir(directories.imports) - if user_files["imports"]["files"]: - for filename in user_files["imports"]["files"]: - url = user_files_url + user_files["imports"]["dir"] + '/' + filename - r = requests.get(url, allow_redirects=False) - open(filename, 'wb').write(r.content) - count = count + 1 + 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): + + version_url = 'https://dcs-helicopters.com/app-updates/versioncheck.yaml' + try: + r = requests.get(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): + 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.") + except: + logger.error("Online version check failed.") + + + +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.yaml' +ratings_url = 'https://dcs-helicopters.com/user-files/ratings.php' + +def loadModules(splashscreen): + + 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.") + return + except: + logger.error("Failed to retrieve module map.") + return + + module_list = yaml.safe_load(r.content) + files_success = [] + files_failed = [] + new_scenarios = [] + updated_scenarios = [] + + + # Download scenarios files + #os.chdir(directories.scenarios) + if module_list: + + for module in module_list: + + should_download = False + new_module = False + + # check if local version already exists + package_file_path = os.path.join(directories.scenarios_downloaded, module, "package.yaml") + + if os.path.exists(package_file_path): + pkg_file = yaml.safe_load(open(package_file_path)) + else: + pkg_file = None + + #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 + + 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 + splash.showMessage("Downloading " + filename + " ...", Qt.AlignHCenter | Qt.AlignTop, Qt.white) + app.processEvents() + + url = modules_url + 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_scenarios.append(module_list[module]["name"]) + else: + updated_scenarios.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: + 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") - msg.setText("We've downloaded " + str(count) + " new files!") + message = "" + if len(new_scenarios) > 0: + message = message + "New scenarios added: \n\n" + for name in new_scenarios: + message = message + name + "\n" + if len(updated_scenarios) > 0: + message = message + "\nScenarios updated: \n" + for name in updated_scenarios: + message = message + name + "\n" + if len(files_failed) > 0: + message = message + "\n\n" + str(len(files_failed)) + " files failed." + 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.show() - # win.loadOnlineContent() - win.checkVersion() - + 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() - sys.exit(app.exec()) + # wv = win.myWebView(win) + # wv.exec_() + sys.exit(app.exec()) diff --git a/Generator/MissionGenerator.spec b/Generator/MissionGenerator.spec index bb0df10..cfef403 100644 --- a/Generator/MissionGenerator.spec +++ b/Generator/MissionGenerator.spec @@ -27,7 +27,7 @@ exe = EXE(pyz, a.datas, [], name='MissionGenerator', - icon='assets\\icon.ico', + icon='..\\assets\\icon.ico', debug=False, bootloader_ignore_signals=False, strip=False, diff --git a/Generator/MissionGeneratorScenario.py b/Generator/MissionGeneratorScenario.py new file mode 100644 index 0000000..90a2a0e --- /dev/null +++ b/Generator/MissionGeneratorScenario.py @@ -0,0 +1,118 @@ +from MissionGenerator import directories +import os +import RotorOpsUtils +import dcs +import math +from os.path import exists + +class Scenario: + def __init__(self, path, name): + self.path = path + self.name = name + self.description = "" + # self.image_path = None + self.map_name = None + self.config = None + self.downloadable = False + self.tags = [] + self.rating = None + self.rating_qty = None + self.packageID = None + self.local_rating = None + + def applyConfig(self, config): + self.config = config + if 'description' in config: + self.description = config["description"] + if 'name' in config: + self.name = config["name"] + if 'map' in config: + self.map_name = config["map"] + if 'tags' in config: + for tag in config['tags']: + self.tags.append(tag) + + + + def evaluateMiz(self): + # check if we have the miz file + if exists(self.path): + self.exists = True + else: + self.exists = False + return None + + source_mission = dcs.mission.Mission() + source_mission.load_file(self.path) + zones = source_mission.triggers.zones() + conflict_zones = 0 + staging_zones = 0 + conflict_zone_size_sum = 0 + conflict_zone_distance_sum = 0 + spawn_zones = 0 + conflict_zone_positions = [] + #friendly_airports = source_mission.getCoalitionAirports("blue") + #enemy_airports = source_mission.getCoalitionAirports("red") + friendly_airports = True + enemy_airports = True + + + + for zone in zones: + if zone.name.rfind("STAGING") == 0: + staging_zones += 1 + if zone.name == "ALPHA" or zone.name == "BRAVO" or zone.name == "CHARLIE" or zone.name == "DELTA": + conflict_zones += 1 + conflict_zone_size_sum += zone.radius + conflict_zone_positions.append(zone.position) + if zone.name.rfind("_SPAWN") > 0: + spawn_zones += 1 + if conflict_zones > 1: + for index, position in enumerate(conflict_zone_positions): + if index > 0: + conflict_zone_distance_sum += RotorOpsUtils.getDistance(conflict_zone_positions[index], conflict_zone_positions[index - 1]) + + def validateTemplate(): + valid = True + if len(staging_zones) < 1: + valid = False + if len(conflict_zones) < 1: + valid = False + if not friendly_airports: + valid = False + if not enemy_airports: + valid = False + return valid + + description = "" + + if self.rating: + description = description + "Rated " + str(self.rating) + "/5 based on " + str(self.rating_qty) + " reviews!\n" + + if self.config: + + if 'name' in self.config and self.config["name"] is not None: + description = description + '

' + self.config["name"] + '

' + if 'description' in self.config and self.config["description"] is not None: + description = description + self.config["description"] + "\n\n" + + if conflict_zones and staging_zones : + average_zone_size = conflict_zone_size_sum / conflict_zones + description = ( + description + + "Map: " + source_mission.terrain.name + "\n" + + "Conflict Zones: " + str(conflict_zones) + "\n" + + "Staging Zones: " + str(staging_zones) + "\n" + + "Average Zone Size: " + str(math.floor(average_zone_size)) + "m \n" + + "Infantry Spawn Zones: " + str(spawn_zones) + "\n" + + "Approx Distance: " + str(math.floor(RotorOpsUtils.convertMeterToNM(conflict_zone_distance_sum))) + "nm \n" + #"Validity Check:" + str(validateTemplate()) + + "\n== BRIEFING ==\n\n" + + source_mission.description_text() + ) + if self.packageID: + description = description + "\n\nScenario module ID: " + self.packageID + self.description = description.replace("\n", "
") + + + diff --git a/Generator/MissionGeneratorUI.py b/Generator/MissionGeneratorUI.py index 7e34bbb..41fc35e 100644 --- a/Generator/MissionGeneratorUI.py +++ b/Generator/MissionGeneratorUI.py @@ -30,70 +30,12 @@ class Ui_MainWindow(object): MainWindow.setWindowIcon(icon) MainWindow.setWindowOpacity(4.0) MainWindow.setAutoFillBackground(False) - MainWindow.setStyleSheet("/*-----QScrollBar-----*/\n" -"QScrollBar:horizontal \n" -"{\n" -" background-color: transparent;\n" -" height: 8px;\n" -" margin: 0px;\n" -" padding: 0px;\n" -"\n" -"}\n" -"\n" -"\n" -"QScrollBar::handle:horizontal \n" -"{\n" -" border: none;\n" -" min-width: 100px;\n" -" background-color: #9b9b9b;\n" -"\n" -"}\n" -"\n" -"\n" -"QScrollBar::add-line:horizontal, \n" -"QScrollBar::sub-line:horizontal,\n" -"QScrollBar::add-page:horizontal, \n" -"QScrollBar::sub-page:horizontal \n" -"{\n" -" width: 0px;\n" -" background-color: transparent;\n" -"\n" -"}\n" -"\n" -"\n" -"QScrollBar:vertical \n" -"{\n" -" background-color: transparent;\n" -" width: 8px;\n" -" margin: 0;\n" -"\n" -"}\n" -"\n" -"\n" -"QScrollBar::handle:vertical \n" -"{\n" -" border: none;\n" -" min-height: 100px;\n" -" background-color: #9b9b9b;\n" -"\n" -"}\n" -"\n" -"\n" -"QScrollBar::add-line:vertical, \n" -"QScrollBar::sub-line:vertical,\n" -"QScrollBar::add-page:vertical, \n" -"QScrollBar::sub-page:vertical \n" -"{\n" -" height: 0px;\n" -" background-color: transparent;\n" -"\n" -"}") + MainWindow.setStyleSheet("") self.centralwidget = QtWidgets.QWidget(MainWindow) self.centralwidget.setObjectName("centralwidget") self.logistics_crates_checkBox = QtWidgets.QCheckBox(self.centralwidget) self.logistics_crates_checkBox.setGeometry(QtCore.QRect(990, 211, 251, 28)) font = QtGui.QFont() - font.setFamily("Arial") font.setPointSize(10) font.setBold(False) self.logistics_crates_checkBox.setFont(font) @@ -102,7 +44,6 @@ class Ui_MainWindow(object): self.zone_sams_checkBox = QtWidgets.QCheckBox(self.centralwidget) self.zone_sams_checkBox.setGeometry(QtCore.QRect(990, 320, 241, 28)) font = QtGui.QFont() - font.setFamily("Arial") font.setPointSize(10) font.setBold(False) self.zone_sams_checkBox.setFont(font) @@ -110,15 +51,13 @@ class Ui_MainWindow(object): self.red_forces_label = QtWidgets.QLabel(self.centralwidget) self.red_forces_label.setGeometry(QtCore.QRect(470, 80, 171, 27)) font = QtGui.QFont() - font.setFamily("Arial") font.setPointSize(10) font.setBold(False) self.red_forces_label.setFont(font) self.red_forces_label.setObjectName("red_forces_label") self.scenario_comboBox = QtWidgets.QComboBox(self.centralwidget) - self.scenario_comboBox.setGeometry(QtCore.QRect(30, 20, 371, 29)) + self.scenario_comboBox.setGeometry(QtCore.QRect(40, 10, 351, 31)) font = QtGui.QFont() - font.setFamily("Arial") font.setPointSize(8) font.setBold(True) self.scenario_comboBox.setFont(font) @@ -129,9 +68,8 @@ class Ui_MainWindow(object): self.scenario_comboBox.setFrame(True) self.scenario_comboBox.setObjectName("scenario_comboBox") self.description_textBrowser = QtWidgets.QTextBrowser(self.centralwidget) - self.description_textBrowser.setGeometry(QtCore.QRect(40, 410, 361, 251)) + self.description_textBrowser.setGeometry(QtCore.QRect(30, 390, 371, 211)) font = QtGui.QFont() - font.setFamily("Arial") font.setPointSize(9) self.description_textBrowser.setFont(font) self.description_textBrowser.setStyleSheet("padding: 5px;") @@ -143,7 +81,6 @@ class Ui_MainWindow(object): self.defense_checkBox.setEnabled(True) self.defense_checkBox.setGeometry(QtCore.QRect(470, 120, 156, 28)) font = QtGui.QFont() - font.setFamily("Arial") font.setPointSize(10) font.setBold(False) self.defense_checkBox.setFont(font) @@ -167,7 +104,6 @@ class Ui_MainWindow(object): sizePolicy.setHeightForWidth(self.redforces_comboBox.sizePolicy().hasHeightForWidth()) self.redforces_comboBox.setSizePolicy(sizePolicy) font = QtGui.QFont() - font.setFamily("Arial") font.setPointSize(9) font.setBold(False) self.redforces_comboBox.setFont(font) @@ -175,7 +111,6 @@ class Ui_MainWindow(object): self.scenario_label_8 = QtWidgets.QLabel(self.centralwidget) self.scenario_label_8.setGeometry(QtCore.QRect(570, 220, 271, 24)) font = QtGui.QFont() - font.setFamily("Arial") font.setPointSize(10) font.setBold(False) self.scenario_label_8.setFont(font) @@ -183,7 +118,6 @@ class Ui_MainWindow(object): self.slot_template_comboBox = QtWidgets.QComboBox(self.centralwidget) self.slot_template_comboBox.setGeometry(QtCore.QRect(960, 384, 271, 33)) font = QtGui.QFont() - font.setFamily("Arial") font.setPointSize(10) font.setBold(False) self.slot_template_comboBox.setFont(font) @@ -198,7 +132,6 @@ class Ui_MainWindow(object): self.blue_forces_label = QtWidgets.QLabel(self.centralwidget) self.blue_forces_label.setGeometry(QtCore.QRect(470, 30, 161, 27)) font = QtGui.QFont() - font.setFamily("Arial") font.setPointSize(10) font.setBold(False) self.blue_forces_label.setFont(font) @@ -216,7 +149,6 @@ class Ui_MainWindow(object): self.blueforces_comboBox = QtWidgets.QComboBox(self.centralwidget) self.blueforces_comboBox.setGeometry(QtCore.QRect(660, 30, 391, 33)) font = QtGui.QFont() - font.setFamily("Arial") font.setPointSize(9) font.setBold(False) self.blueforces_comboBox.setFont(font) @@ -235,7 +167,6 @@ class Ui_MainWindow(object): self.scenario_label_10 = QtWidgets.QLabel(self.centralwidget) self.scenario_label_10.setGeometry(QtCore.QRect(570, 260, 271, 24)) font = QtGui.QFont() - font.setFamily("Arial") font.setPointSize(10) font.setBold(False) self.scenario_label_10.setFont(font) @@ -290,7 +221,6 @@ class Ui_MainWindow(object): self.scenario_label_7 = QtWidgets.QLabel(self.centralwidget) self.scenario_label_7.setGeometry(QtCore.QRect(570, 180, 271, 24)) font = QtGui.QFont() - font.setFamily("Arial") font.setPointSize(10) font.setBold(False) self.scenario_label_7.setFont(font) @@ -298,7 +228,6 @@ class Ui_MainWindow(object): self.label_2 = QtWidgets.QLabel(self.centralwidget) self.label_2.setGeometry(QtCore.QRect(840, 390, 111, 24)) font = QtGui.QFont() - font.setFamily("Arial") font.setPointSize(10) font.setBold(False) self.label_2.setFont(font) @@ -306,57 +235,42 @@ class Ui_MainWindow(object): self.scenario_label_9 = QtWidgets.QLabel(self.centralwidget) self.scenario_label_9.setGeometry(QtCore.QRect(490, 450, 251, 23)) font = QtGui.QFont() - font.setFamily("Arial") font.setPointSize(10) self.scenario_label_9.setFont(font) self.scenario_label_9.setObjectName("scenario_label_9") self.awacs_checkBox = QtWidgets.QCheckBox(self.centralwidget) self.awacs_checkBox.setGeometry(QtCore.QRect(990, 246, 241, 28)) font = QtGui.QFont() - font.setFamily("Arial") font.setPointSize(10) font.setBold(False) self.awacs_checkBox.setFont(font) - self.awacs_checkBox.setStatusTip("") self.awacs_checkBox.setChecked(True) self.awacs_checkBox.setObjectName("awacs_checkBox") self.tankers_checkBox = QtWidgets.QCheckBox(self.centralwidget) self.tankers_checkBox.setGeometry(QtCore.QRect(990, 282, 241, 28)) font = QtGui.QFont() - font.setFamily("Arial") font.setPointSize(10) font.setBold(False) self.tankers_checkBox.setFont(font) self.tankers_checkBox.setChecked(True) self.tankers_checkBox.setObjectName("tankers_checkBox") - self.inf_spawn_voiceovers_checkBox = QtWidgets.QCheckBox(self.centralwidget) - self.inf_spawn_voiceovers_checkBox.setGeometry(QtCore.QRect(960, 455, 271, 24)) - font = QtGui.QFont() - font.setFamily("Arial") - font.setPointSize(9) - self.inf_spawn_voiceovers_checkBox.setFont(font) - self.inf_spawn_voiceovers_checkBox.setChecked(True) - self.inf_spawn_voiceovers_checkBox.setObjectName("inf_spawn_voiceovers_checkBox") self.voiceovers_checkBox = QtWidgets.QCheckBox(self.centralwidget) self.voiceovers_checkBox.setGeometry(QtCore.QRect(960, 517, 171, 24)) font = QtGui.QFont() - font.setFamily("Arial") font.setPointSize(9) self.voiceovers_checkBox.setFont(font) self.voiceovers_checkBox.setChecked(True) self.voiceovers_checkBox.setObjectName("voiceovers_checkBox") self.smoke_pickup_zone_checkBox = QtWidgets.QCheckBox(self.centralwidget) - self.smoke_pickup_zone_checkBox.setGeometry(QtCore.QRect(960, 424, 271, 24)) + self.smoke_pickup_zone_checkBox.setGeometry(QtCore.QRect(960, 460, 271, 24)) font = QtGui.QFont() - font.setFamily("Arial") font.setPointSize(9) self.smoke_pickup_zone_checkBox.setFont(font) self.smoke_pickup_zone_checkBox.setChecked(False) self.smoke_pickup_zone_checkBox.setObjectName("smoke_pickup_zone_checkBox") self.game_status_checkBox = QtWidgets.QCheckBox(self.centralwidget) - self.game_status_checkBox.setGeometry(QtCore.QRect(960, 486, 271, 24)) + self.game_status_checkBox.setGeometry(QtCore.QRect(960, 490, 271, 24)) font = QtGui.QFont() - font.setFamily("Arial") font.setPointSize(9) self.game_status_checkBox.setFont(font) self.game_status_checkBox.setChecked(True) @@ -365,7 +279,6 @@ class Ui_MainWindow(object): self.label = QtWidgets.QLabel(self.centralwidget) self.label.setGeometry(QtCore.QRect(570, 380, 261, 23)) font = QtGui.QFont() - font.setFamily("Arial") font.setPointSize(10) font.setBold(False) self.label.setFont(font) @@ -393,7 +306,6 @@ class Ui_MainWindow(object): self.force_offroad_checkBox = QtWidgets.QCheckBox(self.centralwidget) self.force_offroad_checkBox.setGeometry(QtCore.QRect(960, 548, 161, 24)) font = QtGui.QFont() - font.setFamily("Arial") font.setPointSize(9) self.force_offroad_checkBox.setFont(font) self.force_offroad_checkBox.setChecked(False) @@ -402,7 +314,6 @@ class Ui_MainWindow(object): self.label_3 = QtWidgets.QLabel(self.centralwidget) self.label_3.setGeometry(QtCore.QRect(570, 330, 281, 23)) font = QtGui.QFont() - font.setFamily("Arial") font.setPointSize(10) font.setBold(False) self.label_3.setFont(font) @@ -410,31 +321,23 @@ class Ui_MainWindow(object): self.apcs_spawn_checkBox = QtWidgets.QCheckBox(self.centralwidget) self.apcs_spawn_checkBox.setGeometry(QtCore.QRect(990, 180, 251, 27)) font = QtGui.QFont() - font.setFamily("Arial") font.setPointSize(10) font.setBold(False) self.apcs_spawn_checkBox.setFont(font) self.apcs_spawn_checkBox.setChecked(True) self.apcs_spawn_checkBox.setObjectName("apcs_spawn_checkBox") self.generateButton = QtWidgets.QPushButton(self.centralwidget) + self.generateButton.setEnabled(True) self.generateButton.setGeometry(QtCore.QRect(710, 600, 231, 51)) font = QtGui.QFont() - font.setFamily("Arial") font.setPointSize(8) font.setBold(True) self.generateButton.setFont(font) - self.generateButton.setStyleSheet("background-color: gray;\n" -"color: rgb(255, 255, 255);\n" -"border-style: outset;\n" -"border-width: 1px;\n" -"border-radius: 5px;\n" -"border-color: black;\n" -"padding: 4px;") + self.generateButton.setStyleSheet("") self.generateButton.setObjectName("generateButton") self.farp_always = QtWidgets.QRadioButton(self.centralwidget) self.farp_always.setGeometry(QtCore.QRect(510, 480, 261, 24)) font = QtGui.QFont() - font.setFamily("Arial") font.setPointSize(9) self.farp_always.setFont(font) self.farp_always.setObjectName("farp_always") @@ -444,7 +347,6 @@ class Ui_MainWindow(object): self.farp_never = QtWidgets.QRadioButton(self.centralwidget) self.farp_never.setGeometry(QtCore.QRect(510, 540, 271, 24)) font = QtGui.QFont() - font.setFamily("Arial") font.setPointSize(9) self.farp_never.setFont(font) self.farp_never.setObjectName("farp_never") @@ -452,7 +354,6 @@ class Ui_MainWindow(object): self.farp_gunits = QtWidgets.QRadioButton(self.centralwidget) self.farp_gunits.setGeometry(QtCore.QRect(510, 509, 261, 24)) font = QtGui.QFont() - font.setFamily("Arial") font.setPointSize(9) self.farp_gunits.setFont(font) self.farp_gunits.setChecked(True) @@ -460,7 +361,7 @@ class Ui_MainWindow(object): self.farp_buttonGroup.addButton(self.farp_gunits) self.missionImage = QtWidgets.QLabel(self.centralwidget) self.missionImage.setEnabled(True) - self.missionImage.setGeometry(QtCore.QRect(60, 80, 300, 300)) + self.missionImage.setGeometry(QtCore.QRect(60, 60, 310, 310)) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -470,39 +371,93 @@ class Ui_MainWindow(object): self.missionImage.setMaximumSize(QtCore.QSize(16777215, 16777215)) self.missionImage.setStyleSheet("") self.missionImage.setText("") - self.missionImage.setPixmap(QtGui.QPixmap("assets/briefing1.png")) + self.missionImage.setPixmap(QtGui.QPixmap("../assets/briefing1.png")) self.missionImage.setScaledContents(True) self.missionImage.setWordWrap(False) self.missionImage.setObjectName("missionImage") self.nextScenario_pushButton = QtWidgets.QPushButton(self.centralwidget) - self.nextScenario_pushButton.setGeometry(QtCore.QRect(370, 210, 31, 51)) + self.nextScenario_pushButton.setGeometry(QtCore.QRect(350, 620, 41, 31)) self.nextScenario_pushButton.setObjectName("nextScenario_pushButton") self.prevScenario_pushButton = QtWidgets.QPushButton(self.centralwidget) - self.prevScenario_pushButton.setGeometry(QtCore.QRect(20, 210, 31, 51)) + self.prevScenario_pushButton.setGeometry(QtCore.QRect(40, 620, 41, 31)) self.prevScenario_pushButton.setObjectName("prevScenario_pushButton") self.background_label = QtWidgets.QLabel(self.centralwidget) self.background_label.setGeometry(QtCore.QRect(1020, 600, 241, 51)) self.background_label.setText("") - self.background_label.setPixmap(QtGui.QPixmap("assets/rotorops-dkgray.png")) + self.background_label.setPixmap(QtGui.QPixmap("../assets/rotorops-dkgray.png")) self.background_label.setScaledContents(True) self.background_label.setObjectName("background_label") + self.rateButton1 = QtWidgets.QPushButton(self.centralwidget) + self.rateButton1.setEnabled(True) + self.rateButton1.setGeometry(QtCore.QRect(120, 620, 31, 31)) + font = QtGui.QFont() + font.setPointSize(8) + self.rateButton1.setFont(font) + self.rateButton1.setStyleSheet("border-image:url(\'../assets/star_full.png\');") + self.rateButton1.setText("") + self.rateButton1.setObjectName("rateButton1") + self.hotstart_checkBox = QtWidgets.QCheckBox(self.centralwidget) + self.hotstart_checkBox.setGeometry(QtCore.QRect(960, 430, 271, 24)) + font = QtGui.QFont() + font.setPointSize(9) + self.hotstart_checkBox.setFont(font) + self.hotstart_checkBox.setChecked(False) + self.hotstart_checkBox.setTristate(False) + self.hotstart_checkBox.setObjectName("hotstart_checkBox") + self.rateButton2 = QtWidgets.QPushButton(self.centralwidget) + self.rateButton2.setEnabled(True) + self.rateButton2.setGeometry(QtCore.QRect(160, 620, 31, 31)) + font = QtGui.QFont() + font.setPointSize(8) + self.rateButton2.setFont(font) + self.rateButton2.setStyleSheet("border-image:url(\'../assets/star_full.png\');") + self.rateButton2.setText("") + self.rateButton2.setObjectName("rateButton2") + self.rateButton3 = QtWidgets.QPushButton(self.centralwidget) + self.rateButton3.setEnabled(True) + self.rateButton3.setGeometry(QtCore.QRect(200, 620, 31, 31)) + font = QtGui.QFont() + font.setPointSize(8) + self.rateButton3.setFont(font) + self.rateButton3.setStyleSheet("border-image:url(\'../assets/star_full.png\');") + self.rateButton3.setText("") + self.rateButton3.setObjectName("rateButton3") + self.rateButton4 = QtWidgets.QPushButton(self.centralwidget) + self.rateButton4.setEnabled(True) + self.rateButton4.setGeometry(QtCore.QRect(240, 620, 31, 31)) + font = QtGui.QFont() + font.setPointSize(8) + self.rateButton4.setFont(font) + self.rateButton4.setStyleSheet("border-image:url(\'../assets/star_full.png\');") + self.rateButton4.setText("") + self.rateButton4.setObjectName("rateButton4") + self.rateButton5 = QtWidgets.QPushButton(self.centralwidget) + self.rateButton5.setEnabled(True) + self.rateButton5.setGeometry(QtCore.QRect(280, 620, 31, 31)) + font = QtGui.QFont() + font.setPointSize(8) + self.rateButton5.setFont(font) + self.rateButton5.setStyleSheet("border-image:url(\'../assets/star_full.png\');") + self.rateButton5.setText("") + self.rateButton5.setObjectName("rateButton5") MainWindow.setCentralWidget(self.centralwidget) self.menubar = QtWidgets.QMenuBar(MainWindow) - self.menubar.setGeometry(QtCore.QRect(0, 0, 1280, 26)) + self.menubar.setGeometry(QtCore.QRect(0, 0, 1280, 29)) self.menubar.setObjectName("menubar") self.menuMap = QtWidgets.QMenu(self.menubar) self.menuMap.setObjectName("menuMap") - self.menuGametype_Filter = QtWidgets.QMenu(self.menubar) - self.menuGametype_Filter.setObjectName("menuGametype_Filter") + self.menuFilter = QtWidgets.QMenu(self.menubar) + self.menuFilter.setObjectName("menuFilter") + self.menuPreferences = QtWidgets.QMenu(self.menubar) + self.menuPreferences.setObjectName("menuPreferences") MainWindow.setMenuBar(self.menubar) self.statusbar = QtWidgets.QStatusBar(MainWindow) font = QtGui.QFont() - font.setFamily("Arial") font.setPointSize(9) font.setBold(False) self.statusbar.setFont(font) self.statusbar.setAcceptDrops(False) - self.statusbar.setStyleSheet("color: rgb(255, 255, 255);") + self.statusbar.setStyleSheet("") self.statusbar.setObjectName("statusbar") MainWindow.setStatusBar(self.statusbar) self.action_generateMission = QtWidgets.QAction(MainWindow) @@ -520,43 +475,90 @@ class Ui_MainWindow(object): self.action_prevScenario = QtWidgets.QAction(MainWindow) self.action_prevScenario.setObjectName("action_prevScenario") self.actionCaucasus = QtWidgets.QAction(MainWindow) + self.actionCaucasus.setCheckable(True) + self.actionCaucasus.setChecked(True) self.actionCaucasus.setObjectName("actionCaucasus") self.actionPersian_Gulf = QtWidgets.QAction(MainWindow) + self.actionPersian_Gulf.setCheckable(True) + self.actionPersian_Gulf.setChecked(True) self.actionPersian_Gulf.setObjectName("actionPersian_Gulf") self.actionMarianas = QtWidgets.QAction(MainWindow) + self.actionMarianas.setCheckable(True) + self.actionMarianas.setChecked(True) self.actionMarianas.setObjectName("actionMarianas") self.actionNevada = QtWidgets.QAction(MainWindow) + self.actionNevada.setCheckable(True) + self.actionNevada.setChecked(True) self.actionNevada.setObjectName("actionNevada") self.actionSyria = QtWidgets.QAction(MainWindow) + self.actionSyria.setCheckable(True) + self.actionSyria.setChecked(True) self.actionSyria.setObjectName("actionSyria") - self.actionAll = QtWidgets.QAction(MainWindow) - self.actionAll.setCheckable(True) - self.actionAll.setChecked(True) - self.actionAll.setObjectName("actionAll") self.actionMultiplayer = QtWidgets.QAction(MainWindow) - self.actionMultiplayer.setCheckable(False) + self.actionMultiplayer.setCheckable(True) + self.actionMultiplayer.setChecked(True) self.actionMultiplayer.setObjectName("actionMultiplayer") - self.actionAll_2 = QtWidgets.QAction(MainWindow) - self.actionAll_2.setCheckable(True) - self.actionAll_2.setChecked(True) - self.actionAll_2.setObjectName("actionAll_2") - self.menuMap.addAction(self.actionAll_2) + self.actionSave_Directory = QtWidgets.QAction(MainWindow) + self.actionSave_Directory.setObjectName("actionSave_Directory") + self.action_slotChanged = QtWidgets.QAction(MainWindow) + self.action_slotChanged.setObjectName("action_slotChanged") + self.actionIncluded = QtWidgets.QAction(MainWindow) + self.actionIncluded.setCheckable(True) + self.actionIncluded.setChecked(True) + self.actionIncluded.setObjectName("actionIncluded") + self.actionUser = QtWidgets.QAction(MainWindow) + self.actionUser.setObjectName("actionUser") + self.actionDownloaded = QtWidgets.QAction(MainWindow) + self.actionDownloaded.setObjectName("actionDownloaded") + self.action_downloadButton = QtWidgets.QAction(MainWindow) + self.action_downloadButton.setObjectName("action_downloadButton") + self.action_rateButton1 = QtWidgets.QAction(MainWindow) + self.action_rateButton1.setObjectName("action_rateButton1") + self.actionSingle_Player = QtWidgets.QAction(MainWindow) + self.actionSingle_Player.setCheckable(True) + self.actionSingle_Player.setChecked(True) + self.actionSingle_Player.setObjectName("actionSingle_Player") + self.actionCo_Op = QtWidgets.QAction(MainWindow) + self.actionCo_Op.setCheckable(True) + self.actionCo_Op.setChecked(True) + self.actionCo_Op.setObjectName("actionCo_Op") + self.actionMapMenu = QtWidgets.QAction(MainWindow) + self.actionMapMenu.setObjectName("actionMapMenu") + self.actionFilterMenu = QtWidgets.QAction(MainWindow) + self.actionFilterMenu.setObjectName("actionFilterMenu") + self.action_rateButton2 = QtWidgets.QAction(MainWindow) + self.action_rateButton2.setObjectName("action_rateButton2") + self.action_rateButton3 = QtWidgets.QAction(MainWindow) + self.action_rateButton3.setObjectName("action_rateButton3") + self.action_rateButton4 = QtWidgets.QAction(MainWindow) + self.action_rateButton4.setObjectName("action_rateButton4") + self.action_rateButton5 = QtWidgets.QAction(MainWindow) + self.action_rateButton5.setObjectName("action_rateButton5") self.menuMap.addAction(self.actionCaucasus) self.menuMap.addAction(self.actionPersian_Gulf) self.menuMap.addAction(self.actionMarianas) self.menuMap.addAction(self.actionNevada) self.menuMap.addAction(self.actionSyria) - self.menuGametype_Filter.addAction(self.actionAll) - self.menuGametype_Filter.addAction(self.actionMultiplayer) + self.menuFilter.addAction(self.actionMultiplayer) + self.menuFilter.addAction(self.actionSingle_Player) + self.menuFilter.addAction(self.actionCo_Op) + self.menuPreferences.addAction(self.actionSave_Directory) self.menubar.addAction(self.menuMap.menuAction()) - self.menubar.addAction(self.menuGametype_Filter.menuAction()) + self.menubar.addAction(self.menuFilter.menuAction()) + self.menubar.addAction(self.menuPreferences.menuAction()) self.retranslateUi(MainWindow) self.generateButton.clicked.connect(self.action_generateMission.trigger) - self.scenario_comboBox.currentIndexChanged['int'].connect(self.action_scenarioSelected.trigger) - self.defense_checkBox.stateChanged['int'].connect(self.action_defensiveModeChanged.trigger) - self.nextScenario_pushButton.clicked.connect(self.action_nextScenario.trigger) self.prevScenario_pushButton.clicked.connect(self.action_prevScenario.trigger) + 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) QtCore.QMetaObject.connectSlotsByName(MainWindow) def retranslateUi(self, MainWindow): @@ -571,17 +573,17 @@ class Ui_MainWindow(object): self.description_textBrowser.setHtml(_translate("MainWindow", "\n" "\n" +"\n" "

Provide close air support for our convoys as we take back Las Vegas from the enemy!

")) self.defense_checkBox.setText(_translate("MainWindow", "Blue on Defense")) - self.redqty_spinBox.setStatusTip(_translate("MainWindow", "How many groups should we generate?")) + self.redqty_spinBox.setStatusTip(_translate("MainWindow", "Red vehicle groups per staging or conflict zone.")) self.redforces_comboBox.setStatusTip(_translate("MainWindow", "Tip: You can create your own custom ground forces groups to be automatically generated.")) self.scenario_label_8.setStatusTip(_translate("MainWindow", "Approximate number of enemy attack plane group spawns.")) self.scenario_label_8.setText(_translate("MainWindow", "Enemy Attack Planes")) self.slot_template_comboBox.setStatusTip(_translate("MainWindow", "Default player/client spawn locations at a friendly airport.")) self.scenario_label_5.setText(_translate("MainWindow", "Groups Per Zone")) self.blue_forces_label.setText(_translate("MainWindow", "Blue Forces:")) - self.blueqty_spinBox.setStatusTip(_translate("MainWindow", "How many groups should we generate?")) + self.blueqty_spinBox.setStatusTip(_translate("MainWindow", "Blue vehicle groups per staging or conflict zone.")) self.blueforces_comboBox.setStatusTip(_translate("MainWindow", "Tip: You can create your own custom ground forces groups to be automatically generated.")) self.scenario_label_4.setText(_translate("MainWindow", "Groups Per Zone")) self.version_label.setText(_translate("MainWindow", "Version string")) @@ -594,10 +596,10 @@ class Ui_MainWindow(object): self.scenario_label_7.setText(_translate("MainWindow", "Enemy Attack Helicopters")) self.label_2.setText(_translate("MainWindow", "Player Slots:")) self.scenario_label_9.setText(_translate("MainWindow", "Zone FARP Conditions:")) - self.awacs_checkBox.setText(_translate("MainWindow", "Friendly AWACS")) + self.awacs_checkBox.setStatusTip(_translate("MainWindow", "Spawn a friendly AWACS with fighter escorts.")) + self.awacs_checkBox.setText(_translate("MainWindow", "Friendly AWACS with escort")) + self.tankers_checkBox.setStatusTip(_translate("MainWindow", "Spawn friendly tankers for both boom and basket refueling.")) self.tankers_checkBox.setText(_translate("MainWindow", "Friendly Tankers")) - self.inf_spawn_voiceovers_checkBox.setStatusTip(_translate("MainWindow", "Friendly/enemy APCs will drop infantry when reaching a new conflict zone.")) - self.inf_spawn_voiceovers_checkBox.setText(_translate("MainWindow", "Voiceovers on Infantry Spawn")) self.voiceovers_checkBox.setStatusTip(_translate("MainWindow", "Voiceovers from the ground commander. Helps keep focus on the active zone.")) self.voiceovers_checkBox.setText(_translate("MainWindow", "Voiceovers")) self.smoke_pickup_zone_checkBox.setStatusTip(_translate("MainWindow", "Infinite troop pickup zones will be marked with blue smoke.")) @@ -613,7 +615,8 @@ class Ui_MainWindow(object): self.label_3.setStatusTip(_translate("MainWindow", "The number of troop drops per transport helicopter flight.")) self.label_3.setText(_translate("MainWindow", "Transport Drop Points")) self.apcs_spawn_checkBox.setStatusTip(_translate("MainWindow", "Friendly/enemy APCs will drop infantry when reaching a new conflict zone. Disables infinite troop pickups from conflict zones (you must pick up existing troops).")) - self.apcs_spawn_checkBox.setText(_translate("MainWindow", "APCs Spawn Infantry")) + self.apcs_spawn_checkBox.setText(_translate("MainWindow", "Dynamic Troops")) + self.generateButton.setStatusTip(_translate("MainWindow", "Click to generate mission.")) self.generateButton.setText(_translate("MainWindow", "GENERATE MISSION")) self.farp_always.setStatusTip(_translate("MainWindow", "Always spawn a FARP in defeated conflict zones.")) self.farp_always.setText(_translate("MainWindow", "Always")) @@ -623,8 +626,16 @@ class Ui_MainWindow(object): self.farp_gunits.setText(_translate("MainWindow", "20% Ground Units Remaining")) self.nextScenario_pushButton.setText(_translate("MainWindow", ">")) self.prevScenario_pushButton.setText(_translate("MainWindow", "<")) - self.menuMap.setTitle(_translate("MainWindow", "Map Filter")) - self.menuGametype_Filter.setTitle(_translate("MainWindow", "Gametype Filter")) + self.rateButton1.setStatusTip(_translate("MainWindow", "Submit a review for this mission scenario.")) + self.hotstart_checkBox.setStatusTip(_translate("MainWindow", "Player helicopters start with engines running on the ground. No effect if player slots says \'Locked to scenario\'")) + self.hotstart_checkBox.setText(_translate("MainWindow", "Player Hotstart")) + self.rateButton2.setStatusTip(_translate("MainWindow", "Submit a review for this mission scenario.")) + self.rateButton3.setStatusTip(_translate("MainWindow", "Submit a review for this mission scenario.")) + self.rateButton4.setStatusTip(_translate("MainWindow", "Submit a review for this mission scenario.")) + self.rateButton5.setStatusTip(_translate("MainWindow", "Submit a review for this mission scenario.")) + self.menuMap.setTitle(_translate("MainWindow", "Map")) + self.menuFilter.setTitle(_translate("MainWindow", "Filter")) + self.menuPreferences.setTitle(_translate("MainWindow", "Preferences")) self.action_generateMission.setText(_translate("MainWindow", "_generateMission")) self.action_scenarioSelected.setText(_translate("MainWindow", "_scenarioSelected")) self.action_blueforcesSelected.setText(_translate("MainWindow", "_blueforcesSelected")) @@ -637,9 +648,28 @@ class Ui_MainWindow(object): self.actionMarianas.setText(_translate("MainWindow", "Marianas")) self.actionNevada.setText(_translate("MainWindow", "Nevada")) self.actionSyria.setText(_translate("MainWindow", "Syria")) - self.actionAll.setText(_translate("MainWindow", "All")) self.actionMultiplayer.setText(_translate("MainWindow", "Multiplayer")) - self.actionAll_2.setText(_translate("MainWindow", "All")) + self.actionSave_Directory.setText(_translate("MainWindow", "Save Directory")) + self.action_slotChanged.setText(_translate("MainWindow", "_slotChanged")) + self.actionIncluded.setText(_translate("MainWindow", "Included")) + self.actionUser.setText(_translate("MainWindow", "User")) + self.actionDownloaded.setText(_translate("MainWindow", "Downloaded")) + self.action_downloadButton.setText(_translate("MainWindow", "_downloadButton")) + self.action_downloadButton.setToolTip(_translate("MainWindow", "_downloadButton")) + self.action_rateButton1.setText(_translate("MainWindow", "_rateButton1")) + self.action_rateButton1.setToolTip(_translate("MainWindow", "_rateButton1")) + self.actionSingle_Player.setText(_translate("MainWindow", "Single-Player")) + self.actionCo_Op.setText(_translate("MainWindow", "Co-Op")) + self.actionMapMenu.setText(_translate("MainWindow", "actionMapMenu")) + self.actionFilterMenu.setText(_translate("MainWindow", "FilterMenu")) + self.action_rateButton2.setText(_translate("MainWindow", "_rateButton2")) + self.action_rateButton2.setToolTip(_translate("MainWindow", "_rateButton2")) + self.action_rateButton3.setText(_translate("MainWindow", "_rateButton3")) + self.action_rateButton3.setToolTip(_translate("MainWindow", "_rateButton3")) + self.action_rateButton4.setText(_translate("MainWindow", "_rateButton4")) + self.action_rateButton4.setToolTip(_translate("MainWindow", "_rateButton4")) + self.action_rateButton5.setText(_translate("MainWindow", "_rateButton5")) + self.action_rateButton5.setToolTip(_translate("MainWindow", "_rateButton5")) if __name__ == "__main__": diff --git a/Generator/MissionGeneratorUI.ui b/Generator/MissionGeneratorUI.ui index 5d174ea..fd3dbd0 100644 --- a/Generator/MissionGeneratorUI.ui +++ b/Generator/MissionGeneratorUI.ui @@ -47,64 +47,7 @@ false - /*-----QScrollBar-----*/ -QScrollBar:horizontal -{ - background-color: transparent; - height: 8px; - margin: 0px; - padding: 0px; - -} - - -QScrollBar::handle:horizontal -{ - border: none; - min-width: 100px; - background-color: #9b9b9b; - -} - - -QScrollBar::add-line:horizontal, -QScrollBar::sub-line:horizontal, -QScrollBar::add-page:horizontal, -QScrollBar::sub-page:horizontal -{ - width: 0px; - background-color: transparent; - -} - - -QScrollBar:vertical -{ - background-color: transparent; - width: 8px; - margin: 0; - -} - - -QScrollBar::handle:vertical -{ - border: none; - min-height: 100px; - background-color: #9b9b9b; - -} - - -QScrollBar::add-line:vertical, -QScrollBar::sub-line:vertical, -QScrollBar::add-page:vertical, -QScrollBar::sub-page:vertical -{ - height: 0px; - background-color: transparent; - -} + @@ -118,7 +61,6 @@ QScrollBar::sub-page:vertical - Arial 10 false @@ -144,7 +86,6 @@ QScrollBar::sub-page:vertical - Arial 10 false @@ -167,7 +108,6 @@ QScrollBar::sub-page:vertical - Arial 10 false @@ -179,15 +119,14 @@ QScrollBar::sub-page:vertical - 30 - 20 - 371 - 29 + 40 + 10 + 351 + 31 - Arial 8 true @@ -214,15 +153,14 @@ QScrollBar::sub-page:vertical - 40 - 410 - 361 - 251 + 30 + 390 + 371 + 211 - Arial 9 @@ -242,7 +180,7 @@ QScrollBar::sub-page:vertical <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><meta charset="utf-8" /><style type="text/css"> p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Arial'; font-size:9pt; font-weight:400; font-style:normal;"> +</style></head><body style=" font-family:'Segoe UI'; font-size:9pt; font-weight:400; font-style:normal;"> <p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:10pt;">Provide close air support for our convoys as we take back Las Vegas from the enemy!</span></p></body></html> @@ -260,7 +198,6 @@ p, li { white-space: pre-wrap; } - Arial 10 false @@ -287,7 +224,7 @@ p, li { white-space: pre-wrap; } - How many groups should we generate? + Red vehicle groups per staging or conflict zone. QAbstractSpinBox::PlusMinus @@ -319,7 +256,6 @@ p, li { white-space: pre-wrap; } - Arial 9 false @@ -339,7 +275,6 @@ p, li { white-space: pre-wrap; } - Arial 10 false @@ -362,7 +297,6 @@ p, li { white-space: pre-wrap; } - Arial 10 false @@ -403,7 +337,6 @@ p, li { white-space: pre-wrap; } - Arial 10 false @@ -427,7 +360,7 @@ p, li { white-space: pre-wrap; } - How many groups should we generate? + Blue vehicle groups per staging or conflict zone. QAbstractSpinBox::PlusMinus @@ -453,7 +386,6 @@ p, li { white-space: pre-wrap; } - Arial 9 false @@ -510,7 +442,6 @@ p, li { white-space: pre-wrap; } - Arial 10 false @@ -647,7 +578,6 @@ p, li { white-space: pre-wrap; } - Arial 10 false @@ -670,7 +600,6 @@ p, li { white-space: pre-wrap; } - Arial 10 false @@ -690,7 +619,6 @@ p, li { white-space: pre-wrap; } - Arial 10 @@ -709,16 +637,15 @@ p, li { white-space: pre-wrap; } - Arial 10 false - + Spawn a friendly AWACS with fighter escorts. - Friendly AWACS + Friendly AWACS with escort true @@ -735,11 +662,13 @@ p, li { white-space: pre-wrap; } - Arial 10 false + + Spawn friendly tankers for both boom and basket refueling. + Friendly Tankers @@ -747,31 +676,6 @@ p, li { white-space: pre-wrap; } true - - - - 960 - 455 - 271 - 24 - - - - - Arial - 9 - - - - Friendly/enemy APCs will drop infantry when reaching a new conflict zone. - - - Voiceovers on Infantry Spawn - - - true - - @@ -783,7 +687,6 @@ p, li { white-space: pre-wrap; } - Arial 9 @@ -801,14 +704,13 @@ p, li { white-space: pre-wrap; } 960 - 424 + 460 271 24 - Arial 9 @@ -826,14 +728,13 @@ p, li { white-space: pre-wrap; } 960 - 486 + 490 271 24 - Arial 9 @@ -861,7 +762,6 @@ p, li { white-space: pre-wrap; } - Arial 10 false @@ -944,7 +844,6 @@ p, li { white-space: pre-wrap; } - Arial 9 @@ -972,7 +871,6 @@ p, li { white-space: pre-wrap; } - Arial 10 false @@ -995,7 +893,6 @@ p, li { white-space: pre-wrap; } - Arial 10 false @@ -1004,13 +901,16 @@ p, li { white-space: pre-wrap; } Friendly/enemy APCs will drop infantry when reaching a new conflict zone. Disables infinite troop pickups from conflict zones (you must pick up existing troops). - APCs Spawn Infantry + Dynamic Troops true + + true + 710 @@ -1021,19 +921,15 @@ p, li { white-space: pre-wrap; } - Arial 8 true + + Click to generate mission. + - background-color: gray; -color: rgb(255, 255, 255); -border-style: outset; -border-width: 1px; -border-radius: 5px; -border-color: black; -padding: 4px; + GENERATE MISSION @@ -1050,7 +946,6 @@ padding: 4px; - Arial 9 @@ -1075,7 +970,6 @@ padding: 4px; - Arial 9 @@ -1100,7 +994,6 @@ padding: 4px; - Arial 9 @@ -1124,9 +1017,9 @@ padding: 4px; 60 - 80 - 300 - 300 + 60 + 310 + 310 @@ -1154,7 +1047,7 @@ padding: 4px; - assets/briefing1.png + ../assets/briefing1.png true @@ -1166,10 +1059,10 @@ padding: 4px; - 370 - 210 - 31 - 51 + 350 + 620 + 41 + 31 @@ -1179,10 +1072,10 @@ padding: 4px; - 20 - 210 - 31 - 51 + 40 + 620 + 41 + 31 @@ -1202,12 +1095,174 @@ padding: 4px; - assets/rotorops-dkgray.png + ../assets/rotorops-dkgray.png true + + + true + + + + 120 + 620 + 31 + 31 + + + + + 8 + + + + Submit a review for this mission scenario. + + + border-image:url('../assets/star_full.png'); + + + + + + + + + 960 + 430 + 271 + 24 + + + + + 9 + + + + Player helicopters start with engines running on the ground. No effect if player slots says 'Locked to scenario' + + + Player Hotstart + + + false + + + false + + + + + true + + + + 160 + 620 + 31 + 31 + + + + + 8 + + + + Submit a review for this mission scenario. + + + border-image:url('../assets/star_full.png'); + + + + + + + + true + + + + 200 + 620 + 31 + 31 + + + + + 8 + + + + Submit a review for this mission scenario. + + + border-image:url('../assets/star_full.png'); + + + + + + + + true + + + + 240 + 620 + 31 + 31 + + + + + 8 + + + + Submit a review for this mission scenario. + + + border-image:url('../assets/star_full.png'); + + + + + + + + true + + + + 280 + 620 + 31 + 31 + + + + + 8 + + + + Submit a review for this mission scenario. + + + border-image:url('../assets/star_full.png'); + + + + + @@ -1215,34 +1270,40 @@ padding: 4px; 0 0 1280 - 26 + 29 - Map Filter + Map - - + - Gametype Filter + Filter - + + + + + + Preferences + + - + + - Arial 9 false @@ -1251,7 +1312,7 @@ padding: 4px; false - color: rgb(255, 255, 255); + @@ -1290,31 +1351,50 @@ padding: 4px; + + true + + + true + Caucasus + + true + + + true + Persian Gulf + + true + + + true + Marianas + + true + + + true + Nevada - - Syria - - - true @@ -1322,18 +1402,31 @@ padding: 4px; true - All + Syria - false + true + + + true Multiplayer - + + + Save Directory + + + + + _slotChanged + + + true @@ -1341,7 +1434,97 @@ padding: 4px; true - All + Included + + + + + User + + + + + Downloaded + + + + + _downloadButton + + + _downloadButton + + + + + _rateButton1 + + + _rateButton1 + + + + + true + + + true + + + Single-Player + + + + + true + + + true + + + Co-Op + + + + + actionMapMenu + + + + + FilterMenu + + + + + _rateButton2 + + + _rateButton2 + + + + + _rateButton3 + + + _rateButton3 + + + + + _rateButton4 + + + _rateButton4 + + + + + _rateButton5 + + + _rateButton5 @@ -1363,6 +1546,54 @@ padding: 4px; + + prevScenario_pushButton + clicked() + action_prevScenario + trigger() + + + 35 + 261 + + + -1 + -1 + + + + + defense_checkBox + clicked() + action_defensiveModeChanged + trigger() + + + 560 + 173 + + + -1 + -1 + + + + + slot_template_comboBox + activated(int) + action_slotChanged + trigger() + + + 1095 + 426 + + + -1 + -1 + + + scenario_comboBox currentIndexChanged(int) @@ -1379,22 +1610,6 @@ padding: 4px; - - defense_checkBox - stateChanged(int) - action_defensiveModeChanged - trigger() - - - 560 - 173 - - - -1 - -1 - - - nextScenario_pushButton clicked() @@ -1406,20 +1621,84 @@ padding: 4px; 257 - 372 - 63 + -1 + -1 - prevScenario_pushButton + rateButton1 clicked() - action_prevScenario + action_rateButton1 trigger() - 35 - 261 + 300 + 101 + + + -1 + -1 + + + + + rateButton2 + clicked() + action_rateButton2 + trigger() + + + 175 + 664 + + + -1 + -1 + + + + + rateButton3 + clicked() + action_rateButton3 + trigger() + + + 215 + 664 + + + -1 + -1 + + + + + rateButton4 + clicked() + action_rateButton4 + trigger() + + + 255 + 664 + + + -1 + -1 + + + + + rateButton5 + clicked() + action_rateButton5 + trigger() + + + 295 + 664 -1 diff --git a/Generator/MissionGeneratorUI_bkup11.ui b/Generator/MissionGeneratorUI_bkup11.ui new file mode 100644 index 0000000..b7458cf --- /dev/null +++ b/Generator/MissionGeneratorUI_bkup11.ui @@ -0,0 +1,1380 @@ + + + MainWindow + + + + 0 + 0 + 1280 + 720 + + + + + 0 + 0 + + + + + 1280 + 720 + + + + + 1280 + 720 + + + + + 10 + + + + RotorOps Mission Generator + + + + assets/icon.icoassets/icon.ico + + + 4.000000000000000 + + + false + + + + + + + + + 990 + 211 + 251 + 28 + + + + + 10 + false + + + + Enable CTLD logistics crates for building ground units and air defenses. Pickup logistics containers to create new logistics sites. + + + Logistics + + + true + + + + + + 990 + 320 + 241 + 28 + + + + + 10 + false + + + + Inactive conflict zones will be protected by SAMs. When a zone is cleared, SAMs at next active zone will be destroyed. + + + Inactive Zone SAMs + + + + + + 470 + 80 + 171 + 27 + + + + + 10 + false + + + + Red Forces: + + + + + + 30 + 20 + 371 + 29 + + + + + 8 + true + + + + + + + -1 + + + Tip: You can create your own templates that include mission options like kneeboards, briefings, weather, static units, triggers, scripts, etc. + + + + + + QComboBox::AdjustToContentsOnFirstShow + + + true + + + + + + 40 + 410 + 361 + 251 + + + + + 9 + + + + padding: 5px; + + + QFrame::StyledPanel + + + QFrame::Plain + + + 1 + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><meta charset="utf-8" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Arial'; font-size:9pt; font-weight:400; font-style:normal;"> +<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:10pt;">Provide close air support for our convoys as we take back Las Vegas from the enemy!</span></p></body></html> + + + + + true + + + + 470 + 120 + 156 + 28 + + + + + 10 + false + + + + Blue on Defense + + + true + + + + + + 1070 + 80 + 51 + 31 + + + + + 12 + + + + Red vehicle groups per staging or conflict zone. + + + QAbstractSpinBox::PlusMinus + + + 0 + + + 8 + + + 2 + + + + + + 660 + 80 + 391 + 33 + + + + + 0 + 0 + + + + + 9 + false + + + + Tip: You can create your own custom ground forces groups to be automatically generated. + + + + + + 570 + 220 + 271 + 24 + + + + + 10 + false + + + + Approximate number of enemy attack plane group spawns. + + + Enemy Attack Planes + + + + + + 960 + 384 + 271 + 33 + + + + + 10 + false + + + + Default player/client spawn locations at a friendly airport. + + + + + + 1130 + 40 + 131 + 18 + + + + + 8 + + + + Groups Per Zone + + + Qt::AlignCenter + + + + + + 470 + 30 + 161 + 27 + + + + + 10 + false + + + + Blue Forces: + + + + + + 1070 + 30 + 51 + 31 + + + + + 12 + + + + Blue vehicle groups per staging or conflict zone. + + + QAbstractSpinBox::PlusMinus + + + 0 + + + 8 + + + 3 + + + + + + 660 + 30 + 391 + 33 + + + + + 9 + false + + + + Tip: You can create your own custom ground forces groups to be automatically generated. + + + + + + 1130 + 90 + 131 + 18 + + + + + 8 + + + + Groups Per Zone + + + Qt::AlignCenter + + + + + + 1140 + 650 + 111 + 20 + + + + Version string + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 570 + 260 + 271 + 24 + + + + + 10 + false + + + + Approximate number of enemy transport helicopter spawns. + + + Enemy Transport Helicopters + + + + + + 510 + 260 + 51 + 31 + + + + + 0 + 0 + + + + + 12 + + + + Approximate number of enemy transport helicopter spawns. + + + QAbstractSpinBox::PlusMinus + + + 0 + + + 8 + + + 1 + + + + + + 510 + 220 + 51 + 31 + + + + + 0 + 0 + + + + + 12 + + + + Approximate number of enemy attack plane group spawns. + + + QAbstractSpinBox::PlusMinus + + + 0 + + + 8 + + + 1 + + + + + + 510 + 180 + 51 + 31 + + + + + 0 + 0 + + + + + 12 + + + + Approximate number of enemy attack helicopter group spawns. + + + false + + + QAbstractSpinBox::PlusMinus + + + true + + + 0 + + + 8 + + + 2 + + + + + + 570 + 180 + 271 + 24 + + + + + 10 + false + + + + Approximate number of enemy attack helicopter group spawns. + + + Enemy Attack Helicopters + + + + + + 840 + 390 + 111 + 24 + + + + + 10 + false + + + + Player Slots: + + + + + + 490 + 450 + 251 + 23 + + + + + 10 + + + + Zone FARP Conditions: + + + + + + 990 + 246 + 241 + 28 + + + + + 10 + false + + + + Spawn a friendly AWACS with fighter escorts. + + + Friendly AWACS with escort + + + true + + + + + + 990 + 282 + 241 + 28 + + + + + 10 + false + + + + Spawn friendly tankers for both boom and basket refueling. + + + Friendly Tankers + + + true + + + + + + 960 + 455 + 271 + 24 + + + + + 9 + + + + Friendly/enemy APCs will drop infantry when reaching a new conflict zone. + + + Voiceovers on Infantry Spawn + + + true + + + + + + 960 + 517 + 171 + 24 + + + + + 9 + + + + Voiceovers from the ground commander. Helps keep focus on the active zone. + + + Voiceovers + + + true + + + + + + 960 + 424 + 271 + 24 + + + + + 9 + + + + Infinite troop pickup zones will be marked with blue smoke. + + + Smoke at Troop Pickup Zones + + + false + + + + + + 960 + 486 + 271 + 24 + + + + + 9 + + + + Enable an onscreen zone status display. This helps keep focus on the active conflict zone. + + + Game Status Display + + + true + + + false + + + + + + 570 + 380 + 261 + 23 + + + + + 10 + false + + + + This value is multiplied by the number of spawn zones in the mission template. + + + Infantry Spawns per zone + + + + + + 510 + 380 + 47 + 31 + + + + + 12 + + + + This value is multiplied by the number of spawn zones in the mission template. + + + QAbstractSpinBox::PlusMinus + + + 0 + + + 20 + + + 2 + + + + + + 510 + 330 + 47 + 31 + + + + + 12 + + + + The number of troop drops per transport helicopter flight. + + + QAbstractSpinBox::PlusMinus + + + 0 + + + 10 + + + 4 + + + + + + 960 + 548 + 161 + 24 + + + + + 9 + + + + May help prevent long travel times or pathfinding issues. + + + Force Offroad + + + false + + + false + + + + + + 570 + 330 + 281 + 23 + + + + + 10 + false + + + + The number of troop drops per transport helicopter flight. + + + Transport Drop Points + + + + + + 990 + 180 + 251 + 27 + + + + + 10 + false + + + + Friendly/enemy APCs will drop infantry when reaching a new conflict zone. Disables infinite troop pickups from conflict zones (you must pick up existing troops). + + + Dynamic Troops + + + true + + + + + + 710 + 600 + 231 + 51 + + + + + 8 + true + + + + Click to generate mission. + + + + + + GENERATE MISSION + + + + + + 510 + 480 + 261 + 24 + + + + + 9 + + + + Always spawn a FARP in defeated conflict zones. + + + Always + + + farp_buttonGroup + + + + + + 510 + 540 + 271 + 24 + + + + + 9 + + + + Never spawn FARPs in defeated conflict zones. + + + Never + + + farp_buttonGroup + + + + + + 510 + 509 + 261 + 24 + + + + + 9 + + + + Only spawn FARPs in defeated conflict zones if we have sufficient ground units remaining. + + + 20% Ground Units Remaining + + + true + + + farp_buttonGroup + + + + + true + + + + 60 + 80 + 300 + 300 + + + + + 0 + 0 + + + + + 300 + 300 + + + + + 16777215 + 16777215 + + + + + + + + + + ../assets/briefing1.png + + + true + + + false + + + + + + 370 + 210 + 31 + 51 + + + + > + + + + + + 20 + 210 + 31 + 51 + + + + < + + + + + + 1020 + 600 + 241 + 51 + + + + + + + ../assets/rotorops-dkgray.png + + + true + + + + + + + 0 + 0 + 1280 + 26 + + + + + Map Filter + + + + + + + + + + + Gametype Filter + + + + + + + Preferences + + + + + + + + + + + 9 + false + + + + false + + + + + + + + _generateMission + + + + + _scenarioSelected + + + + + _blueforcesSelected + + + + + _redforcesSelected + + + + + _defensiveModeChanged + + + + + _nextScenario + + + + + _prevScenario + + + + + Caucasus + + + + + Persian Gulf + + + + + Marianas + + + + + Nevada + + + + + Syria + + + + + true + + + true + + + All + + + + + false + + + Multiplayer + + + + + true + + + true + + + All + + + + + Save Directory + + + + + _slotChanged + + + + + + + generateButton + clicked() + action_generateMission + trigger() + + + 1030 + 616 + + + -1 + -1 + + + + + scenario_comboBox + currentIndexChanged(int) + action_scenarioSelected + trigger() + + + 285 + 71 + + + -1 + -1 + + + + + defense_checkBox + stateChanged(int) + action_defensiveModeChanged + trigger() + + + 560 + 173 + + + -1 + -1 + + + + + nextScenario_pushButton + clicked() + action_nextScenario + trigger() + + + 389 + 257 + + + -1 + -1 + + + + + prevScenario_pushButton + clicked() + action_prevScenario + trigger() + + + 35 + 261 + + + -1 + -1 + + + + + slot_template_comboBox + activated(int) + action_slotChanged + trigger() + + + 1095 + 426 + + + -1 + -1 + + + + + + + + diff --git a/Generator/MissionGeneratorWeb.py b/Generator/MissionGeneratorWeb.py new file mode 100644 index 0000000..53ac111 --- /dev/null +++ b/Generator/MissionGeneratorWeb.py @@ -0,0 +1,88 @@ +from PyQt5.QtWidgets import QMessageBox + +from MissionGenerator import directories, build, logger +import requests +import yaml +import os + +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/modules.yaml' + +def checkVersion(self): + try: + r = requests.get(version_url, allow_redirects=False, timeout=3) + v = yaml.safe_load(r.content) + print(v["build"]) + avail_build = v["build"] + if avail_build > build: + msg = QMessageBox() + msg.setWindowTitle(v["title"]) + msg.setText(v["description"]) + x = msg.exec_() + except TimeoutError: + logger.error("Online version check failed: connection timed out.") + except ConnectionError: + logger.error("Online version check failed: connection error.") + except: + logger.error("Online version check failed.") + + + +# def loadOnlineContent(self): +# url = user_files_url + 'directory.yaml' +# r = requests.get(url, allow_redirects=False) +# user_files = yaml.safe_load(r.content) +# count = 0 +# +# # Download scenarios files +# os.chdir(directories.scenarios) +# if user_files["scenarios"]["files"]: +# for filename in user_files["scenarios"]["files"]: +# url = user_files_url + user_files["scenarios"]["dir"] + '/' + filename +# r = requests.get(url, allow_redirects=False) +# open(filename, 'wb').write(r.content) +# count = count + 1 +# +# # Download blue forces files +# os.chdir(directories.forces + '/blue') +# if user_files["forces_blue"]["files"]: +# for filename in user_files["forces_blue"]["files"]: +# url = user_files_url + user_files["forces_blue"]["dir"] + '/' + filename +# r = requests.get(url, allow_redirects=False) +# open(filename, 'wb').write(r.content) +# count = count + 1 +# +# # Download red forces files +# os.chdir(directories.forces + '/red') +# if user_files["forces_red"]["files"]: +# for filename in user_files["forces_red"]["files"]: +# url = user_files_url + user_files["forces_red"]["dir"] + '/' + filename +# r = requests.get(url, allow_redirects=False) +# open(filename, 'wb').write(r.content) +# count = count + 1 +# +# # Download imports files +# os.chdir(directories.imports) +# if user_files["imports"]["files"]: +# for filename in user_files["imports"]["files"]: +# url = user_files_url + user_files["imports"]["dir"] + '/' + filename +# r = requests.get(url, allow_redirects=False) +# open(filename, 'wb').write(r.content) +# count = count + 1 +# +# msg = QMessageBox() +# msg.setWindowTitle("Downloaded Files") +# msg.setText("We've downloaded " + str(count) + " new files!") +# x = msg.exec_() + +# class Module: +# +# def __init__(self, remote_dir, local_dir): +# self.remote_dir = remote_dir +# self.local_dir = local_dir +# +# @classmethod +# def createFromFile(cls): + + diff --git a/Generator/Output/readme.txt b/Generator/Output/readme.txt deleted file mode 100644 index 3d24709..0000000 --- a/Generator/Output/readme.txt +++ /dev/null @@ -1 +0,0 @@ -Your mission files will appear in this directory after using the mission generator! \ No newline at end of file diff --git a/Generator/RotorOpsConflict.py b/Generator/RotorOpsConflict.py index 4347449..f255c5b 100644 --- a/Generator/RotorOpsConflict.py +++ b/Generator/RotorOpsConflict.py @@ -5,11 +5,13 @@ jtf_red = "Combined Joint Task Forces Red" jtf_blue = "Combined Joint Task Forces Blue" + def triggerSetup(rops, options): # get the boolean value from ui option and convert to lua string def lb(var): return str(options[var]).lower() + game_flag = 100 # Add the first trigger trig = dcs.triggers.TriggerOnce(comment="RotorOps Setup Scripts") @@ -26,7 +28,7 @@ def triggerSetup(rops, options): "RotorOps.force_offroad = " + lb("force_offroad") + "\n\n" + "RotorOps.voice_overs = " + lb("voiceovers") + "\n\n" + "RotorOps.zone_status_display = " + lb("game_display") + "\n\n" + - "RotorOps.inf_spawn_messages = " + lb("inf_spawn_msgs") + "\n\n" + + "RotorOps.inf_spawn_messages = true\n\n" + "RotorOps.inf_spawns_per_zone = " + lb("inf_spawn_qty") + "\n\n" + "RotorOps.apcs_spawn_infantry = " + lb("apc_spawns_inf") + " \n\n") if not options["smoke_pickup_zones"]: @@ -38,7 +40,7 @@ def triggerSetup(rops, options): trig = dcs.triggers.TriggerOnce(comment="RotorOps Setup Zones") trig.rules.append(dcs.condition.TimeAfter(2)) for s_zone in rops.staging_zones: - trig.actions.append(dcs.action.DoScript(dcs.action.String("RotorOps.stagingZone('" + s_zone + "')"))) + trig.actions.append(dcs.action.DoScript(dcs.action.String("RotorOps.addStagingZone('" + s_zone + "')"))) for c_zone in rops.conflict_zones: zone_flag = rops.conflict_zones[c_zone].flag trig.actions.append( @@ -61,6 +63,15 @@ def triggerSetup(rops, options): z_active_trig.actions.append(dcs.action.DoScript(dcs.action.String("--Add any action you want here!"))) rops.m.triggerrules.triggers.append(z_active_trig) + # # Add CTLD beacons - this might be cool but we'd need to address placement of the 3D objects + # trig = dcs.triggers.TriggerOnce(comment="RotorOps CTLD Beacons") + # trig.rules.append(dcs.condition.TimeAfter(5)) + # trig.actions.append(dcs.action.DoScript(dcs.action.String("ctld.createRadioBeaconAtZone('STAGING','blue', 1440,'STAGING/LOGISTICS')"))) + # for c_zone in rops.conflict_zones: + # trig.actions.append( + # 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): @@ -131,11 +142,10 @@ def triggerSetup(rops, options): # Add transport helos triggers for index in range(options["e_transport_helos"]): - random_zone_index = random.randrange(1, len(rops.conflict_zones)) - random_zone_obj = list(rops.conflict_zones.items())[random_zone_index] + random_zone_obj = random.choice(list(rops.conflict_zones.items())) zone = random_zone_obj[1] z_weak_trig = dcs.triggers.TriggerOnce(comment=zone.name + " Transport Helo") - z_weak_trig.rules.append(dcs.condition.FlagEquals(game_flag, random_zone_index + 1)) + z_weak_trig.rules.append(dcs.condition.FlagIsMore(zone.flag, 1)) z_weak_trig.rules.append(dcs.condition.FlagIsLess(zone.flag, random.randrange(20, 100))) z_weak_trig.actions.append(dcs.action.DoScript(dcs.action.String( "---Flag " + str(game_flag) + " value represents the index of the active zone. "))) @@ -150,10 +160,13 @@ def triggerSetup(rops, options): 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"))) + trig.actions.append( + dcs.action.DoScript(dcs.action.String("RotorOps.gameMsg(RotorOps.gameMsgs.success)"))) rops.m.triggerrules.triggers.append(trig) - trig = dcs.triggers.TriggerOnce(comment="RotorOps Conflict LOST") trig.rules.append(dcs.condition.FlagEquals(game_flag, 98)) trig.actions.append( dcs.action.DoScript(dcs.action.String("---Add an action you want to happen when the game is LOST"))) + 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 diff --git a/Generator/RotorOpsMission.py b/Generator/RotorOpsMission.py index 2c45f46..a2381ba 100644 --- a/Generator/RotorOpsMission.py +++ b/Generator/RotorOpsMission.py @@ -9,6 +9,7 @@ import RotorOpsGroups import RotorOpsUnits import RotorOpsUtils import RotorOpsConflict +import aircraftMods from RotorOpsImport import ImportObjects import time from MissionGenerator import logger @@ -21,15 +22,6 @@ class RotorOpsMission: def __init__(self): self.m = dcs.mission.Mission() - # os.chdir("../") - # directories.home_dir = os.getcwd() - # directories.scenarios = directories.home_dir + "\Generator\Scenarios" - # directories.forces = directories.home_dir + "\Generator\Forces" - # directories.scripts = directories.home_dir - # directories.sound = directories.home_dir + "\sound\embedded" - # directories.output = directories.home_dir + "\Generator\Output" - # directories.assets = directories.home_dir + "\Generator/assets" - # directories.imports = directories.home_dir + "\Generator\Imports" self.conflict_zones = {} self.staging_zones = {} @@ -38,6 +30,7 @@ class RotorOpsMission: self.res_map = {} self.config = None + class RotorOpsZone: def __init__(self, name: str, flag: int, position: dcs.point, size: int): self.name = name @@ -82,7 +75,7 @@ class RotorOpsMission: logger.info("Adding script to mission: " + filename) self.scripts[filename] = self.m.map_resource.add_resource_file(filename) - def getUnitsFromMiz(self, filename, side): + def getUnitsFromMiz(self, file, side='both'): forces = {} vehicles = [] @@ -90,49 +83,53 @@ class RotorOpsMission: transport_helos = [] attack_planes = [] fighter_planes = [] + helicopters = [] - os.chdir(directories.home_dir) - os.chdir(directories.forces + "/" + side) - logger.info("Looking for " + side + " Forces files in '" + os.getcwd()) source_mission = dcs.mission.Mission() try: - source_mission.load_file(filename) - - for country_name in source_mission.coalition.get(side).countries: - country_obj = source_mission.coalition.get(side).countries[country_name] - for vehicle_group in country_obj.vehicle_group: - vehicles.append(vehicle_group) - for helicopter_group in country_obj.helicopter_group: - if helicopter_group.task == 'CAS': - attack_helos.append(helicopter_group) - elif helicopter_group.task == 'Transport': - transport_helos.append(helicopter_group) - for plane_group in country_obj.plane_group: - if plane_group.task == 'CAS': - attack_planes.append(plane_group) - elif plane_group.task == 'CAP': - fighter_planes.append(plane_group) + source_mission.load_file(file) + if side == 'both': + sides = ['red', 'blue'] + else: + sides = [side] + for side in sides: + for country_name in source_mission.coalition.get(side).countries: + country_obj = source_mission.coalition.get(side).countries[country_name] + for vehicle_group in country_obj.vehicle_group: + vehicles.append(vehicle_group) + for helicopter_group in country_obj.helicopter_group: + helicopters.append(helicopter_group) + if helicopter_group.task == 'CAS': + attack_helos.append(helicopter_group) + elif helicopter_group.task == 'Transport': + transport_helos.append(helicopter_group) + for plane_group in country_obj.plane_group: + if plane_group.task == 'CAS': + attack_planes.append(plane_group) + elif plane_group.task == 'CAP': + fighter_planes.append(plane_group) forces["vehicles"] = vehicles forces["attack_helos"] = attack_helos forces["transport_helos"] = transport_helos forces["attack_planes"] = attack_planes forces["fighter_planes"] = fighter_planes + forces["helicopters"] = helicopters return forces except: - logger.error("Failed to load units from " + filename) + logger.error("Failed to load units from " + file) + + def generateMission(self, window, options): - def generateMission(self, options): os.chdir(directories.scenarios) logger.info("Looking for mission files in " + os.getcwd()) - - - self.m.load_file(options["scenario_filename"]) - + window.statusBar().showMessage("Loading scenario mission", 10000) + self.m.load_file(options["scenario_file"]) + self.addMods() self.importObjects() #todo: test @@ -142,8 +139,10 @@ class RotorOpsMission: failure_msg = "You must include a CombinedJointTaskForcesBlue and CombinedJointTaskForcesRed unit in the scenario template. See the instructions in " + directories.scenarios return {"success": False, "failure_msg": failure_msg} - red_forces = self.getUnitsFromMiz(options["red_forces_filename"], "red") - blue_forces = self.getUnitsFromMiz(options["blue_forces_filename"], "blue") + # red_forces = self.getUnitsFromMiz(directories.forces + "/red/" + options["red_forces_filename"], "red") + # blue_forces = self.getUnitsFromMiz(directories.forces + "/blue/" + options["blue_forces_filename"], "blue") + red_forces = self.getUnitsFromMiz(directories.forces + "/" + options["red_forces_filename"], "both") + blue_forces = self.getUnitsFromMiz(directories.forces + "/" + options["blue_forces_filename"], "both") # Add coalitions (we may be able to add CJTF here instead of requiring templates to have objects of those coalitions) self.m.coalition.get("red").add_country(dcs.countries.Russia()) @@ -183,6 +182,7 @@ class RotorOpsMission: #Populate Red zones with ground units + window.statusBar().showMessage("Populating units into mission...", 10000) for zone_name in red_zones: if red_forces["vehicles"]: @@ -249,10 +249,7 @@ class RotorOpsMission: 180, zone_name + " FARP", late_activation=False) #add logistics sites - if options["crates"] and zone_name in self.staging_zones: - # RotorOpsGroups.VehicleTemplate.CombinedJointTaskForcesBlue.logistics_site(self.m, self.m.country(jtf_blue), - # blue_zones[zone_name].position, - # 180, zone_name) + if options["crates"] and zone_name == "STAGING": os.chdir(directories.imports) staging_flag = self.m.find_group(zone_name) if staging_flag: @@ -266,13 +263,6 @@ class RotorOpsMission: i.copyAll(self.m, jtf_blue, "Staging Logistics Zone", staging_position, staging_heading) - - - - - - - if options["zone_protect_sams"] and options["defending"]: vg = self.m.vehicle_group( self.m.country(jtf_blue), @@ -286,6 +276,7 @@ class RotorOpsMission: #Add player slots + window.statusBar().showMessage("Adding flights to mission...", 10000) if options["slots"] != "Locked to Scenario" and options["slots"] != "None": self.addPlayerHelos(options) @@ -297,15 +288,22 @@ class RotorOpsMission: self.m.map.zoom = 100000 #add files and triggers necessary for RotorOps.lua script + + window.statusBar().showMessage("Adding resources to mission...", 10000) self.addResources(directories.sound, directories.scripts) RotorOpsConflict.triggerSetup(self, options) #Save the mission file - os.chdir(directories.output) - output_filename = options["scenario_filename"].removesuffix('.miz') + " " + time.strftime('%a%H%M%S') + '.miz' + window.statusBar().showMessage("Saving mission...", 10000) + if window.user_output_dir: + output_dir = window.user_output_dir # if user has set output dir + else: + output_dir = directories.output # default dir + os.chdir(output_dir) + output_filename = options["scenario_name"] + " " + time.strftime('%a%H%M%S') + '.miz' success = self.m.save(output_filename) - return {"success": success, "filename": output_filename, "directory": directories.output} #let the UI know the result + return {"success": success, "filename": output_filename, "directory": output_dir} #let the UI know the result def addGroundGroups(self, zone, _country, groups, quantity): for a in range(0, quantity): @@ -459,7 +457,16 @@ class RotorOpsMission: client_helos = RotorOpsUnits.client_helos for helicopter in dcs.helicopters.helicopter_map: if helicopter == options["slots"]: - client_helos = [dcs.helicopters.helicopter_map[helicopter]] + client_helos = [dcs.helicopters.helicopter_map[helicopter]] #if out ui slot option matches a specific helicopter type name + + # get loadouts from miz file and put into a simple dict + default_loadouts = {} + default_unit_groups = self.getUnitsFromMiz(directories.home_dir + "\\config\\blue_player_loadouts.miz", "blue") + for helicopter_group in default_unit_groups["helicopters"]: + default_loadouts[helicopter_group.units[0].unit_type.id] = {} + default_loadouts[helicopter_group.units[0].unit_type.id]["pylons"] = helicopter_group.units[0].pylons + default_loadouts[helicopter_group.units[0].unit_type.id]["livery_id"] = helicopter_group.units[0].livery_id + default_loadouts[helicopter_group.units[0].unit_type.id]["fuel"] = helicopter_group.units[0].fuel #find friendly carriers and farps carrier = self.m.country(jtf_blue).find_ship_group(name="HELO_CARRIER") @@ -474,16 +481,33 @@ class RotorOpsMission: heading = 0 group_size = 1 + player_helicopters = [] + if options["slots"] == "Multiple Slots": + player_helicopters = options["player_slots"] + else: + player_helicopters.append(options["slots"]) # single helicopter type + if len(client_helos) == 1: group_size = 2 #add a wingman if singleplayer - for helotype in client_helos: + start_type = dcs.mission.StartType.Cold + if options["player_hotstart"]: + start_type = dcs.mission.StartType.Warm + + farp_helicopter_count = 1 + for helicopter_id in player_helicopters: + helotype = None + if helicopter_id in dcs.helicopters.helicopter_map: + helotype = dcs.helicopters.helicopter_map[helicopter_id] + else: + continue if carrier: fg = self.m.flight_group_from_unit(self.m.country(jtf_blue), "CARRIER " + helotype.id, helotype, carrier, - dcs.task.CAS, group_size=group_size) - elif farp: + dcs.task.CAS, group_size=group_size, start_type=start_type) + elif farp and farp_helicopter_count <= 4: + farp_helicopter_count = farp_helicopter_count + 1 fg = self.m.flight_group_from_unit(self.m.country(jtf_blue), "FARP " + helotype.id, helotype, farp, - dcs.task.CAS, group_size=group_size) + dcs.task.CAS, group_size=group_size, start_type=start_type) #invisible farps need manual unit placement for multiple units if farp.units[0].type == 'Invisible FARP': @@ -493,13 +517,20 @@ class RotorOpsMission: heading += 90 else: fg = self.m.flight_group_from_airport(self.m.country(jtf_blue), primary_f_airport.name + " " + helotype.id, helotype, - self.getParking(primary_f_airport, helotype), group_size=group_size) + self.getParking(primary_f_airport, helotype), group_size=group_size, start_type=start_type) fg.units[0].set_client() - fg.load_task_default_loadout(dcs.task.CAS) + #fg.load_task_default_loadout(dcs.task.CAS) + if helotype.id in default_loadouts: + fg.units[0].pylons = default_loadouts[helotype.id]["pylons"] + fg.units[0].livery_id = default_loadouts[helotype.id]["livery_id"] + fg.units[0].fuel = default_loadouts[helotype.id]["fuel"] #setup wingman for single player if len(fg.units) == 2: fg.units[1].skill = dcs.unit.Skill.High + fg.units[1].pylons = fg.units[0].pylons + fg.units[1].livery_id = fg.units[0].livery_id + fg.units[1].fuel = fg.units[0].fuel class TrainingScenario(): @@ -683,7 +714,7 @@ class RotorOpsMission: farp, maintask=dcs.task.CAS, start_type=dcs.mission.StartType.Cold, - group_size=group_size) + group_size=1) # more than one spawn on top of each other, setting group size to one for now zone_attack(afg, farp) elif airport: @@ -794,3 +825,6 @@ class RotorOpsMission: i.anchorByGroupName("ANCHOR") new_statics, new_vehicles, new_helicopters = i.copyAll(self.m, country_name, group.units[0].name, group.units[0].position, group.units[0].heading) + def addMods(self): + dcs.helicopters.helicopter_map["UH-60L"] = aircraftMods.UH_60L + self.m.country(jtf_blue).helicopters.append(aircraftMods.UH_60L) diff --git a/Generator/RotorOpsUnits.py b/Generator/RotorOpsUnits.py index 476639a..e0449f7 100644 --- a/Generator/RotorOpsUnits.py +++ b/Generator/RotorOpsUnits.py @@ -1,12 +1,27 @@ import dcs +import aircraftMods client_helos = [ dcs.helicopters.UH_1H, + #aircraftMods.UH_60L, dcs.helicopters.AH_64D_BLK_II, dcs.helicopters.Mi_24P, dcs.helicopters.Ka_50, ] +player_helos = [ + dcs.helicopters.AH_64D_BLK_II, + dcs.helicopters.Ka_50, + dcs.helicopters.Mi_8MT, + dcs.helicopters.Mi_24P, + dcs.helicopters.SA342M, + dcs.helicopters.SA342L, + dcs.helicopters.SA342Minigun, + dcs.helicopters.SA342Mistral, + dcs.helicopters.UH_1H, + aircraftMods.UH_60L, +] + e_attack_helos = [ dcs.helicopters.Mi_24P, dcs.helicopters.Ka_50, diff --git a/Generator/Scenarios/Caucasus Conflict - Batumi to Kobuleti (GRIMM).miz b/Generator/Scenarios/Caucasus Conflict - Batumi to Kobuleti (GRIMM).miz deleted file mode 100644 index d653c7c..0000000 Binary files a/Generator/Scenarios/Caucasus Conflict - Batumi to Kobuleti (GRIMM).miz and /dev/null differ diff --git a/Generator/Scenarios/Caucasus Conflict - Nalchik to Beslan (GRIMM).miz b/Generator/Scenarios/Caucasus Conflict - Nalchik to Beslan (GRIMM).miz deleted file mode 100644 index 22aaf14..0000000 Binary files a/Generator/Scenarios/Caucasus Conflict - Nalchik to Beslan (GRIMM).miz and /dev/null differ diff --git a/Generator/Scenarios/Mariana Conflict - Anderson to Won Pat (GRIMM).miz b/Generator/Scenarios/Mariana Conflict - Anderson to Won Pat (GRIMM).miz deleted file mode 100644 index 45bbf50..0000000 Binary files a/Generator/Scenarios/Mariana Conflict - Anderson to Won Pat (GRIMM).miz and /dev/null differ diff --git a/Generator/Scenarios/Mariana Conflict - Rota Landing (Mr Nobody).miz b/Generator/Scenarios/Mariana Conflict - Rota Landing (Mr Nobody).miz deleted file mode 100644 index 7f0c263..0000000 Binary files a/Generator/Scenarios/Mariana Conflict - Rota Landing (Mr Nobody).miz and /dev/null differ diff --git a/Generator/Scenarios/Nevada Conflict - Vegas Tour (GRIMM).miz b/Generator/Scenarios/Nevada Conflict - Vegas Tour (GRIMM).miz deleted file mode 100644 index 9235775..0000000 Binary files a/Generator/Scenarios/Nevada Conflict - Vegas Tour (GRIMM).miz and /dev/null differ diff --git a/Generator/Scenarios/PG Conflict - Dubai Tour (GRIMM).miz b/Generator/Scenarios/PG Conflict - Dubai Tour (GRIMM).miz deleted file mode 100644 index 440ef08..0000000 Binary files a/Generator/Scenarios/PG Conflict - Dubai Tour (GRIMM).miz and /dev/null differ diff --git a/Generator/Scenarios/PG Conflict - Musandam Valley (Mr Nobody).miz b/Generator/Scenarios/PG Conflict - Musandam Valley (Mr Nobody).miz deleted file mode 100644 index 9ca861a..0000000 Binary files a/Generator/Scenarios/PG Conflict - Musandam Valley (Mr Nobody).miz and /dev/null differ diff --git a/Generator/Scenarios/Syria Conflict - Aleppo Tour (GRIMM).miz b/Generator/Scenarios/Syria Conflict - Aleppo Tour (GRIMM).miz deleted file mode 100644 index 5a53187..0000000 Binary files a/Generator/Scenarios/Syria Conflict - Aleppo Tour (GRIMM).miz and /dev/null differ diff --git a/Generator/Scenarios/Syria Conflict - Mount Olympus (Mr Nobody).miz b/Generator/Scenarios/Syria Conflict - Mount Olympus (Mr Nobody).miz deleted file mode 100644 index 861b46d..0000000 Binary files a/Generator/Scenarios/Syria Conflict - Mount Olympus (Mr Nobody).miz and /dev/null differ diff --git a/Generator/Scenarios/_How to create your own scenarios.txt b/Generator/Scenarios/_How to create your own scenarios.txt deleted file mode 100644 index c06af51..0000000 --- a/Generator/Scenarios/_How to create your own scenarios.txt +++ /dev/null @@ -1,46 +0,0 @@ -You can add your own scenarios in this directory and they will appear in the mission generator. See the other scenario .miz files for examples of what to include in your template. - - -A scenario .miz file MUST have: - -1) Between 1-4 circular trigger zones called "ALPHA", "BRAVO", "CHARLIE", "DELTA" -2) At least one trigger zone with a name that starts with "STAGING". -3) A blue airport (recommend somewhere near/on-side your staging zone). -4) A red airport (recommend somewhere near/on-side your last conflict zone). -5) At least one Russian unit or static object. Anything will work, even a cow. You can set "HIDDEN ON MAP". -6) At least one USA unit or static object. See previous point. - - - -Optional: -7) USA FARP called "HELO_FARP" for automatic player placement. (Strongly suggest using immortal and invisible options for support vehicles, and add "static" to the group name to keep them from moving) -8) USA Carrier called "HELO_CARRIER" for automatic player placement. -9) Infantry spawn zones inside conflict zones. Add near buildings to simulate infantry hiding within. Name trigger zones like "ALPHA_SPAWN", "ALPHA_SPAWN_2, etc. - -Testing: -You should test your scenarios to ensure that vehicals move between zones as you expect. In some circumstances, vehicles may not be able to calculate a valid route to the next zone, and they will stop. Make sure they can route correctly by testing your scenario in fast forward, with few defending units. You can also get a good idea as to how long your scenario will take to complete. - - -Tips: --Position the center of conflict zones over an open area, as this position will be used to spawn units and FARPs. --Position the center of staging zones over an open area of at least 1000 x 1000ft to provide space for logistics zones. --For very scenery dense areas like forests or urban areas, smaller conflict zone sizes are highly recommended (eg 4000ft radius). A zone center near a roadway may also help keep units moving smoothly. --The conflict game type can be played with blue forces on defense. In this mode the last conflict zone is the only troop pickup zone. --Design your template so that it can be played in normal 'attacking' mode or 'defending' the conflict zone from enemy ground units starting from the staging zone. --Keep the zones fairly close together, both for helicopter and ground unit travel times. --You can place static objects and units in a scenario template. --You can change mission briefing and other mission options in the template. --Drop your templates in the RotorOps Discord if you'd like to have them added in a release for everyone. Maintain a similar naming convention and be sure to credit yourself in the .miz name. --Airfields can be captured with ground units. You might consider placing conflict zones over neutral airfields for a rearming area, or perhaps unarmed client slots. --Player/client slots are placed in this priority order: Carrier named "HELO_CARRIER", FARP named "HELO_FARP", and the blue airfield. --Friendly AWACs and tankers will be placed at the blue airport if parking is available, otherwise they will start in the air. --Enemy helicopters and planes will spawn at the red airport. --Late activation FARPs might be useful for rearming far from the player spawn point. --In "Defense" or with "Swap sides" option, USA and Russia ships, helicopters, planes, and ground units will swap countries. Static objects may not. Test it out. --Turn off or limit civilian road traffic. --Pay attention to rivers and bridges, as a far away bridge crossing may break routing if it's the only way across. See the testing notes above. - -v0.6: -You can now control the FARP spawning location and heading by adding a static object (such as a mark flag) with group name 'ALPHA' etc. Same for staging area logistics site..but the group name should be 'STAGING'. Change the object heading to better align with roads or map objects. The flags for these must be CJTFB country. - -You can dynamically insert complex arangements of objects such as large bases with multiplayer spawns. See the 'Imports' folder for more details. diff --git a/Generator/aircraftMods.py b/Generator/aircraftMods.py new file mode 100644 index 0000000..530d902 --- /dev/null +++ b/Generator/aircraftMods.py @@ -0,0 +1,23 @@ +import dcs.task as task +from dcs.helicopters import HelicopterType +from typing import Set + +# RotorOps class for UH-60L mod +class UH_60L(HelicopterType): + id = "UH-60L" + flyable = True + height = 5.13 + width = 16.4 + length = 19.76 + fuel_max = 1362 + max_speed = 300 + chaff = 30 + flare = 60 + charge_total = 90 + chaff_charge_size = 1 + flare_charge_size = 1 + + pylons: Set[int] = {1, 2, 3, 4, 5, 6, 7} + + tasks = [task.Transport] + task_default = task.Transport diff --git a/Generator/assets/briefing1.png b/Generator/assets/briefing1.png deleted file mode 100644 index 2fc2585..0000000 Binary files a/Generator/assets/briefing1.png and /dev/null differ diff --git a/Generator/build command.txt b/Generator/build command.txt index 20fdd10..d966d57 100644 --- a/Generator/build command.txt +++ b/Generator/build command.txt @@ -1,5 +1,8 @@ #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/install-config.ifp b/Generator/install-config.ifp new file mode 100644 index 0000000..08d0bf2 Binary files /dev/null and b/Generator/install-config.ifp differ diff --git a/Generator/installforge_constants.txt b/Generator/installforge_constants.txt new file mode 100644 index 0000000..777a13c --- /dev/null +++ b/Generator/installforge_constants.txt @@ -0,0 +1,34 @@ +for reference from here: https://installforge.net/forums/viewtopic.php?t=2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Generator/resources.py b/Generator/resources.py new file mode 100644 index 0000000..aeec8e3 --- /dev/null +++ b/Generator/resources.py @@ -0,0 +1,1197 @@ +# -*- coding: utf-8 -*- + +# Resource object code +# +# Created by: The Resource Compiler for PyQt5 (Qt v5.15.2) +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore + +qt_resource_data = b"\ +\x00\x00\x22\x09\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x92\x00\x00\x00\x8a\x08\x06\x00\x00\x00\x41\xfe\x13\x78\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x04\x67\x41\x4d\x41\x00\x00\xb1\x8f\x0b\xfc\x61\x05\x00\x00\x00\ +\x09\x70\x48\x59\x73\x00\x00\x0e\xc4\x00\x00\x0e\xc4\x01\x95\x2b\ +\x0e\x1b\x00\x00\x00\x21\x74\x45\x58\x74\x43\x72\x65\x61\x74\x69\ +\x6f\x6e\x20\x54\x69\x6d\x65\x00\x32\x30\x32\x32\x3a\x30\x34\x3a\ +\x31\x30\x20\x31\x37\x3a\x34\x32\x3a\x33\x33\x07\x28\xe8\x92\x00\ +\x00\x21\x71\x49\x44\x41\x54\x78\x5e\xed\x9d\x0d\x70\x55\xd5\xb5\ +\xc7\x69\xbc\x86\x10\x42\x88\x31\x84\x10\x53\x8c\x0c\x32\x6a\x11\ +\x2a\x2a\x8a\x02\x22\x41\xf9\x08\x55\x10\xa2\x45\x51\xaa\x68\x79\ +\xb6\xf2\xfa\xc4\x79\xf2\x9e\x8e\xbe\xb1\x53\x3f\x2a\x6a\xa7\xb6\ +\xa3\xaf\xd4\x5a\xdb\xa2\x28\xbc\xfa\x01\xe2\x57\x79\x7e\xbe\x32\ +\xb4\xf3\xaa\x3e\x6b\x5b\x47\x1d\x8a\x48\x42\x80\x84\x86\x10\x42\ +\x80\x18\xdf\xfa\x9f\xdc\x7f\xf8\x67\x71\x6e\x12\x14\xb8\x87\xdc\ +\xfb\x9b\x59\xb3\xd6\x5e\x6b\xef\x7d\xf6\xd9\x7b\x9d\x7d\xcf\xb9\ +\xb9\xf7\xa6\x47\x9a\x34\x69\x0e\x13\x3f\xfb\xd9\xcf\x4e\x7a\xf2\ +\xc9\x27\xcf\x8e\x17\xd3\x84\x90\x11\xd7\x69\x3a\x60\xd8\xb0\x61\ +\x73\x4b\x4a\x4a\xae\x8f\x17\xd3\x84\x90\x4e\xa4\xce\xc9\xfe\xf4\ +\xd3\x4f\xaf\xda\xbc\x79\xf3\x25\x66\x17\xb4\xba\xd2\x78\xd2\x89\ +\xd4\x09\xf7\xde\x7b\xef\xcc\x1d\x3b\x76\x14\xee\xda\xb5\x2b\x67\ +\xc5\x8a\x15\x57\xc5\xdd\x69\x1c\xe9\x44\xea\x98\x8c\x11\x23\x46\ +\xcc\xfb\xfc\xf3\xcf\x83\x42\x9f\x3e\x7d\xe6\x9a\x8a\x05\x85\x34\ +\xed\x48\x27\x52\x07\xc4\x62\xb1\x11\xeb\xd7\xaf\x1f\xd9\xd2\xd2\ +\xd2\x03\xb2\x69\xd3\xa6\x93\x86\x0f\x1f\x3e\x3e\x1e\x4e\x23\xa4\ +\x13\xa9\x03\x1e\x7f\xfc\x71\xec\x46\x31\x24\x11\x76\xa5\xcf\x3e\ +\xfb\x2c\xe3\xfe\xfb\xef\x9f\x17\x0f\xa7\x11\xd2\x89\x94\x98\x82\ +\xbd\x7b\xf7\xce\xe4\xcb\x1a\x34\xa4\xb6\xb6\x76\x92\x15\x4b\x03\ +\x67\x9a\x36\xd2\x89\x94\x80\xdb\x6e\xbb\xed\x9b\xbb\x77\xef\xce\ +\x8b\x17\xdb\xb0\xe4\xca\x7e\xe4\x91\x47\xae\x8e\x17\xd3\xc4\x49\ +\x27\x52\x38\xb1\x31\x63\xc6\x5c\xc7\x5d\xc8\x5e\xd2\xe2\xee\x1e\ +\xc1\xbd\x52\x71\x71\xf1\xb7\xcc\xcc\x6e\xf5\xa4\x01\xe9\x44\x0a\ +\xa1\x67\xcf\x9e\xa3\xab\xaa\xaa\x86\xc2\xd6\x97\x36\xb2\x6d\xdb\ +\xb6\x81\xe5\xe5\xe5\x53\xe2\xc5\x34\x46\x3a\x91\x42\x58\xbe\x7c\ +\xf9\xf5\xb6\xf3\x64\x68\xf2\x00\xec\x46\x00\xfe\x85\x0b\x17\xe2\ +\xa6\x3b\x3d\x7f\x71\xd2\x13\xb1\x3f\x25\xf5\xf5\xf5\x53\xfd\x4e\ +\xf4\x95\xaf\x7c\x25\x10\x96\x2b\x2b\x2b\xc7\x9a\x3a\x25\x28\xa4\ +\x49\x27\x92\x67\xf1\xe2\xc5\x57\x35\x37\x37\x67\x23\x61\x90\x38\ +\x80\x5a\xb1\xdd\x29\xf3\xa9\xa7\x9e\xc2\x1b\x94\x69\x8c\x74\x22\ +\xb5\x27\x6b\xc0\x80\x01\x73\xb9\xeb\x00\xee\x42\xd4\x8c\x41\xf7\ +\xea\xd5\xeb\x72\x33\xf7\x7b\xb2\x4b\x45\xd2\x89\x24\x9c\x79\xe6\ +\x99\x13\x6a\x6b\x6b\x07\xc5\x8b\x6d\x09\xa4\x92\x91\x91\xd1\x96\ +\x4c\x0d\x0d\x0d\x85\xf3\xe7\xcf\xc7\x1f\x73\x53\x9e\x74\x22\x09\ +\x77\xde\x79\xe7\x77\xe3\x66\x90\x34\xaa\x01\x6c\xdd\x91\x20\xb3\ +\x67\xcf\x4e\xdf\x74\x1b\xe9\x44\xda\xc7\xe0\xcd\x9b\x37\x4f\x80\ +\x81\x04\xe1\x13\x1a\x77\x22\xa2\x89\x04\xd6\xad\x5b\x77\x46\x2c\ +\x16\x1b\x19\x14\x52\x98\x74\x22\xc5\x79\xe2\x89\x27\xda\xfd\x5d\ +\x8d\x09\x14\x26\x4c\x22\xd4\x35\xc9\x58\xb1\x62\x45\xca\xff\xfd\ +\x2d\x9d\x48\xad\x64\x67\x66\x66\x5e\xc5\x04\xc1\x7d\x10\x08\x4b\ +\x22\xc6\x00\xca\xa0\xb1\xb1\xf1\x52\x53\x29\xfd\xa1\xb7\x74\x22\ +\x19\xd7\x5f\x7f\xfd\x25\x96\x0c\x85\xf1\x62\xdb\x8e\x84\xa4\xd1\ +\x24\x22\x2c\xa3\x1e\x64\xcf\x9e\x3d\xd9\x3f\xf9\xc9\x4f\x52\xfa\ +\x43\x6f\xe9\x44\x32\x2a\x2a\x2a\x82\xcf\x63\x73\x47\x02\x3e\x79\ +\x80\xfa\x58\x97\x2f\x85\xa7\x9e\x7a\xea\x75\x56\x4c\xd9\x0f\xbd\ +\xa5\x13\xa9\x47\x8f\x11\x55\x55\x55\x67\x33\x31\x98\x2c\xba\x1b\ +\xc1\x66\x59\x93\x4d\x6d\xeb\xe3\xa4\xfc\xfc\xfc\x71\xf1\x62\xca\ +\x91\xf2\x89\xb4\x72\xe5\x4a\xdc\x64\x07\x7f\x57\x83\xf0\x69\x8d\ +\x49\xc2\x64\x62\x12\x79\x9b\x6d\xa0\x97\x2d\x5b\x96\xb2\xdf\x34\ +\x49\xf5\x44\xca\xdf\xb5\x6b\xd7\xa5\x4c\x04\x26\x07\xa0\xf6\x37\ +\xd7\x14\xf8\xd1\x8e\xf5\xd0\xb6\xb6\xb6\x16\x9f\x08\x48\xc9\x0f\ +\xbd\xa5\x74\x22\x2d\x5a\xb4\xe8\xf2\xa6\xa6\xa6\x3c\x26\x11\x04\ +\x30\xa1\x20\xdc\xa1\x00\xca\x5a\x57\xcb\xa8\xd7\xdc\xdc\x9c\xf5\ +\xe8\xa3\x8f\xa6\xe4\x87\xde\x52\x39\x91\x62\x67\x9c\x71\x06\x6e\ +\x90\xdb\xe0\xee\xa2\xbb\x10\xef\x8d\x88\xc6\x3c\x48\xa8\xa2\xa2\ +\x22\x7c\xe8\x2d\xab\xd5\x93\x3a\xa4\x6c\x22\xe1\xc3\x6b\x95\x95\ +\x95\x43\xf5\x65\x4d\x41\x99\x3b\x0e\xd0\x64\x82\xcd\x32\xdb\xb2\ +\xde\x3f\xfe\xf1\x8f\x81\x13\x26\x4c\x48\xb9\x0f\xbd\xa5\x6c\x22\ +\x3d\xf3\xcc\x33\xd7\xe3\x5b\x21\x9a\x04\x7a\xcf\xc3\x9d\xc7\xef\ +\x40\xa8\xcb\x44\xd2\x64\x52\x7d\xfb\xed\xb7\xb7\xfd\xcd\x2e\x55\ +\x48\xd5\x44\xc2\x87\xd7\xda\x76\x0d\x26\x80\xdf\x65\x00\x77\x2c\ +\x10\x16\x67\x42\x69\xfd\xaa\xaa\xaa\xd1\x66\xa6\xd4\x87\xde\x52\ +\x32\x91\x1e\x7b\xec\xb1\xab\xf6\xec\xd9\x93\xc3\x84\x60\x82\xa8\ +\xa6\x8d\x1d\x09\xa2\x75\x35\xae\x30\xe9\x6c\xa7\xc3\x87\xde\xda\ +\xdd\x7f\x75\x77\x52\x31\x91\xb2\xfa\xf7\xef\x7f\xb5\xde\x1b\xc1\ +\x06\x9a\x28\x00\x1a\x3e\x85\x31\xb6\x25\xb4\xe9\xcf\xca\xca\xfa\ +\xa6\x15\x53\xe6\x43\x6f\x29\x97\x48\x13\x27\x4e\xbc\xb0\xa6\xa6\ +\x66\x70\xbc\xd8\x2e\x19\x34\x49\x68\xf3\x1e\x09\x65\xfa\x68\x23\ +\x46\x9b\x31\x80\xf6\x0d\x0d\x0d\x45\xdf\xf9\xce\x77\xa6\xc5\x5d\ +\xdd\x9e\x94\x4b\xa4\x05\x0b\x16\x04\x3f\x0a\xc1\x04\x62\x02\x68\ +\x22\xa8\x0d\xc2\xea\x84\xc1\x7e\x21\xd8\xe5\xe2\x1f\x7a\x4b\x89\ +\xbf\xbf\xa5\x5a\x22\x0d\xd9\xba\x75\x6b\xf0\xe1\x35\xc0\x97\xb7\ +\x44\x89\x03\xc2\x12\x8e\xbe\x30\xb4\x3f\xfc\x00\x85\xa9\x11\x41\ +\xa1\x9b\x93\x52\x89\xb4\x74\xe9\xd2\xb9\xb8\x11\xe6\x3d\x11\xc0\ +\xc2\x73\xf1\x29\x3e\x51\xb4\xac\x75\x81\xb6\x63\x59\xee\xbf\x52\ +\xe6\x43\x6f\xa9\x94\x48\x39\x76\x03\x1c\x7c\x66\x08\x0b\xcd\xaf\ +\x61\xf3\x3e\x87\x30\x51\x3c\x61\x3e\xa2\x89\x06\x58\x86\xc6\x0f\ +\x51\x98\xd9\xf6\x59\xa7\xee\x4a\xca\x24\xd2\xdc\xb9\x73\x2f\xda\ +\xb1\x63\x47\x91\x26\x0a\x6c\x5d\x74\xa0\x09\x43\xdb\x6b\xe2\xcb\ +\x04\x7e\xf6\xdd\xd4\xd4\x94\x7b\xff\xfd\xf7\xe3\x6b\x4b\xdd\x9a\ +\x54\x49\xa4\x8c\x2b\xae\xb8\xe2\x7a\x2e\x30\x60\x12\xf8\x27\x2f\ +\xfa\x49\x58\xd9\xfb\xb4\x0f\xc0\x63\x90\xf8\xdf\xf4\xba\xf5\x4d\ +\x77\xaa\x24\xd2\xb0\xaa\xaa\xaa\x73\x78\x6f\xc4\x7b\x18\x25\x51\ +\x72\x40\x87\xc1\xc4\xf1\xed\x08\xfc\x3c\x4e\x65\x65\xe5\x29\xc5\ +\xc5\xc5\xdd\xfa\x43\x6f\x29\x91\x48\xcf\x3f\xff\x7c\xf0\xe1\xb5\ +\x78\x31\xa0\xb3\x24\x21\x4c\x3e\xfa\xa0\x35\xce\x32\x44\x77\x26\ +\x24\x10\x34\xeb\x2c\x59\xb2\xa4\x5b\x7f\xe8\x2d\x15\x12\x29\x77\ +\xd7\xae\x5d\x97\x73\x77\xd0\xdd\x88\x8b\xad\x0b\x4e\x1b\x20\xce\ +\xe4\x00\xbe\x1e\x85\x84\xf9\xd0\x07\x64\xdb\xb6\x6d\x53\xad\x58\ +\xdc\xea\xed\x7e\x74\xfb\x44\xb2\x1b\xdd\xd9\xb8\xe1\xe5\x82\x42\ +\x74\xb1\x51\x06\xba\xf8\x80\xf5\x40\x58\x4c\xd1\xfe\xd4\x56\x9a\ +\x9b\x9b\x33\x97\x2e\x5d\x7a\x6d\xbc\xd8\xed\xe8\xee\x89\x94\x61\ +\x37\xba\xed\xde\xc7\xc1\x0e\xa3\xc9\x13\xf6\xf2\x16\x96\x08\x68\ +\xc3\xe4\xf2\xc9\xa2\x49\x07\x7c\x1c\xa0\x4e\x51\x51\x11\x7e\xbd\ +\x24\xb3\xd5\xd3\xbd\xe8\xd6\x89\x14\x8b\xc5\xce\xc1\x87\xd7\x98\ +\x04\xc0\x2f\x3a\x60\x32\xd1\xcf\xba\x0a\x93\x83\xa2\x3e\xbe\xfc\ +\x41\x6b\x5f\xf4\x91\xad\x5b\xb7\x0e\x9c\x32\x65\x0a\x5e\xe2\xba\ +\x1d\xdd\x3a\x91\xec\x26\xfb\xbb\x76\x4f\xd4\xf6\xcb\x6b\x3e\x41\ +\xb0\xd0\xf0\xa9\xdf\xdb\xbc\xa7\x62\x62\xf8\xfa\x80\x65\xf5\x27\ +\xaa\x7f\xeb\xad\xb7\x76\xcb\x77\xba\xbb\x73\x22\x15\x6f\xdf\xbe\ +\xfd\x22\x3e\x75\x71\x31\xb9\xc0\x7e\xe7\x80\x68\xb9\x23\xb8\x03\ +\x79\xd1\x3e\x09\x63\x00\x63\xa8\xae\xae\xc6\x0f\xbe\x77\xbb\x0f\ +\xbd\x75\xc7\x44\xc2\x1b\x7f\xd9\x0b\x17\x2e\xbc\x06\x5f\xa5\xf6\ +\x3b\x02\xe0\xc2\xea\x82\x93\x30\x9f\x82\x64\xe9\xa8\x3f\x88\x26\ +\x1a\xd0\x31\x58\x62\xc7\x1e\x7c\xf0\x41\xbc\x41\x89\x5f\xc5\xed\ +\x36\x6f\x52\x76\x3c\x6b\xc9\x07\x89\x8e\x6f\x64\x40\x72\x4c\xf2\ +\x7a\xf7\xee\x5d\x30\x63\xc6\x8c\x82\x51\xa3\x46\x15\xf6\xef\xdf\ +\x3f\xcf\xee\x83\x8e\xcd\xc9\xc9\x29\xb2\x45\xcb\xb7\x05\xc4\xf7\ +\xd4\x20\xb9\x8d\x8d\x8d\x79\x9f\x7d\xf6\x59\xcc\x2d\x62\xbb\x45\ +\x07\x4c\x0c\xbf\x9b\x68\x12\xa0\x9d\x1d\x37\x28\xfb\x1d\x4e\xfb\ +\x07\xea\xf7\xe0\x18\x71\x69\xb6\xfe\xea\x4c\xea\x73\x73\x73\xeb\ +\x77\xef\xde\xbd\xc5\xc2\x0d\x75\x75\x75\x1b\xec\xe9\xae\xe1\xa3\ +\x8f\x3e\xda\x54\x53\x53\xd3\x70\xf7\xdd\x77\x6f\xb4\x73\xa8\xb3\ +\xd8\x36\x93\x7a\x93\x3d\x26\x4d\x26\xcd\x26\x91\xe2\x70\x26\xd2\ +\x7e\x49\xd1\xb7\x6f\xdf\x82\x09\x13\x26\xe4\x8d\x1c\x39\xb2\xa8\ +\xb4\xb4\x34\xef\xe8\xa3\x8f\xee\xd7\xab\x57\xaf\x42\x4b\x8e\x02\ +\xb3\xf3\x6c\x82\x83\xa4\x30\x9d\x6d\x0b\x98\x6d\x93\xda\x6e\x07\ +\xc5\x62\x61\xc1\xb9\x68\x5c\x7c\x5d\x6c\x8a\x82\x7a\xac\x0b\x68\ +\xab\x5f\x6d\x5b\xdc\xfd\x12\x89\xf8\xfe\x61\xa3\x1d\xeb\xc1\x66\ +\x9c\x7d\x32\x79\x59\x66\x0c\x3e\x26\x34\xda\x4b\xac\xc5\xe6\xa3\ +\xd1\xa4\x09\x09\xd8\xa7\x4f\x9f\x06\xdb\x6d\xab\xad\x7e\xa3\x25\ +\xdc\x06\xab\xdb\xf0\xc9\x27\x9f\x6c\xfe\xdb\xdf\xfe\x56\xb3\x7c\ +\xf9\xf2\x9a\x2d\x5b\xb6\xd4\x58\x33\x24\x20\x93\xaf\xd1\xa4\xfd\ +\xc0\x0f\x32\xad\x23\xfd\x62\xe0\x31\x16\x09\x81\x2d\x3a\xaf\x67\ +\xcf\x9e\x05\x65\x65\x65\xf9\x67\x9d\x75\x56\xe1\x90\x21\x43\xf2\ +\x2d\x21\xfa\x65\x67\x67\x17\x58\x52\x14\x66\x66\x66\xe6\x61\xa7\ +\xd8\xb9\x73\x67\x1e\xde\x4f\xd9\xbb\x77\x6f\x96\x9d\x7c\xf0\x18\ +\xcc\x49\x56\xd4\x17\x16\x27\x9c\x7c\xe2\xcb\xb0\x59\x86\x96\x85\ +\x09\x34\xd0\x45\x05\x3e\x86\x05\x85\xd8\xae\xb7\x5f\x7f\xd4\x7a\ +\x5c\xaf\x35\x06\x50\x0e\x3b\x16\xd1\xfa\xb4\xb5\x9e\x2f\x03\x94\ +\xe9\x8b\x27\x60\x8b\x8d\xbd\xc5\xe6\xbe\xc9\xd6\x65\x8f\xed\x7a\ +\x5b\x90\x84\x46\xb5\x5d\x8c\xf5\xf6\xf4\xb8\xd1\xec\xed\x1f\x7f\ +\xfc\x71\xcd\x9a\x35\x6b\xaa\xff\xfc\xe7\x3f\xd7\x57\x55\x55\x55\ +\x5b\xf3\x06\x13\x24\x20\x93\xaf\xcb\xe8\x88\x32\xe7\xcf\x9f\x5f\ +\xf8\x97\xbf\xfc\x25\xf7\xe4\x93\x4f\x2e\xb4\x01\xe5\xd9\x2e\x51\ +\x60\x57\x40\xa1\x4d\x62\xbf\xac\xac\x2c\xbc\xac\x14\x61\xa7\xb0\ +\x44\x28\xb0\xc4\xc8\xb3\xab\x22\xcb\x06\x96\x69\x03\xc7\x4b\x48\ +\x06\x4e\x86\x27\x4a\x21\x9c\x1c\xa0\x36\xe8\x2c\xc6\xc5\x26\x7a\ +\x9c\x30\x3f\x41\x39\x3e\xb1\x6d\x65\xd6\x51\x9f\xe2\xfd\xa8\xcb\ +\x44\x62\x59\x8f\xc1\xbe\xbc\x1f\xf8\xb2\xc7\x1f\x5b\xeb\x6b\x2c\ +\xcc\x0f\xad\xe7\x46\x50\x0e\x3b\x2e\x7c\xea\xf7\x65\x03\x3b\x56\ +\x8b\xb5\x6f\x3a\xea\xa8\xa3\x9a\x91\x7c\xb6\x11\x34\xec\xd8\xb1\ +\x63\x8b\xbd\x22\x6c\xab\xaf\xaf\xdf\x62\xb7\x0b\x5b\x6d\x33\xd8\ +\x66\xbb\x5f\xb5\xe5\x43\xe3\xea\xd5\xab\x37\xda\xab\x4a\xe3\x1b\ +\x6f\xbc\x51\xa5\xa3\xc8\xfa\xfd\xef\x7f\x7f\xf7\x87\x1f\x7e\x78\ +\x83\x0d\x30\xf8\xe5\x32\xc0\x83\x61\x80\x1c\x34\x07\x4b\x1f\xc5\ +\xfb\x58\x57\xd1\xc1\xb3\x3e\xed\x30\xd4\x9f\xa8\xae\x3f\x06\xd0\ +\x3a\x3e\x11\xb5\xec\xc7\x09\x8d\x98\x1e\x4b\x13\x09\x70\x01\xb5\ +\x4f\x94\x39\x67\x1a\xf3\x75\xb4\x6f\xe0\xe3\x1e\x1e\x4b\x63\xf4\ +\x91\xae\xf4\x01\xb4\x1e\x6c\x15\xa0\xf5\x68\x03\xc6\x39\x0e\x1e\ +\x03\xbb\x5e\x49\x49\xc9\xaf\xc7\x8f\x1f\xff\x5d\x7f\xd4\x8c\x97\ +\x5e\x7a\xe9\x9f\x37\x6d\xda\xb4\xc8\x5e\x82\x82\x27\x0a\xdc\x1f\ +\xb0\xa1\x6a\x74\x6e\x99\x1b\x94\x01\x16\x87\x7e\x68\x15\xfa\x38\ +\x20\x4f\x47\x71\xc6\x80\xef\x4b\xdb\xc0\x56\x3f\xe8\x28\xe6\xcb\ +\x44\xe3\x00\x13\x0a\xe1\x4b\x9b\xa2\x7d\x40\x33\xce\xb2\xb7\x95\ +\x30\x5f\x47\xa0\xbe\x8e\xa9\xab\xfd\xa2\xcc\xb6\xd0\x68\xcb\x3a\ +\x2c\x87\xc5\x60\x6b\x0c\x1a\x82\x75\x86\xd8\xda\xb7\x0c\x1c\x38\ +\xf0\x8e\x71\xe3\xc6\xfd\x00\xd5\xf7\x65\x42\x2b\x9f\x2f\x59\xb2\ +\x64\xad\x65\xd9\x3b\x56\x09\xef\xc1\x04\xf7\x31\xbc\x82\xd1\x11\ +\xa0\x06\x3c\xb0\x3f\x20\x84\xc9\xc5\xb8\xb7\xbd\x8f\x5a\x85\x7d\ +\x40\x33\xae\x3e\x1f\x53\x50\xa6\x00\xd6\x03\x3e\xe6\xb5\x82\xf3\ +\xb2\x7b\x8d\xfd\xea\x6b\x5d\x96\x21\x1c\x57\x47\xe2\xcf\xc1\x4b\ +\x67\x7d\x30\xee\x8f\x4d\xa1\x9f\xe0\x1c\x20\x68\x47\x1b\x71\xda\ +\xac\xc3\x36\x61\x3e\xb4\x85\xe0\x7e\xab\xa1\xa1\xe1\x5b\x15\x15\ +\x15\x3f\x45\x15\xc4\xf6\x1d\x69\x7f\xbe\x6e\x4f\x00\xcf\xe1\xbb\ +\xec\xec\x94\x19\x8b\x8e\x61\xa3\x53\x80\x32\x77\x27\xd8\x10\x9e\ +\x28\x84\x78\x9b\x7d\xb1\x7f\xa0\x75\x3c\xbe\x4d\x47\x1a\xc0\xf6\ +\x30\x96\xa8\x0d\x61\x99\xe7\xac\xf7\x48\x5d\x81\xfd\xd2\xf6\xe8\ +\x31\xbb\xd2\x27\xeb\x68\xdd\x8e\x7c\x80\x36\xcf\xa1\xab\x02\xa0\ +\xd1\x0e\x63\xe4\x38\xb1\xc6\x36\x0f\x5b\xe6\xcd\x9b\x57\x51\x57\ +\x57\xf7\x66\xe0\x8c\xb3\xff\x19\xb6\x67\xe0\xaa\x55\xab\x96\xdb\ +\x1d\xfd\x48\x1e\x04\x9d\x13\x4e\x96\x26\x0d\x0f\xea\x13\x09\x1a\ +\x75\xa9\x11\xd7\xb2\xd6\x03\xea\x4b\x84\xb6\x0d\xeb\x4b\x7d\x80\ +\x7e\x10\xd6\x46\xdb\x52\xfb\x44\x02\xda\x1f\x6d\xc0\x36\xea\x53\ +\xf4\x18\x61\xed\x80\xef\x9b\x65\xad\x0f\x1b\x71\xae\x05\xcb\xb4\ +\x29\xac\xa3\x3e\x15\xa0\x71\xad\x4f\x1b\x9a\x72\xdc\x71\xc7\xfd\ +\x75\xf2\xe4\xc9\x17\x5b\xb3\x8f\x83\xc6\xc2\xbe\xbd\x3e\x9c\x0d\ +\xe5\xe5\xe5\x17\xf4\xeb\xd7\xef\x59\x76\x86\xac\x84\xe6\xc1\x29\ +\x1c\x80\x0e\x4c\x7d\xea\xd7\x18\x05\xf8\x32\xd0\x98\xd7\xde\xa7\ +\x78\x1f\xc7\x7c\x20\x74\xa5\x5f\x0f\xe2\x9c\x2b\x15\x8d\x69\x1f\ +\x2c\x53\x08\xe7\x07\xa8\xa6\xad\xf3\xc7\xb2\x3d\x41\xb7\x6b\xe7\ +\xe7\x58\x25\x6c\x1c\xa8\x0f\x78\x91\x73\x33\x80\x2e\x2a\x2a\x7a\ +\xc5\x92\xe8\x3c\x0b\xef\x97\x44\xa0\xb3\x44\x02\xf5\xd3\xa6\x4d\ +\xbb\xcc\x3a\xbe\x2f\x16\x8b\xe1\xf1\x30\xe8\xd8\xec\x76\x49\x05\ +\x54\xf3\x24\xfc\xc9\x68\x99\x75\xa9\xd5\x0f\xc2\xea\x74\x84\xd6\ +\xc3\xb8\x68\x87\x69\x85\x75\x95\xb0\x3e\x12\x11\x56\x2f\xd1\xf1\ +\x12\x95\xa1\x55\xfc\x98\xb4\x5e\x47\x73\xaa\x3e\xd6\x57\x21\xbe\ +\x0d\xe1\x71\x99\x44\xb8\xa9\xb6\xdd\xf8\x91\xa9\x53\xa7\x4e\x37\ +\x37\xde\xe8\x0c\xc5\xdf\x6c\x27\xe2\xb3\x95\x2b\x57\xfe\xf7\xe0\ +\xc1\x83\x37\x17\x17\x17\x4f\xb0\x41\x04\x4f\x74\x1c\x2c\xc0\x60\ +\x70\x60\x6a\xa2\x83\x54\x1b\xa0\x4c\x9f\x6f\xa7\x76\x57\x08\xab\ +\x9f\xa8\x8f\x8e\xfa\xf6\x31\x4c\x28\xce\x13\x37\xdb\x80\x71\x68\ +\x8e\x5d\x7d\x5e\xa3\x3d\xf1\x71\x05\x3e\x0a\xcb\x3a\x5f\xbe\x0c\ +\x38\x67\x7e\xee\x00\xeb\x42\xd3\xd6\x3e\x54\xd3\xcf\xb1\x52\x5b\ +\x12\x35\x37\x36\x36\xde\x7e\xcd\x35\xd7\xfc\x9b\x15\xf1\x26\x65\ +\x42\xf6\x3f\xa3\x4e\x18\x3a\x74\xe8\x94\x5b\x6e\xb9\xe5\x37\x76\ +\x80\x7c\x4d\x24\x6c\xab\x80\x27\x04\xad\x36\xa1\x8d\xc1\xf2\x24\ +\x40\x67\x5a\x81\x4f\x27\x80\x84\xd5\x0d\x03\xf5\xba\xda\x07\x62\ +\x38\xcf\x3e\x7d\xfa\xc4\x3d\xfb\x16\xc1\xf7\x11\xa6\x01\xeb\x13\ +\xf8\x75\xee\x14\x6d\x4f\x60\x53\x58\xa6\x86\x68\x7f\x8c\x01\xc6\ +\xe9\xf3\x65\xc0\x76\x1c\x2b\x34\xc4\x2e\x9c\x06\x7c\xa1\xd4\x36\ +\x90\x65\x41\xa0\x13\xba\x36\xf3\xfb\x33\xf4\xd9\x67\x9f\x5d\x59\ +\x5b\x5b\x5b\x8a\x81\x40\x70\x70\x26\x13\xd0\x2b\x11\xf8\xc4\x61\ +\x59\xfd\x14\xe2\xfb\x40\x8c\x75\x13\x69\x0f\x7d\x1a\x57\x5b\xdb\ +\x86\xf5\x01\x1f\x44\x6f\xb6\x15\xdf\x36\x51\x1f\x89\x60\x7d\x9f\ +\x58\xec\x4b\x05\xd0\x66\x5c\xdb\xf9\x7a\xec\x57\x7d\x6a\x53\xa3\ +\x1e\x05\xe4\xe6\xe6\x56\x5f\x76\xd9\x65\xd3\x9b\x9b\x9b\xd7\x06\ +\x8e\x2e\xd0\x7e\xa5\xba\xce\xfb\x76\xdf\x34\xaa\xa4\xa4\x64\x2d\ +\x16\x1b\xf7\x4a\x18\x10\xee\x9b\x80\x3f\x49\xd8\x3a\x70\x5f\x26\ +\xea\x27\x3e\xee\xe1\xc9\x13\xd6\x51\x3f\x7c\x2c\x7f\xd1\x3e\x14\ +\xc4\x7d\x3f\xbe\x2e\xcb\x89\x34\xfb\x40\x59\x93\x81\x71\x9d\x3b\ +\xfa\x00\xcb\xd0\xa8\xa3\xf5\xd9\x27\x60\x9c\xb6\x6a\x05\xeb\xa7\ +\x72\xfc\xf1\xc7\xbf\x3f\x63\xc6\x8c\x73\x0f\x24\x89\xc0\x17\x4d\ +\x24\x50\x3d\x71\xe2\xc4\xb2\xbc\xbc\xbc\x65\x38\x19\x26\x13\x35\ +\x09\x3b\x41\xa0\x7e\x1f\x23\x5d\xf1\xfb\x58\x22\x58\x0f\x63\xf5\ +\x0b\xe3\xfb\x60\x5c\xfd\x5a\x4f\xdb\xc3\x66\x0c\xa2\x6d\xb5\x1e\ +\xe1\xe2\x26\xaa\xe7\xe3\xd0\xda\x3f\xe0\xdc\x21\xa6\x36\xeb\xb0\ +\x0f\xe0\x63\xea\x53\x01\xf6\x78\xff\xd2\xf8\xf1\xe3\xc7\x98\xb9\ +\x2e\x70\x1c\x00\x5f\x26\x91\x40\xa3\x65\xef\x2c\x7b\x49\xbb\x07\ +\x77\xf7\xc8\x68\x80\x64\x02\x1c\x20\x4f\x42\x4f\x06\xd0\x86\xf6\ +\x27\xaf\x1a\xfd\x78\x1f\x60\xff\x40\x6d\xa2\xed\xc2\xfa\x80\x2f\ +\xac\x0f\x3d\x06\xce\xc9\xf7\xa1\x71\xa0\x7d\x28\xac\xcb\x76\x40\ +\xfb\xa0\xcf\x6b\x00\x9b\x49\x02\xf1\xc7\xd6\x38\x50\x9b\xf5\x58\ +\x87\xc2\x18\x40\x7f\x14\x9c\x63\xaf\x5e\xbd\x1e\x9a\x30\x61\x02\ +\xde\x23\xc2\x5f\xff\x0f\x98\xae\x3e\xb5\x75\xc4\xe7\x78\xa2\x1b\ +\x30\x60\xc0\xc6\xd2\xd2\xd2\x89\x96\x54\x31\x0e\x50\x07\xad\xc0\ +\xcf\x38\x63\x5c\x30\x96\xa1\x13\xd9\x1e\xf8\x7c\x5b\xea\x44\x36\ +\x51\x5f\xa2\x3e\xe0\xcf\xcc\xcc\x6c\xf3\x01\xfa\x69\x13\xb5\x3b\ +\x43\xfb\xa0\x06\x1c\x07\xe3\x89\xea\xa8\xdf\x97\x15\x8d\x01\xf6\ +\x0d\xb1\x8b\x7e\x4f\x75\x75\xf5\xcd\xf3\xe6\xcd\xfb\x0f\x0b\x7d\ +\xe1\x0f\xcc\x75\xfd\xac\xbb\x40\x7e\x7e\xfe\x84\x87\x1f\x7e\xf8\ +\x71\xfc\x8b\x4e\xdc\x78\x63\xa0\xbc\x52\xa0\x39\x78\x94\xa1\x91\ +\x3c\xd0\x84\x71\xfa\x7d\x8c\xed\x08\xca\xa8\x4b\x9b\x75\x80\x6f\ +\xaf\x71\xfa\x7d\x39\xac\x0f\x80\xb1\x67\x67\x67\x07\x65\xdf\xc6\ +\xd3\x59\x8c\x71\xce\x47\x57\xfc\x10\x2d\xa3\x0e\xcb\xd4\x14\xc0\ +\x3e\xd4\x8f\x32\xfb\x85\xc6\xab\x46\x56\x56\x56\xdd\xe2\xc5\x8b\ +\xe7\xae\x5e\xbd\xfa\xe9\x20\xf0\x25\x08\x3f\xe3\x2f\xc7\x29\xb6\ +\x43\x3d\xb3\x79\xf3\xe6\x21\x38\x21\x3d\x39\x08\x17\x9e\x27\xc5\ +\x32\x34\xea\x32\x89\xe8\x03\x2c\x03\xda\x9c\x1c\x00\xcd\xe3\x00\ +\xad\x4f\xc2\xda\x01\xb6\x4d\xd4\x07\x7c\x10\x26\x12\xd1\xb8\xf6\ +\xa1\xb0\xac\x0b\xcf\xba\xde\x26\xb4\x7d\x5c\xe7\x12\xb0\x4c\x9f\ +\xda\xfe\x78\x0a\xca\x78\x28\x3a\xe6\x98\x63\x36\x7c\xe3\x1b\xdf\ +\xa8\x30\xd7\x1f\x5b\x23\x5f\x8e\xd6\x95\x3a\xb8\xfc\xd5\x06\x38\ +\xc6\x6e\xdc\xde\x44\x22\x30\x31\x20\xfe\x46\x5c\x4f\x5e\xfd\x40\ +\x27\xc0\xb7\xf1\xd0\x17\x36\x69\xf4\x69\x9d\xb0\x3e\x48\x58\x1f\ +\x8a\xef\xcf\xa3\x7e\xd8\x10\xed\x23\x2c\x4e\x68\x87\xd5\xd1\xe3\ +\x6a\xa2\x50\xd3\x0e\x4b\x22\xc6\xb8\x1e\x27\x9c\x70\xc2\xff\xda\ +\x1a\xe1\xcf\x1d\x07\x25\x89\xc0\xa1\x48\x24\xb0\x65\xb2\x91\x9b\ +\x9b\xbb\x04\x27\xc3\x13\x00\x28\xeb\x89\x2b\x89\x26\x5c\xfd\x1e\ +\xdf\xcf\x17\xe9\xc3\x13\xd6\x47\x67\xed\x19\x87\xf6\x82\x3e\xa0\ +\x19\x27\x89\xfa\x66\x59\xdb\x31\x41\x40\x58\x1f\x40\x6d\xdf\x07\ +\x2f\xe8\xc2\xc2\xc2\x15\x63\xc6\x8c\x29\xb3\xd0\xfa\xa0\xc2\x41\ +\xe2\x50\x25\x12\x68\xbc\xf4\xd2\x4b\xaf\xde\xb3\x67\xcf\xf7\xed\ +\x24\x82\x27\x3a\x08\x76\x25\x9e\x14\x4e\x32\x4c\x30\x69\x8c\x53\ +\x13\xb5\x3b\x23\x51\x1f\x07\x82\x8e\x93\xc0\x47\x68\x33\xae\xf5\ +\xd8\x4e\x8f\x4f\xad\x7d\xb0\x1e\x51\x1b\x73\x81\xb2\x8e\x83\x3e\ +\x40\x1f\xc5\xfb\x40\x7c\xbe\xf1\x05\x82\x07\xa7\x4c\x99\x82\x97\ +\x33\x7c\x29\xe0\xa0\x72\x30\x9e\xda\x3a\xa2\x65\xd5\xaa\x55\xaf\ +\xe7\xe4\xe4\xac\x1b\x32\x64\xc8\x14\x3b\xb1\xe0\x1d\x4b\x9d\x58\ +\x4e\x28\xb4\x0a\x7d\xc0\xd7\x05\xf0\xf9\x7a\x1a\x07\x28\xab\x2f\ +\xcc\x56\xad\x71\x8f\x2d\x42\x5b\x1d\x1d\x8f\x1f\x87\xd6\xa1\x3f\ +\xac\xbe\xee\x30\x84\x31\xd6\xd7\x32\x6d\xfa\x55\x7b\xf4\x38\x10\ +\x3c\x99\x7d\xf0\xc1\x07\x37\x2e\x58\xb0\x00\x9f\x66\x3c\x24\x5f\ +\x65\x4a\x3c\x73\x07\x19\x7b\x42\x18\xfb\xab\x5f\xfd\x6a\x39\x9e\ +\xe8\x78\x45\x71\x32\x39\x41\x3c\x71\xc0\x5d\x0b\xd0\xaf\x93\x43\ +\x9b\x71\x42\x1b\x71\xf4\x01\x0d\xd4\xef\xeb\x7b\x1f\xa1\x1f\xe3\ +\xc4\xcd\x36\xd0\x3e\x09\xfb\xa0\x4d\x0d\x1f\xfd\x61\xba\xa3\x98\ +\xb6\x0f\x13\xc0\x79\x54\x1f\x8f\x0f\x30\x56\x3c\x99\x2d\x5a\xb4\ +\xe8\xca\xb7\xdf\x7e\xfb\xf9\xb8\xfb\x90\xb0\xff\xec\x1d\x5a\x86\ +\x3c\xff\xfc\xf3\xcf\x6c\xd9\xb2\xe5\x14\xfd\xbb\x9c\x26\x14\x05\ +\x60\x22\x80\x4e\xce\x97\xb1\xc3\xe8\x2c\xce\x45\xea\xe8\xf1\x9f\ +\x65\x6a\xc6\x50\x4e\xa4\x71\xce\x6c\x43\x3f\xf0\x65\xce\x0d\x35\ +\x63\xd0\xea\x83\xf0\xb8\x1c\xc3\xb1\xc7\x1e\xbb\xde\x6e\xaa\xf1\ +\xf1\x8f\x77\x83\xc0\x21\xa4\xe3\x59\x3c\x34\x14\xbc\xf8\xe2\x8b\ +\x4f\x6d\xda\xb4\x69\x7c\xd8\xa4\x00\x4e\x04\x85\xc0\xe6\x84\x79\ +\x3f\x35\xe2\x61\x09\xa8\xf8\x3e\x58\x26\xbe\x8c\xb1\xf9\x44\x22\ +\x61\x7d\xf8\x3a\xb0\x29\x04\x7d\xb2\x0d\x6c\xb6\x61\x1d\xda\x5a\ +\xc7\xc7\x39\x67\x6c\x8b\xf3\x86\x0d\x19\x34\x68\xd0\x1f\x47\x8f\ +\x1e\x8d\x24\xaa\x0a\x2a\x1d\x62\x0e\xe5\xcd\x76\x22\x6a\xec\x81\ +\xae\xdc\x9e\xe8\x1e\xd5\x05\xa7\xcd\x09\xd2\x89\x83\x30\x46\x38\ +\x89\x3e\x86\xbe\x80\xb6\x4b\x84\x8f\x1f\x68\x1f\xa8\xa7\x71\xda\ +\xd0\xec\x03\x84\x95\x09\x63\xd0\x3c\x67\x9e\x1b\xa1\x1f\x40\xd3\ +\xd6\x3e\x35\x89\x0a\x0a\x0a\x96\x59\x12\x5d\x60\xee\xc3\x92\x44\ +\x20\x19\x89\x04\x9a\x66\xce\x9c\x79\xdd\xee\xdd\xbb\x6f\x8d\xc5\ +\x62\xcd\x9c\x04\x68\xb5\x81\x4e\x20\x75\x98\x8f\x5a\xe3\x5f\x94\ +\xce\xda\x73\xc1\x14\xfa\xd0\x96\x31\xad\xa3\x7e\x0f\x62\x48\x16\ +\x6d\xef\xc7\xc0\x32\xfb\x40\x19\x73\x84\xa7\x60\x3e\x09\x9b\x6e\ +\x31\xff\x7d\xf6\x64\x76\x85\x55\x39\xe8\x4f\x66\x1d\x71\xa8\x9f\ +\xda\x3a\xe2\x73\xbb\x5f\x7a\xab\x57\xaf\x5e\x1f\x9d\x7c\xf2\xc9\ +\x93\x6c\x22\xf1\x8d\xdd\x20\x80\x49\xf1\x13\x47\xe0\xf7\x13\xce\ +\x3a\xd0\xf4\x31\x21\x15\xd6\x55\x3f\x6d\xd5\x10\xad\x0b\x5b\x9f\ +\xda\x80\x3f\xae\xd6\xd7\x3a\x0a\xca\xea\xf3\x71\x4d\x26\x00\x4d\ +\x1f\xcb\x8c\xe9\x71\x90\x48\x99\x99\x99\x4d\x7f\xfa\xd3\x9f\x6e\ +\xb8\xf9\xe6\x9b\xef\x31\x57\xfb\x2d\xed\x30\xd0\x3a\x92\x24\x63\ +\xbb\xd2\x39\xcb\x96\x2d\x5b\x5e\x5f\x5f\x5f\x8c\x89\x03\xaa\x75\ +\xd2\x00\x6d\x6a\x26\x1e\x34\xa1\x8d\x3a\x88\x69\x7b\x45\xe3\x5e\ +\x03\xd8\x10\xdc\x23\xb1\x4f\x1f\xf7\x6d\x34\x06\xfc\x39\xc1\xcf\ +\xf3\x82\xcd\x32\xf1\x31\xfa\x08\xfb\xe7\xc5\xd2\xbb\x77\xef\x6d\ +\xdf\xfe\xf6\xb7\x67\x6d\xdd\xba\xf5\x95\x20\x90\x04\x5a\x47\x14\ +\x0d\x06\xfd\xee\x77\xbf\x7b\xa6\xb2\xb2\x72\x98\x4e\x34\x9f\xee\ +\xe0\x43\x99\x02\xba\x6a\x13\x2e\x0a\xe3\xec\xb3\x33\x30\x96\xce\ +\x12\x49\xd1\x32\xe2\x61\xc2\xc4\xf0\x7e\x8e\x0b\xa2\x71\x80\x18\ +\xda\x61\x07\x62\x79\xc0\x80\x01\x1f\x96\x95\x95\xcd\xb0\xe2\xfb\ +\x81\x33\x49\xec\xbb\x84\x93\xcf\xba\x0b\x2e\xb8\xe0\xfc\xfe\xfd\ +\xfb\xbf\x80\x05\xe3\x64\xf1\xaa\xd3\x45\xd4\xc9\xd5\x2b\x95\x3e\ +\xa0\x36\x40\x19\xfd\x40\x3c\xbe\x6e\x67\xa0\x8f\xae\xb6\x09\x1b\ +\x07\x48\xd4\x07\x7c\xf4\xab\xcd\xfa\x98\x17\xd8\x98\x8f\xd2\xd2\ +\xd2\x35\x96\x44\xf8\x9b\x59\x52\x93\x08\x44\x29\x91\xc0\x36\x7b\ +\xa2\x9b\xde\xb7\x6f\xdf\xc5\x9c\x2c\x6a\x4c\x22\x6c\xc2\x89\x25\ +\x6a\x03\xad\x9b\x88\xae\xd4\x09\x43\xc7\xd2\x59\x1f\x1a\xe7\x18\ +\xa1\x75\x47\x22\xb0\xb5\x0e\x6d\xf6\xa1\xf3\x91\x9f\x9f\xff\x44\ +\xfc\xc9\x0c\x3f\x47\x93\x74\xa2\x96\x48\x60\xcf\xb4\x69\xd3\xe6\ +\xd5\xd5\xd5\xdd\x64\x57\x5f\x33\x77\x26\x6a\x9d\x68\x92\xc8\xf6\ +\x74\x14\x53\xb4\x9e\x4f\x14\xc6\xa0\xbd\x1d\x56\xa6\x84\xed\x9c\ +\x3e\x8e\x63\x69\x99\x36\x40\x0c\x62\xf3\xd0\x62\x4f\xbb\x3f\xb0\ +\x0b\xee\x4a\x73\x1f\xd0\x6f\x18\x1d\x4a\x3a\xbe\x9c\x92\xcc\xa8\ +\x51\xa3\xa6\xcd\x9f\x3f\xff\x37\x7b\xf7\xee\xcd\xe1\xaf\xa2\x70\ +\x82\x39\xb1\xb0\x79\xa5\x52\x80\x2f\x7b\xd4\xcf\xfe\xc2\xe0\x62\ +\xea\x3d\x52\x67\xf8\xbe\x35\x29\xba\x22\x80\x89\x87\xbe\x78\x7e\ +\x78\x32\x7b\xe9\xa5\x97\xae\x7f\xf4\xd1\x47\x1f\x0b\x82\x11\x22\ +\x7c\xf6\xa2\xc5\xc8\xa7\x9f\x7e\x7a\xf9\xf6\xed\xdb\x83\x1f\xb3\ +\xd0\x4f\x5e\x86\x25\x10\x48\xe4\x27\xf4\xa1\xbf\xb0\xb8\xfa\xa1\ +\x71\xcc\x44\x89\xc4\xba\xbe\x0d\xca\x44\x13\x29\x51\x52\x01\x68\ +\x26\x10\x6c\xf6\x87\xe3\xe2\x87\xaf\x2e\xbe\xf8\xe2\x59\x56\x7c\ +\x35\x70\x46\x8c\x28\xbe\xb4\x79\xfe\x78\xc9\x25\x97\x9c\x8f\x3f\ +\x3e\xa2\xe0\x17\x0d\xe8\x02\xa8\x5f\xe1\x62\x01\xda\xbe\x6e\x22\ +\x3f\xf0\xc7\xf3\x75\x7d\x1b\x96\xb5\x6e\x22\x1b\xf8\x32\x81\x2f\ +\x16\x8b\x35\x59\x12\xe1\x33\x44\x91\x4c\x22\x70\x24\x24\x12\x68\ +\xc2\xff\xe9\x87\xd1\xd9\xe4\xfb\x38\xc1\xc2\xfa\xba\x89\xd0\x7a\ +\x4c\x50\x82\x18\xfa\xf2\x89\x43\xd8\x96\x3a\x2c\xc1\xba\xd2\x07\ +\x05\x75\x6c\x0c\xf8\x9d\xaa\xf6\x03\x89\x18\x47\x44\x22\x0d\x1f\ +\x3e\x1c\x3f\xab\x13\x7c\x3b\xc5\xbf\xbc\x60\xb2\x13\xa1\x0b\xc5\ +\x45\xa1\xed\x25\x11\xbe\x8d\xb7\xc3\x60\x8c\x6d\xc3\xc6\x01\xcd\ +\x97\x39\x25\xac\x5f\xf3\x65\xcc\x99\x33\x67\x74\xbc\x18\x49\x8e\ +\x88\x44\xba\xf9\xe6\x9b\xf1\x5e\x49\xdb\xe4\xd3\xd6\x05\xea\x8c\ +\xb0\xba\x5c\x34\xe8\x8e\x84\x0b\x0e\xf1\xf5\x7d\x8c\x68\x1d\x45\ +\xc7\x01\x5b\xcb\xa8\xeb\xc7\xc9\xf6\xf6\xf2\x1e\xcc\x41\x54\x39\ +\x12\x12\x29\x63\xc0\x80\x01\x6d\x57\xa3\x9f\xe8\xce\xca\xba\xa0\ +\x94\x44\x7e\x4a\x57\xe2\xc0\xdb\x5a\xc6\x38\x28\x61\x84\xc5\xb4\ +\x4c\x1b\x1a\xfd\xda\x3d\xe2\x39\x56\x8c\xec\x7f\x0a\x38\x12\x12\ +\x29\x6f\xd3\xa6\x4d\xc3\xe2\x76\xdb\x62\xe9\x44\x53\x0e\x04\xf6\ +\x13\xa6\x13\x89\xc6\x15\x94\xc3\x8e\xef\xeb\x75\x15\xed\x8b\x7d\ +\xd7\xd6\xd6\x0e\xb4\x22\x24\x92\x44\x3e\x91\x86\x0d\x1b\x36\xd2\ +\x1e\xbf\x33\xfd\x62\x1d\x68\xe2\x00\xf4\x41\x51\xd4\x9f\x48\x58\ +\x4f\x5f\x5a\xe9\x07\xf4\x7b\xb4\x0e\xc7\xac\x3e\x05\x71\x7f\x8e\ +\x90\xf8\xb1\x32\xe6\xce\x9d\x8b\x5d\x29\x92\x44\x3e\x91\x6e\xbd\ +\xf5\xd6\xb6\x7b\x03\x2c\x56\x67\x8b\x41\x3f\xb4\xde\xbf\xf8\x85\ +\xa6\x4f\xfd\xac\xaf\x1a\xef\x21\xb1\x0e\x7c\x38\x3e\xe3\x5a\x97\ +\x71\x2f\xea\xa7\xcd\xe4\x60\x59\x6d\xd5\x7a\xae\xb0\xa7\x4f\x9f\ +\x7e\x7e\xe0\x88\x20\x91\x4f\xa4\x7e\xfd\xfa\x8d\x85\xc6\x64\xf2\ +\x89\x0d\x93\x4a\xf1\x93\x4e\xad\x70\x21\x68\x33\x31\xe8\x63\x32\ +\x68\x7f\xaa\x29\x2c\xab\xf6\xc7\x63\x19\x9a\x42\xd0\x46\xe3\x2c\ +\xfb\x7a\x2c\xeb\x38\x61\xe3\x0b\x14\x81\x23\x82\x44\x3d\x91\xb2\ +\xb7\x6e\xdd\x3a\x82\x13\x8e\xc9\xe4\xe4\xea\x42\xc2\x66\x99\x36\ +\x84\x31\xef\x67\x99\x7d\x01\xed\x9b\x36\x04\x7d\x00\x6d\x4f\x01\ +\x6c\xe3\xfd\x5a\x66\x1f\x84\x65\x68\x0a\xcb\x6c\x0f\x60\xa3\x7f\ +\xf6\x53\x53\x53\x53\x6a\xee\xa2\xd6\x68\xb4\x88\x74\x22\x1d\x75\ +\xd4\x51\x23\xf1\xff\x4e\x60\x73\x42\x75\xd2\x89\xda\x0a\x17\x85\ +\x71\x96\x55\x53\xb4\xac\x36\x3f\x0f\x05\xe8\xd3\x3a\xaa\x7d\x22\ +\x10\xf8\x10\xa3\xf8\x3a\xda\x0f\xff\xbc\x43\x9f\xda\x36\x07\xb1\ +\xf2\xf2\xf2\x48\xbe\x9f\x14\xe9\x44\x7a\xea\xa9\xa7\xda\x26\x4d\ +\xdf\x88\xc4\xe4\x02\xbf\x20\x61\xa0\x2e\x77\x0d\xa0\x8b\x46\xad\ +\xfd\xb0\x1c\x26\x44\x17\x57\xfd\xde\xe6\x38\x09\xe3\xf0\x6b\x8c\ +\xb6\xf6\xab\x75\xb4\xee\xb5\xd7\x5e\x1b\xc9\xf7\x93\x22\x9d\x48\ +\x05\x05\x05\xed\xde\x88\xd4\xc9\x05\x7a\xcf\x44\xfc\xe4\x73\x61\ +\xa0\x21\xf0\xab\x8f\x30\x4e\x1f\xdb\xe3\x18\x3e\xe6\xb5\x07\x7e\ +\x1e\x07\x9a\x7d\x81\x44\x6d\xb4\x0e\x61\x1f\x80\xba\xb0\xb0\x30\ +\x92\xf7\x49\x51\x4e\xa4\xac\xda\xda\xda\x33\x60\x70\x92\x75\x11\ +\xba\x32\xf1\x14\x85\xbb\x93\xaf\x47\x1b\xa0\x0e\x04\x65\xff\xd4\ +\x46\x3f\xeb\x02\x96\xd5\x9f\x48\x73\x8c\x89\xe2\x40\xcf\x57\x6d\ +\x48\x65\x65\xe5\x10\x2b\x16\x04\xce\x08\x11\xe5\x44\x1a\xb6\x7b\ +\xf7\xee\x5c\x4e\x20\x26\x94\x93\xaa\xd0\xa7\x31\xad\x4f\x3f\x7c\ +\x00\x65\xf6\x49\x1f\x6d\xc6\xe8\x03\xec\x83\xc9\xa4\xb0\x9d\xc2\ +\xe3\x11\x96\xd9\x0f\x8f\xe3\xfd\x2c\xeb\x2e\xeb\x5f\xce\xd1\xb6\ +\xb9\xb9\x39\x6b\xd8\xb0\x61\x67\xc7\xdd\x91\x21\xb2\x89\xb4\x74\ +\xe9\xd2\xb1\x36\x71\x6d\xe3\xe3\xa2\xe9\xa4\x2b\x88\x01\xc6\x58\ +\x9f\xa2\x3e\x45\x63\x3e\x59\x50\xc6\x8e\x84\x18\x17\x12\x40\xd3\ +\x56\xbf\xa2\xe3\xa0\xd6\x7a\xb0\x51\x87\xed\x59\xd6\x7a\xaa\x55\ +\x16\x2e\x5c\x18\xb9\xfb\xa4\xc8\x26\x52\x51\x51\xd1\x18\x4e\x24\ +\xc0\x24\xe3\x0a\xe5\x02\x71\x11\xb4\x4c\x34\x46\xa1\x5f\xb5\xf6\ +\x0f\xb8\x50\x04\xf5\xf4\x1e\x49\x49\xd4\x87\xc2\x3a\x00\x36\x05\ +\xf8\x73\x61\x39\xcc\x26\x28\x83\xc1\x83\x07\x47\xee\xc9\x2d\xaa\ +\x89\x94\x59\x5f\x5f\x1f\x6c\xdf\x5c\x28\x5d\x4c\x4e\x28\x7d\xf4\ +\x2b\xf4\xeb\x3d\x4d\xd8\xfd\x0d\x7d\xdc\x8d\x34\x0e\xcd\x1d\x09\ +\xa0\x8e\xf6\xe1\x25\xac\x2f\x45\xcb\xb4\xa1\xc3\x84\x84\xc5\x36\ +\x6e\xdc\x38\xd4\x42\x79\xad\x35\xa2\x41\x54\x13\xe9\x94\xc6\xc6\ +\xc6\x76\x37\x94\xbc\x42\x01\x26\x13\xb0\x0c\xad\x13\xcd\x85\x24\ +\xbe\xbe\xd6\x65\x5b\x6a\xc6\x99\x14\xda\x86\x36\x81\x4f\xdb\x68\ +\x1f\x6c\xdf\x19\x68\xe3\xfb\x65\x59\xb5\xd6\xb1\x7b\xc7\x9c\xe2\ +\xe2\xe2\xe0\x41\x24\x2a\x44\x32\x91\x16\x2d\x5a\x74\x8e\xed\x04\ +\x19\x5c\x28\x0a\x50\x4d\x9b\x8b\x06\x61\x12\x61\x27\xe1\x6e\x42\ +\x3f\xcb\x5a\x1f\x02\xc2\xfa\xa0\x0f\x68\x5d\xd6\xa1\x8f\x82\xfe\ +\xe9\xa7\xe6\x38\xbc\x1f\xa8\x4d\x90\x30\xf0\xd1\xcf\x32\x50\xff\ +\x5d\x77\xdd\x85\x1f\x56\x8f\x0c\x91\x4c\xa4\xd3\x4f\x3f\xfd\x3c\ +\x4c\x18\x27\x3a\x91\xe8\x42\xd0\x47\x9b\xda\xf7\xc1\xb2\xd7\xac\ +\xaf\x76\xd8\x22\x42\xc2\xfa\x84\xb0\x1e\xcb\x4c\x20\x08\xeb\x02\ +\xf5\xb1\xcc\x38\x7d\xc0\x97\x09\x7c\x83\x06\x0d\x8a\xd4\x7d\x52\ +\x14\x13\x29\xb6\x6b\xd7\xae\xb3\x13\x4d\x20\x35\x6d\x5d\x10\x68\ +\x94\xb9\x0b\xb0\x9e\xaf\x43\x4d\xdb\xc7\xf5\x65\x04\x37\xbb\x14\ +\xf5\xa3\x9e\x6f\xe7\x7d\xb0\x59\xf6\xb1\x44\x1a\xc0\xd6\x63\x01\ +\xf8\x20\xec\xa3\xa6\xa6\x66\x84\xb9\x73\x5b\xa3\xc9\x27\x8a\x89\ +\x34\xb8\xae\xae\xae\x58\x27\x4e\x17\x00\x30\xa6\xc2\x7a\x10\xe2\ +\xeb\x68\x72\x85\x09\x8f\x03\x01\x9a\x40\x10\xd8\x40\x6d\xa0\xed\ +\x28\x3c\x16\xe1\xb8\xe0\xf3\x63\x04\x9a\x38\xb4\x55\x53\x00\xda\ +\xe0\x3d\x36\x33\x71\xd3\x1d\x09\x22\x97\x48\x0b\x17\x2e\x3c\xc7\ +\x26\x7a\xbf\x8f\x94\x62\x12\x75\x11\x58\x0e\x13\x5d\x58\xb5\x81\ +\xfa\x58\xa6\x8d\xe4\xc0\x37\x7a\x29\x38\x06\x7c\xf8\x62\x62\x56\ +\x56\x56\x03\xcb\x4c\x22\x68\xb6\x87\x70\x6c\xea\x83\xe8\x4b\x1c\ +\x04\xfd\xd0\x0e\xab\x0f\xa1\x1f\xe8\xb9\x4b\x9d\x8c\x9f\xff\xfc\ +\xe7\x91\x79\x79\x8b\x5c\x22\x8d\x1e\x3d\x7a\x0c\x27\x99\x93\xe6\ +\x26\xb0\x4d\x08\x6d\xef\xd3\x76\xdc\x21\xc2\xfa\xe2\x42\x31\x41\ +\xe8\x43\x32\xe5\xe5\xe5\xbd\x7a\xcd\x35\xd7\x9c\x36\x73\xe6\xcc\ +\xaf\xe5\xe4\xe4\x3c\x6d\xbe\xe0\xdf\xb1\x7a\x61\x3b\x08\x8e\x01\ +\xa0\x69\x33\x46\x21\x6a\x6b\x7f\xf0\xb3\x5f\x40\x9b\x1a\xfd\x0e\ +\x1e\x3c\x38\x32\x6f\x4c\x46\x2d\x91\x62\x46\x70\x95\x71\x82\x55\ +\xab\xcd\x45\x52\x49\xe4\x87\x30\xa6\xfd\x00\x2c\x0c\x12\x08\x49\ +\x03\x68\x1f\x73\xcc\x31\x55\xcf\x3d\xf7\xdc\x95\x15\x15\x15\xf8\ +\xa1\x86\x0f\x4c\x36\x5c\x74\xd1\x45\x33\x7e\xf8\xc3\x1f\x4e\xef\ +\xdf\xbf\xff\xc7\xba\x63\x41\xb8\xc0\xec\x5f\x8f\xa9\xc7\xd5\x58\ +\x47\x82\xfe\xa0\xb5\x4f\x4f\x43\x43\x03\xde\x02\x08\xff\x8f\x84\ +\x87\x99\xa8\x25\xd2\x40\xbb\x89\x0c\xbe\x9a\x8d\x89\xc4\x84\x2b\ +\x5c\x2c\x40\x5b\x17\x50\xb5\xc7\xfb\x59\x46\x7b\xee\x44\xf1\x24\ +\xda\x63\x2f\x63\xff\x69\x09\x34\x7c\xe5\xca\x95\x4b\xcc\xdd\x6e\ +\x10\xef\xbc\xf3\xce\x8a\xb2\xb2\xb2\xd3\x6d\x6c\xf7\xf4\xec\xd9\ +\xb3\xed\x47\x1c\x74\x6c\x89\xc6\x00\xc2\x62\xf0\x51\x58\xc6\xb9\ +\xa3\x4f\x15\xc0\x39\x41\x9d\x1d\x3b\x76\xe0\xbd\x36\xfc\x11\x37\ +\xe9\xec\x3b\xfb\x08\x30\x6b\xd6\xac\x6f\x4e\x9a\x34\x69\x29\xaf\ +\x58\x68\xda\x20\x91\x06\xb0\x31\xd9\x9c\x68\x10\x56\x4f\x17\x05\ +\xbb\x8a\xee\x26\x25\x25\x25\xff\x6b\xbb\xce\xf7\x9a\x9b\x9b\xd7\ +\x04\x8e\xce\x19\xb6\x76\xed\xda\x1f\xff\xfd\xef\x7f\x1f\x87\x63\ +\x50\x30\x06\x8e\x45\x8f\x4d\x78\xbc\x03\x81\xfd\x40\xa3\x7d\x3c\ +\xe9\x7b\x7c\xf8\xe1\x87\xf3\xef\xb8\xe3\x8e\x9f\x06\xc1\x24\x12\ +\xa9\x1d\xc9\xee\x43\xda\xde\x3f\xd2\x27\x2c\xe0\x27\x52\x35\xfd\ +\x14\x5f\xa6\x8f\xa0\x1d\x5f\x9a\x20\xf8\x5d\x81\x4d\x9b\x36\xdd\ +\x38\x65\xca\x14\xfc\x0b\xce\xae\x26\x11\x78\xef\xec\xb3\xcf\xbe\ +\xe0\xad\xb7\xde\xba\xba\x4f\x9f\x3e\xd5\xdc\xd9\xd0\xa7\x6a\xe0\ +\xc7\xa0\xe3\x82\xe6\x58\x28\x44\xcb\x61\x7d\xd8\x3d\x65\x24\xbe\ +\x10\x70\xe0\x97\xc6\xa1\x23\x63\xf5\xea\xd5\xff\xb7\x61\xc3\x86\ +\xa1\x98\x20\xee\x2c\xd4\xf0\x71\x22\xbd\x06\x5a\x8f\x1a\x0b\x0b\ +\x3f\x17\x03\x65\x2e\x76\xfc\x8a\x6e\xb1\x04\x78\xfa\xb2\xcb\x2e\ +\xbb\xc9\x5c\x1b\x82\xc0\x17\xa7\xd0\xee\xa9\xee\xdc\xb5\x6b\xd7\ +\xb7\xec\x98\x31\x1c\x1f\xc7\x86\xc0\x86\x60\x0c\x1c\x1f\x60\x82\ +\x10\x5f\x56\xd8\x07\x41\x5d\x5c\x0c\x76\x2f\xb7\x71\xea\xd4\xa9\ +\x27\x9a\xab\xa9\x35\x92\x1c\xa2\xb4\x23\x15\x55\x57\x57\x0f\x8e\ +\xdb\xc1\xa4\x69\x72\x70\x12\xa1\x75\x41\x34\x06\xd4\xcf\x45\x04\ +\x68\xc3\x85\x42\x12\x15\x14\x14\x7c\xfc\xd0\x43\x0f\x5d\x6c\x49\ +\x74\x99\xb9\xbe\x6c\x12\x01\xfc\xec\xcc\x75\x73\xe7\xce\xbd\xe0\ +\x84\x13\x4e\x78\x97\xc7\x53\xd1\x71\x02\x94\x21\x89\xe2\x04\x31\ +\xc2\xfa\xb4\xeb\xeb\xeb\xf1\x65\x00\x7c\x29\x20\xa9\x44\x26\x91\ +\xc6\x8e\x1d\x7b\xc6\xde\xbd\x7b\xb3\x38\xb9\x80\xb6\x4e\x1c\xd0\ +\xab\xdc\x0b\xeb\x42\x43\x70\xd5\x52\x50\x3e\xfa\xe8\xa3\x9b\x8c\ +\xbb\x66\xcc\x98\x71\x5a\xfc\xff\x73\xec\xbb\xa9\x3a\x08\xec\xdc\ +\xb9\xf3\xf5\x51\xc6\xf6\xed\xdb\x17\x66\x67\x67\xd7\x73\x17\xa4\ +\x00\x6a\xa2\x09\x4f\xad\x78\x1f\x2f\x30\x60\x76\x6c\xc1\x82\x05\ +\x49\x7f\x3f\x69\x5f\xaa\x27\x99\x25\x4b\x96\xfc\xc8\x12\xe9\x5f\ +\x38\xa9\xf8\x85\x36\xa2\x93\x0c\x41\x42\x70\x32\xa1\xb5\xac\xe8\ +\xc2\x41\x0a\x0b\x0b\x5f\xb7\x04\x9a\x6f\xae\xc3\xf5\xe3\x9d\xa5\ +\x2f\xbf\xfc\xf2\x8f\xed\x49\xf4\x22\x9e\x17\x05\xf8\x71\xa3\xec\ +\xb5\x9e\xaf\x6f\xcb\xf3\x3a\xf6\xd8\x63\x9f\x88\xff\x48\x7b\xd2\ +\x88\xca\x8e\x94\x61\x8b\x7c\x0e\x27\x89\x5a\x51\x1f\x27\x55\xc1\ +\xc4\x42\xfc\x0e\x04\x9d\x93\x93\x53\xfd\xda\x6b\xaf\xcd\xb1\x24\ +\xc2\x8f\x55\x1d\xce\x5f\x80\x5d\x3f\x71\xe2\xc4\x8b\x7f\xf1\x8b\ +\x5f\x4c\xb7\x97\xd2\x0d\x5c\x78\x08\xc6\x86\x73\x80\x06\x2c\x7b\ +\x3a\xf2\x03\xc4\xec\x1c\x47\x9a\x89\xdf\x50\x4a\x1a\x51\x49\xa4\ +\xfc\xcd\x9b\x37\x07\x7f\x37\xc2\x4e\x14\x76\xf5\x02\xef\xe3\xd5\ +\x4c\x1f\x17\x88\x62\x13\xdc\x9c\x99\x99\xb9\x78\xce\x9c\x39\x5f\ +\x5b\xb6\x6c\xd9\xaf\xd1\x24\x68\x70\x98\x79\xf5\xd5\x57\x9f\x9d\ +\x3c\x79\xf2\xd7\xec\x49\xf4\x3e\xfc\xb9\x85\xe3\x84\x06\x4c\x0a\ +\x6a\x9e\x9f\x9e\xab\x47\x9f\x6a\xe3\x5f\x9c\x2c\x69\x8d\x24\x87\ +\xd6\x91\x27\x99\x53\x4e\x39\x65\xd2\x4d\x37\xdd\xf4\x22\x26\x85\ +\x7f\x97\x02\x3a\x89\x9c\x34\x42\x9b\x49\xe3\xed\xe2\xe2\xe2\xb7\ +\xed\x3e\xfa\x7b\x76\x3f\xf4\x3f\x81\x23\x3a\x7c\xfd\xcd\x37\xdf\ +\xfc\xf1\xa7\x9f\x7e\x1a\x7c\xad\x88\xe7\x0a\x8d\xb1\x53\xe3\xfc\ +\x90\x68\x5a\x86\x0d\xad\x71\x80\x5d\xd7\x76\xdc\x2b\x7f\xf9\xcb\ +\x5f\xe2\x0d\xd4\xa4\x10\x89\x1d\xe9\xc6\x1b\x6f\x6c\xfb\xfb\x1a\ +\x27\x8b\x5a\x05\x50\x03\x4c\xa8\x97\x9e\x3d\x7b\xd6\xed\xd8\xb1\ +\xe3\x5f\xed\x09\xea\xdc\x08\x26\x11\x78\xd7\x1e\x2c\xca\xd6\xac\ +\x59\x73\x1d\x7e\x60\x54\x77\x51\x9c\x1b\x35\x50\xcd\xf9\x00\xb0\ +\xa9\xe9\x2b\x2f\x2f\x4f\xea\xdf\xdd\x22\xb1\x23\xbd\xf0\xc2\x0b\ +\x6f\x54\x56\x56\x8e\xe5\x6e\x84\xc9\x81\x60\x52\x01\xcb\x98\x74\ +\x68\x5c\x81\xa8\xc7\x45\x00\xe6\x6b\xb1\x85\x79\x36\xfe\x9e\xd0\ +\x41\xfd\xc7\xbf\x87\x90\x62\x3b\xf7\x3b\xed\x09\x6f\xb6\x9d\x4f\ +\x4c\x13\x04\xe7\xc5\x32\xce\x59\x6d\x08\xe0\x7c\xc4\x62\xb1\x1e\ +\x5f\xfd\xea\x57\xff\x5a\x56\x56\x76\xaa\xb9\x5b\x2b\x1e\x66\xa2\ +\xb0\x23\xe5\xf0\x87\x22\x38\x61\xb4\x29\x80\x09\x43\xf4\x4a\xb6\ +\xa7\x96\x75\x8f\x3c\xf2\xc8\x74\x4b\x22\xfc\xe3\xdf\x23\x25\x89\ +\x40\x95\x3d\x6d\xcd\xbd\xf6\xda\x6b\x27\x96\x96\x96\xbe\x87\x0b\ +\x84\xe7\x04\x68\x73\x0e\xfc\xbc\x50\x30\x67\x1b\x37\x6e\xc4\xdf\ +\xdc\x92\xf6\x03\x13\x49\x4f\x24\x7b\xa2\x1a\x81\x5f\xac\xd5\x2b\ +\x4e\x35\xe0\x84\x22\x79\x70\xf5\xc1\xc6\xa4\xe3\x3d\x21\xdb\xc5\ +\xee\xad\xa8\xa8\x38\xed\x0f\x7f\xf8\xc3\x0a\xab\x9a\x94\xab\xf1\ +\x4b\xd2\xb2\x73\xe7\xce\x57\xcf\x3d\xf7\xdc\x51\xbb\x77\xef\xfe\ +\x77\x7c\xee\x09\xe7\x88\x73\x85\x00\x9c\xaf\x6a\xa0\x89\x04\x6c\ +\xfe\x62\x13\x26\x4c\x48\xda\x17\x27\xdb\x5f\xe6\x49\xe0\x81\x07\ +\x1e\xb8\xa5\x77\xef\xde\x77\xf2\x29\x04\x1a\xc0\x46\xb2\x00\xbe\ +\x94\x61\x82\x01\x26\xb4\x7f\xff\xfe\xaf\xdb\x7d\xd0\xf7\xac\xf8\ +\x5e\xe0\xec\x3e\x0c\x7e\xe3\x8d\x37\x7e\x64\x3b\xcc\x54\xcc\x01\ +\xce\x9b\xc2\xb9\x01\xb0\x31\x0f\x10\x26\x1c\xfe\x1d\xbb\x3d\xa1\ +\x62\x4e\x0e\x3b\x49\xdf\x91\x6c\x4b\x0f\x6e\xb4\x79\x75\x41\x00\ +\x5f\xba\x90\x44\xba\x13\xf5\xe9\xd3\x67\xcb\xda\xb5\x6b\xaf\xb6\ +\x24\xc2\xe7\x84\xba\x5b\x12\x81\x8f\xcf\x3b\xef\xbc\x8b\x9f\x78\ +\xe2\x89\x0a\xbc\xf7\xc4\xf3\xe7\x5c\x40\x03\x6a\x26\x1b\xb0\xfb\ +\xa4\xa4\xbd\xc3\x9d\xec\x44\xca\xb4\x1b\xcd\xe0\x77\x11\x35\x99\ +\x38\x71\x14\x60\x93\xd8\x92\x9d\x9d\xfd\xe8\xe5\x97\x5f\x7e\x6a\ +\xfc\x7f\x71\x1c\x92\xff\x5f\x1f\x11\x5a\x56\xad\x5a\xf5\x5f\x93\ +\x26\x4d\x1a\x6e\xf3\xf1\x80\x5d\x44\x7b\x74\x4e\x00\x2f\x38\x6a\ +\xcc\xdf\x27\x9f\x7c\x82\x1f\x6d\xcd\x0f\x1c\x87\x99\x64\x27\xd2\ +\xd7\xed\x11\x3d\x97\x49\x04\x38\x59\xb8\xfa\x78\x05\x1e\x77\xdc\ +\x71\xef\xdd\x70\xc3\x0d\xe7\xcf\x9e\x3d\x7b\xae\x55\xd9\x12\x54\ +\x4c\x0d\xea\xec\x9c\x6f\xb2\x8b\xe7\xac\xe3\x8f\x3f\x7e\x8d\x26\ +\x13\xe6\xc6\xd3\xdc\xdc\x1c\x3b\xed\xb4\xd3\xf0\x2e\xf7\x61\x27\ +\xa9\x89\x74\xcf\x3d\xf7\x8c\x46\x02\x31\x89\x80\x6e\xdf\x3d\x7b\ +\xf6\xac\xc7\x7b\x42\x53\xa7\x4e\x3d\xd3\x12\xee\xcd\xc0\x99\x9a\ +\xbc\x3b\x6e\xdc\xb8\xf3\xde\x79\xe7\x9d\x79\xf6\xd2\x5e\xe3\x93\ +\x48\xe7\x70\xde\xbc\x79\x49\x79\x3f\xa9\x75\xc5\x92\xc4\x6f\x7f\ +\xfb\xdb\x67\xaa\xab\xab\xa7\x71\x47\xe2\x15\x67\x5b\x79\x8b\x3d\ +\xd2\x3f\x3f\x73\xe6\xcc\x1b\xad\xda\xba\xd6\xda\x69\xe2\x14\x2f\ +\x5f\xbe\xfc\x6e\xbb\xc0\x2e\xb7\x1b\xee\xe0\x73\x4f\x4c\x22\xcc\ +\xdd\xa0\x41\x83\xd6\xd8\xd3\xdb\xb9\x81\xe3\x30\x92\xcc\x1d\x29\ +\xd6\xd0\xd0\x10\xec\x48\x04\x13\x91\x9f\x9f\xbf\xee\xc9\x27\x9f\ +\xac\xb0\x24\x9a\x6e\xae\x74\x12\xed\x4f\x55\x45\x45\xc5\x9c\xef\ +\x7f\xff\xfb\xe5\x25\x25\x25\xef\xfb\x97\x39\x7b\xda\x4b\xca\x17\ +\x27\x93\x96\x48\xf6\xc8\x7f\xd2\xce\x9d\x3b\xf3\x31\x11\xd8\x89\ +\xf0\xc7\x4c\xb3\xef\x9b\x35\x6b\xd6\xe9\xaf\xbd\xf6\xda\xd3\x56\ +\xe5\x48\x7c\x4f\xe8\xb0\xb1\x7e\xfd\xfa\x57\x26\x4f\x9e\x8c\xf7\ +\x9e\x6e\xb3\xb9\x6b\xe0\x6e\x6e\xbb\x54\x96\x5d\x84\x87\xfd\x07\ +\x26\x92\xf6\xd2\x66\x8f\xb7\xff\x54\x57\x57\xf7\x30\x76\xa4\x13\ +\x4f\x3c\xf1\xcd\xf2\xf2\xf2\x1b\xf7\xee\xdd\xfb\x76\x3c\x9c\xe6\ +\xc0\x38\xe9\x95\x57\x5e\x59\xb4\x61\xc3\x86\xa9\x48\xa6\xd2\xd2\ +\xd2\xdb\xca\xca\xca\x7e\x10\x8f\x1d\x16\x92\xb6\x23\xf5\xeb\xd7\ +\xef\x3c\xdb\x95\x6a\xec\xfe\xe8\xba\x0b\x2f\xbc\xb0\x2c\x9d\x44\ +\x5f\x8a\x0f\x6c\x0e\x2f\xb6\x07\x92\x8a\xbe\x7d\xfb\x6e\xb4\x5d\ +\x29\xb2\xff\x21\xe0\x60\x93\x61\x57\xd0\xdd\xa6\x0b\x5b\x8b\x69\ +\x0e\x22\x79\x2f\xbf\xfc\xf2\x9d\xa6\xb3\x5b\x8b\x69\xd2\x1c\x31\ +\xf4\xe8\xf1\xff\x4c\xec\xc7\xd3\x0e\x68\x2d\x77\x00\x00\x00\x00\ +\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x24\xa1\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x92\x00\x00\x00\x8a\x08\x06\x00\x00\x00\x41\xfe\x13\x78\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x04\x67\x41\x4d\x41\x00\x00\xb1\x8f\x0b\xfc\x61\x05\x00\x00\x00\ +\x09\x70\x48\x59\x73\x00\x00\x0e\xc4\x00\x00\x0e\xc4\x01\x95\x2b\ +\x0e\x1b\x00\x00\x00\x21\x74\x45\x58\x74\x43\x72\x65\x61\x74\x69\ +\x6f\x6e\x20\x54\x69\x6d\x65\x00\x32\x30\x32\x32\x3a\x30\x34\x3a\ +\x31\x30\x20\x31\x37\x3a\x34\x32\x3a\x33\x33\x07\x28\xe8\x92\x00\ +\x00\x24\x09\x49\x44\x41\x54\x78\x5e\xed\x7d\x7d\x90\x5d\xc5\x75\ +\x67\x77\xbf\xd1\x68\x24\x24\x21\x84\x10\x32\xeb\xc8\xae\x2c\xcb\ +\x52\x98\xd8\x01\x63\xbe\x85\x3c\x48\x32\x18\x13\x61\x01\x22\x8e\ +\x93\xe0\x10\x56\x51\x51\x24\x9b\xca\x52\x2c\xe5\x4a\xb9\x28\x6f\ +\x6d\x3e\x8b\xbf\xf6\x8f\xd4\x96\x93\xda\x72\x6c\xc7\x9b\x0a\x36\ +\xc6\x2c\xa1\x1c\xec\xb2\x64\x3e\x04\xa6\x70\x42\x5c\x98\x4d\x79\ +\x29\x02\x0a\x16\x42\x12\xb2\x34\x8c\x46\xa3\x99\xf7\xba\xf7\xfc\ +\x4e\x9f\xd3\xb7\xef\x7d\x6f\x66\xf4\x31\x33\xef\x32\xef\xfd\xde\ +\xf4\x3d\x9f\x7d\xde\xbd\xdd\xe7\x76\xf7\xbd\xef\xbe\x79\xa6\x8f\ +\x3e\xfa\x98\x27\x84\xef\x5c\x7c\xa1\x7f\xf2\xf2\x2b\x45\xec\xa3\ +\x03\x9c\xd0\x3e\xa6\x81\x1f\xd9\x73\xb7\x71\xfe\x1e\x11\xfb\xe8\ +\x80\x7e\x22\xcd\x80\xf0\xe2\xcd\x4b\x4d\x38\x76\x67\x08\xf6\xd6\ +\xb0\xf3\xe6\xd5\xa2\xee\xa3\x82\x7e\x22\xcd\x80\xf0\xc6\x8b\xb7\ +\x5b\x33\xb9\xc6\x59\xbb\x2c\x34\x0f\xdd\x29\xea\x3e\x2a\xe8\x27\ +\xd2\x34\x08\xe1\x41\x17\x5a\x23\x3b\xac\xc8\x34\xbd\xdd\x1d\x76\ +\x6e\x18\x10\xa9\x8f\x0c\xfd\x44\x9a\x06\xe1\xdb\x7f\x7b\xa9\xf1\ +\xc7\x2e\x17\x11\x99\x75\x61\xf0\xfe\x7a\x91\xfa\xc8\xd0\x4f\xa4\ +\x69\x10\x9a\x6f\xed\xb0\x2e\xe4\x23\x90\x0b\xa1\xb9\x43\xf8\x3e\ +\x32\xf4\x13\x69\x0a\x84\x9d\x1f\x5d\x6d\x9b\x63\xb7\xdb\x90\x26\ +\x36\x63\x2d\xf8\xd6\x8d\x61\xe7\x0d\x1f\x8c\x9a\x3e\x14\xfd\x44\ +\x9a\x02\xe1\x9d\x3d\x9f\xb1\xb6\xb5\xd2\x14\x79\xc4\xa0\x64\x5a\ +\xea\xfd\xc8\x5d\x22\xf6\x21\xe8\x27\x52\x07\x84\x9d\x0f\x0e\x84\ +\xd6\xbb\xdb\x39\x89\x42\xd4\x29\x2c\x94\xc1\xff\x56\x78\xf1\x77\ +\x96\x8a\xaa\x0f\x42\x3f\x91\x3a\xe1\xf0\x97\xaf\x35\xe6\xf8\xc5\ +\xcc\x67\x23\x92\xe6\x94\x35\x61\x5d\x78\xf7\x95\x9b\x44\xec\x83\ +\xd0\x4f\xa4\x0e\xf0\x93\xef\xdc\xe3\x6c\x70\x6d\xa3\x51\x20\x05\ +\xfd\x59\x34\x9b\x9f\xd8\x81\xdb\x03\x62\xea\x79\xf4\x1b\xa2\x02\ +\xff\xc4\x2f\xbd\x9f\x2e\xf9\x6f\xe6\xa1\x08\xb3\x58\x9e\x4c\x58\ +\x6c\xf3\x08\xc5\x09\x75\x5d\xd8\xb5\xfb\x22\xd6\xf7\xd1\x4f\xa4\ +\x36\x1c\xfd\xd9\x9d\xb4\xc8\x4e\xeb\x1f\xbe\x50\x4b\x90\xac\x22\ +\xa5\xb5\x6e\x30\xf8\xd1\xbb\xa3\xa2\x8f\x7e\x22\x65\x08\x3b\x37\ +\x0c\x71\x72\x94\xb3\x27\x21\x8d\x4e\x9a\x4f\xa1\xf5\xd9\xb0\xf3\ +\x96\x95\x51\xea\x6d\xf4\x13\x29\x43\xf8\xf9\xff\xdb\x64\xcd\xc4\ +\x2f\x8a\xd8\x86\x78\x1f\x09\x8c\x12\xbb\x26\xf8\x83\xb7\x46\xa9\ +\xb7\xd1\x4f\xa4\x0c\xa1\x75\xe4\x5e\xcd\x92\x7c\x69\x54\xa0\xd0\ +\x32\x47\x89\x15\x4c\xb3\xbf\xe8\x26\xf4\x13\x49\xe0\xbf\x73\xd1\ +\xf9\xb6\x75\x6c\x13\x06\x1d\x4b\x59\xd2\x79\x72\x23\x2d\x32\x88\ +\xe6\x38\xb5\xd3\x95\xdc\x65\x61\xd7\xae\xe2\xf3\xb8\x1e\x45\x3f\ +\x91\x04\x61\x74\x6f\xf1\xb9\x5a\xe7\x2c\x6a\x07\x12\xce\x3a\x17\ +\xfc\x44\xcf\x7f\xfe\xd6\x4f\x24\x02\xdf\xa5\x6e\x1d\xbd\xb3\x98\ +\xb8\xa6\x01\x27\x99\x64\x9a\x0c\x50\x34\x27\xde\xd1\xeb\x0f\xbd\ +\xf5\x13\x89\x10\xf6\x3c\x7a\x2b\x1e\x5e\x13\x71\x4a\x84\x98\x36\ +\x45\x02\x09\x9c\xb1\x4b\x7b\xfd\xa1\xb7\x7e\x22\x11\x42\x73\xf4\ +\x1e\x5c\x91\xe9\x8c\x96\x27\x09\x78\x95\xd5\x5e\xba\x4b\x09\x1e\ +\x8b\x6e\xdb\xda\x8e\xcf\xe8\x44\xdb\x73\xe8\xf9\x44\xf2\x8f\xfd\ +\x12\x1e\x5e\xe3\x6f\x88\xe4\x09\x94\x23\x25\x18\x39\xc4\x1c\xc2\ +\x90\x54\xf6\x76\x26\x5c\x68\xcc\xae\x8f\x8b\xd8\x73\xe8\xf9\x44\ +\x0a\xc7\x5f\xdf\x61\xf5\x73\x35\x29\x9c\x38\x99\xac\xe0\x31\x0b\ +\x97\x74\xb9\x12\x20\x31\xd0\xa8\xe4\x5b\x13\x3d\xfb\x4d\x93\x9e\ +\x4e\x24\xbf\xfb\x13\xab\x4c\xeb\xd8\x1d\x31\x41\x44\x29\x54\x53\ +\x05\xb7\x03\x80\x98\x3e\xb8\xec\x87\x2f\x6d\xd5\x20\x75\x59\x0a\ +\xcd\x9b\x7a\xf5\xa1\xb7\xde\x1e\x91\xf6\xfd\xd3\x67\xad\x6d\xb6\ +\x7f\xc4\x41\x59\x93\x12\x48\x32\x4a\xd3\x86\x17\xdc\xa4\x4c\x0b\ +\x6f\xa5\x44\x9c\x75\x43\xbd\xfa\xd0\x5b\xcf\x26\x12\x3f\xbc\x36\ +\xf9\xee\xf6\x34\xb0\x10\xf2\x41\x46\x51\xb6\x63\x3c\x8a\x0a\x7d\ +\x04\xb7\x3c\x32\x51\xc1\x43\x6f\x3b\x3f\x37\x24\xca\x9e\x41\xef\ +\x8e\x48\x87\xbf\x26\x0f\xaf\xe1\x63\x8e\x38\xae\xe8\x18\x03\x30\ +\x8f\xbc\x60\x29\x42\x79\xac\x87\x24\x9f\x32\x5d\xe4\x89\xac\x0b\ +\xfe\xd5\x9e\x7b\xe8\xad\x67\x13\xc9\x4f\xee\xbf\x87\x52\x88\x8f\ +\x5f\xf3\x82\xd7\xd1\x02\xc9\x93\x44\x01\xf0\x98\xea\x72\x1d\x2b\ +\xa0\x13\x3d\x0f\x4a\xa1\x75\x6f\x34\xf6\x0e\x7a\x32\x91\xfc\x13\ +\x97\xbf\xdf\xf8\x31\x1a\x35\x24\x25\x24\x81\x84\xc4\x75\x91\xea\ +\x88\xaa\x18\xc4\x00\x2a\xe6\x08\x0e\x13\x35\xec\x62\xc2\xb5\xfe\ +\xa9\xeb\x7b\xea\xa1\xb7\xde\x1c\x91\x8e\xbe\x8a\x87\xd7\x96\x89\ +\x94\xf2\x29\x5f\xee\x70\xa1\xa4\x48\xa3\x95\x16\x52\xe4\x37\x2f\ +\xab\x80\xbf\xb3\x66\x30\x4c\x1e\xdb\x2e\xaa\x9e\x40\xcf\x25\x12\ +\x16\xc2\xc1\x1f\xbb\xab\x58\x24\x13\x64\x78\x89\xa3\x49\x4c\x18\ +\x06\x31\xa2\x2a\x03\x23\x12\x9c\x3b\x18\x63\x0c\x4a\x34\xef\x3f\ +\xd3\x4b\x0f\xbd\xf5\x5e\x22\x1d\xf9\xee\x27\xac\x19\x3f\x5f\x44\ +\x46\x35\x1f\x72\xb9\x34\xf2\x68\xf2\x81\x72\x01\x1f\x55\xd9\xf7\ +\x28\x23\xac\x59\x6b\xcc\x81\x4f\x8b\xb4\xe0\xd1\x7b\x89\x34\x31\ +\xb2\xa3\x9c\x1d\x29\x17\xda\xa0\x23\x94\x22\xae\x91\x2a\xd0\xdb\ +\x00\x1d\x32\xc9\xb7\xfc\x8e\x5e\xf9\xfc\xad\xa7\x12\xc9\xff\xfd\ +\x85\x17\x18\x7f\x6c\x93\x88\x45\x02\x4d\x91\x58\x4c\xb3\xdc\x49\ +\xd3\x21\x4f\x6b\x30\xe0\x4e\x77\x87\xe4\x62\x3d\xe0\x2f\x0f\xee\ +\xe9\x4b\x99\x5d\xe0\xe8\xa9\x44\x0a\x63\xfb\xee\xa6\x45\xf6\x20\ +\xfa\x3e\xa6\x41\xdc\xb4\x8d\x3c\x5a\x28\x1b\xf4\xfe\x50\x2c\xfa\ +\x22\xb0\x1e\x9f\xfa\x43\x88\x36\x36\xb0\x1c\xf5\x94\x78\x2e\x4c\ +\x8e\xf5\xc4\x43\x6f\x3d\x93\x48\x61\xe7\x86\x65\x74\xc9\x7f\x27\ +\x3a\x19\x7d\x9d\xee\x19\xa1\xc3\x85\x65\xc4\x8c\x29\x17\x45\x26\ +\xf3\x7d\x23\x1e\x99\xa2\x2e\xc6\x50\x19\x54\x78\xd3\xba\xdd\x3f\ +\xbb\x75\xc6\x67\x9d\xde\xeb\xe8\x9d\x44\x7a\xe7\xff\x6e\xb1\x66\ +\x82\x16\xc0\xc4\x73\x26\x81\x8b\x7d\xad\x05\xc8\x6d\x4c\x95\xaf\ +\x20\xfa\xd1\x86\xfe\xc0\xc7\x18\x2a\x47\x0a\x58\xe3\x56\x98\x63\ +\xfb\x3f\x1b\xa5\x85\x8b\x9e\x48\x24\xfe\xcf\x6b\xe1\xe8\x3d\xe9\ +\x4a\x4b\xb3\x86\x20\xfd\x9d\x90\xe4\xaa\x41\xa1\xeb\x24\x75\xa0\ +\x58\x25\xd7\x24\x8b\x96\xfc\xe3\x43\x6f\x0b\xfb\x3f\xbd\xf5\xc6\ +\x88\xf4\xf7\x5f\xff\x30\x4d\x6b\x57\xa7\x4e\xd6\x9e\xcf\x68\x49\ +\x4f\x00\xab\xa5\x04\x9a\xb2\x58\x57\x5d\x58\x11\x92\xaf\x98\xb4\ +\xbe\x0b\x1e\x77\xb9\x17\xf4\x43\x6f\x3d\x91\x48\x7e\x6c\x3f\x3f\ +\xbc\x96\x06\x13\x41\xb5\xe3\xf3\x04\x50\x54\xd3\x85\xaf\xdc\xe0\ +\x20\x4e\xe9\x4a\x2e\xd3\x81\x32\xcb\x1b\x5a\x86\x5b\x67\xc2\x02\ +\x7f\xe8\x6d\xc1\x27\x92\x7f\xe2\xc6\x15\x74\xb9\xf6\x59\x2c\x64\ +\x78\x10\xd1\x02\xe4\x89\x93\x67\x4c\x66\x87\x4d\x0b\x10\x4d\x2a\ +\x15\xc8\x3f\xf0\x2d\x10\xfd\xe2\xb6\x75\xb3\x7f\x66\xcb\x79\xcc\ +\x2e\x40\x2c\xfc\x11\xe9\xd8\x4b\xbf\xe1\xcc\xe4\x8a\x3c\x19\x4a\ +\xd0\x04\x20\xa3\xda\x59\x85\x4d\xa5\x02\x46\x1f\x4e\x18\x2d\x25\ +\x44\x67\xfd\x14\x2e\x26\x6d\x11\x84\x16\xdd\x83\x66\x7c\xff\x7f\ +\x62\x61\x01\x62\x41\x27\x12\x2f\xb2\x9b\x87\x77\x70\x67\xa2\x3f\ +\xa5\xc4\xae\x95\x5c\x50\x21\x43\x75\x0a\x4c\xd0\xbc\x40\x0c\x50\ +\x92\x8b\xcf\xdc\x32\x4a\x28\x85\x88\x2a\x1a\x14\xc3\xdd\xe1\xe5\ +\x6d\x83\x51\x5a\x58\x58\xd8\x23\xd2\xff\xf9\xea\xd5\xfc\xf0\x1a\ +\xf5\x6a\xbe\x36\xe6\x4b\xf7\x4c\x4e\xbd\x2e\x34\xe5\x46\x05\x72\ +\x2b\x92\xa1\x97\xfa\x0c\xd0\xb4\x76\x8a\x4a\xbd\x25\xc0\x90\x87\ +\x95\x88\xac\x0b\x6f\xef\xbd\x59\xb4\x0b\x0a\x0b\x3a\x91\xfc\xf1\ +\x03\xf7\xd2\xca\x88\x2e\xfd\xa3\x0c\xc2\x3c\x15\x4e\xa6\xc8\xa6\ +\xc4\xa9\x26\x50\xd2\x4b\x41\x32\x30\x2f\x41\x38\xb1\x54\x17\x25\ +\xa1\x91\x03\x92\xc4\x75\xe0\xdc\x5a\x90\x77\xba\x17\x6c\x22\xf9\ +\x27\x2f\x39\xcf\xb4\xc6\xb6\x70\xe7\x29\xa8\x2f\x75\xda\xca\xb4\ +\x8c\xaa\x9c\x10\xf3\xa1\x00\xc7\x40\x42\xa0\x4e\xa4\x49\x2f\x94\ +\x39\x50\xf1\x63\xf0\x1b\x53\x52\x85\xd6\xf5\xfe\xa9\x1b\x17\xdc\ +\x43\x6f\x0b\x2e\x91\xf8\xa1\xfe\xc7\x3e\xba\xd4\x8c\xbc\xf1\xdb\ +\xd6\xfa\xd2\x7f\x9e\x4d\x1d\xcd\x4c\x44\xf5\x6a\x8b\x4d\xb4\xc9\ +\x5c\x0a\x90\xaf\xe4\x43\x64\xb8\xae\x2c\xc0\x33\xc4\xba\x99\x1e\ +\x54\x7d\x9d\x1b\x08\x93\x47\xb6\x63\x1f\x17\xd2\x93\x01\x1d\xdb\ +\xab\x2e\xe0\xff\x3b\xf4\xa3\x1f\x0d\x85\x83\xaf\x0d\xd9\x89\xf1\ +\x65\x61\x60\xe9\x4a\x33\x79\x60\xb5\x69\x99\xd5\xc6\x8f\xae\x31\ +\x66\xf9\xca\x10\xde\x39\xdb\xd8\xa1\xb5\xc6\x4f\xae\xa2\x2b\xa3\ +\x55\x26\x4c\xac\x0a\xce\xae\x30\xad\xc9\x95\x34\x72\x14\x1d\xa5\ +\x09\x00\xc8\x51\x73\x47\x23\x1f\x88\x72\x5e\x28\x8d\x6a\x66\xc0\ +\xf3\xf6\x6c\xfc\xe7\x1a\xa9\x98\x21\x6a\x68\x02\xa5\x17\x46\xa8\ +\x82\x46\x2d\x10\xb7\x05\x48\x6e\x92\xf5\x30\x39\x8c\xd0\x3e\x8c\ +\x10\xbf\x9f\xca\x28\xc9\x7b\x82\x75\xa3\xd6\x4c\xbe\x65\xdc\xd2\ +\x51\xd3\x1a\x7f\xd3\x0c\x9c\x7d\xd8\xfa\x03\x87\xc2\xe2\x5f\x18\ +\xb1\xc7\x0f\x4c\x18\xf3\xf1\x71\x3b\xfc\x45\xaa\x5f\x2f\xb4\xb7\ +\xcc\x1c\x21\x26\xc5\x5b\x94\x14\xcf\x0c\xd9\x86\x59\x16\xc6\x8e\ +\xe0\xe9\x41\x4a\x8a\x63\x2b\xa9\xf3\xd7\x1a\x33\xb4\x32\xf8\x77\ +\xcf\x31\x66\xd1\x1a\x13\x9a\xab\x8d\x1b\x58\x49\xb6\x55\x74\x0a\ +\xaf\xa0\xca\x4b\xa9\xd7\x97\xda\xd0\xe2\x11\x14\x3b\x1d\x3b\x47\ +\xbb\x31\xca\x69\x90\xa0\x0d\x8f\x1c\x60\xa3\x63\x1b\x34\x71\x14\ +\xd3\xc7\x20\x41\x13\x29\x39\xe2\x2f\x26\x8e\x2a\x59\x26\x5f\x4c\ +\x7d\xac\x4f\x31\x0a\x99\x91\xc5\xe0\x0d\x67\x70\xa6\x53\x91\x37\ +\xa8\xe9\xbc\x09\x7e\x8c\xda\x62\x9c\x2c\x87\x49\x49\x49\x67\xf7\ +\x51\x19\xa3\x35\xd7\x1e\x63\x17\x8d\x5a\xdf\x7c\xdb\x34\x16\x1f\ +\xb4\xcd\xe3\x07\xcd\x92\x55\x07\xcd\xe4\xd1\xc3\x61\xf1\x39\x94\ +\x7c\x76\xc2\x7c\xfc\xa2\x31\x6b\xbf\xe8\x29\xda\x9c\x01\xbb\x7a\ +\x4a\x08\x2f\x3f\x38\x18\xde\x7c\x78\x99\x59\xbc\x6c\xa9\x39\xf4\ +\xc6\x4a\x3b\x38\xb0\x3a\x4c\x52\xc7\x9b\x26\x25\x82\x5b\x15\xfc\ +\xc4\x39\x74\xe0\x34\x72\x1c\x5f\x63\xdc\x10\x25\xc5\xd8\x2a\xe3\ +\xdc\x4a\xeb\xfd\x20\x35\xf5\x90\x33\x4d\xa2\x45\xfb\x31\x27\x7b\ +\x53\xe8\x08\xea\x04\x24\x65\x84\x26\x83\xd2\xe4\x47\x98\xd5\x18\ +\x30\x20\x91\xac\xac\x04\xb8\x02\x28\x6d\x68\x48\x29\x25\x90\x06\ +\x43\x4d\x66\x33\x5d\xb2\xc1\x0c\x3e\xb2\xec\x87\x18\x22\x46\x14\ +\x75\xa6\x8d\x81\x20\x18\x5a\xa1\xa6\x0d\xf6\x86\x58\x4a\xbc\x80\ +\xc4\x19\x37\xae\x31\x41\x49\xb8\x3f\x26\xa1\xa7\xe4\x6b\x8c\x90\ +\xfc\x26\x0d\xd6\x47\x8c\x69\x1d\x34\x61\xd1\x3e\xeb\xfc\x48\x30\ +\x4b\xf6\xd1\x88\x3e\x4a\x27\xf4\x61\xb3\xfc\xdc\x09\x7b\xd9\x97\ +\xc6\xf8\x3d\x4e\x10\x7a\x28\x31\x31\x0e\xfc\x70\x8d\x39\xf2\xc2\ +\x0a\xdb\x58\xbe\xc6\x34\x47\x69\x84\xb0\x94\x08\xe3\x6b\xa8\xbd\ +\xce\xa1\x1d\xa3\x11\x64\x92\x47\x0e\xe3\xc7\x56\xd3\x8e\x11\x6d\ +\x0e\xd1\x32\x6b\xd0\xd8\xd6\x00\xed\x7e\xc7\xd1\x22\x27\x72\xd8\ +\xa9\x2d\xb4\x1d\x60\x60\x99\x58\xa9\x5c\x54\x00\x88\x87\x08\xa4\ +\x2a\x5a\x27\x29\x84\x02\x59\xbd\xd9\x89\x41\x02\x8f\x48\x2e\xf6\ +\x21\xc9\xf8\x84\x5f\x4d\x31\x46\x16\x84\x15\xc4\x4e\x11\xb0\x1a\ +\x83\x41\xe6\xe8\x21\x89\xc9\x3c\x30\x43\x0c\xb1\x45\x59\x3d\x66\ +\x8a\x21\x75\x92\x37\x17\x4e\x3e\xb2\x8c\xd3\xf4\x8a\xa9\x73\x3f\ +\x9d\xf8\xa3\xc6\xb7\x68\xda\x6d\x1c\xa2\x3e\xde\x6f\xed\xe2\x03\ +\xc6\x4f\x1c\xb2\x8d\x81\x7d\xe4\x39\x66\x16\x2d\x7a\xd3\x2e\x5a\ +\x3e\x66\x96\xff\xf2\x5e\x8d\xce\x0f\xc5\xfb\x83\x8f\xfd\x89\x69\ +\x1d\xfe\x5d\xda\x29\x24\x86\xbc\x4d\x44\x1c\xae\x0b\xa4\xf5\x45\ +\x14\x19\x08\x96\xda\x53\x48\xc7\xf6\x05\x20\x83\x8a\x4e\x5c\x12\ +\x55\xb0\x2c\xbe\x3c\x00\x80\xc2\x20\x4e\x90\xb1\x61\x1f\x50\xd1\ +\x89\x99\xc1\x32\xec\xa0\xb4\x49\x76\x71\x82\x8c\xcd\x94\x31\xc8\ +\x60\x29\x91\xb8\x7b\x92\x3d\x6f\x21\xe9\x36\x0a\x1e\x74\x84\xa2\ +\x17\x0f\x14\x1c\x3c\x82\x63\x92\xae\x1a\x23\x6f\x4b\x75\x07\x0f\ +\xcc\x1c\x43\xa8\x08\xaa\x03\xc0\x03\x27\x1f\x83\xf6\x4c\x14\x20\ +\x40\x8a\xa1\x0a\x02\xb1\x48\xbe\xaf\xb8\xb3\xd6\xdc\x9b\xae\xda\ +\xec\xf0\x5f\x8f\xbb\xdb\xff\xf3\x7d\x76\xd1\x8a\xfb\x43\xb0\xcd\ +\xd8\x68\xa8\x19\x29\xbf\x31\x1c\x85\x22\x1e\xc7\x14\x99\x6d\x0a\ +\x31\xea\xce\xb2\x0f\xd1\x52\x0c\xc8\xe2\x1a\x37\x05\x55\x1f\x94\ +\xb6\x03\x86\x0c\x3b\xa1\x14\x43\x21\xc2\xac\xc6\x10\xc2\x75\x41\ +\x69\xc3\x09\x00\x0d\xf1\xac\x87\x4e\x1d\x18\x5c\x3b\x6e\xa1\x83\ +\x5d\x7d\xa1\x87\x2c\xce\xbc\xe5\x0d\x59\x41\xf3\x42\x98\x2e\x06\ +\x24\x95\xe3\x66\x36\x62\x00\xc4\x80\xd7\x42\xe0\x18\x02\x3a\x56\ +\x1a\xbd\xc2\x17\xdd\xa6\x4f\xde\x6d\x2f\x7b\x7c\x2c\xb7\x25\xf8\ +\x47\x56\xdf\x1c\x26\x7e\xfe\xbf\xad\x6b\x2d\x43\x0d\x6d\xec\x14\ +\x10\x32\xf1\x25\x1a\x4d\x91\xd1\xa8\xca\xe7\x3e\xe2\x38\x6d\x8c\ +\x1c\x50\x92\x8d\xfe\x92\x9d\xf9\x6a\x5d\xa5\xd1\xa5\x0c\x28\xc9\ +\xc6\xf5\x58\x21\x7c\xb5\xae\xd2\xe8\x52\x00\x06\x59\x6c\xa7\xf5\ +\x0a\x24\xe1\xd1\x0d\x9a\x58\x4c\xa1\xa7\x17\xfe\x50\x87\x19\x21\ +\x40\x35\x06\xbb\x66\xa7\x3c\x77\x2b\xb3\x9a\x6a\x52\x59\x08\x90\ +\xc7\x60\x77\xaa\x3b\x3f\x31\x20\xfa\x71\xeb\x16\xdd\xed\x36\x3e\ +\xfb\x75\x56\x10\x3a\xde\x47\x72\xb7\x1e\x7c\xdc\x2d\x3e\x77\x7d\ +\xf0\x8d\x3d\x90\x11\x04\x21\x70\xcc\x8a\x8c\x4d\x76\x06\x18\xd9\ +\xb7\xa4\x24\x3a\x53\x0c\xad\xc3\x3e\x60\x2a\x31\x4a\xef\x91\x61\ +\x2a\xdd\xac\xc7\x60\xa0\xe1\xb1\x15\x6b\x66\x8b\x3a\x01\xf4\xd4\ +\x19\xf1\x25\x60\xdf\x58\xaf\x14\x83\xc0\xc9\xc7\x9d\x07\x88\x4e\ +\x3a\xaf\x56\x31\x48\x15\x8c\xdf\x6f\xdd\xe2\x1b\xf2\x24\x02\x3a\ +\x26\x12\x60\xb7\xee\x7d\xc9\x2d\xfd\x8f\xeb\x43\x18\x78\x01\xcf\ +\x26\xe3\x4d\x73\xe4\x62\xfe\xd6\xa5\xdd\x80\x93\x38\x42\x9e\x2e\ +\x46\xaa\x98\x21\x8f\xd1\xc1\xcc\xd0\x18\x53\xd9\x67\x2d\x06\x40\ +\x67\xab\xe5\xb9\x00\xc1\x24\xa0\xbc\x01\x37\x37\xf1\xf1\x44\x11\ +\x3b\x28\x00\x3d\x33\x51\x9f\xc7\xc0\x8b\x47\x25\xd6\x8b\x0f\xeb\ +\x88\x24\x99\x00\x17\x66\x2a\x31\xc4\x3e\x1f\x31\xbc\x37\xaf\xd8\ +\xc6\x92\x6b\xdc\xc6\xa7\x9f\x82\x26\xc7\x94\x89\x04\xd8\x2d\xaf\ +\xec\xb1\x2b\x2e\xd9\xec\xed\xd2\x47\x59\x81\xd8\x71\x4f\xda\x10\ +\x77\x85\x68\xc5\x27\xee\x54\x05\x53\xc4\xd0\x20\x4c\x2a\x31\x34\ +\x7e\x42\x25\x46\xb2\xcf\x65\x0c\x7a\xb5\xc5\x60\x14\x4e\xf1\xf8\ +\x8b\x37\xe3\x04\xcb\x2a\x55\x63\x70\x4d\x39\x51\x93\x9e\xea\x33\ +\x2f\x8a\xe9\x62\xf0\x48\x01\x66\x8e\x63\xf8\x60\x9f\x74\x03\x93\ +\x1b\xdc\xf0\x0f\x5e\x65\x5b\x05\xd3\x26\x12\xe0\x6e\x7a\x61\xc4\ +\x5d\xf4\xa9\x5f\x0d\xf6\x8c\x87\x3c\x56\xe9\x02\xb4\x55\x64\x4a\ +\x24\xea\x75\x67\x88\xd7\x1d\x61\xa8\x2c\xa8\xc6\x50\x3b\x8b\xc4\ +\xb0\xaf\xd6\x51\xb9\x82\x14\x43\x31\x57\x31\xc0\x82\x49\xb2\x54\ +\x82\x40\x2c\x77\x85\x3a\x02\x29\x46\xf4\x53\x53\x35\x46\xa9\xe3\ +\x88\x61\x6f\xa6\xe0\x62\x47\x4e\x17\x03\x09\x32\x97\x31\x48\xef\ +\x49\xf8\x2b\x77\x6c\x72\xab\x1d\xfe\xd1\x41\xb6\x77\xc0\x8c\x89\ +\x04\xd8\x8b\x1f\x9e\x70\x3f\x19\x7d\xc0\xba\x95\xf7\xd2\x15\xdd\ +\x38\xbf\xa3\x14\x1e\x1d\x45\x64\x40\x87\xbd\x50\x1b\x28\x1b\x0a\ +\xb9\x5a\x72\x7d\xaa\xd3\x21\x06\x78\x14\x6d\x84\x52\x51\x10\x3f\ +\xeb\x31\x08\x4c\x48\x89\xe1\x9f\xe3\x90\x91\xab\x88\x3d\x3a\x13\ +\x49\xb2\x74\x1d\x6d\x38\x06\xea\x81\xa9\xc4\x60\x9e\x8b\xfa\x21\ +\x12\x47\x66\x1e\x2a\xa6\x6c\xeb\x10\x83\xe5\x48\x99\x65\xe5\x2c\ +\xc5\x08\xbe\x49\x9a\x2f\xd8\xeb\x6f\xdc\x61\xb7\xfc\x68\xda\x1b\ +\x94\xa8\x72\x52\xf0\xdf\x5a\x7b\x53\x38\x7e\xe0\xab\xd6\xb6\x56\ +\x61\x37\xb0\xbb\x78\x63\x74\x0c\x07\x13\x3e\x01\x4a\xb1\xb1\x5a\ +\x65\xf1\x53\x7d\xb2\x77\x02\x19\xb9\x63\x09\xec\xab\x75\xab\x31\ +\x44\xee\x88\xd3\x8a\x41\x0a\xb9\xb3\x0d\x3f\x38\xb4\xdf\x90\x54\ +\x1a\x3b\x88\x43\x94\x02\x71\x4d\x26\xcc\x49\x0c\x56\x91\x5b\xec\ +\x78\x18\xa9\x3e\xbd\x62\xfd\xb4\x11\x74\x88\x01\x6f\x08\xb3\x1c\ +\x83\x06\xa2\x51\x6b\xf9\xca\xec\xef\xe0\x36\x13\x4e\x68\x44\xca\ +\xe1\xb6\xee\x7b\xc2\x9e\xf1\xbe\x0d\xc1\x0c\xbc\x9e\x7a\x86\xc0\ +\x8d\x48\x54\xdb\x0e\xb2\xee\x1c\xef\x31\x64\x90\xa2\x0a\xcb\x71\ +\x53\x80\xeb\x08\xa6\x8a\x91\x83\xe5\x8a\x72\xd6\x63\x48\xe1\x18\ +\x00\x19\x2b\xee\x24\x27\x63\x24\x24\xe6\x5e\xd3\xc6\x20\x46\x25\ +\xbe\x6a\xe2\xba\x11\x33\xc5\x00\xf8\x12\x9e\xd8\xd9\x8a\x41\xfc\ +\x3e\xdb\x58\xb4\xf9\x44\x93\x08\x38\xe9\x44\x02\xdc\xaf\xbc\xf9\ +\xb2\xb3\x67\x5d\x15\xcc\xd0\xf3\x68\x40\x4e\x1e\xec\x47\xb1\xbf\ +\x0c\xde\x37\xe8\xd2\x4e\x17\x2c\x43\xfc\x99\xd0\x86\x69\x25\x4e\ +\x8a\x21\x7c\xba\x3a\x55\x54\xe4\x39\x8b\x41\x25\xc5\xe0\x97\x40\ +\xcf\x0c\x35\x8a\x1c\xc5\x38\x3a\x71\x40\x2a\x7c\xd7\x5b\xdc\x34\ +\x06\xc4\x78\x89\x4d\x12\xfb\x45\x1e\xaf\x13\x89\x81\x2b\x2f\x1e\ +\x01\x67\x25\x06\x16\xc1\xfe\x65\x67\x16\x5d\xe3\x86\x9f\x7d\x3e\ +\x7a\x9c\x18\x4e\x29\x91\x00\x7b\xc7\x81\x7d\x76\xe8\xac\x8d\x74\ +\x45\xc7\x59\x1b\x9b\xaf\x1d\xd8\xb9\x36\xe0\x20\xa0\x17\x9b\x76\ +\x2c\x8b\xc2\xe7\xd0\x18\x7c\xf0\xca\x63\x93\xc5\xd0\x6a\x73\x1a\ +\x43\x1c\xe3\x19\x9e\xf1\x08\x2e\x45\xcf\x7e\xf6\xd5\xde\x52\xd0\ +\x81\x76\x8c\x91\xf9\x21\x0c\xe4\x64\x9f\x21\x46\xe4\x66\x21\x06\ +\x55\xa2\xe9\xec\x3b\xae\xf1\xbe\xf5\x76\xf3\x33\xaf\xb1\xf1\x24\ +\x70\xca\x89\x04\xd8\x2d\x6f\x8d\xb9\x9f\x8c\xfd\x5a\x70\x2b\xfe\ +\x94\x76\xc6\xc7\x5d\x8b\xfb\x8d\x83\x51\x3e\x07\x44\x56\xc1\x07\ +\x14\x00\x83\x92\xf9\x56\x63\x28\x9f\x7c\x40\xa1\x8f\x52\x59\x2f\ +\x98\xb5\x18\xca\xd3\x8b\x63\xf0\xa6\xec\x14\x7d\x32\x1d\x78\xf8\ +\xd1\x5f\xaa\xc7\xda\xf6\x18\x6a\xab\x82\x2e\x6c\x66\x8e\x21\x67\ +\xe1\xe9\xc5\xc0\x48\x64\xff\xc2\xad\xfd\xc0\x2d\x76\xf8\xdb\x87\ +\xa3\xc7\xc9\xe1\xb4\x12\x09\xb0\x5f\x34\xbe\xb1\xed\xc8\xe7\xad\ +\x5b\xb1\x9d\xaf\xe8\xb4\x49\xe3\xf1\xb5\x43\x8e\xab\x0d\xb9\xbf\ +\xd8\xf3\x18\xca\xab\xaa\x1a\xa3\x2d\xe6\x6c\xc6\x88\x84\xa1\x3c\ +\x53\xb2\xf3\xda\x22\x43\x92\x93\x1e\x14\x23\x55\xa4\x40\x1e\x03\ +\xe3\x00\x68\x39\xae\xf8\xa9\x52\xea\x76\x8a\xc1\x11\xc8\xff\x94\ +\x62\x90\x8d\xb8\x09\xda\xdc\xe7\x36\xde\xf0\x7b\xb8\x3a\x67\xc3\ +\x29\x20\xbd\xcd\x6c\x20\x7c\x63\xd5\x26\xdf\x7a\xf7\x6f\x9c\xc5\ +\x2f\x0d\xc5\xd0\x38\x9e\xbc\x33\x01\x1c\x06\x4d\xcb\xe5\x77\x27\ +\x19\x27\x4f\x9b\x9e\x90\x62\x74\xa8\xa3\xb2\xfa\xb0\xaa\xea\x47\ +\x38\xad\x18\x30\xe0\xd3\x7f\x32\xe2\x55\x04\xcb\x11\x2b\xc4\xc4\ +\x10\x0a\x15\xfc\xb3\x60\x6a\x6f\xdb\xa1\xf4\xe6\x1d\xf4\xa0\x73\ +\x10\x23\x78\x73\xd8\xba\x01\x5c\x99\x3d\xc2\xca\xd3\xc0\x69\x8f\ +\x48\x39\xec\xed\x87\xbe\x67\x17\x9f\x3d\xec\xcd\xc0\x4f\x79\xe7\ +\xe5\x78\x84\x95\x4d\x71\x3c\x7c\x7c\x04\x3d\x4e\x86\x52\xe8\xa4\ +\x70\x9b\x88\x98\x20\x75\x34\x06\x90\xd8\xd9\x8e\x21\xd0\xce\x2b\ +\xc7\x88\x52\x8a\x41\x94\x79\x14\xbc\x29\xe2\xb0\x0c\x43\xa7\x18\ +\x51\xce\xc1\xae\xaa\x9a\x29\x06\x6f\x4e\x32\x06\x10\xc2\x1e\x6b\ +\x07\xf1\x99\xd9\x69\x27\x11\x30\xab\x89\x04\xb8\x4f\xef\x7b\xc5\ +\x36\xd6\xad\xf7\x76\xf1\x53\xd8\xf3\x74\x2c\xb2\xe1\x03\x64\x4d\ +\x01\xed\xe4\x92\x01\x3a\xd1\xa7\x18\xa8\x2f\x7c\x35\x06\x7c\xd9\ +\x7d\x2e\x62\x08\xb8\x0f\x28\x00\x5e\x3a\x85\xb5\xc7\x20\x2b\xd7\ +\xc1\x1b\x91\x55\xea\x68\xa0\x6a\x8c\xd8\x42\x64\x23\x9d\x84\x14\ +\xcf\xb8\x9d\x31\x06\xcb\xac\x60\xdd\x89\xc4\xf0\x21\xbc\x68\x1b\ +\x67\x6d\x70\x9b\x9e\x7e\x81\xed\xb3\x80\x59\x4f\x24\xc0\xdd\xf6\ +\xda\x7e\x37\x74\xf1\x27\xe9\x8a\xee\x6b\x7c\x10\x72\x3c\x00\x1f\ +\x8b\x20\x63\x19\xb9\x4d\xda\x43\x9b\xb9\x80\x1a\x04\xc9\x26\xfa\ +\x39\x8d\x91\x3b\xaa\x20\x34\x5d\x35\x49\x4f\xb2\x88\x0d\xfd\x71\ +\xb2\x80\x42\x84\x9e\xa1\x31\xa0\x54\x39\x8a\xa9\x00\x33\xc6\x80\ +\x8c\x4d\x21\x97\x0a\xa0\x31\x68\xdf\x7c\xf0\x8f\xd9\x65\xe7\x6f\ +\xb4\xc3\xff\xf0\xba\x58\x67\x05\x73\x92\x48\x00\x6e\xa9\xbb\x73\ +\x3e\x76\x57\x70\xcb\xff\x1b\x2d\xc2\xe9\x8a\x2e\x9e\x7b\x27\x02\ +\xcd\x3d\xf8\xa3\x0d\x4a\xf5\xa4\x71\x84\x24\x54\xe5\x59\x8f\x21\ +\x94\x51\xf4\x24\xe9\x45\xab\x09\xc4\x5b\x01\xe9\xf0\x8a\x75\xb1\ +\x66\xca\xda\x20\xc5\xd0\xfa\x91\x30\xc8\x2f\x5a\x49\x39\x43\x0c\ +\xfe\xaf\x71\xaa\x99\x22\x06\xd4\x34\x0a\x51\x1f\xd8\xff\xe1\xde\ +\xf7\x81\x6d\xee\xca\xbf\x19\x61\xd3\x2c\x42\x8f\x66\x4e\xe1\x1f\ +\x5e\xf9\x1b\xc1\x8f\xfc\xa5\xb5\x21\xfe\xd8\x0b\x1d\x19\xb7\x23\ +\x8e\x90\xa8\x90\x29\x91\xdb\xd1\x6e\x9a\x18\xac\x9b\xa9\xb2\xe0\ +\xb4\x62\xa0\x42\x7a\xd4\x16\x15\x63\xe7\x80\xe7\x2b\x26\xd1\x71\ +\x50\x85\xc6\x84\x1e\x10\x5b\xee\xaf\x31\x14\x1a\x82\x7d\x58\x43\ +\x5b\x30\xd3\xc4\x48\x3e\x82\x4e\x31\xbc\xf1\x13\xd6\xb8\xfb\xec\ +\xc6\x1b\xfe\x62\xae\xbe\x4d\x92\xed\xc2\xdc\xc2\x3f\xba\xf6\xba\ +\x30\x7e\xe0\x61\xeb\xfc\x9a\xf4\xa6\xd2\x0e\x6d\x50\xbd\xb4\x9f\ +\xfa\xe4\xee\xda\x60\xf3\x13\x83\x18\x79\x42\x12\x15\xb8\x1e\xec\ +\x04\x1e\x25\xaa\x9d\x9a\xd7\x2b\x29\x00\x54\x2e\xc7\x88\xc8\x7d\ +\x08\x5a\xad\x08\x26\x14\x40\xe5\x22\x06\xef\x43\x9b\x0f\x81\xd8\ +\xe0\xfd\x61\x33\x30\xf8\x9b\x6e\xf8\x99\xc7\x45\x3b\x27\x98\xb3\ +\xa9\xad\x0a\x5a\x84\x3f\x65\x97\xaf\xc3\x53\x97\xaf\xe0\x6c\x61\ +\x64\xed\x52\x02\x1a\x40\x68\xa5\x5d\x12\x38\x01\x98\x11\x5a\x05\ +\xe9\x67\x2d\x86\x42\xfc\x78\x3a\x03\x4f\xa5\x3c\x1a\xc1\x5b\x6c\ +\x5a\x13\x66\xc8\x5c\xd4\x06\x22\x3c\xcb\x19\xaf\x72\x8a\x43\x20\ +\x3a\x5d\x8c\x78\x1c\xe5\x18\x48\x2e\xef\xfd\xeb\x76\x60\xc9\xf0\ +\x5c\x27\x11\x30\x6f\x89\x04\xb8\x4f\xfd\xeb\x4f\xdd\xb9\x1f\xd9\ +\x10\xcc\xd0\xf7\x71\xdc\x5c\x00\x6e\x88\x42\x04\x44\xd5\x86\xdc\ +\x87\x51\x51\xcc\x7d\x8c\x68\xe5\x93\x21\x9d\x10\xe8\x39\xe2\xb9\ +\x47\x63\x0d\x9d\x58\x00\xbe\xdd\xc1\xfe\x51\x56\x26\x9e\x50\x49\ +\x59\x40\xe3\x64\xa6\xa9\x62\xb4\x57\xa7\x14\xe2\x7d\x09\x2f\xd8\ +\x30\x70\x8d\x1d\xfe\xc1\x4b\x62\x98\x53\xcc\x6b\x22\x01\x78\x38\ +\xca\xad\xb9\xe2\x53\xde\x0e\xfd\x2f\x9c\x35\x38\x68\x1d\xa1\xb4\ +\xe9\x59\x6a\x6b\xa0\x32\xa4\x4a\x82\xca\x73\x1b\x43\x3a\x58\x41\ +\x1d\x8e\x63\x48\x20\x36\x26\x98\x7a\x61\xfa\x11\x7b\x4a\xb2\x4a\ +\x0c\xf0\x62\xe3\xaa\x52\x9f\xc1\x6e\xd3\xc7\xe0\xf7\x87\x39\x4b\ +\x3e\xfa\xfb\x3b\x73\xdc\x6c\x76\x9f\xd8\xbd\x97\x9d\xe6\x01\xf3\ +\x9e\x48\x00\x9d\x25\xe3\x6e\xdb\x7f\xdd\x6e\xdc\xca\x3f\xa4\x2b\ +\xba\xe2\x7b\xec\xda\x5e\x68\x8c\xd8\x4e\x6d\xe0\x33\x73\x3a\xcc\ +\x47\x0c\xea\xb4\x7c\x91\xcc\x80\x0c\x95\x16\x00\x2a\x2a\xfa\xdc\ +\x51\xd2\x03\x95\x18\x48\x08\x16\xb5\x28\x88\x9f\x31\x86\xd8\x28\ +\xe1\xbc\x37\xf6\x21\xd7\x18\xfa\x75\x3c\xd9\xca\xca\x79\x42\xbe\ +\x5b\x5d\x81\xff\xe6\xea\xcf\x84\xc9\x43\x7f\x69\x5d\x58\xc6\x3b\ +\x83\x4e\xec\xb4\x57\xa2\xc7\xc9\xc9\x0d\x2b\x34\xf9\xe7\xf5\x72\ +\x3e\x87\xe8\x4f\x3a\x06\x1c\xe5\xc1\x36\x45\xaa\x42\xb6\xb8\x4e\ +\x12\x45\x8e\xf4\x06\x9d\xd1\x56\x85\x15\x15\xed\x0c\x31\x50\x07\ +\xc9\x8e\xaf\x08\x39\xd7\xf8\x7d\x7b\xfd\x73\x5f\x12\xcb\xbc\xa2\ +\x2b\x23\x52\x0e\x77\xdb\xc1\xbf\xb5\x8b\xd7\xde\x60\x42\x63\x2f\ +\xce\x4a\x34\x23\x43\x19\xa5\xd2\x96\x53\x35\x69\x5e\x6f\x4e\x62\ +\xe4\x20\x5d\x8a\x51\xe9\xe4\x34\x0d\x11\xda\x62\xe4\xb1\xf2\x18\ +\x40\xb2\x91\x36\xf3\x4b\xac\x32\x99\x8d\x79\x4a\x3c\x4a\xa2\x43\ +\xd6\x0e\xdc\xd2\xad\x24\x02\xba\x9e\x48\x80\xdb\xba\x77\xb7\x3d\ +\xe3\xbc\xf5\x26\x0c\xfe\x38\xb5\x94\xb6\xb2\x52\xa8\xb9\xe1\x58\ +\x2a\x20\x72\xea\x4f\xa2\x39\x5f\xa2\xa7\x13\x83\x21\xfb\x96\x28\ +\x20\x7c\x8a\x51\x54\x88\x3c\xd9\x59\x95\x51\x46\x11\x23\x9e\x3e\ +\xb4\x2d\xf9\x45\x4c\x1f\x83\xb8\x60\x7e\x6a\x1b\x8d\x0d\x6e\xe3\ +\xee\x27\x45\xd5\x15\xd4\x22\x91\x00\x7b\xf3\x9e\xd7\xcc\x99\x6b\ +\x86\x83\x1d\x7a\x22\x3f\xab\x01\x16\xd1\x80\x54\xca\x16\x42\x9b\ +\x42\x50\xd1\xcf\x5a\x0c\x80\x3a\x37\x99\xa6\xaa\xab\x48\x76\xce\ +\x80\x8e\x31\xf8\x1e\x50\x4a\x40\xa5\x19\x3a\xc5\xa0\x8d\x0f\x7e\ +\xb7\x6d\x0c\x6d\x70\xc3\xcf\xbd\xcc\x86\x2e\xa2\x36\x89\x04\xb8\ +\x1b\xdf\x3c\xe4\xcc\x2f\x6e\x0d\x6e\xf9\x97\x52\x83\x13\xf2\xa6\ +\x4d\xcd\xdd\xa1\xbd\x4b\xa8\xd8\x67\x25\x86\x56\xa0\x7d\x2b\x62\ +\xcc\x10\xa4\x62\xef\x14\x63\x46\x54\x63\x50\xf1\xc6\x7c\xdd\x9d\ +\xb5\x66\x33\x5d\xb8\xec\x8b\xda\xee\xe2\x84\x8f\x65\xbe\xe1\xbf\ +\x71\xd6\x7f\x31\xcd\x23\x7f\x66\x9d\x1f\x48\x97\x4e\x20\x48\xb0\ +\x7c\xaf\x67\x92\x15\xd0\x03\xa7\x12\x03\x59\xad\x8b\x6d\x8d\xa3\ +\xd0\x78\x8a\xaa\x9c\x43\x63\x4e\x15\xa3\x4a\x3b\x80\x26\x40\x7c\ +\x66\xf6\xc7\x6e\xe3\x0d\x0f\xce\xf5\x3f\xcf\x3a\x19\xe8\xa1\xd5\ +\x12\xe1\x5b\x67\x7e\xda\x1f\x3f\xfa\x55\xeb\x9a\xcb\xd0\xb0\x1d\ +\xcf\x7e\x6d\xf8\x0e\x98\xc6\x54\xc6\x4c\x31\xf2\x44\x4a\xa8\x56\ +\x52\xb9\x3d\x18\xd6\x40\xf1\x23\x8c\x2a\xda\x7d\x15\xd5\x3a\xbc\ +\x8e\xf2\x61\x9c\x16\xd5\xf7\xd8\x4d\xbb\xbf\x2c\xea\xda\xa0\x56\ +\x53\x5b\x15\x76\xeb\x91\x47\xed\xe0\xd9\x1b\x83\x1f\xd8\x53\x6d\ +\xef\x34\xf5\x55\xf5\x42\x81\xce\x5d\x54\xe0\xe4\x63\xb4\x5b\xb8\ +\x83\x99\xe9\xe0\x29\x6f\x50\x4e\xa2\xa9\xa3\x33\x52\x1d\x81\xc8\ +\x74\x65\xb6\xdf\xb8\xc5\x9f\xaa\x63\x12\x01\xb5\x4e\x24\xc0\xdd\ +\xfa\xf6\x0b\x6e\xd9\x79\xc3\x21\x0c\xf2\x43\xe9\xda\x0d\xd5\xc1\ +\x49\x93\xa2\x43\xd7\x30\xf2\xee\x3b\xe5\x18\x5a\x91\x68\x8a\xa1\ +\xde\x42\xca\x31\x68\x0b\x59\x9d\x09\x9d\x62\x94\x90\x76\xaa\xa0\ +\xde\xfb\x71\xdb\x58\xba\xd1\x6d\x7c\xfa\xfb\xa2\xac\x1d\x6a\x9f\ +\x48\x40\x18\x3c\x7b\x9c\x96\x06\xfc\x3b\xfd\x53\xde\x95\xd6\x8e\ +\x8c\x84\x91\xf3\x30\xab\x7c\xca\x31\xa8\x93\x59\x26\x3f\xed\xe6\ +\x36\x48\x22\xa4\x7a\x24\xe6\x83\xd5\x09\xc5\x10\x68\x0c\xaa\x32\ +\x68\x1a\x8d\xda\xac\x87\x3a\xe1\x3d\x91\x48\xf6\xe8\xcf\x2e\x77\ +\xb6\xc9\xff\xea\xb8\x32\x83\x14\xad\x9d\x7a\xae\x40\xee\x0a\xb3\ +\xca\xa7\x1e\x03\xeb\x16\xe5\xab\x10\x4d\x1a\x72\x08\xe2\x9c\xdd\ +\x2c\x98\x26\x46\x94\xd2\x54\xa9\x7e\xb4\xa1\x51\x8f\x0e\x7f\xe2\ +\x5a\x56\xd7\x14\xef\x89\x44\xf2\xcd\xf1\x0d\xc5\x90\x1f\x91\xf7\ +\x17\x50\xb6\xb6\xa3\x93\xfd\xe4\x63\x88\x47\x56\x2f\xdd\xf3\x12\ +\x52\x8a\x01\x1d\xeb\x0b\x6d\xa7\x18\x11\x31\xdd\x8a\xf5\x54\xe1\ +\x87\xcf\xd9\x68\x7a\xdb\x10\x15\xf5\x44\xed\x13\x89\xff\x3f\x77\ +\x68\xa5\xb3\x31\x75\x49\xd1\x37\x8c\xfc\x0e\xc1\x4c\x38\xed\x18\ +\xe4\xd0\x1e\x23\x32\xa5\x18\xd8\x24\x7b\x05\xa4\xaf\x9a\x3a\xba\ +\xc2\x8f\x93\xce\x5f\x5d\xe7\x9f\x33\xad\x7f\x22\x3d\xf7\x93\x95\ +\xc6\x1f\xff\xb0\x88\x09\xa9\xd1\x85\xe9\xd8\x09\x33\xe0\xe4\x63\ +\xb4\x0d\x23\x54\xa7\x5c\xb9\x18\x51\xa6\x42\x7b\x8c\x19\x41\x21\ +\x69\x4c\x5a\x67\x06\x96\xad\x13\x4d\xed\x50\xff\xa9\xed\xc0\x33\ +\x58\x1f\xd5\xe7\x37\xce\x38\x0f\xf2\x64\x10\xbe\x63\x7e\x90\x32\ +\xf9\x67\x0e\x49\xa7\xc8\xed\x1d\x28\xfd\x51\x82\xba\xd0\x3c\x72\ +\xb5\x28\x6b\x87\xfa\x8f\x48\x13\xc7\x36\xa4\x67\x71\x32\xa0\x99\ +\xb5\xa9\x81\xac\xd9\x13\xcd\x7d\x94\xe6\xc8\xed\x40\xd5\x57\xed\ +\x85\xcc\x43\x43\xc9\x21\xb7\x03\xba\x58\xe6\x2d\xcd\x73\xd1\x4e\ +\x54\xe6\xbc\x6a\x0c\xf5\x53\x5f\x36\x32\x23\xb2\xc6\xc0\xd5\x5e\ +\xab\x39\x0c\x55\x1d\x51\xff\x11\x29\x4c\x5e\x17\xbb\x20\x22\x36\ +\x76\xec\x8b\xac\x3f\x98\xef\x44\x01\xf8\x54\x65\x00\x3a\x94\x93\ +\x8e\x41\x0c\xd7\x21\x8a\xa9\x0c\x7a\xc9\x13\x91\xc4\x17\x76\xd1\ +\x83\xef\x18\x83\xc0\x75\x93\x81\x20\x3c\x13\xda\x14\xe7\x91\xbd\ +\x4e\x98\xda\xa1\xd6\x89\x14\x5e\xfc\x9d\xa5\xc6\x4c\x5e\x9a\x2e\ +\x8c\x88\xea\x3d\xa0\x74\xc5\x05\xaa\x05\x44\x78\xf6\x25\x16\xfe\ +\x28\xa7\x15\x43\x8a\x5e\xa1\xc5\x18\xc2\xe7\x95\x98\x15\x3d\xcb\ +\x64\xa5\x12\xf7\x83\xa8\xc8\xd1\x2e\x3a\xf2\x87\x1d\xf5\xf4\x3d\ +\xd4\x87\x9d\x44\x8e\x31\x9a\x1f\x0c\x3f\xdc\xb6\x36\x1a\xeb\x85\ +\x7a\x8f\x48\x6f\xfd\xc3\xe5\x36\x4c\x0e\xc5\x16\x96\xce\x64\x43\ +\xe4\x13\x55\x81\x90\xeb\xd5\x17\x02\x9f\xd5\xa7\x13\x83\xc0\x9f\ +\xf5\x55\x63\x08\xc3\x75\x32\x83\x7e\x2e\x88\x11\x8a\x13\x03\x32\ +\x95\xf6\x18\x51\x56\x0a\x3b\x4a\x4c\x26\x78\x61\x1b\x65\xfe\xad\ +\xb7\xb1\xb7\x6a\x79\x3f\xa9\xde\x23\xd2\xf1\x23\xdc\x68\xdc\x9c\ +\xb1\x4d\x13\xe5\xb6\xa7\x0d\xd3\xc8\x46\x9a\xdb\x01\x92\xb9\x4f\ +\x84\xcf\x29\xab\x69\x93\xea\x2a\xad\xc6\x20\x24\x1e\x36\x2e\xd1\ +\x49\xd7\x6f\xe5\x71\x45\xfc\x89\x8d\xd3\x56\xa6\x03\x58\x87\x12\ +\xf5\x52\x85\x7d\xd5\x87\x93\x49\xfc\x8a\x18\xa4\xf3\xad\x5a\xde\ +\x4f\xaa\x77\x22\xf9\x09\x6a\x34\x6a\x3c\x16\xa8\xa0\x2d\x73\x90\ +\xdc\x41\x15\xf5\x6a\x93\x9e\x39\xad\x18\x82\x38\x8d\x49\x40\xa5\ +\x92\xa5\xf1\x9f\x9f\x03\x22\x83\xd2\x1f\xea\xcb\x11\xb0\x6f\xc7\ +\x18\x0c\xe2\x11\x4b\xe2\xe9\x74\x06\x75\x29\x86\x31\xb5\x5c\x27\ +\xd5\x36\x91\xf0\x6b\x4d\xb4\xd0\xbe\x0c\x7c\xea\x4c\xb4\x65\x6a\ +\xcf\x88\x8a\x58\xf4\x85\x50\xb5\xcf\x4e\x0c\x8a\x22\xfa\x6a\x25\ +\xf5\x57\xa4\x7a\xc4\x30\xcf\x0e\xed\x31\x38\xb5\xd8\x21\xa6\x4b\ +\x0e\x4d\xbb\x3c\x86\xf5\xad\x0b\xc2\x8b\xbf\xb6\x9a\x1d\x6a\x84\ +\xfa\x26\xd2\xe8\xee\x0f\x1b\xd3\x5c\x81\x51\x81\xfb\x40\xdb\x19\ +\x85\x5b\xb7\x0c\x1e\xf9\x99\x29\x28\x8f\x28\x52\x4e\x2b\x46\x64\ +\x63\xb7\xe6\x41\x01\x21\xea\xa8\x6b\x23\x95\x41\x75\xdd\xc3\xd3\ +\x55\x87\x18\x5c\x87\x7d\x85\x17\x1d\x27\x1d\xeb\x8b\xfa\xb4\x19\ +\x0a\x3f\xff\xb7\x2b\xd9\xa9\x46\xa8\xef\xd4\x36\xbe\xff\x3a\x87\ +\x1f\x13\xd4\x0e\x07\xd5\x22\xed\x0b\x28\x55\xbf\x5c\xe6\xe4\xd1\ +\x02\xe4\xf2\xc9\xc4\x50\x28\x0f\xa5\x18\x8a\x18\xc9\xc8\x5b\xd6\ +\x83\x25\xbf\x38\xe2\x68\x81\x21\xea\x51\x90\x2c\xb9\x5d\xf9\x42\ +\x17\x7d\x55\x46\xdc\x60\xeb\xf7\xb9\x5b\x7d\x47\x24\xd3\x5c\x8f\ +\x33\x11\x2d\x07\xc2\x2d\xa8\x7c\x8e\xaa\x4c\x50\xff\xb6\x7a\x39\ +\x9f\xa3\x2a\x13\x4a\x31\x04\x89\x67\x7d\x66\x60\x54\x65\xf5\xc7\ +\x48\x22\x02\x95\x54\x8d\xc5\x28\xc4\x91\x26\x52\x19\x75\x32\x1a\ +\x9d\x99\x8a\xce\x18\x5f\xbb\x2b\xb7\x5a\x26\x12\x7e\x16\xd5\xf8\ +\x89\x2b\x71\x12\xc6\x33\x32\x2b\x02\x16\x69\xa3\x3e\x99\x29\x21\ +\xb7\x97\x8a\x80\x45\xda\xcc\x18\x43\xf8\x58\x41\x8a\x40\xeb\x17\ +\x23\x09\x94\xd1\x16\xd7\x3d\x65\x7b\x35\x46\xd4\x47\xb9\xf0\x51\ +\x05\x48\xa7\x18\xfe\xe2\xb0\xf3\x16\xfc\xb8\x74\x6d\x50\xcf\x11\ +\xe9\xf5\x87\x2f\xa2\xf5\xd1\x6a\x9c\x7c\x68\x4b\x3e\xd9\xb5\x64\ +\x88\x67\x2b\x18\xa1\x02\xae\x43\xd0\x13\xf8\xb4\x63\x10\x8d\x31\ +\x44\x48\x95\x08\x50\xb1\x0e\x05\x82\xf0\x04\xfe\x11\x3d\xa8\x48\ +\x8f\xc2\xb7\x0a\x92\x4f\x74\x8a\x23\x0f\xb3\xac\x4b\xef\xc3\x15\ +\x3b\xc7\x70\xd6\x2e\x33\xe6\x1d\xbe\x10\xa9\x0b\xea\x39\x22\x1d\ +\xdf\x7b\x35\x35\x19\xef\x1b\xda\x54\x4f\x50\x40\x48\x42\xb2\x0b\ +\xc0\x92\x8a\xa9\xea\x67\x25\x46\xc6\x03\x3c\x3a\x30\x03\x0b\xec\ +\x58\xeb\x40\x66\x91\xed\xaa\xd3\x6a\x53\xc6\x00\xb0\x13\x4c\x24\ +\x99\xc4\xd6\x31\x06\x8a\x0f\xeb\x59\x51\x13\xd4\x33\x91\x42\x93\ +\xef\x1f\x29\xb8\xa3\x95\x57\x86\x90\x74\x64\x57\xef\x2a\x55\xcc\ +\x4e\x8c\x7c\xd1\xab\xd6\x4c\x57\x8a\x41\x1c\xfb\x45\x9b\xa2\x3d\ +\x86\xd8\xa5\xc4\x04\x64\x2d\xf3\xd0\xb5\xc7\x60\x5b\xad\xd6\x49\ +\xb5\x4b\xa4\xb0\xf3\xc1\x01\xd3\x9a\xb8\x12\x8d\x95\x23\xc9\x19\ +\x65\x56\xdb\x37\xf3\xe7\x36\xa7\x32\x37\x31\x72\xa7\x48\x4a\x3a\ +\x0d\x42\xa4\x2d\xac\x24\x43\x47\x7f\xa2\xf1\x45\xe0\xf7\x4d\x69\ +\xc6\xa8\xc6\x30\xa1\x75\xa9\x7f\xfe\xd7\x57\x44\xa1\xfb\xa8\xdf\ +\x88\x74\xfc\x9b\xe7\x9b\xd0\x3c\x4f\xa4\x84\xec\x84\x64\xe4\xa3\ +\x0a\x43\xdb\x97\x0a\xf7\x53\x6a\xf9\x02\xa7\x1a\x03\x72\x34\xc5\ +\x69\x86\x8d\x62\x2f\xe5\x04\x23\x7a\xea\xc8\x12\x7d\xc5\x49\x82\ +\xe6\x31\x68\x0c\x2a\x78\x36\x83\x8f\x31\xc0\x43\x15\x75\xcc\x09\ +\x85\x6c\x56\x98\xa3\x7b\x2f\x8e\xca\xee\xa3\x7e\x23\xd2\xd8\xbe\ +\xab\xad\xf5\x6d\x8f\x94\x6a\x3b\xa6\x64\x10\xb9\x13\xd4\xa5\x8a\ +\x53\x8d\x11\x8c\x1b\xa7\xc4\x18\xd5\x3e\x07\x4d\x31\x64\x8d\x94\ +\x82\xe9\x9b\x10\x52\x0c\x38\x43\x90\x92\xc7\xe0\x54\xc9\x6c\x71\ +\x3a\x8b\x31\xc0\x43\xcd\xa8\xc4\x70\xb4\xe6\x36\xf6\x78\x6d\xa6\ +\xb7\xfa\x25\x52\x6b\x7c\x7d\xd1\x15\x11\xda\x98\x4c\xab\x46\x95\ +\x85\x82\x54\x5d\x80\x93\x89\x81\x4e\x03\x8b\xbe\xf3\x76\xe8\xfb\ +\x6e\xc9\x2f\x5c\xe2\x86\xce\xfc\x90\x37\xe6\x11\xea\xdc\xf8\xe3\ +\x3d\x69\xd4\x88\xa4\x78\x87\x08\x9d\x9a\x18\xa5\xe4\x8a\x7e\x59\ +\x8a\xc4\x18\x14\x0f\x6e\x3c\xed\xd1\x1f\xec\xd3\xc7\xa0\x91\xcd\ +\xd7\xe7\xc6\x64\xad\x12\x89\xd7\x47\xbe\x79\x2d\x3f\x0d\x08\x19\ +\x45\xda\x3b\x6b\xf6\xa4\x4f\x54\x8b\xda\xaa\x05\x1b\xe1\x15\xaa\ +\x4f\x54\x0b\x5b\x29\x81\x4c\x63\xaf\x71\x67\xfe\xa6\x7b\xf9\xd8\ +\x66\xbb\xe5\x8d\x7f\xb1\xeb\xbf\xb7\xa7\xb1\xe9\xf9\xdb\x8c\x6d\ +\x6c\xa5\x64\xe2\x1f\x08\x8e\x23\x46\xec\x56\x2e\x90\xb5\x24\x9d\ +\x50\x79\x71\x92\x40\x09\x50\xf2\xf0\x8b\xeb\x88\x8f\xf0\x48\xa1\ +\xa8\x83\xbf\x50\x79\x15\x31\xfc\x65\x61\xe7\x36\xfe\xbe\x5f\xb7\ +\x51\xaf\x11\xa9\xf9\xc8\x3a\x13\x26\xd7\xe1\xdc\x4b\x05\x1b\xe5\ +\xf3\x42\x9b\x44\x45\x09\x02\x88\x58\x14\x31\x74\xd2\x27\x0a\x06\ +\xdd\xe4\xed\x44\x68\x9c\xf1\x3f\xdd\xd0\x47\x3e\xe2\xb6\x1d\xfe\ +\x1a\x7e\xfd\x09\x16\x85\xdb\xb8\xfb\x31\xbb\xfc\xac\x8f\x86\x60\ +\xf1\xd3\x62\x63\x18\x98\x62\x0c\xea\x7a\x09\xc8\x34\xe9\xa0\x92\ +\xd1\xa5\xa4\x47\xaa\xc4\x17\xfe\x22\x54\x1f\x93\x06\xee\xd1\xb7\ +\x73\x0c\xda\xae\x0e\xee\x9d\x0b\x58\xd1\x65\xd4\x6b\x44\x1a\xd9\ +\x7f\xb9\xb5\xad\x41\x6a\xb3\x78\x16\x4a\x81\x5c\x2d\x55\x7b\x9a\ +\x69\x32\x5d\xd5\xa7\x5a\x72\x7b\x1c\x45\x06\x5f\xc4\x8f\xf2\x34\ +\x6e\x3f\x7a\x8f\xdd\x32\xcd\x2f\x4a\x5f\xf9\x9d\x11\x1a\x9d\x3e\ +\x6f\xcd\xa2\xab\xbc\xb5\xbb\x62\x5d\x02\x6d\x62\x37\xeb\x7e\x60\ +\x93\xc5\x17\x39\x96\xc8\xab\x8e\xd3\x42\x65\x40\x78\xb5\x33\xc9\ +\x64\xae\x83\x75\xd2\xe4\xf1\x5a\x7c\x21\xa0\x5e\x89\x14\xc6\x36\ +\xa4\x9e\x98\x02\x68\x43\xf5\x91\x93\x93\x65\xd6\x13\x40\x93\x7e\ +\x0a\xe4\x31\xb8\x6e\x70\x87\xcd\xc0\x99\x7f\xe0\xec\xf9\xd7\xb8\ +\xad\x07\x76\x8b\x65\x46\xb8\x4d\xcf\xfc\xd8\xb9\x1b\x36\x53\x88\ +\xbb\x68\xe5\xb4\x8f\xa7\x2a\xd9\x11\xac\x97\x79\xd4\x48\xef\xc3\ +\x0a\xe1\xe3\xa6\xf0\x41\x89\xfa\x6a\x9d\xe9\x62\x60\xec\x0a\xc6\ +\xd7\xe2\x0b\x01\xba\x8b\x5d\x07\xbe\x08\xe9\x1f\xfe\xb3\x7f\x76\ +\xe1\xf8\xc5\xa9\xa3\x35\x3b\x08\xb9\xa8\x7c\xda\x79\x62\xd0\x81\ +\xb9\x9e\x7d\x13\x13\x91\x8b\xdc\x05\xc6\x7a\xe3\x96\x3c\xe2\x16\ +\xaf\xbd\xcf\x6e\x79\x6d\x8f\x98\x4e\x09\xfe\xd9\xcd\x6b\xc2\xd8\ +\xc8\x1f\xd1\x3b\xfc\x16\x1e\x89\x85\x8e\x47\x18\xb6\x96\x51\xda\ +\x3f\x80\x76\x9e\x75\x48\x92\x54\x21\x32\x31\x59\xda\xa1\x31\x68\ +\x84\x7a\xd3\x0d\x5c\xf8\x1f\xec\xf0\x5f\x8f\xb3\xa1\x4b\xa8\xcd\ +\x88\x14\xbe\xfb\xd8\x5a\x1b\x9a\xe7\xa3\x85\xb8\x7d\xa9\x95\x98\ +\x0a\xd0\x68\x49\x56\x86\x68\xa9\x91\x33\x3d\xb3\x1d\x63\x60\x8b\ +\x24\x1a\x78\xd5\x2e\x3a\xf3\x16\xf7\xf2\xd8\xaf\x9e\x6e\x12\x01\ +\xee\x9a\xef\xee\x6f\x6c\xfe\xe1\x76\x37\xb0\x68\x33\x75\xee\x4b\ +\x48\x8e\xf8\x23\xc4\xf1\xfd\xf4\x7d\x51\xf0\x62\x99\xa7\xa9\xb8\ +\xf6\xe1\x91\x06\x66\xde\x30\x23\x98\x21\x86\x09\x6b\x8d\x79\xeb\ +\x83\xc4\x74\x15\xf5\x99\xda\xc6\xde\xbe\x8c\x56\xdb\xf1\x47\x6f\ +\x2a\xd0\xd1\x3c\x35\xaf\x32\x55\x3a\x1d\x24\xb3\xe8\x6a\x6c\x3c\ +\xd8\x33\xfe\xd8\xd9\x0b\x2e\x71\xb7\x1e\x7e\xbc\xba\x98\x3e\x5d\ +\xd8\xe1\x67\x77\xb9\xc6\xd0\x55\xf4\x5e\x0f\xd0\x6e\x8d\x14\xfb\ +\x16\x0f\x02\x23\x0c\xb6\xaa\x8e\xbb\x45\x12\x8a\xc8\xd1\x27\xc3\ +\x34\x31\x9c\xb3\x03\xc1\x8c\x76\xfd\x7e\x52\x7d\x46\xa4\xc9\x11\ +\x9e\xeb\xa5\x3d\x19\xca\xe6\x3a\x69\x43\x26\x39\xcd\xd1\x29\x06\ +\x94\xc1\x0e\xed\xb2\x4b\x3f\xf0\xb1\xc6\x1d\x47\xff\xd0\xde\xf1\ +\xca\xa8\x58\x66\x1d\xfc\x0f\xe9\x37\xbd\xf0\xe7\x8e\x2e\xfd\x68\ +\xdc\x78\x0c\xba\x74\x2c\xcc\x89\x44\xfb\x8d\x75\x52\xd2\xa4\xe3\ +\x20\x89\xce\x9e\x34\xad\x89\x3e\xd6\x55\xaa\x3c\xf9\xb4\x4c\xd7\ +\xd7\x49\xb5\x48\x24\xfe\x47\x11\xc6\x5f\xcd\x43\x8f\x34\xec\x94\ +\x20\x63\xd1\x8c\x1d\x7c\xd1\xe8\xa5\x18\xd4\xec\xc1\xee\x33\x03\ +\xab\x3e\xe7\xb6\x3d\xb0\xd1\xfd\xca\x6b\xf3\xf6\x1f\x60\x29\xa1\ +\x5e\x77\x9b\x9e\xbf\xc5\x36\x16\x6d\xa5\xbd\xd8\x93\x86\x56\x06\ +\x76\x32\x26\x4b\xdc\xe9\x08\x3d\xb6\x62\x7d\x45\x89\xc2\x77\xba\ +\xd5\xa7\xf0\x65\x9e\x9d\x5a\x97\xf3\x33\x5c\x5d\x44\x3d\x46\xa4\ +\x5d\xbb\x56\x19\x3f\x19\x3f\x37\x8a\xad\x37\x3d\xc4\x47\x5d\x75\ +\x04\x4a\x23\x51\x21\x37\x83\x3b\xe3\x4b\x76\xe5\x85\x1f\x72\xb7\ +\x1d\xfa\x4a\xb7\xfe\x79\xa7\x1d\x7e\xe6\x51\xd7\x58\xf7\x21\xba\ +\xb2\x7b\x88\x12\x4a\x16\xc5\xd8\x49\x24\x79\xa4\x0c\xdd\x7f\x42\ +\x31\xea\xe4\x05\x50\x0a\x88\x3e\x84\x0f\x9a\x9f\x3f\xf3\xfe\xa8\ +\xeb\x0e\xea\x31\x22\x8d\xbc\x7e\x99\x35\x93\x4b\x45\xe4\x73\xae\ +\x74\xf2\x56\xa0\x36\x75\x69\xf7\xa5\x6e\xb0\x83\xff\xe8\x96\xbe\ +\x7f\xb8\xb1\xed\xe8\x0e\x77\xe3\x2b\x87\xc4\xd0\x35\xd8\xe1\x87\ +\x47\x69\x31\x7e\xbf\x6b\x0c\xd0\xfa\xc9\x3c\xc5\xbb\x4c\x1b\xde\ +\xf7\x12\x05\x53\xe6\x41\xd2\x6d\x80\xcc\xce\x85\x36\xce\xd2\x3a\ +\xa9\xd9\xdd\xfb\x49\xf5\x48\xa4\xc9\xc3\xeb\xa5\x55\x22\x88\xe5\ +\xf3\x4e\x55\x39\xcd\xdc\xaa\xc0\x8a\x22\x78\x7b\xd8\xb8\x33\xef\ +\x77\xe7\xac\xbd\xc6\x6e\xf9\xb7\x67\xc4\x54\x1b\xd8\xe1\xdd\x2f\ +\xd9\xc6\xd0\xc6\x60\x1a\xdb\x69\x34\xda\x9f\xc6\x19\x19\x5c\x22\ +\x13\x0f\x32\x8d\xb0\xf1\xc8\x98\xb2\x12\xc9\xa4\x8b\x2b\x58\xc0\ +\x76\xf9\x8b\x93\xf5\x98\xda\xfc\x24\x5d\x75\xc4\x56\xe3\x36\x02\ +\x1b\xc5\x08\xf0\xa4\xa7\xbf\xb2\xbe\x0c\xef\xed\xd0\x23\x6e\xf1\ +\x39\x97\xb8\x6d\x47\x1e\xb2\xc3\x6f\x74\xf5\xbe\xca\x74\xa0\xb5\ +\x53\xd3\x6d\xda\xfd\x57\x76\x68\xc5\x25\x94\x06\x5f\xa6\x29\xaf\ +\x29\x47\x97\x21\xca\xbc\xe0\x46\x83\xa4\x36\x41\x03\x45\x12\x57\ +\x58\x5a\x2f\x5c\x1d\xd7\x9a\xdd\x41\xd7\x13\x29\xec\xdc\xb0\x8c\ +\x86\xa4\x4b\x79\xe8\xce\x93\x44\xdb\x47\x21\x6d\x59\x46\xd4\xf8\ +\x30\xf0\x1a\x2d\xa6\xb7\xd2\x62\x7a\x9b\xdd\xfa\xf6\xac\xfe\x7a\ +\xf4\x5c\xc2\x5d\xfb\xbd\xbd\xee\xfa\x4f\xde\x6d\xdd\xe2\x1b\x68\ +\x54\x89\xbf\xc3\xc2\x53\x17\x8e\x0b\xbc\x14\x45\x92\x63\x89\xcf\ +\x73\x53\x81\xe8\xfd\x05\xe1\xd9\x7f\xea\xda\x3f\x98\xe8\x7e\x22\ +\x1d\xdd\x7b\x29\xad\x8f\xe2\x27\xd8\x92\x4b\xba\x0c\x60\x81\x90\ +\x64\x40\x7c\xd0\x80\x21\x34\xc6\x83\x5b\xf6\xe7\x76\xf9\x07\x2e\ +\x71\xb7\xbd\xf3\x58\x9d\xfe\x13\xfe\x89\x02\xfb\x8c\x7f\x7b\xec\ +\xce\x5c\x7d\x95\xb5\xee\xf3\xde\xfb\xd1\x38\x2c\x47\x3b\xb7\x47\ +\x64\xa3\x40\x85\x65\xe1\x75\x26\xc4\x3a\xc9\x1c\x7f\xa7\x6b\x5f\ +\x9c\xec\xfe\xd4\x76\xec\x00\x4d\x6b\x58\x03\x10\x78\x23\x8d\x43\ +\xd0\x04\xe2\x65\x01\xe4\x28\xc6\x06\x34\x43\xbb\xcc\xd0\xda\x2b\ +\x1a\xdb\xde\x7d\xc0\xdd\xf4\xea\xbc\xfe\xc8\xdd\x5c\xc0\x5e\xf6\ +\xf8\x98\xdd\xf8\xdc\x9f\xda\x81\x25\x97\xd0\xd1\xf1\x6f\xd0\x6a\ +\xc2\xe0\x5e\x51\xfc\x8b\x2d\x80\xe6\x29\x4f\x6b\x00\x4f\x81\x5d\ +\x5b\x27\xd5\x60\x8d\x34\xc9\xdf\x86\x40\x93\x20\x71\xb8\x69\xf2\ +\xf6\x21\xe8\x09\x8a\xb5\x02\x8d\x42\xfb\x6d\xe3\xac\xbb\xdc\x39\ +\x57\x6c\x76\xb7\xfc\x8c\xa6\x83\x85\x05\x37\xfc\x83\x57\xed\xf5\ +\x37\xde\x62\x1a\x6e\x1b\x0d\xaf\xc5\x47\x37\x7c\x2f\x29\x02\xcb\ +\x00\xb4\x07\xf3\x68\x2c\x24\x1b\xcc\xb6\x7b\x5f\x08\xa8\x74\xd9\ +\xfc\x22\xbc\xbc\x6d\xd0\xbf\xf2\xed\x03\x34\xb5\xad\xc0\x8e\x14\ +\x8d\x43\xbc\x0a\x0c\x08\xd6\x07\xbb\xf4\xcb\xb6\xb1\xec\xf3\xee\ +\xb6\xb7\xf7\x47\xfd\xc2\x06\xbe\x04\xe9\xfd\xdb\x5f\xb0\x21\xfc\ +\x2e\x1d\xff\x60\x6a\x21\x6a\x20\x5d\x1a\x31\xb4\xf1\x02\x2d\xda\ +\xcf\x58\x77\xae\xbb\xfa\x1b\xf3\x7e\xbb\xa3\xab\x23\x52\x78\xf3\ +\x27\xbf\x6c\xfd\x44\xe9\x9b\x10\x29\xa1\xc0\x08\x82\x59\xfc\x63\ +\xb3\xf8\xdc\xe1\xc6\x1d\x47\xef\xee\x95\x24\x02\xec\xf0\xb7\x0f\ +\x37\x36\x3e\x7f\x9f\x6d\x34\xae\x08\xf8\x6d\x36\xd5\x53\x41\x1b\ +\x45\x39\x4e\x70\x3c\xd1\xf1\x3a\x69\xdf\xe5\xac\x9e\x67\x74\x77\ +\x6a\x1b\x7d\xeb\x5a\xeb\xb2\x8c\x21\xa4\xb3\x0c\xd3\x98\x71\x23\ +\x66\x60\xd9\xfd\xce\xfe\xfb\x8f\xb9\x4f\xef\x7b\x4a\x2c\x3d\x07\ +\xdc\x7b\x72\x03\x4b\x36\x18\xe7\x76\x50\xb3\x1c\x8c\xa3\xb5\x4e\ +\x6f\x98\xee\x79\xcb\x72\x68\xe1\x3b\x81\xf3\x8f\xee\x26\x52\x98\ +\x88\xeb\x23\x6e\x01\xa1\x60\xf1\xe3\x88\x6e\xe8\x31\x77\xc6\xbf\ +\xa3\xab\xb1\x77\x1f\xb2\x77\xbc\x32\x11\x2d\xbd\x0b\xdc\x7b\xc2\ +\x6f\xd6\xda\x25\xcb\x3f\x42\xcd\xf4\x15\x13\x7c\xb3\xb8\xdb\x8d\ +\xc2\x23\x12\x5c\xbb\xf2\x8f\xb8\xba\x96\x48\x78\xd0\x3f\x98\xe6\ +\xb5\xf1\x6c\xd2\x82\xb3\xcb\xbd\x66\x17\xad\xde\xe6\x6e\x7f\x60\ +\x2b\xff\x3c\x69\x1f\x25\xe0\xde\x53\x63\xd3\x0f\x3f\x87\x9f\xdc\ +\xa2\x8b\x8f\x97\xe3\x78\x1e\xa7\xb7\x78\xe3\x12\x5f\x9c\xfc\xbd\ +\x79\xff\xe2\x64\xf7\x12\x69\xe2\x5b\x17\xda\xd6\xc4\x2a\x16\x70\ +\x4f\xc8\xd8\xf1\xd0\x38\xe3\x21\x37\x78\xf1\x47\xdd\xad\x07\x1e\ +\x79\x2f\xde\x13\x9a\x4f\xb8\x8d\x4f\x3f\x69\x1b\x43\x57\xd1\xf4\ +\xff\x85\xe0\xfd\x28\xb7\x20\x5e\xd6\x0e\x99\xa3\x2f\xcd\xfb\x3f\ +\x98\xe8\xde\xd4\x76\x74\xdf\xb5\x94\x40\xbc\x42\xa2\xc5\xf4\x53\ +\x76\xc9\xb9\xd7\x34\x6e\x3f\x7a\xbf\xdd\xfa\xcf\xfc\xbb\x6c\x7d\ +\xcc\x0c\x9a\xee\x46\xdd\xc6\xe7\xfe\x3b\xad\x9f\x3e\x46\xe2\xe3\ +\x71\x95\x84\x16\x6d\xce\xfb\x07\xb8\xdd\x1b\x91\x5a\xa3\x1b\x82\ +\x6d\x1c\x0c\x03\x67\x6f\x77\xe7\x5c\xb9\xd1\x6d\x79\xeb\x1f\xc5\ +\xd4\xc7\x49\x82\x12\xea\x5f\xf4\xde\x13\xcd\x6e\x6f\xd2\x14\x57\ +\xdb\x5f\x08\x98\x55\xf0\x83\xfe\xdf\x5a\xfb\x27\xfe\x9b\xe7\xae\ +\x11\x55\x1f\xb3\x84\xb0\x73\xc3\xca\xd6\x77\xaf\xf8\xa3\xf0\xe2\ +\x83\xe9\xb1\x9c\x3e\xfa\x78\x8f\xc0\x98\xff\x0f\x36\xc7\xa6\x71\ +\xfb\x71\xb4\x2d\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\ +" + +qt_resource_name = b"\ +\x00\x06\ +\x07\x03\x7d\xc3\ +\x00\x69\ +\x00\x6d\x00\x61\x00\x67\x00\x65\x00\x73\ +\x00\x0a\ +\x08\x57\x12\xb9\ +\x00\x73\ +\x00\x74\x00\x61\x00\x72\x00\x5f\x00\x65\x00\x6d\x00\x70\x00\x74\x00\x79\ +\x00\x09\ +\x08\x85\x31\x6c\ +\x00\x73\ +\x00\x74\x00\x61\x00\x72\x00\x5f\x00\x66\x00\x75\x00\x6c\x00\x6c\ +" + +qt_resource_struct_v1 = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x02\ +\x00\x00\x00\x12\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +\x00\x00\x00\x2c\x00\x00\x00\x00\x00\x01\x00\x00\x22\x0d\ +" + +qt_resource_struct_v2 = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\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\x00\x2c\x00\x00\x00\x00\x00\x01\x00\x00\x22\x0d\ +\x00\x00\x01\x80\x16\x13\x53\x5c\ +" + +qt_version = [int(v) for v in QtCore.qVersion().split('.')] +if qt_version < [5, 8, 0]: + rcc_version = 1 + qt_resource_struct = qt_resource_struct_v1 +else: + rcc_version = 2 + qt_resource_struct = qt_resource_struct_v2 + +def qInitResources(): + QtCore.qRegisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) + +def qCleanupResources(): + QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) + +qInitResources() diff --git a/Generator/resources.qrc b/Generator/resources.qrc new file mode 100644 index 0000000..48e1161 --- /dev/null +++ b/Generator/resources.qrc @@ -0,0 +1,6 @@ + + + resources/star_full.png + resources/star_empty.png + + diff --git a/Generator/resources/star_empty.png b/Generator/resources/star_empty.png new file mode 100644 index 0000000..d83640d Binary files /dev/null and b/Generator/resources/star_empty.png differ diff --git a/Generator/resources/star_full.png b/Generator/resources/star_full.png new file mode 100644 index 0000000..c06eb32 Binary files /dev/null and b/Generator/resources/star_full.png differ diff --git a/Generator/user.py b/Generator/user.py new file mode 100644 index 0000000..ab7b2ca --- /dev/null +++ b/Generator/user.py @@ -0,0 +1,41 @@ +import secrets +import os +import winreg + + +path = winreg.HKEY_CURRENT_USER + +def saveReg(k,v): + try: + key = winreg.OpenKeyEx(path, r"SOFTWARE\\") + newKey = winreg.CreateKey(key,"RotorOps") + winreg.SetValueEx(newKey, k, 0, winreg.REG_SZ, str(v)) + if newKey: + winreg.CloseKey(newKey) + return True + except Exception as e: + print(e) + return False + + +def readReg(k): + try: + key = winreg.OpenKeyEx(path, r"SOFTWARE\\RotorOps\\") + value = winreg.QueryValueEx(key,k) + if key: + winreg.CloseKey(key) + return value[0] + except Exception as e: + print(e) + return None + +def createUserKey(): + userid = readReg('User') + if not userid or userid == 'None': + print("Unable to find userid in registry.") + userid = secrets.token_urlsafe(10) + if saveReg('User', userid): + print("Saved userid to registry") + return userid + + diff --git a/MissionGenerator.exe b/MissionGenerator.exe deleted file mode 100644 index 2dd91ab..0000000 Binary files a/MissionGenerator.exe and /dev/null differ diff --git a/Generator/Output/.gitignore b/MissionOutput/.gitignore similarity index 100% rename from Generator/Output/.gitignore rename to MissionOutput/.gitignore diff --git a/README.md b/README.md index 8c05ced..765b0ea 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -![alt text](https://github.com/spencershepard/RotorOps/blob/main/Generator/assets/briefing1.png?raw=true) +![alt text](https://dcs-helicopters.com/images/briefing1.png?raw=true) # What is RotorOps? RotorOps is a mission generator and gameplay script for DCS: World. At its heart is a game type called Conflict, which requires helicopter operations to win battles on the ground. This is a territory-capture game that promotes focus on individual 'conflict zones'. At the core of the RotorOps script are AI enhancements that provide a dynamic ground war by causing automatic conflicts between ground forces and a progression of the front line. -![alt text](https://github.com/spencershepard/RotorOps/blob/main/documentation/images/rotorops%20ss%200_3.PNG?raw=true) +![alt text](https://dcs-helicopters.com/images/RotorOps%20v1%20UI.png?raw=true) # Key Features: diff --git a/Generator/assets/background.PNG b/assets/background.PNG similarity index 100% rename from Generator/assets/background.PNG rename to assets/background.PNG diff --git a/assets/briefing1.png b/assets/briefing1.png new file mode 100644 index 0000000..2071880 Binary files /dev/null and b/assets/briefing1.png differ diff --git a/Generator/assets/briefing2.png b/assets/briefing2.png similarity index 100% rename from Generator/assets/briefing2.png rename to assets/briefing2.png diff --git a/Generator/assets/frameless.qss b/assets/frameless.qss similarity index 100% rename from Generator/assets/frameless.qss rename to assets/frameless.qss diff --git a/Generator/assets/icon.ico b/assets/icon.ico similarity index 100% rename from Generator/assets/icon.ico rename to assets/icon.ico diff --git a/Generator/assets/rotorops-dkgray.png b/assets/rotorops-dkgray.png similarity index 100% rename from Generator/assets/rotorops-dkgray.png rename to assets/rotorops-dkgray.png diff --git a/assets/splash.jpg b/assets/splash.jpg new file mode 100644 index 0000000..a878e32 Binary files /dev/null and b/assets/splash.jpg differ diff --git a/assets/star_empty.png b/assets/star_empty.png new file mode 100644 index 0000000..d83640d Binary files /dev/null and b/assets/star_empty.png differ diff --git a/assets/star_full.png b/assets/star_full.png new file mode 100644 index 0000000..c06eb32 Binary files /dev/null and b/assets/star_full.png differ diff --git a/Generator/assets/style.qss b/assets/style.qss similarity index 100% rename from Generator/assets/style.qss rename to assets/style.qss diff --git a/config/blue_player_loadouts.miz b/config/blue_player_loadouts.miz new file mode 100644 index 0000000..db72de0 Binary files /dev/null and b/config/blue_player_loadouts.miz differ diff --git a/config/default-config.yaml b/config/default-config.yaml new file mode 100644 index 0000000..7a41b49 --- /dev/null +++ b/config/default-config.yaml @@ -0,0 +1,29 @@ +description: +player_spawn: auto +checkboxes: + defense_checkBox: false + apcs_spawn_checkBox: true + logistics_crates_checkBox: true + awacs_checkBox: true + tankers_checkBox: true + zone_sams_checkBox: false + smoke_pickup_zone_checkBox: false + inf_spawn_voiceovers_checkBox: true + game_status_checkBox: true + voiceovers_checkBox: true + force_offroad_checkBox: false + hotstart_checkBox: false +disable_checkboxes: +spinboxes: + blueqty_spinBox: 4 + redqty_spinBox: 2 + e_attack_helos_spinBox: 2 + e_attack_planes_spinBox: 1 + e_transport_helos_spinBox: 1 + troop_drop_spinBox: 4 + inf_spawn_spinBox: 2 +radiobuttons: farp_gunits, +red_forces: "RED Default Armor (HARD)" +blue_forces: "BLUE Default US Armor" + + \ No newline at end of file diff --git a/config/user-data.yaml b/config/user-data.yaml new file mode 100644 index 0000000..3664bc3 --- /dev/null +++ b/config/user-data.yaml @@ -0,0 +1,2 @@ +local_ratings: + C:\RotorOps\templates\Scenarios\included\007d3d\Nevada Conflict - Vegas Tour (GRIMM).miz: 4 diff --git a/license.txt b/license.txt deleted file mode 100644 index e51b136..0000000 --- a/license.txt +++ /dev/null @@ -1 +0,0 @@ -Use of this software or derivitives of this software is not permitted on 24/7 public multiplayer servers without permission. Scheduled events are excepted. \ No newline at end of file diff --git a/CTLD.lua b/scripts/CTLD.lua similarity index 100% rename from CTLD.lua rename to scripts/CTLD.lua diff --git a/scripts/CTLD2.lua b/scripts/CTLD2.lua new file mode 100644 index 0000000..c7a3c40 --- /dev/null +++ b/scripts/CTLD2.lua @@ -0,0 +1,6427 @@ +--[[ + Combat Troop and Logistics Drop + + Allows Huey, Mi-8 and C130 to transport troops internally and Helicopters to transport Logistic / Vehicle units to the field via sling-loads + without requiring external mods. + + Supports all of the original CTTS functionality such as AI auto troop load and unload as well as group spawning and preloading of troops into units. + + Supports deployment of Auto Lasing JTAC to the field + + See https://github.com/ciribob/DCS-CTLD for a user manual and the latest version + + Contributors: + - Steggles - https://github.com/Bob7heBuilder + - mvee - https://github.com/mvee + - jmontleon - https://github.com/jmontleon + - emilianomolina - https://github.com/emilianomolina + - davidp57 - https://github.com/veaf + + - Allow minimum distance from friendly logistics to be set + ]] + +ctld = {} -- DONT REMOVE! + +--- Identifier. All output in DCS.log will start with this. +ctld.Id = "CTLD - " + +--- Version. +ctld.Version = "20211113.01" + +-- debug level, specific to this module +ctld.Debug = true +-- trace level, specific to this module +ctld.Trace = true + +ctld.alreadyInitialized = false -- if true, ctld.initialize() will not run + +-- ************************************************************************ +-- ********************* USER CONFIGURATION ****************************** +-- ************************************************************************ +ctld.staticBugWorkaround = false -- DCS had a bug where destroying statics would cause a crash. If this happens again, set this to TRUE + +ctld.disableAllSmoke = false -- if true, all smoke is diabled at pickup and drop off zones regardless of settings below. Leave false to respect settings below + +ctld.hoverPickup = true -- if set to false you can load crates with the F10 menu instead of hovering... Only if not using real crates! + +ctld.enableCrates = true -- if false, Helis will not be able to spawn or unpack crates so will be normal CTTS +ctld.slingLoad = false -- if false, crates can be used WITHOUT slingloading, by hovering above the crate, simulating slingloading but not the weight... +-- There are some bug with Sling-loading that can cause crashes, if these occur set slingLoad to false +-- to use the other method. +-- Set staticBugFix to FALSE if use set ctld.slingLoad to TRUE + +ctld.enableSmokeDrop = true -- if false, helis and c-130 will not be able to drop smoke + +ctld.maxExtractDistance = 125 -- max distance from vehicle to troops to allow a group extraction +ctld.maximumDistanceLogistic = 200 -- max distance from vehicle to logistics to allow a loading or spawning operation +ctld.maximumSearchDistance = 4000 -- max distance for troops to search for enemy +ctld.maximumMoveDistance = 2000 -- max distance for troops to move from drop point if no enemy is nearby + +ctld.minimumDeployDistance = 1000 -- minimum distance from a friendly pickup zone where you can deploy a crate + +ctld.numberOfTroops = 10 -- default number of troops to load on a transport heli or C-130 + -- also works as maximum size of group that'll fit into a helicopter unless overridden +ctld.enableFastRopeInsertion = true -- allows you to drop troops by fast rope +ctld.fastRopeMaximumHeight = 18.28 -- in meters which is 60 ft max fast rope (not rappell) safe height + +ctld.vehiclesForTransportRED = { "BRDM-2", "BTR_D" } -- vehicles to load onto Il-76 - Alternatives {"Strela-1 9P31","BMP-1"} +ctld.vehiclesForTransportBLUE = { "M1045 HMMWV TOW", "M1043 HMMWV Armament" } -- vehicles to load onto c130 - Alternatives {"M1128 Stryker MGS","M1097 Avenger"} +ctld.vehiclesWeight = { + ["BRDM-2"] = 7000, + ["BTR_D"] = 8000, + ["M1045 HMMWV TOW"] = 3220, + ["M1043 HMMWV Armament"] = 2500 +} + +ctld.aaLaunchers = 3 -- controls how many launchers to add to the kub/buk when its spawned. +ctld.hawkLaunchers = 8 -- controls how many launchers to add to the hawk when its spawned. + +ctld.spawnRPGWithCoalition = true --spawns a friendly RPG unit with Coalition forces +ctld.spawnStinger = false -- spawns a stinger / igla soldier with a group of 6 or more soldiers! + +ctld.enabledFOBBuilding = true -- if true, you can load a crate INTO a C-130 than when unpacked creates a Forward Operating Base (FOB) which is a new place to spawn (crates) and carry crates from +-- In future i'd like it to be a FARP but so far that seems impossible... +-- You can also enable troop Pickup at FOBS + +ctld.cratesRequiredForFOB = 3 -- The amount of crates required to build a FOB. Once built, helis can spawn crates at this outpost to be carried and deployed in another area. +-- The large crates can only be loaded and dropped by large aircraft, like the C-130 and listed in ctld.vehicleTransportEnabled +-- Small FOB crates can be moved by helicopter. The FOB will require ctld.cratesRequiredForFOB larges crates and small crates are 1/3 of a large fob crate +-- To build the FOB entirely out of small crates you will need ctld.cratesRequiredForFOB * 3 + +ctld.troopPickupAtFOB = true -- if true, troops can also be picked up at a created FOB + +ctld.buildTimeFOB = 120 --time in seconds for the FOB to be built + +ctld.crateWaitTime = 120 -- time in seconds to wait before you can spawn another crate + +ctld.forceCrateToBeMoved = true -- a crate must be picked up at least once and moved before it can be unpacked. Helps to reduce crate spam + +ctld.radioSound = "beacon.ogg" -- the name of the sound file to use for the FOB radio beacons. If this isnt added to the mission BEACONS WONT WORK! +ctld.radioSoundFC3 = "beaconsilent.ogg" -- name of the second silent radio file, used so FC3 aircraft dont hear ALL the beacon noises... :) + +ctld.deployedBeaconBattery = 30 -- the battery on deployed beacons will last for this number minutes before needing to be re-deployed + +ctld.enabledRadioBeaconDrop = true -- if its set to false then beacons cannot be dropped by units + +ctld.allowRandomAiTeamPickups = false -- Allows the AI to randomize the loading of infantry teams (specified below) at pickup zones + +-- Simulated Sling load configuration + +ctld.minimumHoverHeight = 7.5 -- Lowest allowable height for crate hover +ctld.maximumHoverHeight = 12.0 -- Highest allowable height for crate hover +ctld.maxDistanceFromCrate = 5.5 -- Maximum distance from from crate for hover +ctld.hoverTime = 10 -- Time to hold hover above a crate for loading in seconds + +-- end of Simulated Sling load configuration + +-- AA SYSTEM CONFIG -- +-- Sets a limit on the number of active AA systems that can be built for RED. +-- A system is counted as Active if its fully functional and has all parts +-- If a system is partially destroyed, it no longer counts towards the total +-- When this limit is hit, a player will still be able to get crates for an AA system, just unable +-- to unpack them + +ctld.AASystemLimitRED = 20 -- Red side limit + +ctld.AASystemLimitBLUE = 20 -- Blue side limit + +--END AA SYSTEM CONFIG -- + +-- ***************** JTAC CONFIGURATION ***************** + +ctld.JTAC_LIMIT_RED = 10 -- max number of JTAC Crates for the RED Side +ctld.JTAC_LIMIT_BLUE = 10 -- max number of JTAC Crates for the BLUE Side + +ctld.JTAC_dropEnabled = true -- allow JTAC Crate spawn from F10 menu + +ctld.JTAC_maxDistance = 10000 -- How far a JTAC can "see" in meters (with Line of Sight) + +ctld.JTAC_smokeOn_RED = true -- enables marking of target with smoke for RED forces +ctld.JTAC_smokeOn_BLUE = true -- enables marking of target with smoke for BLUE forces + +ctld.JTAC_smokeColour_RED = 4 -- RED side smoke colour -- Green = 0 , Red = 1, White = 2, Orange = 3, Blue = 4 +ctld.JTAC_smokeColour_BLUE = 1 -- BLUE side smoke colour -- Green = 0 , Red = 1, White = 2, Orange = 3, Blue = 4 + +ctld.JTAC_jtacStatusF10 = true -- enables F10 JTAC Status menu + +ctld.JTAC_location = true -- shows location of target in JTAC message +ctld.location_DMS = false -- shows coordinates as Degrees Minutes Seconds instead of Degrees Decimal minutes + +ctld.JTAC_lock = "all" -- "vehicle" OR "troop" OR "all" forces JTAC to only lock vehicles or troops or all ground units + +-- ***************** Pickup, dropoff and waypoint zones ***************** + +-- Available colors (anything else like "none" disables smoke): "green", "red", "white", "orange", "blue", "none", + +-- Use any of the predefined names or set your own ones + +-- You can add number as a third option to limit the number of soldier or vehicle groups that can be loaded from a zone. +-- Dropping back a group at a limited zone will add one more to the limit + +-- If a zone isn't ACTIVE then you can't pickup from that zone until the zone is activated by ctld.activatePickupZone +-- using the Mission editor + +-- You can pickup from a SHIP by adding the SHIP UNIT NAME instead of a zone name + +-- Side - Controls which side can load/unload troops at the zone + +-- Flag Number - Optional last field. If set the current number of groups remaining can be obtained from the flag value + +--pickupZones = { "Zone name or Ship Unit Name", "smoke color", "limit (-1 unlimited)", "ACTIVE (yes/no)", "side (0 = Both sides / 1 = Red / 2 = Blue )", flag number (optional) } +ctld.pickupZones = { + { "pickzone1", "blue", -1, "yes", 0 }, + { "pickzone2", "red", -1, "yes", 0 }, + { "pickzone3", "none", -1, "yes", 0 }, + { "pickzone4", "none", -1, "yes", 0 }, + { "pickzone5", "none", -1, "yes", 0 }, + { "pickzone6", "none", -1, "yes", 0 }, + { "pickzone7", "none", -1, "yes", 0 }, + { "pickzone8", "none", -1, "yes", 0 }, + { "pickzone9", "none", 5, "yes", 1 }, -- limits pickup zone 9 to 5 groups of soldiers or vehicles, only red can pick up + { "pickzone10", "none", 10, "yes", 2 }, -- limits pickup zone 10 to 10 groups of soldiers or vehicles, only blue can pick up + + { "pickzone11", "blue", 20, "no", 2 }, -- limits pickup zone 11 to 20 groups of soldiers or vehicles, only blue can pick up. Zone starts inactive! + { "pickzone12", "red", 20, "no", 1 }, -- limits pickup zone 11 to 20 groups of soldiers or vehicles, only blue can pick up. Zone starts inactive! + { "pickzone13", "none", -1, "yes", 0 }, + { "pickzone14", "none", -1, "yes", 0 }, + { "pickzone15", "none", -1, "yes", 0 }, + { "pickzone16", "none", -1, "yes", 0 }, + { "pickzone17", "none", -1, "yes", 0 }, + { "pickzone18", "none", -1, "yes", 0 }, + { "pickzone19", "none", 5, "yes", 0 }, + { "pickzone20", "none", 10, "yes", 0, 1000 }, -- optional extra flag number to store the current number of groups available in + + { "USA Carrier", "blue", 10, "yes", 0, 1001 }, -- instead of a Zone Name you can also use the UNIT NAME of a ship +} + + +-- dropOffZones = {"name","smoke colour",0,side 1 = Red or 2 = Blue or 0 = Both sides} +ctld.dropOffZones = { + { "dropzone1", "green", 2 }, + { "dropzone2", "blue", 2 }, + { "dropzone3", "orange", 2 }, + { "dropzone4", "none", 2 }, + { "dropzone5", "none", 1 }, + { "dropzone6", "none", 1 }, + { "dropzone7", "none", 1 }, + { "dropzone8", "none", 1 }, + { "dropzone9", "none", 1 }, + { "dropzone10", "none", 1 }, +} + + +--wpZones = { "Zone name", "smoke color", "ACTIVE (yes/no)", "side (0 = Both sides / 1 = Red / 2 = Blue )", } +ctld.wpZones = { + { "wpzone1", "green","yes", 2 }, + { "wpzone2", "blue","yes", 2 }, + { "wpzone3", "orange","yes", 2 }, + { "wpzone4", "none","yes", 2 }, + { "wpzone5", "none","yes", 2 }, + { "wpzone6", "none","yes", 1 }, + { "wpzone7", "none","yes", 1 }, + { "wpzone8", "none","yes", 1 }, + { "wpzone9", "none","yes", 1 }, + { "wpzone10", "none","no", 0 }, -- Both sides as its set to 0 +} + + +-- ******************** Transports names ********************** + +-- Use any of the predefined names or set your own ones +ctld.transportPilotNames = { + "helicargo1", + "helicargo2", + "helicargo3", + "helicargo4", + "helicargo5", + "helicargo6", + "helicargo7", + "helicargo8", + "helicargo9", + "helicargo10", + + "helicargo11", + "helicargo12", + "helicargo13", + "helicargo14", + "helicargo15", + "helicargo16", + "helicargo17", + "helicargo18", + "helicargo19", + "helicargo20", + + "helicargo21", + "helicargo22", + "helicargo23", + "helicargo24", + "helicargo25", + + "MEDEVAC #1", + "MEDEVAC #2", + "MEDEVAC #3", + "MEDEVAC #4", + "MEDEVAC #5", + "MEDEVAC #6", + "MEDEVAC #7", + "MEDEVAC #8", + "MEDEVAC #9", + "MEDEVAC #10", + "MEDEVAC #11", + "MEDEVAC #12", + "MEDEVAC #13", + "MEDEVAC #14", + "MEDEVAC #15", + "MEDEVAC #16", + + "MEDEVAC RED #1", + "MEDEVAC RED #2", + "MEDEVAC RED #3", + "MEDEVAC RED #4", + "MEDEVAC RED #5", + "MEDEVAC RED #6", + "MEDEVAC RED #7", + "MEDEVAC RED #8", + "MEDEVAC RED #9", + "MEDEVAC RED #10", + "MEDEVAC RED #11", + "MEDEVAC RED #12", + "MEDEVAC RED #13", + "MEDEVAC RED #14", + "MEDEVAC RED #15", + "MEDEVAC RED #16", + "MEDEVAC RED #17", + "MEDEVAC RED #18", + "MEDEVAC RED #19", + "MEDEVAC RED #20", + "MEDEVAC RED #21", + + "MEDEVAC BLUE #1", + "MEDEVAC BLUE #2", + "MEDEVAC BLUE #3", + "MEDEVAC BLUE #4", + "MEDEVAC BLUE #5", + "MEDEVAC BLUE #6", + "MEDEVAC BLUE #7", + "MEDEVAC BLUE #8", + "MEDEVAC BLUE #9", + "MEDEVAC BLUE #10", + "MEDEVAC BLUE #11", + "MEDEVAC BLUE #12", + "MEDEVAC BLUE #13", + "MEDEVAC BLUE #14", + "MEDEVAC BLUE #15", + "MEDEVAC BLUE #16", + "MEDEVAC BLUE #17", + "MEDEVAC BLUE #18", + "MEDEVAC BLUE #19", + "MEDEVAC BLUE #20", + "MEDEVAC BLUE #21", + + -- *** AI transports names (different names only to ease identification in mission) *** + + -- Use any of the predefined names or set your own ones + + "transport1", + "transport2", + "transport3", + "transport4", + "transport5", + "transport6", + "transport7", + "transport8", + "transport9", + "transport10", + + "transport11", + "transport12", + "transport13", + "transport14", + "transport15", + "transport16", + "transport17", + "transport18", + "transport19", + "transport20", + + "transport21", + "transport22", + "transport23", + "transport24", + "transport25", +} + +-- *************** Optional Extractable GROUPS ***************** + +-- Use any of the predefined names or set your own ones + +ctld.extractableGroups = { + "extract1", + "extract2", + "extract3", + "extract4", + "extract5", + "extract6", + "extract7", + "extract8", + "extract9", + "extract10", + + "extract11", + "extract12", + "extract13", + "extract14", + "extract15", + "extract16", + "extract17", + "extract18", + "extract19", + "extract20", + + "extract21", + "extract22", + "extract23", + "extract24", + "extract25", +} + +-- ************** Logistics UNITS FOR CRATE SPAWNING ****************** + +-- Use any of the predefined names or set your own ones +-- When a logistic unit is destroyed, you will no longer be able to spawn crates + +ctld.logisticUnits = { + "logistic1", + "logistic2", + "logistic3", + "logistic4", + "logistic5", + "logistic6", + "logistic7", + "logistic8", + "logistic9", + "logistic10", +} + +-- ************** UNITS ABLE TO TRANSPORT VEHICLES ****************** +-- Add the model name of the unit that you want to be able to transport and deploy vehicles +-- units db has all the names or you can extract a mission.miz file by making it a zip and looking +-- in the contained mission file +ctld.vehicleTransportEnabled = { + "76MD", -- the il-76 mod doesnt use a normal - sign so il-76md wont match... !!!! GRR + "Hercules", +} + + +-- ************** Maximum Units SETUP for UNITS ****************** + +-- Put the name of the Unit you want to limit group sizes too +-- i.e +-- ["UH-1H"] = 10, +-- +-- Will limit UH1 to only transport groups with a size 10 or less +-- Make sure the unit name is exactly right or it wont work + +ctld.unitLoadLimits = { + + -- Remove the -- below to turn on options + -- ["SA342Mistral"] = 4, + -- ["SA342L"] = 4, + -- ["SA342M"] = 4, + +} + + +-- ************** Allowable actions for UNIT TYPES ****************** + +-- Put the name of the Unit you want to limit actions for +-- NOTE - the unit must've been listed in the transportPilotNames list above +-- This can be used in conjunction with the options above for group sizes +-- By default you can load both crates and troops unless overriden below +-- i.e +-- ["UH-1H"] = {crates=true, troops=false}, +-- +-- Will limit UH1 to only transport CRATES but NOT TROOPS +-- +-- ["SA342Mistral"] = {crates=fales, troops=true}, +-- Will allow Mistral Gazelle to only transport crates, not troops + +ctld.unitActions = { + + -- Remove the -- below to turn on options + -- ["SA342Mistral"] = {crates=true, troops=true}, + -- ["SA342L"] = {crates=false, troops=true}, + -- ["SA342M"] = {crates=false, troops=true}, + +} + +-- ************** WEIGHT CALCULATIONS FOR INFANTRY GROUPS ****************** + +-- Infantry groups weight is calculated based on the soldiers' roles, and the weight of their kit +-- Every soldier weights between 90% and 120% of ctld.SOLDIER_WEIGHT, and they all carry a backpack and their helmet (ctld.KIT_WEIGHT) +-- Standard grunts have a rifle and ammo (ctld.RIFLE_WEIGHT) +-- AA soldiers have a MANPAD tube (ctld.MANPAD_WEIGHT) +-- Anti-tank soldiers have a RPG and a rocket (ctld.RPG_WEIGHT) +-- Machine gunners have the squad MG and 200 bullets (ctld.MG_WEIGHT) +-- JTAC have the laser sight, radio and binoculars (ctld.JTAC_WEIGHT) +-- Mortar servants carry their tube and a few rounds (ctld.MORTAR_WEIGHT) + +ctld.SOLDIER_WEIGHT = 80 -- kg, will be randomized between 90% and 120% +ctld.KIT_WEIGHT = 20 -- kg +ctld.RIFLE_WEIGHT = 5 -- kg +ctld.MANPAD_WEIGHT = 18 -- kg +ctld.RPG_WEIGHT = 7.6 -- kg +ctld.MG_WEIGHT = 10 -- kg +ctld.MORTAR_WEIGHT = 26 -- kg +ctld.JTAC_WEIGHT = 15 -- kg + +-- ************** INFANTRY GROUPS FOR PICKUP ****************** +-- Unit Types +-- inf is normal infantry +-- mg is M249 +-- at is RPG-16 +-- aa is Stinger or Igla +-- mortar is a 2B11 mortar unit +-- jtac is a JTAC soldier, which will use JTACAutoLase +-- You must add a name to the group for it to work +-- You can also add an optional coalition side to limit the group to one side +-- for the side - 2 is BLUE and 1 is RED +ctld.loadableGroups = { + {name = "Standard Group", inf = 6, mg = 2, at = 2 }, -- will make a loadable group with 6 infantry, 2 MGs and 2 anti-tank for both coalitions + {name = "Anti Air", inf = 2, aa = 3 }, + {name = "Anti Tank", inf = 2, at = 6 }, + {name = "Mortar Squad", mortar = 6 }, + {name = "JTAC Group", inf = 4, jtac = 1 }, -- will make a loadable group with 4 infantry and a JTAC soldier for both coalitions + {name = "Single JTAC", jtac = 1 }, -- will make a loadable group witha single JTAC soldier for both coalitions + -- {name = "Mortar Squad Red", inf = 2, mortar = 5, side =1 }, --would make a group loadable by RED only +} + +-- ************** SPAWNABLE CRATES ****************** +-- Weights must be unique as we use the weight to change the cargo to the correct unit +-- when we unpack +-- +ctld.spawnableCrates = { + -- name of the sub menu on F10 for spawning crates + ["Ground Forces"] = { + --crates you can spawn + -- weight in KG + -- Desc is the description on the F10 MENU + -- unit is the model name of the unit to spawn + -- cratesRequired - if set requires that many crates of the same type within 100m of each other in order build the unit + -- side is optional but 2 is BLUE and 1 is RED + -- dont use that option with the HAWK Crates + { weight = 500, desc = "HMMWV - TOW", unit = "M1045 HMMWV TOW", side = 2 }, + { weight = 505, desc = "HMMWV - MG", unit = "M1043 HMMWV Armament", side = 2 }, + + { weight = 510, desc = "BTR-D", unit = "BTR_D", side = 1 }, + { weight = 515, desc = "BRDM-2", unit = "BRDM-2", side = 1 }, + + { weight = 520, desc = "HMMWV - JTAC", unit = "Hummer", side = 2, }, -- used as jtac and unarmed, not on the crate list if JTAC is disabled + { weight = 525, desc = "SKP-11 - JTAC", unit = "SKP-11", side = 1, }, -- used as jtac and unarmed, not on the crate list if JTAC is disabled + + { weight = 100, desc = "2B11 Mortar", unit = "2B11 mortar" }, + + { weight = 250, desc = "SPH 2S19 Msta", unit = "SAU Msta", side = 1, cratesRequired = 3 }, + { weight = 255, desc = "M-109", unit = "M-109", side = 2, cratesRequired = 3 }, + + { weight = 252, desc = "Ural-375 Ammo Truck", unit = "Ural-375", side = 1, cratesRequired = 2 }, + { weight = 253, desc = "M-818 Ammo Truck", unit = "M 818", side = 2, cratesRequired = 2 }, + + { weight = 800, desc = "FOB Crate - Small", unit = "FOB-SMALL" }, -- Builds a FOB! - requires 3 * ctld.cratesRequiredForFOB + }, + ["AA short range"] = { + { weight = 50, desc = "Stinger", unit = "Soldier stinger", side = 2 }, + { weight = 55, desc = "Igla", unit = "SA-18 Igla manpad", side = 1 }, + + { weight = 405, desc = "Strela-1 9P31", unit = "Strela-1 9P31", side = 1, cratesRequired = 3 }, + { weight = 400, desc = "M1097 Avenger", unit = "M1097 Avenger", side = 2, cratesRequired = 3 }, + }, + ["AA mid range"] = { + -- HAWK System + { weight = 540, desc = "HAWK Launcher", unit = "Hawk ln", side = 2}, + { weight = 545, desc = "HAWK Search Radar", unit = "Hawk sr", side = 2 }, + { weight = 546, desc = "HAWK Track Radar", unit = "Hawk tr", side = 2 }, + { weight = 547, desc = "HAWK PCP", unit = "Hawk pcp" , side = 2 }, -- Remove this if on 1.2 + { weight = 548, desc = "HAWK CWAR", unit = "Hawk cwar" , side = 2 }, -- Remove this if on 2.5 + { weight = 549, desc = "HAWK Repair", unit = "HAWK Repair" , side = 2 }, + -- End of HAWK + + -- KUB SYSTEM + { weight = 560, desc = "KUB Launcher", unit = "Kub 2P25 ln", side = 1}, + { weight = 565, desc = "KUB Radar", unit = "Kub 1S91 str", side = 1 }, + { weight = 570, desc = "KUB Repair", unit = "KUB Repair", side = 1}, + -- End of KUB + + -- BUK System + -- { weight = 575, desc = "BUK Launcher", unit = "SA-11 Buk LN 9A310M1"}, + -- { weight = 580, desc = "BUK Search Radar", unit = "SA-11 Buk SR 9S18M1"}, + -- { weight = 585, desc = "BUK CC Radar", unit = "SA-11 Buk CC 9S470M1"}, + -- { weight = 590, desc = "BUK Repair", unit = "BUK Repair"}, + -- END of BUK + }, + ["AA long range"] = { + -- Patriot System + { weight = 555, desc = "Patriot Launcher", unit = "Patriot ln", side = 2 }, + { weight = 556, desc = "Patriot Radar", unit = "Patriot str" , side = 2 }, + { weight = 557, desc = "Patriot ECS", unit = "Patriot ECS", side = 2 }, + -- { weight = 553, desc = "Patriot ICC", unit = "Patriot cp", side = 2 }, + -- { weight = 554, desc = "Patriot EPP", unit = "Patriot EPP", side = 2 }, + { weight = 558, desc = "Patriot AMG (optional)", unit = "Patriot AMG" , side = 2 }, + { weight = 559, desc = "Patriot Repair", unit = "Patriot Repair" , side = 2 }, + -- End of Patriot + + { weight = 595, desc = "Early Warning Radar", unit = "1L13 EWR", side = 1 }, -- cant be used by BLUE coalition + }, +} + +--- 3D model that will be used to represent a loadable crate ; by default, a generator +ctld.spawnableCratesModel_load = { + ["category"] = "Fortifications", + ["shape_name"] = "GeneratorF", + ["type"] = "GeneratorF" +} + +--- 3D model that will be used to represent a slingable crate ; by default, a crate +ctld.spawnableCratesModel_sling = { + ["category"] = "Cargos", + ["shape_name"] = "bw_container_cargo", + ["type"] = "container_cargo" +} + +--[[ Placeholder for different type of cargo containers. Let's say pipes and trunks, fuel for FOB building + ["shape_name"] = "ab-212_cargo", + ["type"] = "uh1h_cargo" --new type for the container previously used + + ["shape_name"] = "ammo_box_cargo", + ["type"] = "ammo_cargo", + + ["shape_name"] = "barrels_cargo", + ["type"] = "barrels_cargo", + + ["shape_name"] = "bw_container_cargo", + ["type"] = "container_cargo", + + ["shape_name"] = "f_bar_cargo", + ["type"] = "f_bar_cargo", + + ["shape_name"] = "fueltank_cargo", + ["type"] = "fueltank_cargo", + + ["shape_name"] = "iso_container_cargo", + ["type"] = "iso_container", + + ["shape_name"] = "iso_container_small_cargo", + ["type"] = "iso_container_small", + + ["shape_name"] = "oiltank_cargo", + ["type"] = "oiltank_cargo", + + ["shape_name"] = "pipes_big_cargo", + ["type"] = "pipes_big_cargo", + + ["shape_name"] = "pipes_small_cargo", + ["type"] = "pipes_small_cargo", + + ["shape_name"] = "tetrapod_cargo", + ["type"] = "tetrapod_cargo", + + ["shape_name"] = "trunks_long_cargo", + ["type"] = "trunks_long_cargo", + + ["shape_name"] = "trunks_small_cargo", + ["type"] = "trunks_small_cargo", +]]-- + +-- if the unit is on this list, it will be made into a JTAC when deployed +ctld.jtacUnitTypes = { + "SKP", "Hummer" -- there are some wierd encoding issues so if you write SKP-11 it wont match as the - sign is encoded differently... +} + + +-- *************************************************************** +-- **************** Mission Editor Functions ********************* +-- *************************************************************** + + +----------------------------------------------------------------- +-- Spawn group at a trigger and set them as extractable. Usage: +-- ctld.spawnGroupAtTrigger("groupside", number, "triggerName", radius) +-- Variables: +-- "groupSide" = "red" for Russia "blue" for USA +-- _number = number of groups to spawn OR Group description +-- "triggerName" = trigger name in mission editor between commas +-- _searchRadius = random distance for units to move from spawn zone (0 will leave troops at the spawn position - no search for enemy) +-- +-- Example: ctld.spawnGroupAtTrigger("red", 2, "spawn1", 1000) +-- +-- This example will spawn 2 groups of russians at the specified point +-- and they will search for enemy or move randomly withing 1000m +-- OR +-- +-- ctld.spawnGroupAtTrigger("blue", {mg=1,at=2,aa=3,inf=4,mortar=5},"spawn2", 2000) +-- Spawns 1 machine gun, 2 anti tank, 3 anti air, 4 standard soldiers and 5 mortars +-- +function ctld.spawnGroupAtTrigger(_groupSide, _number, _triggerName, _searchRadius) + local _spawnTrigger = trigger.misc.getZone(_triggerName) -- trigger to use as reference position + + if _spawnTrigger == nil then + trigger.action.outText("CTLD.lua ERROR: Cant find trigger called " .. _triggerName, 10) + return + end + + local _country + if _groupSide == "red" then + _groupSide = 1 + _country = 0 + else + _groupSide = 2 + _country = 2 + end + + if _searchRadius < 0 then + _searchRadius = 0 + end + + local _pos2 = { x = _spawnTrigger.point.x, y = _spawnTrigger.point.z } + local _alt = land.getHeight(_pos2) + local _pos3 = { x = _pos2.x, y = _alt, z = _pos2.y } + + local _groupDetails = ctld.generateTroopTypes(_groupSide, _number, _country) + + local _droppedTroops = ctld.spawnDroppedGroup(_pos3, _groupDetails, false, _searchRadius); + + if _groupSide == 1 then + table.insert(ctld.droppedTroopsRED, _droppedTroops:getName()) + else + table.insert(ctld.droppedTroopsBLUE, _droppedTroops:getName()) + end +end + + +----------------------------------------------------------------- +-- Spawn group at a Vec3 Point and set them as extractable. Usage: +-- ctld.spawnGroupAtPoint("groupside", number,Vec3 Point, radius) +-- Variables: +-- "groupSide" = "red" for Russia "blue" for USA +-- _number = number of groups to spawn OR Group Description +-- Vec3 Point = A vec3 point like {x=1,y=2,z=3}. Can be obtained from a unit like so: Unit.getName("Unit1"):getPoint() +-- _searchRadius = random distance for units to move from spawn zone (0 will leave troops at the spawn position - no search for enemy) +-- +-- Example: ctld.spawnGroupAtPoint("red", 2, {x=1,y=2,z=3}, 1000) +-- +-- This example will spawn 2 groups of russians at the specified point +-- and they will search for enemy or move randomly withing 1000m +-- OR +-- +-- ctld.spawnGroupAtPoint("blue", {mg=1,at=2,aa=3,inf=4,mortar=5}, {x=1,y=2,z=3}, 2000) +-- Spawns 1 machine gun, 2 anti tank, 3 anti air, 4 standard soldiers and 5 mortars +function ctld.spawnGroupAtPoint(_groupSide, _number, _point, _searchRadius) + + local _country + if _groupSide == "red" then + _groupSide = 1 + _country = 0 + else + _groupSide = 2 + _country = 2 + end + + if _searchRadius < 0 then + _searchRadius = 0 + end + + local _groupDetails = ctld.generateTroopTypes(_groupSide, _number, _country) + + local _droppedTroops = ctld.spawnDroppedGroup(_point, _groupDetails, false, _searchRadius); + + if _groupSide == 1 then + table.insert(ctld.droppedTroopsRED, _droppedTroops:getName()) + else + table.insert(ctld.droppedTroopsBLUE, _droppedTroops:getName()) + end +end + + +-- Preloads a transport with troops or vehicles +-- replaces any troops currently on board +function ctld.preLoadTransport(_unitName, _number, _troops) + + local _unit = ctld.getTransportUnit(_unitName) + + if _unit ~= nil then + + -- will replace any units currently on board + -- if not ctld.troopsOnboard(_unit,_troops) then + ctld.loadTroops(_unit, _troops, _number) + -- end + end +end + + +-- Continuously counts the number of crates in a zone and sets the value of the passed in flag +-- to the count amount +-- This means you can trigger actions based on the count and also trigger messages before the count is reached +-- Just pass in the zone name and flag number like so as a single (NOT Continuous) Trigger +-- This will now work for Mission Editor and Spawned Crates +-- e.g. ctld.cratesInZone("DropZone1", 5) +function ctld.cratesInZone(_zone, _flagNumber) + local _triggerZone = trigger.misc.getZone(_zone) -- trigger to use as reference position + + if _triggerZone == nil then + trigger.action.outText("CTLD.lua ERROR: Cant find zone called " .. _zone, 10) + return + end + + local _zonePos = mist.utils.zoneToVec3(_zone) + + --ignore side, if crate has been used its discounted from the count + local _crateTables = { ctld.spawnedCratesRED, ctld.spawnedCratesBLUE, ctld.missionEditorCargoCrates } + + local _crateCount = 0 + + for _, _crates in pairs(_crateTables) do + + for _crateName, _dontUse in pairs(_crates) do + + --get crate + local _crate = ctld.getCrateObject(_crateName) + + --in air seems buggy with crates so if in air is true, get the height above ground and the speed magnitude + if _crate ~= nil and _crate:getLife() > 0 + and (ctld.inAir(_crate) == false) then + + local _dist = ctld.getDistance(_crate:getPoint(), _zonePos) + + if _dist <= _triggerZone.radius then + _crateCount = _crateCount + 1 + end + end + end + end + + --set flag stuff + trigger.action.setUserFlag(_flagNumber, _crateCount) + + -- env.info("FLAG ".._flagNumber.." crates ".._crateCount) + + --retrigger in 5 seconds + timer.scheduleFunction(function(_args) + + ctld.cratesInZone(_args[1], _args[2]) + end, { _zone, _flagNumber }, timer.getTime() + 5) +end + +-- Creates an extraction zone +-- any Soldiers (not vehicles) dropped at this zone by a helicopter will disappear +-- and be added to a running total of soldiers for a set flag number +-- The idea is you can then drop say 20 troops in a zone and trigger an action using the mission editor triggers +-- and the flag value +-- +-- The ctld.createExtractZone function needs to be called once in a trigger action do script. +-- if you dont want smoke, pass -1 to the function. +--Green = 0 , Red = 1, White = 2, Orange = 3, Blue = 4, NO SMOKE = -1 +-- +-- e.g. ctld.createExtractZone("extractzone1", 2, -1) will create an extraction zone at trigger zone "extractzone1", store the number of troops dropped at +-- the zone in flag 2 and not have smoke +-- +-- +-- +function ctld.createExtractZone(_zone, _flagNumber, _smoke) + local _triggerZone = trigger.misc.getZone(_zone) -- trigger to use as reference position + + if _triggerZone == nil then + trigger.action.outText("CTLD.lua ERROR: Cant find zone called " .. _zone, 10) + return + end + + local _pos2 = { x = _triggerZone.point.x, y = _triggerZone.point.z } + local _alt = land.getHeight(_pos2) + local _pos3 = { x = _pos2.x, y = _alt, z = _pos2.y } + + trigger.action.setUserFlag(_flagNumber, 0) --start at 0 + + local _details = { point = _pos3, name = _zone, smoke = _smoke, flag = _flagNumber, radius = _triggerZone.radius} + + ctld.extractZones[_zone.."-".._flagNumber] = _details + + if _smoke ~= nil and _smoke > -1 then + + local _smokeFunction + + _smokeFunction = function(_args) + + local _extractDetails = ctld.extractZones[_zone.."-".._flagNumber] + -- check zone is still active + if _extractDetails == nil then + -- stop refreshing smoke, zone is done + return + end + + + trigger.action.smoke(_args.point, _args.smoke) + --refresh in 5 minutes + timer.scheduleFunction(_smokeFunction, _args, timer.getTime() + 300) + end + + --run local function + _smokeFunction(_details) + end +end + + +-- Removes an extraction zone +-- +-- The smoke will take up to 5 minutes to disappear depending on the last time the smoke was activated +-- +-- The ctld.removeExtractZone function needs to be called once in a trigger action do script. +-- +-- e.g. ctld.removeExtractZone("extractzone1", 2) will remove an extraction zone at trigger zone "extractzone1" +-- that was setup with flag 2 +-- +-- +-- +function ctld.removeExtractZone(_zone,_flagNumber) + + local _extractDetails = ctld.extractZones[_zone.."-".._flagNumber] + + if _extractDetails ~= nil then + --remove zone + ctld.extractZones[_zone.."-".._flagNumber] = nil + + end +end + +-- CONTINUOUS TRIGGER FUNCTION +-- This function will count the current number of extractable RED and BLUE +-- GROUPS in a zone and store the values in two flags +-- A group is only counted as being in a zone when the leader of that group +-- is in the zone +-- Use: ctld.countDroppedGroupsInZone("Zone Name", flagBlue, flagRed) +function ctld.countDroppedGroupsInZone(_zone, _blueFlag, _redFlag) + + local _triggerZone = trigger.misc.getZone(_zone) -- trigger to use as reference position + + if _triggerZone == nil then + trigger.action.outText("CTLD.lua ERROR: Cant find zone called " .. _zone, 10) + return + end + + local _zonePos = mist.utils.zoneToVec3(_zone) + + local _redCount = 0; + local _blueCount = 0; + + local _allGroups = {ctld.droppedTroopsRED,ctld.droppedTroopsBLUE,ctld.droppedVehiclesRED,ctld.droppedVehiclesBLUE} + for _, _extractGroups in pairs(_allGroups) do + for _,_groupName in pairs(_extractGroups) do + local _groupUnits = ctld.getGroup(_groupName) + + if #_groupUnits > 0 then + local _zonePos = mist.utils.zoneToVec3(_zone) + local _dist = ctld.getDistance(_groupUnits[1]:getPoint(), _zonePos) + + if _dist <= _triggerZone.radius then + + if (_groupUnits[1]:getCoalition() == 1) then + _redCount = _redCount + 1; + else + _blueCount = _blueCount + 1; + end + end + end + end + end + --set flag stuff + trigger.action.setUserFlag(_blueFlag, _blueCount) + trigger.action.setUserFlag(_redFlag, _redCount) + + -- env.info("Groups in zone ".._blueCount.." ".._redCount) + +end + +-- CONTINUOUS TRIGGER FUNCTION +-- This function will count the current number of extractable RED and BLUE +-- UNITS in a zone and store the values in two flags + +-- Use: ctld.countDroppedUnitsInZone("Zone Name", flagBlue, flagRed) +function ctld.countDroppedUnitsInZone(_zone, _blueFlag, _redFlag) + + local _triggerZone = trigger.misc.getZone(_zone) -- trigger to use as reference position + + if _triggerZone == nil then + trigger.action.outText("CTLD.lua ERROR: Cant find zone called " .. _zone, 10) + return + end + + local _zonePos = mist.utils.zoneToVec3(_zone) + + local _redCount = 0; + local _blueCount = 0; + + local _allGroups = {ctld.droppedTroopsRED,ctld.droppedTroopsBLUE,ctld.droppedVehiclesRED,ctld.droppedVehiclesBLUE} + + for _, _extractGroups in pairs(_allGroups) do + for _,_groupName in pairs(_extractGroups) do + local _groupUnits = ctld.getGroup(_groupName) + + if #_groupUnits > 0 then + + local _zonePos = mist.utils.zoneToVec3(_zone) + for _,_unit in pairs(_groupUnits) do + local _dist = ctld.getDistance(_unit:getPoint(), _zonePos) + + if _dist <= _triggerZone.radius then + + if (_unit:getCoalition() == 1) then + _redCount = _redCount + 1; + else + _blueCount = _blueCount + 1; + end + end + end + end + end + end + + + --set flag stuff + trigger.action.setUserFlag(_blueFlag, _blueCount) + trigger.action.setUserFlag(_redFlag, _redCount) + + -- env.info("Units in zone ".._blueCount.." ".._redCount) +end + + +-- Creates a radio beacon on a random UHF - VHF and HF/FM frequency for homing +-- This WILL NOT WORK if you dont add beacon.ogg and beaconsilent.ogg to the mission!!! +-- e.g. ctld.createRadioBeaconAtZone("beaconZone","red", 1440,"Waypoint 1") will create a beacon at trigger zone "beaconZone" for the Red side +-- that will last 1440 minutes (24 hours ) and named "Waypoint 1" in the list of radio beacons +-- +-- e.g. ctld.createRadioBeaconAtZone("beaconZoneBlue","blue", 20) will create a beacon at trigger zone "beaconZoneBlue" for the Blue side +-- that will last 20 minutes +function ctld.createRadioBeaconAtZone(_zone, _coalition, _batteryLife, _name) + local _triggerZone = trigger.misc.getZone(_zone) -- trigger to use as reference position + + if _triggerZone == nil then + trigger.action.outText("CTLD.lua ERROR: Cant find zone called " .. _zone, 10) + return + end + + local _zonePos = mist.utils.zoneToVec3(_zone) + + ctld.beaconCount = ctld.beaconCount + 1 + + if _name == nil or _name == "" then + _name = "Beacon #" .. ctld.beaconCount + end + + if _coalition == "red" then + ctld.createRadioBeacon(_zonePos, 1, 0, _name, _batteryLife) --1440 + else + ctld.createRadioBeacon(_zonePos, 2, 2, _name, _batteryLife) --1440 + end +end + + +-- Activates a pickup zone +-- Activates a pickup zone when called from a trigger +-- EG: ctld.activatePickupZone("pickzone3") +-- This is enable pickzone3 to be used as a pickup zone for the team set +function ctld.activatePickupZone(_zoneName) + ctld.logDebug(string.format("ctld.activatePickupZone(_zoneName=%s)", ctld.p(_zoneName))) + + local _triggerZone = trigger.misc.getZone(_zoneName) -- trigger to use as reference position + + if _triggerZone == nil then + local _ship = ctld.getTransportUnit(_triggerZone) + + if _ship then + local _point = _ship:getPoint() + _triggerZone = {} + _triggerZone.point = _point + end + + end + + if _triggerZone == nil then + trigger.action.outText("CTLD.lua ERROR: Cant find zone or ship called " .. _zoneName, 10) + end + + for _, _zoneDetails in pairs(ctld.pickupZones) do + + if _zoneName == _zoneDetails[1] then + + --smoke could get messy if designer keeps calling this on an active zone, check its not active first + if _zoneDetails[4] == 1 then + -- they might have a continuous trigger so i've hidden the warning + --trigger.action.outText("CTLD.lua ERROR: Pickup Zone already active: " .. _zoneName, 10) + return + end + + _zoneDetails[4] = 1 --activate zone + + if ctld.disableAllSmoke == true then --smoke disabled + return + end + + if _zoneDetails[2] >= 0 then + + -- Trigger smoke marker + -- This will cause an overlapping smoke marker on next refreshsmoke call + -- but will only happen once + local _pos2 = { x = _triggerZone.point.x, y = _triggerZone.point.z } + local _alt = land.getHeight(_pos2) + local _pos3 = { x = _pos2.x, y = _alt, z = _pos2.y } + + trigger.action.smoke(_pos3, _zoneDetails[2]) + end + end + end +end + + +-- Deactivates a pickup zone +-- Deactivates a pickup zone when called from a trigger +-- EG: ctld.deactivatePickupZone("pickzone3") +-- This is disables pickzone3 and can no longer be used to as a pickup zone +-- These functions can be called by triggers, like if a set of buildings is used, you can trigger the zone to be 'not operational' +-- once they are destroyed +function ctld.deactivatePickupZone(_zoneName) + + local _triggerZone = trigger.misc.getZone(_zoneName) -- trigger to use as reference position + + if _triggerZone == nil then + local _ship = ctld.getTransportUnit(_triggerZone) + + if _ship then + local _point = _ship:getPoint() + _triggerZone = {} + _triggerZone.point = _point + end + + end + + if _triggerZone == nil then + trigger.action.outText("CTLD.lua ERROR: Cant find zone called " .. _zoneName, 10) + return + end + + for _, _zoneDetails in pairs(ctld.pickupZones) do + + if _zoneName == _zoneDetails[1] then + + -- i'd just ignore it if its already been deactivated + -- if _zoneDetails[4] == 0 then --this really needed?? + -- trigger.action.outText("CTLD.lua ERROR: Pickup Zone already deactiveated: " .. _zoneName, 10) + -- return + -- end + + _zoneDetails[4] = 0 --deactivate zone + end + end +end + +-- Change the remaining groups currently available for pickup at a zone +-- e.g. ctld.changeRemainingGroupsForPickupZone("pickup1", 5) -- adds 5 groups +-- ctld.changeRemainingGroupsForPickupZone("pickup1", -3) -- remove 3 groups +function ctld.changeRemainingGroupsForPickupZone(_zoneName, _amount) + local _triggerZone = trigger.misc.getZone(_zoneName) -- trigger to use as reference position + + if _triggerZone == nil then + local _ship = ctld.getTransportUnit(_triggerZone) + + if _ship then + local _point = _ship:getPoint() + _triggerZone = {} + _triggerZone.point = _point + end + + end + + if _triggerZone == nil then + trigger.action.outText("CTLD.lua ctld.changeRemainingGroupsForPickupZone ERROR: Cant find zone called " .. _zoneName, 10) + return + end + + for _, _zoneDetails in pairs(ctld.pickupZones) do + + if _zoneName == _zoneDetails[1] then + ctld.updateZoneCounter(_zoneName, _amount) + end + end + + +end + +-- Activates a Waypoint zone +-- Activates a Waypoint zone when called from a trigger +-- EG: ctld.activateWaypointZone("pickzone3") +-- This means that troops dropped within the radius of the zone will head to the center +-- of the zone instead of searching for troops +function ctld.activateWaypointZone(_zoneName) + local _triggerZone = trigger.misc.getZone(_zoneName) -- trigger to use as reference position + + + if _triggerZone == nil then + trigger.action.outText("CTLD.lua ERROR: Cant find zone called " .. _zoneName, 10) + + return + end + + for _, _zoneDetails in pairs(ctld.wpZones) do + + if _zoneName == _zoneDetails[1] then + + --smoke could get messy if designer keeps calling this on an active zone, check its not active first + if _zoneDetails[3] == 1 then + -- they might have a continuous trigger so i've hidden the warning + --trigger.action.outText("CTLD.lua ERROR: Pickup Zone already active: " .. _zoneName, 10) + return + end + + _zoneDetails[3] = 1 --activate zone + + if ctld.disableAllSmoke == true then --smoke disabled + return + end + + if _zoneDetails[2] >= 0 then + + -- Trigger smoke marker + -- This will cause an overlapping smoke marker on next refreshsmoke call + -- but will only happen once + local _pos2 = { x = _triggerZone.point.x, y = _triggerZone.point.z } + local _alt = land.getHeight(_pos2) + local _pos3 = { x = _pos2.x, y = _alt, z = _pos2.y } + + trigger.action.smoke(_pos3, _zoneDetails[2]) + end + end + end +end + + +-- Deactivates a Waypoint zone +-- Deactivates a Waypoint zone when called from a trigger +-- EG: ctld.deactivateWaypointZone("wpzone3") +-- This disables wpzone3 so that troops dropped in this zone will search for troops as normal +-- These functions can be called by triggers +function ctld.deactivateWaypointZone(_zoneName) + + local _triggerZone = trigger.misc.getZone(_zoneName) + + if _triggerZone == nil then + trigger.action.outText("CTLD.lua ERROR: Cant find zone called " .. _zoneName, 10) + return + end + + for _, _zoneDetails in pairs(ctld.pickupZones) do + + if _zoneName == _zoneDetails[1] then + + _zoneDetails[3] = 0 --deactivate zone + end + end +end + +-- Continuous Trigger Function +-- Causes an AI unit with the specified name to unload troops / vehicles when +-- an enemy is detected within a specified distance +-- The enemy must have Line or Sight to the unit to be detected +function ctld.unloadInProximityToEnemy(_unitName,_distance) + + local _unit = ctld.getTransportUnit(_unitName) + + if _unit ~= nil and _unit:getPlayerName() == nil then + + -- no player name means AI! + -- the findNearest visible enemy you'd want to modify as it'll find enemies quite far away + -- limited by ctld.JTAC_maxDistance + local _nearestEnemy = ctld.findNearestVisibleEnemy(_unit,"all",_distance) + + if _nearestEnemy ~= nil then + + if ctld.troopsOnboard(_unit, true) then + ctld.deployTroops(_unit, true) + return true + end + + if ctld.unitCanCarryVehicles(_unit) and ctld.troopsOnboard(_unit, false) then + ctld.deployTroops(_unit, false) + return true + end + end + end + + return false + +end + + + +-- Unit will unload any units onboard if the unit is on the ground +-- when this function is called +function ctld.unloadTransport(_unitName) + + local _unit = ctld.getTransportUnit(_unitName) + + if _unit ~= nil then + + if ctld.troopsOnboard(_unit, true) then + ctld.unloadTroops({_unitName,true}) + end + + if ctld.unitCanCarryVehicles(_unit) and ctld.troopsOnboard(_unit, false) then + ctld.unloadTroops({_unitName,false}) + end + end + +end + +-- Loads Troops and Vehicles from a zone or picks up nearby troops or vehicles +function ctld.loadTransport(_unitName) + + local _unit = ctld.getTransportUnit(_unitName) + + if _unit ~= nil then + + ctld.loadTroopsFromZone({ _unitName, true,"",true }) + + if ctld.unitCanCarryVehicles(_unit) then + ctld.loadTroopsFromZone({ _unitName, false,"",true }) + end + + end + +end + +-- adds a callback that will be called for many actions ingame +function ctld.addCallback(_callback) + + table.insert(ctld.callbacks,_callback) + +end + +-- Spawns a sling loadable crate at a Trigger Zone +-- +-- Weights can be found in the ctld.spawnableCrates list +-- e.g. ctld.spawnCrateAtZone("red", 500,"triggerzone1") -- spawn a humvee at triggerzone 1 for red side +-- e.g. ctld.spawnCrateAtZone("blue", 505,"triggerzone1") -- spawn a tow humvee at triggerzone1 for blue side +-- +function ctld.spawnCrateAtZone(_side, _weight,_zone) + local _spawnTrigger = trigger.misc.getZone(_zone) -- trigger to use as reference position + + if _spawnTrigger == nil then + trigger.action.outText("CTLD.lua ERROR: Cant find zone called " .. _zone, 10) + return + end + + local _crateType = ctld.crateLookupTable[tostring(_weight)] + + if _crateType == nil then + trigger.action.outText("CTLD.lua ERROR: Cant find crate with weight " .. _weight, 10) + return + end + + local _country + if _side == "red" then + _side = 1 + _country = 0 + else + _side = 2 + _country = 2 + end + + local _pos2 = { x = _spawnTrigger.point.x, y = _spawnTrigger.point.z } + local _alt = land.getHeight(_pos2) + local _point = { x = _pos2.x, y = _alt, z = _pos2.y } + + local _unitId = ctld.getNextUnitId() + + local _name = string.format("%s #%i", _crateType.desc, _unitId) + + local _spawnedCrate = ctld.spawnCrateStatic(_country, _unitId, _point, _name, _crateType.weight,_side) + +end + +-- Spawns a sling loadable crate at a Point +-- +-- Weights can be found in the ctld.spawnableCrates list +-- Points can be made by hand or obtained from a Unit position by Unit.getByName("PilotName"):getPoint() +-- e.g. ctld.spawnCrateAtZone("red", 500,{x=1,y=2,z=3}) -- spawn a humvee at triggerzone 1 for red side at a specified point +-- e.g. ctld.spawnCrateAtZone("blue", 505,{x=1,y=2,z=3}) -- spawn a tow humvee at triggerzone1 for blue side at a specified point +-- +-- +function ctld.spawnCrateAtPoint(_side, _weight,_point) + + + local _crateType = ctld.crateLookupTable[tostring(_weight)] + + if _crateType == nil then + trigger.action.outText("CTLD.lua ERROR: Cant find crate with weight " .. _weight, 10) + return + end + + local _country + if _side == "red" then + _side = 1 + _country = 0 + else + _side = 2 + _country = 2 + end + + local _unitId = ctld.getNextUnitId() + + local _name = string.format("%s #%i", _crateType.desc, _unitId) + + local _spawnedCrate = ctld.spawnCrateStatic(_country, _unitId, _point, _name, _crateType.weight,_side) + +end + +-- *************************************************************** +-- **************** BE CAREFUL BELOW HERE ************************ +-- *************************************************************** + +--- Tells CTLD What multipart AA Systems there are and what parts they need +-- A New system added here also needs the launcher added +ctld.AASystemTemplate = { + + { + name = "HAWK AA System", + count = 4, + parts = { + {name = "Hawk ln", desc = "HAWK Launcher", launcher = true}, + {name = "Hawk tr", desc = "HAWK Track Radar"}, + {name = "Hawk sr", desc = "HAWK Search Radar"}, + {name = "Hawk pcp", desc = "HAWK PCP"}, + {name = "Hawk cwar", desc = "HAWK CWAR"}, + }, + repair = "HAWK Repair", + }, + { + name = "Patriot AA System", + count = 4, + parts = { + {name = "Patriot ln", desc = "Patriot Launcher", launcher = true}, + {name = "Patriot ECS", desc = "Patriot Control Unit"}, + {name = "Patriot str", desc = "Patriot Search and Track Radar"}, + }, + repair = "Patriot Repair", + }, + { + name = "BUK AA System", + count = 3, + parts = { + {name = "SA-11 Buk LN 9A310M1", desc = "BUK Launcher" , launcher = true}, + {name = "SA-11 Buk CC 9S470M1", desc = "BUK CC Radar"}, + {name = "SA-11 Buk SR 9S18M1", desc = "BUK Search Radar"}, + }, + repair = "BUK Repair", + }, + { + name = "KUB AA System", + count = 2, + parts = { + {name = "Kub 2P25 ln", desc = "KUB Launcher", launcher = true}, + {name = "Kub 1S91 str", desc = "KUB Radar"}, + }, + repair = "KUB Repair", + }, +} + + +ctld.crateWait = {} +ctld.crateMove = {} + +---------------- INTERNAL FUNCTIONS ---------------- +--- +--- +------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Utility methods +------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- print an object for a debugging log +function ctld.p(o, level) + local MAX_LEVEL = 20 + if level == nil then level = 0 end + if level > MAX_LEVEL then + ctld.logError("max depth reached in ctld.p : "..tostring(MAX_LEVEL)) + return "" + end + local text = "" + if (type(o) == "table") then + text = "\n" + for key,value in pairs(o) do + for i=0, level do + text = text .. " " + end + text = text .. ".".. key.."="..ctld.p(value, level+1) .. "\n" + end + elseif (type(o) == "function") then + text = "[function]" + elseif (type(o) == "boolean") then + if o == true then + text = "[true]" + else + text = "[false]" + end + else + if o == nil then + text = "[nil]" + else + text = tostring(o) + end + end + return text +end + +function ctld.logError(message) + env.info(" E - " .. ctld.Id .. message) +end + +function ctld.logInfo(message) + env.info(" I - " .. ctld.Id .. message) +end + +function ctld.logDebug(message) + if message and ctld.Debug then + env.info(" D - " .. ctld.Id .. message) + end +end + +function ctld.logTrace(message) + if message and ctld.Trace then + env.info(" T - " .. ctld.Id .. message) + end +end + +ctld.nextUnitId = 1; +ctld.getNextUnitId = function() + ctld.nextUnitId = ctld.nextUnitId + 1 + + return ctld.nextUnitId +end + +ctld.nextGroupId = 1; + +ctld.getNextGroupId = function() + ctld.nextGroupId = ctld.nextGroupId + 1 + + return ctld.nextGroupId +end + +function ctld.getTransportUnit(_unitName) + + if _unitName == nil then + return nil + end + + local _heli = Unit.getByName(_unitName) + + if _heli ~= nil and _heli:isActive() and _heli:getLife() > 0 then + + return _heli + end + + return nil +end + +function ctld.spawnCrateStatic(_country, _unitId, _point, _name, _weight,_side) + + local _crate + local _spawnedCrate + + if ctld.staticBugWorkaround and ctld.slingLoad == false then + local _groupId = ctld.getNextGroupId() + local _groupName = "Crate Group #".._groupId + + local _group = { + ["visible"] = false, + -- ["groupId"] = _groupId, + ["hidden"] = false, + ["units"] = {}, + -- ["y"] = _positions[1].z, + -- ["x"] = _positions[1].x, + ["name"] = _groupName, + ["task"] = {}, + } + + _group.units[1] = ctld.createUnit(_point.x , _point.z , 0, {type="UAZ-469",name=_name,unitId=_unitId}) + + --switch to MIST + _group.category = Group.Category.GROUND; + _group.country = _country; + + local _spawnedGroup = Group.getByName(mist.dynAdd(_group).name) + + -- Turn off AI + trigger.action.setGroupAIOff(_spawnedGroup) + + _spawnedCrate = Unit.getByName(_name) + else + + if ctld.slingLoad then + _crate = mist.utils.deepCopy(ctld.spawnableCratesModel_sling) + _crate["canCargo"] = true + else + _crate = mist.utils.deepCopy(ctld.spawnableCratesModel_load) + _crate["canCargo"] = false + end + + _crate["y"] = _point.z + _crate["x"] = _point.x + _crate["mass"] = _weight + _crate["name"] = _name + _crate["heading"] = 0 + _crate["country"] = _country + + ctld.logTrace(string.format("_crate=%s", ctld.p(_crate))) + mist.dynAddStatic(_crate) + + _spawnedCrate = StaticObject.getByName(_crate["name"]) + end + + + local _crateType = ctld.crateLookupTable[tostring(_weight)] + + if _side == 1 then + ctld.spawnedCratesRED[_name] =_crateType + else + ctld.spawnedCratesBLUE[_name] = _crateType + end + + return _spawnedCrate +end + +function ctld.spawnFOBCrateStatic(_country, _unitId, _point, _name) + + local _crate = { + ["category"] = "Fortifications", + ["shape_name"] = "konteiner_red1", + ["type"] = "Container red 1", + -- ["unitId"] = _unitId, + ["y"] = _point.z, + ["x"] = _point.x, + ["name"] = _name, + ["canCargo"] = false, + ["heading"] = 0, + } + + _crate["country"] = _country + + mist.dynAddStatic(_crate) + + local _spawnedCrate = StaticObject.getByName(_crate["name"]) + --local _spawnedCrate = coalition.addStaticObject(_country, _crate) + + return _spawnedCrate +end + + +function ctld.spawnFOB(_country, _unitId, _point, _name) + + local _crate = { + ["category"] = "Fortifications", + ["type"] = "outpost", + -- ["unitId"] = _unitId, + ["y"] = _point.z, + ["x"] = _point.x, + ["name"] = _name, + ["canCargo"] = false, + ["heading"] = 0, + } + + _crate["country"] = _country + mist.dynAddStatic(_crate) + local _spawnedCrate = StaticObject.getByName(_crate["name"]) + --local _spawnedCrate = coalition.addStaticObject(_country, _crate) + + local _id = ctld.getNextUnitId() + local _tower = { + ["type"] = "house2arm", + -- ["unitId"] = _id, + ["rate"] = 100, + ["y"] = _point.z + -36.57142857, + ["x"] = _point.x + 14.85714286, + ["name"] = "FOB Watchtower #" .. _id, + ["category"] = "Fortifications", + ["canCargo"] = false, + ["heading"] = 0, + } + --coalition.addStaticObject(_country, _tower) + _tower["country"] = _country + + mist.dynAddStatic(_tower) + + return _spawnedCrate +end + + +function ctld.spawnCrate(_arguments) + + local _status, _err = pcall(function(_args) + + -- use the cargo weight to guess the type of unit as no way to add description :( + + local _crateType = ctld.crateLookupTable[tostring(_args[2])] + local _heli = ctld.getTransportUnit(_args[1]) + + if _crateType ~= nil and _heli ~= nil and ctld.inAir(_heli) == false then + + if ctld.inLogisticsZone(_heli) == false then + + ctld.displayMessageToGroup(_heli, "You are not close enough to friendly logistics to get a crate!", 10) + + return + end + + if ctld.isJTACUnitType(_crateType.unit) then + + local _limitHit = false + + if _heli:getCoalition() == 1 then + + if ctld.JTAC_LIMIT_RED == 0 then + _limitHit = true + else + ctld.JTAC_LIMIT_RED = ctld.JTAC_LIMIT_RED - 1 + end + else + if ctld.JTAC_LIMIT_BLUE == 0 then + _limitHit = true + else + ctld.JTAC_LIMIT_BLUE = ctld.JTAC_LIMIT_BLUE - 1 + end + end + + if _limitHit then + ctld.displayMessageToGroup(_heli, "No more JTAC Crates Left!", 10) + return + end + end + + local _position = _heli:getPosition() + + -- check crate spam + if _heli:getPlayerName() ~= nil and ctld.crateWait[_heli:getPlayerName()] and ctld.crateWait[_heli:getPlayerName()] > timer.getTime() then + + ctld.displayMessageToGroup(_heli,"Sorry you must wait "..(ctld.crateWait[_heli:getPlayerName()] - timer.getTime()).. " seconds before you can get another crate", 20) + return + end + + if _heli:getPlayerName() ~= nil then + ctld.crateWait[_heli:getPlayerName()] = timer.getTime() + ctld.crateWaitTime + end + -- trigger.action.outText("Spawn Crate".._args[1].." ".._args[2],10) + + local _heli = ctld.getTransportUnit(_args[1]) + + local _point = ctld.getPointAt12Oclock(_heli, 30) + + local _unitId = ctld.getNextUnitId() + + local _side = _heli:getCoalition() + + local _name = string.format("%s #%i", _crateType.desc, _unitId) + + local _spawnedCrate = ctld.spawnCrateStatic(_heli:getCountry(), _unitId, _point, _name, _crateType.weight,_side) + + -- add to move table + ctld.crateMove[_name] = _name + + ctld.displayMessageToGroup(_heli, string.format("A %s crate weighing %s kg has been brought out and is at your 12 o'clock ", _crateType.desc, _crateType.weight), 20) + + else + env.info("Couldn't find crate item to spawn") + end + end, _arguments) + + if (not _status) then + env.error(string.format("CTLD ERROR: %s", _err)) + end +end + +function ctld.getPointAt12Oclock(_unit, _offset) + + local _position = _unit:getPosition() + local _angle = math.atan2(_position.x.z, _position.x.x) + local _xOffset = math.cos(_angle) * _offset + local _yOffset = math.sin(_angle) * _offset + + local _point = _unit:getPoint() + return { x = _point.x + _xOffset, z = _point.z + _yOffset, y = _point.y } +end + +function ctld.troopsOnboard(_heli, _troops) + + if ctld.inTransitTroops[_heli:getName()] ~= nil then + + local _onboard = ctld.inTransitTroops[_heli:getName()] + + if _troops then + + if _onboard.troops ~= nil and _onboard.troops.units ~= nil and #_onboard.troops.units > 0 then + return true + else + return false + end + else + + if _onboard.vehicles ~= nil and _onboard.vehicles.units ~= nil and #_onboard.vehicles.units > 0 then + return true + else + return false + end + end + + else + return false + end +end + +-- if its dropped by AI then there is no player name so return the type of unit +function ctld.getPlayerNameOrType(_heli) + + if _heli:getPlayerName() == nil then + + return _heli:getTypeName() + else + return _heli:getPlayerName() + end +end + +function ctld.inExtractZone(_heli) + + local _heliPoint = _heli:getPoint() + + for _, _zoneDetails in pairs(ctld.extractZones) do + + --get distance to center + local _dist = ctld.getDistance(_heliPoint, _zoneDetails.point) + + if _dist <= _zoneDetails.radius then + return _zoneDetails + end + end + + return false +end + +-- safe to fast rope if speed is less than 0.5 Meters per second +function ctld.safeToFastRope(_heli) + + if ctld.enableFastRopeInsertion == false then + return false + end + + --landed or speed is less than 8 km/h and height is less than fast rope height + if (ctld.inAir(_heli) == false or (ctld.heightDiff(_heli) <= ctld.fastRopeMaximumHeight + 3.0 and mist.vec.mag(_heli:getVelocity()) < 2.2)) then + return true + end +end + +function ctld.metersToFeet(_meters) + + local _feet = _meters * 3.2808399 + + return mist.utils.round(_feet) +end + +function ctld.inAir(_heli) + + if _heli:inAir() == false then + return false + end + + -- less than 5 cm/s a second so landed + -- BUT AI can hold a perfect hover so ignore AI + if mist.vec.mag(_heli:getVelocity()) < 0.05 and _heli:getPlayerName() ~= nil then + return false + end + return true +end + +function ctld.deployTroops(_heli, _troops) + + local _onboard = ctld.inTransitTroops[_heli:getName()] + + -- deloy troops + if _troops then + if _onboard.troops ~= nil and #_onboard.troops.units > 0 then + if ctld.inAir(_heli) == false or ctld.safeToFastRope(_heli) then + + -- check we're not in extract zone + local _extractZone = ctld.inExtractZone(_heli) + + if _extractZone == false then + + local _droppedTroops = ctld.spawnDroppedGroup(_heli:getPoint(), _onboard.troops, false) + ctld.logTrace(string.format("_onboard.troops=%s", ctld.p(_onboard.troops))) + if _onboard.troops.jtac or _droppedTroops:getName():lower():find("jtac") then + local _code = table.remove(ctld.jtacGeneratedLaserCodes, 1) + ctld.logTrace(string.format("_code=%s", ctld.p(_code))) + table.insert(ctld.jtacGeneratedLaserCodes, _code) + ctld.logTrace(string.format("_droppedTroops:getName()=%s", ctld.p(_droppedTroops:getName()))) + ctld.JTACAutoLase(_droppedTroops:getName(), _code) + end + + if _heli:getCoalition() == 1 then + + table.insert(ctld.droppedTroopsRED, _droppedTroops:getName()) + else + + table.insert(ctld.droppedTroopsBLUE, _droppedTroops:getName()) + end + + ctld.inTransitTroops[_heli:getName()].troops = nil + ctld.adaptWeightToCargo(_heli:getName()) + + if ctld.inAir(_heli) then + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " fast-ropped troops from " .. _heli:getTypeName() .. " into combat", 10) + else + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " dropped troops from " .. _heli:getTypeName() .. " into combat", 10) + end + + ctld.processCallback({unit = _heli, unloaded = _droppedTroops, action = "dropped_troops"}) + + + else + --extract zone! + local _droppedCount = trigger.misc.getUserFlag(_extractZone.flag) + + _droppedCount = (#_onboard.troops.units) + _droppedCount + + trigger.action.setUserFlag(_extractZone.flag, _droppedCount) + + ctld.inTransitTroops[_heli:getName()].troops = nil + ctld.adaptWeightToCargo(_heli:getName()) + + if ctld.inAir(_heli) then + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " troops fast-ropped from " .. _heli:getTypeName() .. " into " .. _extractZone.name, 10) + else + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " troops dropped from " .. _heli:getTypeName() .. " into " .. _extractZone.name, 10) + end + end + else + ctld.displayMessageToGroup(_heli, "Too high or too fast to drop troops into combat! Hover below " .. ctld.metersToFeet(ctld.fastRopeMaximumHeight) .. " feet or land.", 10) + end + end + + else + if ctld.inAir(_heli) == false then + if _onboard.vehicles ~= nil and #_onboard.vehicles.units > 0 then + + local _droppedVehicles = ctld.spawnDroppedGroup(_heli:getPoint(), _onboard.vehicles, true) + + if _heli:getCoalition() == 1 then + + table.insert(ctld.droppedVehiclesRED, _droppedVehicles:getName()) + else + + table.insert(ctld.droppedVehiclesBLUE, _droppedVehicles:getName()) + end + + ctld.inTransitTroops[_heli:getName()].vehicles = nil + ctld.adaptWeightToCargo(_heli:getName()) + + ctld.processCallback({unit = _heli, unloaded = _droppedVehicles, action = "dropped_vehicles"}) + + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " dropped vehicles from " .. _heli:getTypeName() .. " into combat", 10) + end + end + end +end + +function ctld.insertIntoTroopsArray(_troopType,_count,_troopArray,_troopName) + + for _i = 1, _count do + local _unitId = ctld.getNextUnitId() + table.insert(_troopArray, { type = _troopType, unitId = _unitId, name = string.format("Dropped %s #%i", _troopName or _troopType, _unitId) }) + end + + return _troopArray + +end + + +function ctld.generateTroopTypes(_side, _countOrTemplate, _country) + local _troops = {} + local _weight = 0 + local _hasJTAC = false + + local function getSoldiersWeight(count, additionalWeight) + local _weight = 0 + for i = 1, count do + local _soldierWeight = math.random(90, 120) * ctld.SOLDIER_WEIGHT / 100 + ctld.logTrace(string.format("_soldierWeight=%s", ctld.p(_soldierWeight))) + _weight = _weight + _soldierWeight + ctld.KIT_WEIGHT + additionalWeight + end + return _weight + end + + if type(_countOrTemplate) == "table" then + + if _countOrTemplate.aa then + ctld.logTrace(string.format("_countOrTemplate.aa=%s", ctld.p(_countOrTemplate.aa))) + if _side == 2 then + _troops = ctld.insertIntoTroopsArray("Soldier stinger",_countOrTemplate.aa,_troops) + else + _troops = ctld.insertIntoTroopsArray("SA-18 Igla manpad",_countOrTemplate.aa,_troops) + end + _weight = _weight + getSoldiersWeight(_countOrTemplate.aa, ctld.MANPAD_WEIGHT) + ctld.logTrace(string.format("_weight=%s", ctld.p(_weight))) + end + + if _countOrTemplate.inf then + ctld.logTrace(string.format("_countOrTemplate.inf=%s", ctld.p(_countOrTemplate.inf))) + if _side == 2 then + _troops = ctld.insertIntoTroopsArray("Soldier M4 GRG",_countOrTemplate.inf,_troops) + else + _troops = ctld.insertIntoTroopsArray("Soldier AK",_countOrTemplate.inf,_troops) + end + _weight = _weight + getSoldiersWeight(_countOrTemplate.inf, ctld.RIFLE_WEIGHT) + ctld.logTrace(string.format("_weight=%s", ctld.p(_weight))) + end + + if _countOrTemplate.mg then + ctld.logTrace(string.format("_countOrTemplate.mg=%s", ctld.p(_countOrTemplate.mg))) + if _side == 2 then + _troops = ctld.insertIntoTroopsArray("Soldier M249",_countOrTemplate.mg,_troops) + else + _troops = ctld.insertIntoTroopsArray("Paratrooper AKS-74",_countOrTemplate.mg,_troops) + end + _weight = _weight + getSoldiersWeight(_countOrTemplate.mg, ctld.MG_WEIGHT) + ctld.logTrace(string.format("_weight=%s", ctld.p(_weight))) + end + + if _countOrTemplate.at then + ctld.logTrace(string.format("_countOrTemplate.at=%s", ctld.p(_countOrTemplate.at))) + _troops = ctld.insertIntoTroopsArray("Paratrooper RPG-16",_countOrTemplate.at,_troops) + _weight = _weight + getSoldiersWeight(_countOrTemplate.at, ctld.RPG_WEIGHT) + ctld.logTrace(string.format("_weight=%s", ctld.p(_weight))) + end + + if _countOrTemplate.mortar then + ctld.logTrace(string.format("_countOrTemplate.mortar=%s", ctld.p(_countOrTemplate.mortar))) + _troops = ctld.insertIntoTroopsArray("2B11 mortar",_countOrTemplate.mortar,_troops) + _weight = _weight + getSoldiersWeight(_countOrTemplate.mortar, ctld.MORTAR_WEIGHT) + ctld.logTrace(string.format("_weight=%s", ctld.p(_weight))) + end + + if _countOrTemplate.jtac then + ctld.logTrace(string.format("_countOrTemplate.jtac=%s", ctld.p(_countOrTemplate.jtac))) + if _side == 2 then + _troops = ctld.insertIntoTroopsArray("Soldier M4 GRG",_countOrTemplate.jtac,_troops, "JTAC") + else + _troops = ctld.insertIntoTroopsArray("Soldier AK",_countOrTemplate.jtac,_troops, "JTAC") + end + _hasJTAC = true + _weight = _weight + getSoldiersWeight(_countOrTemplate.jtac, ctld.JTAC_WEIGHT + ctld.RIFLE_WEIGHT) + ctld.logTrace(string.format("_weight=%s", ctld.p(_weight))) + end + + else + for _i = 1, _countOrTemplate do + + local _unitType = "Soldier AK" + + if _side == 2 then + if _i <=2 then + _unitType = "Soldier M249" + _weight = _weight + getSoldiersWeight(1, ctld.MG_WEIGHT) + ctld.logTrace(string.format("_unitType=%s, _weight=%s", ctld.p(_unitType), ctld.p(_weight))) + elseif ctld.spawnRPGWithCoalition and _i > 2 and _i <= 4 then + _unitType = "Paratrooper RPG-16" + _weight = _weight + getSoldiersWeight(1, ctld.RPG_WEIGHT) + ctld.logTrace(string.format("_unitType=%s, _weight=%s", ctld.p(_unitType), ctld.p(_weight))) + elseif ctld.spawnStinger and _i > 4 and _i <= 5 then + _unitType = "Soldier stinger" + _weight = _weight + getSoldiersWeight(1, ctld.MANPAD_WEIGHT) + ctld.logTrace(string.format("_unitType=%s, _weight=%s", ctld.p(_unitType), ctld.p(_weight))) + else + _unitType = "Soldier M4 GRG" + _weight = _weight + getSoldiersWeight(1, ctld.RIFLE_WEIGHT) + ctld.logTrace(string.format("_unitType=%s, _weight=%s", ctld.p(_unitType), ctld.p(_weight))) + end + else + if _i <=2 then + _unitType = "Paratrooper AKS-74" + _weight = _weight + getSoldiersWeight(1, ctld.MG_WEIGHT) + ctld.logTrace(string.format("_unitType=%s, _weight=%s", ctld.p(_unitType), ctld.p(_weight))) + elseif ctld.spawnRPGWithCoalition and _i > 2 and _i <= 4 then + _unitType = "Paratrooper RPG-16" + _weight = _weight + getSoldiersWeight(1, ctld.RPG_WEIGHT) + ctld.logTrace(string.format("_unitType=%s, _weight=%s", ctld.p(_unitType), ctld.p(_weight))) + elseif ctld.spawnStinger and _i > 4 and _i <= 5 then + _unitType = "SA-18 Igla manpad" + _weight = _weight + getSoldiersWeight(1, ctld.MANPAD_WEIGHT) + ctld.logTrace(string.format("_unitType=%s, _weight=%s", ctld.p(_unitType), ctld.p(_weight))) + else + _unitType = "Infantry AK" + _weight = _weight + getSoldiersWeight(1, ctld.RIFLE_WEIGHT) + ctld.logTrace(string.format("_unitType=%s, _weight=%s", ctld.p(_unitType), ctld.p(_weight))) + end + end + + local _unitId = ctld.getNextUnitId() + + _troops[_i] = { type = _unitType, unitId = _unitId, name = string.format("Dropped %s #%i", _unitType, _unitId) } + end + end + + local _groupId = ctld.getNextGroupId() + local _groupName = "Dropped Group" + if _hasJTAC then + _groupName = "Dropped JTAC Group" + end + local _details = { units = _troops, groupId = _groupId, groupName = string.format("%s %i", _groupName, _groupId), side = _side, country = _country, weight = _weight, jtac = _hasJTAC } + ctld.logTrace(string.format("total weight=%s", ctld.p(_weight))) + + return _details +end + +--Special F10 function for players for troops +function ctld.unloadExtractTroops(_args) + + local _heli = ctld.getTransportUnit(_args[1]) + + if _heli == nil then + return false + end + + + local _extract = nil + if not ctld.inAir(_heli) then + if _heli:getCoalition() == 1 then + _extract = ctld.findNearestGroup(_heli, ctld.droppedTroopsRED) + else + _extract = ctld.findNearestGroup(_heli, ctld.droppedTroopsBLUE) + end + + end + + if _extract ~= nil and not ctld.troopsOnboard(_heli, true) then + -- search for nearest troops to pickup + return ctld.extractTroops({_heli:getName(), true}) + else + return ctld.unloadTroops({_heli:getName(),true,true}) + end + + +end + +-- load troops onto vehicle +function ctld.loadTroops(_heli, _troops, _numberOrTemplate) + + -- load troops + vehicles if c130 or herc + -- "M1045 HMMWV TOW" + -- "M1043 HMMWV Armament" + local _onboard = ctld.inTransitTroops[_heli:getName()] + + --number doesnt apply to vehicles + if _numberOrTemplate == nil or (type(_numberOrTemplate) ~= "table" and type(_numberOrTemplate) ~= "number") then + _numberOrTemplate = ctld.numberOfTroops + end + + if _onboard == nil then + _onboard = { troops = {}, vehicles = {} } + end + + local _list + if _heli:getCoalition() == 1 then + _list = ctld.vehiclesForTransportRED + else + _list = ctld.vehiclesForTransportBLUE + end + + ctld.logTrace(string.format("_troops=%s", ctld.p(_troops))) + if _troops then + _onboard.troops = ctld.generateTroopTypes(_heli:getCoalition(), _numberOrTemplate, _heli:getCountry()) + ctld.logTrace(string.format("_onboard.troops=%s", ctld.p(_onboard.troops))) + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " loaded troops into " .. _heli:getTypeName(), 10) + + ctld.processCallback({unit = _heli, onboard = _onboard.troops, action = "load_troops"}) + else + + _onboard.vehicles = ctld.generateVehiclesForTransport(_heli:getCoalition(), _heli:getCountry()) + + local _count = #_list + + ctld.processCallback({unit = _heli, onboard = _onboard.vehicles, action = "load_vehicles"}) + + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " loaded " .. _count .. " vehicles into " .. _heli:getTypeName(), 10) + end + + ctld.inTransitTroops[_heli:getName()] = _onboard + ctld.logTrace(string.format("ctld.inTransitTroops=%s", ctld.p(ctld.inTransitTroops[_heli:getName()]))) + ctld.adaptWeightToCargo(_heli:getName()) +end + +function ctld.generateVehiclesForTransport(_side, _country) + + local _vehicles = {} + local _list + if _side == 1 then + _list = ctld.vehiclesForTransportRED + else + _list = ctld.vehiclesForTransportBLUE + end + + + for _i, _type in ipairs(_list) do + + local _unitId = ctld.getNextUnitId() + local _weight = ctld.vehiclesWeight[_type] or 2500 + _vehicles[_i] = { type = _type, unitId = _unitId, name = string.format("Dropped %s #%i", _type, _unitId), weight = _weight } + end + + + local _groupId = ctld.getNextGroupId() + local _details = { units = _vehicles, groupId = _groupId, groupName = string.format("Dropped Group %i", _groupId), side = _side, country = _country } + + return _details +end + +function ctld.loadUnloadFOBCrate(_args) + + local _heli = ctld.getTransportUnit(_args[1]) + local _troops = _args[2] + + if _heli == nil then + return + end + + if ctld.inAir(_heli) == true then + return + end + + + local _side = _heli:getCoalition() + + local _inZone = ctld.inLogisticsZone(_heli) + local _crateOnboard = ctld.inTransitFOBCrates[_heli:getName()] ~= nil + + if _inZone == false and _crateOnboard == true then + + ctld.inTransitFOBCrates[_heli:getName()] = nil + + local _position = _heli:getPosition() + + --try to spawn at 6 oclock to us + local _angle = math.atan2(_position.x.z, _position.x.x) + local _xOffset = math.cos(_angle) * -60 + local _yOffset = math.sin(_angle) * -60 + + local _point = _heli:getPoint() + + local _side = _heli:getCoalition() + + local _unitId = ctld.getNextUnitId() + + local _name = string.format("FOB Crate #%i", _unitId) + + local _spawnedCrate = ctld.spawnFOBCrateStatic(_heli:getCountry(), ctld.getNextUnitId(), { x = _point.x + _xOffset, z = _point.z + _yOffset }, _name) + + if _side == 1 then + ctld.droppedFOBCratesRED[_name] = _name + else + ctld.droppedFOBCratesBLUE[_name] = _name + end + + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " delivered a FOB Crate", 10) + + ctld.displayMessageToGroup(_heli, "Delivered FOB Crate 60m at 6'oclock to you", 10) + + elseif _inZone == true and _crateOnboard == true then + + ctld.displayMessageToGroup(_heli, "FOB Crate dropped back to base", 10) + + ctld.inTransitFOBCrates[_heli:getName()] = nil + + elseif _inZone == true and _crateOnboard == false then + ctld.displayMessageToGroup(_heli, "FOB Crate Loaded", 10) + + ctld.inTransitFOBCrates[_heli:getName()] = true + + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " loaded a FOB Crate ready for delivery!", 10) + + else + + -- nearest Crate + local _crates = ctld.getCratesAndDistance(_heli) + local _nearestCrate = ctld.getClosestCrate(_heli, _crates, "FOB") + + if _nearestCrate ~= nil and _nearestCrate.dist < 150 then + + ctld.displayMessageToGroup(_heli, "FOB Crate Loaded", 10) + ctld.inTransitFOBCrates[_heli:getName()] = true + + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " loaded a FOB Crate ready for delivery!", 10) + + if _side == 1 then + ctld.droppedFOBCratesRED[_nearestCrate.crateUnit:getName()] = nil + else + ctld.droppedFOBCratesBLUE[_nearestCrate.crateUnit:getName()] = nil + end + + --remove + _nearestCrate.crateUnit:destroy() + + else + ctld.displayMessageToGroup(_heli, "There are no friendly logistic units nearby to load a FOB crate from!", 10) + end + end +end + +function ctld.loadTroopsFromZone(_args) + + local _heli = ctld.getTransportUnit(_args[1]) + local _troops = _args[2] + local _groupTemplate = _args[3] or "" + local _allowExtract = _args[4] + + if _heli == nil then + return false + end + + local _zone = ctld.inPickupZone(_heli) + + if ctld.troopsOnboard(_heli, _troops) then + + if _troops then + ctld.displayMessageToGroup(_heli, "You already have troops onboard.", 10) + else + ctld.displayMessageToGroup(_heli, "You already have vehicles onboard.", 10) + end + + return false + end + + local _extract + + if _allowExtract then + -- first check for extractable troops regardless of if we're in a zone or not + if _troops then + if _heli:getCoalition() == 1 then + _extract = ctld.findNearestGroup(_heli, ctld.droppedTroopsRED) + else + _extract = ctld.findNearestGroup(_heli, ctld.droppedTroopsBLUE) + end + else + + if _heli:getCoalition() == 1 then + _extract = ctld.findNearestGroup(_heli, ctld.droppedVehiclesRED) + else + _extract = ctld.findNearestGroup(_heli, ctld.droppedVehiclesBLUE) + end + end + end + + if _extract ~= nil then + -- search for nearest troops to pickup + return ctld.extractTroops({_heli:getName(), _troops}) + elseif _zone.inZone == true then + + if _zone.limit - 1 >= 0 then + -- decrease zone counter by 1 + ctld.updateZoneCounter(_zone.index, -1) + + ctld.loadTroops(_heli, _troops,_groupTemplate) + + return true + else + ctld.displayMessageToGroup(_heli, "This area has no more reinforcements available!", 20) + + return false + end + + else + if _allowExtract then + ctld.displayMessageToGroup(_heli, "You are not in a pickup zone and no one is nearby to extract", 10) + else + ctld.displayMessageToGroup(_heli, "You are not in a pickup zone", 10) + end + + return false + end +end + + + +function ctld.unloadTroops(_args) + + local _heli = ctld.getTransportUnit(_args[1]) + local _troops = _args[2] + + if _heli == nil then + return false + end + + local _zone = ctld.inPickupZone(_heli) + if not ctld.troopsOnboard(_heli, _troops) then + + ctld.displayMessageToGroup(_heli, "No one to unload", 10) + + return false + else + + -- troops must be onboard to get here + if _zone.inZone == true then + + if _troops then + ctld.displayMessageToGroup(_heli, "Dropped troops back to base", 20) + + ctld.processCallback({unit = _heli, unloaded = ctld.inTransitTroops[_heli:getName()].troops, action = "unload_troops_zone"}) + + ctld.inTransitTroops[_heli:getName()].troops = nil + + else + ctld.displayMessageToGroup(_heli, "Dropped vehicles back to base", 20) + + ctld.processCallback({unit = _heli, unloaded = ctld.inTransitTroops[_heli:getName()].vehicles, action = "unload_vehicles_zone"}) + + ctld.inTransitTroops[_heli:getName()].vehicles = nil + end + + ctld.adaptWeightToCargo(_heli:getName()) + + -- increase zone counter by 1 + ctld.updateZoneCounter(_zone.index, 1) + + return true + + elseif ctld.troopsOnboard(_heli, _troops) then + + return ctld.deployTroops(_heli, _troops) + end + end + +end + +function ctld.extractTroops(_args) + + local _heli = ctld.getTransportUnit(_args[1]) + local _troops = _args[2] + + if _heli == nil then + return false + end + + if ctld.inAir(_heli) then + return false + end + + if ctld.troopsOnboard(_heli, _troops) then + if _troops then + ctld.displayMessageToGroup(_heli, "You already have troops onboard.", 10) + else + ctld.displayMessageToGroup(_heli, "You already have vehicles onboard.", 10) + end + + return false + end + + local _onboard = ctld.inTransitTroops[_heli:getName()] + + if _onboard == nil then + _onboard = { troops = nil, vehicles = nil } + end + + local _extracted = false + + if _troops then + + local _extractTroops + + if _heli:getCoalition() == 1 then + _extractTroops = ctld.findNearestGroup(_heli, ctld.droppedTroopsRED) + else + _extractTroops = ctld.findNearestGroup(_heli, ctld.droppedTroopsBLUE) + end + + + if _extractTroops ~= nil then + + local _limit = ctld.getTransportLimit(_heli:getTypeName()) + + local _size = #_extractTroops.group:getUnits() + + if _limit < #_extractTroops.group:getUnits() then + + ctld.displayMessageToGroup(_heli, "Sorry - The group of ".._size.." is too large to fit. \n\nLimit is ".._limit.." for ".._heli:getTypeName(), 20) + + return + end + + _onboard.troops = _extractTroops.details + _onboard.troops.weight = #_extractTroops.group:getUnits() * 130 -- default to 130kg per soldier + + if _extractTroops.group:getName():lower():find("jtac") then + _onboard.troops.jtac = true + end + + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " extracted troops in " .. _heli:getTypeName() .. " from combat", 10) + + if _heli:getCoalition() == 1 then + ctld.droppedTroopsRED[_extractTroops.group:getName()] = nil + else + ctld.droppedTroopsBLUE[_extractTroops.group:getName()] = nil + end + + ctld.processCallback({unit = _heli, extracted = _extractTroops, action = "extract_troops"}) + + --remove + _extractTroops.group:destroy() + + _extracted = true + else + _onboard.troops = nil + ctld.displayMessageToGroup(_heli, "No extractable troops nearby!", 20) + end + + else + + local _extractVehicles + + + if _heli:getCoalition() == 1 then + + _extractVehicles = ctld.findNearestGroup(_heli, ctld.droppedVehiclesRED) + else + + _extractVehicles = ctld.findNearestGroup(_heli, ctld.droppedVehiclesBLUE) + end + + if _extractVehicles ~= nil then + _onboard.vehicles = _extractVehicles.details + + if _heli:getCoalition() == 1 then + + ctld.droppedVehiclesRED[_extractVehicles.group:getName()] = nil + else + + ctld.droppedVehiclesBLUE[_extractVehicles.group:getName()] = nil + end + + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " extracted vehicles in " .. _heli:getTypeName() .. " from combat", 10) + + ctld.processCallback({unit = _heli, extracted = _extractVehicles, action = "extract_vehicles"}) + --remove + _extractVehicles.group:destroy() + _extracted = true + + else + _onboard.vehicles = nil + ctld.displayMessageToGroup(_heli, "No extractable vehicles nearby!", 20) + end + end + + ctld.inTransitTroops[_heli:getName()] = _onboard + ctld.adaptWeightToCargo(_heli:getName()) + + return _extracted +end + + +function ctld.checkTroopStatus(_args) + local _unitName = _args[1] + --list onboard troops, if c130 + local _heli = ctld.getTransportUnit(_unitName) + + if _heli == nil then + return + end + + local _, _message = ctld.getWeightOfCargo(_unitName) + ctld.logTrace(string.format("_message=%s", ctld.p(_message))) + if _message and _message ~= "" then + ctld.displayMessageToGroup(_heli, _message, 10) + end +end + +-- Removes troops from transport when it dies +function ctld.checkTransportStatus() + + timer.scheduleFunction(ctld.checkTransportStatus, nil, timer.getTime() + 3) + + for _, _name in ipairs(ctld.transportPilotNames) do + + local _transUnit = ctld.getTransportUnit(_name) + + if _transUnit == nil then + --env.info("CTLD Transport Unit Dead event") + ctld.inTransitTroops[_name] = nil + ctld.inTransitFOBCrates[_name] = nil + ctld.inTransitSlingLoadCrates[_name] = nil + end + end +end + +function ctld.adaptWeightToCargo(unitName) + local _weight = ctld.getWeightOfCargo(unitName) + trigger.action.setUnitInternalCargo(unitName, _weight) +end + +function ctld.getWeightOfCargo(unitName) + ctld.logDebug(string.format("ctld.getWeightOfCargo(%s)", ctld.p(unitName))) + + local FOB_CRATE_WEIGHT = 800 + local _weight = 0 + local _description = "" + + -- add troops weight + if ctld.inTransitTroops[unitName] then + ctld.logTrace("ctld.inTransitTroops = true") + local _inTransit = ctld.inTransitTroops[unitName] + if _inTransit then + ctld.logTrace(string.format("_inTransit=%s", ctld.p(_inTransit))) + local _troops = _inTransit.troops + if _troops and _troops.units then + ctld.logTrace(string.format("_troops.weight=%s", ctld.p(_troops.weight))) + _description = _description .. string.format("%s troops onboard (%s kg)\n", #_troops.units, _troops.weight) + _weight = _weight + _troops.weight + end + local _vehicles = _inTransit.vehicles + if _vehicles and _vehicles.units then + for _, _unit in pairs(_vehicles.units) do + _weight = _weight + _unit.weight + end + ctld.logTrace(string.format("_weight=%s", ctld.p(_weight))) + _description = _description .. string.format("%s vehicles onboard (%s kg)\n", #_vehicles.units, _weight) + end + end + end + ctld.logTrace(string.format("with troops and vehicles : weight = %s", tostring(_weight))) + + -- add FOB crates weight + if ctld.inTransitFOBCrates[unitName] then + ctld.logTrace("ctld.inTransitFOBCrates = true") + _weight = _weight + FOB_CRATE_WEIGHT + _description = _description .. string.format("1 FOB Crate oboard (%s kg)\n", FOB_CRATE_WEIGHT) + end + ctld.logTrace(string.format("with FOB crates : weight = %s", tostring(_weight))) + + -- add simulated slingload crates weight + local _crate = ctld.inTransitSlingLoadCrates[unitName] + if _crate then + ctld.logTrace(string.format("_crate=%s", ctld.p(_crate))) + if _crate.simulatedSlingload then + ctld.logTrace(string.format("_crate.weight=%s", ctld.p(_crate.weight))) + _weight = _weight + _crate.weight + _description = _description .. string.format("1 %s crate onboard (%s kg)\n", _crate.desc, _crate.weight) + end + end + ctld.logTrace(string.format("with simulated slingload crates : weight = %s", tostring(_weight))) + if _description ~= "" then + _description = _description .. string.format("Total weight of cargo : %s kg\n", _weight) + else + _description = "No cargo." + end + ctld.logTrace(string.format("_description = %s", tostring(_description))) + + return _weight, _description +end + +function ctld.checkHoverStatus() + --ctld.logDebug(string.format("ctld.checkHoverStatus()")) + timer.scheduleFunction(ctld.checkHoverStatus, nil, timer.getTime() + 1.0) + + local _status, _result = pcall(function() + + for _, _name in ipairs(ctld.transportPilotNames) do + + local _reset = true + local _transUnit = ctld.getTransportUnit(_name) + + --only check transports that are hovering and not planes + if _transUnit ~= nil and ctld.inTransitSlingLoadCrates[_name] == nil and ctld.inAir(_transUnit) and ctld.unitCanCarryVehicles(_transUnit) == false then + + --ctld.logTrace(string.format("%s - capable of slingloading", ctld.p(_name))) + + local _crates = ctld.getCratesAndDistance(_transUnit) + --ctld.logTrace(string.format("_crates = %s", ctld.p(_crates))) + + for _, _crate in pairs(_crates) do + --ctld.logTrace(string.format("_crate = %s", ctld.p(_crate))) + if _crate.dist < ctld.maxDistanceFromCrate and _crate.details.unit ~= "FOB" then + + --check height! + local _height = _transUnit:getPoint().y - _crate.crateUnit:getPoint().y + --env.info("HEIGHT " .. _name .. " " .. _height .. " " .. _transUnit:getPoint().y .. " " .. _crate.crateUnit:getPoint().y) + -- ctld.heightDiff(_transUnit) + --env.info("HEIGHT ABOVE GROUD ".._name.." ".._height.." ".._transUnit:getPoint().y.." ".._crate.crateUnit:getPoint().y) + --ctld.logTrace(string.format("_height = %s", ctld.p(_height))) + + if _height > ctld.minimumHoverHeight and _height <= ctld.maximumHoverHeight then + + local _time = ctld.hoverStatus[_transUnit:getName()] + --ctld.logTrace(string.format("_time = %s", ctld.p(_time))) + + if _time == nil then + ctld.hoverStatus[_transUnit:getName()] = ctld.hoverTime + _time = ctld.hoverTime + else + _time = ctld.hoverStatus[_transUnit:getName()] - 1 + ctld.hoverStatus[_transUnit:getName()] = _time + end + + if _time > 0 then + ctld.displayMessageToGroup(_transUnit, "Hovering above " .. _crate.details.desc .. " crate. \n\nHold hover for " .. _time .. " seconds! \n\nIf the countdown stops you're too far away!", 10,true) + else + ctld.hoverStatus[_transUnit:getName()] = nil + ctld.displayMessageToGroup(_transUnit, "Loaded " .. _crate.details.desc .. " crate!", 10,true) + + --crates been moved once! + ctld.crateMove[_crate.crateUnit:getName()] = nil + + if _transUnit:getCoalition() == 1 then + ctld.spawnedCratesRED[_crate.crateUnit:getName()] = nil + else + ctld.spawnedCratesBLUE[_crate.crateUnit:getName()] = nil + end + + _crate.crateUnit:destroy() + + local _copiedCrate = mist.utils.deepCopy(_crate.details) + _copiedCrate.simulatedSlingload = true + --ctld.logTrace(string.format("_copiedCrate = %s", ctld.p(_copiedCrate))) + ctld.inTransitSlingLoadCrates[_name] = _copiedCrate + ctld.adaptWeightToCargo(_name) + end + + _reset = false + + break + elseif _height <= ctld.minimumHoverHeight then + ctld.displayMessageToGroup(_transUnit, "Too low to hook " .. _crate.details.desc .. " crate.\n\nHold hover for " .. ctld.hoverTime .. " seconds", 5,true) + break + else + ctld.displayMessageToGroup(_transUnit, "Too high to hook " .. _crate.details.desc .. " crate.\n\nHold hover for " .. ctld.hoverTime .. " seconds", 5, true) + break + end + end + end + end + + if _reset then + ctld.hoverStatus[_name] = nil + end + end + end) + + if (not _status) then + env.error(string.format("CTLD ERROR: %s", _result)) + end +end + +function ctld.loadNearbyCrate(_name) + local _transUnit = ctld.getTransportUnit(_name) + + if _transUnit ~= nil then + + if ctld.inAir(_transUnit) then + ctld.displayMessageToGroup(_transUnit, "You must land before you can load a crate!", 10,true) + return + end + + if ctld.inTransitSlingLoadCrates[_name] == nil then + local _crates = ctld.getCratesAndDistance(_transUnit) + + for _, _crate in pairs(_crates) do + + if _crate.dist < 50.0 then + ctld.displayMessageToGroup(_transUnit, "Loaded " .. _crate.details.desc .. " crate!", 10,true) + + if _transUnit:getCoalition() == 1 then + ctld.spawnedCratesRED[_crate.crateUnit:getName()] = nil + else + ctld.spawnedCratesBLUE[_crate.crateUnit:getName()] = nil + end + + ctld.crateMove[_crate.crateUnit:getName()] = nil + + _crate.crateUnit:destroy() + + local _copiedCrate = mist.utils.deepCopy(_crate.details) + _copiedCrate.simulatedSlingload = true + ctld.inTransitSlingLoadCrates[_name] = _copiedCrate + ctld.adaptWeightToCargo(_name) + return + end + end + + ctld.displayMessageToGroup(_transUnit, "No Crates within 50m to load!", 10,true) + + else + -- crate onboard + ctld.displayMessageToGroup(_transUnit, "You already have a "..ctld.inTransitSlingLoadCrates[_name].desc.." crate onboard!", 10,true) + end + end + + +end + +--recreates beacons to make sure they work! +function ctld.refreshRadioBeacons() + + timer.scheduleFunction(ctld.refreshRadioBeacons, nil, timer.getTime() + 30) + + + for _index, _beaconDetails in ipairs(ctld.deployedRadioBeacons) do + + --trigger.action.outTextForCoalition(_beaconDetails.coalition,_beaconDetails.text,10) + if ctld.updateRadioBeacon(_beaconDetails) == false then + + --search used frequencies + remove, add back to unused + + for _i, _freq in ipairs(ctld.usedUHFFrequencies) do + if _freq == _beaconDetails.uhf then + + table.insert(ctld.freeUHFFrequencies, _freq) + table.remove(ctld.usedUHFFrequencies, _i) + end + end + + for _i, _freq in ipairs(ctld.usedVHFFrequencies) do + if _freq == _beaconDetails.vhf then + + table.insert(ctld.freeVHFFrequencies, _freq) + table.remove(ctld.usedVHFFrequencies, _i) + end + end + + for _i, _freq in ipairs(ctld.usedFMFrequencies) do + if _freq == _beaconDetails.fm then + + table.insert(ctld.freeFMFrequencies, _freq) + table.remove(ctld.usedFMFrequencies, _i) + end + end + + --clean up beacon table + table.remove(ctld.deployedRadioBeacons, _index) + end + end +end + +function ctld.getClockDirection(_heli, _crate) + + -- Source: Helicopter Script - Thanks! + + local _position = _crate:getPosition().p -- get position of crate + local _playerPosition = _heli:getPosition().p -- get position of helicopter + local _relativePosition = mist.vec.sub(_position, _playerPosition) + + local _playerHeading = mist.getHeading(_heli) -- the rest of the code determines the 'o'clock' bearing of the missile relative to the helicopter + + local _headingVector = { x = math.cos(_playerHeading), y = 0, z = math.sin(_playerHeading) } + + local _headingVectorPerpendicular = { x = math.cos(_playerHeading + math.pi / 2), y = 0, z = math.sin(_playerHeading + math.pi / 2) } + + local _forwardDistance = mist.vec.dp(_relativePosition, _headingVector) + + local _rightDistance = mist.vec.dp(_relativePosition, _headingVectorPerpendicular) + + local _angle = math.atan2(_rightDistance, _forwardDistance) * 180 / math.pi + + if _angle < 0 then + _angle = 360 + _angle + end + _angle = math.floor(_angle * 12 / 360 + 0.5) + if _angle == 0 then + _angle = 12 + end + + return _angle +end + + +function ctld.getCompassBearing(_ref, _unitPos) + + _ref = mist.utils.makeVec3(_ref, 0) -- turn it into Vec3 if it is not already. + _unitPos = mist.utils.makeVec3(_unitPos, 0) -- turn it into Vec3 if it is not already. + + local _vec = { x = _unitPos.x - _ref.x, y = _unitPos.y - _ref.y, z = _unitPos.z - _ref.z } + + local _dir = mist.utils.getDir(_vec, _ref) + + local _bearing = mist.utils.round(mist.utils.toDegree(_dir), 0) + + return _bearing +end + +function ctld.listNearbyCrates(_args) + + local _message = "" + + local _heli = ctld.getTransportUnit(_args[1]) + + if _heli == nil then + + return -- no heli! + end + + local _crates = ctld.getCratesAndDistance(_heli) + + --sort + local _sort = function( a,b ) return a.dist < b.dist end + table.sort(_crates,_sort) + + for _, _crate in pairs(_crates) do + + if _crate.dist < 1000 and _crate.details.unit ~= "FOB" then + _message = string.format("%s\n%s crate - kg %i - %i m - %d o'clock", _message, _crate.details.desc, _crate.details.weight, _crate.dist, ctld.getClockDirection(_heli, _crate.crateUnit)) + end + end + + + local _fobMsg = "" + for _, _fobCrate in pairs(_crates) do + + if _fobCrate.dist < 1000 and _fobCrate.details.unit == "FOB" then + _fobMsg = _fobMsg .. string.format("FOB Crate - %d m - %d o'clock\n", _fobCrate.dist, ctld.getClockDirection(_heli, _fobCrate.crateUnit)) + end + end + + if _message ~= "" or _fobMsg ~= "" then + + local _txt = "" + + if _message ~= "" then + _txt = "Nearby Crates:\n" .. _message + end + + if _fobMsg ~= "" then + + if _message ~= "" then + _txt = _txt .. "\n\n" + end + + _txt = _txt .. "Nearby FOB Crates (Not Slingloadable):\n" .. _fobMsg + end + + ctld.displayMessageToGroup(_heli, _txt, 20) + + else + --no crates nearby + + local _txt = "No Nearby Crates" + + ctld.displayMessageToGroup(_heli, _txt, 20) + end +end + + +function ctld.listFOBS(_args) + + local _msg = "FOB Positions:" + + local _heli = ctld.getTransportUnit(_args[1]) + + if _heli == nil then + + return -- no heli! + end + + -- get fob positions + + local _fobs = ctld.getSpawnedFobs(_heli) + + -- now check spawned fobs + for _, _fob in ipairs(_fobs) do + _msg = string.format("%s\nFOB @ %s", _msg, ctld.getFOBPositionString(_fob)) + end + + if _msg == "FOB Positions:" then + ctld.displayMessageToGroup(_heli, "Sorry, there are no active FOBs!", 20) + else + ctld.displayMessageToGroup(_heli, _msg, 20) + end +end + +function ctld.getFOBPositionString(_fob) + + local _lat, _lon = coord.LOtoLL(_fob:getPosition().p) + + local _latLngStr = mist.tostringLL(_lat, _lon, 3, ctld.location_DMS) + + -- local _mgrsString = mist.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(_fob:getPosition().p)), 5) + + local _message = _latLngStr + + local _beaconInfo = ctld.fobBeacons[_fob:getName()] + + if _beaconInfo ~= nil then + _message = string.format("%s - %.2f KHz ", _message, _beaconInfo.vhf / 1000) + _message = string.format("%s - %.2f MHz ", _message, _beaconInfo.uhf / 1000000) + _message = string.format("%s - %.2f MHz ", _message, _beaconInfo.fm / 1000000) + end + + return _message +end + + +function ctld.displayMessageToGroup(_unit, _text, _time,_clear) + + local _groupId = ctld.getGroupId(_unit) + if _groupId then + if _clear == true then + trigger.action.outTextForGroup(_groupId, _text, _time,_clear) + else + trigger.action.outTextForGroup(_groupId, _text, _time) + end + end +end + +function ctld.heightDiff(_unit) + + local _point = _unit:getPoint() + + -- env.info("heightunit " .. _point.y) + --env.info("heightland " .. land.getHeight({ x = _point.x, y = _point.z })) + + return _point.y - land.getHeight({ x = _point.x, y = _point.z }) +end + +--includes fob crates! +function ctld.getCratesAndDistance(_heli) + + local _crates = {} + + local _allCrates + if _heli:getCoalition() == 1 then + _allCrates = ctld.spawnedCratesRED + else + _allCrates = ctld.spawnedCratesBLUE + end + + for _crateName, _details in pairs(_allCrates) do + + --get crate + local _crate = ctld.getCrateObject(_crateName) + + --in air seems buggy with crates so if in air is true, get the height above ground and the speed magnitude + if _crate ~= nil and _crate:getLife() > 0 + and (ctld.inAir(_crate) == false) then + + local _dist = ctld.getDistance(_crate:getPoint(), _heli:getPoint()) + + local _crateDetails = { crateUnit = _crate, dist = _dist, details = _details } + + table.insert(_crates, _crateDetails) + end + end + + local _fobCrates + if _heli:getCoalition() == 1 then + _fobCrates = ctld.droppedFOBCratesRED + else + _fobCrates = ctld.droppedFOBCratesBLUE + end + + for _crateName, _details in pairs(_fobCrates) do + + --get crate + local _crate = ctld.getCrateObject(_crateName) + + if _crate ~= nil and _crate:getLife() > 0 then + + local _dist = ctld.getDistance(_crate:getPoint(), _heli:getPoint()) + + local _crateDetails = { crateUnit = _crate, dist = _dist, details = { unit = "FOB" }, } + + table.insert(_crates, _crateDetails) + end + end + + return _crates +end + + +function ctld.getClosestCrate(_heli, _crates, _type) + + local _closetCrate = nil + local _shortestDistance = -1 + local _distance = 0 + + for _, _crate in pairs(_crates) do + + if (_crate.details.unit == _type or _type == nil) then + _distance = _crate.dist + + if _distance ~= nil and (_shortestDistance == -1 or _distance < _shortestDistance) then + _shortestDistance = _distance + _closetCrate = _crate + end + end + end + + return _closetCrate +end + +function ctld.findNearestAASystem(_heli,_aaSystem) + + local _closestHawkGroup = nil + local _shortestDistance = -1 + local _distance = 0 + + for _groupName, _hawkDetails in pairs(ctld.completeAASystems) do + + local _hawkGroup = Group.getByName(_groupName) + + -- env.info(_groupName..": "..mist.utils.tableShow(_hawkDetails)) + if _hawkGroup ~= nil and _hawkGroup:getCoalition() == _heli:getCoalition() and _hawkDetails[1].system.name == _aaSystem.name then + + local _units = _hawkGroup:getUnits() + + for _, _leader in pairs(_units) do + + if _leader ~= nil and _leader:getLife() > 0 then + + _distance = ctld.getDistance(_leader:getPoint(), _heli:getPoint()) + + if _distance ~= nil and (_shortestDistance == -1 or _distance < _shortestDistance) then + _shortestDistance = _distance + _closestHawkGroup = _hawkGroup + end + + break + end + end + end + end + + if _closestHawkGroup ~= nil then + + return { group = _closestHawkGroup, dist = _shortestDistance } + end + return nil +end + +function ctld.getCrateObject(_name) + local _crate + + if ctld.staticBugWorkaround then + _crate = Unit.getByName(_name) + else + _crate = StaticObject.getByName(_name) + end + return _crate +end + + + +function ctld.unpackCrates(_arguments) + + local _status, _err = pcall(function(_args) + + -- trigger.action.outText("Unpack Crates".._args[1],10) + + local _heli = ctld.getTransportUnit(_args[1]) + + if _heli ~= nil and ctld.inAir(_heli) == false then + + local _crates = ctld.getCratesAndDistance(_heli) + local _crate = ctld.getClosestCrate(_heli, _crates) + + + if ctld.inLogisticsZone(_heli) == true or ctld.farEnoughFromLogisticZone(_heli) == false then + + ctld.displayMessageToGroup(_heli, "You can't unpack that here! Take it to where it's needed!", 20) + + return + end + + + + if _crate ~= nil and _crate.dist < 750 + and (_crate.details.unit == "FOB" or _crate.details.unit == "FOB-SMALL") then + + ctld.unpackFOBCrates(_crates, _heli) + + return + + elseif _crate ~= nil and _crate.dist < 200 then + + if ctld.forceCrateToBeMoved and ctld.crateMove[_crate.crateUnit:getName()] then + ctld.displayMessageToGroup(_heli,"Sorry you must move this crate before you unpack it!", 20) + return + end + + + local _aaTemplate = ctld.getAATemplate(_crate.details.unit) + + if _aaTemplate then + + if _crate.details.unit == _aaTemplate.repair then + ctld.repairAASystem(_heli, _crate,_aaTemplate) + else + ctld.unpackAASystem(_heli, _crate, _crates,_aaTemplate) + end + + return -- stop processing + -- is multi crate? + elseif _crate.details.cratesRequired ~= nil and _crate.details.cratesRequired > 1 then + -- multicrate + + ctld.unpackMultiCrate(_heli, _crate, _crates) + + return + + else + -- single crate + local _cratePoint = _crate.crateUnit:getPoint() + local _crateName = _crate.crateUnit:getName() + + -- ctld.spawnCrateStatic( _heli:getCoalition(),ctld.getNextUnitId(),{x=100,z=100},_crateName,100) + + --remove crate + -- if ctld.slingLoad == false then + _crate.crateUnit:destroy() + -- end + + local _spawnedGroups = ctld.spawnCrateGroup(_heli, { _cratePoint }, { _crate.details.unit }) + + if _heli:getCoalition() == 1 then + ctld.spawnedCratesRED[_crateName] = nil + else + ctld.spawnedCratesBLUE[_crateName] = nil + end + + ctld.processCallback({unit = _heli, crate = _crate , spawnedGroup = _spawnedGroups, action = "unpack"}) + + if _crate.details.unit == "1L13 EWR" then + ctld.addEWRTask(_spawnedGroups) + + -- env.info("Added EWR") + end + + + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " successfully deployed " .. _crate.details.desc .. " to the field", 10) + + if ctld.isJTACUnitType(_crate.details.unit) and ctld.JTAC_dropEnabled then + + local _code = table.remove(ctld.jtacGeneratedLaserCodes, 1) + --put to the end + table.insert(ctld.jtacGeneratedLaserCodes, _code) + + ctld.JTACAutoLase(_spawnedGroups:getName(), _code) --(_jtacGroupName, _laserCode, _smoke, _lock, _colour) + end + end + + else + + ctld.displayMessageToGroup(_heli, "No friendly crates close enough to unpack", 20) + end + end + end, _arguments) + + if (not _status) then + env.error(string.format("CTLD ERROR: %s", _err)) + end +end + + +-- builds a fob! +function ctld.unpackFOBCrates(_crates, _heli) + + if ctld.inLogisticsZone(_heli) == true then + + ctld.displayMessageToGroup(_heli, "You can't unpack that here! Take it to where it's needed!", 20) + + return + end + + -- unpack multi crate + local _nearbyMultiCrates = {} + + local _bigFobCrates = 0 + local _smallFobCrates = 0 + local _totalCrates = 0 + + for _, _nearbyCrate in pairs(_crates) do + + if _nearbyCrate.dist < 750 then + + if _nearbyCrate.details.unit == "FOB" then + _bigFobCrates = _bigFobCrates + 1 + table.insert(_nearbyMultiCrates, _nearbyCrate) + elseif _nearbyCrate.details.unit == "FOB-SMALL" then + _smallFobCrates = _smallFobCrates + 1 + table.insert(_nearbyMultiCrates, _nearbyCrate) + end + + --catch divide by 0 + if _smallFobCrates > 0 then + _totalCrates = _bigFobCrates + (_smallFobCrates/3.0) + else + _totalCrates = _bigFobCrates + end + + if _totalCrates >= ctld.cratesRequiredForFOB then + break + end + end + end + + --- check crate count + if _totalCrates >= ctld.cratesRequiredForFOB then + + -- destroy crates + + local _points = {} + + for _, _crate in pairs(_nearbyMultiCrates) do + + if _heli:getCoalition() == 1 then + ctld.droppedFOBCratesRED[_crate.crateUnit:getName()] = nil + ctld.spawnedCratesRED[_crate.crateUnit:getName()] = nil + else + ctld.droppedFOBCratesBLUE[_crate.crateUnit:getName()] = nil + ctld.spawnedCratesBLUE[_crate.crateUnit:getName()] = nil + end + + table.insert(_points, _crate.crateUnit:getPoint()) + + --destroy + _crate.crateUnit:destroy() + end + + local _centroid = ctld.getCentroid(_points) + + timer.scheduleFunction(function(_args) + + local _unitId = ctld.getNextUnitId() + local _name = "Deployed FOB #" .. _unitId + + local _fob = ctld.spawnFOB(_args[2], _unitId, _args[1], _name) + + --make it able to deploy crates + table.insert(ctld.logisticUnits, _fob:getName()) + + ctld.beaconCount = ctld.beaconCount + 1 + + local _radioBeaconName = "FOB Beacon #" .. ctld.beaconCount + + local _radioBeaconDetails = ctld.createRadioBeacon(_args[1], _args[3], _args[2], _radioBeaconName, nil, true) + + ctld.fobBeacons[_name] = { vhf = _radioBeaconDetails.vhf, uhf = _radioBeaconDetails.uhf, fm = _radioBeaconDetails.fm } + + if ctld.troopPickupAtFOB == true then + table.insert(ctld.builtFOBS, _fob:getName()) + + trigger.action.outTextForCoalition(_args[3], "Finished building FOB! Crates and Troops can now be picked up.", 10) + else + trigger.action.outTextForCoalition(_args[3], "Finished building FOB! Crates can now be picked up.", 10) + end + end, { _centroid, _heli:getCountry(), _heli:getCoalition() }, timer.getTime() + ctld.buildTimeFOB) + + local _txt = string.format("%s started building FOB using %d FOB crates, it will be finished in %d seconds.\nPosition marked with smoke.", ctld.getPlayerNameOrType(_heli), _totalCrates, ctld.buildTimeFOB) + + ctld.processCallback({unit = _heli, position = _centroid, action = "fob"}) + + trigger.action.smoke(_centroid, trigger.smokeColor.Green) + + trigger.action.outTextForCoalition(_heli:getCoalition(), _txt, 10) + else + local _txt = string.format("Cannot build FOB!\n\nIt requires %d Large FOB crates ( 3 small FOB crates equal 1 large FOB Crate) and there are the equivalent of %d large FOB crates nearby\n\nOr the crates are not within 750m of each other", ctld.cratesRequiredForFOB, _totalCrates) + ctld.displayMessageToGroup(_heli, _txt, 20) + end +end + +--unloads the sling crate when the helicopter is on the ground or between 4.5 - 10 meters +function ctld.dropSlingCrate(_args) + local _heli = ctld.getTransportUnit(_args[1]) + + if _heli == nil then + return -- no heli! + end + + local _currentCrate = ctld.inTransitSlingLoadCrates[_heli:getName()] + + if _currentCrate == nil then + if ctld.hoverPickup then + ctld.displayMessageToGroup(_heli, "You are not currently transporting any crates. \n\nTo Pickup a crate, hover for "..ctld.hoverTime.." seconds above the crate", 10) + else + ctld.displayMessageToGroup(_heli, "You are not currently transporting any crates. \n\nTo Pickup a crate - land and use F10 Crate Commands to load one.", 10) + end + else + + local _heli = ctld.getTransportUnit(_args[1]) + + local _point = _heli:getPoint() + + local _unitId = ctld.getNextUnitId() + + local _side = _heli:getCoalition() + + local _name = string.format("%s #%i", _currentCrate.desc, _unitId) + + + local _heightDiff = ctld.heightDiff(_heli) + + if ctld.inAir(_heli) == false or _heightDiff <= 7.5 then + ctld.displayMessageToGroup(_heli, _currentCrate.desc .. " crate has been safely unhooked and is at your 12 o'clock", 10) + _point = ctld.getPointAt12Oclock(_heli, 30) + -- elseif _heightDiff > 40.0 then + -- ctld.inTransitSlingLoadCrates[_heli:getName()] = nil + -- ctld.displayMessageToGroup(_heli, "You were too high! The crate has been destroyed", 10) + -- return + elseif _heightDiff > 7.5 and _heightDiff <= 40.0 then + ctld.displayMessageToGroup(_heli, _currentCrate.desc .. " crate has been safely dropped below you", 10) + else -- _heightDiff > 40.0 + ctld.inTransitSlingLoadCrates[_heli:getName()] = nil + ctld.displayMessageToGroup(_heli, "You were too high! The crate has been destroyed", 10) + return + end + + + --remove crate from cargo + ctld.inTransitSlingLoadCrates[_heli:getName()] = nil + ctld.adaptWeightToCargo(_heli:getName()) + local _spawnedCrate = ctld.spawnCrateStatic(_heli:getCountry(), _unitId, _point, _name, _currentCrate.weight,_side) + end +end + +--spawns a radio beacon made up of two units, +-- one for VHF and one for UHF +-- The units are set to to NOT engage +function ctld.createRadioBeacon(_point, _coalition, _country, _name, _batteryTime, _isFOB) + + local _uhfGroup = ctld.spawnRadioBeaconUnit(_point, _country, "UHF") + local _vhfGroup = ctld.spawnRadioBeaconUnit(_point, _country, "VHF") + local _fmGroup = ctld.spawnRadioBeaconUnit(_point, _country, "FM") + + local _freq = ctld.generateADFFrequencies() + + --create timeout + local _battery + + if _batteryTime == nil then + _battery = timer.getTime() + (ctld.deployedBeaconBattery * 60) + else + _battery = timer.getTime() + (_batteryTime * 60) + end + + local _lat, _lon = coord.LOtoLL(_point) + + local _latLngStr = mist.tostringLL(_lat, _lon, 3, ctld.location_DMS) + + --local _mgrsString = mist.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(_point)), 5) + + local _message = _name + + if _isFOB then + -- _message = "FOB " .. _message + _battery = -1 --never run out of power! + end + + _message = _message .. " - " .. _latLngStr + + -- env.info("GEN UHF: ".. _freq.uhf) + -- env.info("GEN VHF: ".. _freq.vhf) + + _message = string.format("%s - %.2f KHz", _message, _freq.vhf / 1000) + + _message = string.format("%s - %.2f MHz", _message, _freq.uhf / 1000000) + + _message = string.format("%s - %.2f MHz ", _message, _freq.fm / 1000000) + + + + local _beaconDetails = { + vhf = _freq.vhf, + vhfGroup = _vhfGroup:getName(), + uhf = _freq.uhf, + uhfGroup = _uhfGroup:getName(), + fm = _freq.fm, + fmGroup = _fmGroup:getName(), + text = _message, + battery = _battery, + coalition = _coalition, + } + ctld.updateRadioBeacon(_beaconDetails) + + table.insert(ctld.deployedRadioBeacons, _beaconDetails) + + return _beaconDetails +end + +function ctld.generateADFFrequencies() + + if #ctld.freeUHFFrequencies <= 3 then + ctld.freeUHFFrequencies = ctld.usedUHFFrequencies + ctld.usedUHFFrequencies = {} + end + + --remove frequency at RANDOM + local _uhf = table.remove(ctld.freeUHFFrequencies, math.random(#ctld.freeUHFFrequencies)) + table.insert(ctld.usedUHFFrequencies, _uhf) + + + if #ctld.freeVHFFrequencies <= 3 then + ctld.freeVHFFrequencies = ctld.usedVHFFrequencies + ctld.usedVHFFrequencies = {} + end + + local _vhf = table.remove(ctld.freeVHFFrequencies, math.random(#ctld.freeVHFFrequencies)) + table.insert(ctld.usedVHFFrequencies, _vhf) + + if #ctld.freeFMFrequencies <= 3 then + ctld.freeFMFrequencies = ctld.usedFMFrequencies + ctld.usedFMFrequencies = {} + end + + local _fm = table.remove(ctld.freeFMFrequencies, math.random(#ctld.freeFMFrequencies)) + table.insert(ctld.usedFMFrequencies, _fm) + + return { uhf = _uhf, vhf = _vhf, fm = _fm } + --- return {uhf=_uhf,vhf=_vhf} +end + + + +function ctld.spawnRadioBeaconUnit(_point, _country, _type) + + local _groupId = ctld.getNextGroupId() + + local _unitId = ctld.getNextUnitId() + + local _radioGroup = { + ["visible"] = false, + -- ["groupId"] = _groupId, + ["hidden"] = false, + ["units"] = { + [1] = { + ["y"] = _point.z, + ["type"] = "TACAN_beacon", + ["name"] = _type .. " Radio Beacon Unit #" .. _unitId, + -- ["unitId"] = _unitId, + ["heading"] = 0, + ["playerCanDrive"] = true, + ["skill"] = "Excellent", + ["x"] = _point.x, + } + }, + -- ["y"] = _positions[1].z, + -- ["x"] = _positions[1].x, + ["name"] = _type .. " Radio Beacon Group #" .. _groupId, + ["task"] = {}, + --added two fields below for MIST + ["category"] = Group.Category.GROUND, + ["country"] = _country + } + + -- return coalition.addGroup(_country, Group.Category.GROUND, _radioGroup) + return Group.getByName(mist.dynAdd(_radioGroup).name) +end + +function ctld.updateRadioBeacon(_beaconDetails) + + local _vhfGroup = Group.getByName(_beaconDetails.vhfGroup) + + local _uhfGroup = Group.getByName(_beaconDetails.uhfGroup) + + local _fmGroup = Group.getByName(_beaconDetails.fmGroup) + + local _radioLoop = {} + + if _vhfGroup ~= nil and _vhfGroup:getUnits() ~= nil and #_vhfGroup:getUnits() == 1 then + table.insert(_radioLoop, { group = _vhfGroup, freq = _beaconDetails.vhf, silent = false, mode = 0 }) + end + + if _uhfGroup ~= nil and _uhfGroup:getUnits() ~= nil and #_uhfGroup:getUnits() == 1 then + table.insert(_radioLoop, { group = _uhfGroup, freq = _beaconDetails.uhf, silent = true, mode = 0 }) + end + + if _fmGroup ~= nil and _fmGroup:getUnits() ~= nil and #_fmGroup:getUnits() == 1 then + table.insert(_radioLoop, { group = _fmGroup, freq = _beaconDetails.fm, silent = false, mode = 1 }) + end + + local _batLife = _beaconDetails.battery - timer.getTime() + + if (_batLife <= 0 and _beaconDetails.battery ~= -1) or #_radioLoop ~= 3 then + -- ran out of batteries + + if _vhfGroup ~= nil then + _vhfGroup:destroy() + end + if _uhfGroup ~= nil then + _uhfGroup:destroy() + end + if _fmGroup ~= nil then + _fmGroup:destroy() + end + + return false + end + + --fobs have unlimited battery life + -- if _battery ~= -1 then + -- _text = _text.." "..mist.utils.round(_batLife).." seconds of battery" + -- end + + for _, _radio in pairs(_radioLoop) do + + local _groupController = _radio.group:getController() + + local _sound = ctld.radioSound + if _radio.silent then + _sound = ctld.radioSoundFC3 + end + + _sound = "l10n/DEFAULT/".._sound + + _groupController:setOption(AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.WEAPON_HOLD) + + trigger.action.radioTransmission(_sound, _radio.group:getUnit(1):getPoint(), _radio.mode, false, _radio.freq, 1000) + --This function doesnt actually stop transmitting when then sound is false. My hope is it will stop if a new beacon is created on the same + -- frequency... OR they fix the bug where it wont stop. + -- end + + -- + end + + return true + + -- trigger.action.radioTransmission(ctld.radioSound, _point, 1, true, _frequency, 1000) +end + +function ctld.listRadioBeacons(_args) + + local _heli = ctld.getTransportUnit(_args[1]) + local _message = "" + + if _heli ~= nil then + + for _x, _details in pairs(ctld.deployedRadioBeacons) do + + if _details.coalition == _heli:getCoalition() then + _message = _message .. _details.text .. "\n" + end + end + + if _message ~= "" then + ctld.displayMessageToGroup(_heli, "Radio Beacons:\n" .. _message, 20) + else + ctld.displayMessageToGroup(_heli, "No Active Radio Beacons", 20) + end + end +end + +function ctld.dropRadioBeacon(_args) + + local _heli = ctld.getTransportUnit(_args[1]) + local _message = "" + + if _heli ~= nil and ctld.inAir(_heli) == false then + + --deploy 50 m infront + --try to spawn at 12 oclock to us + local _point = ctld.getPointAt12Oclock(_heli, 50) + + ctld.beaconCount = ctld.beaconCount + 1 + local _name = "Beacon #" .. ctld.beaconCount + + local _radioBeaconDetails = ctld.createRadioBeacon(_point, _heli:getCoalition(), _heli:getCountry(), _name, nil, false) + + -- mark with flare? + + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " deployed a Radio Beacon.\n\n" .. _radioBeaconDetails.text, 20) + + else + ctld.displayMessageToGroup(_heli, "You need to land before you can deploy a Radio Beacon!", 20) + end +end + +--remove closet radio beacon +function ctld.removeRadioBeacon(_args) + + local _heli = ctld.getTransportUnit(_args[1]) + local _message = "" + + if _heli ~= nil and ctld.inAir(_heli) == false then + + -- mark with flare? + + local _closetBeacon = nil + local _shortestDistance = -1 + local _distance = 0 + + for _x, _details in pairs(ctld.deployedRadioBeacons) do + + if _details.coalition == _heli:getCoalition() then + + local _group = Group.getByName(_details.vhfGroup) + + if _group ~= nil and #_group:getUnits() == 1 then + + _distance = ctld.getDistance(_heli:getPoint(), _group:getUnit(1):getPoint()) + if _distance ~= nil and (_shortestDistance == -1 or _distance < _shortestDistance) then + _shortestDistance = _distance + _closetBeacon = _details + end + end + end + end + + if _closetBeacon ~= nil and _shortestDistance then + local _vhfGroup = Group.getByName(_closetBeacon.vhfGroup) + + local _uhfGroup = Group.getByName(_closetBeacon.uhfGroup) + + local _fmGroup = Group.getByName(_closetBeacon.fmGroup) + + if _vhfGroup ~= nil then + _vhfGroup:destroy() + end + if _uhfGroup ~= nil then + _uhfGroup:destroy() + end + if _fmGroup ~= nil then + _fmGroup:destroy() + end + + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " removed a Radio Beacon.\n\n" .. _closetBeacon.text, 20) + else + ctld.displayMessageToGroup(_heli, "No Radio Beacons within 500m.", 20) + end + + else + ctld.displayMessageToGroup(_heli, "You need to land before remove a Radio Beacon", 20) + end +end + +-- gets the center of a bunch of points! +-- return proper DCS point with height +function ctld.getCentroid(_points) + local _tx, _ty = 0, 0 + for _index, _point in ipairs(_points) do + _tx = _tx + _point.x + _ty = _ty + _point.z + end + + local _npoints = #_points + + local _point = { x = _tx / _npoints, z = _ty / _npoints } + + _point.y = land.getHeight({ _point.x, _point.z }) + + return _point +end + +function ctld.getAATemplate(_unitName) + + for _,_system in pairs(ctld.AASystemTemplate) do + + if _system.repair == _unitName then + return _system + end + + for _,_part in pairs(_system.parts) do + + if _unitName == _part.name then + return _system + end + end + end + + return nil + +end + +function ctld.getLauncherUnitFromAATemplate(_aaTemplate) + for _,_part in pairs(_aaTemplate.parts) do + + if _part.launcher then + return _part.name + end + end + + return nil +end + +function ctld.rearmAASystem(_heli, _nearestCrate, _nearbyCrates, _aaSystemTemplate) + + -- are we adding to existing aa system? + -- check to see if the crate is a launcher + if ctld.getLauncherUnitFromAATemplate(_aaSystemTemplate) == _nearestCrate.details.unit then + + -- find nearest COMPLETE AA system + local _nearestSystem = ctld.findNearestAASystem(_heli, _aaSystemTemplate) + + if _nearestSystem ~= nil and _nearestSystem.dist < 300 then + + local _uniqueTypes = {} -- stores each unique part of system + local _types = {} + local _points = {} + + local _units = _nearestSystem.group:getUnits() + + if _units ~= nil and #_units > 0 then + + for x = 1, #_units do + if _units[x]:getLife() > 0 then + + --this allows us to count each type once + _uniqueTypes[_units[x]:getTypeName()] = _units[x]:getTypeName() + + table.insert(_points, _units[x]:getPoint()) + table.insert(_types, _units[x]:getTypeName()) + end + end + end + + -- do we have the correct number of unique pieces and do we have enough points for all the pieces + if ctld.countTableEntries(_uniqueTypes) == _aaSystemTemplate.count and #_points >= _aaSystemTemplate.count then + + -- rearm aa system + -- destroy old group + ctld.completeAASystems[_nearestSystem.group:getName()] = nil + + _nearestSystem.group:destroy() + + local _spawnedGroup = ctld.spawnCrateGroup(_heli, _points, _types) + + ctld.completeAASystems[_spawnedGroup:getName()] = ctld.getAASystemDetails(_spawnedGroup, _aaSystemTemplate) + + ctld.processCallback({unit = _heli, crate = _nearestCrate , spawnedGroup = _spawnedGroup, action = "rearm"}) + + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " successfully rearmed a full ".._aaSystemTemplate.name.." in the field", 10) + + if _heli:getCoalition() == 1 then + ctld.spawnedCratesRED[_nearestCrate.crateUnit:getName()] = nil + else + ctld.spawnedCratesBLUE[_nearestCrate.crateUnit:getName()] = nil + end + + -- remove crate + -- if ctld.slingLoad == false then + _nearestCrate.crateUnit:destroy() + -- end + + return true -- all done so quit + end + end + end + + return false +end + +function ctld.getAASystemDetails(_hawkGroup,_aaSystemTemplate) + + local _units = _hawkGroup:getUnits() + + local _hawkDetails = {} + + for _, _unit in pairs(_units) do + table.insert(_hawkDetails, { point = _unit:getPoint(), unit = _unit:getTypeName(), name = _unit:getName(), system =_aaSystemTemplate}) + end + + return _hawkDetails +end + +function ctld.countTableEntries(_table) + + if _table == nil then + return 0 + end + + + local _count = 0 + + for _key, _value in pairs(_table) do + + _count = _count + 1 + end + + return _count +end + +function ctld.unpackAASystem(_heli, _nearestCrate, _nearbyCrates,_aaSystemTemplate) + + if ctld.rearmAASystem(_heli, _nearestCrate, _nearbyCrates,_aaSystemTemplate) then + -- rearmed hawk + return + end + + -- are there all the pieces close enough together + local _systemParts = {} + + --initialise list of parts + for _,_part in pairs(_aaSystemTemplate.parts) do + _systemParts[_part.name] = {name = _part.name,desc = _part.desc,found = false} + end + + -- find all nearest crates and add them to the list if they're part of the AA System + for _, _nearbyCrate in pairs(_nearbyCrates) do + + if _nearbyCrate.dist < 500 then + + if _systemParts[_nearbyCrate.details.unit] ~= nil and _systemParts[_nearbyCrate.details.unit].found == false then + local _foundPart = _systemParts[_nearbyCrate.details.unit] + + _foundPart.found = true + _foundPart.crate = _nearbyCrate + + _systemParts[_nearbyCrate.details.unit] = _foundPart + end + end + end + + local _count = 0 + local _txt = "" + + local _posArray = {} + local _typeArray = {} + for _name, _systemPart in pairs(_systemParts) do + + if _systemPart.found == false then + _txt = _txt.."Missing ".._systemPart.desc.."\n" + else + + local _launcherPart = ctld.getLauncherUnitFromAATemplate(_aaSystemTemplate) + + --handle multiple launchers from one crate + if (_name == "Hawk ln" and ctld.hawkLaunchers > 1) + or (_launcherPart == _name and ctld.aaLaunchers > 1) then + + --add multiple launcher + local _launchers = ctld.aaLaunchers + + if _name == "Hawk ln" then + _launchers = ctld.hawkLaunchers + end + + for _i = 1, _launchers do + + -- spawn in a circle around the crate + local _angle = math.pi * 2 * (_i - 1) / _launchers + local _xOffset = math.cos(_angle) * 12 + local _yOffset = math.sin(_angle) * 12 + + local _point = _systemPart.crate.crateUnit:getPoint() + + _point = { x = _point.x + _xOffset, y = _point.y, z = _point.z + _yOffset } + + table.insert(_posArray, _point) + table.insert(_typeArray, _name) + end + else + table.insert(_posArray, _systemPart.crate.crateUnit:getPoint()) + table.insert(_typeArray, _name) + end + end + end + + local _activeLaunchers = ctld.countCompleteAASystems(_heli) + + local _allowed = ctld.getAllowedAASystems(_heli) + + env.info("Active: ".._activeLaunchers.." Allowed: ".._allowed) + + if _activeLaunchers + 1 > _allowed then + trigger.action.outTextForCoalition(_heli:getCoalition(), "Out of parts for AA Systems. Current limit is ".._allowed.." \n", 10) + return + end + + if _txt ~= "" then + ctld.displayMessageToGroup(_heli, "Cannot build ".._aaSystemTemplate.name.."\n" .. _txt .. "\n\nOr the crates are not close enough together", 20) + return + else + + -- destroy crates + for _name, _systemPart in pairs(_systemParts) do + + if _heli:getCoalition() == 1 then + ctld.spawnedCratesRED[_systemPart.crate.crateUnit:getName()] = nil + else + ctld.spawnedCratesBLUE[_systemPart.crate.crateUnit:getName()] = nil + end + + --destroy + -- if ctld.slingLoad == false then + _systemPart.crate.crateUnit:destroy() + --end + end + + -- HAWK / BUK READY! + local _spawnedGroup = ctld.spawnCrateGroup(_heli, _posArray, _typeArray) + + ctld.completeAASystems[_spawnedGroup:getName()] = ctld.getAASystemDetails(_spawnedGroup,_aaSystemTemplate) + + ctld.processCallback({unit = _heli, crate = _nearestCrate , spawnedGroup = _spawnedGroup, action = "unpack"}) + + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " successfully deployed a full ".._aaSystemTemplate.name.." to the field. \n\nAA Active System limit is: ".._allowed.."\nActive: "..(_activeLaunchers+1), 10) + + end +end + +--count the number of captured cities, sets the amount of allowed AA Systems +function ctld.getAllowedAASystems(_heli) + + if _heli:getCoalition() == 1 then + return ctld.AASystemLimitBLUE + else + return ctld.AASystemLimitRED + end + + +end + + +function ctld.countCompleteAASystems(_heli) + + local _count = 0 + + for _groupName, _hawkDetails in pairs(ctld.completeAASystems) do + + local _hawkGroup = Group.getByName(_groupName) + + -- env.info(_groupName..": "..mist.utils.tableShow(_hawkDetails)) + if _hawkGroup ~= nil and _hawkGroup:getCoalition() == _heli:getCoalition() then + + local _units = _hawkGroup:getUnits() + + if _units ~=nil and #_units > 0 then + --get the system template + local _aaSystemTemplate = _hawkDetails[1].system + + local _uniqueTypes = {} -- stores each unique part of system + local _types = {} + local _points = {} + + if _units ~= nil and #_units > 0 then + + for x = 1, #_units do + if _units[x]:getLife() > 0 then + + --this allows us to count each type once + _uniqueTypes[_units[x]:getTypeName()] = _units[x]:getTypeName() + + table.insert(_points, _units[x]:getPoint()) + table.insert(_types, _units[x]:getTypeName()) + end + end + end + + -- do we have the correct number of unique pieces and do we have enough points for all the pieces + if ctld.countTableEntries(_uniqueTypes) == _aaSystemTemplate.count and #_points >= _aaSystemTemplate.count then + _count = _count +1 + end + end + end + end + + return _count +end + + +function ctld.repairAASystem(_heli, _nearestCrate,_aaSystem) + + -- find nearest COMPLETE AA system + local _nearestHawk = ctld.findNearestAASystem(_heli,_aaSystem) + + + + if _nearestHawk ~= nil and _nearestHawk.dist < 300 then + + local _oldHawk = ctld.completeAASystems[_nearestHawk.group:getName()] + + --spawn new one + + local _types = {} + local _points = {} + + for _, _part in pairs(_oldHawk) do + table.insert(_points, _part.point) + table.insert(_types, _part.unit) + end + + --remove old system + ctld.completeAASystems[_nearestHawk.group:getName()] = nil + _nearestHawk.group:destroy() + + local _spawnedGroup = ctld.spawnCrateGroup(_heli, _points, _types) + + ctld.completeAASystems[_spawnedGroup:getName()] = ctld.getAASystemDetails(_spawnedGroup,_aaSystem) + + ctld.processCallback({unit = _heli, crate = _nearestCrate , spawnedGroup = _spawnedGroup, action = "repair"}) + + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " successfully repaired a full ".._aaSystem.name.." in the field", 10) + + if _heli:getCoalition() == 1 then + ctld.spawnedCratesRED[_nearestCrate.crateUnit:getName()] = nil + else + ctld.spawnedCratesBLUE[_nearestCrate.crateUnit:getName()] = nil + end + + -- remove crate + -- if ctld.slingLoad == false then + _nearestCrate.crateUnit:destroy() + -- end + + else + ctld.displayMessageToGroup(_heli, "Cannot repair ".._aaSystem.name..". No damaged ".._aaSystem.name.." within 300m", 10) + end +end + +function ctld.unpackMultiCrate(_heli, _nearestCrate, _nearbyCrates) + + -- unpack multi crate + local _nearbyMultiCrates = {} + + for _, _nearbyCrate in pairs(_nearbyCrates) do + + if _nearbyCrate.dist < 300 then + + if _nearbyCrate.details.unit == _nearestCrate.details.unit then + + table.insert(_nearbyMultiCrates, _nearbyCrate) + + if #_nearbyMultiCrates == _nearestCrate.details.cratesRequired then + break + end + end + end + end + + --- check crate count + if #_nearbyMultiCrates == _nearestCrate.details.cratesRequired then + + local _point = _nearestCrate.crateUnit:getPoint() + + -- destroy crates + for _, _crate in pairs(_nearbyMultiCrates) do + + if _point == nil then + _point = _crate.crateUnit:getPoint() + end + + if _heli:getCoalition() == 1 then + ctld.spawnedCratesRED[_crate.crateUnit:getName()] = nil + else + ctld.spawnedCratesBLUE[_crate.crateUnit:getName()] = nil + end + + --destroy + -- if ctld.slingLoad == false then + _crate.crateUnit:destroy() + -- end + end + + + local _spawnedGroup = ctld.spawnCrateGroup(_heli, { _point }, { _nearestCrate.details.unit }) + + ctld.processCallback({unit = _heli, crate = _nearestCrate , spawnedGroup = _spawnedGroup, action = "unpack"}) + + local _txt = string.format("%s successfully deployed %s to the field using %d crates", ctld.getPlayerNameOrType(_heli), _nearestCrate.details.desc, #_nearbyMultiCrates) + + trigger.action.outTextForCoalition(_heli:getCoalition(), _txt, 10) + + else + + local _txt = string.format("Cannot build %s!\n\nIt requires %d crates and there are %d \n\nOr the crates are not within 300m of each other", _nearestCrate.details.desc, _nearestCrate.details.cratesRequired, #_nearbyMultiCrates) + + ctld.displayMessageToGroup(_heli, _txt, 20) + end +end + + +function ctld.spawnCrateGroup(_heli, _positions, _types) + + local _id = ctld.getNextGroupId() + + local _groupName = _types[1] .. " #" .. _id + + local _side = _heli:getCoalition() + + local _group = { + ["visible"] = false, + -- ["groupId"] = _id, + ["hidden"] = false, + ["units"] = {}, + -- ["y"] = _positions[1].z, + -- ["x"] = _positions[1].x, + ["name"] = _groupName, + ["task"] = {}, + } + + if #_positions == 1 then + + local _unitId = ctld.getNextUnitId() + local _details = { type = _types[1], unitId = _unitId, name = string.format("Unpacked %s #%i", _types[1], _unitId) } + + _group.units[1] = ctld.createUnit(_positions[1].x + 5, _positions[1].z + 5, 120, _details) + + else + + for _i, _pos in ipairs(_positions) do + + local _unitId = ctld.getNextUnitId() + local _details = { type = _types[_i], unitId = _unitId, name = string.format("Unpacked %s #%i", _types[_i], _unitId) } + + _group.units[_i] = ctld.createUnit(_pos.x + 5, _pos.z + 5, 120, _details) + end + end + + --mist function + _group.category = Group.Category.GROUND + _group.country = _heli:getCountry() + + local _spawnedGroup = Group.getByName(mist.dynAdd(_group).name) + + return _spawnedGroup +end + + + +-- spawn normal group +function ctld.spawnDroppedGroup(_point, _details, _spawnBehind, _maxSearch) + + local _groupName = _details.groupName + + local _group = { + ["visible"] = false, + -- ["groupId"] = _details.groupId, + ["hidden"] = false, + ["units"] = {}, + -- ["y"] = _positions[1].z, + -- ["x"] = _positions[1].x, + ["name"] = _groupName, + ["task"] = {}, + } + + + if _spawnBehind == false then + + -- spawn in circle around heli + + local _pos = _point + + for _i, _detail in ipairs(_details.units) do + + local _angle = math.pi * 2 * (_i - 1) / #_details.units + local _xOffset = math.cos(_angle) * 30 + local _yOffset = math.sin(_angle) * 30 + + _group.units[_i] = ctld.createUnit(_pos.x + _xOffset, _pos.z + _yOffset, _angle, _detail) + end + + else + + local _pos = _point + + --try to spawn at 6 oclock to us + local _angle = math.atan2(_pos.z, _pos.x) + local _xOffset = math.cos(_angle) * -30 + local _yOffset = math.sin(_angle) * -30 + + + for _i, _detail in ipairs(_details.units) do + _group.units[_i] = ctld.createUnit(_pos.x + (_xOffset + 10 * _i), _pos.z + (_yOffset + 10 * _i), _angle, _detail) + end + end + + --switch to MIST + _group.category = Group.Category.GROUND; + _group.country = _details.country; + + local _spawnedGroup = Group.getByName(mist.dynAdd(_group).name) + + --local _spawnedGroup = coalition.addGroup(_details.country, Group.Category.GROUND, _group) + + + -- find nearest enemy and head there + if _maxSearch == nil then + _maxSearch = ctld.maximumSearchDistance + end + + local _wpZone = ctld.inWaypointZone(_point,_spawnedGroup:getCoalition()) + + if _wpZone.inZone then + ctld.orderGroupToMoveToPoint(_spawnedGroup:getUnit(1), _wpZone.point) + env.info("Heading to waypoint - In Zone ".._wpZone.name) + else + local _enemyPos = ctld.findNearestEnemy(_details.side, _point, _maxSearch) + + ctld.orderGroupToMoveToPoint(_spawnedGroup:getUnit(1), _enemyPos) + end + + return _spawnedGroup +end + +function ctld.findNearestEnemy(_side, _point, _searchDistance) + + local _closestEnemy = nil + + local _groups + + local _closestEnemyDist = _searchDistance + + local _heliPoint = _point + + if _side == 2 then + _groups = coalition.getGroups(1, Group.Category.GROUND) + else + _groups = coalition.getGroups(2, Group.Category.GROUND) + end + + for _, _group in pairs(_groups) do + + if _group ~= nil then + local _units = _group:getUnits() + + if _units ~= nil and #_units > 0 then + + local _leader = nil + + -- find alive leader + for x = 1, #_units do + if _units[x]:getLife() > 0 then + _leader = _units[x] + break + end + end + + if _leader ~= nil then + local _leaderPos = _leader:getPoint() + local _dist = ctld.getDistance(_heliPoint, _leaderPos) + if _dist < _closestEnemyDist then + _closestEnemyDist = _dist + _closestEnemy = _leaderPos + end + end + end + end + end + + + -- no enemy - move to random point + if _closestEnemy ~= nil then + + -- env.info("found enemy") + return _closestEnemy + else + + local _x = _heliPoint.x + math.random(0, ctld.maximumMoveDistance) - math.random(0, ctld.maximumMoveDistance) + local _z = _heliPoint.z + math.random(0, ctld.maximumMoveDistance) - math.random(0, ctld.maximumMoveDistance) + local _y = _heliPoint.y + math.random(0, ctld.maximumMoveDistance) - math.random(0, ctld.maximumMoveDistance) + + return { x = _x, z = _z,y=_y } + end +end + +function ctld.findNearestGroup(_heli, _groups) + + local _closestGroupDetails = {} + local _closestGroup = nil + + local _closestGroupDist = ctld.maxExtractDistance + + local _heliPoint = _heli:getPoint() + + for _, _groupName in pairs(_groups) do + + local _group = Group.getByName(_groupName) + + if _group ~= nil then + local _units = _group:getUnits() + + if _units ~= nil and #_units > 0 then + + local _leader = nil + + local _groupDetails = { groupId = _group:getID(), groupName = _group:getName(), side = _group:getCoalition(), units = {} } + + -- find alive leader + for x = 1, #_units do + if _units[x]:getLife() > 0 then + + if _leader == nil then + _leader = _units[x] + -- set country based on leader + _groupDetails.country = _leader:getCountry() + end + + local _unitDetails = { type = _units[x]:getTypeName(), unitId = _units[x]:getID(), name = _units[x]:getName() } + + table.insert(_groupDetails.units, _unitDetails) + end + end + + if _leader ~= nil then + local _leaderPos = _leader:getPoint() + local _dist = ctld.getDistance(_heliPoint, _leaderPos) + if _dist < _closestGroupDist then + _closestGroupDist = _dist + _closestGroupDetails = _groupDetails + _closestGroup = _group + end + end + end + end + end + + + if _closestGroup ~= nil then + + return { group = _closestGroup, details = _closestGroupDetails } + else + + return nil + end +end + + +function ctld.createUnit(_x, _y, _angle, _details) + + local _newUnit = { + ["y"] = _y, + ["type"] = _details.type, + ["name"] = _details.name, + -- ["unitId"] = _details.unitId, + ["heading"] = _angle, + ["playerCanDrive"] = true, + ["skill"] = "Excellent", + ["x"] = _x, + } + + return _newUnit +end + +function ctld.addEWRTask(_group) + + -- delayed 2 second to work around bug + timer.scheduleFunction(function(_ewrGroup) + local _grp = ctld.getAliveGroup(_ewrGroup) + + if _grp ~= nil then + local _controller = _grp:getController(); + local _EWR = { + id = 'EWR', + auto = true, + params = { + } + } + _controller:setTask(_EWR) + end + end + , _group:getName(), timer.getTime() + 2) + +end + +function ctld.orderGroupToMoveToPoint(_leader, _destination) + + local _group = _leader:getGroup() + + local _path = {} + table.insert(_path, mist.ground.buildWP(_leader:getPoint(), 'Off Road', 50)) + table.insert(_path, mist.ground.buildWP(_destination, 'Off Road', 50)) + + local _mission = { + id = 'Mission', + params = { + route = { + points =_path + }, + }, + } + + + -- delayed 2 second to work around bug + timer.scheduleFunction(function(_arg) + local _grp = ctld.getAliveGroup(_arg[1]) + + if _grp ~= nil then + local _controller = _grp:getController(); + Controller.setOption(_controller, AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.AUTO) + Controller.setOption(_controller, AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.OPEN_FIRE) + _controller:setTask(_arg[2]) + end + end + , {_group:getName(), _mission}, timer.getTime() + 2) + +end + +-- are we in pickup zone +function ctld.inPickupZone(_heli) + ctld.logDebug(string.format("ctld.inPickupZone(_heli=%s)", ctld.p(_heli))) + + if ctld.inAir(_heli) then + return { inZone = false, limit = -1, index = -1 } + end + + local _heliPoint = _heli:getPoint() + + for _i, _zoneDetails in pairs(ctld.pickupZones) do + ctld.logTrace(string.format("_zoneDetails=%s", ctld.p(_zoneDetails))) + + local _triggerZone = trigger.misc.getZone(_zoneDetails[1]) + + if _triggerZone == nil then + local _ship = ctld.getTransportUnit(_zoneDetails[1]) + + if _ship then + local _point = _ship:getPoint() + _triggerZone = {} + _triggerZone.point = _point + _triggerZone.radius = 200 -- should be big enough for ship + end + + end + + if _triggerZone ~= nil then + + --get distance to center + + local _dist = ctld.getDistance(_heliPoint, _triggerZone.point) + ctld.logTrace(string.format("_dist=%s", ctld.p(_dist))) + if _dist <= _triggerZone.radius then + local _heliCoalition = _heli:getCoalition() + if _zoneDetails[4] == 1 and (_zoneDetails[5] == _heliCoalition or _zoneDetails[5] == 0) then + return { inZone = true, limit = _zoneDetails[3], index = _i } + end + end + end + end + + local _fobs = ctld.getSpawnedFobs(_heli) + + -- now check spawned fobs + for _, _fob in ipairs(_fobs) do + + --get distance to center + + local _dist = ctld.getDistance(_heliPoint, _fob:getPoint()) + + if _dist <= 150 then + return { inZone = true, limit = 10000, index = -1 }; + end + end + + + + return { inZone = false, limit = -1, index = -1 }; +end + +function ctld.getSpawnedFobs(_heli) + + local _fobs = {} + + for _, _fobName in ipairs(ctld.builtFOBS) do + + local _fob = StaticObject.getByName(_fobName) + + if _fob ~= nil and _fob:isExist() and _fob:getCoalition() == _heli:getCoalition() and _fob:getLife() > 0 then + + table.insert(_fobs, _fob) + end + end + + return _fobs +end + +-- are we in a dropoff zone +function ctld.inDropoffZone(_heli) + + if ctld.inAir(_heli) then + return false + end + + local _heliPoint = _heli:getPoint() + + for _, _zoneDetails in pairs(ctld.dropOffZones) do + + local _triggerZone = trigger.misc.getZone(_zoneDetails[1]) + + if _triggerZone ~= nil and (_zoneDetails[3] == _heli:getCoalition() or _zoneDetails[3]== 0) then + + --get distance to center + + local _dist = ctld.getDistance(_heliPoint, _triggerZone.point) + + if _dist <= _triggerZone.radius then + return true + end + end + end + + return false +end + +-- are we in a waypoint zone +function ctld.inWaypointZone(_point,_coalition) + + for _, _zoneDetails in pairs(ctld.wpZones) do + + local _triggerZone = trigger.misc.getZone(_zoneDetails[1]) + + --right coalition and active? + if _triggerZone ~= nil and (_zoneDetails[4] == _coalition or _zoneDetails[4]== 0) and _zoneDetails[3] == 1 then + + --get distance to center + + local _dist = ctld.getDistance(_point, _triggerZone.point) + + if _dist <= _triggerZone.radius then + return {inZone = true, point = _triggerZone.point, name = _zoneDetails[1]} + end + end + end + + return {inZone = false} +end + +-- are we near friendly logistics zone +function ctld.inLogisticsZone(_heli) + + if ctld.inAir(_heli) then + return false + end + + local _heliPoint = _heli:getPoint() + + for _, _name in pairs(ctld.logisticUnits) do + + local _logistic = StaticObject.getByName(_name) + + if _logistic ~= nil and _logistic:getCoalition() == _heli:getCoalition() then + + --get distance + local _dist = ctld.getDistance(_heliPoint, _logistic:getPoint()) + + if _dist <= ctld.maximumDistanceLogistic then + return true + end + end + end + + return false +end + + +-- are far enough from a friendly logistics zone +function ctld.farEnoughFromLogisticZone(_heli) + + if ctld.inAir(_heli) then + return false + end + + local _heliPoint = _heli:getPoint() + + local _farEnough = true + + for _, _name in pairs(ctld.logisticUnits) do + + local _logistic = StaticObject.getByName(_name) + + if _logistic ~= nil and _logistic:getCoalition() == _heli:getCoalition() then + + --get distance + local _dist = ctld.getDistance(_heliPoint, _logistic:getPoint()) + -- env.info("DIST ".._dist) + if _dist <= ctld.minimumDeployDistance then + -- env.info("TOO CLOSE ".._dist) + _farEnough = false + end + end + end + + return _farEnough +end + +function ctld.refreshSmoke() + + if ctld.disableAllSmoke == true then + return + end + + for _, _zoneGroup in pairs({ ctld.pickupZones, ctld.dropOffZones }) do + + for _, _zoneDetails in pairs(_zoneGroup) do + + local _triggerZone = trigger.misc.getZone(_zoneDetails[1]) + + if _triggerZone == nil then + local _ship = ctld.getTransportUnit(_triggerZone) + + if _ship then + local _point = _ship:getPoint() + _triggerZone = {} + _triggerZone.point = _point + end + + end + + + --only trigger if smoke is on AND zone is active + if _triggerZone ~= nil and _zoneDetails[2] >= 0 and _zoneDetails[4] == 1 then + + -- Trigger smoke markers + + local _pos2 = { x = _triggerZone.point.x, y = _triggerZone.point.z } + local _alt = land.getHeight(_pos2) + local _pos3 = { x = _pos2.x, y = _alt, z = _pos2.y } + + trigger.action.smoke(_pos3, _zoneDetails[2]) + end + end + end + + --waypoint zones + for _, _zoneDetails in pairs(ctld.wpZones) do + + local _triggerZone = trigger.misc.getZone(_zoneDetails[1]) + + --only trigger if smoke is on AND zone is active + if _triggerZone ~= nil and _zoneDetails[2] >= 0 and _zoneDetails[3] == 1 then + + -- Trigger smoke markers + + local _pos2 = { x = _triggerZone.point.x, y = _triggerZone.point.z } + local _alt = land.getHeight(_pos2) + local _pos3 = { x = _pos2.x, y = _alt, z = _pos2.y } + + trigger.action.smoke(_pos3, _zoneDetails[2]) + end + end + + + --refresh in 5 minutes + timer.scheduleFunction(ctld.refreshSmoke, nil, timer.getTime() + 300) +end + +function ctld.dropSmoke(_args) + + local _heli = ctld.getTransportUnit(_args[1]) + + if _heli ~= nil then + + local _colour = "" + + if _args[2] == trigger.smokeColor.Red then + + _colour = "RED" + elseif _args[2] == trigger.smokeColor.Blue then + + _colour = "BLUE" + elseif _args[2] == trigger.smokeColor.Green then + + _colour = "GREEN" + elseif _args[2] == trigger.smokeColor.Orange then + + _colour = "ORANGE" + end + + local _point = _heli:getPoint() + + local _pos2 = { x = _point.x, y = _point.z } + local _alt = land.getHeight(_pos2) + local _pos3 = { x = _point.x, y = _alt, z = _point.z } + + trigger.action.smoke(_pos3, _args[2]) + + trigger.action.outTextForCoalition(_heli:getCoalition(), ctld.getPlayerNameOrType(_heli) .. " dropped " .. _colour .. " smoke ", 10) + end +end + +function ctld.unitCanCarryVehicles(_unit) + + local _type = string.lower(_unit:getTypeName()) + + for _, _name in ipairs(ctld.vehicleTransportEnabled) do + local _nameLower = string.lower(_name) + if string.match(_type, _nameLower) then + return true + end + end + + return false +end + +function ctld.isJTACUnitType(_type) + + _type = string.lower(_type) + + for _, _name in ipairs(ctld.jtacUnitTypes) do + local _nameLower = string.lower(_name) + if string.match(_type, _nameLower) then + return true + end + end + + return false +end + +function ctld.updateZoneCounter(_index, _diff) + + if ctld.pickupZones[_index] ~= nil then + + ctld.pickupZones[_index][3] = ctld.pickupZones[_index][3] + _diff + + if ctld.pickupZones[_index][3] < 0 then + ctld.pickupZones[_index][3] = 0 + end + + if ctld.pickupZones[_index][6] ~= nil then + trigger.action.setUserFlag(ctld.pickupZones[_index][6], ctld.pickupZones[_index][3]) + end + -- env.info(ctld.pickupZones[_index][1].." = " ..ctld.pickupZones[_index][3]) + end +end + +function ctld.processCallback(_callbackArgs) + + for _, _callback in pairs(ctld.callbacks) do + + local _status, _result = pcall(function() + + _callback(_callbackArgs) + + end) + + if (not _status) then + env.error(string.format("CTLD Callback Error: %s", _result)) + end + end +end + + +-- checks the status of all AI troop carriers and auto loads and unloads troops +-- as long as the troops are on the ground +function ctld.checkAIStatus() + + timer.scheduleFunction(ctld.checkAIStatus, nil, timer.getTime() + 2) + + + for _, _unitName in pairs(ctld.transportPilotNames) do + local status, error = pcall(function() + + local _unit = ctld.getTransportUnit(_unitName) + + -- no player name means AI! + if _unit ~= nil and _unit:getPlayerName() == nil then + local _zone = ctld.inPickupZone(_unit) + -- env.error("Checking.. ".._unit:getName()) + if _zone.inZone == true and not ctld.troopsOnboard(_unit, true) then + -- env.error("in zone, loading.. ".._unit:getName()) + + if ctld.allowRandomAiTeamPickups == true then + -- Random troop pickup implementation + local _team = nil + if _unit:getCoalition() == 1 then + _team = math.floor((math.random(#ctld.redTeams * 100) / 100) + 1) + ctld.loadTroopsFromZone({ _unitName, true,ctld.loadableGroups[ctld.redTeams[_team]],true }) + else + _team = math.floor((math.random(#ctld.blueTeams * 100) / 100) + 1) + ctld.loadTroopsFromZone({ _unitName, true,ctld.loadableGroups[ctld.blueTeams[_team]],true }) + end + else + ctld.loadTroopsFromZone({ _unitName, true,"",true }) + end + + elseif ctld.inDropoffZone(_unit) and ctld.troopsOnboard(_unit, true) then + -- env.error("in dropoff zone, unloading.. ".._unit:getName()) + ctld.unloadTroops( { _unitName, true }) + end + + if ctld.unitCanCarryVehicles(_unit) then + + if _zone.inZone == true and not ctld.troopsOnboard(_unit, false) then + + ctld.loadTroopsFromZone({ _unitName, false,"",true }) + + elseif ctld.inDropoffZone(_unit) and ctld.troopsOnboard(_unit, false) then + + ctld.unloadTroops( { _unitName, false }) + end + end + end + end) + + if (not status) then + env.error(string.format("Error with ai status: %s", error), false) + end + end + + +end + +function ctld.getTransportLimit(_unitType) + + if ctld.unitLoadLimits[_unitType] then + + return ctld.unitLoadLimits[_unitType] + end + + return ctld.numberOfTroops + +end + +function ctld.getUnitActions(_unitType) + + if ctld.unitActions[_unitType] then + return ctld.unitActions[_unitType] + end + + return {crates=true,troops=true} + +end + +-- Adds menuitem to all heli units that are active +function ctld.addF10MenuOptions() + -- Loop through all Heli units + + timer.scheduleFunction(ctld.addF10MenuOptions, nil, timer.getTime() + 10) + + for _, _unitName in pairs(ctld.transportPilotNames) do + + local status, error = pcall(function() + + local _unit = ctld.getTransportUnit(_unitName) + + if _unit ~= nil then + + local _groupId = ctld.getGroupId(_unit) + + if _groupId then + + if ctld.addedTo[tostring(_groupId)] == nil then + + local _rootPath = missionCommands.addSubMenuForGroup(_groupId, "CTLD") + + local _unitActions = ctld.getUnitActions(_unit:getTypeName()) + ctld.logTrace(string.format("_unitActions=%s", ctld.p(_unitActions))) + + missionCommands.addCommandForGroup(_groupId, "Check Cargo", _rootPath, ctld.checkTroopStatus, { _unitName }) + + if _unitActions.troops then + + local _troopCommandsPath = missionCommands.addSubMenuForGroup(_groupId, "Troop Transport", _rootPath) + + missionCommands.addCommandForGroup(_groupId, "Unload / Extract Troops", _troopCommandsPath, ctld.unloadExtractTroops, { _unitName }) + + + -- local _loadPath = missionCommands.addSubMenuForGroup(_groupId, "Load From Zone", _troopCommandsPath) + local _transportLimit = ctld.getTransportLimit(_unit:getTypeName()) + ctld.logTrace(string.format("_transportLimit=%s", ctld.p(_transportLimit))) + for _,_loadGroup in pairs(ctld.loadableGroups) do + ctld.logTrace(string.format("_loadGroup=%s", ctld.p(_loadGroup))) + if not _loadGroup.side or _loadGroup.side == _unit:getCoalition() then + + -- check size & unit + if _transportLimit >= _loadGroup.total then + missionCommands.addCommandForGroup(_groupId, "Load ".._loadGroup.name, _troopCommandsPath, ctld.loadTroopsFromZone, { _unitName, true,_loadGroup,false }) + end + end + end + + if ctld.unitCanCarryVehicles(_unit) then + + local _vehicleCommandsPath = missionCommands.addSubMenuForGroup(_groupId, "Vehicle / FOB Transport", _rootPath) + + missionCommands.addCommandForGroup(_groupId, "Unload Vehicles", _vehicleCommandsPath, ctld.unloadTroops, { _unitName, false }) + missionCommands.addCommandForGroup(_groupId, "Load / Extract Vehicles", _vehicleCommandsPath, ctld.loadTroopsFromZone, { _unitName, false,"",true }) + + if ctld.enabledFOBBuilding and ctld.staticBugWorkaround == false then + + missionCommands.addCommandForGroup(_groupId, "Load / Unload FOB Crate", _vehicleCommandsPath, ctld.loadUnloadFOBCrate, { _unitName, false }) + end + missionCommands.addCommandForGroup(_groupId, "Check Cargo", _vehicleCommandsPath, ctld.checkTroopStatus, { _unitName }) + end + + end + + + if ctld.enableCrates and _unitActions.crates then + + if ctld.unitCanCarryVehicles(_unit) == false then + + -- local _cratePath = missionCommands.addSubMenuForGroup(_groupId, "Spawn Crate", _rootPath) + -- add menu for spawning crates + for _subMenuName, _crates in pairs(ctld.spawnableCrates) do + + local _cratePath = missionCommands.addSubMenuForGroup(_groupId, _subMenuName, _rootPath) + for _, _crate in pairs(_crates) do + + if ctld.isJTACUnitType(_crate.unit) == false + or (ctld.isJTACUnitType(_crate.unit) == true and ctld.JTAC_dropEnabled) then + if _crate.side == nil or (_crate.side == _unit:getCoalition()) then + + local _crateRadioMsg = _crate.desc + + --add in the number of crates required to build something + if _crate.cratesRequired ~= nil and _crate.cratesRequired > 1 then + _crateRadioMsg = _crateRadioMsg.." (".._crate.cratesRequired..")" + end + + missionCommands.addCommandForGroup(_groupId,_crateRadioMsg, _cratePath, ctld.spawnCrate, { _unitName, _crate.weight }) + end + end + end + end + end + end + + if (ctld.enabledFOBBuilding or ctld.enableCrates) and _unitActions.crates then + + local _crateCommands = missionCommands.addSubMenuForGroup(_groupId, "CTLD Commands", _rootPath) + if ctld.hoverPickup == false then + if ctld.slingLoad == false then + missionCommands.addCommandForGroup(_groupId, "Load Nearby Crate", _crateCommands, ctld.loadNearbyCrate, _unitName ) + end + end + + missionCommands.addCommandForGroup(_groupId, "Unpack Any Crate", _crateCommands, ctld.unpackCrates, { _unitName }) + + if ctld.slingLoad == false then + missionCommands.addCommandForGroup(_groupId, "Drop Crate", _crateCommands, ctld.dropSlingCrate, { _unitName }) + end + + missionCommands.addCommandForGroup(_groupId, "List Nearby Crates", _crateCommands, ctld.listNearbyCrates, { _unitName }) + + if ctld.enabledFOBBuilding then + missionCommands.addCommandForGroup(_groupId, "List FOBs", _crateCommands, ctld.listFOBS, { _unitName }) + end + end + + + if ctld.enableSmokeDrop then + local _smokeMenu = missionCommands.addSubMenuForGroup(_groupId, "Smoke Markers", _rootPath) + missionCommands.addCommandForGroup(_groupId, "Drop Red Smoke", _smokeMenu, ctld.dropSmoke, { _unitName, trigger.smokeColor.Red }) + missionCommands.addCommandForGroup(_groupId, "Drop Blue Smoke", _smokeMenu, ctld.dropSmoke, { _unitName, trigger.smokeColor.Blue }) + missionCommands.addCommandForGroup(_groupId, "Drop Orange Smoke", _smokeMenu, ctld.dropSmoke, { _unitName, trigger.smokeColor.Orange }) + missionCommands.addCommandForGroup(_groupId, "Drop Green Smoke", _smokeMenu, ctld.dropSmoke, { _unitName, trigger.smokeColor.Green }) + end + + if ctld.enabledRadioBeaconDrop then + local _radioCommands = missionCommands.addSubMenuForGroup(_groupId, "Radio Beacons", _rootPath) + missionCommands.addCommandForGroup(_groupId, "List Beacons", _radioCommands, ctld.listRadioBeacons, { _unitName }) + missionCommands.addCommandForGroup(_groupId, "Drop Beacon", _radioCommands, ctld.dropRadioBeacon, { _unitName }) + missionCommands.addCommandForGroup(_groupId, "Remove Closet Beacon", _radioCommands, ctld.removeRadioBeacon, { _unitName }) + elseif ctld.deployedRadioBeacons ~= {} then + local _radioCommands = missionCommands.addSubMenuForGroup(_groupId, "Radio Beacons", _rootPath) + missionCommands.addCommandForGroup(_groupId, "List Beacons", _radioCommands, ctld.listRadioBeacons, { _unitName }) + end + + ctld.addedTo[tostring(_groupId)] = true + end + end + else + -- env.info(string.format("unit nil %s",_unitName)) + end + end) + + if (not status) then + env.error(string.format("Error adding f10 to transport: %s", error), false) + end + end + + local status, error = pcall(function() + + -- now do any player controlled aircraft that ARENT transport units + if ctld.enabledRadioBeaconDrop then + -- get all BLUE players + ctld.addRadioListCommand(2) + + -- get all RED players + ctld.addRadioListCommand(1) + end + + + if ctld.JTAC_jtacStatusF10 then + -- get all BLUE players + ctld.addJTACRadioCommand(2) + + -- get all RED players + ctld.addJTACRadioCommand(1) + end + + end) + + if (not status) then + env.error(string.format("Error adding f10 to other players: %s", error), false) + end + + +end + +--add to all players that arent transport +function ctld.addRadioListCommand(_side) + + local _players = coalition.getPlayers(_side) + + if _players ~= nil then + + for _, _playerUnit in pairs(_players) do + + local _groupId = ctld.getGroupId(_playerUnit) + + if _groupId then + + if ctld.addedTo[tostring(_groupId)] == nil then + missionCommands.addCommandForGroup(_groupId, "List Radio Beacons", nil, ctld.listRadioBeacons, { _playerUnit:getName() }) + ctld.addedTo[tostring(_groupId)] = true + end + end + end + end +end + +function ctld.addJTACRadioCommand(_side) + + local _players = coalition.getPlayers(_side) + + if _players ~= nil then + + for _, _playerUnit in pairs(_players) do + + local _groupId = ctld.getGroupId(_playerUnit) + + if _groupId then + -- env.info("adding command for "..index) + if ctld.jtacRadioAdded[tostring(_groupId)] == nil then + -- env.info("about command for "..index) + missionCommands.addCommandForGroup(_groupId, "JTAC Status", nil, ctld.getJTACStatus, { _playerUnit:getName() }) + ctld.jtacRadioAdded[tostring(_groupId)] = true + -- env.info("Added command for " .. index) + end + end + + + end + end +end + +function ctld.getGroupId(_unit) + + local _unitDB = mist.DBs.unitsById[tonumber(_unit:getID())] + if _unitDB ~= nil and _unitDB.groupId then + return _unitDB.groupId + end + + return nil +end + +--get distance in meters assuming a Flat world +function ctld.getDistance(_point1, _point2) + + local xUnit = _point1.x + local yUnit = _point1.z + local xZone = _point2.x + local yZone = _point2.z + + local xDiff = xUnit - xZone + local yDiff = yUnit - yZone + + return math.sqrt(xDiff * xDiff + yDiff * yDiff) +end + + +------------ JTAC ----------- + + +ctld.jtacLaserPoints = {} +ctld.jtacIRPoints = {} +ctld.jtacSmokeMarks = {} +ctld.jtacUnits = {} -- list of JTAC units for f10 command +ctld.jtacStop = {} -- jtacs to tell to stop lasing +ctld.jtacCurrentTargets = {} +ctld.jtacRadioAdded = {} --keeps track of who's had the radio command added +ctld.jtacGeneratedLaserCodes = {} -- keeps track of generated codes, cycles when they run out +ctld.jtacLaserPointCodes = {} +ctld.jtacRadioData = {} + +function ctld.JTACAutoLase(_jtacGroupName, _laserCode, _smoke, _lock, _colour, _radio) + ctld.logDebug(string.format("ctld.JTACAutoLase(_jtacGroupName=%s, _laserCode=%s", ctld.p(_jtacGroupName), ctld.p(_laserCode))) + + local _radio = _radio + if not _radio then + _radio = {} + if _laserCode then + local _laserCode = tonumber(_laserCode) + if _laserCode and _laserCode >= 1111 and _laserCode <= 1688 then + local _laserB = math.floor((_laserCode - 1000)/100) + local _laserCD = _laserCode - 1000 - _laserB*100 + local _frequency = tostring(30+_laserB+_laserCD*0.05) + ctld.logTrace(string.format("_laserB=%s", ctld.p(_laserB))) + ctld.logTrace(string.format("_laserCD=%s", ctld.p(_laserCD))) + ctld.logTrace(string.format("_frequency=%s", ctld.p(_frequency))) + _radio.freq = _frequency + _radio.mod = "fm" + end + end + end + + if _radio and not _radio.name then + _radio.name = _jtacGroupName + end + + if ctld.jtacStop[_jtacGroupName] == true then + ctld.jtacStop[_jtacGroupName] = nil -- allow it to be started again + ctld.cleanupJTAC(_jtacGroupName) + return + end + + if _lock == nil then + + _lock = ctld.JTAC_lock + end + + + ctld.jtacLaserPointCodes[_jtacGroupName] = _laserCode + ctld.jtacRadioData[_jtacGroupName] = _radio + + local _jtacGroup = ctld.getGroup(_jtacGroupName) + local _jtacUnit + + if _jtacGroup == nil or #_jtacGroup == 0 then + + --check not in a heli + if ctld.inTransitTroops then + for _, _onboard in pairs(ctld.inTransitTroops) do + if _onboard ~= nil then + if _onboard.troops ~= nil and _onboard.troops.groupName ~= nil and _onboard.troops.groupName == _jtacGroupName then + + --jtac soldier being transported by heli + ctld.cleanupJTAC(_jtacGroupName) + + env.info(_jtacGroupName .. ' in Transport - Waiting 10 seconds') + timer.scheduleFunction(ctld.timerJTACAutoLase, { _jtacGroupName, _laserCode, _smoke, _lock, _colour, _radio }, timer.getTime() + 10) + return + end + + if _onboard.vehicles ~= nil and _onboard.vehicles.groupName ~= nil and _onboard.vehicles.groupName == _jtacGroupName then + --jtac vehicle being transported by heli + ctld.cleanupJTAC(_jtacGroupName) + + env.info(_jtacGroupName .. ' in Transport - Waiting 10 seconds') + timer.scheduleFunction(ctld.timerJTACAutoLase, { _jtacGroupName, _laserCode, _smoke, _lock, _colour, _radio }, timer.getTime() + 10) + return + end + end + end + end + + + if ctld.jtacUnits[_jtacGroupName] ~= nil then + ctld.notifyCoalition("JTAC Group " .. _jtacGroupName .. " KIA!", 10, ctld.jtacUnits[_jtacGroupName].side, _radio) + end + + --remove from list + ctld.jtacUnits[_jtacGroupName] = nil + + ctld.cleanupJTAC(_jtacGroupName) + + return + else + + _jtacUnit = _jtacGroup[1] + --add to list + ctld.jtacUnits[_jtacGroupName] = { name = _jtacUnit:getName(), side = _jtacUnit:getCoalition(), radio = _radio } + + -- work out smoke colour + if _colour == nil then + + if _jtacUnit:getCoalition() == 1 then + _colour = ctld.JTAC_smokeColour_RED + else + _colour = ctld.JTAC_smokeColour_BLUE + end + end + + + if _smoke == nil then + + if _jtacUnit:getCoalition() == 1 then + _smoke = ctld.JTAC_smokeOn_RED + else + _smoke = ctld.JTAC_smokeOn_BLUE + end + end + end + + + -- search for current unit + + if _jtacUnit:isActive() == false then + + ctld.cleanupJTAC(_jtacGroupName) + + env.info(_jtacGroupName .. ' Not Active - Waiting 30 seconds') + timer.scheduleFunction(ctld.timerJTACAutoLase, { _jtacGroupName, _laserCode, _smoke, _lock, _colour, _radio }, timer.getTime() + 30) + + return + end + + local _enemyUnit = ctld.getCurrentUnit(_jtacUnit, _jtacGroupName) + local targetDestroyed = false + local targetLost = false + + if _enemyUnit == nil and ctld.jtacCurrentTargets[_jtacGroupName] ~= nil then + + local _tempUnitInfo = ctld.jtacCurrentTargets[_jtacGroupName] + + -- env.info("TEMP UNIT INFO: " .. tempUnitInfo.name .. " " .. tempUnitInfo.unitType) + + local _tempUnit = Unit.getByName(_tempUnitInfo.name) + + if _tempUnit ~= nil and _tempUnit:getLife() > 0 and _tempUnit:isActive() == true then + targetLost = true + else + targetDestroyed = true + end + + --remove from smoke list + ctld.jtacSmokeMarks[_tempUnitInfo.name] = nil + + -- JTAC Unit: resume his route ------------ + trigger.action.groupContinueMoving(Group.getByName(_jtacGroupName)) + + -- remove from target list + ctld.jtacCurrentTargets[_jtacGroupName] = nil + + --stop lasing + ctld.cancelLase(_jtacGroupName) + end + + + if _enemyUnit == nil then + _enemyUnit = ctld.findNearestVisibleEnemy(_jtacUnit, _lock) + + if _enemyUnit ~= nil then + + -- store current target for easy lookup + ctld.jtacCurrentTargets[_jtacGroupName] = { name = _enemyUnit:getName(), unitType = _enemyUnit:getTypeName(), unitId = _enemyUnit:getID() } + local action = ", lasing new target, " + if targetLost then + action = ", target lost " .. action + targetLost = false + elseif targetDestroyed then + action = ", target destroyed " .. action + targetDestroyed = false + end + + local message = _jtacGroupName .. action .. _enemyUnit:getTypeName() + local fullMessage = message .. '. CODE: ' .. _laserCode .. ". POSITION: " .. ctld.getPositionString(_enemyUnit) + ctld.notifyCoalition(fullMessage, 10, _jtacUnit:getCoalition(), _radio, message) + + -- JTAC Unit stop his route ----------------- + trigger.action.groupStopMoving(Group.getByName(_jtacGroupName)) -- stop JTAC + + -- create smoke + if _smoke == true then + + --create first smoke + ctld.createSmokeMarker(_enemyUnit, _colour) + end + end + end + + if _enemyUnit ~= nil then + + ctld.laseUnit(_enemyUnit, _jtacUnit, _jtacGroupName, _laserCode) + + -- env.info('Timer timerSparkleLase '..jtacGroupName.." "..laserCode.." "..enemyUnit:getName()) + timer.scheduleFunction(ctld.timerJTACAutoLase, { _jtacGroupName, _laserCode, _smoke, _lock, _colour, _radio }, timer.getTime() + 15) + + + if _smoke == true then + local _nextSmokeTime = ctld.jtacSmokeMarks[_enemyUnit:getName()] + + --recreate smoke marker after 5 mins + if _nextSmokeTime ~= nil and _nextSmokeTime < timer.getTime() then + + ctld.createSmokeMarker(_enemyUnit, _colour) + end + end + + else + -- env.info('LASE: No Enemies Nearby') + + -- stop lazing the old spot + ctld.cancelLase(_jtacGroupName) + -- env.info('Timer Slow timerSparkleLase '..jtacGroupName.." "..laserCode.." "..enemyUnit:getName()) + + timer.scheduleFunction(ctld.timerJTACAutoLase, { _jtacGroupName, _laserCode, _smoke, _lock, _colour, _radio }, timer.getTime() + 5) + end + + if targetLost then + ctld.notifyCoalition(_jtacGroupName .. ", target lost.", 10, _jtacUnit:getCoalition(), _radio) + elseif targetDestroyed then + ctld.notifyCoalition(_jtacGroupName .. ", target destroyed.", 10, _jtacUnit:getCoalition(), _radio) + end +end + +function ctld.JTACAutoLaseStop(_jtacGroupName) + ctld.jtacStop[_jtacGroupName] = true +end + +-- used by the timer function +function ctld.timerJTACAutoLase(_args) + + ctld.JTACAutoLase(_args[1], _args[2], _args[3], _args[4], _args[5], _args[6]) +end + +function ctld.cleanupJTAC(_jtacGroupName) + -- clear laser - just in case + ctld.cancelLase(_jtacGroupName) + + -- Cleanup + ctld.jtacUnits[_jtacGroupName] = nil + + ctld.jtacCurrentTargets[_jtacGroupName] = nil + + ctld.jtacRadioData[_jtacGroupName] = nil +end + + +--- send a message to the coalition +--- if _radio is set, the message will be read out loud via SRS +function ctld.notifyCoalition(_message, _displayFor, _side, _radio, _shortMessage) + ctld.logDebug(string.format("ctld.notifyCoalition(_message=%s)", ctld.p(_message))) + ctld.logTrace(string.format("_radio=%s", ctld.p(_radio))) + + local _shortMessage = _shortMessage + if _shortMessage == nil then + _shortMessage = _message + end + + if STTS and STTS.TextToSpeech and _radio and _radio.freq then + local _freq = _radio.freq + local _modulation = _radio.mod or "FM" + local _volume = _radio.volume or "1.0" + local _name = _radio.name or "JTAC" + local _gender = _radio.gender or "male" + local _culture = _radio.culture or "en-US" + local _voice = _radio.voice + local _googleTTS = _radio.googleTTS or false + ctld.logTrace(string.format("calling STTS.TextToSpeech(%s)", ctld.p(_shortMessage))) + ctld.logTrace(string.format("_freq=%s", ctld.p(_freq))) + ctld.logTrace(string.format("_modulation=%s", ctld.p(_modulation))) + ctld.logTrace(string.format("_volume=%s", ctld.p(_volume))) + ctld.logTrace(string.format("_name=%s", ctld.p(_name))) + ctld.logTrace(string.format("_gender=%s", ctld.p(_gender))) + ctld.logTrace(string.format("_culture=%s", ctld.p(_culture))) + ctld.logTrace(string.format("_voice=%s", ctld.p(_voice))) + ctld.logTrace(string.format("_googleTTS=%s", ctld.p(_googleTTS))) + STTS.TextToSpeech(_shortMessage, _freq, _modulation, _volume, _name, _side, nil, 1, _gender, _culture, _voice, _googleTTS) + end + + trigger.action.outTextForCoalition(_side, _message, _displayFor) + trigger.action.outSoundForCoalition(_side, "radiobeep.ogg") +end + +function ctld.createSmokeMarker(_enemyUnit, _colour) + + --recreate in 5 mins + ctld.jtacSmokeMarks[_enemyUnit:getName()] = timer.getTime() + 300.0 + + -- move smoke 2 meters above target for ease + local _enemyPoint = _enemyUnit:getPoint() + trigger.action.smoke({ x = _enemyPoint.x, y = _enemyPoint.y + 2.0, z = _enemyPoint.z }, _colour) +end + +function ctld.cancelLase(_jtacGroupName) + + --local index = "JTAC_"..jtacUnit:getID() + + local _tempLase = ctld.jtacLaserPoints[_jtacGroupName] + + if _tempLase ~= nil then + Spot.destroy(_tempLase) + ctld.jtacLaserPoints[_jtacGroupName] = nil + + -- env.info('Destroy laze '..index) + + _tempLase = nil + end + + local _tempIR = ctld.jtacIRPoints[_jtacGroupName] + + if _tempIR ~= nil then + Spot.destroy(_tempIR) + ctld.jtacIRPoints[_jtacGroupName] = nil + + -- env.info('Destroy laze '..index) + + _tempIR = nil + end +end + +function ctld.laseUnit(_enemyUnit, _jtacUnit, _jtacGroupName, _laserCode) + + --cancelLase(jtacGroupName) + + local _spots = {} + + local _enemyVector = _enemyUnit:getPoint() + local _enemyVectorUpdated = { x = _enemyVector.x, y = _enemyVector.y + 2.0, z = _enemyVector.z } + + local _oldLase = ctld.jtacLaserPoints[_jtacGroupName] + local _oldIR = ctld.jtacIRPoints[_jtacGroupName] + + if _oldLase == nil or _oldIR == nil then + + -- create lase + + local _status, _result = pcall(function() + _spots['irPoint'] = Spot.createInfraRed(_jtacUnit, { x = 0, y = 2.0, z = 0 }, _enemyVectorUpdated) + _spots['laserPoint'] = Spot.createLaser(_jtacUnit, { x = 0, y = 2.0, z = 0 }, _enemyVectorUpdated, _laserCode) + return _spots + end) + + if not _status then + env.error('ERROR: ' .. _result, false) + else + if _result.irPoint then + + -- env.info(jtacUnit:getName() .. ' placed IR Pointer on '..enemyUnit:getName()) + + ctld.jtacIRPoints[_jtacGroupName] = _result.irPoint --store so we can remove after + end + if _result.laserPoint then + + -- env.info(jtacUnit:getName() .. ' is Lasing '..enemyUnit:getName()..'. CODE:'..laserCode) + + ctld.jtacLaserPoints[_jtacGroupName] = _result.laserPoint + end + end + + else + + -- update lase + + if _oldLase ~= nil then + _oldLase:setPoint(_enemyVectorUpdated) + end + + if _oldIR ~= nil then + _oldIR:setPoint(_enemyVectorUpdated) + end + end +end + +-- get currently selected unit and check they're still in range +function ctld.getCurrentUnit(_jtacUnit, _jtacGroupName) + + + local _unit = nil + + if ctld.jtacCurrentTargets[_jtacGroupName] ~= nil then + _unit = Unit.getByName(ctld.jtacCurrentTargets[_jtacGroupName].name) + end + + local _tempPoint = nil + local _tempDist = nil + local _tempPosition = nil + + local _jtacPosition = _jtacUnit:getPosition() + local _jtacPoint = _jtacUnit:getPoint() + + if _unit ~= nil and _unit:getLife() > 0 and _unit:isActive() == true then + + -- calc distance + _tempPoint = _unit:getPoint() + -- tempPosition = unit:getPosition() + + _tempDist = ctld.getDistance(_unit:getPoint(), _jtacUnit:getPoint()) + if _tempDist < ctld.JTAC_maxDistance then + -- calc visible + + -- check slightly above the target as rounding errors can cause issues, plus the unit has some height anyways + local _offsetEnemyPos = { x = _tempPoint.x, y = _tempPoint.y + 2.0, z = _tempPoint.z } + local _offsetJTACPos = { x = _jtacPoint.x, y = _jtacPoint.y + 2.0, z = _jtacPoint.z } + + if land.isVisible(_offsetEnemyPos, _offsetJTACPos) then + return _unit + end + end + end + return nil +end + + +-- Find nearest enemy to JTAC that isn't blocked by terrain +function ctld.findNearestVisibleEnemy(_jtacUnit, _targetType,_distance) + + --local startTime = os.clock() + + local _maxDistance = _distance or ctld.JTAC_maxDistance + + local _nearestDistance = _maxDistance + + local _jtacPoint = _jtacUnit:getPoint() + local _coa = _jtacUnit:getCoalition() + + local _offsetJTACPos = { x = _jtacPoint.x, y = _jtacPoint.y + 2.0, z = _jtacPoint.z } + + local _volume = { + id = world.VolumeType.SPHERE, + params = { + point = _offsetJTACPos, + radius = _maxDistance + } + } + + local _unitList = {} + + + local _search = function(_unit, _coa) + pcall(function() + + if _unit ~= nil + and _unit:getLife() > 0 + and _unit:isActive() + and _unit:getCoalition() ~= _coa + and not _unit:inAir() + and not ctld.alreadyTarget(_jtacUnit,_unit) then + + local _tempPoint = _unit:getPoint() + local _offsetEnemyPos = { x = _tempPoint.x, y = _tempPoint.y + 2.0, z = _tempPoint.z } + + if land.isVisible(_offsetJTACPos,_offsetEnemyPos ) then + + local _dist = ctld.getDistance(_offsetJTACPos, _offsetEnemyPos) + + if _dist < _maxDistance then + table.insert(_unitList,{unit=_unit, dist=_dist}) + + end + end + end + end) + + return true + end + + world.searchObjects(Object.Category.UNIT, _volume, _search, _coa) + + --log.info(string.format("JTAC Search elapsed time: %.4f\n", os.clock() - startTime)) + + -- generate list order by distance & visible + + -- first check + -- hpriority + -- priority + -- vehicle + -- unit + + local _sort = function( a,b ) return a.dist < b.dist end + table.sort(_unitList,_sort) + -- sort list + + -- check for hpriority + for _, _enemyUnit in ipairs(_unitList) do + local _enemyName = _enemyUnit.unit:getName() + + if string.match(_enemyName, "hpriority") then + return _enemyUnit.unit + end + end + + for _, _enemyUnit in ipairs(_unitList) do + local _enemyName = _enemyUnit.unit:getName() + + if string.match(_enemyName, "priority") then + return _enemyUnit.unit + end + end + + local result = nil + for _, _enemyUnit in ipairs(_unitList) do + local _enemyName = _enemyUnit.unit:getName() + --log.info(string.format("CTLD - checking _enemyName=%s", _enemyName)) + + -- check for air defenses + --log.info(string.format("CTLD - _enemyUnit.unit:getDesc()[attributes]=%s", ctld.p(_enemyUnit.unit:getDesc()["attributes"]))) + local airdefense = (_enemyUnit.unit:getDesc()["attributes"]["Air Defence"] ~= nil) + --log.info(string.format("CTLD - airdefense=%s", tostring(airdefense))) + + if (_targetType == "vehicle" and ctld.isVehicle(_enemyUnit.unit)) or _targetType == "all" then + if airdefense then + return _enemyUnit.unit + else + result = _enemyUnit.unit + end + + elseif (_targetType == "troop" and ctld.isInfantry(_enemyUnit.unit)) or _targetType == "all" then + if airdefense then + return _enemyUnit.unit + else + result = _enemyUnit.unit + end + end + end + + return result + +end + + +function ctld.listNearbyEnemies(_jtacUnit) + + local _maxDistance = ctld.JTAC_maxDistance + + local _jtacPoint = _jtacUnit:getPoint() + local _coa = _jtacUnit:getCoalition() + + local _offsetJTACPos = { x = _jtacPoint.x, y = _jtacPoint.y + 2.0, z = _jtacPoint.z } + + local _volume = { + id = world.VolumeType.SPHERE, + params = { + point = _offsetJTACPos, + radius = _maxDistance + } + } + local _enemies = nil + + local _search = function(_unit, _coa) + pcall(function() + + if _unit ~= nil + and _unit:getLife() > 0 + and _unit:isActive() + and _unit:getCoalition() ~= _coa + and not _unit:inAir() then + + local _tempPoint = _unit:getPoint() + local _offsetEnemyPos = { x = _tempPoint.x, y = _tempPoint.y + 2.0, z = _tempPoint.z } + + if land.isVisible(_offsetJTACPos,_offsetEnemyPos ) then + + if not _enemies then + _enemies = {} + end + + _enemies[_unit:getTypeName()] = _unit:getTypeName() + + end + end + end) + + return true + end + + world.searchObjects(Object.Category.UNIT, _volume, _search, _coa) + + return _enemies +end + +-- tests whether the unit is targeted by another JTAC +function ctld.alreadyTarget(_jtacUnit, _enemyUnit) + + for _, _jtacTarget in pairs(ctld.jtacCurrentTargets) do + + if _jtacTarget.unitId == _enemyUnit:getID() then + -- env.info("ALREADY TARGET") + return true + end + end + + return false +end + + +-- Returns only alive units from group but the group / unit may not be active + +function ctld.getGroup(groupName) + + local _groupUnits = Group.getByName(groupName) + + local _filteredUnits = {} --contains alive units + local _x = 1 + + if _groupUnits ~= nil and _groupUnits:isExist() then + + _groupUnits = _groupUnits:getUnits() + + if _groupUnits ~= nil and #_groupUnits > 0 then + for _x = 1, #_groupUnits do + if _groupUnits[_x]:getLife() > 0 then -- removed and _groupUnits[_x]:isExist() as isExist doesnt work on single units! + table.insert(_filteredUnits, _groupUnits[_x]) + end + end + end + end + + return _filteredUnits +end + +function ctld.getAliveGroup(_groupName) + + local _group = Group.getByName(_groupName) + + if _group and _group:isExist() == true and #_group:getUnits() > 0 then + return _group + end + + return nil +end + +-- gets the JTAC status and displays to coalition units +function ctld.getJTACStatus(_args) + + --returns the status of all JTAC units + + local _playerUnit = ctld.getTransportUnit(_args[1]) + + if _playerUnit == nil then + return + end + + local _side = _playerUnit:getCoalition() + + local _jtacGroupName = nil + local _jtacUnit = nil + + local _message = "JTAC STATUS: \n\n" + + for _jtacGroupName, _jtacDetails in pairs(ctld.jtacUnits) do + + --look up units + _jtacUnit = Unit.getByName(_jtacDetails.name) + + if _jtacUnit ~= nil and _jtacUnit:getLife() > 0 and _jtacUnit:isActive() == true and _jtacUnit:getCoalition() == _side then + + local _enemyUnit = ctld.getCurrentUnit(_jtacUnit, _jtacGroupName) + + local _laserCode = ctld.jtacLaserPointCodes[_jtacGroupName] + + local _start = _jtacGroupName + if (_jtacDetails.radio) then + _start = _start .. ", available on ".._jtacDetails.radio.freq.." ".._jtacDetails.radio.mod .."," + end + + if _laserCode == nil then + _laserCode = "UNKNOWN" + end + + if _enemyUnit ~= nil and _enemyUnit:getLife() > 0 and _enemyUnit:isActive() == true then + _message = _message .. "" .. _start .. " targeting " .. _enemyUnit:getTypeName() .. " CODE: " .. _laserCode .. ctld.getPositionString(_enemyUnit) .. "\n" + + local _list = ctld.listNearbyEnemies(_jtacUnit) + + if _list then + _message = _message.."Visual On: " + + for _,_type in pairs(_list) do + _message = _message.._type.." " + end + _message = _message.."\n" + end + + else + _message = _message .. "" .. _start .. " searching for targets" .. ctld.getPositionString(_jtacUnit) .. "\n" + end + end + end + + if _message == "JTAC STATUS: \n\n" then + _message = "No Active JTACs" + end + + + ctld.notifyCoalition(_message, 10, _side) +end + + + +function ctld.isInfantry(_unit) + + local _typeName = _unit:getTypeName() + + --type coerce tostring + _typeName = string.lower(_typeName .. "") + + local _soldierType = { "infantry", "paratrooper", "stinger", "manpad", "mortar" } + + for _key, _value in pairs(_soldierType) do + if string.match(_typeName, _value) then + return true + end + end + + return false +end + +-- assume anything that isnt soldier is vehicle +function ctld.isVehicle(_unit) + + if ctld.isInfantry(_unit) then + return false + end + + return true +end + +-- The entered value can range from 1111 - 1788, +-- -- but the first digit of the series must be a 1 or 2 +-- -- and the last three digits must be between 1 and 8. +-- The range used to be bugged so its not 1 - 8 but 0 - 7. +-- function below will use the range 1-7 just incase +function ctld.generateLaserCode() + + ctld.jtacGeneratedLaserCodes = {} + + -- generate list of laser codes + local _code = 1111 + + local _count = 1 + + while _code < 1777 and _count < 30 do + + while true do + + _code = _code + 1 + + if not ctld.containsDigit(_code, 8) + and not ctld.containsDigit(_code, 9) + and not ctld.containsDigit(_code, 0) then + + table.insert(ctld.jtacGeneratedLaserCodes, _code) + + --env.info(_code.." Code") + break + end + end + _count = _count + 1 + end +end + +function ctld.containsDigit(_number, _numberToFind) + + local _thisNumber = _number + local _thisDigit = 0 + + while _thisNumber ~= 0 do + + _thisDigit = _thisNumber % 10 + _thisNumber = math.floor(_thisNumber / 10) + + if _thisDigit == _numberToFind then + return true + end + end + + return false +end + +-- 200 - 400 in 10KHz +-- 400 - 850 in 10 KHz +-- 850 - 1250 in 50 KHz +function ctld.generateVHFrequencies() + + --ignore list + --list of all frequencies in KHZ that could conflict with + -- 191 - 1290 KHz, beacon range + local _skipFrequencies = { + 745, --Astrahan + 381, + 384, + 300.50, + 312.5, + 1175, + 342, + 735, + 300.50, + 353.00, + 440, + 795, + 525, + 520, + 690, + 625, + 291.5, + 300.50, + 435, + 309.50, + 920, + 1065, + 274, + 312.50, + 580, + 602, + 297.50, + 750, + 485, + 950, + 214, + 1025, 730, 995, 455, 307, 670, 329, 395, 770, + 380, 705, 300.5, 507, 740, 1030, 515, + 330, 309.5, + 348, 462, 905, 352, 1210, 942, 435, + 324, + 320, 420, 311, 389, 396, 862, 680, 297.5, + 920, 662, + 866, 907, 309.5, 822, 515, 470, 342, 1182, 309.5, 720, 528, + 337, 312.5, 830, 740, 309.5, 641, 312, 722, 682, 1050, + 1116, 935, 1000, 430, 577, + 326 -- Nevada + } + + ctld.freeVHFFrequencies = {} + local _start = 200000 + + -- first range + while _start < 400000 do + + -- skip existing NDB frequencies + local _found = false + for _, value in pairs(_skipFrequencies) do + if value * 1000 == _start then + _found = true + break + end + end + + + if _found == false then + table.insert(ctld.freeVHFFrequencies, _start) + end + + _start = _start + 10000 + end + + _start = 400000 + -- second range + while _start < 850000 do + + -- skip existing NDB frequencies + local _found = false + for _, value in pairs(_skipFrequencies) do + if value * 1000 == _start then + _found = true + break + end + end + + if _found == false then + table.insert(ctld.freeVHFFrequencies, _start) + end + + + _start = _start + 10000 + end + + _start = 850000 + -- third range + while _start <= 1250000 do + + -- skip existing NDB frequencies + local _found = false + for _, value in pairs(_skipFrequencies) do + if value * 1000 == _start then + _found = true + break + end + end + + if _found == false then + table.insert(ctld.freeVHFFrequencies, _start) + end + + _start = _start + 50000 + end +end + +-- 220 - 399 MHZ, increments of 0.5MHZ +function ctld.generateUHFrequencies() + + ctld.freeUHFFrequencies = {} + local _start = 220000000 + + while _start < 399000000 do + table.insert(ctld.freeUHFFrequencies, _start) + _start = _start + 500000 + end +end + + +-- 220 - 399 MHZ, increments of 0.5MHZ +-- -- first digit 3-7MHz +-- -- second digit 0-5KHz +-- -- third digit 0-9 +-- -- fourth digit 0 or 5 +-- -- times by 10000 +-- +function ctld.generateFMFrequencies() + + ctld.freeFMFrequencies = {} + local _start = 220000000 + + while _start < 399000000 do + + _start = _start + 500000 + end + + for _first = 3, 7 do + for _second = 0, 5 do + for _third = 0, 9 do + local _frequency = ((100 * _first) + (10 * _second) + _third) * 100000 --extra 0 because we didnt bother with 4th digit + table.insert(ctld.freeFMFrequencies, _frequency) + end + end + end +end + +function ctld.getPositionString(_unit) + + if ctld.JTAC_location == false then + return "" + end + + local _lat, _lon = coord.LOtoLL(_unit:getPosition().p) + + local _latLngStr = mist.tostringLL(_lat, _lon, 3, ctld.location_DMS) + + local _mgrsString = mist.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(_unit:getPosition().p)), 5) + + return " @ " .. _latLngStr .. " - MGRS " .. _mgrsString +end + + +-- ***************** SETUP SCRIPT **************** +function ctld.initialize(force) + ctld.logInfo(string.format("Initializing version %s", ctld.Version)) + ctld.logTrace(string.format("ctld.alreadyInitialized=%s", ctld.p(ctld.alreadyInitialized))) + ctld.logTrace(string.format("force=%s", ctld.p(force))) + + if ctld.alreadyInitialized and not force then + ctld.logInfo(string.format("Bypassing initialization because ctld.alreadyInitialized = true")) + return + end + + assert(mist ~= nil, "\n\n** HEY MISSION-DESIGNER! **\n\nMiST has not been loaded!\n\nMake sure MiST 3.6 or higher is running\n*before* running this script!\n") + + ctld.addedTo = {} + ctld.spawnedCratesRED = {} -- use to store crates that have been spawned + ctld.spawnedCratesBLUE = {} -- use to store crates that have been spawned + + ctld.droppedTroopsRED = {} -- stores dropped troop groups + ctld.droppedTroopsBLUE = {} -- stores dropped troop groups + + ctld.droppedVehiclesRED = {} -- stores vehicle groups for c-130 / hercules + ctld.droppedVehiclesBLUE = {} -- stores vehicle groups for c-130 / hercules + + ctld.inTransitTroops = {} + + ctld.inTransitFOBCrates = {} + + ctld.inTransitSlingLoadCrates = {} -- stores crates that are being transported by helicopters for alternative to real slingload + + ctld.droppedFOBCratesRED = {} + ctld.droppedFOBCratesBLUE = {} + + ctld.builtFOBS = {} -- stores fully built fobs + + ctld.completeAASystems = {} -- stores complete spawned groups from multiple crates + + ctld.fobBeacons = {} -- stores FOB radio beacon details, refreshed every 60 seconds + + ctld.deployedRadioBeacons = {} -- stores details of deployed radio beacons + + ctld.beaconCount = 1 + + ctld.usedUHFFrequencies = {} + ctld.usedVHFFrequencies = {} + ctld.usedFMFrequencies = {} + + ctld.freeUHFFrequencies = {} + ctld.freeVHFFrequencies = {} + ctld.freeFMFrequencies = {} + + --used to lookup what the crate will contain + ctld.crateLookupTable = {} + + ctld.extractZones = {} -- stored extract zones + + ctld.missionEditorCargoCrates = {} --crates added by mission editor for triggering cratesinzone + ctld.hoverStatus = {} -- tracks status of a helis hover above a crate + + ctld.callbacks = {} -- function callback + + + -- Remove intransit troops when heli / cargo plane dies + --ctld.eventHandler = {} + --function ctld.eventHandler:onEvent(_event) + -- + -- if _event == nil or _event.initiator == nil then + -- env.info("CTLD null event") + -- elseif _event.id == 9 then + -- -- Pilot dead + -- ctld.inTransitTroops[_event.initiator:getName()] = nil + -- + -- elseif world.event.S_EVENT_EJECTION == _event.id or _event.id == 8 then + -- -- env.info("Event unit - Pilot Ejected or Unit Dead") + -- ctld.inTransitTroops[_event.initiator:getName()] = nil + -- + -- -- env.info(_event.initiator:getName()) + -- end + -- + --end + + -- create crate lookup table + for _subMenuName, _crates in pairs(ctld.spawnableCrates) do + + for _, _crate in pairs(_crates) do + -- convert number to string otherwise we'll have a pointless giant + -- table. String means 'hashmap' so it will only contain the right number of elements + ctld.crateLookupTable[tostring(_crate.weight)] = _crate + end + end + + + --sort out pickup zones + for _, _zone in pairs(ctld.pickupZones) do + + local _zoneName = _zone[1] + local _zoneColor = _zone[2] + local _zoneActive = _zone[4] + + if _zoneColor == "green" then + _zone[2] = trigger.smokeColor.Green + elseif _zoneColor == "red" then + _zone[2] = trigger.smokeColor.Red + elseif _zoneColor == "white" then + _zone[2] = trigger.smokeColor.White + elseif _zoneColor == "orange" then + _zone[2] = trigger.smokeColor.Orange + elseif _zoneColor == "blue" then + _zone[2] = trigger.smokeColor.Blue + else + _zone[2] = -1 -- no smoke colour + end + + -- add in counter for troops or units + if _zone[3] == -1 then + _zone[3] = 10000; + end + + -- change active to 1 / 0 + if _zoneActive == "yes" then + _zone[4] = 1 + else + _zone[4] = 0 + end + end + + --sort out dropoff zones + for _, _zone in pairs(ctld.dropOffZones) do + + local _zoneColor = _zone[2] + + if _zoneColor == "green" then + _zone[2] = trigger.smokeColor.Green + elseif _zoneColor == "red" then + _zone[2] = trigger.smokeColor.Red + elseif _zoneColor == "white" then + _zone[2] = trigger.smokeColor.White + elseif _zoneColor == "orange" then + _zone[2] = trigger.smokeColor.Orange + elseif _zoneColor == "blue" then + _zone[2] = trigger.smokeColor.Blue + else + _zone[2] = -1 -- no smoke colour + end + + --mark as active for refresh smoke logic to work + _zone[4] = 1 + end + + --sort out waypoint zones + for _, _zone in pairs(ctld.wpZones) do + + local _zoneColor = _zone[2] + + if _zoneColor == "green" then + _zone[2] = trigger.smokeColor.Green + elseif _zoneColor == "red" then + _zone[2] = trigger.smokeColor.Red + elseif _zoneColor == "white" then + _zone[2] = trigger.smokeColor.White + elseif _zoneColor == "orange" then + _zone[2] = trigger.smokeColor.Orange + elseif _zoneColor == "blue" then + _zone[2] = trigger.smokeColor.Blue + else + _zone[2] = -1 -- no smoke colour + end + + --mark as active for refresh smoke logic to work + -- change active to 1 / 0 + if _zone[3] == "yes" then + _zone[3] = 1 + else + _zone[3] = 0 + end + end + + -- Sort out extractable groups + for _, _groupName in pairs(ctld.extractableGroups) do + + local _group = Group.getByName(_groupName) + + if _group ~= nil then + + if _group:getCoalition() == 1 then + table.insert(ctld.droppedTroopsRED, _group:getName()) + else + table.insert(ctld.droppedTroopsBLUE, _group:getName()) + end + end + end + + + -- Seperate troop teams into red and blue for random AI pickups + if ctld.allowRandomAiTeamPickups == true then + ctld.redTeams = {} + ctld.blueTeams = {} + for _,_loadGroup in pairs(ctld.loadableGroups) do + if not _loadGroup.side then + table.insert(ctld.redTeams, _) + table.insert(ctld.blueTeams, _) + elseif _loadGroup.side == 1 then + table.insert(ctld.redTeams, _) + elseif _loadGroup.side == 2 then + table.insert(ctld.blueTeams, _) + end + end + end + + -- add total count + + for _,_loadGroup in pairs(ctld.loadableGroups) do + + _loadGroup.total = 0 + if _loadGroup.aa then + _loadGroup.total = _loadGroup.aa + _loadGroup.total + end + + if _loadGroup.inf then + _loadGroup.total = _loadGroup.inf + _loadGroup.total + end + + + if _loadGroup.mg then + _loadGroup.total = _loadGroup.mg + _loadGroup.total + end + + if _loadGroup.at then + _loadGroup.total = _loadGroup.at + _loadGroup.total + end + + if _loadGroup.mortar then + _loadGroup.total = _loadGroup.mortar + _loadGroup.total + end + + end + + + -- Scheduled functions (run cyclically) -- but hold execution for a second so we can override parts + + timer.scheduleFunction(ctld.checkAIStatus, nil, timer.getTime() + 1) + timer.scheduleFunction(ctld.checkTransportStatus, nil, timer.getTime() + 5) + + timer.scheduleFunction(function() + + timer.scheduleFunction(ctld.refreshRadioBeacons, nil, timer.getTime() + 5) + timer.scheduleFunction(ctld.refreshSmoke, nil, timer.getTime() + 5) + timer.scheduleFunction(ctld.addF10MenuOptions, nil, timer.getTime() + 5) + + if ctld.enableCrates == true and ctld.slingLoad == false and ctld.hoverPickup == true then + timer.scheduleFunction(ctld.checkHoverStatus, nil, timer.getTime() + 1) + end + + end,nil, timer.getTime()+1 ) + + --event handler for deaths + --world.addEventHandler(ctld.eventHandler) + + --env.info("CTLD event handler added") + + env.info("Generating Laser Codes") + ctld.generateLaserCode() + env.info("Generated Laser Codes") + + + + env.info("Generating UHF Frequencies") + ctld.generateUHFrequencies() + env.info("Generated UHF Frequencies") + + env.info("Generating VHF Frequencies") + ctld.generateVHFrequencies() + env.info("Generated VHF Frequencies") + + + env.info("Generating FM Frequencies") + ctld.generateFMFrequencies() + env.info("Generated FM Frequencies") + + -- Search for crates + -- Crates are NOT returned by coalition.getStaticObjects() for some reason + -- Search for crates in the mission editor instead + env.info("Searching for Crates") + for _coalitionName, _coalitionData in pairs(env.mission.coalition) do + + if (_coalitionName == 'red' or _coalitionName == 'blue') + and type(_coalitionData) == 'table' then + if _coalitionData.country then --there is a country table + for _, _countryData in pairs(_coalitionData.country) do + + if type(_countryData) == 'table' then + for _objectTypeName, _objectTypeData in pairs(_countryData) do + if _objectTypeName == "static" then + + if ((type(_objectTypeData) == 'table') + and _objectTypeData.group + and (type(_objectTypeData.group) == 'table') + and (#_objectTypeData.group > 0)) then + + for _groupId, _group in pairs(_objectTypeData.group) do + if _group and _group.units and type(_group.units) == 'table' then + for _unitNum, _unit in pairs(_group.units) do + if _unit.canCargo == true then + local _cargoName = env.getValueDictByKey(_unit.name) + ctld.missionEditorCargoCrates[_cargoName] = _cargoName + env.info("Crate Found: " .. _unit.name.." - Unit: ".._cargoName) + end + end + end + end + end + end + end + end + end + end + end + end + env.info("END search for crates") + + -- don't initialize more than once + ctld.alreadyInitialized = true + + env.info("CTLD READY") +end + + +-- initialize the random number generator to make it almost random +math.random(); math.random(); math.random() + +--- Enable/Disable error boxes displayed on screen. +env.setErrorMessageBoxEnabled(false) + +-- initialize CTLD in 2 seconds, so other scripts have a chance to modify the configuration before initialization +ctld.logInfo(string.format("Loading version %s in 2 seconds", ctld.Version)) +timer.scheduleFunction(ctld.initialize, nil, timer.getTime() + 2) + +--DEBUG FUNCTION +-- for key, value in pairs(getmetatable(_spawnedCrate)) do +-- env.info(tostring(key)) +-- env.info(tostring(value)) +-- end diff --git a/RotorOps.lua b/scripts/RotorOps.lua similarity index 96% rename from RotorOps.lua rename to scripts/RotorOps.lua index 4546b9c..ee44a99 100644 --- a/RotorOps.lua +++ b/scripts/RotorOps.lua @@ -1,5 +1,5 @@ RotorOps = {} -RotorOps.version = "1.2.7" +RotorOps.version = "1.2.8" local debug = true @@ -31,6 +31,7 @@ RotorOps.CTLD_crates = false RotorOps.CTLD_sound_effects = true --sound effects for troop pickup/dropoffs RotorOps.exclude_ai_group_name = "Static" --include this somewhere in a group name to exclude the group from being tasked in the active zone RotorOps.pickup_zone_smoke = "blue" +RotorOps.apc_group = {mg=1,at=0,aa=0,inf=3,mortar=0} --not used yet, but we should define the CTLD groups ---[[END OF OPTIONS]]--- @@ -45,7 +46,7 @@ RotorOps.zones = {} RotorOps.active_zone = "" --name of the active zone RotorOps.active_zone_index = 0 RotorOps.game_state_flag = 1 --user flag to store the game state -RotorOps.staging_zone = "" +RotorOps.staging_zones = {} RotorOps.ctld_pickup_zones = {} --keep track of ctld zones we've added, mainly for map markup RotorOps.ai_defending_infantry_groups = {} RotorOps.ai_attacking_infantry_groups = {} @@ -59,6 +60,7 @@ trigger.action.outText("ROTOR OPS STARTED: "..RotorOps.version, 5) env.info("ROTOR OPS STARTED: "..RotorOps.version) RotorOps.staged_units = {} --table of ground units that started in the staging zone +RotorOps.staged_units_by_zone = {} RotorOps.eventHandler = {} local commandDB = {} local game_message_buffer = {} @@ -200,7 +202,7 @@ function RotorOps.eventHandler:onEvent(event) if (world.event.S_EVENT_ENGINE_STARTUP == event.id) then --play some sound files when a player starts engines local initiator = event.initiator:getGroup():getID() - if #event.initiator:getGroup():getUnits() == 1 then --if there are no other units in the player flight group (preventing duplicated messages for ai wingman flights) + if #event.initiator:getGroup():getUnits() == 1 and RotorOps.voice_overs then --if there are no other units in the player flight group (preventing duplicated messages for ai wingman flights) if RotorOps.defending then trigger.action.outSoundForGroup(initiator , RotorOps.gameMsgs.enemy_pushing[RotorOps.active_zone_index + 1][2]) else @@ -423,7 +425,12 @@ function RotorOps.getValidUnitFromGroup(grp) else group_obj = grp end - if grp:isExist() ~= true then return nil end + if not grp then + return nil + end + if grp:isExist() ~= true then + return nil + end local first_valid_unit for index, unit in pairs(grp:getUnits()) do @@ -521,6 +528,36 @@ function RotorOps.aiTask(grp, task, zone) end +--add units to the staged_units table for ai tasking as attackers +function RotorOps.tallyZone(zone_name) + local new_units + if RotorOps.defending then + new_units = mist.getUnitsInZones(mist.makeUnitTable({'[red][vehicle]'}), {zone_name}) + else + new_units = mist.getUnitsInZones(mist.makeUnitTable({'[blue][vehicle]'}), {zone_name}) + end + + if new_units and #new_units > 0 then + + for index, unit in pairs(new_units) do + if not hasValue(RotorOps.staged_units, unit) then + env.info("RotorOps adding new units to staged_units: "..#new_units) + table.insert(RotorOps.staged_units, unit) + RotorOps.aiTask(unit:getGroup(),"move_to_active_zone", RotorOps.zones[RotorOps.active_zone_index].name) + else + --env.info("unit already in table") + end + end + + end +-- +-- for index, unit in pairs(RotorOps.staged_units) do +-- if string.find(Unit.getGroup(unit):getName():lower(), RotorOps.exclude_ai_group_name:lower()) then +-- RotorOps.staged_units[index] = nil --remove 'static' units +-- end +-- end +end + ---AI CORE BEHAVIOR-- @@ -976,11 +1013,9 @@ function RotorOps.assessUnitsInZone(var) if RotorOps.defending == true then RotorOps.game_state = RotorOps.game_states.lost trigger.action.setUserFlag(RotorOps.game_state_flag, RotorOps.game_states.lost) - RotorOps.gameMsg(RotorOps.gameMsgs.failure) else RotorOps.game_state = RotorOps.game_states.won trigger.action.setUserFlag(RotorOps.game_state_flag, RotorOps.game_states.won) - RotorOps.gameMsg(RotorOps.gameMsgs.success) end return --we won't reset our timer to fire this function again end @@ -995,7 +1030,6 @@ function RotorOps.assessUnitsInZone(var) if RotorOps.defending and defending_game_won then RotorOps.game_state = RotorOps.game_states.won trigger.action.setUserFlag(RotorOps.game_state_flag, RotorOps.game_states.won) - RotorOps.gameMsg(RotorOps.gameMsgs.success) return --we won't reset our timer to fire this function again end @@ -1193,7 +1227,6 @@ function RotorOps.setActiveZone(new_index) RotorOps.gameMsg(RotorOps.gameMsgs.enemy_pushing, new_index) else RotorOps.gameMsg(RotorOps.gameMsgs.push, new_index) - RotorOps.gameMsg(RotorOps.gameMsgs.get_troops_to_zone, new_index) end end @@ -1281,13 +1314,14 @@ function RotorOps.addZone(_name, _zone_defenders_flag) RotorOps.addPickupZone(_name, RotorOps.pickup_zone_smoke, -1, "no", 2) end -function RotorOps.stagingZone(_name) + +function RotorOps.addStagingZone(_name) if trigger.misc.getZone(_name) == nil then trigger.action.outText(_name.." trigger zone missing! Check RotorOps setup!", 60) env.warning(_name.." trigger zone missing! Check RotorOps setup!") end RotorOps.addPickupZone(_name, RotorOps.pickup_zone_smoke, -1, "no", 0) - RotorOps.staging_zone = _name + RotorOps.staging_zones[#RotorOps.staging_zones + 1] = _name end @@ -1325,17 +1359,26 @@ function RotorOps.addPickupZone(zone_name, smoke, limit, active, side) RotorOps.ctld_pickup_zones[#RotorOps.ctld_pickup_zones + 1] = zone_name ctld.pickupZones[#ctld.pickupZones + 1] = {zone_name, smoke, limit, active, side} end + function RotorOps.startConflict() - if RotorOps.game_state ~= RotorOps.game_states.not_started then return end + --if RotorOps.game_state ~= RotorOps.game_states.not_started then return end --make some changes to the radio menu --local conflict_zones_menu = commandDB['conflict_zones_menu'] --missionCommands.removeItem(commandDB['start_conflict']) --commandDB['clear_zone'] = missionCommands.addCommand( "[CHEAT] Force Clear Zone" , conflict_zones_menu , RotorOps.clearActiveZone) - - RotorOps.staged_units = mist.getUnitsInZones(mist.makeUnitTable({'[all][vehicle]'}), {RotorOps.staging_zone}) + + RotorOps.staged_units = mist.getUnitsInZones(mist.makeUnitTable({'[all][vehicle]'}), RotorOps.staging_zones) + + --filter out 'static' units +-- for index, unit in pairs(RotorOps.staged_units) do +-- if string.find(Unit.getGroup(unit):getName():lower(), RotorOps.exclude_ai_group_name:lower()) then +-- RotorOps.staged_units[index] = nil --remove 'static' units +-- end +-- end + if RotorOps.staged_units[1] == nil then trigger.action.outText("RotorOps failed: You must place ground units in the staging and conflict zones!" , 60, false) @@ -1347,10 +1390,16 @@ function RotorOps.startConflict() RotorOps.defending = true RotorOps.gameMsg(RotorOps.gameMsgs.start_defense) ctld.activatePickupZone(RotorOps.zones[#RotorOps.zones].name) --make the last zone a pickup zone for defenders - ctld.deactivatePickupZone(RotorOps.staging_zone) + for index, zone in pairs(RotorOps.staging_zones) do + ctld.deactivatePickupZone(zone) + end + else RotorOps.gameMsg(RotorOps.gameMsgs.start) - ctld.activatePickupZone(RotorOps.staging_zone) + for index, zone in pairs(RotorOps.staging_zones) do + ctld.activatePickupZone(zone) + end + end RotorOps.setActiveZone(1) diff --git a/ScriptLoader.lua b/scripts/ScriptLoader.lua similarity index 100% rename from ScriptLoader.lua rename to scripts/ScriptLoader.lua diff --git a/Splash_Damage_2_0.lua b/scripts/Splash_Damage_2_0.lua similarity index 100% rename from Splash_Damage_2_0.lua rename to scripts/Splash_Damage_2_0.lua diff --git a/mist_4_4_90.lua b/scripts/mist_4_4_90.lua similarity index 100% rename from mist_4_4_90.lua rename to scripts/mist_4_4_90.lua diff --git a/mist_4_5_107_grimm.lua b/scripts/mist_4_5_107_grimm.lua similarity index 100% rename from mist_4_5_107_grimm.lua rename to scripts/mist_4_5_107_grimm.lua diff --git a/server/user-files/modules/mapscript.py b/server/user-files/modules/mapscript.py new file mode 100644 index 0000000..fe174d0 --- /dev/null +++ b/server/user-files/modules/mapscript.py @@ -0,0 +1,66 @@ +# 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/Generator/Forces/blue/BLUE Default US Armor.miz b/templates/Forces/BLUE Default US Armor.miz similarity index 100% rename from Generator/Forces/blue/BLUE Default US Armor.miz rename to templates/Forces/BLUE Default US Armor.miz diff --git a/Generator/Forces/blue/BLUE Greece Armor (Mr Nobody).miz b/templates/Forces/BLUE Greece Armor (Mr Nobody).miz similarity index 100% rename from Generator/Forces/blue/BLUE Greece Armor (Mr Nobody).miz rename to templates/Forces/BLUE Greece Armor (Mr Nobody).miz diff --git a/Generator/Forces/blue/BLUE Iran Armor (Mr Nobody).miz b/templates/Forces/BLUE Iran Armor (Mr Nobody).miz similarity index 100% rename from Generator/Forces/blue/BLUE Iran Armor (Mr Nobody).miz rename to templates/Forces/BLUE Iran Armor (Mr Nobody).miz diff --git a/Generator/Forces/blue/BLUE Turkey Armor (Mr Nobody).miz b/templates/Forces/BLUE Turkey Armor (Mr Nobody).miz similarity index 100% rename from Generator/Forces/blue/BLUE Turkey Armor (Mr Nobody).miz rename to templates/Forces/BLUE Turkey Armor (Mr Nobody).miz diff --git a/Generator/Forces/blue/BLUE UK Armor (Mr Nobody).miz b/templates/Forces/BLUE UK Armor (Mr Nobody).miz similarity index 100% rename from Generator/Forces/blue/BLUE UK Armor (Mr Nobody).miz rename to templates/Forces/BLUE UK Armor (Mr Nobody).miz diff --git a/Generator/Forces/blue/BLUE US 1970s Armor & Infantry (Mr Nobody).miz b/templates/Forces/BLUE US 1970s Armor & Infantry (Mr Nobody).miz similarity index 100% rename from Generator/Forces/blue/BLUE US 1970s Armor & Infantry (Mr Nobody).miz rename to templates/Forces/BLUE US 1970s Armor & Infantry (Mr Nobody).miz diff --git a/Generator/Forces/red/RED Default Armor (HARD).miz b/templates/Forces/RED Default Armor (HARD).miz similarity index 100% rename from Generator/Forces/red/RED Default Armor (HARD).miz rename to templates/Forces/RED Default Armor (HARD).miz diff --git a/Generator/Forces/red/RED Default Armor, Infantry & Artillery (MED).miz b/templates/Forces/RED Default Armor, Infantry & Artillery (MED).miz similarity index 100% rename from Generator/Forces/red/RED Default Armor, Infantry & Artillery (MED).miz rename to templates/Forces/RED Default Armor, Infantry & Artillery (MED).miz diff --git a/Generator/Forces/red/RED Default Trucks & Infantry (EASY).miz b/templates/Forces/RED Default Trucks & Infantry (EASY).miz similarity index 100% rename from Generator/Forces/red/RED Default Trucks & Infantry (EASY).miz rename to templates/Forces/RED Default Trucks & Infantry (EASY).miz diff --git a/Generator/Forces/red/RED Greece Armor (Mr Nobody).miz b/templates/Forces/RED Greece Armor (Mr Nobody).miz similarity index 100% rename from Generator/Forces/red/RED Greece Armor (Mr Nobody).miz rename to templates/Forces/RED Greece Armor (Mr Nobody).miz diff --git a/Generator/Forces/red/RED Iran Armor & Infantry (Mr Nobody).miz b/templates/Forces/RED Iran Armor & Infantry (Mr Nobody).miz similarity index 100% rename from Generator/Forces/red/RED Iran Armor & Infantry (Mr Nobody).miz rename to templates/Forces/RED Iran Armor & Infantry (Mr Nobody).miz diff --git a/templates/Forces/RED North Vietnam Armor & Infantry (Mr Nobody).miz b/templates/Forces/RED North Vietnam Armor & Infantry (Mr Nobody).miz new file mode 100644 index 0000000..5da964e Binary files /dev/null and b/templates/Forces/RED North Vietnam Armor & Infantry (Mr Nobody).miz differ diff --git a/templates/Forces/_How to add your own templates.txt b/templates/Forces/_How to add your own templates.txt new file mode 100644 index 0000000..c83d46e --- /dev/null +++ b/templates/Forces/_How to add your own templates.txt @@ -0,0 +1,24 @@ +## Forces Templates + +The friendly/enemy forces templates available in the generator are simply .miz files in the Generator/Forces folder. + +A Forces template defines the groups of ground units available, AI aircraft, liveries, and loadouts. + +To create your own Forces template: + +1) Create an empty mission on Caucasus +2) Add ground unit groups. +3) Save the mission in this directory. + +Optional: + +4) Add helicopters with "CAS" main task for attack helicopters. +5) Add helicopters with "Transport" main task for transport helicopters. +6) Add planes with "CAS" main task for attack planes. +7) Add planes with "CAP" main task for fighters. +8) Configure loadouts, liveries, and skill for aircraft. + +Tips: +- The mission generator will only extract blue ground units from the template when selected from the "Blue Forces" menu, and vice versa. +- Only unit types are used from ground units. Liveries or other attributes are able to be copied. +- For aircraft, group size is currently capped at 2 units per group to help prevent issues with parking. Only the first unit in the group is used as a source. diff --git a/Generator/Imports/FARP_DEFAULT_ZONE.miz b/templates/Imports/FARP_DEFAULT_ZONE.miz similarity index 100% rename from Generator/Imports/FARP_DEFAULT_ZONE.miz rename to templates/Imports/FARP_DEFAULT_ZONE.miz diff --git a/Generator/Imports/FARP_MINIMUM_ROADSIDE_INVULNERABLE.miz b/templates/Imports/FARP_MINIMUM_ROADSIDE_INVULNERABLE.miz similarity index 100% rename from Generator/Imports/FARP_MINIMUM_ROADSIDE_INVULNERABLE.miz rename to templates/Imports/FARP_MINIMUM_ROADSIDE_INVULNERABLE.miz diff --git a/Generator/Imports/FARP_MINIMUM_ROADSIDE_STATICS.miz b/templates/Imports/FARP_MINIMUM_ROADSIDE_STATICS.miz similarity index 100% rename from Generator/Imports/FARP_MINIMUM_ROADSIDE_STATICS.miz rename to templates/Imports/FARP_MINIMUM_ROADSIDE_STATICS.miz diff --git a/Generator/Imports/FARP_MOBILE_ROADSIDE_INVULNERABLE.miz b/templates/Imports/FARP_MOBILE_ROADSIDE_INVULNERABLE.miz similarity index 100% rename from Generator/Imports/FARP_MOBILE_ROADSIDE_INVULNERABLE.miz rename to templates/Imports/FARP_MOBILE_ROADSIDE_INVULNERABLE.miz diff --git a/Generator/Imports/FARP_MOBILE_ROADSIDE_STATICS.miz b/templates/Imports/FARP_MOBILE_ROADSIDE_STATICS.miz similarity index 100% rename from Generator/Imports/FARP_MOBILE_ROADSIDE_STATICS.miz rename to templates/Imports/FARP_MOBILE_ROADSIDE_STATICS.miz diff --git a/templates/Imports/FOB_16_SPWN_WIDE.miz b/templates/Imports/FOB_16_SPWN_WIDE.miz new file mode 100644 index 0000000..65859d8 Binary files /dev/null and b/templates/Imports/FOB_16_SPWN_WIDE.miz differ diff --git a/Generator/Imports/FOB_8_SPWN.miz b/templates/Imports/FOB_8_SPWN.miz similarity index 100% rename from Generator/Imports/FOB_8_SPWN.miz rename to templates/Imports/FOB_8_SPWN.miz diff --git a/templates/Imports/How to use imports.txt b/templates/Imports/How to use imports.txt new file mode 100644 index 0000000..556d285 --- /dev/null +++ b/templates/Imports/How to use imports.txt @@ -0,0 +1,20 @@ +## Imports + +A breakthrough feature of mission design with RotorOps is the ability to import complex arrangements of statics and vehicles. This allows you to create reusable mission assets like bases, FARPs, objective sites, or just about anything you can imagine building in the mission editor. You can place these on any map, at your desired point, rotation and coalition! + +A selection of FOBs, FARPs, and other objects are available in the Imports folder. Included are multiplayer FOBs with up to 16 helicopter spawns! A guide to the included templates is available here: [RotorOps IMPORT Assets](http://dcs-helicopters.com/wp-content/uploads/2022/03/RotorOps_IMPORT_TEMPLATES-1.pdf) + + +To use an import template: +1) Place a static mark flag in the scenario template mission. +2) Change the group name to 'IMPORT-filename', where the filename is a .miz file in the Generator/Imports folder. +3) Change the flag coalition to CJTF Blue/Red or UN Peacekeepers. +4) Change the flag heading and position as desired. +5) Change the flag UNIT NAME to something relevant (ie. 'North Base') +6) For multiple imports of the same template, the import object GROUP NAME should end with '-01' etc + +To create a new import template: +1) Make an empty mission on Caucasus. +2) Place units/objects on the map. +3) Make one unit group name: 'ANCHOR' This will represent the point of insertion/rotation in the target mission. +4) Save the template .miz file in Generator/Imports \ No newline at end of file diff --git a/templates/Imports/INSURGENT_COMPOUND.miz b/templates/Imports/INSURGENT_COMPOUND.miz new file mode 100644 index 0000000..b27911e Binary files /dev/null and b/templates/Imports/INSURGENT_COMPOUND.miz differ diff --git a/Generator/Imports/MARKET_PLACE.miz b/templates/Imports/MARKET_PLACE.miz similarity index 100% rename from Generator/Imports/MARKET_PLACE.miz rename to templates/Imports/MARKET_PLACE.miz diff --git a/Generator/Imports/STAGING_LOGISTIC_HUB.miz b/templates/Imports/STAGING_LOGISTIC_HUB.miz similarity index 100% rename from Generator/Imports/STAGING_LOGISTIC_HUB.miz rename to templates/Imports/STAGING_LOGISTIC_HUB.miz diff --git a/Generator/Imports/VILLA_GRIMM.miz b/templates/Imports/VILLA_GRIMM.miz similarity index 100% rename from Generator/Imports/VILLA_GRIMM.miz rename to templates/Imports/VILLA_GRIMM.miz