From ef23ce58d1138e78f238ac61f8b552ea182c56f0 Mon Sep 17 00:00:00 2001 From: Anthony Conrad Date: Sun, 23 Aug 2020 21:54:12 -0700 Subject: [PATCH 01/61] Added keyboard support for the menu system --- qt_ui/windows/QLiberationWindow.py | 41 +++++++++++++++++------------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/qt_ui/windows/QLiberationWindow.py b/qt_ui/windows/QLiberationWindow.py index 382b3289..4641be37 100644 --- a/qt_ui/windows/QLiberationWindow.py +++ b/qt_ui/windows/QLiberationWindow.py @@ -69,27 +69,31 @@ class QLiberationWindow(QMainWindow): GameUpdateSignal.get_instance().debriefingReceived.connect(self.onDebriefing) def initActions(self): - self.newGameAction = QAction("New Game", self) + self.newGameAction = QAction("&New Game", self) self.newGameAction.setIcon(QIcon(CONST.ICONS["New"])) self.newGameAction.triggered.connect(self.newGame) + self.newGameAction.setShortcut('CTRL+N') - self.openAction = QAction("Open", self) + self.openAction = QAction("&Open", self) self.openAction.setIcon(QIcon(CONST.ICONS["Open"])) self.openAction.triggered.connect(self.openFile) + self.openAction.setShortcut('CTRL+O') - self.saveGameAction = QAction("Save", self) + self.saveGameAction = QAction("&Save", self) self.saveGameAction.setIcon(QIcon(CONST.ICONS["Save"])) self.saveGameAction.triggered.connect(self.saveGame) + self.saveGameAction.setShortcut('CTRL+S') - self.saveAsAction = QAction("Save As", self) + self.saveAsAction = QAction("Save &As", self) self.saveAsAction.setIcon(QIcon(CONST.ICONS["Save"])) self.saveAsAction.triggered.connect(self.saveGameAs) + self.saveAsAction.setShortcut('CTRL+A') self.showAboutDialogAction = QAction("About DCS Liberation", self) self.showAboutDialogAction.setIcon(QIcon.fromTheme("help-about")) self.showAboutDialogAction.triggered.connect(self.showAboutDialog) - self.showLiberationPrefDialogAction = QAction("Preferences", self) + self.showLiberationPrefDialogAction = QAction("&Preferences", self) self.showLiberationPrefDialogAction.setIcon(QIcon.fromTheme("help-about")) self.showLiberationPrefDialogAction.triggered.connect(self.showLiberationDialog) @@ -102,27 +106,17 @@ class QLiberationWindow(QMainWindow): def initMenuBar(self): self.menu = self.menuBar() - file_menu = self.menu.addMenu("File") + file_menu = self.menu.addMenu("&File") file_menu.addAction(self.newGameAction) file_menu.addAction(self.openAction) + file_menu.addSeparator() file_menu.addAction(self.saveGameAction) file_menu.addAction(self.saveAsAction) file_menu.addSeparator() file_menu.addAction(self.showLiberationPrefDialogAction) file_menu.addSeparator() #file_menu.addAction("Close Current Game", lambda: self.closeGame()) # Not working - file_menu.addAction("Exit" , lambda: self.exit()) - - help_menu = self.menu.addMenu("Help") - help_menu.addAction("Discord Server", lambda: webbrowser.open_new_tab("https://" + "discord.gg" + "/" + "bKrt" + "rkJ")) - help_menu.addAction("Github Repository", lambda: webbrowser.open_new_tab("https://github.com/khopa/dcs_liberation")) - help_menu.addAction("Releases", lambda: webbrowser.open_new_tab("https://github.com/Khopa/dcs_liberation/releases")) - help_menu.addAction("Online Manual", lambda: webbrowser.open_new_tab(URLS["Manual"])) - help_menu.addAction("ED Forum Thread", lambda: webbrowser.open_new_tab(URLS["ForumThread"])) - help_menu.addAction("Report an issue", lambda: webbrowser.open_new_tab(URLS["Issues"])) - - help_menu.addSeparator() - help_menu.addAction(self.showAboutDialogAction) + file_menu.addAction("E&xit" , lambda: self.exit()) displayMenu = self.menu.addMenu("Display") @@ -164,6 +158,17 @@ class QLiberationWindow(QMainWindow): displayMenu.addAction(tg_sam_visibility) displayMenu.addAction(tg_flight_path_visibility) + help_menu = self.menu.addMenu("&Help") + help_menu.addAction("Discord Server", lambda: webbrowser.open_new_tab("https://" + "discord.gg" + "/" + "bKrt" + "rkJ")) + help_menu.addAction("Github Repository", lambda: webbrowser.open_new_tab("https://github.com/khopa/dcs_liberation")) + help_menu.addAction("Releases", lambda: webbrowser.open_new_tab("https://github.com/Khopa/dcs_liberation/releases")) + help_menu.addAction("Online Manual", lambda: webbrowser.open_new_tab(URLS["Manual"])) + help_menu.addAction("ED Forum Thread", lambda: webbrowser.open_new_tab(URLS["ForumThread"])) + help_menu.addAction("Report an issue", lambda: webbrowser.open_new_tab(URLS["Issues"])) + + help_menu.addSeparator() + help_menu.addAction(self.showAboutDialogAction) + def newGame(self): wizard = NewGameWizard(self) wizard.show() From 04c878f57c915dc8baf12144f878188c8472d418 Mon Sep 17 00:00:00 2001 From: Anthony Conrad Date: Sun, 23 Aug 2020 22:25:16 -0700 Subject: [PATCH 02/61] Added keyboard support to the menu system --- .vscode/launch.json | 19 ++++++++++++++++ .vscode/settings.json | 4 ++++ .vscode/tasks.json | 35 ++++++++++++++++++++++++++++++ qt_ui/windows/QLiberationWindow.py | 28 ++++++++++++------------ 4 files changed, 72 insertions(+), 14 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..646c8768 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,19 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Main", + "type": "python", + "request": "launch", + "program": "qt_ui\\main.py", + "console": "integratedTerminal", + "env": { + "PYTHONPATH": ".;./pydcs" + }, + "preLaunchTask": "Prepare Environment" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..d8c82a7e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python.pythonPath": "g:\\python\\dcs_liberation\\venv\\Scripts\\python.exe", + "vsintellicode.python.completionsEnabled": true +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..84bd2413 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,35 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Prepare Environment", + "type": "shell", + "isBackground": false, + "problemMatcher": [], + "command": "powershell", + "args": [ + "-Command", + "& {if (-not (Test-Path ${workspaceFolder}\\venv)) { python -m venv ${workspaceFolder}\\venv; . ${workspaceFolder}\\venv\\scripts\\activate.ps1; pip install -r requirements.txt; Copy-Item ${workspaceFolder}\\venv\\Lib\\site-packages\\shiboken2\\shiboken2.abi3.dll ${workspaceFolder}\\venv\\Lib\\site-packages\\PySide2 } }", + ], + "group": "build", + "options": { + "env": { + "PYTHONPATH": ".;./pydcs" + } + }, + "presentation": { + "echo": true, + "reveal": "never", + "focus": false, + "panel": "shared", + "showReuseMessage": true, + "clear": false + }, + "runOptions": { + "runOn": "folderOpen" + } + } + ] +} \ No newline at end of file diff --git a/qt_ui/windows/QLiberationWindow.py b/qt_ui/windows/QLiberationWindow.py index 4641be37..3d19cdf5 100644 --- a/qt_ui/windows/QLiberationWindow.py +++ b/qt_ui/windows/QLiberationWindow.py @@ -89,7 +89,7 @@ class QLiberationWindow(QMainWindow): self.saveAsAction.triggered.connect(self.saveGameAs) self.saveAsAction.setShortcut('CTRL+A') - self.showAboutDialogAction = QAction("About DCS Liberation", self) + self.showAboutDialogAction = QAction("&About DCS Liberation", self) self.showAboutDialogAction.setIcon(QIcon.fromTheme("help-about")) self.showAboutDialogAction.triggered.connect(self.showAboutDialog) @@ -118,35 +118,35 @@ class QLiberationWindow(QMainWindow): #file_menu.addAction("Close Current Game", lambda: self.closeGame()) # Not working file_menu.addAction("E&xit" , lambda: self.exit()) - displayMenu = self.menu.addMenu("Display") + displayMenu = self.menu.addMenu("&Display") - tg_cp_visibility = QAction('Control Point', displayMenu) + tg_cp_visibility = QAction('&Control Point', displayMenu) tg_cp_visibility.setCheckable(True) tg_cp_visibility.setChecked(True) tg_cp_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("cp", tg_cp_visibility.isChecked())) - tg_go_visibility = QAction('Ground Objects', displayMenu) + tg_go_visibility = QAction('&Ground Objects', displayMenu) tg_go_visibility.setCheckable(True) tg_go_visibility.setChecked(True) tg_go_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("go", tg_go_visibility.isChecked())) - tg_line_visibility = QAction('Lines', displayMenu) + tg_line_visibility = QAction('&Lines', displayMenu) tg_line_visibility.setCheckable(True) tg_line_visibility.setChecked(True) tg_line_visibility.toggled.connect( lambda: QLiberationMap.set_display_rule("lines", tg_line_visibility.isChecked())) - tg_event_visibility = QAction('Events', displayMenu) + tg_event_visibility = QAction('&Events', displayMenu) tg_event_visibility.setCheckable(True) tg_event_visibility.setChecked(True) tg_event_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("events", tg_event_visibility.isChecked())) - tg_sam_visibility = QAction('SAM Range', displayMenu) + tg_sam_visibility = QAction('&SAM Range', displayMenu) tg_sam_visibility.setCheckable(True) tg_sam_visibility.setChecked(True) tg_sam_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("sam", tg_sam_visibility.isChecked())) - tg_flight_path_visibility = QAction('Flight Paths', displayMenu) + tg_flight_path_visibility = QAction('&Flight Paths', displayMenu) tg_flight_path_visibility.setCheckable(True) tg_flight_path_visibility.setChecked(False) tg_flight_path_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("flight_paths", tg_flight_path_visibility.isChecked())) @@ -159,12 +159,12 @@ class QLiberationWindow(QMainWindow): displayMenu.addAction(tg_flight_path_visibility) help_menu = self.menu.addMenu("&Help") - help_menu.addAction("Discord Server", lambda: webbrowser.open_new_tab("https://" + "discord.gg" + "/" + "bKrt" + "rkJ")) - help_menu.addAction("Github Repository", lambda: webbrowser.open_new_tab("https://github.com/khopa/dcs_liberation")) - help_menu.addAction("Releases", lambda: webbrowser.open_new_tab("https://github.com/Khopa/dcs_liberation/releases")) - help_menu.addAction("Online Manual", lambda: webbrowser.open_new_tab(URLS["Manual"])) - help_menu.addAction("ED Forum Thread", lambda: webbrowser.open_new_tab(URLS["ForumThread"])) - help_menu.addAction("Report an issue", lambda: webbrowser.open_new_tab(URLS["Issues"])) + help_menu.addAction("&Discord Server", lambda: webbrowser.open_new_tab("https://" + "discord.gg" + "/" + "bKrt" + "rkJ")) + help_menu.addAction("&Github Repository", lambda: webbrowser.open_new_tab("https://github.com/khopa/dcs_liberation")) + help_menu.addAction("&Releases", lambda: webbrowser.open_new_tab("https://github.com/Khopa/dcs_liberation/releases")) + help_menu.addAction("&Online Manual", lambda: webbrowser.open_new_tab(URLS["Manual"])) + help_menu.addAction("&ED Forum Thread", lambda: webbrowser.open_new_tab(URLS["ForumThread"])) + help_menu.addAction("Report an &issue", lambda: webbrowser.open_new_tab(URLS["Issues"])) help_menu.addSeparator() help_menu.addAction(self.showAboutDialogAction) From e92fb38271df0a9af879ab0e86f378d316e72628 Mon Sep 17 00:00:00 2001 From: Khopa Date: Mon, 24 Aug 2020 22:32:37 +0200 Subject: [PATCH 03/61] Fixed Sweden 1990 faction not working --- game/factions/sweden_1990.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game/factions/sweden_1990.py b/game/factions/sweden_1990.py index 5bafb20b..ebc754f9 100644 --- a/game/factions/sweden_1990.py +++ b/game/factions/sweden_1990.py @@ -11,7 +11,7 @@ Sweden_1990 = { UH_1H, - AirDefence.SAM_Hawk_LN_M192, + AirDefence.SAM_Hawk_PCP, Armor.IFV_MCV_80, # Standing as Strf 90 Armor.MBT_Leopard_2, From 75bb6941d36eb1d0cbc7ae3e652e0391d2f69384 Mon Sep 17 00:00:00 2001 From: Khopa Date: Mon, 24 Aug 2020 22:49:55 +0200 Subject: [PATCH 04/61] Added version string in the window title --- qt_ui/windows/QLiberationWindow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qt_ui/windows/QLiberationWindow.py b/qt_ui/windows/QLiberationWindow.py index 3d19cdf5..da50e03f 100644 --- a/qt_ui/windows/QLiberationWindow.py +++ b/qt_ui/windows/QLiberationWindow.py @@ -29,7 +29,7 @@ class QLiberationWindow(QMainWindow): self.setGame(persistency.restore_game()) self.setGeometry(300, 100, 270, 100) - self.setWindowTitle("DCS Liberation") + self.setWindowTitle("DCS Liberation - v" + CONST.VERSION_STRING) self.setWindowIcon(QIcon("./resources/icon.png")) self.statusBar().showMessage('Ready') From 139c4c1dd8bca81bdbc4f1ee766c4229ad5a4d3f Mon Sep 17 00:00:00 2001 From: Khopa Date: Thu, 27 Aug 2020 23:46:10 +0200 Subject: [PATCH 05/61] Fixed crash on mission generation when clearing slots. --- gen/aircraft.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/gen/aircraft.py b/gen/aircraft.py index cdcf77c9..c5eb8ae8 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -256,14 +256,15 @@ class AircraftConflictGenerator: def generate_flights(self, cp, country, flight_planner:FlightPlanner): # Clear pydcs parking slots - logging.info("CLEARING SLOTS @ " + cp.airport.name) - logging.info("===============") if cp.airport is not None: - for ps in cp.airport.parking_slots: - logging.info("SLOT : " + str(ps.unit_id)) - ps.unit_id = None - logging.info("----------------") - logging.info("===============") + logging.info("CLEARING SLOTS @ " + cp.airport.name) + logging.info("===============") + if cp.airport is not None: + for ps in cp.airport.parking_slots: + logging.info("SLOT : " + str(ps.unit_id)) + ps.unit_id = None + logging.info("----------------") + logging.info("===============") for flight in flight_planner.flights: From 7817d59989e1ccb6774fb238eee7537a4866d10d Mon Sep 17 00:00:00 2001 From: Khopa Date: Thu, 27 Aug 2020 23:47:00 +0200 Subject: [PATCH 06/61] Fixed campaign sometimes not starting when the user does not explicitly re-select a campaign and just kee p the default one.. --- qt_ui/windows/newgame/QNewGameWizard.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qt_ui/windows/newgame/QNewGameWizard.py b/qt_ui/windows/newgame/QNewGameWizard.py index cb88fc21..3daace57 100644 --- a/qt_ui/windows/newgame/QNewGameWizard.py +++ b/qt_ui/windows/newgame/QNewGameWizard.py @@ -12,7 +12,7 @@ import qt_ui.uiconstants as CONST from game import db, Game from game.settings import Settings from gen import namegen -from qt_ui.windows.newgame.QCampaignList import QCampaignList +from qt_ui.windows.newgame.QCampaignList import QCampaignList, CAMPAIGNS from theater import start_generator, persiangulf, nevada, caucasus, ConflictTheater, normandy, thechannel @@ -42,6 +42,8 @@ class NewGameWizard(QtWidgets.QWizard): redFaction = [c for c in db.FACTIONS][self.field("redFaction")] selectedCampaign = self.field("selectedCampaign") + if selectedCampaign is None: + selectedCampaign = CAMPAIGNS[0] conflictTheater = selectedCampaign[1]() timePeriod = db.TIME_PERIODS[list(db.TIME_PERIODS.keys())[self.field("timePeriod")]] From d50e791c30350b19d710cf43e16ecd56a907d9a4 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Fri, 28 Aug 2020 12:53:31 -0700 Subject: [PATCH 07/61] Make the mission planning window non-modal. Doesn't appear to be any need for this to be modal. Making it non-modal allows interacting with the map during planning. --- qt_ui/windows/mission/QMissionPlanning.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qt_ui/windows/mission/QMissionPlanning.py b/qt_ui/windows/mission/QMissionPlanning.py index 7dcc2231..04ffdeb3 100644 --- a/qt_ui/windows/mission/QMissionPlanning.py +++ b/qt_ui/windows/mission/QMissionPlanning.py @@ -17,7 +17,6 @@ class QMissionPlanning(QDialog): self.game = game self.setWindowFlags(Qt.WindowStaysOnTopHint) self.setMinimumSize(1000, 440) - self.setModal(True) self.setWindowTitle("Mission Preparation") self.setWindowIcon(EVENT_ICONS["strike"]) self.init_ui() From 001752a81e56d4f4514c87546789257c8231a762 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sat, 29 Aug 2020 19:56:44 -0700 Subject: [PATCH 08/61] Update gitignore. --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index bdd27ca3..f96c1292 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,8 @@ tests/** # User-specific stuff .idea/ -liberation_preferences.json +/liberation_preferences.json +/state.json logs/liberation.log From 66af6be0634a4381ee94093f36f2aa7c37323e36 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sun, 30 Aug 2020 18:33:21 -0700 Subject: [PATCH 09/61] Update pydcs. --- pydcs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydcs b/pydcs index dcc3d846..d278df68 160000 --- a/pydcs +++ b/pydcs @@ -1 +1 @@ -Subproject commit dcc3d846316af2925c93ae09840c3ab4a1150e59 +Subproject commit d278df68eee2f486f840c178e17893f58313efb8 From e7e82dcd0b9fbb819bcac9f69b4ea2adda7c1635 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Thu, 27 Aug 2020 01:52:21 -0700 Subject: [PATCH 10/61] Build mission kneeboards. This includes most of the briefing information in the kneeboard: * Airfield info * Waypoint info * Comm info * AWACS * Tankers * JTAC There's more that could be done: * Restrict tankers to the type compatible with the current aircraft * Support for carriers * Merge all relevant comm info (tankers, AWACS, JTAC, other flights) into the comm ladder This gives us a good start and a framework to build on. Very likely that we'll want to split part of this (probably the comm ladder) off onto a separate page once we start adding more to this, since it's a pretty full page currently. Also missing is any checking that the contents do not go beyond the bounds of the page. We could add this if needed. For now the page has enough room for about a dozen waypoints, which is quite a bit more than most missions need. --- .gitignore | 1 + game/operation/operation.py | 19 +- gen/__init__.py | 1 + gen/airfields.py | 45 +++++ gen/kneeboard.py | 327 ++++++++++++++++++++++++++++++++ gen/units.py | 6 + requirements.txt | 3 + resources/fonts/Inconsolata.otf | Bin 0 -> 58560 bytes resources/fonts/OFL.txt | 38 ++++ 9 files changed, 436 insertions(+), 4 deletions(-) create mode 100644 gen/airfields.py create mode 100644 gen/kneeboard.py create mode 100644 gen/units.py create mode 100644 resources/fonts/Inconsolata.otf create mode 100644 resources/fonts/OFL.txt diff --git a/.gitignore b/.gitignore index f96c1292..68c604e4 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ tests/** # User-specific stuff .idea/ +/kneeboards /liberation_preferences.json /state.json diff --git a/game/operation/operation.py b/game/operation/operation.py index 033d3c34..5f7a9503 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -221,17 +221,28 @@ class Operation: load_dcs_libe.add_action(DoScript(String(script))) self.current_mission.triggerrules.triggers.append(load_dcs_libe) + kneeboard_generator = KneeboardGenerator(self.current_mission, self.game) + # Briefing Generation for i, tanker_type in enumerate(self.airsupportgen.generated_tankers): - self.briefinggen.append_frequency("Tanker {} ({})".format(TANKER_CALLSIGNS[i], tanker_type), "{}X/{} MHz AM".format(60+i, 130+i)) + callsign = TANKER_CALLSIGNS[i] + tacan = f"{60 + i}X" + freq = f"{130 + i} MHz AM" + self.briefinggen.append_frequency(f"Tanker {callsign} ({tanker_type})", f"{tacan}/{freq}") + kneeboard_generator.add_tanker(callsign, tanker_type, freq, tacan) if self.is_awacs_enabled: - self.briefinggen.append_frequency("AWACS", "233 MHz AM") + callsign = "AWACS" + freq = "233 MHz AM" + self.briefinggen.append_frequency(callsign, freq) + kneeboard_generator.add_awacs(callsign, freq) self.briefinggen.append_frequency("Flight", "251 MHz AM") + kneeboard_generator.add_comm("Flight", "251 MHz AM") # Generate the briefing self.briefinggen.generate() - - + for region, code, name in self.game.jtacs: + kneeboard_generator.add_jtac(name, region, code) + kneeboard_generator.generate() diff --git a/gen/__init__.py b/gen/__init__.py index d910a19d..ad11614f 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -9,6 +9,7 @@ from .environmentgen import * from .groundobjectsgen import * from .briefinggen import * from .forcedoptionsgen import * +from .kneeboard import * from . import naming diff --git a/gen/airfields.py b/gen/airfields.py new file mode 100644 index 00000000..79e540d7 --- /dev/null +++ b/gen/airfields.py @@ -0,0 +1,45 @@ +"""Extra airfield data that is not exposed by pydcs. + +Remove once https://github.com/pydcs/dcs/issues/69 tracks getting the missing +data added to pydcs. Until then, missing data can be manually filled in here. +""" +from dataclasses import dataclass, field +from typing import Dict, List, Optional + + +RadioFrequency = str + + +@dataclass +class AtcData: + hf: RadioFrequency + vhf_fm: RadioFrequency + vhf_am: RadioFrequency + uhf: RadioFrequency + + +@dataclass +class AirfieldData: + """Additional airfield data not included in pydcs.""" + + #: Radio channels used by the airfield's ATC. + atc: AtcData + + #: TACAN channel as a string, i.e. "74X". + tacan: Optional[str] = None + + #: Dict of runway heading -> ILS frequency. + ils: Dict[str, RadioFrequency] = field(default_factory=dict) + + def ils_freq(self, runway: str) -> Optional[RadioFrequency]: + return self.ils.get(runway) + + +# TODO: Add more airfields. +AIRFIELD_DATA = { + "Incirlik": AirfieldData( + AtcData("3.85", "38.6", "129.4", "360.1"), + "21X", + {"050": "109.3", "230": "111.7"} + ), +} diff --git a/gen/kneeboard.py b/gen/kneeboard.py new file mode 100644 index 00000000..1d679e4a --- /dev/null +++ b/gen/kneeboard.py @@ -0,0 +1,327 @@ +"""Generates kneeboard pages relevant to the player's mission. + +The player kneeboard includes the following information: + +* Airfield (departure, arrival, divert) info. +* Flight plan (waypoint numbers, names, altitudes). +* Comm channels. +* AWACS info. +* Tanker info. +* JTAC info. + +Things we should add: + +* Flight plan ToT and fuel ladder (current have neither available). +* Support for planning an arrival/divert airfield separate from departure. +* Mission package infrastructure to include information about the larger + mission, i.e. information about the escort flight for a strike package. +* Target information. Steerpoints, preplanned objectives, ToT, etc. + +For multiplayer missions, a kneeboard will be generated per flight. +https://forums.eagle.ru/showthread.php?t=206360 claims that kneeboard pages can +only be added per airframe, so PvP missions where each side have the same +aircraft will be able to see the enemy's kneeboard for the same airframe. +""" +from collections import defaultdict +from dataclasses import dataclass +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +from PIL import Image, ImageDraw, ImageFont +from tabulate import tabulate + +from pydcs.dcs.mission import Mission +from pydcs.dcs.terrain.terrain import Airport +from pydcs.dcs.unittype import FlyingType +from .airfields import AIRFIELD_DATA +from .flights.flight import Flight +from . import units + + +class KneeboardPageWriter: + """Creates kneeboard images.""" + + def __init__(self, page_margin: int = 24, line_spacing: int = 12) -> None: + self.image = Image.new('RGB', (768, 1024), (0xff, 0xff, 0xff)) + # These font sizes create a relatively full page for current sorties. If + # we start generating more complicated flight plans, or start including + # more information in the comm ladder (the latter of which we should + # probably do), we'll need to split some of this information off into a + # second page. + self.title_font = ImageFont.truetype("arial.ttf", 32) + self.heading_font = ImageFont.truetype("arial.ttf", 24) + self.content_font = ImageFont.truetype("arial.ttf", 20) + self.table_font = ImageFont.truetype( + "resources/fonts/Inconsolata.otf", 20) + self.draw = ImageDraw.Draw(self.image) + self.x = page_margin + self.y = page_margin + self.line_spacing = line_spacing + + @property + def position(self) -> Tuple[int, int]: + return self.x, self.y + + def text(self, text: str, font=None, + fill: Tuple[int, int, int] = (0, 0, 0)) -> None: + if font is None: + font = self.content_font + + self.draw.text(self.position, text, font=font, fill=fill) + width, height = self.draw.textsize(text, font=font) + self.y += height + self.line_spacing + + def title(self, title: str) -> None: + self.text(title, font=self.title_font) + + def heading(self, text: str) -> None: + self.text(text, font=self.heading_font) + + def table(self, cells: List[List[str]], + headers: Optional[List[str]] = None) -> None: + table = tabulate(cells, headers=headers, numalign="right") + self.text(table, font=self.table_font) + + def write(self, path: Path) -> None: + self.image.save(path) + + +class KneeboardPage: + """Base class for all kneeboard pages.""" + + def write(self, path: Path) -> None: + """Writes the kneeboard page to the given path.""" + raise NotImplementedError + + +class AirfieldInfo: + def __init__(self, airfield: Airport) -> None: + self.airport = airfield + # TODO: Implement logic for picking preferred runway. + runway = airfield.runways[0] + runway_side = ["", "L", "R"][runway.leftright] + self.runway = f"{runway.heading}{runway_side}" + try: + extra_data = AIRFIELD_DATA[airfield.name] + self.atc = extra_data.atc.uhf or "" + self.tacan = extra_data.tacan or "" + self.ils = extra_data.ils_freq(self.runway) or "" + except KeyError: + self.atc = "" + self.ils = "" + self.tacan = "" + + +@dataclass +class CommInfo: + """Communications information for the kneeboard.""" + name: str + freq: str + + +@dataclass +class AwacsInfo: + """AWACS information for the kneeboard.""" + callsign: str + freq: str + + +@dataclass +class TankerInfo: + """Tanker information for the kneeboard.""" + callsign: str + variant: str + freq: str + tacan: str + + +@dataclass +class JtacInfo: + """JTAC information for the kneeboard.""" + callsign: str + region: str + code: str + + +class BriefingPage(KneeboardPage): + """A kneeboard page containing briefing information.""" + def __init__(self, flight: Flight, comms: List[CommInfo], + awacs: List[AwacsInfo], tankers: List[TankerInfo], + jtacs: List[JtacInfo]) -> None: + self.flight = flight + self.comms = comms + self.awacs = awacs + self.tankers = tankers + self.jtacs = jtacs + self.departure = flight.from_cp.airport + self.arrival = flight.from_cp.airport + self.divert: Optional[Airport] = None + + def write(self, path: Path) -> None: + writer = KneeboardPageWriter() + # TODO: Assign callsigns to flights and include that info. + # https://github.com/Khopa/dcs_liberation/issues/113 + writer.title(f"Mission Info") + + # TODO: Handle carriers. + writer.heading("Airfield Info") + writer.table([ + self.airfield_info_row("Departure", self.departure), + self.airfield_info_row("Arrival", self.arrival), + self.airfield_info_row("Divert", self.divert), + ], headers=["", "Airbase", "ATC", "TCN", "ILS", "RWY"]) + + writer.heading("Flight Plan") + flight_plan = [] + for num, waypoint in enumerate(self.flight.points): + alt = int(units.meters_to_feet(waypoint.alt)) + flight_plan.append([num, waypoint.pretty_name, str(alt)]) + writer.table(flight_plan, headers=["STPT", "Action", "Alt"]) + + writer.heading("Comm Ladder") + comms = [] + for comm in self.comms: + comms.append([comm.name, comm.freq]) + writer.table(comms, headers=["Name", "UHF"]) + + writer.heading("AWACS") + awacs = [] + for a in self.awacs: + awacs.append([a.callsign, a.freq]) + writer.table(awacs, headers=["Callsign", "UHF"]) + + writer.heading("Tankers") + tankers = [] + for tanker in self.tankers: + tankers.append([ + tanker.callsign, + tanker.variant, + tanker.tacan, + tanker.freq, + ]) + writer.table(tankers, headers=["Callsign", "Type", "TACAN", "UHF"]) + + writer.heading("JTAC") + jtacs = [] + for jtac in self.jtacs: + jtacs.append([jtac.callsign, jtac.region, jtac.code]) + writer.table(jtacs, headers=["Callsign", "Region", "Laser Code"]) + + writer.write(path) + + def airfield_info_row(self, row_title: str, + airfield: Optional[Airport]) -> List[str]: + """Creates a table row for a given airfield. + + Args: + row_title: Purpose of the airfield. e.g. "Departure", "Arrival" or + "Divert". + airfield: The airfield described by this row. + + Returns: + A list of strings to be used as a row of the airfield table. + """ + if airfield is None: + return [row_title, "", "", "", "", ""] + info = AirfieldInfo(airfield) + return [ + row_title, + airfield.name, + info.atc, + info.tacan, + info.ils, + info.runway, + ] + + +class KneeboardGenerator: + """Creates kneeboard pages for each client flight in the mission.""" + + def __init__(self, mission: Mission, game) -> None: + self.mission = mission + self.game = game + self.comms: List[CommInfo] = [] + self.awacs: List[AwacsInfo] = [] + self.tankers: List[TankerInfo] = [] + self.jtacs: List[JtacInfo] = [] + + def add_comm(self, name: str, freq: str) -> None: + """Adds communications info to the kneeboard. + + Args: + name: Name of the radio channel. + freq: Frequency of the radio channel. + """ + self.comms.append(CommInfo(name, freq)) + + def add_awacs(self, callsign: str, freq: str) -> None: + """Adds an AWACS/GCI to the kneeboard. + + Args: + callsign: Callsign of the AWACS/GCI. + freq: Radio frequency used by the AWACS/GCI. + """ + self.awacs.append(AwacsInfo(callsign, freq)) + + def add_tanker(self, callsign: str, variant: str, freq: str, + tacan: str) -> None: + """Adds a tanker to the kneeboard. + + Args: + callsign: Callsign of the tanker. + variant: Aircraft type. + freq: Radio frequency used by the tanker. + tacan: TACAN channel of the tanker. + """ + self.tankers.append(TankerInfo(callsign, variant, freq, tacan)) + + def add_jtac(self, callsign: str, region: str, code: str) -> None: + """Adds a JTAC to the kneeboard. + + Args: + callsign: Callsign of the JTAC. + region: JTAC's area of responsibility. + code: Laser code used by the JTAC. + """ + # TODO: Radio info? Type? + self.jtacs.append(JtacInfo(callsign, region, code)) + + def generate(self) -> None: + """Generates a kneeboard per client flight.""" + temp_dir = Path("kneeboards") + temp_dir.mkdir(exist_ok=True) + for aircraft, pages in self.pages_by_airframe().items(): + aircraft_dir = temp_dir / aircraft.id + aircraft_dir.mkdir(exist_ok=True) + for idx, page in enumerate(pages): + page_path = aircraft_dir / f"page{idx:02}.png" + page.write(page_path) + self.mission.add_aircraft_kneeboard(aircraft, page_path) + + def pages_by_airframe(self) -> Dict[FlyingType, List[KneeboardPage]]: + """Returns a list of kneeboard pages per airframe in the mission. + + Only client flights will be included, but because DCS does not support + group-specific kneeboard pages, flights (possibly from opposing sides) + will be able to see the kneeboards of all aircraft of the same type. + + Returns: + A dict mapping aircraft types to the list of kneeboard pages for + that aircraft. + """ + all_flights: Dict[FlyingType, List[KneeboardPage]] = defaultdict(list) + for cp in self.game.theater.controlpoints: + if cp.id in self.game.planners.keys(): + for flight in self.game.planners[cp.id].flights: + if flight.client_count > 0: + all_flights[flight.unit_type].extend( + self.generate_flight_kneeboard(flight)) + return all_flights + + def generate_flight_kneeboard(self, flight: Flight) -> List[KneeboardPage]: + """Returns a list of kneeboard pages for the given flight.""" + return [ + BriefingPage( + flight, self.comms, self.awacs, self.tankers, self.jtacs + ), + ] diff --git a/gen/units.py b/gen/units.py new file mode 100644 index 00000000..005e1576 --- /dev/null +++ b/gen/units.py @@ -0,0 +1,6 @@ +"""Unit conversions.""" + + +def meters_to_feet(meters: float) -> float: + """Convers meters to feet.""" + return meters * 3.28084 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 6f314f97..12d48655 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,6 @@ Pyside2>=5.13.0 pyinstaller==3.6 pyproj==2.6.1.post1 + +Pillow~=7.2.0 +tabulate~=0.8.7 \ No newline at end of file diff --git a/resources/fonts/Inconsolata.otf b/resources/fonts/Inconsolata.otf new file mode 100644 index 0000000000000000000000000000000000000000..e7e1fa0cd74847ceaa92fd3c9b4bfc75f329155e GIT binary patch literal 58560 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 Date: Tue, 1 Sep 2020 13:47:41 +0200 Subject: [PATCH 11/61] Added additional informations to the AirfieldData class --- gen/airfields.py | 94 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 88 insertions(+), 6 deletions(-) diff --git a/gen/airfields.py b/gen/airfields.py index 79e540d7..ebc09026 100644 --- a/gen/airfields.py +++ b/gen/airfields.py @@ -22,24 +22,106 @@ class AtcData: class AirfieldData: """Additional airfield data not included in pydcs.""" - #: Radio channels used by the airfield's ATC. - atc: AtcData + #: ICAO airport code + icao: Optional[str] = None + + #: Elevation (in ft). + elevation: int = 0 + + #: Runway length (in ft). + runway_length: int = 0 #: TACAN channel as a string, i.e. "74X". tacan: Optional[str] = None + #: TACAN callsign + tacan_callsign: Optional[str] = None + + #: VOR channel as a string, i.e. "114.90 (MA)". + vor: Optional[str] = None + + #: RSBN channel as a string, i.e. "ch 28 (KW)". + rsbn: Optional[str] = None + + #: Radio channels used by the airfield's ATC. + atc: AtcData = AtcData("", "", "", "") + #: Dict of runway heading -> ILS frequency. ils: Dict[str, RadioFrequency] = field(default_factory=dict) + #: Dict of runway heading -> PRMG info, i.e "ch 26 (KW)" + prmg: Dict[str, str] = field(default_factory=dict) + + #: Dict of runway heading -> outer ndb, i.e "408.00 (KW)" + outer_ndb: Dict[str, str] = field(default_factory=dict) + + #: Dict of runway heading -> inner ndb, i.e "803.00 (K) + inner_ndb: Dict[str, str] = field(default_factory=dict) + def ils_freq(self, runway: str) -> Optional[RadioFrequency]: return self.ils.get(runway) # TODO: Add more airfields. AIRFIELD_DATA = { - "Incirlik": AirfieldData( - AtcData("3.85", "38.6", "129.4", "360.1"), - "21X", - {"050": "109.3", "230": "111.7"} + + # TODO : CAUCASUS MAP + "Batumi": AirfieldData( + "UGSB", + 32, 6792, + "16X", "BTM", + "", "", + AtcData("4.250", "131.000", "40.400", "260.000"), + {"13": "110.30 (ILU)"}, + {}, + {}, + {} ), + + "Kobuleti": AirfieldData( + "UG5X", + 59, 7406, + "67X", "KBL", + "", "", + AtcData("4.350", "133.000", "40.800", "262.000"), + {"7": "111.50 (IKB)"}, + {}, + {"7": "870.00 (KT)"}, + {"7": "490.00 (T)"}, + ), + + "Senaki-Kolkhi": AirfieldData( + "UGKS", + 43, 7256, + "31X", "TSK", + "", "", + AtcData("4.300", "132.000", "40.600", "261.000"), + {"9": "108.90 (ITS)"}, + {}, + {"9": "335.00 (BI)"}, + {"9": "688.00 (I)"}, + ), + + "Kutaisi": AirfieldData( + "UGKO", + 147, 7937, + "44X", "KTS", + "113.60 (KT)", "", + AtcData("4.400", "134.000", "41.000", "263.000"), + {"8": "109.75 (IKS)"}, + {}, + {}, + {}, + ), + + # TODO : PERSIAN GULF MAP + # TODO : SYRIA MAP + # "Incirlik": AirfieldData( + # AtcData("3.85", "38.6", "129.4", "360.1"), + # "21X", + # {"050": "109.3", "230": "111.7"} + # ), + # TODO : NEVADA MAP + # TODO : NORMANDY MAP + # TODO : THE CHANNEL MAP } From 91d6ed0ee7b0f013de1777f8f8b41eeeb9970b96 Mon Sep 17 00:00:00 2001 From: Khopa Date: Tue, 1 Sep 2020 20:23:52 +0200 Subject: [PATCH 12/61] Completed Caucasus airfield data. --- gen/airfields.py | 204 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 204 insertions(+) diff --git a/gen/airfields.py b/gen/airfields.py index ebc09026..7ac283a1 100644 --- a/gen/airfields.py +++ b/gen/airfields.py @@ -114,6 +114,210 @@ AIRFIELD_DATA = { {}, ), + "Sukhumi-Babushara": AirfieldData( + "UGSS", + 43, 11217, + "", "", + "", "", + AtcData("4.150", "129.000", "40.000", "258.000"), + {}, + {}, + {"30": "489.00 (AV)"}, + {"30": "995.00 (A)"}, + ), + + "Gudauta": AirfieldData( + "UG23", + 68, 7839, + "", "", + "", "", + AtcData("4.200", "120.000", "40.200", "259.000"), + {}, + {}, + {}, + {}, + ), + + "Sochi-Adler": AirfieldData( + "URSS", + 98, 9686, + "", "", + "", "", + AtcData("4.050", "127.000", "39.600", "256.000"), + {"6": "111.10 (ISO)"}, + {}, + {}, + {}, + ), + + "Gelendzhik": AirfieldData( + "URKG", + 72, 5452, + "", "", + "114.30 (GN)", "", + AtcData("4.000", "126.000", "39.400", "255.000"), + {}, + {}, + {}, + {}, + ), + + "Novorossiysk": AirfieldData( + "URKN", + 131, 5639, + "", "", + "", "", + AtcData("3.850", "123.000", "38.800", "252.000"), + {}, + {}, + {}, + {}, + ), + + "Anapa-Vityazevo": AirfieldData( + "URKA", + 141, 8623, + "", "", + "", "", + AtcData("3.750", "121.000", "38.400", "250.000"), + {}, + {}, + {"22": "443.00 (AP)", "4": "443.00 (AN)"}, + {"22": "215.00 (P)", "4": "215.00 (N)"}, + ), + + "Krymsk": AirfieldData( + "URKW", + 65, 6733, + "", "", + "", "ch 28 (KW)", + AtcData("3.900", "124.000", "39.000", "253.000"), + {}, + {"4": "ch 26 (OX)", "22": "ch 26 (KW)"}, + {"4": "408.00 (OX)", "22": "408.00 (KW)"}, + {"4": "803.00 (O)", "22": "803.00 (K)"}, + ), + + "Krasnodar-Center": AirfieldData( + "URKL", + 98, 7659, + "", "", + "", "ch 40 (MB)", + AtcData("3.800", "122.000", "38.600", "251.000"), + {}, + {"9": "ch 38 (MB)"}, + {"9": "625.00 (MB)", "27": "625.00 (OC)"}, + {"9": "303.00 (M)", "27": "303.00 (C)"}, + ), + + "Krasnodar-Pashkovsky": AirfieldData( + "URKK", + 111, 9738, + "", "", + "115.80 (KN)", "", + AtcData("4.100", "128.000", "39.800", "257.000"), + {}, + {}, + {"23": "493.00 (LD)", "5": "493.00 (KR)"}, + {"23": "240.00 (L)", "5": "240.00 (K)"}, + ), + + "Maykop-Khanskaya": AirfieldData( + "URKH", + 590, 10195, + "", "", + "", "ch 34 (DG)", + AtcData("3.950", "125.000", "39.200", "254.000"), + {}, + {"4": "ch 36 (DG)"}, + {"4": "289.00 (DG)", "22": "289.00 (RK)"}, + {"4": "591.00 (D)", "22": "591.00 (R)"}, + ), + + "Mineralnye Vody": AirfieldData( + "URMM", + 1049, 12316, + "", "", + "117.10 (MN)", "", + AtcData("4.450", "135.000", "41.200", "264.000"), + {"30": "109.30 (IMW)", "12": "111.70 (IMD)"}, + {}, + {"30": "583.00 (NR)", "12": "583.00 (MD)"}, + {"30": "283.00 (N)", "12": "283.00 (D)"}, + ), + + "Nalchik": AirfieldData( + "URMN", + 1410, 7082, + "", "", + "", "", + AtcData("4.500", "136.000", "41.400", "265.000"), + {"24": "110.50 (INL)"}, + {}, + {"24": "718.00 (NL)"}, + {"24": "350.00 (N)"}, + ), + + "Mozdok": AirfieldData( + "XRMF", + 507, 7734, + "", "", + "", "ch 20 (MZ)", + AtcData("4.550", "137.000", "41.600", "266.000"), + {}, + {"26": "ch 22 (MZ)", "8": "ch 22 (MZ)"}, + {"26": "525.00 (RM)", "8": "525.00 (DO)"}, + {"26": "1.06 (R)", "8": "1.06 (D)"} + ), + + "Beslan": AirfieldData( + "URMO", + 1719, 9327, + "", "", + "", "", + AtcData("4.750", "141.000", "42.400", "270.000"), + {"10": "110.50 (ICH)"}, + {}, + {"10": "1.05 (CX)"}, + {"10": "250.00 (C)"} + ), + + "Tbilisi-Lochini": AirfieldData( + "UGTB", + 1573, 7692, + "25X", "GTB", + "113.70 (INA)", "", + AtcData("4.600", "138.000", "41.800", "267.000"), + {"13": "110.30 (INA)", "30": "108.90 (INA)"}, + {}, + {"13": "342.00 (BP)", "30": "211.00 (NA)"}, + {"13": "923.00 (B)", "30": "435.00 (N)"}, + ), + + "Soganlung": AirfieldData( + "UG24", + 1474, 7871, + "25X", "GTB", + "113.70 (INA)", "", + AtcData("4.650", "139.000", "42.000", "268.000"), + {}, + {}, + {}, + {}, + ), + + "Vaziani": AirfieldData( + "UG27", + 1523, 7842, + "22X", "VAS", + "", "", + AtcData("4.700", "140.000", "42.200", "269.000"), + {"13": "108.75 (IVZ)", "31": "108.75 (IVZ)"}, + {}, + {}, + {}, + ), + # TODO : PERSIAN GULF MAP # TODO : SYRIA MAP # "Incirlik": AirfieldData( From f5f770f401ee13d82448faadfa17389e7275b73e Mon Sep 17 00:00:00 2001 From: Khopa Date: Tue, 1 Sep 2020 20:32:32 +0200 Subject: [PATCH 13/61] Added airfields data for The Channel map. --- gen/airfields.py | 111 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 110 insertions(+), 1 deletion(-) diff --git a/gen/airfields.py b/gen/airfields.py index 7ac283a1..23452f37 100644 --- a/gen/airfields.py +++ b/gen/airfields.py @@ -327,5 +327,114 @@ AIRFIELD_DATA = { # ), # TODO : NEVADA MAP # TODO : NORMANDY MAP - # TODO : THE CHANNEL MAP + + # Channel Map + "Detling": AirfieldData( + "", + 623, 2557, + "", "", + "", "", + AtcData("3.950", "118.400", "38.800", "250.400"), + {}, + {}, + {}, + {}, + ), + + "High Halden": AirfieldData( + "", + 104, 3296, + "", "", + "", "", + AtcData("3.750", "118.800", "38.400", "250.000"), + {}, + {}, + {}, + {}, + ), + + "Lympne": AirfieldData( + "", + 351, 2548, + "", "", + "", "", + AtcData("3.925", "118.350", "38.750", "250.350"), + {}, + {}, + {}, + {}, + ), + + "Hawkinge": AirfieldData( + "", + 524, 3013, + "", "", + "", "", + AtcData("3.900", "38.700", "118.300", "250.300"), + {}, + {}, + {}, + {}, + ), + + "Manston": AirfieldData( + "", + 160, 8626, + "", "", + "", "", + AtcData("3.875", "38.650", "118.250", "250.250"), + {}, + {}, + {}, + {}, + ), + + "Dunkirk Mardyck": AirfieldData( + "", + 16, 1737, + "", "", + "", "", + AtcData("3.850", "38.600", "118.200", "250.200"), + {}, + {}, + {}, + {}, + ), + + "Saint Omer Longuenesse": AirfieldData( + "", + 219, 1929, + "", "", + "", "", + AtcData("3.825", "38.550", "118.150", "250.150"), + {}, + {}, + {}, + {}, + ), + + "Merville Calonne": AirfieldData( + "", + 52, 7580, + "", "", + "", "", + AtcData("3.800", "38.500", "118.100", "250.100"), + {}, + {}, + {}, + {}, + ), + + "Abbeville Drucat": AirfieldData( + "", + 183, 4726, + "", "", + "", "", + AtcData("3.775", "38.450", "118.050", "250.050"), + {}, + {}, + {}, + {}, + ) + } From d429804c1fea364ecdc53039d5bd676f9a0b10f8 Mon Sep 17 00:00:00 2001 From: Khopa Date: Tue, 1 Sep 2020 20:58:27 +0200 Subject: [PATCH 14/61] Airfields data for Nevada --- gen/airfields.py | 216 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 210 insertions(+), 6 deletions(-) diff --git a/gen/airfields.py b/gen/airfields.py index 23452f37..2b51995a 100644 --- a/gen/airfields.py +++ b/gen/airfields.py @@ -326,6 +326,210 @@ AIRFIELD_DATA = { # {"050": "109.3", "230": "111.7"} # ), # TODO : NEVADA MAP + "Mina Airport 3Q0": AirfieldData( + "", + 4562, 4222, + "", "", + "", "", + AtcData("", "", "", ""), + {}, + {}, + {}, + {}, + ), + + "Tonopah Airport": AirfieldData( + "KTPH", + 5394, 6715, + "", "", + "", "", + AtcData("", "", "", ""), + {}, + {}, + {}, + {}, + ), + + "Tonopah Test Range Airfield": AirfieldData( + "KTNX", + 5534, 11633, + "77X", "TQQ", + "113.00 (TQQ)", "", + AtcData("3.800", "124.750", "38.500", "257.950"), + {"32": "111.70 (I-UVV)", "14": "108.30 (I-RVP)"}, + {}, + {}, + {}, + ), + + "Beatty Airport": AirfieldData( + "KBTY", + 3173, 5380, + "", "", + "", "", + AtcData("", "", "", ""), + {}, + {}, + {}, + {}, + ), + + "Pahute Mesa Airstrip": AirfieldData( + "", + 5056, 5420, + "", "", + "", "", + AtcData("", "", "", ""), + {}, + {}, + {}, + {}, + ), + + "Groom Lake AFB": AirfieldData( + "KXTA", + 4494, 11008, + "18X", "GRL", + "", "", + AtcData("3.850", "118.000", "38.600", "250.050"), + {"32": "109.30 (GLRI)"}, + {}, + {}, + {}, + ), + + "Lincoln County": AirfieldData( + "", + 4815, 4408, + "", "", + "", "", + AtcData("", "", "", ""), + {}, + {}, + {}, + {}, + ), + + "Mesquite": AirfieldData( + "67L", + 1858, 4937, + "", "", + "", "", + AtcData("", "", "", ""), + {}, + {}, + {}, + {}, + ), + + "Creech AFB": AirfieldData( + "KINS", + 3126, 6100, + "87X", "INS", + "", "", + AtcData("3.825", "118.300", "38.550", "360.600"), + {"8": "108.70 (ICRR)"}, + {}, + {}, + {}, + ), + + "Echo Bay": AirfieldData( + "OL9", + 3126, 6100, + "87X", "INS", + "", "", + AtcData("3.825", "118.300", "38.550", "360.600"), + {}, + {}, + {}, + {}, + ), + + "Nellis AFB": AirfieldData( + "KLSV", + 1841, 9454, + "12X", "LSV", + "", "", + AtcData("3.900", "132.550", "38.700", "327.000"), + {"21": "109.10 (IDIQ)"}, + {}, + {}, + {}, + ), + + "North Las Vegas": AirfieldData( + "KVGT", + 2228, 4734, + "", "", + "", "", + AtcData("3.775", "125.700", "38.450", "360.750"), + {}, + {}, + {}, + {}, + ), + + "McCarran International Airport": AirfieldData( + "KLAS", + 2169, 10377, + "116X", "LAS", + "116.90 (LAS)", "", + AtcData("3.875", "119.900", "38.650", "257.800"), + {"25": "110.30 (I-LAS)"}, + {}, + {}, + {}, + ), + + "Henderson Executive Airport": AirfieldData( + "KHND", + 2491, 5999, + "", "", + "", "", + AtcData("3.925", "125.100", "38.750", "250.100"), + {}, + {}, + {}, + {}, + ), + + "Boulder City Airport": AirfieldData( + "KBVU", + 2121, 4612, + "", "", + "", "", + AtcData("", "", "", ""), + {}, + {}, + {}, + {}, + ), + + "Jean Airport": AirfieldData( + "", + 2824, 4053, + "", "", + "", "", + AtcData("", "", "", ""), + {}, + {}, + {}, + {}, + ), + + "Laughlin Airport": AirfieldData( + "KIFP", + 656, 7139, + "", "", + "", "", + AtcData("3.750", "123.900", "38.400", "250.000"), + {}, + {}, + {}, + {}, + ), + # TODO : NORMANDY MAP # Channel Map @@ -370,7 +574,7 @@ AIRFIELD_DATA = { 524, 3013, "", "", "", "", - AtcData("3.900", "38.700", "118.300", "250.300"), + AtcData("3.900", "118.300", "38.700", "250.300"), {}, {}, {}, @@ -382,7 +586,7 @@ AIRFIELD_DATA = { 160, 8626, "", "", "", "", - AtcData("3.875", "38.650", "118.250", "250.250"), + AtcData("3.875", "118.250", "38.650", "250.250"), {}, {}, {}, @@ -394,7 +598,7 @@ AIRFIELD_DATA = { 16, 1737, "", "", "", "", - AtcData("3.850", "38.600", "118.200", "250.200"), + AtcData("3.850", "118.200", "38.600", "250.200"), {}, {}, {}, @@ -406,7 +610,7 @@ AIRFIELD_DATA = { 219, 1929, "", "", "", "", - AtcData("3.825", "38.550", "118.150", "250.150"), + AtcData("3.825", "118.150", "38.550" "250.150"), {}, {}, {}, @@ -418,7 +622,7 @@ AIRFIELD_DATA = { 52, 7580, "", "", "", "", - AtcData("3.800", "38.500", "118.100", "250.100"), + AtcData("3.800", "118.100", "38.500", "250.100"), {}, {}, {}, @@ -430,7 +634,7 @@ AIRFIELD_DATA = { 183, 4726, "", "", "", "", - AtcData("3.775", "38.450", "118.050", "250.050"), + AtcData("3.775", "118.050", "38.450", "250.050"), {}, {}, {}, From f5f45a098e5348ce3866d4865bf97fd365165f34 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Thu, 3 Sep 2020 00:37:48 -0700 Subject: [PATCH 15/61] Sort objective name combo boxes. --- qt_ui/widgets/combos/QFilteredComboBox.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qt_ui/widgets/combos/QFilteredComboBox.py b/qt_ui/widgets/combos/QFilteredComboBox.py index 9597c42b..7d152b2e 100644 --- a/qt_ui/widgets/combos/QFilteredComboBox.py +++ b/qt_ui/widgets/combos/QFilteredComboBox.py @@ -35,6 +35,7 @@ class QFilteredComboBox(QComboBox): super(QFilteredComboBox, self).setModel(model) self.pFilterModel.setSourceModel(model) self.completer.setModel(self.pFilterModel) + self.model().sort(0) def setModelColumn(self, column): self.completer.setCompletionColumn(column) From b4e3067718e2fb77c64c6499e72b7268be376a22 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Wed, 2 Sep 2020 16:34:42 -0700 Subject: [PATCH 16/61] Update pydcs. --- pydcs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydcs b/pydcs index d278df68..5c02bf8e 160000 --- a/pydcs +++ b/pydcs @@ -1 +1 @@ -Subproject commit d278df68eee2f486f840c178e17893f58313efb8 +Subproject commit 5c02bf8ea5e3ec5afccc0135e31a3dd15e21342b From af596c58c32f24d16f95da6105d3a7879c5fbbc1 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sun, 30 Aug 2020 17:46:02 -0700 Subject: [PATCH 17/61] Control radio/TACAN allocation, set flight radios. Add central registries for allocating TACAN/radio channels to the Operation. These ensure that each channel is allocated uniquely, and removes the caller's need to think about which frequency to use. The registry allocates frequencies based on the radio it is given, which ensures that the allocated frequency will be compatible with the radio that needs it. A mapping from aircraft to the radio used by that aircraft for intra-flight comms (i.e. the F-16 uses the AN/ARC-222) exists for creating infra-flight channels appropriate for the aircraft. Inter-flight channels are allocated by a generic UHF radio. I've moved the inter-flight radio channels from the VHF to UHF range, since that's the most easily allocated band, and inter-flight will be in the highest demand. Intra-flight radios are now generally not shared. For aircraft where the radio type is not known we will still fall back to the shared channel, but that will stop being the case as we gain more data. Tankers have been moved to the Y TACAN band. Not completely needed, but seems typical for most missions and deconflicts the tankers from any unknown airfields (which always use the X band in DCS). --- game/operation/operation.py | 49 +- gen/aircraft.py | 112 ++++- gen/airfields.py | 907 ++++++++++++++++++------------------ gen/airsupportgen.py | 79 +++- gen/flights/flight.py | 18 +- gen/kneeboard.py | 47 +- gen/radios.py | 200 ++++++++ gen/tacan.py | 83 ++++ 8 files changed, 960 insertions(+), 535 deletions(-) create mode 100644 gen/radios.py create mode 100644 gen/tacan.py diff --git a/game/operation/operation.py b/game/operation/operation.py index 5f7a9503..09242628 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -4,8 +4,9 @@ from dcs.terrain import Terrain from gen import * from userdata.debriefing import * - -TANKER_CALLSIGNS = ["Texaco", "Arco", "Shell"] +from gen.airfields import AIRFIELD_DATA +from gen.radios import RadioRegistry +from gen.tacan import TacanRegistry class Operation: @@ -25,6 +26,8 @@ class Operation: groundobjectgen = None # type: GroundObjectsGenerator briefinggen = None # type: BriefingGenerator forcedoptionsgen = None # type: ForcedOptionsGenerator + radio_registry: Optional[RadioRegistry] = None + tacan_registry: Optional[TacanRegistry] = None environment_settings = None trigger_radius = TRIGGER_RADIUS_MEDIUM @@ -63,8 +66,14 @@ class Operation: def initialize(self, mission: Mission, conflict: Conflict): self.current_mission = mission self.conflict = conflict - self.airgen = AircraftConflictGenerator(mission, conflict, self.game.settings, self.game) - self.airsupportgen = AirSupportConflictGenerator(mission, conflict, self.game) + self.radio_registry = RadioRegistry() + self.tacan_registry = TacanRegistry() + self.airgen = AircraftConflictGenerator( + mission, conflict, self.game.settings, self.game, + self.radio_registry) + self.airsupportgen = AirSupportConflictGenerator( + mission, conflict, self.game, self.radio_registry, + self.tacan_registry) self.triggersgen = TriggersGenerator(mission, conflict, self.game) self.visualgen = VisualGenerator(mission, conflict, self.game) self.envgen = EnviromentGenerator(mission, conflict, self.game) @@ -120,6 +129,17 @@ class Operation: # Generate ground object first self.groundobjectgen.generate() + for airfield, data in AIRFIELD_DATA.items(): + if data.theater == self.game.theater.terrain.name: + self.radio_registry.reserve(data.atc.hf) + self.radio_registry.reserve(data.atc.vhf_fm) + self.radio_registry.reserve(data.atc.vhf_am) + self.radio_registry.reserve(data.atc.uhf) + for ils in data.ils.values(): + self.radio_registry.reserve(ils) + if data.tacan is not None: + self.tacan_registry.reserve(data.tacan) + # Generate destroyed units for d in self.game.get_destroyed_units(): try: @@ -224,21 +244,16 @@ class Operation: kneeboard_generator = KneeboardGenerator(self.current_mission, self.game) # Briefing Generation - for i, tanker_type in enumerate(self.airsupportgen.generated_tankers): - callsign = TANKER_CALLSIGNS[i] - tacan = f"{60 + i}X" - freq = f"{130 + i} MHz AM" - self.briefinggen.append_frequency(f"Tanker {callsign} ({tanker_type})", f"{tacan}/{freq}") - kneeboard_generator.add_tanker(callsign, tanker_type, freq, tacan) + for tanker in self.airsupportgen.air_support.tankers: + self.briefinggen.append_frequency( + f"Tanker {tanker.callsign} ({tanker.variant})", + f"{tanker.tacan}/{tanker.freq}") + kneeboard_generator.add_tanker(tanker) if self.is_awacs_enabled: - callsign = "AWACS" - freq = "233 MHz AM" - self.briefinggen.append_frequency(callsign, freq) - kneeboard_generator.add_awacs(callsign, freq) - - self.briefinggen.append_frequency("Flight", "251 MHz AM") - kneeboard_generator.add_comm("Flight", "251 MHz AM") + for awacs in self.airsupportgen.air_support.awacs: + self.briefinggen.append_frequency(awacs.callsign, awacs.freq) + kneeboard_generator.add_awacs(awacs) # Generate the briefing self.briefinggen.generate() diff --git a/gen/aircraft.py b/gen/aircraft.py index c5eb8ae8..aa5a62e4 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -1,14 +1,32 @@ -from dcs.action import ActivateGroup, AITaskPush, MessageToCoalition, MessageToAll -from dcs.condition import TimeAfter, CoalitionHasAirdrome, PartOfCoalitionInZone -from dcs.helicopters import UH_1H -from dcs.terrain.terrain import NoParkingSlotError -from dcs.triggers import TriggerOnce, Event +from dataclasses import dataclass +from typing import Dict from game.data.cap_capabilities_db import GUNFIGHTERS from game.settings import Settings from game.utils import nm_to_meter from gen.flights.ai_flight_planner import FlightPlanner from gen.flights.flight import Flight, FlightType, FlightWaypointType +from gen.radios import get_radio, MHz, Radio, RadioFrequency, RadioRegistry +from pydcs.dcs import helicopters +from pydcs.dcs.action import ActivateGroup, AITaskPush, MessageToAll +from pydcs.dcs.condition import TimeAfter, CoalitionHasAirdrome, PartOfCoalitionInZone +from pydcs.dcs.helicopters import helicopter_map, UH_1H +from pydcs.dcs.mission import Mission, StartType +from pydcs.dcs.planes import ( + Bf_109K_4, + FW_190A8, + FW_190D9, + I_16, + Ju_88A4, + P_47D_30, + P_51D, + P_51D_30_NA, + SpitfireLFMkIX, + SpitfireLFMkIXCW, +) +from pydcs.dcs.terrain.terrain import NoParkingSlotError +from pydcs.dcs.triggers import TriggerOnce, Event +from pydcs.dcs.unittype import UnitType from .conflictgen import * from .naming import * @@ -23,17 +41,83 @@ RTB_ALTITUDE = 800 RTB_DISTANCE = 5000 HELI_ALT = 500 +# Note that fallback radio channels will *not* be reserved. It's possible that +# flights using these will overlap with other channels. This is because we would +# need to make sure we fell back to a frequency that is not used by any beacon +# or ATC, which we don't have the information to predict. Deal with the minor +# annoyance for now since we'll be fleshing out radio info soon enough. +ALLIES_WW2_CHANNEL = MHz(124) +GERMAN_WW2_CHANNEL = MHz(40) +HELICOPTER_CHANNEL = MHz(127) +UHF_FALLBACK_CHANNEL = MHz(251) + + +@dataclass(frozen=True) +class AircraftData: + """Additional aircraft data not exposed by pydcs.""" + + #: The type of radio used for intra-flight communications. + intra_flight_radio: Radio + + +# Indexed by the id field of the pydcs PlaneType. +AIRCRAFT_DATA: Dict[str, AircraftData] = { + "A-10C": AircraftData(get_radio("AN/ARC-186(V) AM")), + "F-16C_50": AircraftData(get_radio("AN/ARC-222")), + "F/A-18C": AircraftData(get_radio("AN/ARC-210")), +} + + +# TODO: Get radio information for all the special cases. +def get_fallback_channel(unit_type: UnitType) -> RadioFrequency: + if unit_type in helicopter_map.values() and unit_type != UH_1H: + return HELICOPTER_CHANNEL + + german_ww2_aircraft = [ + Bf_109K_4, + FW_190A8, + FW_190D9, + Ju_88A4, + ] + + if unit_type in german_ww2_aircraft: + return GERMAN_WW2_CHANNEL + + allied_ww2_aircraft = [ + I_16, + P_47D_30, + P_51D, + P_51D_30_NA, + SpitfireLFMkIX, + SpitfireLFMkIXCW, + ] + + if unit_type in allied_ww2_aircraft: + return ALLIES_WW2_CHANNEL + + return UHF_FALLBACK_CHANNEL + class AircraftConflictGenerator: escort_targets = [] # type: typing.List[typing.Tuple[FlyingGroup, int]] - def __init__(self, mission: Mission, conflict: Conflict, settings: Settings, game): + def __init__(self, mission: Mission, conflict: Conflict, settings: Settings, + game, radio_registry: RadioRegistry): self.m = mission self.game = game self.settings = settings self.conflict = conflict + self.radio_registry = radio_registry self.escort_targets = [] + def get_intra_flight_channel(self, airframe: UnitType) -> RadioFrequency: + try: + aircraft_data = AIRCRAFT_DATA[airframe.id] + return self.radio_registry.alloc_for_radio( + aircraft_data.intra_flight_radio) + except KeyError: + return get_fallback_channel(airframe) + def _start_type(self) -> StartType: return self.settings.cold_start and StartType.Cold or StartType.Warm @@ -90,19 +174,9 @@ class AircraftConflictGenerator: group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)) - # TODO : refactor this following bad specific special case code :( - - if unit_type in helicopters.helicopter_map.values() and unit_type not in [UH_1H]: - group.set_frequency(127.5) - else: - if unit_type not in [P_51D_30_NA, P_51D, SpitfireLFMkIX, SpitfireLFMkIXCW, P_47D_30, I_16, FW_190A8, FW_190D9, Bf_109K_4]: - group.set_frequency(251.0) - else: - # WW2 - if unit_type in [FW_190A8, FW_190D9, Bf_109K_4, Ju_88A4]: - group.set_frequency(40) - else: - group.set_frequency(124.0) + channel = self.get_intra_flight_channel(unit_type) + group.set_frequency(channel.mhz) + flight.intra_flight_channel = channel # Special case so Su 33 carrier take off if unit_type is Su_33: diff --git a/gen/airfields.py b/gen/airfields.py index 2b51995a..38d4ccdb 100644 --- a/gen/airfields.py +++ b/gen/airfields.py @@ -4,10 +4,10 @@ Remove once https://github.com/pydcs/dcs/issues/69 tracks getting the missing data added to pydcs. Until then, missing data can be manually filled in here. """ from dataclasses import dataclass, field -from typing import Dict, List, Optional +from typing import Dict, Optional, Tuple - -RadioFrequency = str +from .radios import MHz, RadioFrequency +from .tacan import TacanBand, TacanChannel @dataclass @@ -21,6 +21,8 @@ class AtcData: @dataclass class AirfieldData: """Additional airfield data not included in pydcs.""" + #: Name of the theater the airport is in. + theater: str #: ICAO airport code icao: Optional[str] = None @@ -31,614 +33,615 @@ class AirfieldData: #: Runway length (in ft). runway_length: int = 0 - #: TACAN channel as a string, i.e. "74X". - tacan: Optional[str] = None + #: TACAN channel for the airfield. + tacan: Optional[TacanChannel] = None #: TACAN callsign tacan_callsign: Optional[str] = None - #: VOR channel as a string, i.e. "114.90 (MA)". - vor: Optional[str] = None + #: VOR as a tuple of (callsign, frequency). + vor: Optional[Tuple[str, RadioFrequency]] = None - #: RSBN channel as a string, i.e. "ch 28 (KW)". - rsbn: Optional[str] = None + #: RSBN channel as a tuple of (callsign, channel). + rsbn: Optional[Tuple[str, int]] = None - #: Radio channels used by the airfield's ATC. - atc: AtcData = AtcData("", "", "", "") + #: Radio channels used by the airfield's ATC. Note that not all airfields + #: have ATCs. + atc: Optional[AtcData] = None - #: Dict of runway heading -> ILS frequency. - ils: Dict[str, RadioFrequency] = field(default_factory=dict) + #: Dict of runway heading -> ILS tuple of (callsign, frequency). + ils: Dict[str, Tuple[str, RadioFrequency]] = field(default_factory=dict) - #: Dict of runway heading -> PRMG info, i.e "ch 26 (KW)" - prmg: Dict[str, str] = field(default_factory=dict) + #: Dict of runway heading -> PRMG tuple of (callsign, channel). + prmg: Dict[str, Tuple[str, int]] = field(default_factory=dict) - #: Dict of runway heading -> outer ndb, i.e "408.00 (KW)" - outer_ndb: Dict[str, str] = field(default_factory=dict) + #: Dict of runway heading -> outer NDB tuple of (callsign, frequency). + outer_ndb: Dict[str, Tuple[str, RadioFrequency]] = field(default_factory=dict) - #: Dict of runway heading -> inner ndb, i.e "803.00 (K) - inner_ndb: Dict[str, str] = field(default_factory=dict) + #: Dict of runway heading -> inner NDB tuple of (callsign, frequency). + inner_ndb: Dict[str, Tuple[str, RadioFrequency]] = field(default_factory=dict) def ils_freq(self, runway: str) -> Optional[RadioFrequency]: - return self.ils.get(runway) + ils = self.ils.get(runway) + if ils is not None: + return ils[1] + return None # TODO: Add more airfields. AIRFIELD_DATA = { + # Caucasus - # TODO : CAUCASUS MAP "Batumi": AirfieldData( - "UGSB", - 32, 6792, - "16X", "BTM", - "", "", - AtcData("4.250", "131.000", "40.400", "260.000"), - {"13": "110.30 (ILU)"}, - {}, - {}, - {} + theater="Caucasus", + icao="UGSB", + elevation=32, + runway_length=6792, + tacan=TacanChannel(16, TacanBand.X), + tacan_callsign="BTM", + atc=AtcData(MHz(4, 250), MHz(131, 0), MHz(40, 400), MHz(260, 0)), + ils={ + "13": ("ILU", MHz(110, 30)), + }, ), "Kobuleti": AirfieldData( - "UG5X", - 59, 7406, - "67X", "KBL", - "", "", - AtcData("4.350", "133.000", "40.800", "262.000"), - {"7": "111.50 (IKB)"}, - {}, - {"7": "870.00 (KT)"}, - {"7": "490.00 (T)"}, + theater="Caucasus", + icao="UG5X", + elevation=59, + runway_length=7406, + tacan=TacanChannel(67, TacanBand.X), + tacan_callsign="KBL", + atc=AtcData(MHz(4, 350), MHz(133, 0), MHz(40, 800), MHz(262, 0)), + ils={ + "7": ("IKB", MHz(111, 50)), + }, + outer_ndb={ + "7": ("KT", MHz(870, 0)), + }, + inner_ndb={ + "7": ("T", MHz(490, 0)), + }, ), "Senaki-Kolkhi": AirfieldData( - "UGKS", - 43, 7256, - "31X", "TSK", - "", "", - AtcData("4.300", "132.000", "40.600", "261.000"), - {"9": "108.90 (ITS)"}, - {}, - {"9": "335.00 (BI)"}, - {"9": "688.00 (I)"}, + theater="Caucasus", + icao="UGKS", + elevation=43, + runway_length=7256, + tacan=TacanChannel(31, TacanBand.X), + tacan_callsign="TSK", + atc=AtcData(MHz(4, 300), MHz(132, 0), MHz(40, 600), MHz(261, 0)), + ils={ + "9": ("ITS", MHz(108, 90)), + }, + outer_ndb={ + "9": ("BI", MHz(335, 0)), + }, + inner_ndb={ + "9": ("I", MHz(688, 0)), + }, ), "Kutaisi": AirfieldData( - "UGKO", - 147, 7937, - "44X", "KTS", - "113.60 (KT)", "", - AtcData("4.400", "134.000", "41.000", "263.000"), - {"8": "109.75 (IKS)"}, - {}, - {}, - {}, + theater="Caucasus", + icao="UGKO", + elevation=147, + runway_length=7937, + tacan=TacanChannel(44, TacanBand.X), + tacan_callsign="KTS", + atc=AtcData(MHz(4, 400), MHz(134, 0), MHz(41, 0), MHz(263, 0)), + ils={ + "8": ("IKS", MHz(109, 75)), + }, ), "Sukhumi-Babushara": AirfieldData( - "UGSS", - 43, 11217, - "", "", - "", "", - AtcData("4.150", "129.000", "40.000", "258.000"), - {}, - {}, - {"30": "489.00 (AV)"}, - {"30": "995.00 (A)"}, + theater="Caucasus", + icao="UGSS", + elevation=43, + runway_length=11217, + atc=AtcData(MHz(4, 150), MHz(129, 0), MHz(40, 0), MHz(258, 0)), + outer_ndb={ + "30": ("AV", MHz(489, 0)), + }, + inner_ndb={ + "30": ("A", MHz(995, 0)), + }, ), "Gudauta": AirfieldData( - "UG23", - 68, 7839, - "", "", - "", "", - AtcData("4.200", "120.000", "40.200", "259.000"), - {}, - {}, - {}, - {}, + theater="Caucasus", + icao="UG23", + elevation=68, + runway_length=7839, + atc=AtcData(MHz(4, 200), MHz(120, 0), MHz(40, 200), MHz(259, 0)), ), "Sochi-Adler": AirfieldData( - "URSS", - 98, 9686, - "", "", - "", "", - AtcData("4.050", "127.000", "39.600", "256.000"), - {"6": "111.10 (ISO)"}, - {}, - {}, - {}, + theater="Caucasus", + icao="URSS", + elevation=98, + runway_length=9686, + atc=AtcData(MHz(4, 50), MHz(127, 0), MHz(39, 600), MHz(256, 0)), + ils={ + "6": ("ISO", MHz(111, 10)), + }, ), "Gelendzhik": AirfieldData( - "URKG", - 72, 5452, - "", "", - "114.30 (GN)", "", - AtcData("4.000", "126.000", "39.400", "255.000"), - {}, - {}, - {}, - {}, + theater="Caucasus", + icao="URKG", + elevation=72, + runway_length=5452, + vor=("GN", MHz(114, 30)), + atc=AtcData(MHz(4, 0), MHz(126, 0), MHz(39, 400), MHz(255, 0)), ), "Novorossiysk": AirfieldData( - "URKN", - 131, 5639, - "", "", - "", "", - AtcData("3.850", "123.000", "38.800", "252.000"), - {}, - {}, - {}, - {}, + theater="Caucasus", + icao="URKN", + elevation=131, + runway_length=5639, + atc=AtcData(MHz(3, 850), MHz(123, 0), MHz(38, 800), MHz(252, 0)), ), "Anapa-Vityazevo": AirfieldData( - "URKA", - 141, 8623, - "", "", - "", "", - AtcData("3.750", "121.000", "38.400", "250.000"), - {}, - {}, - {"22": "443.00 (AP)", "4": "443.00 (AN)"}, - {"22": "215.00 (P)", "4": "215.00 (N)"}, + theater="Caucasus", + icao="URKA", + elevation=141, + runway_length=8623, + atc=AtcData(MHz(3, 750), MHz(121, 0), MHz(38, 400), MHz(250, 0)), + outer_ndb={ + "22": ("AP", MHz(443, 0)), "4": "443.00 (AN)" + }, + inner_ndb={ + "22": ("P", MHz(215, 0)), "4": "215.00 (N)" + }, ), "Krymsk": AirfieldData( - "URKW", - 65, 6733, - "", "", - "", "ch 28 (KW)", - AtcData("3.900", "124.000", "39.000", "253.000"), - {}, - {"4": "ch 26 (OX)", "22": "ch 26 (KW)"}, - {"4": "408.00 (OX)", "22": "408.00 (KW)"}, - {"4": "803.00 (O)", "22": "803.00 (K)"}, + theater="Caucasus", + icao="URKW", + elevation=65, + runway_length=6733, + rsbn=("KW", 28), + atc=AtcData(MHz(3, 900), MHz(124, 0), MHz(39, 0), MHz(253, 0)), + prmg={ + "4": ("OX", 26), + "22": ("KW", 26), + }, + outer_ndb={ + "4": ("OX", MHz(408, 0)), + "22": ("KW", MHz(408, 0)), + }, + inner_ndb={ + "4": ("O", MHz(803, 0)), + "22": ("K", MHz(803, 0)), + }, ), "Krasnodar-Center": AirfieldData( - "URKL", - 98, 7659, - "", "", - "", "ch 40 (MB)", - AtcData("3.800", "122.000", "38.600", "251.000"), - {}, - {"9": "ch 38 (MB)"}, - {"9": "625.00 (MB)", "27": "625.00 (OC)"}, - {"9": "303.00 (M)", "27": "303.00 (C)"}, + theater="Caucasus", + icao="URKL", + elevation=98, + runway_length=7659, + rsbn=("MB", 40), + atc=AtcData(MHz(3, 800), MHz(122, 0), MHz(38, 600), MHz(251, 0)), + prmg={ + "9": ("MB", 38), + }, + outer_ndb={ + "9": ("MB", MHz(625, 0)), + "27": ("OC", MHz(625, 0)), + }, + inner_ndb={ + "9": ("M", MHz(303, 0)), + "27": ("C", MHz(303, 0)), + }, ), "Krasnodar-Pashkovsky": AirfieldData( - "URKK", - 111, 9738, - "", "", - "115.80 (KN)", "", - AtcData("4.100", "128.000", "39.800", "257.000"), - {}, - {}, - {"23": "493.00 (LD)", "5": "493.00 (KR)"}, - {"23": "240.00 (L)", "5": "240.00 (K)"}, + theater="Caucasus", + icao="URKK", + elevation=111, + runway_length=9738, + vor=("KN", MHz(115, 80)), + atc=AtcData(MHz(4, 100), MHz(128, 0), MHz(39, 800), MHz(257, 0)), + outer_ndb={ + "23": ("LD", MHz(493, 0)), + "5": ("KR", MHz(493, 0)), + }, + inner_ndb={ + "23": ("L", MHz(240, 0)), + "5": ("K", MHz(240, 0)), + }, ), "Maykop-Khanskaya": AirfieldData( - "URKH", - 590, 10195, - "", "", - "", "ch 34 (DG)", - AtcData("3.950", "125.000", "39.200", "254.000"), - {}, - {"4": "ch 36 (DG)"}, - {"4": "289.00 (DG)", "22": "289.00 (RK)"}, - {"4": "591.00 (D)", "22": "591.00 (R)"}, + theater="Caucasus", + icao="URKH", + elevation=590, + runway_length=10195, + rsbn=("DG", 34), + atc=AtcData(MHz(3, 950), MHz(125, 0), MHz(39, 200), MHz(254, 0)), + prmg={ + "4": ("DG", 36), + }, + outer_ndb={ + "4": ("DG", MHz(289, 0)), + "22": ("RK", MHz(289, 0)), + }, + inner_ndb={ + "4": ("D", MHz(591, 0)), + "22": ("R", MHz(591, 0)), + }, ), "Mineralnye Vody": AirfieldData( - "URMM", - 1049, 12316, - "", "", - "117.10 (MN)", "", - AtcData("4.450", "135.000", "41.200", "264.000"), - {"30": "109.30 (IMW)", "12": "111.70 (IMD)"}, - {}, - {"30": "583.00 (NR)", "12": "583.00 (MD)"}, - {"30": "283.00 (N)", "12": "283.00 (D)"}, + theater="Caucasus", + icao="URMM", + elevation=1049, + runway_length=12316, + vor=("MN", MHz(117, 10)), + atc=AtcData(MHz(4, 450), MHz(135, 0), MHz(41, 200), MHz(264, 0)), + ils={ + "30": ("IMW", MHz(109, 30)), + "12": ("IMD", MHz(111, 70)), + }, + outer_ndb={ + "30": ("NR", MHz(583, 0)), + "12": ("MD", MHz(583, 0)), + }, + inner_ndb={ + "30": ("N", MHz(283, 0)), + "12": ("D", MHz(283, 0)), + }, ), "Nalchik": AirfieldData( - "URMN", - 1410, 7082, - "", "", - "", "", - AtcData("4.500", "136.000", "41.400", "265.000"), - {"24": "110.50 (INL)"}, - {}, - {"24": "718.00 (NL)"}, - {"24": "350.00 (N)"}, + theater="Caucasus", + icao="URMN", + elevation=1410, + runway_length=7082, + atc=AtcData(MHz(4, 500), MHz(136, 0), MHz(41, 400), MHz(265, 0)), + ils={ + "24": ("INL", MHz(110, 50)), + }, + outer_ndb={ + "24": ("NL", MHz(718, 0)), + }, + inner_ndb={ + "24": ("N", MHz(350, 0)), + }, ), "Mozdok": AirfieldData( - "XRMF", - 507, 7734, - "", "", - "", "ch 20 (MZ)", - AtcData("4.550", "137.000", "41.600", "266.000"), - {}, - {"26": "ch 22 (MZ)", "8": "ch 22 (MZ)"}, - {"26": "525.00 (RM)", "8": "525.00 (DO)"}, - {"26": "1.06 (R)", "8": "1.06 (D)"} + theater="Caucasus", + icao="XRMF", + elevation=507, + runway_length=7734, + rsbn=("MZ", 20), + atc=AtcData(MHz(4, 550), MHz(137, 0), MHz(41, 600), MHz(266, 0)), + prmg={ + "26": ("MZ", 22), + "8": ("MZ", 22), + }, + outer_ndb={ + "26": ("RM", MHz(525, 0)), + "8": ("DO", MHz(525, 0)), + }, + inner_ndb={ + "26": ("R", MHz(1, 6)), + "8": ("D", MHz(1, 6)), + } ), "Beslan": AirfieldData( - "URMO", - 1719, 9327, - "", "", - "", "", - AtcData("4.750", "141.000", "42.400", "270.000"), - {"10": "110.50 (ICH)"}, - {}, - {"10": "1.05 (CX)"}, - {"10": "250.00 (C)"} + theater="Caucasus", + icao="URMO", + elevation=1719, + runway_length=9327, + atc=AtcData(MHz(4, 750), MHz(141, 0), MHz(42, 400), MHz(270, 0)), + ils={ + "10": ("ICH", MHz(110, 50)), + }, + outer_ndb={ + "10": ("CX", MHz(1, 5)), + }, + inner_ndb={ + "10": ("C", MHz(250, 0)), + } ), "Tbilisi-Lochini": AirfieldData( - "UGTB", - 1573, 7692, - "25X", "GTB", - "113.70 (INA)", "", - AtcData("4.600", "138.000", "41.800", "267.000"), - {"13": "110.30 (INA)", "30": "108.90 (INA)"}, - {}, - {"13": "342.00 (BP)", "30": "211.00 (NA)"}, - {"13": "923.00 (B)", "30": "435.00 (N)"}, + theater="Caucasus", + icao="UGTB", + elevation=1573, + runway_length=7692, + tacan=TacanChannel(25, TacanBand.X), + tacan_callsign="GTB", + atc=AtcData(MHz(4, 600), MHz(138, 0), MHz(41, 800), MHz(267, 0)), + ils={ + "13": ("INA", MHz(110, 30)), + "30": ("INA", MHz(108, 90)), + }, + outer_ndb={ + "13": ("BP", MHz(342, 0)), + "30": ("NA", MHz(211, 0)), + }, + inner_ndb={ + "13": ("B", MHz(923, 0)), + "30": ("N", MHz(435, 0)), + }, ), "Soganlung": AirfieldData( - "UG24", - 1474, 7871, - "25X", "GTB", - "113.70 (INA)", "", - AtcData("4.650", "139.000", "42.000", "268.000"), - {}, - {}, - {}, - {}, + theater="Caucasus", + icao="UG24", + elevation=1474, + runway_length=7871, + tacan=TacanChannel(25, TacanBand.X), + tacan_callsign="GTB", + atc=AtcData(MHz(4, 650), MHz(139, 0), MHz(42, 0), MHz(268, 0)), ), "Vaziani": AirfieldData( - "UG27", - 1523, 7842, - "22X", "VAS", - "", "", - AtcData("4.700", "140.000", "42.200", "269.000"), - {"13": "108.75 (IVZ)", "31": "108.75 (IVZ)"}, - {}, - {}, - {}, + theater="Caucasus", + icao="UG27", + elevation=1523, + runway_length=7842, + tacan=TacanChannel(22, TacanBand.X), + tacan_callsign="VAS", + atc=AtcData(MHz(4, 700), MHz(140, 0), MHz(42, 200), MHz(269, 0)), + ils={ + "13": ("IVZ", MHz(108, 75)), + "31": ("IVZ", MHz(108, 75)), + }, ), # TODO : PERSIAN GULF MAP # TODO : SYRIA MAP - # "Incirlik": AirfieldData( - # AtcData("3.85", "38.6", "129.4", "360.1"), - # "21X", - # {"050": "109.3", "230": "111.7"} - # ), - # TODO : NEVADA MAP + + "Incirlik": AirfieldData( + theater="Syria", + icao="LTAG", + elevation=156, + runway_length=9662, + tacan=TacanChannel(21, TacanBand.X), + tacan_callsign="DAN", + vor=("DAN", MHz(108, 400)), + atc=AtcData(MHz(3, 850), MHz(38, 600), MHz(129, 400), MHz(360, 100)), + ils={ + "050": ("IDAN", MHz(109, 300)), + "230": ("DANM", MHz(111, 700)), + }, + ), + + # NTTR "Mina Airport 3Q0": AirfieldData( - "", - 4562, 4222, - "", "", - "", "", - AtcData("", "", "", ""), - {}, - {}, - {}, - {}, + theater="NTTR", + elevation=4562, + runway_length=4222, ), "Tonopah Airport": AirfieldData( - "KTPH", - 5394, 6715, - "", "", - "", "", - AtcData("", "", "", ""), - {}, - {}, - {}, - {}, + theater="NTTR", + icao="KTPH", + elevation=5394, + runway_length=6715, ), "Tonopah Test Range Airfield": AirfieldData( - "KTNX", - 5534, 11633, - "77X", "TQQ", - "113.00 (TQQ)", "", - AtcData("3.800", "124.750", "38.500", "257.950"), - {"32": "111.70 (I-UVV)", "14": "108.30 (I-RVP)"}, - {}, - {}, - {}, + theater="NTTR", + icao="KTNX", + elevation=5534, + runway_length=11633, + tacan=TacanChannel(77, TacanBand.X), + tacan_callsign="TQQ", + atc=AtcData(MHz(3, 800), MHz(124, 750), MHz(38, 500), MHz(257, 950)), + ils={ + "32": ("I-UVV", MHz(111, 70)), + "14": ("I-RVP", MHz(108, 30)), + }, ), "Beatty Airport": AirfieldData( - "KBTY", - 3173, 5380, - "", "", - "", "", - AtcData("", "", "", ""), - {}, - {}, - {}, - {}, + theater="NTTR", + icao="KBTY", + elevation=3173, + runway_length=5380, ), "Pahute Mesa Airstrip": AirfieldData( - "", - 5056, 5420, - "", "", - "", "", - AtcData("", "", "", ""), - {}, - {}, - {}, - {}, + theater="NTTR", + elevation=5056, + runway_length=5420, ), "Groom Lake AFB": AirfieldData( - "KXTA", - 4494, 11008, - "18X", "GRL", - "", "", - AtcData("3.850", "118.000", "38.600", "250.050"), - {"32": "109.30 (GLRI)"}, - {}, - {}, - {}, + theater="NTTR", + icao="KXTA", + elevation=4494, + runway_length=11008, + tacan=TacanChannel(18, TacanBand.X), + tacan_callsign="GRL", + atc=AtcData(MHz(3, 850), MHz(118, 0), MHz(38, 600), MHz(250, 50)), + ils={ + "32": ("GLRI", MHz(109, 30)), + }, ), "Lincoln County": AirfieldData( - "", - 4815, 4408, - "", "", - "", "", - AtcData("", "", "", ""), - {}, - {}, - {}, - {}, + theater="NTTR", + elevation=4815, + runway_length=4408, ), "Mesquite": AirfieldData( - "67L", - 1858, 4937, - "", "", - "", "", - AtcData("", "", "", ""), - {}, - {}, - {}, - {}, + theater="NTTR", + icao="67L", + elevation=1858, + runway_length=4937, ), "Creech AFB": AirfieldData( - "KINS", - 3126, 6100, - "87X", "INS", - "", "", - AtcData("3.825", "118.300", "38.550", "360.600"), - {"8": "108.70 (ICRR)"}, - {}, - {}, - {}, + theater="NTTR", + icao="KINS", + elevation=3126, + runway_length=6100, + tacan=TacanChannel(87, TacanBand.X), + tacan_callsign="INS", + atc=AtcData(MHz(3, 825), MHz(118, 300), MHz(38, 550), MHz(360, 600)), + ils={ + "8": ("ICRR", MHz(108, 70)), + }, ), "Echo Bay": AirfieldData( - "OL9", - 3126, 6100, - "87X", "INS", - "", "", - AtcData("3.825", "118.300", "38.550", "360.600"), - {}, - {}, - {}, - {}, + theater="NTTR", + icao="OL9", + elevation=3126, + runway_length=6100, + tacan=TacanChannel(87, TacanBand.X), + tacan_callsign="INS", + atc=AtcData(MHz(3, 825), MHz(118, 300), MHz(38, 550), MHz(360, 600)), ), "Nellis AFB": AirfieldData( - "KLSV", - 1841, 9454, - "12X", "LSV", - "", "", - AtcData("3.900", "132.550", "38.700", "327.000"), - {"21": "109.10 (IDIQ)"}, - {}, - {}, - {}, + theater="NTTR", + icao="KLSV", + elevation=1841, + runway_length=9454, + tacan=TacanChannel(12, TacanBand.X), + tacan_callsign="LSV", + atc=AtcData(MHz(3, 900), MHz(132, 550), MHz(38, 700), MHz(327, 0)), + ils={ + "21": ("IDIQ", MHz(109, 10)), + }, ), "North Las Vegas": AirfieldData( - "KVGT", - 2228, 4734, - "", "", - "", "", - AtcData("3.775", "125.700", "38.450", "360.750"), - {}, - {}, - {}, - {}, + theater="NTTR", + icao="KVGT", + elevation=2228, + runway_length=4734, + atc=AtcData(MHz(3, 775), MHz(125, 700), MHz(38, 450), MHz(360, 750)), ), "McCarran International Airport": AirfieldData( - "KLAS", - 2169, 10377, - "116X", "LAS", - "116.90 (LAS)", "", - AtcData("3.875", "119.900", "38.650", "257.800"), - {"25": "110.30 (I-LAS)"}, - {}, - {}, - {}, + theater="NTTR", + icao="KLAS", + elevation=2169, + runway_length=10377, + tacan=TacanChannel(116, TacanBand.X), + tacan_callsign="LAS", + atc=AtcData(MHz(3, 875), MHz(119, 900), MHz(38, 650), MHz(257, 800)), + ils={ + "25": ("I-LAS", MHz(110, 30)), + }, ), "Henderson Executive Airport": AirfieldData( - "KHND", - 2491, 5999, - "", "", - "", "", - AtcData("3.925", "125.100", "38.750", "250.100"), - {}, - {}, - {}, - {}, + theater="NTTR", + icao="KHND", + elevation=2491, + runway_length=5999, + atc=AtcData(MHz(3, 925), MHz(125, 100), MHz(38, 750), MHz(250, 100)), ), "Boulder City Airport": AirfieldData( - "KBVU", - 2121, 4612, - "", "", - "", "", - AtcData("", "", "", ""), - {}, - {}, - {}, - {}, + theater="NTTR", + icao="KBVU", + elevation=2121, + runway_length=4612, ), "Jean Airport": AirfieldData( - "", - 2824, 4053, - "", "", - "", "", - AtcData("", "", "", ""), - {}, - {}, - {}, - {}, + theater="NTTR", + elevation=2824, + runway_length=4053, ), "Laughlin Airport": AirfieldData( - "KIFP", - 656, 7139, - "", "", - "", "", - AtcData("3.750", "123.900", "38.400", "250.000"), - {}, - {}, - {}, - {}, + theater="NTTR", + icao="KIFP", + elevation=656, + runway_length=7139, + atc=AtcData(MHz(3, 750), MHz(123, 900), MHz(38, 400), MHz(250, 0)), ), # TODO : NORMANDY MAP # Channel Map "Detling": AirfieldData( - "", - 623, 2557, - "", "", - "", "", - AtcData("3.950", "118.400", "38.800", "250.400"), - {}, - {}, - {}, - {}, + theater="Channel", + elevation=623, + runway_length=2557, + atc=AtcData(MHz(3, 950), MHz(118, 400), MHz(38, 800), MHz(250, 400)), ), "High Halden": AirfieldData( - "", - 104, 3296, - "", "", - "", "", - AtcData("3.750", "118.800", "38.400", "250.000"), - {}, - {}, - {}, - {}, + theater="Channel", + elevation=104, + runway_length=3296, + atc=AtcData(MHz(3, 750), MHz(118, 800), MHz(38, 400), MHz(250, 0)), ), "Lympne": AirfieldData( - "", - 351, 2548, - "", "", - "", "", - AtcData("3.925", "118.350", "38.750", "250.350"), - {}, - {}, - {}, - {}, + theater="Channel", + elevation=351, + runway_length=2548, + atc=AtcData(MHz(3, 925), MHz(118, 350), MHz(38, 750), MHz(250, 350)), ), "Hawkinge": AirfieldData( - "", - 524, 3013, - "", "", - "", "", - AtcData("3.900", "118.300", "38.700", "250.300"), - {}, - {}, - {}, - {}, + theater="Channel", + elevation=524, + runway_length=3013, + atc=AtcData(MHz(3, 900), MHz(118, 300), MHz(38, 700), MHz(250, 300)), ), "Manston": AirfieldData( - "", - 160, 8626, - "", "", - "", "", - AtcData("3.875", "118.250", "38.650", "250.250"), - {}, - {}, - {}, - {}, + theater="Channel", + elevation=160, + runway_length=8626, + atc=AtcData(MHz(3, 875), MHz(118, 250), MHz(38, 650), MHz(250, 250)), ), "Dunkirk Mardyck": AirfieldData( - "", - 16, 1737, - "", "", - "", "", - AtcData("3.850", "118.200", "38.600", "250.200"), - {}, - {}, - {}, - {}, + theater="Channel", + elevation=16, + runway_length=1737, + atc=AtcData(MHz(3, 850), MHz(118, 200), MHz(38, 600), MHz(250, 200)), ), "Saint Omer Longuenesse": AirfieldData( - "", - 219, 1929, - "", "", - "", "", - AtcData("3.825", "118.150", "38.550" "250.150"), - {}, - {}, - {}, - {}, + theater="Channel", + elevation=219, + runway_length=1929, + atc=AtcData(MHz(3, 825), MHz(118, 150), MHz(38, 550), MHz(250, 150)), ), "Merville Calonne": AirfieldData( - "", - 52, 7580, - "", "", - "", "", - AtcData("3.800", "118.100", "38.500", "250.100"), - {}, - {}, - {}, - {}, + theater="Channel", + elevation=52, + runway_length=7580, + atc=AtcData(MHz(3, 800), MHz(118, 100), MHz(38, 500), MHz(250, 100)), ), "Abbeville Drucat": AirfieldData( - "", - 183, 4726, - "", "", - "", "", - AtcData("3.775", "118.050", "38.450", "250.050"), - {}, - {}, - {}, - {}, - ) - + theater="Channel", + elevation=183, + runway_length=4726, + atc=AtcData(MHz(3, 775), MHz(118, 50), MHz(38, 450), MHz(250, 50)), + ), } + +# TODO: Add list of all beacons on the map so we can reserve those frequencies. +# This list could be generated from the beasons.lua file in the terrain mod +# directory. As-is, we're allocating channels that might include VOR beacons, +# and those will broadcast their callsign consistently (probably with a lot of +# static, depending on how far away the beacon is. The F-16's VHF radio starts +# at 116 MHz, which happens to be the Damascus VOR beacon, so this is more or +# less guaranteed to happen. diff --git a/gen/airsupportgen.py b/gen/airsupportgen.py index 2e0ef249..e65b5a7a 100644 --- a/gen/airsupportgen.py +++ b/gen/airsupportgen.py @@ -1,12 +1,15 @@ -from game import db +from typing import List +from dataclasses import dataclass, field + from .conflictgen import * from .naming import * +from .radios import RadioFrequency, RadioRegistry +from .tacan import TacanBand, TacanChannel, TacanRegistry from dcs.mission import * from dcs.unitgroup import * from dcs.unittype import * from dcs.task import * -from dcs.terrain.terrain import NoParkingSlotError TANKER_DISTANCE = 15000 TANKER_ALT = 4572 @@ -15,15 +18,61 @@ TANKER_HEADING_OFFSET = 45 AWACS_DISTANCE = 150000 AWACS_ALT = 13000 +AWACS_CALLSIGNS = [ + "Overlord", + "Magic", + "Wizard", + "Focus", + "Darkstar", +] + + +@dataclass +class TankerCallsign: + full: str + short: str + + +TANKER_CALLSIGNS = [ + TankerCallsign("Texaco", "TEX"), + TankerCallsign("Arco", "ARC"), + TankerCallsign("Shell", "SHL"), +] + + +@dataclass +class AwacsInfo: + """AWACS information for the kneeboard.""" + callsign: str + freq: RadioFrequency + + +@dataclass +class TankerInfo: + """Tanker information for the kneeboard.""" + callsign: str + variant: str + freq: RadioFrequency + tacan: TacanChannel + + +@dataclass +class AirSupport: + awacs: List[AwacsInfo] = field(default_factory=list) + tankers: List[TankerInfo] = field(default_factory=list) + class AirSupportConflictGenerator: - generated_tankers = None # type: typing.List[str] - def __init__(self, mission: Mission, conflict: Conflict, game): + def __init__(self, mission: Mission, conflict: Conflict, game, + radio_registry: RadioRegistry, + tacan_registry: TacanRegistry) -> None: self.mission = mission self.conflict = conflict self.game = game - self.generated_tankers = [] + self.air_support = AirSupport() + self.radio_registry = radio_registry + self.tacan_registry = tacan_registry @classmethod def support_tasks(cls) -> typing.Collection[typing.Type[MainTask]]: @@ -32,9 +81,11 @@ class AirSupportConflictGenerator: def generate(self, is_awacs_enabled): player_cp = self.conflict.from_cp if self.conflict.from_cp.captured else self.conflict.to_cp - CALLSIGNS = ["TKR", "TEX", "FUL", "FUE", ""] for i, tanker_unit_type in enumerate(db.find_unittype(Refueling, self.conflict.attackers_side)): - self.generated_tankers.append(db.unit_type_name(tanker_unit_type)) + callsign = TANKER_CALLSIGNS[i] + variant = db.unit_type_name(tanker_unit_type) + freq = self.radio_registry.alloc_uhf() + tacan = self.tacan_registry.alloc_for_band(TacanBand.Y) tanker_heading = self.conflict.to_cp.position.heading_between_point(self.conflict.from_cp.position) + TANKER_HEADING_OFFSET * i tanker_position = player_cp.position.point_from_heading(tanker_heading, TANKER_DISTANCE) tanker_group = self.mission.refuel_flight( @@ -45,21 +96,26 @@ class AirSupportConflictGenerator: position=tanker_position, altitude=TANKER_ALT, race_distance=58000, - frequency=130 + i, + frequency=freq.mhz, start_type=StartType.Warm, speed=574, - tacanchannel="{}X".format(60 + i), + tacanchannel=str(tacan), ) if tanker_unit_type != IL_78M: tanker_group.points[0].tasks.pop() # Override PyDCS tacan channel - tanker_group.points[0].tasks.append(ActivateBeaconCommand(60 + i, "X", CALLSIGNS[i], True, tanker_group.units[0].id, True)) + tanker_group.points[0].tasks.append(ActivateBeaconCommand( + tacan.number, tacan.band.value, callsign.short, True, tanker_group.units[0].id, True)) tanker_group.points[0].tasks.append(SetInvisibleCommand(True)) tanker_group.points[0].tasks.append(SetImmortalCommand(True)) + self.air_support.tankers.append(TankerInfo(callsign.full, variant, freq, tacan)) + if is_awacs_enabled: try: + callsign = AWACS_CALLSIGNS[0] + freq = self.radio_registry.alloc_uhf() awacs_unit = db.find_unittype(AWACS, self.conflict.attackers_side)[0] awacs_flight = self.mission.awacs_flight( country=self.mission.country(self.game.player_country), @@ -68,11 +124,12 @@ class AirSupportConflictGenerator: altitude=AWACS_ALT, airport=None, position=self.conflict.position.random_point_within(AWACS_DISTANCE, AWACS_DISTANCE), - frequency=233, + frequency=freq.mhz, start_type=StartType.Warm, ) awacs_flight.points[0].tasks.append(SetInvisibleCommand(True)) awacs_flight.points[0].tasks.append(SetImmortalCommand(True)) + self.air_support.awacs.append(AwacsInfo(callsign, freq)) except: print("No AWACS for faction") diff --git a/gen/flights/flight.py b/gen/flights/flight.py index 57c7097f..912946e4 100644 --- a/gen/flights/flight.py +++ b/gen/flights/flight.py @@ -1,10 +1,10 @@ from enum import Enum -from typing import List +from typing import List, Optional -from dcs.mission import StartType -from dcs.unittype import UnitType +from pydcs.dcs.unittype import UnitType from game import db +from gen.radios import RadioFrequency class FlightType(Enum): @@ -96,6 +96,13 @@ class Flight: # How long before this flight should take off scheduled_in = 0 + # Populated during mission generation time by AircraftConflictGenerator. + # TODO: Decouple radio planning from the Flight. + # Make AircraftConflictGenerator generate a FlightData object that is + # returned to the Operation rather than relying on the Flight object, which + # represents a game UI flight rather than a fully planned flight. + intra_flight_channel: Optional[RadioFrequency] + def __init__(self, unit_type: UnitType, count: int, from_cp, flight_type: FlightType): self.unit_type = unit_type self.count = count @@ -105,6 +112,7 @@ class Flight: self.targets = [] self.loadout = {} self.start_type = "Runway" + self.intra_flight_channel = None def __repr__(self): return self.flight_type.name + " | " + str(self.count) + "x" + db.unit_type_name(self.unit_type) \ @@ -113,10 +121,10 @@ class Flight: # Test if __name__ == '__main__': - from dcs.planes import A_10C + from pydcs.dcs.planes import A_10C from theater import ControlPoint, Point, List from_cp = ControlPoint(0, "AA", Point(0, 0), None, [], 0, 0) - f = Flight(A_10C, 4, from_cp, FlightType.CAS) + f = Flight(A_10C(), 4, from_cp, FlightType.CAS) f.scheduled_in = 50 print(f) diff --git a/gen/kneeboard.py b/gen/kneeboard.py index 1d679e4a..1241078e 100644 --- a/gen/kneeboard.py +++ b/gen/kneeboard.py @@ -33,9 +33,11 @@ from tabulate import tabulate from pydcs.dcs.mission import Mission from pydcs.dcs.terrain.terrain import Airport from pydcs.dcs.unittype import FlyingType -from .airfields import AIRFIELD_DATA -from .flights.flight import Flight from . import units +from .airfields import AIRFIELD_DATA +from .airsupportgen import AwacsInfo, TankerInfo +from .flights.flight import Flight +from .radios import RadioFrequency class KneeboardPageWriter: @@ -116,23 +118,7 @@ class AirfieldInfo: class CommInfo: """Communications information for the kneeboard.""" name: str - freq: str - - -@dataclass -class AwacsInfo: - """AWACS information for the kneeboard.""" - callsign: str - freq: str - - -@dataclass -class TankerInfo: - """Tanker information for the kneeboard.""" - callsign: str - variant: str - freq: str - tacan: str + freq: RadioFrequency @dataclass @@ -153,6 +139,10 @@ class BriefingPage(KneeboardPage): self.awacs = awacs self.tankers = tankers self.jtacs = jtacs + if self.flight.intra_flight_channel is not None: + self.comms.append( + CommInfo("Flight", self.flight.intra_flight_channel) + ) self.departure = flight.from_cp.airport self.arrival = flight.from_cp.airport self.divert: Optional[Airport] = None @@ -245,7 +235,7 @@ class KneeboardGenerator: self.tankers: List[TankerInfo] = [] self.jtacs: List[JtacInfo] = [] - def add_comm(self, name: str, freq: str) -> None: + def add_comm(self, name: str, freq: RadioFrequency) -> None: """Adds communications info to the kneeboard. Args: @@ -254,26 +244,21 @@ class KneeboardGenerator: """ self.comms.append(CommInfo(name, freq)) - def add_awacs(self, callsign: str, freq: str) -> None: + def add_awacs(self, awacs: AwacsInfo) -> None: """Adds an AWACS/GCI to the kneeboard. Args: - callsign: Callsign of the AWACS/GCI. - freq: Radio frequency used by the AWACS/GCI. + awacs: AWACS information. """ - self.awacs.append(AwacsInfo(callsign, freq)) + self.awacs.append(awacs) - def add_tanker(self, callsign: str, variant: str, freq: str, - tacan: str) -> None: + def add_tanker(self, tanker: TankerInfo) -> None: """Adds a tanker to the kneeboard. Args: - callsign: Callsign of the tanker. - variant: Aircraft type. - freq: Radio frequency used by the tanker. - tacan: TACAN channel of the tanker. + tanker: Tanker information. """ - self.tankers.append(TankerInfo(callsign, variant, freq, tacan)) + self.tankers.append(tanker) def add_jtac(self, callsign: str, region: str, code: str) -> None: """Adds a JTAC to the kneeboard. diff --git a/gen/radios.py b/gen/radios.py new file mode 100644 index 00000000..2cfeac8a --- /dev/null +++ b/gen/radios.py @@ -0,0 +1,200 @@ +"""Radio frequency types and allocators.""" +import itertools +from dataclasses import dataclass +from typing import Dict, Iterator, List, Set + + +@dataclass(frozen=True) +class RadioFrequency: + """A radio frequency. + + Not currently concerned with tracking modulation, just the frequency. + """ + + #: The frequency in kilohertz. + hertz: int + + def __str__(self): + if self.hertz >= 1000000: + return self.format("MHz", 1000000) + return self.format("kHz", 1000) + + def format(self, units: str, divisor: int) -> str: + converted = self.hertz / divisor + if converted.is_integer(): + return f"{int(converted)} {units}" + return f"{converted:0.3f} {units}" + + @property + def mhz(self) -> float: + """Returns the frequency in megahertz. + + Returns: + The frequency in megahertz. + """ + return self.hertz / 1000000 + + +def MHz(num: int, khz: int = 0) -> RadioFrequency: + return RadioFrequency(num * 1000000 + khz * 1000) + + +def kHz(num: int) -> RadioFrequency: + return RadioFrequency(num * 1000) + + +@dataclass(frozen=True) +class Radio: + """A radio. + + Defines the minimum (inclusive) and maximum (exclusive) range of the radio. + """ + + #: The name of the radio. + name: str + + #: The minimum (inclusive) frequency tunable by this radio. + minimum: RadioFrequency + + #: The maximum (exclusive) frequency tunable by this radio. + maximum: RadioFrequency + + #: The spacing between adjacent frequencies. + step: RadioFrequency + + def __str__(self) -> str: + return self.name + + def range(self) -> Iterator[RadioFrequency]: + """Returns an iterator over the usable frequencies of this radio.""" + return (RadioFrequency(x) for x in range( + self.minimum.hertz, self.maximum.hertz, self.step.hertz + )) + + +class OutOfChannelsError(RuntimeError): + """Raised when all channels usable by this radio have been allocated.""" + + def __init__(self, radio: Radio) -> None: + super().__init__(f"No available channels for {radio}") + + +class ChannelInUseError(RuntimeError): + """Raised when attempting to reserve an in-use frequency.""" + + def __init__(self, frequency: RadioFrequency) -> None: + super().__init__(f"{frequency} is already in use") + + +# TODO: Figure out appropriate steps for each radio. These are just guesses. +#: List of all known radios used by aircraft in the game. +RADIOS: List[Radio] = [ + Radio("AN/ARC-164", MHz(225), MHz(400), step=MHz(1)), + Radio("AN/ARC-186(V) AM", MHz(116), MHz(152), step=MHz(1)), + Radio("AN/ARC-186(V) FM", MHz(30), MHz(76), step=MHz(1)), + # The AN/ARC-210 can also use [30, 88) and [108, 118), but the current + # implementation can't implement the gap and the radio can't transmit on the + # latter. There's still plenty of channels between 118 MHz and 400 MHz, so + # not worth worrying about. + Radio("AN/ARC-210", MHz(118), MHz(400), step=MHz(1)), + Radio("AN/ARC-222", MHz(116), MHz(174), step=MHz(1)), + Radio("SCR-522", MHz(100), MHz(156), step=MHz(1)), + Radio("A.R.I. 1063", MHz(100), MHz(156), step=MHz(1)), + Radio("BC-1206", kHz(200), kHz(400), step=kHz(10)), +] + + +def get_radio(name: str) -> Radio: + """Returns the radio with the given name. + + Args: + name: Name of the radio to return. + + Returns: + The radio matching name. + + Raises: + KeyError: No matching radio was found. + """ + for radio in RADIOS: + if radio.name == name: + return radio + raise KeyError + + +class RadioRegistry: + """Manages allocation of radio channels. + + There's some room for improvement here. We could prefer to allocate + frequencies that are available to the fewest number of radios first, so + radios with wide bands like the AN/ARC-210 don't exhaust all the channels + available to narrower radios like the AN/ARC-186(V). In practice there are + probably plenty of channels, so we can deal with that later if we need to. + + We could also allocate using a larger increment, returning to smaller + increments each time the range is exhausted. This would help with the + previous problem, as the AN/ARC-186(V) would still have plenty of 25 kHz + increment channels left after the AN/ARC-210 moved on to the higher + frequencies. This would also look a little nicer than having every flight + allocated in the 30 MHz range. + """ + + # Not a real radio, but useful for allocating a channel usable for + # inter-flight communications. + BLUFOR_UHF = Radio("BLUFOR UHF", MHz(225), MHz(400), step=MHz(1)) + + def __init__(self) -> None: + self.allocated_channels: Set[RadioFrequency] = set() + self.radio_allocators: Dict[Radio, Iterator[RadioFrequency]] = {} + + radios = itertools.chain(RADIOS, [self.BLUFOR_UHF]) + for radio in radios: + self.radio_allocators[radio] = radio.range() + + def alloc_for_radio(self, radio: Radio) -> RadioFrequency: + """Allocates a radio channel tunable by the given radio. + + Args: + radio: The radio to allocate a channel for. + + Returns: + A radio channel compatible with the given radio. + + Raises: + OutOfChannelsError: All channels compatible with the given radio are + already allocated. + """ + allocator = self.radio_allocators[radio] + try: + while (channel := next(allocator)) in self.allocated_channels: + pass + return channel + except StopIteration: + raise OutOfChannelsError(radio) + + def alloc_uhf(self) -> RadioFrequency: + """Allocates a UHF radio channel suitable for inter-flight comms. + + Returns: + A UHF radio channel suitable for inter-flight comms. + + Raises: + OutOfChannelsError: All channels compatible with the given radio are + already allocated. + """ + return self.alloc_for_radio(self.BLUFOR_UHF) + + def reserve(self, frequency: RadioFrequency) -> None: + """Reserves the given channel. + + Reserving a channel ensures that it will not be allocated in the future. + + Args: + frequency: The channel to reserve. + + Raises: + ChannelInUseError: The given frequency is already in use. + """ + if frequency in self.allocated_channels: + raise ChannelInUseError(frequency) + self.allocated_channels.add(frequency) diff --git a/gen/tacan.py b/gen/tacan.py new file mode 100644 index 00000000..5e43202a --- /dev/null +++ b/gen/tacan.py @@ -0,0 +1,83 @@ +"""TACAN channel handling.""" +from dataclasses import dataclass +from enum import Enum +from typing import Dict, Iterator, Set + + +class TacanBand(Enum): + X = "X" + Y = "Y" + + def range(self) -> Iterator["TacanChannel"]: + """Returns an iterator over the channels in this band.""" + return (TacanChannel(x, self) for x in range(1, 100)) + + +@dataclass(frozen=True) +class TacanChannel: + number: int + band: TacanBand + + def __str__(self) -> str: + return f"{self.number}{self.band.value}" + + +class OutOfTacanChannelsError(RuntimeError): + """Raised when all channels in this band have been allocated.""" + + def __init__(self, band: TacanBand) -> None: + super().__init__(f"No available channels in TACAN {band.value} band") + + +class TacanChannelInUseError(RuntimeError): + """Raised when attempting to reserve an in-use channel.""" + + def __init__(self, channel: TacanChannel) -> None: + super().__init__(f"{channel} is already in use") + + +class TacanRegistry: + """Manages allocation of TACAN channels.""" + + def __init__(self) -> None: + self.allocated_channels: Set[TacanChannel] = set() + self.band_allocators: Dict[TacanBand, Iterator[TacanChannel]] = {} + + for band in TacanBand: + self.band_allocators[band] = band.range() + + def alloc_for_band(self, band: TacanBand) -> TacanChannel: + """Allocates a TACAN channel in the given band. + + Args: + band: The TACAN band to allocate a channel for. + + Returns: + A TACAN channel in the given band. + + Raises: + OutOfChannelsError: All channels compatible with the given radio are + already allocated. + """ + allocator = self.band_allocators[band] + try: + while (channel := next(allocator)) in self.allocated_channels: + pass + return channel + except StopIteration: + raise OutOfTacanChannelsError(band) + + def reserve(self, channel: TacanChannel) -> None: + """Reserves the given channel. + + Reserving a channel ensures that it will not be allocated in the future. + + Args: + channel: The channel to reserve. + + Raises: + ChannelInUseError: The given frequency is already in use. + """ + if channel in self.allocated_channels: + raise TacanChannelInUseError(channel) + self.allocated_channels.add(channel) From d0518593712f229a0448bd9ed8240e87e3bc80a2 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Mon, 31 Aug 2020 14:21:00 -0700 Subject: [PATCH 18/61] Add beacon list importer. --- gen/airfields.py | 8 -- gen/beacons.py | 74 ++++++++++++ resources/tools/import_beacons.py | 183 ++++++++++++++++++++++++++++++ 3 files changed, 257 insertions(+), 8 deletions(-) create mode 100644 gen/beacons.py create mode 100644 resources/tools/import_beacons.py diff --git a/gen/airfields.py b/gen/airfields.py index 38d4ccdb..8b98668d 100644 --- a/gen/airfields.py +++ b/gen/airfields.py @@ -637,11 +637,3 @@ AIRFIELD_DATA = { atc=AtcData(MHz(3, 775), MHz(118, 50), MHz(38, 450), MHz(250, 50)), ), } - -# TODO: Add list of all beacons on the map so we can reserve those frequencies. -# This list could be generated from the beasons.lua file in the terrain mod -# directory. As-is, we're allocating channels that might include VOR beacons, -# and those will broadcast their callsign consistently (probably with a lot of -# static, depending on how far away the beacon is. The F-16's VHF radio starts -# at 116 MHz, which happens to be the Damascus VOR beacon, so this is more or -# less guaranteed to happen. diff --git a/gen/beacons.py b/gen/beacons.py new file mode 100644 index 00000000..b54eacb1 --- /dev/null +++ b/gen/beacons.py @@ -0,0 +1,74 @@ +from dataclasses import dataclass +from enum import auto, IntEnum +import json +from pathlib import Path +from typing import Iterable, Optional + +from gen.radios import RadioFrequency +from gen.tacan import TacanBand, TacanChannel + + +BEACONS_RESOURCE_PATH = Path("resources/dcs/beacons") + + +class BeaconType(IntEnum): + BEACON_TYPE_NULL = auto() + BEACON_TYPE_VOR = auto() + BEACON_TYPE_DME = auto() + BEACON_TYPE_VOR_DME = auto() + BEACON_TYPE_TACAN = auto() + BEACON_TYPE_VORTAC = auto() + BEACON_TYPE_RSBN = auto() + BEACON_TYPE_BROADCAST_STATION = auto() + + BEACON_TYPE_HOMER = auto() + BEACON_TYPE_AIRPORT_HOMER = auto() + BEACON_TYPE_AIRPORT_HOMER_WITH_MARKER = auto() + BEACON_TYPE_ILS_FAR_HOMER = auto() + BEACON_TYPE_ILS_NEAR_HOMER = auto() + + BEACON_TYPE_ILS_LOCALIZER = auto() + BEACON_TYPE_ILS_GLIDESLOPE = auto() + + BEACON_TYPE_PRMG_LOCALIZER = auto() + BEACON_TYPE_PRMG_GLIDESLOPE = auto() + + BEACON_TYPE_ICLS_LOCALIZER = auto() + BEACON_TYPE_ICLS_GLIDESLOPE = auto() + + BEACON_TYPE_NAUTICAL_HOMER = auto() + + +@dataclass(frozen=True) +class Beacon: + name: str + callsign: str + beacon_type: BeaconType + hertz: int + channel: Optional[int] + + @property + def frequency(self) -> RadioFrequency: + return RadioFrequency(self.hertz) + + @property + def is_tacan(self) -> bool: + return self.beacon_type in ( + BeaconType.BEACON_TYPE_VORTAC, + BeaconType.BEACON_TYPE_TACAN, + ) + + @property + def tacan_channel(self) -> TacanChannel: + assert self.is_tacan + assert self.channel is not None + return TacanChannel(self.channel, TacanBand.X) + + +def load_beacons_for_terrain(name: str) -> Iterable[Beacon]: + beacons_file = BEACONS_RESOURCE_PATH / f"{name.lower()}.json" + if not beacons_file.exists(): + raise RuntimeError(f"Beacon file {beacons_file.resolve()} is missing") + + for beacon in json.loads(beacons_file.read_text()): + yield Beacon(**beacon) diff --git a/resources/tools/import_beacons.py b/resources/tools/import_beacons.py new file mode 100644 index 00000000..9f3dd38e --- /dev/null +++ b/resources/tools/import_beacons.py @@ -0,0 +1,183 @@ +"""Generates resources/dcs/beacons.json from the DCS installation. + +DCS has a beacons.lua file for each terrain mod that includes information about +the radio beacons present on the map: + +beacons = { + { + display_name = _('INCIRLIC'); + beaconId = 'airfield16_0'; + type = BEACON_TYPE_VORTAC; + callsign = 'DAN'; + frequency = 108400000.000000; + channel = 21; + position = { 222639.437500, 73.699811, -33216.257813 }; + direction = 0.000000; + positionGeo = { latitude = 37.015611, longitude = 35.448194 }; + sceneObjects = {'t:124814096'}; + }; + ... +} + +""" +import argparse +from contextlib import contextmanager +import dataclasses +import gettext +import os +from pathlib import Path +import textwrap +from typing import Dict, Iterable, Union + +import lupa + +import game # Needed to resolve cyclic import, for some reason. +from gen.beacons import Beacon, BeaconType, BEACONS_RESOURCE_PATH + +THIS_DIR = Path(__file__).parent.resolve() +SRC_DIR = THIS_DIR.parents[1] +EXPORT_DIR = SRC_DIR / BEACONS_RESOURCE_PATH + + +@contextmanager +def cd(path: Path): + cwd = os.getcwd() + os.chdir(path) + try: + yield + finally: + os.chdir(cwd) + + +def convert_lua_frequency(raw: Union[float, int]) -> int: + if isinstance(raw, float): + if not raw.is_integer(): + # The values are in hertz, and everything should be a whole number. + raise ValueError(f"Unexpected non-integer frequency: {raw}") + return int(raw) + else: + return raw + + +def beacons_from_terrain(dcs_path: Path, path: Path) -> Iterable[Beacon]: + # TODO: Fix case-sensitive issues. + # The beacons.lua file differs by case in some terrains. Will need to be + # fixed if the tool is to be run on Linux, but presumably the server + # wouldn't be able to find these anyway. + beacons_lua = path / "beacons.lua" + with cd(dcs_path): + lua = lupa.LuaRuntime() + + lua.execute(textwrap.dedent("""\ + function module(name) + end + + """)) + + bind_gettext = lua.eval(textwrap.dedent("""\ + function(py_gettext) + package.preload["i_18n"] = function() + return { + translate = py_gettext + } + end + end + + """)) + translator = gettext.translation( + "messages", path / "l10n", languages=["en"]) + + def translate(message_name: str) -> str: + if not message_name: + return message_name + return translator.gettext(message_name) + bind_gettext(translate) + + src = beacons_lua.read_text() + lua.execute(src) + + beacon_types_map: Dict[int, BeaconType] = {} + for beacon_type in BeaconType: + beacon_value = lua.eval(beacon_type.name) + beacon_types_map[beacon_value] = beacon_type + + beacons = lua.eval("beacons") + for beacon in beacons.values(): + beacon_type_lua = beacon["type"] + if beacon_type_lua not in beacon_types_map: + raise KeyError( + f"Unknown beacon type {beacon_type_lua}. Check that all " + f"beacon types in {beacon_types_path} are present in " + f"{BeaconType.__class__.__name__}" + ) + beacon_type = beacon_types_map[beacon_type_lua] + + yield Beacon( + beacon["display_name"], + beacon["callsign"], + beacon_type, + convert_lua_frequency(beacon["frequency"]), + getattr(beacon, "channel", None) + ) + + +class Importer: + """Imports beacon definitions from each available terrain mod. + + Only beacons for maps owned by the user can be imported. Other maps that + have been previously imported will not be disturbed. + """ + + def __init__(self, dcs_path: Path, export_dir: Path) -> None: + self.dcs_path = dcs_path + self.export_dir = export_dir + + def run(self) -> None: + """Exports the beacons for each available terrain mod.""" + terrains_path = self.dcs_path / "Mods" / "terrains" + self.export_dir.mkdir(parents=True, exist_ok=True) + for terrain in terrains_path.iterdir(): + beacons = beacons_from_terrain(self.dcs_path, terrain) + self.export_beacons(terrain.name, beacons) + + def export_beacons(self, terrain: str, beacons: Iterable[Beacon]) -> None: + terrain_py_path = self.export_dir / f"{terrain.lower()}.json" + import json + terrain_py_path.write_text(json.dumps([ + dataclasses.asdict(b) for b in beacons + ], indent=True)) + + + +def parse_args() -> argparse.Namespace: + """Parses and returns command line arguments.""" + parser = argparse.ArgumentParser() + + def resolved_path(val: str) -> Path: + """Returns the given string as a fully resolved Path.""" + return Path(val).resolve() + + parser.add_argument( + "--export-to", + type=resolved_path, + default=EXPORT_DIR, + help="Output directory for generated JSON files.") + + parser.add_argument( + "dcs_path", + metavar="DCS_PATH", + type=resolved_path, + help="Path to DCS installation." + ) + + return parser.parse_args() + + +def main() -> None: + """Program entry point.""" + args = parse_args() + Importer(args.dcs_path, args.export_to).run() + + +if __name__ == "__main__": + main() From 95b9a3e1aafe1813ff57672f2f97f9445a04ce47 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Mon, 31 Aug 2020 15:19:55 -0700 Subject: [PATCH 19/61] Check in beacon lists for most maps. --- resources/dcs/beacons/caucasus.json | 1157 ++++++++++++++++++++++++ resources/dcs/beacons/nevada.json | 317 +++++++ resources/dcs/beacons/normandy.json | 1 + resources/dcs/beacons/persiangulf.json | 709 +++++++++++++++ resources/dcs/beacons/syria.json | 408 +++++++++ 5 files changed, 2592 insertions(+) create mode 100644 resources/dcs/beacons/caucasus.json create mode 100644 resources/dcs/beacons/nevada.json create mode 100644 resources/dcs/beacons/normandy.json create mode 100644 resources/dcs/beacons/persiangulf.json create mode 100644 resources/dcs/beacons/syria.json diff --git a/resources/dcs/beacons/caucasus.json b/resources/dcs/beacons/caucasus.json new file mode 100644 index 00000000..d84fefc0 --- /dev/null +++ b/resources/dcs/beacons/caucasus.json @@ -0,0 +1,1157 @@ +[ + { + "name": "", + "callsign": "AP", + "beacon_type": 12, + "hertz": 443000, + "channel": null + }, + { + "name": "", + "callsign": "P", + "beacon_type": 13, + "hertz": 215000, + "channel": null + }, + { + "name": "", + "callsign": "AN", + "beacon_type": 12, + "hertz": 443000, + "channel": null + }, + { + "name": "", + "callsign": "N", + "beacon_type": 13, + "hertz": 215000, + "channel": null + }, + { + "name": "", + "callsign": "ILU", + "beacon_type": 14, + "hertz": 110300000, + "channel": null + }, + { + "name": "", + "callsign": "", + "beacon_type": 15, + "hertz": 110300000, + "channel": null + }, + { + "name": "", + "callsign": "BTM", + "beacon_type": 5, + "hertz": 977000000, + "channel": 16 + }, + { + "name": "", + "callsign": "LU", + "beacon_type": 10, + "hertz": 430000, + "channel": null + }, + { + "name": "", + "callsign": "CX", + "beacon_type": 12, + "hertz": 1050000, + "channel": null + }, + { + "name": "", + "callsign": "C", + "beacon_type": 13, + "hertz": 250000, + "channel": null + }, + { + "name": "", + "callsign": "ICH", + "beacon_type": 14, + "hertz": 110500000, + "channel": null + }, + { + "name": "", + "callsign": "", + "beacon_type": 15, + "hertz": 110500000, + "channel": null + }, + { + "name": "", + "callsign": "GN", + "beacon_type": 10, + "hertz": 1000000, + "channel": null + }, + { + "name": "", + "callsign": "GN", + "beacon_type": 2, + "hertz": 114300000, + "channel": 90 + }, + { + "name": "", + "callsign": "XC", + "beacon_type": 11, + "hertz": 395000, + "channel": null + }, + { + "name": "", + "callsign": "KT", + "beacon_type": 12, + "hertz": 870000, + "channel": null + }, + { + "name": "", + "callsign": "T", + "beacon_type": 13, + "hertz": 490000, + "channel": null + }, + { + "name": "", + "callsign": "IKB", + "beacon_type": 14, + "hertz": 111500000, + "channel": null + }, + { + "name": "", + "callsign": "", + "beacon_type": 15, + "hertz": 111500000, + "channel": null + }, + { + "name": "", + "callsign": "KBL", + "beacon_type": 5, + "hertz": 1154000000, + "channel": 67 + }, + { + "name": "", + "callsign": "OC", + "beacon_type": 12, + "hertz": 625000, + "channel": null + }, + { + "name": "", + "callsign": "O", + "beacon_type": 13, + "hertz": 303000, + "channel": null + }, + { + "name": "", + "callsign": "MB", + "beacon_type": 12, + "hertz": 625000, + "channel": null + }, + { + "name": "", + "callsign": "M", + "beacon_type": 13, + "hertz": 303000, + "channel": null + }, + { + "name": "", + "callsign": "MB", + "beacon_type": 16, + "hertz": 838000000, + "channel": 38 + }, + { + "name": "", + "callsign": "MB", + "beacon_type": 17, + "hertz": 838000000, + "channel": 38 + }, + { + "name": "", + "callsign": "MB", + "beacon_type": 7, + "hertz": 840000000, + "channel": 40 + }, + { + "name": "", + "callsign": "KR", + "beacon_type": 12, + "hertz": 493000, + "channel": null + }, + { + "name": "", + "callsign": "K", + "beacon_type": 13, + "hertz": 240000, + "channel": null + }, + { + "name": "", + "callsign": "LD", + "beacon_type": 12, + "hertz": 493000, + "channel": null + }, + { + "name": "", + "callsign": "L", + "beacon_type": 13, + "hertz": 240000, + "channel": null + }, + { + "name": "", + "callsign": "KN", + "beacon_type": 2, + "hertz": 115800000, + "channel": 105 + }, + { + "name": "", + "callsign": "KW", + "beacon_type": 12, + "hertz": 408000, + "channel": null + }, + { + "name": "", + "callsign": "K", + "beacon_type": 13, + "hertz": 803000, + "channel": null + }, + { + "name": "", + "callsign": "OX", + "beacon_type": 12, + "hertz": 408000, + "channel": null + }, + { + "name": "", + "callsign": "O", + "beacon_type": 13, + "hertz": 803000, + "channel": null + }, + { + "name": "", + "callsign": "", + "beacon_type": 17, + "hertz": 826000000, + "channel": null + }, + { + "name": "", + "callsign": "", + "beacon_type": 17, + "hertz": 826000000, + "channel": null + }, + { + "name": "", + "callsign": "KW", + "beacon_type": 16, + "hertz": 826000000, + "channel": 26 + }, + { + "name": "", + "callsign": "OX", + "beacon_type": 16, + "hertz": 826000000, + "channel": 26 + }, + { + "name": "", + "callsign": "KW", + "beacon_type": 7, + "hertz": 828000000, + "channel": 28 + }, + { + "name": "", + "callsign": "IKS", + "beacon_type": 14, + "hertz": 109750000, + "channel": null + }, + { + "name": "", + "callsign": "", + "beacon_type": 15, + "hertz": 109750000, + "channel": null + }, + { + "name": "", + "callsign": "TI", + "beacon_type": 11, + "hertz": 477000, + "channel": null + }, + { + "name": "", + "callsign": "KTS", + "beacon_type": 5, + "hertz": 1005000000, + "channel": 44 + }, + { + "name": "KUTAISI", + "callsign": "KT", + "beacon_type": 2, + "hertz": 113600000, + "channel": 83 + }, + { + "name": "", + "callsign": "BP", + "beacon_type": 12, + "hertz": 342000, + "channel": null + }, + { + "name": "", + "callsign": "B", + "beacon_type": 13, + "hertz": 923000, + "channel": null + }, + { + "name": "", + "callsign": "INA", + "beacon_type": 14, + "hertz": 110300000, + "channel": null + }, + { + "name": "", + "callsign": "", + "beacon_type": 15, + "hertz": 110300000, + "channel": null + }, + { + "name": "", + "callsign": "NA", + "beacon_type": 12, + "hertz": 211000, + "channel": null + }, + { + "name": "", + "callsign": "N", + "beacon_type": 13, + "hertz": 435000, + "channel": null + }, + { + "name": "", + "callsign": "INA", + "beacon_type": 14, + "hertz": 108900000, + "channel": null + }, + { + "name": "", + "callsign": "", + "beacon_type": 15, + "hertz": 108900000, + "channel": null + }, + { + "name": "", + "callsign": "TB", + "beacon_type": 2, + "hertz": 113700000, + "channel": 84 + }, + { + "name": "", + "callsign": "GTB", + "beacon_type": 5, + "hertz": 986000000, + "channel": 25 + }, + { + "name": "", + "callsign": "RK", + "beacon_type": 12, + "hertz": 289000, + "channel": null + }, + { + "name": "", + "callsign": "R", + "beacon_type": 13, + "hertz": 591000, + "channel": null + }, + { + "name": "", + "callsign": "DG", + "beacon_type": 12, + "hertz": 289000, + "channel": null + }, + { + "name": "", + "callsign": "D", + "beacon_type": 13, + "hertz": 591000, + "channel": null + }, + { + "name": "", + "callsign": "DG", + "beacon_type": 16, + "hertz": 836000000, + "channel": 36 + }, + { + "name": "", + "callsign": "DG", + "beacon_type": 17, + "hertz": 836000000, + "channel": 36 + }, + { + "name": "", + "callsign": "DG", + "beacon_type": 7, + "hertz": 834000000, + "channel": 34 + }, + { + "name": "", + "callsign": "NR", + "beacon_type": 12, + "hertz": 583000, + "channel": null + }, + { + "name": "", + "callsign": "N", + "beacon_type": 13, + "hertz": 283000, + "channel": null + }, + { + "name": "", + "callsign": "IMW", + "beacon_type": 14, + "hertz": 109300000, + "channel": null + }, + { + "name": "", + "callsign": "", + "beacon_type": 15, + "hertz": 109300000, + "channel": null + }, + { + "name": "", + "callsign": "MD", + "beacon_type": 12, + "hertz": 583000, + "channel": null + }, + { + "name": "", + "callsign": "D", + "beacon_type": 13, + "hertz": 283000, + "channel": null + }, + { + "name": "", + "callsign": "IMD", + "beacon_type": 14, + "hertz": 111700000, + "channel": null + }, + { + "name": "", + "callsign": "", + "beacon_type": 15, + "hertz": 111700000, + "channel": null + }, + { + "name": "", + "callsign": "MN", + "beacon_type": 2, + "hertz": 117100000, + "channel": 118 + }, + { + "name": "", + "callsign": "DO", + "beacon_type": 12, + "hertz": 525000, + "channel": null + }, + { + "name": "", + "callsign": "D", + "beacon_type": 13, + "hertz": 1065000, + "channel": null + }, + { + "name": "", + "callsign": "RM", + "beacon_type": 12, + "hertz": 525000, + "channel": null + }, + { + "name": "", + "callsign": "R", + "beacon_type": 13, + "hertz": 1065000, + "channel": null + }, + { + "name": "", + "callsign": "MZ", + "beacon_type": 16, + "hertz": 822000000, + "channel": 22 + }, + { + "name": "", + "callsign": "", + "beacon_type": 17, + "hertz": 822000000, + "channel": 22 + }, + { + "name": "", + "callsign": "MZ", + "beacon_type": 16, + "hertz": 822000000, + "channel": 22 + }, + { + "name": "", + "callsign": "", + "beacon_type": 17, + "hertz": 822000000, + "channel": 22 + }, + { + "name": "", + "callsign": "MZ", + "beacon_type": 7, + "hertz": 820000000, + "channel": 20 + }, + { + "name": "", + "callsign": "NL", + "beacon_type": 12, + "hertz": 718000, + "channel": null + }, + { + "name": "", + "callsign": "N", + "beacon_type": 13, + "hertz": 350000, + "channel": null + }, + { + "name": "", + "callsign": "INL", + "beacon_type": 14, + "hertz": 110500000, + "channel": null + }, + { + "name": "", + "callsign": "", + "beacon_type": 15, + "hertz": 110500000, + "channel": null + }, + { + "name": "", + "callsign": "BI", + "beacon_type": 12, + "hertz": 335000, + "channel": null + }, + { + "name": "", + "callsign": "B", + "beacon_type": 13, + "hertz": 688000, + "channel": null + }, + { + "name": "", + "callsign": "ITS", + "beacon_type": 14, + "hertz": 108900000, + "channel": null + }, + { + "name": "", + "callsign": "", + "beacon_type": 15, + "hertz": 108900000, + "channel": null + }, + { + "name": "", + "callsign": "TSK", + "beacon_type": 5, + "hertz": 992000000, + "channel": 31 + }, + { + "name": "", + "callsign": "CO", + "beacon_type": 11, + "hertz": 761000, + "channel": null + }, + { + "name": "", + "callsign": "ISO", + "beacon_type": 14, + "hertz": 111100000, + "channel": null + }, + { + "name": "", + "callsign": "", + "beacon_type": 15, + "hertz": 111100000, + "channel": null + }, + { + "name": "", + "callsign": "AV", + "beacon_type": 12, + "hertz": 489000, + "channel": null + }, + { + "name": "", + "callsign": "A", + "beacon_type": 13, + "hertz": 995000, + "channel": null + }, + { + "name": "", + "callsign": "IVZ", + "beacon_type": 14, + "hertz": 108750000, + "channel": null + }, + { + "name": "", + "callsign": "", + "beacon_type": 15, + "hertz": 108750000, + "channel": null + }, + { + "name": "", + "callsign": "IVZ", + "beacon_type": 14, + "hertz": 108750000, + "channel": null + }, + { + "name": "", + "callsign": "", + "beacon_type": 15, + "hertz": 108750000, + "channel": null + }, + { + "name": "", + "callsign": "VAS", + "beacon_type": 5, + "hertz": 983000000, + "channel": 22 + }, + { + "name": "", + "callsign": "NZ", + "beacon_type": 9, + "hertz": 330000, + "channel": null + }, + { + "name": "", + "callsign": "AR", + "beacon_type": 9, + "hertz": 440000, + "channel": null + }, + { + "name": "", + "callsign": "DM", + "beacon_type": 9, + "hertz": 690000, + "channel": null + }, + { + "name": "", + "callsign": "AG", + "beacon_type": 9, + "hertz": 381000, + "channel": null + }, + { + "name": "", + "callsign": "MA", + "beacon_type": 9, + "hertz": 682000, + "channel": null + }, + { + "name": "", + "callsign": "HS", + "beacon_type": 9, + "hertz": 1065000, + "channel": null + }, + { + "name": "", + "callsign": "SM", + "beacon_type": 9, + "hertz": 662000, + "channel": null + }, + { + "name": "", + "callsign": "KW", + "beacon_type": 9, + "hertz": 995000, + "channel": null + }, + { + "name": "", + "callsign": "TC", + "beacon_type": 9, + "hertz": 470000, + "channel": null + }, + { + "name": "", + "callsign": "IL", + "beacon_type": 9, + "hertz": 300500, + "channel": null + }, + { + "name": "", + "callsign": "SH", + "beacon_type": 9, + "hertz": 389000, + "channel": null + }, + { + "name": "", + "callsign": "OD", + "beacon_type": 9, + "hertz": 348000, + "channel": null + }, + { + "name": "", + "callsign": "BS", + "beacon_type": 9, + "hertz": 300500, + "channel": null + }, + { + "name": "", + "callsign": "KT", + "beacon_type": 9, + "hertz": 730000, + "channel": null + }, + { + "name": "", + "callsign": "ER", + "beacon_type": 9, + "hertz": 435000, + "channel": null + }, + { + "name": "", + "callsign": "KM", + "beacon_type": 9, + "hertz": 950000, + "channel": null + }, + { + "name": "", + "callsign": "SK", + "beacon_type": 9, + "hertz": 680000, + "channel": null + }, + { + "name": "", + "callsign": "DA", + "beacon_type": 9, + "hertz": 525000, + "channel": null + }, + { + "name": "", + "callsign": "DF", + "beacon_type": 9, + "hertz": 520000, + "channel": null + }, + { + "name": "", + "callsign": "RF", + "beacon_type": 9, + "hertz": 324000, + "channel": null + }, + { + "name": "", + "callsign": "TP", + "beacon_type": 9, + "hertz": 1182000, + "channel": null + }, + { + "name": "", + "callsign": "BJ", + "beacon_type": 9, + "hertz": 735000, + "channel": null + }, + { + "name": "", + "callsign": "NK", + "beacon_type": 9, + "hertz": 1030000, + "channel": null + }, + { + "name": "", + "callsign": "MN", + "beacon_type": 9, + "hertz": 705000, + "channel": null + }, + { + "name": "", + "callsign": "KC", + "beacon_type": 9, + "hertz": 1050000, + "channel": null + }, + { + "name": "", + "callsign": "TY", + "beacon_type": 9, + "hertz": 720000, + "channel": null + }, + { + "name": "", + "callsign": "AL", + "beacon_type": 9, + "hertz": 353000, + "channel": null + }, + { + "name": "", + "callsign": "CA", + "beacon_type": 9, + "hertz": 311000, + "channel": null + }, + { + "name": "", + "callsign": "XT", + "beacon_type": 9, + "hertz": 312000, + "channel": null + }, + { + "name": "", + "callsign": "KH", + "beacon_type": 9, + "hertz": 485000, + "channel": null + }, + { + "name": "", + "callsign": "WS", + "beacon_type": 9, + "hertz": 641000, + "channel": null + }, + { + "name": "", + "callsign": "WR", + "beacon_type": 9, + "hertz": 309500, + "channel": null + }, + { + "name": "", + "callsign": "VM", + "beacon_type": 9, + "hertz": 740000, + "channel": null + }, + { + "name": "", + "callsign": "WK", + "beacon_type": 9, + "hertz": 830000, + "channel": null + }, + { + "name": "", + "callsign": "TH", + "beacon_type": 9, + "hertz": 515000, + "channel": null + }, + { + "name": "", + "callsign": "KC", + "beacon_type": 9, + "hertz": 580000, + "channel": null + }, + { + "name": "", + "callsign": "SN", + "beacon_type": 9, + "hertz": 866000, + "channel": null + }, + { + "name": "", + "callsign": "DW", + "beacon_type": 9, + "hertz": 625000, + "channel": null + }, + { + "name": "", + "callsign": "SR", + "beacon_type": 9, + "hertz": 907000, + "channel": null + }, + { + "name": "", + "callsign": "TD", + "beacon_type": 9, + "hertz": 309500, + "channel": null + }, + { + "name": "", + "callsign": "SH", + "beacon_type": 9, + "hertz": 862000, + "channel": null + }, + { + "name": "", + "callsign": "SH", + "beacon_type": 9, + "hertz": 396000, + "channel": null + }, + { + "name": "", + "callsign": "DV", + "beacon_type": 9, + "hertz": 420000, + "channel": null + }, + { + "name": "", + "callsign": "GE", + "beacon_type": 9, + "hertz": 300500, + "channel": null + }, + { + "name": "", + "callsign": "GW", + "beacon_type": 9, + "hertz": 920000, + "channel": null + }, + { + "name": "", + "callsign": "QG", + "beacon_type": 9, + "hertz": 435000, + "channel": null + }, + { + "name": "", + "callsign": "AL", + "beacon_type": 9, + "hertz": 384000, + "channel": null + }, + { + "name": "", + "callsign": "DO", + "beacon_type": 9, + "hertz": 1175000, + "channel": null + }, + { + "name": "", + "callsign": "ND", + "beacon_type": 9, + "hertz": 507000, + "channel": null + }, + { + "name": "", + "callsign": "PR", + "beacon_type": 9, + "hertz": 1210000, + "channel": null + }, + { + "name": "", + "callsign": "PA", + "beacon_type": 9, + "hertz": 905000, + "channel": null + }, + { + "name": "", + "callsign": "OE", + "beacon_type": 9, + "hertz": 462000, + "channel": null + }, + { + "name": "", + "callsign": "LY", + "beacon_type": 9, + "hertz": 670000, + "channel": null + }, + { + "name": "", + "callsign": "MA", + "beacon_type": 9, + "hertz": 770000, + "channel": null + }, + { + "name": "", + "callsign": "AH", + "beacon_type": 9, + "hertz": 300500, + "channel": null + }, + { + "name": "", + "callsign": "NK", + "beacon_type": 9, + "hertz": 1030000, + "channel": null + }, + { + "name": "", + "callsign": "NE", + "beacon_type": 9, + "hertz": 740000, + "channel": null + }, + { + "name": "", + "callsign": "LE", + "beacon_type": 9, + "hertz": 395000, + "channel": null + }, + { + "name": "", + "callsign": "UH", + "beacon_type": 9, + "hertz": 528000, + "channel": null + }, + { + "name": "", + "callsign": "RE", + "beacon_type": 9, + "hertz": 320000, + "channel": null + }, + { + "name": "", + "callsign": "LA", + "beacon_type": 9, + "hertz": 307000, + "channel": null + }, + { + "name": "", + "callsign": "BD", + "beacon_type": 9, + "hertz": 342000, + "channel": null + }, + { + "name": "", + "callsign": "KP", + "beacon_type": 9, + "hertz": 214000, + "channel": null + }, + { + "name": "", + "callsign": "LA", + "beacon_type": 9, + "hertz": 750000, + "channel": null + }, + { + "name": "", + "callsign": "KS", + "beacon_type": 9, + "hertz": 1025000, + "channel": null + } +] \ No newline at end of file diff --git a/resources/dcs/beacons/nevada.json b/resources/dcs/beacons/nevada.json new file mode 100644 index 00000000..4c93b5c5 --- /dev/null +++ b/resources/dcs/beacons/nevada.json @@ -0,0 +1,317 @@ +[ + { + "name": "", + "callsign": "ICRR", + "beacon_type": 15, + "hertz": 108700000, + "channel": 24 + }, + { + "name": "", + "callsign": "ICRR", + "beacon_type": 14, + "hertz": 108700000, + "channel": 24 + }, + { + "name": "", + "callsign": "ICRS", + "beacon_type": 14, + "hertz": 108500000, + "channel": 22 + }, + { + "name": "", + "callsign": "ICRS", + "beacon_type": 15, + "hertz": 108500000, + "channel": 22 + }, + { + "name": "Indian Springs", + "callsign": "INS", + "beacon_type": 5, + "hertz": null, + "channel": 87 + }, + { + "name": "", + "callsign": "GLRI", + "beacon_type": 14, + "hertz": 109300000, + "channel": 30 + }, + { + "name": "", + "callsign": "GLRI", + "beacon_type": 15, + "hertz": 109300000, + "channel": 30 + }, + { + "name": "Groom Lake", + "callsign": "GRL", + "beacon_type": 5, + "hertz": null, + "channel": 18 + }, + { + "name": "", + "callsign": "I-RLE", + "beacon_type": 15, + "hertz": 111750000, + "channel": null + }, + { + "name": "", + "callsign": "I-LAS", + "beacon_type": 15, + "hertz": 110300000, + "channel": 40 + }, + { + "name": "", + "callsign": "I-RLE", + "beacon_type": 14, + "hertz": 111750000, + "channel": null + }, + { + "name": "", + "callsign": "I-LAS", + "beacon_type": 14, + "hertz": 110300000, + "channel": 40 + }, + { + "name": "Las Vegas", + "callsign": "LAS", + "beacon_type": 6, + "hertz": 116900000, + "channel": 116 + }, + { + "name": "", + "callsign": "IDIQ", + "beacon_type": 15, + "hertz": 109100000, + "channel": null + }, + { + "name": "Nellis", + "callsign": "LSV", + "beacon_type": 5, + "hertz": null, + "channel": 12 + }, + { + "name": "", + "callsign": "IDIQ", + "beacon_type": 14, + "hertz": 109100000, + "channel": null + }, + { + "name": "", + "callsign": "I-HWG", + "beacon_type": 14, + "hertz": 110700000, + "channel": null + }, + { + "name": "", + "callsign": "I-HWG", + "beacon_type": 15, + "hertz": 110700000, + "channel": null + }, + { + "name": "", + "callsign": "I-RVP", + "beacon_type": 14, + "hertz": 108300000, + "channel": null + }, + { + "name": "", + "callsign": "I-UVV", + "beacon_type": 14, + "hertz": 111700000, + "channel": null + }, + { + "name": "", + "callsign": "I-UVV", + "beacon_type": 15, + "hertz": 111700000, + "channel": null + }, + { + "name": "", + "callsign": "I-RVP", + "beacon_type": 15, + "hertz": 108300000, + "channel": null + }, + { + "name": "Silverbow", + "callsign": "TQQ", + "beacon_type": 6, + "hertz": 113000000, + "channel": 77 + }, + { + "name": "St George", + "callsign": "UTI", + "beacon_type": 4, + "hertz": 108600000, + "channel": 23 + }, + { + "name": "Grand Canyon", + "callsign": "GCN", + "beacon_type": 4, + "hertz": 113100000, + "channel": 78 + }, + { + "name": "Kingman", + "callsign": "IGM", + "beacon_type": 4, + "hertz": 108800000, + "channel": 25 + }, + { + "name": "Colorado City", + "callsign": "AZC", + "beacon_type": 10, + "hertz": 403000, + "channel": null + }, + { + "name": "Meggi", + "callsign": "EC", + "beacon_type": 10, + "hertz": 217000, + "channel": null + }, + { + "name": "Daggett", + "callsign": "DAG", + "beacon_type": 6, + "hertz": 113200000, + "channel": 79 + }, + { + "name": "Hector", + "callsign": "HEC", + "beacon_type": 6, + "hertz": 112700000, + "channel": 74 + }, + { + "name": "Needles", + "callsign": "EED", + "beacon_type": 6, + "hertz": 115200000, + "channel": 99 + }, + { + "name": "Milford", + "callsign": "MLF", + "beacon_type": 6, + "hertz": 112100000, + "channel": 58 + }, + { + "name": "GOFFS", + "callsign": "GFS", + "beacon_type": 6, + "hertz": 114400000, + "channel": 91 + }, + { + "name": "Tonopah", + "callsign": "TPH", + "beacon_type": 6, + "hertz": 117200000, + "channel": 119 + }, + { + "name": "Mina", + "callsign": "MVA", + "beacon_type": 6, + "hertz": 115100000, + "channel": 98 + }, + { + "name": "Wilson Creek", + "callsign": "ILC", + "beacon_type": 6, + "hertz": 116300000, + "channel": 110 + }, + { + "name": "Cedar City", + "callsign": "CDC", + "beacon_type": 6, + "hertz": 117300000, + "channel": 120 + }, + { + "name": "Bryce Canyon", + "callsign": "BCE", + "beacon_type": 6, + "hertz": 112800000, + "channel": 75 + }, + { + "name": "Mormon Mesa", + "callsign": "MMM", + "beacon_type": 6, + "hertz": 114300000, + "channel": 90 + }, + { + "name": "Beatty", + "callsign": "BTY", + "beacon_type": 6, + "hertz": 114700000, + "channel": 94 + }, + { + "name": "Bishop", + "callsign": "BIH", + "beacon_type": 6, + "hertz": 109600000, + "channel": 33 + }, + { + "name": "Coaldale", + "callsign": "OAL", + "beacon_type": 6, + "hertz": 117700000, + "channel": 124 + }, + { + "name": "Peach Springs", + "callsign": "PGS", + "beacon_type": 6, + "hertz": 112000000, + "channel": 57 + }, + { + "name": "Boulder City", + "callsign": "BLD", + "beacon_type": 6, + "hertz": 116700000, + "channel": 114 + }, + { + "name": "Mercury", + "callsign": "MCY", + "beacon_type": 10, + "hertz": 326000, + "channel": null + } +] \ No newline at end of file diff --git a/resources/dcs/beacons/normandy.json b/resources/dcs/beacons/normandy.json new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/resources/dcs/beacons/normandy.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/resources/dcs/beacons/persiangulf.json b/resources/dcs/beacons/persiangulf.json new file mode 100644 index 00000000..3f0f870a --- /dev/null +++ b/resources/dcs/beacons/persiangulf.json @@ -0,0 +1,709 @@ +[ + { + "name": "ABUDHABI", + "callsign": "ADV", + "beacon_type": 2, + "hertz": 114250000, + "channel": null + }, + { + "name": "AbuDhabiInt", + "callsign": "ADV", + "beacon_type": 3, + "hertz": 114250000, + "channel": 119 + }, + { + "name": "Abumusa", + "callsign": "ABM", + "beacon_type": 3, + "hertz": 285000, + "channel": 101 + }, + { + "name": "AlAinInt", + "callsign": "ALN", + "beacon_type": 4, + "hertz": 112600000, + "channel": 119 + }, + { + "name": "AlBateenInt", + "callsign": "ALB", + "beacon_type": 2, + "hertz": 114000000, + "channel": 119 + }, + { + "name": "BandarAbbas", + "callsign": "BND", + "beacon_type": 4, + "hertz": 117200000, + "channel": 119 + }, + { + "name": "BandarAbbas", + "callsign": "BND", + "beacon_type": 9, + "hertz": 250000, + "channel": null + }, + { + "name": "", + "callsign": "IBND", + "beacon_type": 14, + "hertz": 333800000, + "channel": null + }, + { + "name": "", + "callsign": "IBND", + "beacon_type": 15, + "hertz": 333800000, + "channel": null + }, + { + "name": "BandarAbbas", + "callsign": "BND", + "beacon_type": 5, + "hertz": null, + "channel": 78 + }, + { + "name": "BandarEJask", + "callsign": "KHM", + "beacon_type": 4, + "hertz": 116300000, + "channel": null + }, + { + "name": "BandarEJask", + "callsign": "JSK", + "beacon_type": 9, + "hertz": 349000000, + "channel": null + }, + { + "name": "BandarLengeh", + "callsign": "LEN", + "beacon_type": 9, + "hertz": 408000, + "channel": null + }, + { + "name": "BandarLengeh", + "callsign": "LEN", + "beacon_type": 4, + "hertz": 114800000, + "channel": 95 + }, + { + "name": "", + "callsign": "MMA", + "beacon_type": 15, + "hertz": 111100000, + "channel": 48 + }, + { + "name": "", + "callsign": "LMA", + "beacon_type": 15, + "hertz": 108700000, + "channel": 24 + }, + { + "name": "", + "callsign": "IMA", + "beacon_type": 15, + "hertz": 109100000, + "channel": 28 + }, + { + "name": "", + "callsign": "RMA", + "beacon_type": 15, + "hertz": 114900000, + "channel": 24 + }, + { + "name": "", + "callsign": "MMA", + "beacon_type": 14, + "hertz": 111100000, + "channel": 48 + }, + { + "name": "", + "callsign": "RMA", + "beacon_type": 14, + "hertz": 114900000, + "channel": 24 + }, + { + "name": "", + "callsign": "LMA", + "beacon_type": 14, + "hertz": 108700000, + "channel": 24 + }, + { + "name": "", + "callsign": "IMA", + "beacon_type": 14, + "hertz": 109100000, + "channel": 28 + }, + { + "name": "AlDhafra", + "callsign": "MA", + "beacon_type": 6, + "hertz": 114900000, + "channel": 96 + }, + { + "name": "", + "callsign": "IDBW", + "beacon_type": 14, + "hertz": 109500000, + "channel": null + }, + { + "name": "", + "callsign": "IDBR", + "beacon_type": 14, + "hertz": 110100000, + "channel": null + }, + { + "name": "", + "callsign": "IDBE", + "beacon_type": 14, + "hertz": 111300000, + "channel": null + }, + { + "name": "", + "callsign": "IDBL", + "beacon_type": 14, + "hertz": 110900000, + "channel": null + }, + { + "name": "", + "callsign": "IDBL", + "beacon_type": 15, + "hertz": 110900000, + "channel": null + }, + { + "name": "", + "callsign": "IDBR", + "beacon_type": 15, + "hertz": 110100000, + "channel": null + }, + { + "name": "", + "callsign": "IDBE", + "beacon_type": 15, + "hertz": 111300000, + "channel": null + }, + { + "name": "", + "callsign": "IDBW", + "beacon_type": 15, + "hertz": 109500000, + "channel": null + }, + { + "name": "", + "callsign": "IJEA", + "beacon_type": 14, + "hertz": 111750000, + "channel": null + }, + { + "name": "", + "callsign": "IJWA", + "beacon_type": 15, + "hertz": 109750000, + "channel": null + }, + { + "name": "", + "callsign": "IJEA", + "beacon_type": 15, + "hertz": 111750000, + "channel": null + }, + { + "name": "", + "callsign": "IJWA", + "beacon_type": 14, + "hertz": 109750000, + "channel": null + }, + { + "name": "Fujairah", + "callsign": "FJV", + "beacon_type": 4, + "hertz": 113800000, + "channel": 85 + }, + { + "name": "", + "callsign": "IFJR", + "beacon_type": 15, + "hertz": 111500000, + "channel": null + }, + { + "name": "", + "callsign": "IFJR", + "beacon_type": 14, + "hertz": 111500000, + "channel": null + }, + { + "name": "Havadarya", + "callsign": "HDR", + "beacon_type": 5, + "hertz": 111000000, + "channel": 47 + }, + { + "name": "", + "callsign": "IBHD", + "beacon_type": 14, + "hertz": 108900000, + "channel": null + }, + { + "name": "", + "callsign": "IBHD", + "beacon_type": 15, + "hertz": 108900000, + "channel": null + }, + { + "name": "Jiroft", + "callsign": "JIR", + "beacon_type": 10, + "hertz": 276000, + "channel": null + }, + { + "name": "KERMAN", + "callsign": "KER", + "beacon_type": 5, + "hertz": 122500000, + "channel": 97 + }, + { + "name": "KERMAN", + "callsign": "KER", + "beacon_type": 4, + "hertz": 112000000, + "channel": 57 + }, + { + "name": "KERMAN", + "callsign": "KER", + "beacon_type": 3, + "hertz": 290000000, + "channel": null + }, + { + "name": "", + "callsign": "IBKS", + "beacon_type": 14, + "hertz": 110300000, + "channel": null + }, + { + "name": "", + "callsign": "IBKS", + "beacon_type": 15, + "hertz": 110300000, + "channel": null + }, + { + "name": "KishIsland", + "callsign": "KIH", + "beacon_type": 9, + "hertz": 201000000, + "channel": null + }, + { + "name": "KishIsland", + "callsign": "KIH", + "beacon_type": 5, + "hertz": null, + "channel": 112 + }, + { + "name": "LAR", + "callsign": "LAR", + "beacon_type": 4, + "hertz": 117900000, + "channel": null + }, + { + "name": "LAR", + "callsign": "OISL", + "beacon_type": 9, + "hertz": 224000, + "channel": null + }, + { + "name": "LavanIsland", + "callsign": "LVA", + "beacon_type": 4, + "hertz": 116850000, + "channel": 115 + }, + { + "name": "LavanIsland", + "callsign": "LVA", + "beacon_type": 9, + "hertz": 310000000, + "channel": 0 + }, + { + "name": "LiwaAirbase", + "callsign": "\u00c4\u00bc", + "beacon_type": 7, + "hertz": null, + "channel": 121 + }, + { + "name": "Minhad", + "callsign": "MIN", + "beacon_type": 5, + "hertz": 115200000, + "channel": 99 + }, + { + "name": "", + "callsign": "IMNW", + "beacon_type": 14, + "hertz": 110700000, + "channel": null + }, + { + "name": "", + "callsign": "IMNW", + "beacon_type": 15, + "hertz": 110700000, + "channel": null + }, + { + "name": "", + "callsign": "IMNR", + "beacon_type": 14, + "hertz": 110750000, + "channel": null + }, + { + "name": "", + "callsign": "IMNR", + "beacon_type": 15, + "hertz": 110750000, + "channel": null + }, + { + "name": "GheshmIsland", + "callsign": "KHM", + "beacon_type": 9, + "hertz": 233000, + "channel": null + }, + { + "name": "GheshmIsland", + "callsign": "KHM", + "beacon_type": 4, + "hertz": 117100000, + "channel": null + }, + { + "name": "RasAlKhaimah", + "callsign": "OMRK", + "beacon_type": 4, + "hertz": 113600000, + "channel": 83 + }, + { + "name": "SasAlNakheelAirport", + "callsign": "SAS", + "beacon_type": 10, + "hertz": 128925, + "channel": null + }, + { + "name": "SasAlNakheel", + "callsign": "SAS", + "beacon_type": 4, + "hertz": 128925000, + "channel": 119 + }, + { + "name": "", + "callsign": "ISRE", + "beacon_type": 14, + "hertz": 108550000, + "channel": null + }, + { + "name": "", + "callsign": "ISHW", + "beacon_type": 14, + "hertz": 111950000, + "channel": null + }, + { + "name": "", + "callsign": "ISHW", + "beacon_type": 15, + "hertz": 111950000, + "channel": null + }, + { + "name": "", + "callsign": "ISRE", + "beacon_type": 15, + "hertz": 108550000, + "channel": null + }, + { + "name": "SHIRAZ", + "callsign": "SYZ", + "beacon_type": 4, + "hertz": 117800000, + "channel": 125 + }, + { + "name": "SHIRAZ", + "callsign": "SYZ1", + "beacon_type": 5, + "hertz": 114700000, + "channel": 94 + }, + { + "name": "SHIRAZ", + "callsign": "SR", + "beacon_type": 9, + "hertz": 205000, + "channel": null + }, + { + "name": "", + "callsign": "ISYZ", + "beacon_type": 15, + "hertz": 109900000, + "channel": null + }, + { + "name": "", + "callsign": "ISYZ", + "beacon_type": 14, + "hertz": 109900000, + "channel": null + }, + { + "name": "SirriIsland", + "callsign": "SIR", + "beacon_type": 9, + "hertz": 300000, + "channel": null + }, + { + "name": "SirriIsland", + "callsign": "SIR", + "beacon_type": 4, + "hertz": 113750000, + "channel": null + }, + { + "name": "Kochak", + "callsign": "KCK", + "beacon_type": 5, + "hertz": 114200000, + "channel": 89 + }, + { + "name": "Kish", + "callsign": "KIS", + "beacon_type": 4, + "hertz": 117400000, + "channel": 121 + }, + { + "name": "DohaAirport", + "callsign": "DIA", + "beacon_type": 4, + "hertz": 112400000, + "channel": 71 + }, + { + "name": "HamadInternationalAirport", + "callsign": "DOH", + "beacon_type": 4, + "hertz": 114400000, + "channel": 91 + }, + { + "name": "DezfulAirport", + "callsign": "DZF", + "beacon_type": 9, + "hertz": 293000000, + "channel": null + }, + { + "name": "AbadanIntAirport", + "callsign": "ABD", + "beacon_type": 4, + "hertz": 115100000, + "channel": 98 + }, + { + "name": "AhvazIntAirport", + "callsign": "AWZ", + "beacon_type": 4, + "hertz": 114000000, + "channel": 87 + }, + { + "name": "AghajariAirport", + "callsign": "AJR", + "beacon_type": 4, + "hertz": 114900000, + "channel": 96 + }, + { + "name": "BirjandIntAirport", + "callsign": "BJD", + "beacon_type": 4, + "hertz": 113500000, + "channel": 82 + }, + { + "name": "BushehrIntAirport", + "callsign": "BUZ", + "beacon_type": 4, + "hertz": 117450000, + "channel": 121 + }, + { + "name": "KonarakAirport", + "callsign": "CBH", + "beacon_type": 4, + "hertz": 115600000, + "channel": 103 + }, + { + "name": "IsfahanIntAirport", + "callsign": "ISN", + "beacon_type": 4, + "hertz": 113200000, + "channel": 79 + }, + { + "name": "KhoramabadAirport", + "callsign": "KRD", + "beacon_type": 4, + "hertz": 113750000, + "channel": 84 + }, + { + "name": "PersianGulfIntAirport", + "callsign": "PRG", + "beacon_type": 4, + "hertz": 112100000, + "channel": 58 + }, + { + "name": "YasoujAirport", + "callsign": "YSJ", + "beacon_type": 4, + "hertz": 116550000, + "channel": 112 + }, + { + "name": "BamAirport", + "callsign": "BAM", + "beacon_type": 4, + "hertz": 114900000, + "channel": 96 + }, + { + "name": "MahshahrAirport", + "callsign": "MAH", + "beacon_type": 4, + "hertz": 115800000, + "channel": 105 + }, + { + "name": "IranShahrAirport", + "callsign": "ISR", + "beacon_type": 4, + "hertz": 117000000, + "channel": 117 + }, + { + "name": "LamerdAirport", + "callsign": "LAM", + "beacon_type": 4, + "hertz": 117000000, + "channel": 117 + }, + { + "name": "SirjanAirport", + "callsign": "SRJ", + "beacon_type": 4, + "hertz": 114600000, + "channel": 93 + }, + { + "name": "YazdIntAirport", + "callsign": "YZD", + "beacon_type": 4, + "hertz": 117700000, + "channel": 124 + }, + { + "name": "ZabolAirport", + "callsign": "ZAL", + "beacon_type": 4, + "hertz": 113100000, + "channel": 78 + }, + { + "name": "ZahedanIntAirport", + "callsign": "ZDN", + "beacon_type": 4, + "hertz": 116000000, + "channel": 107 + }, + { + "name": "RafsanjanAirport", + "callsign": "RAF", + "beacon_type": 4, + "hertz": 112300000, + "channel": 70 + }, + { + "name": "SaravanAirport", + "callsign": "SRN", + "beacon_type": 4, + "hertz": 114100000, + "channel": 88 + }, + { + "name": "BuHasa", + "callsign": "BH", + "beacon_type": 3, + "hertz": 309000000, + "channel": null + } +] \ No newline at end of file diff --git a/resources/dcs/beacons/syria.json b/resources/dcs/beacons/syria.json new file mode 100644 index 00000000..d5ee97cd --- /dev/null +++ b/resources/dcs/beacons/syria.json @@ -0,0 +1,408 @@ +[ + { + "name": "Deir ez-Zor", + "callsign": "DRZ", + "beacon_type": 10, + "hertz": 295000, + "channel": null + }, + { + "name": "GAZIANTEP", + "callsign": "GAZ", + "beacon_type": 10, + "hertz": 432000, + "channel": null + }, + { + "name": "BANIAS", + "callsign": "BAN", + "beacon_type": 10, + "hertz": 304000, + "channel": null + }, + { + "name": "ALEPPO", + "callsign": "ALE", + "beacon_type": 10, + "hertz": 396000, + "channel": null + }, + { + "name": "KAHRAMANMARAS", + "callsign": "KHM", + "beacon_type": 10, + "hertz": 374000, + "channel": null + }, + { + "name": "MEZZEH", + "callsign": "MEZ", + "beacon_type": 10, + "hertz": 358000, + "channel": null + }, + { + "name": "KLEYATE", + "callsign": "RA", + "beacon_type": 10, + "hertz": 450000, + "channel": null + }, + { + "name": "KARIATAIN", + "callsign": "KTN", + "beacon_type": 10, + "hertz": 372500, + "channel": null + }, + { + "name": "ALEPPO", + "callsign": "MER", + "beacon_type": 10, + "hertz": 365000, + "channel": null + }, + { + "name": "TURAIF", + "callsign": "TRF", + "beacon_type": 4, + "hertz": 116100000, + "channel": null + }, + { + "name": "Deir ez-Zor", + "callsign": "DRZ", + "beacon_type": 4, + "hertz": 117000000, + "channel": null + }, + { + "name": "BAYSUR", + "callsign": "BAR", + "beacon_type": 4, + "hertz": 113900000, + "channel": null + }, + { + "name": "ALEPPO", + "callsign": "ALE", + "beacon_type": 4, + "hertz": 114500000, + "channel": null + }, + { + "name": "MARKA", + "callsign": "AMN", + "beacon_type": 4, + "hertz": 116300000, + "channel": null + }, + { + "name": "GAZIANTEP", + "callsign": "GAZ", + "beacon_type": 4, + "hertz": 116700000, + "channel": null + }, + { + "name": "ROSH-PINA", + "callsign": "ROP", + "beacon_type": 4, + "hertz": 115300000, + "channel": null + }, + { + "name": "TANF", + "callsign": "TAN", + "beacon_type": 4, + "hertz": 114000000, + "channel": null + }, + { + "name": "NATANIA", + "callsign": "NAT", + "beacon_type": 4, + "hertz": 112400000, + "channel": null + }, + { + "name": "KAHRAMANMARAS", + "callsign": "KHM", + "beacon_type": 4, + "hertz": 113900000, + "channel": null + }, + { + "name": "KARIATAIN", + "callsign": "KTN", + "beacon_type": 4, + "hertz": 117700000, + "channel": null + }, + { + "name": "", + "callsign": "IADA", + "beacon_type": 14, + "hertz": 108700000, + "channel": null + }, + { + "name": "", + "callsign": "IADA", + "beacon_type": 15, + "hertz": 108700000, + "channel": null + }, + { + "name": "ADANA", + "callsign": "ADN", + "beacon_type": 11, + "hertz": 395000000, + "channel": null + }, + { + "name": "ADANA", + "callsign": "ADA", + "beacon_type": 4, + "hertz": 112700000, + "channel": null + }, + { + "name": "KALDE", + "callsign": "KAD", + "beacon_type": 4, + "hertz": 112600000, + "channel": null + }, + { + "name": "", + "callsign": "IBB", + "beacon_type": 15, + "hertz": 110100000, + "channel": null + }, + { + "name": "", + "callsign": "IKK", + "beacon_type": 14, + "hertz": 110700000, + "channel": null + }, + { + "name": "", + "callsign": "BIL", + "beacon_type": 14, + "hertz": 109500000, + "channel": null + }, + { + "name": "", + "callsign": "IBB", + "beacon_type": 14, + "hertz": 110100000, + "channel": null + }, + { + "name": "", + "callsign": "BIL", + "beacon_type": 15, + "hertz": 109500000, + "channel": null + }, + { + "name": "", + "callsign": "IKK", + "beacon_type": 15, + "hertz": 110700000, + "channel": null + }, + { + "name": "BEIRUT", + "callsign": "BOD", + "beacon_type": 11, + "hertz": 351000000, + "channel": null + }, + { + "name": "", + "callsign": "IDA", + "beacon_type": 15, + "hertz": 109900000, + "channel": null + }, + { + "name": "", + "callsign": "IDA", + "beacon_type": 14, + "hertz": 109900000, + "channel": null + }, + { + "name": "Damascus", + "callsign": "DAM", + "beacon_type": 4, + "hertz": 116000000, + "channel": null + }, + { + "name": "", + "callsign": "DAML", + "beacon_type": 14, + "hertz": 111100000, + "channel": null + }, + { + "name": "DAMASCUS", + "callsign": "DAL", + "beacon_type": 11, + "hertz": 342000000, + "channel": null + }, + { + "name": "ABYAD", + "callsign": "ABD", + "beacon_type": 10, + "hertz": 264000, + "channel": null + }, + { + "name": "", + "callsign": "DAML", + "beacon_type": 15, + "hertz": 111100000, + "channel": null + }, + { + "name": "HATAY", + "callsign": "HTY", + "beacon_type": 4, + "hertz": 112050000, + "channel": null + }, + { + "name": "", + "callsign": "IHAT", + "beacon_type": 14, + "hertz": 108900000, + "channel": null + }, + { + "name": "", + "callsign": "IHAT", + "beacon_type": 15, + "hertz": 108900000, + "channel": null + }, + { + "name": "HATAY", + "callsign": "HTY", + "beacon_type": 10, + "hertz": 336000, + "channel": null + }, + { + "name": "", + "callsign": "IHTY", + "beacon_type": 15, + "hertz": 108150000, + "channel": null + }, + { + "name": "", + "callsign": "IHTY", + "beacon_type": 14, + "hertz": 108150000, + "channel": null + }, + { + "name": "INCIRLIC", + "callsign": "DAN", + "beacon_type": 6, + "hertz": 108400000, + "channel": 21 + }, + { + "name": "", + "callsign": "IDAN", + "beacon_type": 14, + "hertz": 109300000, + "channel": null + }, + { + "name": "", + "callsign": "IDAN", + "beacon_type": 15, + "hertz": 109300000, + "channel": null + }, + { + "name": "", + "callsign": "DANM", + "beacon_type": 15, + "hertz": 111700000, + "channel": null + }, + { + "name": "", + "callsign": "DANM", + "beacon_type": 14, + "hertz": 111700000, + "channel": null + }, + { + "name": "", + "callsign": "IBA", + "beacon_type": 15, + "hertz": 109100000, + "channel": null + }, + { + "name": "", + "callsign": "IBA", + "beacon_type": 14, + "hertz": 109100000, + "channel": null + }, + { + "name": "LATAKIA", + "callsign": "LTK", + "beacon_type": 4, + "hertz": 114800000, + "channel": null + }, + { + "name": "LATAKIA", + "callsign": "LTK", + "beacon_type": 11, + "hertz": 414000000, + "channel": null + }, + { + "name": "PALMYRA", + "callsign": "PLR", + "beacon_type": 10, + "hertz": 363000, + "channel": null + }, + { + "name": "PALMYRA", + "callsign": "PAL", + "beacon_type": 10, + "hertz": 337000, + "channel": null + }, + { + "name": "RAMATDAVID", + "callsign": "RMD", + "beacon_type": 10, + "hertz": 368000, + "channel": null + }, + { + "name": "Cheka", + "callsign": "CAK", + "beacon_type": 4, + "hertz": 116200000, + "channel": null + } +] \ No newline at end of file From 010d505f04ad94012f801ae20bfe123141d32e09 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Mon, 31 Aug 2020 16:30:11 -0700 Subject: [PATCH 20/61] Reserve frequencies used by beacons. --- game/operation/operation.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/game/operation/operation.py b/game/operation/operation.py index 09242628..141f5b5b 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -1,12 +1,14 @@ -from dcs.countries import country_dict -from dcs.lua.parse import loads -from dcs.terrain import Terrain +from typing import Set from gen import * -from userdata.debriefing import * from gen.airfields import AIRFIELD_DATA +from gen.beacons import load_beacons_for_terrain from gen.radios import RadioRegistry from gen.tacan import TacanRegistry +from pydcs.dcs.countries import country_dict +from pydcs.dcs.lua.parse import loads +from pydcs.dcs.terrain.terrain import Terrain +from userdata.debriefing import * class Operation: @@ -119,6 +121,20 @@ class Operation: self.defenders_starting_position = self.to_cp.at def generate(self): + # Dedup beacon frequencies, since some maps have more than one beacon + # per frequency. + beacons = load_beacons_for_terrain(self.game.theater.terrain.name) + unique_beacon_frequencies: Set[RadioFrequency] = set() + for beacon in beacons: + unique_beacon_frequencies.add(beacon.frequency) + if beacon.is_tacan: + if beacon.channel is None: + logging.error( + f"TACAN beacon has no channel: {beacon.callsign}") + else: + self.tacan_registry.reserve(beacon.tacan_channel) + for frequency in unique_beacon_frequencies: + self.radio_registry.reserve(frequency) # Generate meteo if self.environment_settings is None: @@ -135,10 +151,8 @@ class Operation: self.radio_registry.reserve(data.atc.vhf_fm) self.radio_registry.reserve(data.atc.vhf_am) self.radio_registry.reserve(data.atc.uhf) - for ils in data.ils.values(): - self.radio_registry.reserve(ils) - if data.tacan is not None: - self.tacan_registry.reserve(data.tacan) + # No need to reserve ILS or TACAN because those are in the + # beacon list. # Generate destroyed units for d in self.game.get_destroyed_units(): From a9e65cc83d3ce036c57623cf18c56b8426395d53 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 1 Sep 2020 00:03:37 -0700 Subject: [PATCH 21/61] Setup default radio channels for player flights. --- game/operation/operation.py | 77 ++++++++++++++- gen/aircraft.py | 182 ++++++++++++++++++++++++++++++++---- gen/flights/flight.py | 14 +-- gen/kneeboard.py | 98 +++++++++---------- 4 files changed, 289 insertions(+), 82 deletions(-) diff --git a/game/operation/operation.py b/game/operation/operation.py index 141f5b5b..09b5eb0a 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -255,7 +255,7 @@ class Operation: load_dcs_libe.add_action(DoScript(String(script))) self.current_mission.triggerrules.triggers.append(load_dcs_libe) - kneeboard_generator = KneeboardGenerator(self.current_mission, self.game) + kneeboard_generator = KneeboardGenerator(self.current_mission) # Briefing Generation for tanker in self.airsupportgen.air_support.tankers: @@ -269,9 +269,82 @@ class Operation: self.briefinggen.append_frequency(awacs.callsign, awacs.freq) kneeboard_generator.add_awacs(awacs) + self.assign_channels_to_flights() + # Generate the briefing self.briefinggen.generate() for region, code, name in self.game.jtacs: kneeboard_generator.add_jtac(name, region, code) - kneeboard_generator.generate() + kneeboard_generator.generate(self.airgen.flights) + + def assign_channels_to_flights(self) -> None: + """Assigns preset radio channels for client flights.""" + for flight in self.airgen.flights: + if not flight.client_units: + continue + self.assign_channels_to_flight(flight) + + def assign_channels_to_flight(self, flight: FlightData) -> None: + """Assigns preset radio channels for a client flight.""" + airframe = flight.aircraft_type + + try: + aircraft_data = AIRCRAFT_DATA[airframe.id] + except KeyError: + logging.warning(f"No aircraft data for {airframe.id}") + return + + # Intra-flight channel is set up when the flight is created, however we + # do need to make sure we don't overwrite it. For cases where the + # inter-flight and intra-flight radios share presets (the AV-8B only has + # one set of channels, even though it can use two channels + # simultaneously), start assigning channels at 2. + radio_id = aircraft_data.inter_flight_radio_index + if aircraft_data.intra_flight_radio_index == radio_id: + first_channel = 2 + else: + first_channel = 1 + + last_channel = flight.num_radio_channels(radio_id) + channel_alloc = iter(range(first_channel, last_channel + 1)) + + # TODO: Fix departure/arrival to support carriers. + if flight.departure is not None: + try: + departure = AIRFIELD_DATA[flight.departure.name] + flight.assign_channel( + radio_id, next(channel_alloc), departure.atc.uhf) + except KeyError: + pass + + # TODO: If there ever are multiple AWACS, limit to mission relevant. + for awacs in self.airsupportgen.air_support.awacs: + flight.assign_channel(radio_id, next(channel_alloc), awacs.freq) + + # TODO: Fix departure/arrival to support carriers. + if flight.arrival is not None and flight.arrival != flight.departure: + try: + arrival = AIRFIELD_DATA[flight.arrival.name] + flight.assign_channel( + radio_id, next(channel_alloc), arrival.atc.uhf) + except KeyError: + pass + + try: + # TODO: Skip incompatible tankers. + for tanker in self.airsupportgen.air_support.tankers: + flight.assign_channel( + radio_id, next(channel_alloc), tanker.freq) + + if flight.divert is not None: + try: + divert = AIRFIELD_DATA[flight.divert.name] + flight.assign_channel( + radio_id, next(channel_alloc), divert.atc.uhf) + except KeyError: + pass + except StopIteration: + # Any remaining channels are nice-to-haves, but not necessary for + # the few aircraft with a small number of channels available. + pass diff --git a/gen/aircraft.py b/gen/aircraft.py index aa5a62e4..e0e8460e 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -1,15 +1,21 @@ from dataclasses import dataclass -from typing import Dict +from typing import Dict, List, Optional, Tuple from game.data.cap_capabilities_db import GUNFIGHTERS from game.settings import Settings from game.utils import nm_to_meter from gen.flights.ai_flight_planner import FlightPlanner -from gen.flights.flight import Flight, FlightType, FlightWaypointType +from gen.flights.flight import ( + Flight, + FlightType, + FlightWaypoint, + FlightWaypointType, +) from gen.radios import get_radio, MHz, Radio, RadioFrequency, RadioRegistry from pydcs.dcs import helicopters from pydcs.dcs.action import ActivateGroup, AITaskPush, MessageToAll from pydcs.dcs.condition import TimeAfter, CoalitionHasAirdrome, PartOfCoalitionInZone +from pydcs.dcs.flyingunit import FlyingUnit from pydcs.dcs.helicopters import helicopter_map, UH_1H from pydcs.dcs.mission import Mission, StartType from pydcs.dcs.planes import ( @@ -24,9 +30,9 @@ from pydcs.dcs.planes import ( SpitfireLFMkIX, SpitfireLFMkIXCW, ) -from pydcs.dcs.terrain.terrain import NoParkingSlotError +from pydcs.dcs.terrain.terrain import Airport, NoParkingSlotError from pydcs.dcs.triggers import TriggerOnce, Event -from pydcs.dcs.unittype import UnitType +from pydcs.dcs.unittype import FlyingType, UnitType from .conflictgen import * from .naming import * @@ -59,12 +65,40 @@ class AircraftData: #: The type of radio used for intra-flight communications. intra_flight_radio: Radio + #: Index of the radio used for intra-flight communications. Matches the + #: index of the panel_radio field of the pydcs.dcs.planes object. + inter_flight_radio_index: Optional[int] + + #: Index of the radio used for intra-flight communications. Matches the + #: index of the panel_radio field of the pydcs.dcs.planes object. + intra_flight_radio_index: Optional[int] + # Indexed by the id field of the pydcs PlaneType. AIRCRAFT_DATA: Dict[str, AircraftData] = { - "A-10C": AircraftData(get_radio("AN/ARC-186(V) AM")), - "F-16C_50": AircraftData(get_radio("AN/ARC-222")), - "F/A-18C": AircraftData(get_radio("AN/ARC-210")), + "A-10C": AircraftData( + get_radio("AN/ARC-186(V) AM"), + # The A-10's radio works differently than most aircraft. Doesn't seem to + # be a way to set these from the mission editor, let alone pydcs. + inter_flight_radio_index=None, + intra_flight_radio_index=None + ), + "F-16C_50": AircraftData( + get_radio("AN/ARC-222"), + # COM2 is the AN/ARC-222, which is the VHF radio we want to use for + # intra-flight communication to leave COM1 open for UHF inter-flight. + inter_flight_radio_index=1, + intra_flight_radio_index=2 + ), + "FA-18C_hornet": AircraftData( + get_radio("AN/ARC-210"), + # DCS will clobber channel 1 of the first radio compatible with the + # flight's assigned frequency. Since the F/A-18's two radios are both + # AN/ARC-210s, radio 1 will be compatible regardless of which frequency + # is assigned, so we must use radio 1 for the intra-flight radio. + inter_flight_radio_index=2, + intra_flight_radio_index=1 + ), } @@ -98,6 +132,100 @@ def get_fallback_channel(unit_type: UnitType) -> RadioFrequency: return UHF_FALLBACK_CHANNEL +@dataclass(frozen=True) +class ChannelAssignment: + radio_id: int + channel: int + + @property + def radio_name(self) -> str: + """Returns the name of the radio, i.e. COM1.""" + return f"COM{self.radio_id}" + + +@dataclass +class FlightData: + """Details of a planned flight.""" + + #: List of playable units in the flight. + client_units: List[FlyingUnit] + + # TODO: Arrival and departure should not be optional, but carriers don't count. + #: Arrival airport. + arrival: Optional[Airport] + + #: Departure airport. + departure: Optional[Airport] + + #: Diver airport. + divert: Optional[Airport] + + #: Waypoints of the flight plan. + waypoints: List[FlightWaypoint] + + #: Radio frequency for intra-flight communications. + intra_flight_channel: RadioFrequency + + #: Map of radio frequencies to their assigned radio and channel, if any. + frequency_to_channel_map: Dict[RadioFrequency, ChannelAssignment] + + def __init__(self, client_units: List[FlyingUnit], arrival: Airport, + departure: Airport, divert: Optional[Airport], + waypoints: List[FlightWaypoint], + intra_flight_channel: RadioFrequency) -> None: + self.client_units = client_units + self.arrival = arrival + self.departure = departure + self.divert = divert + self.waypoints = waypoints + self.intra_flight_channel = intra_flight_channel + self.frequency_to_channel_map = {} + + self.assign_intra_flight_channel() + + def assign_intra_flight_channel(self) -> None: + """Assigns a channel to the intra-flight frequency.""" + if not self.client_units: + return + + # pydcs will actually set up the channel for us, but we want to make + # sure that it ends up in frequency_to_channel_map. + try: + data = AIRCRAFT_DATA[self.aircraft_type.id] + self.assign_channel( + data.intra_flight_radio_index, 1, self.intra_flight_channel) + except KeyError: + logging.warning(f"No aircraft data for {self.aircraft_type.id}") + + @property + def aircraft_type(self) -> FlyingType: + """Returns the type of aircraft in this flight.""" + return self.client_units[0].unit_type + + def num_radio_channels(self, radio_id: int) -> int: + """Returns the number of preset channels for the given radio.""" + return self.client_units[0].num_radio_channels(radio_id) + + def channel_for( + self, frequency: RadioFrequency) -> Optional[ChannelAssignment]: + """Returns the radio and channel number for the given frequency.""" + return self.frequency_to_channel_map.get(frequency, None) + + def assign_channel(self, radio_id: int, channel_id: int, + frequency: RadioFrequency) -> None: + """Assigns a preset radio channel to the given frequency.""" + for unit in self.client_units: + unit.set_radio_channel_preset(radio_id, channel_id, frequency.mhz) + + # One frequency could be bound to multiple channels. Prefer the first, + # since with the current implementation it will be the lowest numbered + # channel. + if frequency not in self.frequency_to_channel_map: + self.frequency_to_channel_map[frequency] = ChannelAssignment( + radio_id, channel_id + ) + + class AircraftConflictGenerator: escort_targets = [] # type: typing.List[typing.Tuple[FlyingGroup, int]] @@ -109,14 +237,26 @@ class AircraftConflictGenerator: self.conflict = conflict self.radio_registry = radio_registry self.escort_targets = [] + self.flights: List[FlightData] = [] - def get_intra_flight_channel(self, airframe: UnitType) -> RadioFrequency: + def get_intra_flight_channel( + self, airframe: UnitType) -> Tuple[int, RadioFrequency]: + """Allocates an intra-flight channel to a group. + + Args: + airframe: The type of aircraft a channel should be allocated for. + + Returns: + A tuple of the radio index (for aircraft with multiple radios) and + the frequency of the intra-flight channel. + """ try: aircraft_data = AIRCRAFT_DATA[airframe.id] - return self.radio_registry.alloc_for_radio( + channel = self.radio_registry.alloc_for_radio( aircraft_data.intra_flight_radio) + return aircraft_data.intra_flight_radio_index, channel except KeyError: - return get_fallback_channel(airframe) + return 1, get_fallback_channel(airframe) def _start_type(self) -> StartType: return self.settings.cold_start and StartType.Cold or StartType.Warm @@ -156,12 +296,15 @@ class AircraftConflictGenerator: for unit_instance in group.units: unit_instance.livery_id = db.PLANE_LIVERY_OVERRIDES[unit_type] + clients: List[FlyingUnit] = [] single_client = flight.client_count == 1 for idx in range(0, min(len(group.units), flight.client_count)): + unit = group.units[idx] + clients.append(unit) if single_client: - group.units[idx].set_player() + unit.set_player() else: - group.units[idx].set_client() + unit.set_client() # Do not generate player group with late activation. if group.late_activation: @@ -169,14 +312,21 @@ class AircraftConflictGenerator: # Set up F-14 Client to have pre-stored alignement if unit_type is F_14B: - group.units[idx].set_property(F_14B.Properties.INSAlignmentStored.id, True) + unit.set_property(F_14B.Properties.INSAlignmentStored.id, True) group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)) - channel = self.get_intra_flight_channel(unit_type) - group.set_frequency(channel.mhz) - flight.intra_flight_channel = channel + radio_id, channel = self.get_intra_flight_channel(unit_type) + group.set_frequency(channel.mhz, radio_id) + self.flights.append(FlightData( + client_units=clients, + departure=flight.from_cp.airport, + arrival=flight.from_cp.airport, + divert=None, + waypoints=flight.points, + intra_flight_channel=channel + )) # Special case so Su 33 carrier take off if unit_type is Su_33: diff --git a/gen/flights/flight.py b/gen/flights/flight.py index 912946e4..a9b5e9f8 100644 --- a/gen/flights/flight.py +++ b/gen/flights/flight.py @@ -1,10 +1,8 @@ from enum import Enum -from typing import List, Optional - -from pydcs.dcs.unittype import UnitType +from typing import List from game import db -from gen.radios import RadioFrequency +from pydcs.dcs.unittype import UnitType class FlightType(Enum): @@ -96,13 +94,6 @@ class Flight: # How long before this flight should take off scheduled_in = 0 - # Populated during mission generation time by AircraftConflictGenerator. - # TODO: Decouple radio planning from the Flight. - # Make AircraftConflictGenerator generate a FlightData object that is - # returned to the Operation rather than relying on the Flight object, which - # represents a game UI flight rather than a fully planned flight. - intra_flight_channel: Optional[RadioFrequency] - def __init__(self, unit_type: UnitType, count: int, from_cp, flight_type: FlightType): self.unit_type = unit_type self.count = count @@ -112,7 +103,6 @@ class Flight: self.targets = [] self.loadout = {} self.start_type = "Runway" - self.intra_flight_channel = None def __repr__(self): return self.flight_type.name + " | " + str(self.count) + "x" + db.unit_type_name(self.unit_type) \ diff --git a/gen/kneeboard.py b/gen/kneeboard.py index 1241078e..4c652757 100644 --- a/gen/kneeboard.py +++ b/gen/kneeboard.py @@ -34,9 +34,9 @@ from pydcs.dcs.mission import Mission from pydcs.dcs.terrain.terrain import Airport from pydcs.dcs.unittype import FlyingType from . import units +from .aircraft import FlightData from .airfields import AIRFIELD_DATA from .airsupportgen import AwacsInfo, TankerInfo -from .flights.flight import Flight from .radios import RadioFrequency @@ -96,24 +96,6 @@ class KneeboardPage: raise NotImplementedError -class AirfieldInfo: - def __init__(self, airfield: Airport) -> None: - self.airport = airfield - # TODO: Implement logic for picking preferred runway. - runway = airfield.runways[0] - runway_side = ["", "L", "R"][runway.leftright] - self.runway = f"{runway.heading}{runway_side}" - try: - extra_data = AIRFIELD_DATA[airfield.name] - self.atc = extra_data.atc.uhf or "" - self.tacan = extra_data.tacan or "" - self.ils = extra_data.ils_freq(self.runway) or "" - except KeyError: - self.atc = "" - self.ils = "" - self.tacan = "" - - @dataclass class CommInfo: """Communications information for the kneeboard.""" @@ -131,21 +113,15 @@ class JtacInfo: class BriefingPage(KneeboardPage): """A kneeboard page containing briefing information.""" - def __init__(self, flight: Flight, comms: List[CommInfo], + def __init__(self, flight: FlightData, comms: List[CommInfo], awacs: List[AwacsInfo], tankers: List[TankerInfo], jtacs: List[JtacInfo]) -> None: self.flight = flight - self.comms = comms + self.comms = list(comms) self.awacs = awacs self.tankers = tankers self.jtacs = jtacs - if self.flight.intra_flight_channel is not None: - self.comms.append( - CommInfo("Flight", self.flight.intra_flight_channel) - ) - self.departure = flight.from_cp.airport - self.arrival = flight.from_cp.airport - self.divert: Optional[Airport] = None + self.comms.append(CommInfo("Flight", self.flight.intra_flight_channel)) def write(self, path: Path) -> None: writer = KneeboardPageWriter() @@ -156,14 +132,14 @@ class BriefingPage(KneeboardPage): # TODO: Handle carriers. writer.heading("Airfield Info") writer.table([ - self.airfield_info_row("Departure", self.departure), - self.airfield_info_row("Arrival", self.arrival), - self.airfield_info_row("Divert", self.divert), + self.airfield_info_row("Departure", self.flight.departure), + self.airfield_info_row("Arrival", self.flight.arrival), + self.airfield_info_row("Divert", self.flight.divert), ], headers=["", "Airbase", "ATC", "TCN", "ILS", "RWY"]) writer.heading("Flight Plan") flight_plan = [] - for num, waypoint in enumerate(self.flight.points): + for num, waypoint in enumerate(self.flight.waypoints): alt = int(units.meters_to_feet(waypoint.alt)) flight_plan.append([num, waypoint.pretty_name, str(alt)]) writer.table(flight_plan, headers=["STPT", "Action", "Alt"]) @@ -171,13 +147,13 @@ class BriefingPage(KneeboardPage): writer.heading("Comm Ladder") comms = [] for comm in self.comms: - comms.append([comm.name, comm.freq]) + comms.append([comm.name, self.format_frequency(comm.freq)]) writer.table(comms, headers=["Name", "UHF"]) writer.heading("AWACS") awacs = [] for a in self.awacs: - awacs.append([a.callsign, a.freq]) + awacs.append([a.callsign, self.format_frequency(a.freq)]) writer.table(awacs, headers=["Callsign", "UHF"]) writer.heading("Tankers") @@ -187,7 +163,7 @@ class BriefingPage(KneeboardPage): tanker.callsign, tanker.variant, tanker.tacan, - tanker.freq, + self.format_frequency(tanker.freq), ]) writer.table(tankers, headers=["Callsign", "Type", "TACAN", "UHF"]) @@ -213,23 +189,42 @@ class BriefingPage(KneeboardPage): """ if airfield is None: return [row_title, "", "", "", "", ""] - info = AirfieldInfo(airfield) + + # TODO: Implement logic for picking preferred runway. + runway = airfield.runways[0] + runway_side = ["", "L", "R"][runway.leftright] + runway_text = f"{runway.heading}{runway_side}" + + try: + extra_data = AIRFIELD_DATA[airfield.name] + atc = self.format_frequency(extra_data.atc.uhf) + tacan = extra_data.tacan or "" + ils = extra_data.ils_freq(runway) or "" + except KeyError: + atc = "" + ils = "" + tacan = "" return [ row_title, airfield.name, - info.atc, - info.tacan, - info.ils, - info.runway, + atc, + tacan, + ils, + runway_text, ] + def format_frequency(self, frequency: RadioFrequency) -> str: + channel = self.flight.channel_for(frequency) + if channel is None: + return str(frequency) + return f"{channel.radio_name} Ch {channel.channel}" + class KneeboardGenerator: """Creates kneeboard pages for each client flight in the mission.""" - def __init__(self, mission: Mission, game) -> None: + def __init__(self, mission: Mission) -> None: self.mission = mission - self.game = game self.comms: List[CommInfo] = [] self.awacs: List[AwacsInfo] = [] self.tankers: List[TankerInfo] = [] @@ -271,11 +266,11 @@ class KneeboardGenerator: # TODO: Radio info? Type? self.jtacs.append(JtacInfo(callsign, region, code)) - def generate(self) -> None: + def generate(self, flights: List[FlightData]) -> None: """Generates a kneeboard per client flight.""" temp_dir = Path("kneeboards") temp_dir.mkdir(exist_ok=True) - for aircraft, pages in self.pages_by_airframe().items(): + for aircraft, pages in self.pages_by_airframe(flights).items(): aircraft_dir = temp_dir / aircraft.id aircraft_dir.mkdir(exist_ok=True) for idx, page in enumerate(pages): @@ -283,7 +278,7 @@ class KneeboardGenerator: page.write(page_path) self.mission.add_aircraft_kneeboard(aircraft, page_path) - def pages_by_airframe(self) -> Dict[FlyingType, List[KneeboardPage]]: + def pages_by_airframe(self, flights: List[FlightData]) -> Dict[FlyingType, List[KneeboardPage]]: """Returns a list of kneeboard pages per airframe in the mission. Only client flights will be included, but because DCS does not support @@ -295,15 +290,14 @@ class KneeboardGenerator: that aircraft. """ all_flights: Dict[FlyingType, List[KneeboardPage]] = defaultdict(list) - for cp in self.game.theater.controlpoints: - if cp.id in self.game.planners.keys(): - for flight in self.game.planners[cp.id].flights: - if flight.client_count > 0: - all_flights[flight.unit_type].extend( - self.generate_flight_kneeboard(flight)) + for flight in flights: + if not flight.client_units: + continue + all_flights[flight.aircraft_type].extend( + self.generate_flight_kneeboard(flight)) return all_flights - def generate_flight_kneeboard(self, flight: Flight) -> List[KneeboardPage]: + def generate_flight_kneeboard(self, flight: FlightData) -> List[KneeboardPage]: """Returns a list of kneeboard pages for the given flight.""" return [ BriefingPage( From d02a3a0d3f8f067095da8f4dd1a9ec558f962f50 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 1 Sep 2020 14:10:57 -0700 Subject: [PATCH 22/61] Add carrier support to kneeboards. --- game/operation/operation.py | 60 ++++++++-------- gen/aircraft.py | 85 +++++++++++------------ gen/airfields.py | 38 ++++++++++ gen/groundobjectsgen.py | 84 +++++++++++++++++++--- gen/kneeboard.py | 36 ++++------ resources/tools/generate_loadout_check.py | 2 +- theater/controlpoint.py | 14 ---- 7 files changed, 191 insertions(+), 128 deletions(-) diff --git a/game/operation/operation.py b/game/operation/operation.py index 09b5eb0a..051df639 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -80,7 +80,13 @@ class Operation: self.visualgen = VisualGenerator(mission, conflict, self.game) self.envgen = EnviromentGenerator(mission, conflict, self.game) self.forcedoptionsgen = ForcedOptionsGenerator(mission, conflict, self.game) - self.groundobjectgen = GroundObjectsGenerator(mission, conflict, self.game) + self.groundobjectgen = GroundObjectsGenerator( + mission, + conflict, + self.game, + self.radio_registry, + self.tacan_registry + ) self.briefinggen = BriefingGenerator(mission, conflict, self.game) def prepare(self, terrain: Terrain, is_quick: bool): @@ -136,15 +142,6 @@ class Operation: for frequency in unique_beacon_frequencies: self.radio_registry.reserve(frequency) - # Generate meteo - if self.environment_settings is None: - self.environment_settings = self.envgen.generate() - else: - self.envgen.load(self.environment_settings) - - # Generate ground object first - self.groundobjectgen.generate() - for airfield, data in AIRFIELD_DATA.items(): if data.theater == self.game.theater.terrain.name: self.radio_registry.reserve(data.atc.hf) @@ -154,6 +151,15 @@ class Operation: # No need to reserve ILS or TACAN because those are in the # beacon list. + # Generate meteo + if self.environment_settings is None: + self.environment_settings = self.envgen.generate() + else: + self.envgen.load(self.environment_settings) + + # Generate ground object first + self.groundobjectgen.generate() + # Generate destroyed units for d in self.game.get_destroyed_units(): try: @@ -185,7 +191,12 @@ class Operation: else: country = self.current_mission.country(self.game.enemy_country) if cp.id in self.game.planners.keys(): - self.airgen.generate_flights(cp, country, self.game.planners[cp.id]) + self.airgen.generate_flights( + cp, + country, + self.game.planners[cp.id], + self.groundobjectgen.runways + ) # Generate ground units on frontline everywhere self.game.jtacs = [] @@ -309,27 +320,16 @@ class Operation: last_channel = flight.num_radio_channels(radio_id) channel_alloc = iter(range(first_channel, last_channel + 1)) - # TODO: Fix departure/arrival to support carriers. - if flight.departure is not None: - try: - departure = AIRFIELD_DATA[flight.departure.name] - flight.assign_channel( - radio_id, next(channel_alloc), departure.atc.uhf) - except KeyError: - pass + flight.assign_channel(radio_id, next(channel_alloc),flight.departure.atc) # TODO: If there ever are multiple AWACS, limit to mission relevant. for awacs in self.airsupportgen.air_support.awacs: flight.assign_channel(radio_id, next(channel_alloc), awacs.freq) # TODO: Fix departure/arrival to support carriers. - if flight.arrival is not None and flight.arrival != flight.departure: - try: - arrival = AIRFIELD_DATA[flight.arrival.name] - flight.assign_channel( - radio_id, next(channel_alloc), arrival.atc.uhf) - except KeyError: - pass + if flight.arrival != flight.departure: + flight.assign_channel(radio_id, next(channel_alloc), + flight.arrival.atc) try: # TODO: Skip incompatible tankers. @@ -338,12 +338,8 @@ class Operation: radio_id, next(channel_alloc), tanker.freq) if flight.divert is not None: - try: - divert = AIRFIELD_DATA[flight.divert.name] - flight.assign_channel( - radio_id, next(channel_alloc), divert.atc.uhf) - except KeyError: - pass + flight.assign_channel(radio_id, next(channel_alloc), + flight.divert.atc) except StopIteration: # Any remaining channels are nice-to-haves, but not necessary for # the few aircraft with a small number of channels available. diff --git a/gen/aircraft.py b/gen/aircraft.py index e0e8460e..941bcd38 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -4,6 +4,7 @@ from typing import Dict, List, Optional, Tuple from game.data.cap_capabilities_db import GUNFIGHTERS from game.settings import Settings from game.utils import nm_to_meter +from gen.airfields import RunwayData from gen.flights.ai_flight_planner import FlightPlanner from gen.flights.flight import ( Flight, @@ -150,15 +151,14 @@ class FlightData: #: List of playable units in the flight. client_units: List[FlyingUnit] - # TODO: Arrival and departure should not be optional, but carriers don't count. #: Arrival airport. - arrival: Optional[Airport] + arrival: RunwayData #: Departure airport. - departure: Optional[Airport] + departure: RunwayData #: Diver airport. - divert: Optional[Airport] + divert: Optional[RunwayData] #: Waypoints of the flight plan. waypoints: List[FlightWaypoint] @@ -169,8 +169,8 @@ class FlightData: #: Map of radio frequencies to their assigned radio and channel, if any. frequency_to_channel_map: Dict[RadioFrequency, ChannelAssignment] - def __init__(self, client_units: List[FlyingUnit], arrival: Airport, - departure: Airport, divert: Optional[Airport], + def __init__(self, client_units: List[FlyingUnit], arrival: RunwayData, + departure: RunwayData, divert: Optional[RunwayData], waypoints: List[FlightWaypoint], intra_flight_channel: RadioFrequency) -> None: self.client_units = client_units @@ -261,8 +261,8 @@ class AircraftConflictGenerator: def _start_type(self) -> StartType: return self.settings.cold_start and StartType.Cold or StartType.Warm - - def _setup_group(self, group: FlyingGroup, for_task: typing.Type[Task], flight: Flight): + def _setup_group(self, group: FlyingGroup, for_task: typing.Type[Task], + flight: Flight, dynamic_runways: Dict[str, RunwayData]): did_load_loadout = False unit_type = group.units[0].unit_type @@ -319,10 +319,28 @@ class AircraftConflictGenerator: radio_id, channel = self.get_intra_flight_channel(unit_type) group.set_frequency(channel.mhz, radio_id) + + # TODO: Support for different departure/arrival airfields. + cp = flight.from_cp + fallback_runway = RunwayData(cp.full_name, runway_name="") + if cp.cptype == ControlPointType.AIRBASE: + # TODO: Implement logic for picking preferred runway. + runway = flight.from_cp.airport.runways[0] + runway_side = ["", "L", "R"][runway.leftright] + runway_name = f"{runway.heading}{runway_side}" + departure_runway = RunwayData.for_airfield( + flight.from_cp.airport, runway_name) + elif cp.is_fleet: + departure_runway = dynamic_runways.get(cp.name, fallback_runway) + else: + logging.warning(f"Unhandled departure control point: {cp.cptype}") + departure_runway = fallback_runway + self.flights.append(FlightData( client_units=clients, - departure=flight.from_cp.airport, - arrival=flight.from_cp.airport, + departure=departure_runway, + arrival=departure_runway, + # TODO: Support for divert airfields. divert=None, waypoints=flight.points, intra_flight_channel=channel @@ -477,8 +495,8 @@ class AircraftConflictGenerator: logging.warning("Pylon not found ! => Pylon" + key + " on " + str(flight.unit_type)) - def generate_flights(self, cp, country, flight_planner:FlightPlanner): - + def generate_flights(self, cp, country, flight_planner: FlightPlanner, + dynamic_runways: Dict[str, RunwayData]): # Clear pydcs parking slots if cp.airport is not None: logging.info("CLEARING SLOTS @ " + cp.airport.name) @@ -497,7 +515,8 @@ class AircraftConflictGenerator: continue logging.info("Generating flight : " + str(flight.unit_type)) group = self.generate_planned_flight(cp, country, flight) - self.setup_flight_group(group, flight, flight.flight_type) + self.setup_flight_group(group, flight, flight.flight_type, + dynamic_runways) self.setup_group_activation_trigger(flight, group) @@ -608,19 +627,13 @@ class AircraftConflictGenerator: flight.group = group return group - def setup_group_as_intercept_flight(self, group, flight): - group.points[0].ETA = 0 - group.late_activation = True - self._setup_group(group, Intercept, flight) - for point in flight.points: - group.add_waypoint(Point(point.x,point.y), point.alt) - - def setup_flight_group(self, group, flight, flight_type): + def setup_flight_group(self, group, flight, flight_type, + dynamic_runways: Dict[str, RunwayData]): if flight_type in [FlightType.CAP, FlightType.BARCAP, FlightType.TARCAP, FlightType.INTERCEPTION]: group.task = CAP.name - self._setup_group(group, CAP, flight) + self._setup_group(group, CAP, flight, dynamic_runways) # group.points[0].tasks.clear() group.points[0].tasks.clear() group.points[0].tasks.append(EngageTargets(max_distance=nm_to_meter(50), targets=[Targets.All.Air])) @@ -632,7 +645,7 @@ class AircraftConflictGenerator: elif flight_type in [FlightType.CAS, FlightType.BAI]: group.task = CAS.name - self._setup_group(group, CAS, flight) + self._setup_group(group, CAS, flight, dynamic_runways) group.points[0].tasks.clear() group.points[0].tasks.append(EngageTargets(max_distance=nm_to_meter(10), targets=[Targets.All.GroundUnits.GroundVehicles])) group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)) @@ -641,7 +654,7 @@ class AircraftConflictGenerator: group.points[0].tasks.append(OptRestrictJettison(True)) elif flight_type in [FlightType.SEAD, FlightType.DEAD]: group.task = SEAD.name - self._setup_group(group, SEAD, flight) + self._setup_group(group, SEAD, flight, dynamic_runways) group.points[0].tasks.clear() group.points[0].tasks.append(NoTask()) group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)) @@ -650,14 +663,14 @@ class AircraftConflictGenerator: group.points[0].tasks.append(OptRTBOnOutOfAmmo(OptRTBOnOutOfAmmo.Values.ASM)) elif flight_type in [FlightType.STRIKE]: group.task = PinpointStrike.name - self._setup_group(group, GroundAttack, flight) + self._setup_group(group, GroundAttack, flight, dynamic_runways) group.points[0].tasks.clear() group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)) group.points[0].tasks.append(OptROE(OptROE.Values.OpenFire)) group.points[0].tasks.append(OptRestrictJettison(True)) elif flight_type in [FlightType.ANTISHIP]: group.task = AntishipStrike.name - self._setup_group(group, AntishipStrike, flight) + self._setup_group(group, AntishipStrike, flight, dynamic_runways) group.points[0].tasks.clear() group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)) group.points[0].tasks.append(OptROE(OptROE.Values.OpenFire)) @@ -736,23 +749,3 @@ class AircraftConflictGenerator: pt.name = String(point.name) self._setup_custom_payload(flight, group) - - - def setup_group_as_antiship_flight(self, group, flight): - group.task = AntishipStrike.name - self._setup_group(group, AntishipStrike, flight) - - group.points[0].tasks.clear() - group.points[0].tasks.append(AntishipStrikeTaskAction()) - group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)) - group.points[0].tasks.append(OptROE(OptROE.Values.OpenFireWeaponFree)) - group.points[0].tasks.append(OptRestrictJettison(True)) - - for point in flight.points: - group.add_waypoint(Point(point.x, point.y), point.alt) - - - def setup_radio_preset(self, flight, group): - pass - - diff --git a/gen/airfields.py b/gen/airfields.py index 8b98668d..12680c40 100644 --- a/gen/airfields.py +++ b/gen/airfields.py @@ -4,8 +4,10 @@ Remove once https://github.com/pydcs/dcs/issues/69 tracks getting the missing data added to pydcs. Until then, missing data can be manually filled in here. """ from dataclasses import dataclass, field +import logging from typing import Dict, Optional, Tuple +from pydcs.dcs.terrain.terrain import Airport from .radios import MHz, RadioFrequency from .tacan import TacanBand, TacanChannel @@ -637,3 +639,39 @@ AIRFIELD_DATA = { atc=AtcData(MHz(3, 775), MHz(118, 50), MHz(38, 450), MHz(250, 50)), ), } + + +@dataclass +class RunwayData: + airfield_name: str + runway_name: str + atc: Optional[RadioFrequency] = None + tacan: Optional[TacanChannel] = None + ils: Optional[RadioFrequency] = None + icls: Optional[int] = None + + @classmethod + def for_airfield(cls, airport: Airport, runway: str) -> "RunwayData": + """Creates RunwayData for the given runway of an airfield. + + Args: + airport: The airfield the runway belongs to. + runway: Identifier of the runway to use. e.g. "030" or "200L". + """ + atc: Optional[RadioFrequency] = None + tacan: Optional[TacanChannel] = None + ils: Optional[RadioFrequency] = None + try: + airfield = AIRFIELD_DATA[airport.name] + atc = airfield.atc.uhf + tacan = airfield.tacan + ils = airfield.ils_freq(runway) + except KeyError: + logging.warning(f"No airfield data for {airport.name}") + return cls( + airport.name, + runway, + atc, + tacan, + ils + ) diff --git a/gen/groundobjectsgen.py b/gen/groundobjectsgen.py index 89a09293..6a12579e 100644 --- a/gen/groundobjectsgen.py +++ b/gen/groundobjectsgen.py @@ -1,13 +1,19 @@ -import logging - -from game import db from game.data.building_data import FORTIFICATION_UNITS_ID, FORTIFICATION_UNITS from game.db import unit_type_from_name +from pydcs.dcs.mission import * +from pydcs.dcs.statics import * +from pydcs.dcs.task import ( + ActivateBeaconCommand, + ActivateICLSCommand, + OptAlarmState, +) +from pydcs.dcs.unit import Ship, Vehicle +from pydcs.dcs.unitgroup import StaticGroup +from .airfields import RunwayData from .conflictgen import * from .naming import * - -from dcs.mission import * -from dcs.statics import * +from .radios import RadioRegistry +from .tacan import TacanBand, TacanRegistry FARP_FRONTLINE_DISTANCE = 10000 AA_CP_MIN_DISTANCE = 40000 @@ -16,10 +22,15 @@ AA_CP_MIN_DISTANCE = 40000 class GroundObjectsGenerator: FARP_CAPACITY = 4 - def __init__(self, mission: Mission, conflict: Conflict, game): + def __init__(self, mission: Mission, conflict: Conflict, game, + radio_registry: RadioRegistry, tacan_registry: TacanRegistry): self.m = mission self.conflict = conflict self.game = game + self.radio_registry = radio_registry + self.tacan_registry = tacan_registry + self.icls_alloc = iter(range(1, 21)) + self.runways: Dict[str, RunwayData] = {} def generate_farps(self, number_of_units=1) -> typing.Collection[StaticGroup]: if self.conflict.is_vector: @@ -103,6 +114,8 @@ class GroundObjectsGenerator: utype = db.upgrade_to_supercarrier(utype, cp.name) sg = self.m.ship_group(side, g.name, utype, position=g.position, heading=g.units[0].heading) + atc_channel = self.radio_registry.alloc_uhf() + sg.set_frequency(atc_channel.hertz) sg.units[0].name = self.m.string(g.units[0].name) for i, u in enumerate(g.units): @@ -111,6 +124,8 @@ class GroundObjectsGenerator: ship.position.x = u.position.x ship.position.y = u.position.y ship.heading = u.heading + # TODO: Verify. + ship.set_frequency(atc_channel.hertz) sg.add_unit(ship) # Find carrier direction (In the wind) @@ -125,10 +140,57 @@ class GroundObjectsGenerator: attempt = attempt + 1 # Set UP TACAN and ICLS - modeChannel = "X" if not cp.tacanY else "Y" - sg.points[0].tasks.append(ActivateBeaconCommand(channel=cp.tacanN, modechannel=modeChannel, callsign=cp.tacanI, unit_id=sg.units[0].id, aa=False)) - if ground_object.dcs_identifier == "CARRIER" and hasattr(cp, "icls"): - sg.points[0].tasks.append(ActivateICLSCommand(cp.icls, unit_id=sg.units[0].id)) + tacan = self.tacan_registry.alloc_for_band(TacanBand.X) + icls_channel = next(self.icls_alloc) + # TODO: Assign these properly. + if ground_object.dcs_identifier == "CARRIER": + tacan_callsign = random.choice([ + "STE", + "CVN", + "CVH", + "CCV", + "ACC", + "ARC", + "GER", + "ABR", + "LIN", + "TRU", + ]) + else: + tacan_callsign = random.choice([ + "LHD", + "LHA", + "LHB", + "LHC", + "LHD", + "LDS", + ]) + sg.points[0].tasks.append(ActivateBeaconCommand( + channel=tacan.number, + modechannel=tacan.band.value, + callsign=tacan_callsign, + unit_id=sg.units[0].id, + aa=False + )) + sg.points[0].tasks.append(ActivateICLSCommand( + icls_channel, + unit_id=sg.units[0].id + )) + # TODO: Make unit name usable. + # This relies on one control point mapping exactly + # to one LHA, carrier, or other usable "runway". + # This isn't wholly true, since the DD escorts of + # the carrier group are valid for helicopters, but + # they aren't exposed as such to the game. Should + # clean this up so that's possible. We can't use the + # unit name since it's an arbitrary ID. + self.runways[cp.name] = RunwayData( + cp.name, + "N/A", + atc=atc_channel, + tacan=tacan, + icls=icls_channel, + ) else: diff --git a/gen/kneeboard.py b/gen/kneeboard.py index 4c652757..bb7bb4da 100644 --- a/gen/kneeboard.py +++ b/gen/kneeboard.py @@ -31,11 +31,10 @@ from PIL import Image, ImageDraw, ImageFont from tabulate import tabulate from pydcs.dcs.mission import Mission -from pydcs.dcs.terrain.terrain import Airport from pydcs.dcs.unittype import FlyingType from . import units from .aircraft import FlightData -from .airfields import AIRFIELD_DATA +from .airfields import RunwayData from .airsupportgen import AwacsInfo, TankerInfo from .radios import RadioFrequency @@ -135,7 +134,7 @@ class BriefingPage(KneeboardPage): self.airfield_info_row("Departure", self.flight.departure), self.airfield_info_row("Arrival", self.flight.arrival), self.airfield_info_row("Divert", self.flight.divert), - ], headers=["", "Airbase", "ATC", "TCN", "ILS", "RWY"]) + ], headers=["", "Airbase", "ATC", "TCN", "I(C)LS", "RWY"]) writer.heading("Flight Plan") flight_plan = [] @@ -176,41 +175,30 @@ class BriefingPage(KneeboardPage): writer.write(path) def airfield_info_row(self, row_title: str, - airfield: Optional[Airport]) -> List[str]: + runway: Optional[RunwayData]) -> List[str]: """Creates a table row for a given airfield. Args: row_title: Purpose of the airfield. e.g. "Departure", "Arrival" or "Divert". - airfield: The airfield described by this row. + runway: The runway described by this row. Returns: A list of strings to be used as a row of the airfield table. """ - if airfield is None: + if runway is None: return [row_title, "", "", "", "", ""] - # TODO: Implement logic for picking preferred runway. - runway = airfield.runways[0] - runway_side = ["", "L", "R"][runway.leftright] - runway_text = f"{runway.heading}{runway_side}" - - try: - extra_data = AIRFIELD_DATA[airfield.name] - atc = self.format_frequency(extra_data.atc.uhf) - tacan = extra_data.tacan or "" - ils = extra_data.ils_freq(runway) or "" - except KeyError: - atc = "" - ils = "" - tacan = "" + atc = "" + if runway.atc is not None: + atc = self.format_frequency(runway.atc) return [ row_title, - airfield.name, + runway.airfield_name, atc, - tacan, - ils, - runway_text, + runway.tacan or "", + runway.ils or runway.icls or "", + runway.runway_name, ] def format_frequency(self, frequency: RadioFrequency) -> str: diff --git a/resources/tools/generate_loadout_check.py b/resources/tools/generate_loadout_check.py index ae299a0f..165da58f 100644 --- a/resources/tools/generate_loadout_check.py +++ b/resources/tools/generate_loadout_check.py @@ -30,6 +30,6 @@ for t, uts in db.UNIT_BY_TASK.items(): altitude=10000 ) g.task = t.name - airgen._setup_group(g, t, 0) + airgen._setup_group(g, t, 0, {}) mis.save("loadout_test.miz") diff --git a/theater/controlpoint.py b/theater/controlpoint.py index 96b6605c..d7a726e7 100644 --- a/theater/controlpoint.py +++ b/theater/controlpoint.py @@ -27,7 +27,6 @@ class ControlPoint: full_name = None # type: str base = None # type: theater.base.Base at = None # type: db.StartPosition - icls = 1 allow_sea_units = True connected_points = None # type: typing.List[ControlPoint] @@ -38,7 +37,6 @@ class ControlPoint: frontline_offset = 0.0 cptype: ControlPointType = None - ICLS_counter = 1 alt = 0 def __init__(self, id: int, name: str, position: Point, at, radials: typing.Collection[int], size: int, importance: float, @@ -63,10 +61,6 @@ class ControlPoint: self.base = theater.base.Base() self.cptype = cptype self.stances = {} - self.tacanY = False - self.tacanN = None - self.tacanI = "TAC" - self.icls = 0 self.airport = None @classmethod @@ -81,11 +75,6 @@ class ControlPoint: import theater.conflicttheater cp = cls(id, name, at, at, theater.conflicttheater.LAND, theater.conflicttheater.SIZE_SMALL, 1, has_frontline=False, cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP) - cp.tacanY = False - cp.tacanN = random.randint(26, 49) - cp.tacanI = random.choice(["STE", "CVN", "CVH", "CCV", "ACC", "ARC", "GER", "ABR", "LIN", "TRU"]) - ControlPoint.ICLS_counter = ControlPoint.ICLS_counter + 1 - cp.icls = ControlPoint.ICLS_counter return cp @classmethod @@ -93,9 +82,6 @@ class ControlPoint: import theater.conflicttheater cp = cls(id, name, at, at, theater.conflicttheater.LAND, theater.conflicttheater.SIZE_SMALL, 1, has_frontline=False, cptype=ControlPointType.LHA_GROUP) - cp.tacanY = False - cp.tacanN = random.randint(1,25) - cp.tacanI = random.choice(["LHD", "LHA", "LHB", "LHC", "LHD", "LDS"]) return cp @property From b31e186d1d70430e42ab705a2a6e9e6b9ecc7344 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Wed, 2 Sep 2020 17:45:14 -0700 Subject: [PATCH 23/61] Fix inconsistent runway numbering. pydcs gives us a 3-digit runway, but most of our data is 2-digit runway numbers, so we weren't finding any runways for those airfields. --- gen/aircraft.py | 3 ++- gen/airfields.py | 42 +++++++++++++++++++++--------------------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/gen/aircraft.py b/gen/aircraft.py index 941bcd38..6e0c450d 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -326,8 +326,9 @@ class AircraftConflictGenerator: if cp.cptype == ControlPointType.AIRBASE: # TODO: Implement logic for picking preferred runway. runway = flight.from_cp.airport.runways[0] + runway_number = runway.heading // 10 runway_side = ["", "L", "R"][runway.leftright] - runway_name = f"{runway.heading}{runway_side}" + runway_name = f"{runway_number:02}{runway_side}" departure_runway = RunwayData.for_airfield( flight.from_cp.airport, runway_name) elif cp.is_fleet: diff --git a/gen/airfields.py b/gen/airfields.py index 12680c40..fcd71760 100644 --- a/gen/airfields.py +++ b/gen/airfields.py @@ -96,13 +96,13 @@ AIRFIELD_DATA = { tacan_callsign="KBL", atc=AtcData(MHz(4, 350), MHz(133, 0), MHz(40, 800), MHz(262, 0)), ils={ - "7": ("IKB", MHz(111, 50)), + "07": ("IKB", MHz(111, 50)), }, outer_ndb={ - "7": ("KT", MHz(870, 0)), + "07": ("KT", MHz(870, 0)), }, inner_ndb={ - "7": ("T", MHz(490, 0)), + "07": ("T", MHz(490, 0)), }, ), @@ -115,13 +115,13 @@ AIRFIELD_DATA = { tacan_callsign="TSK", atc=AtcData(MHz(4, 300), MHz(132, 0), MHz(40, 600), MHz(261, 0)), ils={ - "9": ("ITS", MHz(108, 90)), + "09": ("ITS", MHz(108, 90)), }, outer_ndb={ - "9": ("BI", MHz(335, 0)), + "09": ("BI", MHz(335, 0)), }, inner_ndb={ - "9": ("I", MHz(688, 0)), + "09": ("I", MHz(688, 0)), }, ), @@ -134,7 +134,7 @@ AIRFIELD_DATA = { tacan_callsign="KTS", atc=AtcData(MHz(4, 400), MHz(134, 0), MHz(41, 0), MHz(263, 0)), ils={ - "8": ("IKS", MHz(109, 75)), + "08": ("IKS", MHz(109, 75)), }, ), @@ -167,7 +167,7 @@ AIRFIELD_DATA = { runway_length=9686, atc=AtcData(MHz(4, 50), MHz(127, 0), MHz(39, 600), MHz(256, 0)), ils={ - "6": ("ISO", MHz(111, 10)), + "06": ("ISO", MHz(111, 10)), }, ), @@ -210,15 +210,15 @@ AIRFIELD_DATA = { rsbn=("KW", 28), atc=AtcData(MHz(3, 900), MHz(124, 0), MHz(39, 0), MHz(253, 0)), prmg={ - "4": ("OX", 26), + "04": ("OX", 26), "22": ("KW", 26), }, outer_ndb={ - "4": ("OX", MHz(408, 0)), + "04": ("OX", MHz(408, 0)), "22": ("KW", MHz(408, 0)), }, inner_ndb={ - "4": ("O", MHz(803, 0)), + "04": ("O", MHz(803, 0)), "22": ("K", MHz(803, 0)), }, ), @@ -231,14 +231,14 @@ AIRFIELD_DATA = { rsbn=("MB", 40), atc=AtcData(MHz(3, 800), MHz(122, 0), MHz(38, 600), MHz(251, 0)), prmg={ - "9": ("MB", 38), + "09": ("MB", 38), }, outer_ndb={ - "9": ("MB", MHz(625, 0)), + "09": ("MB", MHz(625, 0)), "27": ("OC", MHz(625, 0)), }, inner_ndb={ - "9": ("M", MHz(303, 0)), + "09": ("M", MHz(303, 0)), "27": ("C", MHz(303, 0)), }, ), @@ -252,11 +252,11 @@ AIRFIELD_DATA = { atc=AtcData(MHz(4, 100), MHz(128, 0), MHz(39, 800), MHz(257, 0)), outer_ndb={ "23": ("LD", MHz(493, 0)), - "5": ("KR", MHz(493, 0)), + "05": ("KR", MHz(493, 0)), }, inner_ndb={ "23": ("L", MHz(240, 0)), - "5": ("K", MHz(240, 0)), + "05": ("K", MHz(240, 0)), }, ), @@ -268,10 +268,10 @@ AIRFIELD_DATA = { rsbn=("DG", 34), atc=AtcData(MHz(3, 950), MHz(125, 0), MHz(39, 200), MHz(254, 0)), prmg={ - "4": ("DG", 36), + "04": ("DG", 36), }, outer_ndb={ - "4": ("DG", MHz(289, 0)), + "04": ("DG", MHz(289, 0)), "22": ("RK", MHz(289, 0)), }, inner_ndb={ @@ -415,8 +415,8 @@ AIRFIELD_DATA = { vor=("DAN", MHz(108, 400)), atc=AtcData(MHz(3, 850), MHz(38, 600), MHz(129, 400), MHz(360, 100)), ils={ - "050": ("IDAN", MHz(109, 300)), - "230": ("DANM", MHz(111, 700)), + "50": ("IDAN", MHz(109, 300)), + "23": ("DANM", MHz(111, 700)), }, ), @@ -656,7 +656,7 @@ class RunwayData: Args: airport: The airfield the runway belongs to. - runway: Identifier of the runway to use. e.g. "030" or "200L". + runway: Identifier of the runway to use. e.g. "03" or "20L". """ atc: Optional[RadioFrequency] = None tacan: Optional[TacanChannel] = None From 9d31c478d3e9cef4ce47bc5f5550d3689e688c17 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Fri, 4 Sep 2020 00:35:58 -0700 Subject: [PATCH 24/61] Fix briefing generation. I removed the nav target info from the briefing because that doesn't seem to have been doing what it was intended to do. It didn't give any actual target information, all it would show was (example is a JF-17 strike mission): PP1 PP2 PP3 PP4 Without any additional context that doesn't seem too helpful to me. I'll be following up (hopefully) shortly by adding target information (type, coordinates, STPT/PP, etc) to both the briefing and the kneeboard that will cover that. Refactor a bunch to share some code with the kneeboard generator as well. --- game/operation/frontlineattack.py | 2 - game/operation/operation.py | 30 ++-- gen/aircraft.py | 46 +++++-- gen/airfields.py | 4 +- gen/briefinggen.py | 220 +++++++++++++++++++----------- gen/groundobjectsgen.py | 1 + gen/kneeboard.py | 69 +--------- 7 files changed, 207 insertions(+), 165 deletions(-) diff --git a/game/operation/frontlineattack.py b/game/operation/frontlineattack.py index 902ff6c9..48c5965c 100644 --- a/game/operation/frontlineattack.py +++ b/game/operation/frontlineattack.py @@ -36,6 +36,4 @@ class FrontlineAttackOperation(Operation): def generate(self): self.briefinggen.title = "Frontline CAS" self.briefinggen.description = "Provide CAS for the ground forces attacking enemy lines. Operation will be considered successful if total number of enemy units will be lower than your own by a factor of 1.5 (i.e. with 12 units from both sides, enemy forces need to be reduced to at least 8), meaning that you (and, probably, your wingmans) should concentrate on destroying the enemy units. Target base strength will be lowered as a result. Be advised that your flight will not attack anything until you explicitly tell them so by comms menu." - self.briefinggen.append_waypoint("CAS AREA IP") - self.briefinggen.append_waypoint("CAS AREA EGRESS") super(FrontlineAttackOperation, self).generate() diff --git a/game/operation/operation.py b/game/operation/operation.py index 051df639..f87c13e3 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -266,28 +266,34 @@ class Operation: load_dcs_libe.add_action(DoScript(String(script))) self.current_mission.triggerrules.triggers.append(load_dcs_libe) + self.assign_channels_to_flights() + kneeboard_generator = KneeboardGenerator(self.current_mission) - # Briefing Generation + for dynamic_runway in self.groundobjectgen.runways.values(): + self.briefinggen.add_dynamic_runway(dynamic_runway) + for tanker in self.airsupportgen.air_support.tankers: - self.briefinggen.append_frequency( - f"Tanker {tanker.callsign} ({tanker.variant})", - f"{tanker.tacan}/{tanker.freq}") + self.briefinggen.add_tanker(tanker) kneeboard_generator.add_tanker(tanker) if self.is_awacs_enabled: for awacs in self.airsupportgen.air_support.awacs: - self.briefinggen.append_frequency(awacs.callsign, awacs.freq) + self.briefinggen.add_awacs(awacs) kneeboard_generator.add_awacs(awacs) - self.assign_channels_to_flights() - - # Generate the briefing - self.briefinggen.generate() - for region, code, name in self.game.jtacs: - kneeboard_generator.add_jtac(name, region, code) - kneeboard_generator.generate(self.airgen.flights) + # TODO: Radio info? Type? + jtac = JtacInfo(name, region, code) + self.briefinggen.add_jtac(jtac) + kneeboard_generator.add_jtac(jtac) + + for flight in self.airgen.flights: + self.briefinggen.add_flight(flight) + kneeboard_generator.add_flight(flight) + + self.briefinggen.generate() + kneeboard_generator.generate() def assign_channels_to_flights(self) -> None: """Assigns preset radio channels for client flights.""" diff --git a/gen/aircraft.py b/gen/aircraft.py index 6e0c450d..02451ab5 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -148,8 +148,19 @@ class ChannelAssignment: class FlightData: """Details of a planned flight.""" - #: List of playable units in the flight. - client_units: List[FlyingUnit] + flight_type: FlightType + + #: All units in the flight. + units: List[FlyingUnit] + + #: Total number of aircraft in the flight. + size: int + + #: True if this flight belongs to the player's coalition. + friendly: bool + + #: Number of minutes after mission start the flight is set to depart. + departure_delay: int #: Arrival airport. arrival: RunwayData @@ -169,13 +180,18 @@ class FlightData: #: Map of radio frequencies to their assigned radio and channel, if any. frequency_to_channel_map: Dict[RadioFrequency, ChannelAssignment] - def __init__(self, client_units: List[FlyingUnit], arrival: RunwayData, - departure: RunwayData, divert: Optional[RunwayData], - waypoints: List[FlightWaypoint], + def __init__(self, flight_type: FlightType, units: List[FlyingUnit], + size: int, friendly: bool, departure_delay: int, + departure: RunwayData, arrival: RunwayData, + divert: Optional[RunwayData], waypoints: List[FlightWaypoint], intra_flight_channel: RadioFrequency) -> None: - self.client_units = client_units - self.arrival = arrival + self.flight_type = flight_type + self.units = units + self.size = size + self.friendly = friendly + self.departure_delay = departure_delay self.departure = departure + self.arrival = arrival self.divert = divert self.waypoints = waypoints self.intra_flight_channel = intra_flight_channel @@ -183,6 +199,11 @@ class FlightData: self.assign_intra_flight_channel() + @property + def client_units(self) -> List[FlyingUnit]: + """List of playable units in the flight.""" + return [u for u in self.units if u.is_human()] + def assign_intra_flight_channel(self) -> None: """Assigns a channel to the intra-flight frequency.""" if not self.client_units: @@ -200,10 +221,11 @@ class FlightData: @property def aircraft_type(self) -> FlyingType: """Returns the type of aircraft in this flight.""" - return self.client_units[0].unit_type + return self.units[0].unit_type def num_radio_channels(self, radio_id: int) -> int: """Returns the number of preset channels for the given radio.""" + # Note: pydcs only initializes the radio presets for client slots. return self.client_units[0].num_radio_channels(radio_id) def channel_for( @@ -296,11 +318,9 @@ class AircraftConflictGenerator: for unit_instance in group.units: unit_instance.livery_id = db.PLANE_LIVERY_OVERRIDES[unit_type] - clients: List[FlyingUnit] = [] single_client = flight.client_count == 1 for idx in range(0, min(len(group.units), flight.client_count)): unit = group.units[idx] - clients.append(unit) if single_client: unit.set_player() else: @@ -338,7 +358,11 @@ class AircraftConflictGenerator: departure_runway = fallback_runway self.flights.append(FlightData( - client_units=clients, + flight_type=flight.flight_type, + units=group.units, + size=len(group.units), + friendly=flight.from_cp.captured, + departure_delay=flight.scheduled_in, departure=departure_runway, arrival=departure_runway, # TODO: Support for divert airfields. diff --git a/gen/airfields.py b/gen/airfields.py index fcd71760..e4c5f1fb 100644 --- a/gen/airfields.py +++ b/gen/airfields.py @@ -641,12 +641,13 @@ AIRFIELD_DATA = { } -@dataclass +@dataclass(frozen=True) class RunwayData: airfield_name: str runway_name: str atc: Optional[RadioFrequency] = None tacan: Optional[TacanChannel] = None + tacan_callsign: Optional[str] = None ils: Optional[RadioFrequency] = None icls: Optional[int] = None @@ -665,6 +666,7 @@ class RunwayData: airfield = AIRFIELD_DATA[airport.name] atc = airfield.atc.uhf tacan = airfield.tacan + tacan = airfield.tacan_callsign ils = airfield.ils_freq(runway) except KeyError: logging.warning(f"No airfield data for {airport.name}") diff --git a/gen/briefinggen.py b/gen/briefinggen.py index c3e10931..9eed3af2 100644 --- a/gen/briefinggen.py +++ b/gen/briefinggen.py @@ -1,68 +1,136 @@ -import logging +import os +from collections import defaultdict +from dataclasses import dataclass +import random +from typing import List, Tuple from game import db -from .conflictgen import * -from .naming import * - -from dcs.mission import * +from pydcs.dcs.mission import Mission +from .aircraft import FlightData +from .airfields import RunwayData +from .airsupportgen import AwacsInfo, TankerInfo +from .conflictgen import Conflict +from .ground_forces.combat_stance import CombatStance +from .radios import RadioFrequency -class BriefingGenerator: - freqs = None # type: typing.List[typing.Tuple[str, str]] - title = "" # type: str - description = "" # type: str - targets = None # type: typing.List[typing.Tuple[str, str]] - waypoints = None # type: typing.List[str] +@dataclass +class CommInfo: + """Communications information for the kneeboard.""" + name: str + freq: RadioFrequency + + +@dataclass +class JtacInfo: + """JTAC information for the kneeboard.""" + callsign: str + region: str + code: str + + +class MissionInfoGenerator: + """Base type for generators of mission information for the player. + + Examples of subtypes include briefing generators, kneeboard generators, etc. + """ + + def __init__(self, mission: Mission) -> None: + self.mission = mission + self.awacs: List[AwacsInfo] = [] + self.comms: List[CommInfo] = [] + self.flights: List[FlightData] = [] + self.jtacs: List[JtacInfo] = [] + self.tankers: List[TankerInfo] = [] + + def add_awacs(self, awacs: AwacsInfo) -> None: + """Adds an AWACS/GCI to the mission. + + Args: + awacs: AWACS information. + """ + self.awacs.append(awacs) + + def add_comm(self, name: str, freq: RadioFrequency) -> None: + """Adds communications info to the mission. + + Args: + name: Name of the radio channel. + freq: Frequency of the radio channel. + """ + self.comms.append(CommInfo(name, freq)) + + def add_flight(self, flight: FlightData) -> None: + """Adds flight info to the mission. + + Args: + flight: Flight information. + """ + self.flights.append(flight) + + def add_jtac(self, jtac: JtacInfo) -> None: + """Adds a JTAC to the mission. + + Args: + jtac: JTAC information. + """ + self.jtacs.append(jtac) + + def add_tanker(self, tanker: TankerInfo) -> None: + """Adds a tanker to the mission. + + Args: + tanker: Tanker information. + """ + self.tankers.append(tanker) + + def generate(self) -> None: + """Generates the mission information.""" + raise NotImplementedError + + +class BriefingGenerator(MissionInfoGenerator): def __init__(self, mission: Mission, conflict: Conflict, game): - self.m = mission + super().__init__(mission) self.conflict = conflict self.game = game + self.title = "" self.description = "" + self.dynamic_runways: List[RunwayData] = [] - self.freqs = [] - self.targets = [] - self.waypoints = [] + def add_dynamic_runway(self, runway: RunwayData) -> None: + """Adds a dynamically generated runway to the briefing. - self.jtacs = [] + Dynamic runways are any valid landing point that is a unit rather than a + map feature. These include carriers, ships with a helipad, and FARPs. + """ + self.dynamic_runways.append(runway) - def append_frequency(self, name: str, frequency: str): - self.freqs.append((name, frequency)) + def add_flight_description(self, flight: FlightData): + assert flight.client_units - def append_target(self, description: str, markpoint: str = None): - self.targets.append((description, markpoint)) - - def append_waypoint(self, description: str): - self.waypoints.append(description) - - def add_flight_description(self, flight): - - if flight.client_count <= 0: - return - - flight_unit_name = db.unit_type_name(flight.unit_type) + aircraft = flight.aircraft_type + flight_unit_name = db.unit_type_name(aircraft) self.description += "-" * 50 + "\n" - self.description += flight_unit_name + " x " + str(flight.count) + 2 * "\n" + self.description += f"{flight_unit_name} x {flight.size + 2}\n\n" - self.description += "#0 -- TAKEOFF : Take off from " + flight.from_cp.name + "\n" - for i, wpt in enumerate(flight.points): - self.description += "#" + str(1+i) + " -- " + wpt.name + " : " + wpt.description + "\n" - self.description += "#" + str(len(flight.points) + 1) + " -- RTB\n\n" + departure = flight.departure.airfield_name + self.description += f"#0 -- TAKEOFF : Take off from {departure}\n" + for i, wpt in enumerate(flight.waypoints): + self.description += f"#{i + 1} -- {wpt.name} : {wpt.description}\n" + self.description += f"#{len(flight.waypoints) + 1} -- RTB\n\n" - group = flight.group - if group is not None: - for i, nav_target in enumerate(group.nav_target_points): - self.description += nav_target.text_comment + "\n" - self.description += "\n" - self.description += "-" * 50 + "\n" - - def add_ally_flight_description(self, flight): - if flight.client_count == 0: - flight_unit_name = db.unit_type_name(flight.unit_type) - self.description += flight.flight_type.name + " " + flight_unit_name + " x " + str(flight.count) + ", departing in " + str(flight.scheduled_in) + " minutes \n" + def add_ally_flight_description(self, flight: FlightData): + assert not flight.client_units + aircraft = flight.aircraft_type + flight_unit_name = db.unit_type_name(aircraft) + self.description += ( + f"{flight.flight_type.name} {flight_unit_name} x {flight.size}, " + f"departing in {flight.departure_delay} minutes\n" + ) def generate(self): - self.description = "" self.description += "DCS Liberation turn #" + str(self.game.turn) + "\n" @@ -74,52 +142,50 @@ class BriefingGenerator: self.description += "Your flights:" + "\n" self.description += "=" * 15 + "\n\n" - for planner in self.game.planners.values(): - for flight in planner.flights: + for flight in self.flights: + if flight.client_units: self.add_flight_description(flight) self.description += "\n"*2 self.description += "Planned ally flights:" + "\n" self.description += "=" * 15 + "\n" - for planner in self.game.planners.values(): - if planner.from_cp.captured and len(planner.flights) > 0: - self.description += "\nFrom " + planner.from_cp.full_name + " \n" - self.description += "-" * 50 + "\n\n" - for flight in planner.flights: - self.add_ally_flight_description(flight) + allied_flights_by_departure = defaultdict(list) + for flight in self.flights: + if not flight.client_units and flight.friendly: + name = flight.departure.airfield_name + allied_flights_by_departure[name].append(flight) + for departure, flights in allied_flights_by_departure.items(): + self.description += f"\nFrom {departure}\n" + self.description += "-" * 50 + "\n\n" + for flight in flights: + self.add_ally_flight_description(flight) - if self.freqs: + if self.comms: self.description += "\n\nComms Frequencies:\n" self.description += "=" * 15 + "\n" - for name, freq in self.freqs: - self.description += "{}: {}\n".format(name, freq) + for comm_info in self.comms: + self.description += f"{comm_info.name}: {comm_info.freq}\n" self.description += ("-" * 50) + "\n" - for cp in self.game.theater.controlpoints: - if cp.captured and cp.cptype in [ControlPointType.LHA_GROUP, ControlPointType.AIRCRAFT_CARRIER_GROUP]: - self.description += cp.name + "\n" - self.description += "RADIO : 127.5 Mhz AM\n" - self.description += "TACAN : " - self.description += str(cp.tacanN) - if cp.tacanY: - self.description += "Y" - else: - self.description += "X" - self.description += " " + str(cp.tacanI) + "\n" - - if cp.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP and hasattr(cp, "icls"): - self.description += "ICLS Channel : " + str(cp.icls) + "\n" - self.description += "-" * 50 + "\n" + for runway in self.dynamic_runways: + self.description += f"{runway.airfield_name}\n" + self.description += f"RADIO : {runway.atc}\n" + if runway.tacan is not None: + self.description += f"TACAN : {runway.tacan} {runway.tacan_callsign}\n" + if runway.icls is not None: + self.description += f"ICLS Channel : {runway.icls}\n" + self.description += "-" * 50 + "\n" self.description += "JTACS [F-10 Menu] : \n" self.description += "===================\n\n" - for jtac in self.game.jtacs: - self.description += str(jtac[0]) + " -- Code : " + str(jtac[1]) + "\n" + for jtac in self.jtacs: + self.description += f"{jtac.region} -- Code : {jtac.code}\n" - self.m.set_description_text(self.description) + self.mission.set_description_text(self.description) - self.m.add_picture_blue(os.path.abspath("./resources/ui/splash_screen.png")) + self.mission.add_picture_blue(os.path.abspath( + "./resources/ui/splash_screen.png")) def generate_ongoing_war_text(self): diff --git a/gen/groundobjectsgen.py b/gen/groundobjectsgen.py index 6a12579e..3163aa3b 100644 --- a/gen/groundobjectsgen.py +++ b/gen/groundobjectsgen.py @@ -189,6 +189,7 @@ class GroundObjectsGenerator: "N/A", atc=atc_channel, tacan=tacan, + tacan_callsign=tacan_callsign, icls=icls_channel, ) diff --git a/gen/kneeboard.py b/gen/kneeboard.py index bb7bb4da..8383b392 100644 --- a/gen/kneeboard.py +++ b/gen/kneeboard.py @@ -23,7 +23,6 @@ only be added per airframe, so PvP missions where each side have the same aircraft will be able to see the enemy's kneeboard for the same airframe. """ from collections import defaultdict -from dataclasses import dataclass from pathlib import Path from typing import Dict, List, Optional, Tuple @@ -36,6 +35,7 @@ from . import units from .aircraft import FlightData from .airfields import RunwayData from .airsupportgen import AwacsInfo, TankerInfo +from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator from .radios import RadioFrequency @@ -95,21 +95,6 @@ class KneeboardPage: raise NotImplementedError -@dataclass -class CommInfo: - """Communications information for the kneeboard.""" - name: str - freq: RadioFrequency - - -@dataclass -class JtacInfo: - """JTAC information for the kneeboard.""" - callsign: str - region: str - code: str - - class BriefingPage(KneeboardPage): """A kneeboard page containing briefing information.""" def __init__(self, flight: FlightData, comms: List[CommInfo], @@ -208,57 +193,17 @@ class BriefingPage(KneeboardPage): return f"{channel.radio_name} Ch {channel.channel}" -class KneeboardGenerator: +class KneeboardGenerator(MissionInfoGenerator): """Creates kneeboard pages for each client flight in the mission.""" def __init__(self, mission: Mission) -> None: - self.mission = mission - self.comms: List[CommInfo] = [] - self.awacs: List[AwacsInfo] = [] - self.tankers: List[TankerInfo] = [] - self.jtacs: List[JtacInfo] = [] + super().__init__(mission) - def add_comm(self, name: str, freq: RadioFrequency) -> None: - """Adds communications info to the kneeboard. - - Args: - name: Name of the radio channel. - freq: Frequency of the radio channel. - """ - self.comms.append(CommInfo(name, freq)) - - def add_awacs(self, awacs: AwacsInfo) -> None: - """Adds an AWACS/GCI to the kneeboard. - - Args: - awacs: AWACS information. - """ - self.awacs.append(awacs) - - def add_tanker(self, tanker: TankerInfo) -> None: - """Adds a tanker to the kneeboard. - - Args: - tanker: Tanker information. - """ - self.tankers.append(tanker) - - def add_jtac(self, callsign: str, region: str, code: str) -> None: - """Adds a JTAC to the kneeboard. - - Args: - callsign: Callsign of the JTAC. - region: JTAC's area of responsibility. - code: Laser code used by the JTAC. - """ - # TODO: Radio info? Type? - self.jtacs.append(JtacInfo(callsign, region, code)) - - def generate(self, flights: List[FlightData]) -> None: + def generate(self) -> None: """Generates a kneeboard per client flight.""" temp_dir = Path("kneeboards") temp_dir.mkdir(exist_ok=True) - for aircraft, pages in self.pages_by_airframe(flights).items(): + for aircraft, pages in self.pages_by_airframe().items(): aircraft_dir = temp_dir / aircraft.id aircraft_dir.mkdir(exist_ok=True) for idx, page in enumerate(pages): @@ -266,7 +211,7 @@ class KneeboardGenerator: page.write(page_path) self.mission.add_aircraft_kneeboard(aircraft, page_path) - def pages_by_airframe(self, flights: List[FlightData]) -> Dict[FlyingType, List[KneeboardPage]]: + def pages_by_airframe(self) -> Dict[FlyingType, List[KneeboardPage]]: """Returns a list of kneeboard pages per airframe in the mission. Only client flights will be included, but because DCS does not support @@ -278,7 +223,7 @@ class KneeboardGenerator: that aircraft. """ all_flights: Dict[FlyingType, List[KneeboardPage]] = defaultdict(list) - for flight in flights: + for flight in self.flights: if not flight.client_units: continue all_flights[flight.aircraft_type].extend( From 0fc00fac38ae90740642fa98f72054b4c62a9477 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Wed, 2 Sep 2020 18:03:32 -0700 Subject: [PATCH 25/61] Pick ILS runways if possible. --- gen/aircraft.py | 23 ++++++++++++++++------- gen/airfields.py | 17 ++++++++++++++++- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/gen/aircraft.py b/gen/aircraft.py index 02451ab5..de0eb37c 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -344,13 +344,7 @@ class AircraftConflictGenerator: cp = flight.from_cp fallback_runway = RunwayData(cp.full_name, runway_name="") if cp.cptype == ControlPointType.AIRBASE: - # TODO: Implement logic for picking preferred runway. - runway = flight.from_cp.airport.runways[0] - runway_number = runway.heading // 10 - runway_side = ["", "L", "R"][runway.leftright] - runway_name = f"{runway_number:02}{runway_side}" - departure_runway = RunwayData.for_airfield( - flight.from_cp.airport, runway_name) + departure_runway = self.get_preferred_runway(flight.from_cp.airport) elif cp.is_fleet: departure_runway = dynamic_runways.get(cp.name, fallback_runway) else: @@ -380,6 +374,21 @@ class AircraftConflictGenerator: for unit in group.units: unit.fuel = Su_33.fuel_max * 0.8 + def get_preferred_runway(self, airport: Airport) -> RunwayData: + """Returns the preferred runway for the given airport. + + Right now we're only selecting runways based on whether or not they have + ILS, but we could also choose based on wind conditions, or which + direction flight plans should follow. + """ + runways = list(RunwayData.for_pydcs_airport(airport)) + for runway in runways: + # Prefer any runway with ILS. + if runway.ils is not None: + return runway + # Otherwise we lack the mission information to pick more usefully, + # so just use the first runway. + return runways[0] def _generate_at_airport(self, name: str, side: Country, unit_type: FlyingType, count: int, client_count: int, airport: Airport = None, start_type = None) -> FlyingGroup: assert count > 0 diff --git a/gen/airfields.py b/gen/airfields.py index e4c5f1fb..c75c5ff4 100644 --- a/gen/airfields.py +++ b/gen/airfields.py @@ -5,7 +5,7 @@ data added to pydcs. Until then, missing data can be manually filled in here. """ from dataclasses import dataclass, field import logging -from typing import Dict, Optional, Tuple +from typing import Dict, Iterator, Optional, Tuple from pydcs.dcs.terrain.terrain import Airport from .radios import MHz, RadioFrequency @@ -677,3 +677,18 @@ class RunwayData: tacan, ils ) + + @classmethod + def for_pydcs_airport(cls, airport: Airport) -> Iterator["RunwayData"]: + for runway in airport.runways: + runway_number = runway.heading // 10 + runway_side = ["", "L", "R"][runway.leftright] + runway_name = f"{runway_number:02}{runway_side}" + yield cls.for_airfield(airport, runway_name) + + # pydcs only exposes one runway per physical runway, so to expose + # both sides of the runway we need to generate the other. + runway_number = ((runway.heading + 180) % 360) // 10 + runway_side = ["", "R", "L"][runway.leftright] + runway_name = f"{runway_number:02}{runway_side}" + yield cls.for_airfield(airport, runway_name) From 4b74b5a13d393dd0ed2cdbc3151839bb74a75036 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sun, 6 Sep 2020 01:08:56 -0700 Subject: [PATCH 26/61] Fix incorrect pydcs import paths. I've been wrongly importing these from `pydcs.dcs` instead of just `dcs`, because that was what PyCharm thought they were. These will all be broken when we get back to using a real pydcs instead of relying on its directory being in our tree. This page in the wiki should be updated: https://github.com/Khopa/dcs_liberation/wiki/Developer's-Guide Instead of recommending that `PYTHONPATH` be updated in the run configuration, it should instead recommend that Settings -> Project: dcs_liberation -> Project Structure be set to exclude the pydcs directory from the dcs_liberation content root, and add the pydcs directory as a *separate* content root. Alternatively, we could recommend that configure a virtualenv (good advice anyway, and pycharm knows how to set them up) that have people run `pip install -e pydcs`. I think even easier would be switching from the virtualenv-style requirements.txt to pipenv, which can actually encode the `-e` style pip install into its equivalent of requirements.txt. --- game/operation/operation.py | 6 +++--- gen/aircraft.py | 30 ++++++++---------------------- gen/airfields.py | 2 +- gen/briefinggen.py | 2 +- gen/flights/flight.py | 2 +- gen/groundobjectsgen.py | 12 +++--------- gen/kneeboard.py | 4 ++-- 7 files changed, 19 insertions(+), 39 deletions(-) diff --git a/game/operation/operation.py b/game/operation/operation.py index f87c13e3..46aedc3c 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -5,9 +5,9 @@ from gen.airfields import AIRFIELD_DATA from gen.beacons import load_beacons_for_terrain from gen.radios import RadioRegistry from gen.tacan import TacanRegistry -from pydcs.dcs.countries import country_dict -from pydcs.dcs.lua.parse import loads -from pydcs.dcs.terrain.terrain import Terrain +from dcs.countries import country_dict +from dcs.lua.parse import loads +from dcs.terrain.terrain import Terrain from userdata.debriefing import * diff --git a/gen/aircraft.py b/gen/aircraft.py index 02451ab5..0e28217d 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -1,5 +1,12 @@ from dataclasses import dataclass -from typing import Dict, List, Optional, Tuple + +from dcs import helicopters +from dcs.action import ActivateGroup, AITaskPush, MessageToAll +from dcs.condition import TimeAfter, CoalitionHasAirdrome, PartOfCoalitionInZone +from dcs.flyingunit import FlyingUnit +from dcs.helicopters import helicopter_map, UH_1H +from dcs.terrain.terrain import Airport, NoParkingSlotError +from dcs.triggers import TriggerOnce, Event from game.data.cap_capabilities_db import GUNFIGHTERS from game.settings import Settings @@ -13,27 +20,6 @@ from gen.flights.flight import ( FlightWaypointType, ) from gen.radios import get_radio, MHz, Radio, RadioFrequency, RadioRegistry -from pydcs.dcs import helicopters -from pydcs.dcs.action import ActivateGroup, AITaskPush, MessageToAll -from pydcs.dcs.condition import TimeAfter, CoalitionHasAirdrome, PartOfCoalitionInZone -from pydcs.dcs.flyingunit import FlyingUnit -from pydcs.dcs.helicopters import helicopter_map, UH_1H -from pydcs.dcs.mission import Mission, StartType -from pydcs.dcs.planes import ( - Bf_109K_4, - FW_190A8, - FW_190D9, - I_16, - Ju_88A4, - P_47D_30, - P_51D, - P_51D_30_NA, - SpitfireLFMkIX, - SpitfireLFMkIXCW, -) -from pydcs.dcs.terrain.terrain import Airport, NoParkingSlotError -from pydcs.dcs.triggers import TriggerOnce, Event -from pydcs.dcs.unittype import FlyingType, UnitType from .conflictgen import * from .naming import * diff --git a/gen/airfields.py b/gen/airfields.py index e4c5f1fb..45d3b63b 100644 --- a/gen/airfields.py +++ b/gen/airfields.py @@ -7,7 +7,7 @@ from dataclasses import dataclass, field import logging from typing import Dict, Optional, Tuple -from pydcs.dcs.terrain.terrain import Airport +from dcs.terrain.terrain import Airport from .radios import MHz, RadioFrequency from .tacan import TacanBand, TacanChannel diff --git a/gen/briefinggen.py b/gen/briefinggen.py index 9eed3af2..0b494ac5 100644 --- a/gen/briefinggen.py +++ b/gen/briefinggen.py @@ -5,7 +5,7 @@ import random from typing import List, Tuple from game import db -from pydcs.dcs.mission import Mission +from dcs.mission import Mission from .aircraft import FlightData from .airfields import RunwayData from .airsupportgen import AwacsInfo, TankerInfo diff --git a/gen/flights/flight.py b/gen/flights/flight.py index a9b5e9f8..6e2522f5 100644 --- a/gen/flights/flight.py +++ b/gen/flights/flight.py @@ -2,7 +2,7 @@ from enum import Enum from typing import List from game import db -from pydcs.dcs.unittype import UnitType +from dcs.unittype import UnitType class FlightType(Enum): diff --git a/gen/groundobjectsgen.py b/gen/groundobjectsgen.py index 3163aa3b..d1cd5378 100644 --- a/gen/groundobjectsgen.py +++ b/gen/groundobjectsgen.py @@ -1,14 +1,8 @@ +from dcs.statics import * +from dcs.unit import Ship, Vehicle + from game.data.building_data import FORTIFICATION_UNITS_ID, FORTIFICATION_UNITS from game.db import unit_type_from_name -from pydcs.dcs.mission import * -from pydcs.dcs.statics import * -from pydcs.dcs.task import ( - ActivateBeaconCommand, - ActivateICLSCommand, - OptAlarmState, -) -from pydcs.dcs.unit import Ship, Vehicle -from pydcs.dcs.unitgroup import StaticGroup from .airfields import RunwayData from .conflictgen import * from .naming import * diff --git a/gen/kneeboard.py b/gen/kneeboard.py index 8383b392..4bc5d64d 100644 --- a/gen/kneeboard.py +++ b/gen/kneeboard.py @@ -29,8 +29,8 @@ from typing import Dict, List, Optional, Tuple from PIL import Image, ImageDraw, ImageFont from tabulate import tabulate -from pydcs.dcs.mission import Mission -from pydcs.dcs.unittype import FlyingType +from dcs.mission import Mission +from dcs.unittype import FlyingType from . import units from .aircraft import FlightData from .airfields import RunwayData From 2969ce2d567a8eac86162a70d53fe2949cf2ed5c Mon Sep 17 00:00:00 2001 From: Khopa Date: Sun, 6 Sep 2020 12:11:03 +0200 Subject: [PATCH 27/61] Removed unused file --- gen/flights/radio_generator.py | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 gen/flights/radio_generator.py diff --git a/gen/flights/radio_generator.py b/gen/flights/radio_generator.py deleted file mode 100644 index 1e647287..00000000 --- a/gen/flights/radio_generator.py +++ /dev/null @@ -1,4 +0,0 @@ -from dcs.unitgroup import FlyingGroup - - - From 3c96c1d5b1ea7f4839c9a7683c020043fc164de4 Mon Sep 17 00:00:00 2001 From: Khopa Date: Sun, 6 Sep 2020 12:33:40 +0200 Subject: [PATCH 28/61] Removed incorrect imports causing pydcs being imported twice; --- game/data/building_data.py | 2 +- gen/conflictgen.py | 2 +- qt_ui/main.py | 2 +- resources/tools/generate_loadout_check.py | 2 +- theater/conflicttheater.py | 2 +- userdata/liberation_install.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/game/data/building_data.py b/game/data/building_data.py index f88415ce..bd6ab666 100644 --- a/game/data/building_data.py +++ b/game/data/building_data.py @@ -1,5 +1,5 @@ import inspect -from pydcs import dcs +import dcs DEFAULT_AVAILABLE_BUILDINGS = ['fuel', 'ammo', 'comms', 'oil', 'ware', 'farp', 'fob', 'power', 'factory', 'derrick', 'aa'] diff --git a/gen/conflictgen.py b/gen/conflictgen.py index 1a5dabdb..9b83b51e 100644 --- a/gen/conflictgen.py +++ b/gen/conflictgen.py @@ -1,7 +1,7 @@ import logging import typing import pdb -from pydcs import dcs +import dcs from random import randint from dcs import Mission diff --git a/qt_ui/main.py b/qt_ui/main.py index dff7e51b..e019d32c 100644 --- a/qt_ui/main.py +++ b/qt_ui/main.py @@ -4,7 +4,7 @@ import logging import os import sys -from pydcs import dcs +import dcs from PySide2 import QtWidgets from PySide2.QtGui import QPixmap from PySide2.QtWidgets import QApplication, QSplashScreen diff --git a/resources/tools/generate_loadout_check.py b/resources/tools/generate_loadout_check.py index 165da58f..09835265 100644 --- a/resources/tools/generate_loadout_check.py +++ b/resources/tools/generate_loadout_check.py @@ -1,6 +1,6 @@ import os import sys -from pydcs import dcs +import dcs from game import db from gen.aircraft import AircraftConflictGenerator diff --git a/theater/conflicttheater.py b/theater/conflicttheater.py index 2934f15e..621f106a 100644 --- a/theater/conflicttheater.py +++ b/theater/conflicttheater.py @@ -1,6 +1,6 @@ import typing -from pydcs import dcs +import dcs from dcs.mapping import Point from .controlpoint import ControlPoint diff --git a/userdata/liberation_install.py b/userdata/liberation_install.py index 440fe29f..5f19ec0a 100644 --- a/userdata/liberation_install.py +++ b/userdata/liberation_install.py @@ -2,7 +2,7 @@ import json import os from shutil import copyfile -from pydcs import dcs +import dcs from userdata import persistency From b6938c14ca7bcdb60a212a95623b0bc64144e073 Mon Sep 17 00:00:00 2001 From: Khopa Date: Sun, 6 Sep 2020 15:09:40 +0200 Subject: [PATCH 29/61] Added southern airfields of PG map --- gen/airfields.py | 51 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/gen/airfields.py b/gen/airfields.py index e8b429e2..1e881a98 100644 --- a/gen/airfields.py +++ b/gen/airfields.py @@ -403,6 +403,57 @@ AIRFIELD_DATA = { ), # TODO : PERSIAN GULF MAP + "Liwa Airbase": AirfieldData( + theater="Persian Gulf", + icao="OMLW", + elevation=400, + runway_length=10768, + atc=AtcData(MHz(4, 175), MHz(39, 250), MHz(119, 300), MHz(250, 850)), + ), + + "Al Dhafra AB": AirfieldData( + theater="Persian Gulf", + icao="OMAM", + elevation=52, + runway_length=11530, + tacan=TacanChannel(96, TacanBand.X), + tacan_callsign="MA", + vor=("MA", MHz(114, 90)), + atc=AtcData(MHz(4, 250), MHz(39, 400), MHz(126, 500), MHz(251, 000)), + ils={ + "13": ("MMA", MHz(111, 10)), + "31": ("IMA", MHz(109, 10)), + }, + ), + + "Al-Bateen Airport": AirfieldData( + theater="Persian Gulf", + icao="OMAD", + elevation=11, + runway_length=6808, + vor=("ALB", MHz(114, 00)), + atc=AtcData(MHz(4, 25), MHz(38, 950), MHz(119, 900), MHz(250, 550)), + ), + + "Sas Al Nakheel Airport": AirfieldData( + theater="Persian Gulf", + icao="OMNK", + elevation=9, + runway_length=5387, + vor=("SAS", MHz(128, 93)), + atc=AtcData(MHz(3, 975), MHz(38, 850), MHz(128, 900), MHz(250, 450)), + ), + + "Abu Dhabi International Airport": AirfieldData( + theater="Persian Gulf", + icao="OMAA", + elevation=91, + runway_length=12817, + vor=("ADV", MHz(114, 25)), + atc=AtcData(MHz(4, 000), MHz(38, 900), MHz(119, 200), MHz(250, 500)), + ), + # TODO : finish persian gulf map + # TODO : SYRIA MAP "Incirlik": AirfieldData( From 14fe68eb54ed55daee426bfd456429dc0d8402a8 Mon Sep 17 00:00:00 2001 From: Khopa Date: Sun, 6 Sep 2020 15:10:10 +0200 Subject: [PATCH 30/61] Added Mirage 2000 radios --- gen/radios.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gen/radios.py b/gen/radios.py index 2cfeac8a..21faedec 100644 --- a/gen/radios.py +++ b/gen/radios.py @@ -101,6 +101,10 @@ RADIOS: List[Radio] = [ Radio("SCR-522", MHz(100), MHz(156), step=MHz(1)), Radio("A.R.I. 1063", MHz(100), MHz(156), step=MHz(1)), Radio("BC-1206", kHz(200), kHz(400), step=kHz(10)), + + # Note : The M2000C V/UHF radio has a gap between 149.970 and 225.000Mhz + Radio("TRT ERA 7000 V/UHF", MHz(118), MHz(400), step=MHz(1)), + Radio("TRT ERA 7200 UHF", MHz(225), MHz(400), step=MHz(1)) ] From 98d75bc72129ea85dc537fbebbd95b84cda739de Mon Sep 17 00:00:00 2001 From: Khopa Date: Sun, 6 Sep 2020 15:43:44 +0200 Subject: [PATCH 31/61] Tomcat and M2000 radios support --- game/operation/operation.py | 2 +- gen/aircraft.py | 30 +++++++++++++++++++++++++----- gen/radios.py | 9 ++++++++- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/game/operation/operation.py b/game/operation/operation.py index 46aedc3c..7e75a305 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -326,7 +326,7 @@ class Operation: last_channel = flight.num_radio_channels(radio_id) channel_alloc = iter(range(first_channel, last_channel + 1)) - flight.assign_channel(radio_id, next(channel_alloc),flight.departure.atc) + flight.assign_channel(radio_id, next(channel_alloc), flight.departure.atc) # TODO: If there ever are multiple AWACS, limit to mission relevant. for awacs in self.airsupportgen.air_support.awacs: diff --git a/gen/aircraft.py b/gen/aircraft.py index bc5f7c6c..7ff441b9 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -49,8 +49,8 @@ UHF_FALLBACK_CHANNEL = MHz(251) class AircraftData: """Additional aircraft data not exposed by pydcs.""" - #: The type of radio used for intra-flight communications. - intra_flight_radio: Radio + #: The type of radios used by this plane + radios: List[Radio] #: Index of the radio used for intra-flight communications. Matches the #: index of the panel_radio field of the pydcs.dcs.planes object. @@ -60,25 +60,33 @@ class AircraftData: #: index of the panel_radio field of the pydcs.dcs.planes object. intra_flight_radio_index: Optional[int] + @property + def intra_flight_radio(self): + return self.radios[self.intra_flight_radio_index-1] + + @property + def inter_flight_radio(self): + return self.radios[self.inter_flight_radio_index-1] + # Indexed by the id field of the pydcs PlaneType. AIRCRAFT_DATA: Dict[str, AircraftData] = { "A-10C": AircraftData( - get_radio("AN/ARC-186(V) AM"), + [get_radio("AN/ARC-186(V) AM")], # The A-10's radio works differently than most aircraft. Doesn't seem to # be a way to set these from the mission editor, let alone pydcs. inter_flight_radio_index=None, intra_flight_radio_index=None ), "F-16C_50": AircraftData( - get_radio("AN/ARC-222"), + [get_radio("AN/ARC-164"), get_radio("AN/ARC-222")], # COM2 is the AN/ARC-222, which is the VHF radio we want to use for # intra-flight communication to leave COM1 open for UHF inter-flight. inter_flight_radio_index=1, intra_flight_radio_index=2 ), "FA-18C_hornet": AircraftData( - get_radio("AN/ARC-210"), + [get_radio("AN/ARC-210"), get_radio("AN/ARC-210")], # DCS will clobber channel 1 of the first radio compatible with the # flight's assigned frequency. Since the F/A-18's two radios are both # AN/ARC-210s, radio 1 will be compatible regardless of which frequency @@ -86,6 +94,18 @@ AIRCRAFT_DATA: Dict[str, AircraftData] = { inter_flight_radio_index=2, intra_flight_radio_index=1 ), + + "M-2000C": AircraftData( + [get_radio("TRT ERA 7000 V/UHF"), get_radio("TRT ERA 7200 UHF")], + inter_flight_radio_index=1, + intra_flight_radio_index=2 + ), + + "F-14B": AircraftData( + [get_radio("AN/ARC-159"), get_radio("AN/ARC-182")], + inter_flight_radio_index=1, + intra_flight_radio_index=2 + ) } diff --git a/gen/radios.py b/gen/radios.py index 21faedec..54b0a098 100644 --- a/gen/radios.py +++ b/gen/radios.py @@ -104,7 +104,14 @@ RADIOS: List[Radio] = [ # Note : The M2000C V/UHF radio has a gap between 149.970 and 225.000Mhz Radio("TRT ERA 7000 V/UHF", MHz(118), MHz(400), step=MHz(1)), - Radio("TRT ERA 7200 UHF", MHz(225), MHz(400), step=MHz(1)) + Radio("TRT ERA 7200 UHF", MHz(225), MHz(400), step=MHz(1)), + + # Tomcat radios + # # https://www.heatblur.se/F-14Manual/general.html#an-arc-159-uhf-1-radio + Radio("AN/ARC-159", MHz(225), MHz(399, 975), step=MHz(1)), + # AN/ARC-182, can operate : 30 to 88, 108 to 156, 156 to 174, and 225 to 399.975 MHz + # https://www.heatblur.se/F-14Manual/general.html#an-arc-182-v-uhf-2-radio + Radio("AN/ARC-182", MHz(108), MHz(399, 975), step=MHz(1)) ] From ad42a3d9561909787cb301b16190517d10eadc03 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sun, 6 Sep 2020 16:40:07 -0700 Subject: [PATCH 32/61] Fix radio information. Not every aircraft has a pydcs radio index, so we can't use that to index into a list. Any mission with an A-10C crashes, since it would try to use `None - 1` to index into the list of radios to find the intra-flight radio. Also fix the radio ranges for the newly added radios. The current implementation can't model gaps, so extending the radio ranges across those gaps means that we might allocate channels that aren't tunable by those radios. Additionally, the end frequency is exclusive rather than inclusive, so fix the ranges to include that last tunable frequency. --- gen/aircraft.py | 30 +++++++++++++++--------------- gen/radios.py | 14 +++++++++----- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/gen/aircraft.py b/gen/aircraft.py index 7ff441b9..82062b6e 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -49,8 +49,11 @@ UHF_FALLBACK_CHANNEL = MHz(251) class AircraftData: """Additional aircraft data not exposed by pydcs.""" - #: The type of radios used by this plane - radios: List[Radio] + #: The type of radio used for inter-flight communications. + inter_flight_radio: Radio + + #: The type of radio used for intra-flight communications. + intra_flight_radio: Radio #: Index of the radio used for intra-flight communications. Matches the #: index of the panel_radio field of the pydcs.dcs.planes object. @@ -60,33 +63,28 @@ class AircraftData: #: index of the panel_radio field of the pydcs.dcs.planes object. intra_flight_radio_index: Optional[int] - @property - def intra_flight_radio(self): - return self.radios[self.intra_flight_radio_index-1] - - @property - def inter_flight_radio(self): - return self.radios[self.inter_flight_radio_index-1] - # Indexed by the id field of the pydcs PlaneType. AIRCRAFT_DATA: Dict[str, AircraftData] = { "A-10C": AircraftData( - [get_radio("AN/ARC-186(V) AM")], + inter_flight_radio=get_radio("AN/ARC-164"), + intra_flight_radio=get_radio("AN/ARC-186(V) AM"), # The A-10's radio works differently than most aircraft. Doesn't seem to # be a way to set these from the mission editor, let alone pydcs. inter_flight_radio_index=None, intra_flight_radio_index=None ), "F-16C_50": AircraftData( - [get_radio("AN/ARC-164"), get_radio("AN/ARC-222")], + inter_flight_radio=get_radio("AN/ARC-164"), + intra_flight_radio=get_radio("AN/ARC-222"), # COM2 is the AN/ARC-222, which is the VHF radio we want to use for # intra-flight communication to leave COM1 open for UHF inter-flight. inter_flight_radio_index=1, intra_flight_radio_index=2 ), "FA-18C_hornet": AircraftData( - [get_radio("AN/ARC-210"), get_radio("AN/ARC-210")], + inter_flight_radio=get_radio("AN/ARC-210"), + intra_flight_radio=get_radio("AN/ARC-210"), # DCS will clobber channel 1 of the first radio compatible with the # flight's assigned frequency. Since the F/A-18's two radios are both # AN/ARC-210s, radio 1 will be compatible regardless of which frequency @@ -96,13 +94,15 @@ AIRCRAFT_DATA: Dict[str, AircraftData] = { ), "M-2000C": AircraftData( - [get_radio("TRT ERA 7000 V/UHF"), get_radio("TRT ERA 7200 UHF")], + inter_flight_radio=get_radio("TRT ERA 7000 V/UHF"), + intra_flight_radio=get_radio("TRT ERA 7200 UHF"), inter_flight_radio_index=1, intra_flight_radio_index=2 ), "F-14B": AircraftData( - [get_radio("AN/ARC-159"), get_radio("AN/ARC-182")], + inter_flight_radio=get_radio("AN/ARC-159"), + intra_flight_radio=get_radio("AN/ARC-182"), inter_flight_radio_index=1, intra_flight_radio_index=2 ) diff --git a/gen/radios.py b/gen/radios.py index 54b0a098..62790a4c 100644 --- a/gen/radios.py +++ b/gen/radios.py @@ -102,16 +102,20 @@ RADIOS: List[Radio] = [ Radio("A.R.I. 1063", MHz(100), MHz(156), step=MHz(1)), Radio("BC-1206", kHz(200), kHz(400), step=kHz(10)), - # Note : The M2000C V/UHF radio has a gap between 149.970 and 225.000Mhz - Radio("TRT ERA 7000 V/UHF", MHz(118), MHz(400), step=MHz(1)), + # Note: The M2000C V/UHF can operate in both ranges, but has a gap between + # 150 MHz and 225 MHz. We can't allocate in that gap, and the current + # system doesn't model gaps, so just pretend it ends at 150 MHz for now. We + # can model gaps later if needed. + Radio("TRT ERA 7000 V/UHF", MHz(118), MHz(150), step=MHz(1)), Radio("TRT ERA 7200 UHF", MHz(225), MHz(400), step=MHz(1)), # Tomcat radios # # https://www.heatblur.se/F-14Manual/general.html#an-arc-159-uhf-1-radio - Radio("AN/ARC-159", MHz(225), MHz(399, 975), step=MHz(1)), - # AN/ARC-182, can operate : 30 to 88, 108 to 156, 156 to 174, and 225 to 399.975 MHz + Radio("AN/ARC-159", MHz(225), MHz(400), step=MHz(1)), + # AN/ARC-182 can also operate from 30 MHz to 88 MHz, as well as from 225 MHz + # to 400 MHz range, but we can't model gaps with the current implementation. # https://www.heatblur.se/F-14Manual/general.html#an-arc-182-v-uhf-2-radio - Radio("AN/ARC-182", MHz(108), MHz(399, 975), step=MHz(1)) + Radio("AN/ARC-182", MHz(108), MHz(174), step=MHz(1)), ] From ebedc02a0a8b9a6785033b42eb0b6da5df4d3a6b Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sun, 6 Sep 2020 03:15:17 -0700 Subject: [PATCH 33/61] Add first waypoint to FlightData. The first waypoint is automatically added by pydcs, so it's not actually in our waypoint list from the flight planner. Import is from the group so it shows up in the kneeboard. --- gen/aircraft.py | 6 +++++- gen/briefinggen.py | 2 -- gen/flights/flight.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/gen/aircraft.py b/gen/aircraft.py index 82062b6e..4ae1a80a 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -357,6 +357,10 @@ class AircraftConflictGenerator: logging.warning(f"Unhandled departure control point: {cp.cptype}") departure_runway = fallback_runway + # The first waypoint is set automatically by pydcs, so it's not in our + # list. Convert the pydcs MovingPoint to a FlightWaypoint so it shows up + # in our FlightData. + first_point = FlightWaypoint.from_pydcs(group.points[0], flight.from_cp) self.flights.append(FlightData( flight_type=flight.flight_type, units=group.units, @@ -367,7 +371,7 @@ class AircraftConflictGenerator: arrival=departure_runway, # TODO: Support for divert airfields. divert=None, - waypoints=flight.points, + waypoints=[first_point] + flight.points, intra_flight_channel=channel )) diff --git a/gen/briefinggen.py b/gen/briefinggen.py index 0b494ac5..91d16de3 100644 --- a/gen/briefinggen.py +++ b/gen/briefinggen.py @@ -115,8 +115,6 @@ class BriefingGenerator(MissionInfoGenerator): self.description += "-" * 50 + "\n" self.description += f"{flight_unit_name} x {flight.size + 2}\n\n" - departure = flight.departure.airfield_name - self.description += f"#0 -- TAKEOFF : Take off from {departure}\n" for i, wpt in enumerate(flight.waypoints): self.description += f"#{i + 1} -- {wpt.name} : {wpt.description}\n" self.description += f"#{len(flight.waypoints) + 1} -- RTB\n\n" diff --git a/gen/flights/flight.py b/gen/flights/flight.py index 6e2522f5..d00b4783 100644 --- a/gen/flights/flight.py +++ b/gen/flights/flight.py @@ -3,6 +3,8 @@ from typing import List from game import db from dcs.unittype import UnitType +from dcs.point import MovingPoint, PointAction +from theater.controlpoint import ControlPoint class FlightType(Enum): @@ -77,6 +79,33 @@ class FlightWaypoint: self.data = None + @classmethod + def from_pydcs(cls, point: MovingPoint, + from_cp: ControlPoint) -> "FlightWaypoint": + waypoint = FlightWaypoint(point.position.x, point.position.y, + point.alt) + waypoint.alt_type = point.alt_type + # Other actions exist... but none of them *should* be the first + # waypoint for a flight. + waypoint.waypoint_type = { + PointAction.TurningPoint: FlightWaypointType.NAV, + PointAction.FlyOverPoint: FlightWaypointType.NAV, + PointAction.FromParkingArea: FlightWaypointType.TAKEOFF, + PointAction.FromParkingAreaHot: FlightWaypointType.TAKEOFF, + PointAction.FromRunway: FlightWaypointType.TAKEOFF, + }[point.action] + if waypoint.waypoint_type == FlightWaypointType.NAV: + waypoint.name = "NAV" + waypoint.pretty_name = "Nav" + waypoint.description = "Nav" + else: + waypoint.name = "TAKEOFF" + waypoint.pretty_name = "Takeoff" + waypoint.description = "Takeoff" + waypoint.description = f"Takeoff from {from_cp.name}" + return waypoint + + class Flight: unit_type: UnitType = None from_cp = None From 8a7e43ef428f9bd64cadc9485c195badb8a15dba Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 8 Sep 2020 16:43:47 -0700 Subject: [PATCH 34/61] Also dedup ATC frequencies. Some airports on the Syria map share ATC frequencies. --- game/operation/operation.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/game/operation/operation.py b/game/operation/operation.py index 7e75a305..d8d63de6 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -127,30 +127,31 @@ class Operation: self.defenders_starting_position = self.to_cp.at def generate(self): - # Dedup beacon frequencies, since some maps have more than one beacon - # per frequency. + # Dedup beacon/radio frequencies, since some maps have some frequencies + # used multiple times. beacons = load_beacons_for_terrain(self.game.theater.terrain.name) - unique_beacon_frequencies: Set[RadioFrequency] = set() + unique_map_frequencies: Set[RadioFrequency] = set() for beacon in beacons: - unique_beacon_frequencies.add(beacon.frequency) + unique_map_frequencies.add(beacon.frequency) if beacon.is_tacan: if beacon.channel is None: logging.error( f"TACAN beacon has no channel: {beacon.callsign}") else: self.tacan_registry.reserve(beacon.tacan_channel) - for frequency in unique_beacon_frequencies: - self.radio_registry.reserve(frequency) for airfield, data in AIRFIELD_DATA.items(): if data.theater == self.game.theater.terrain.name: - self.radio_registry.reserve(data.atc.hf) - self.radio_registry.reserve(data.atc.vhf_fm) - self.radio_registry.reserve(data.atc.vhf_am) - self.radio_registry.reserve(data.atc.uhf) + unique_map_frequencies.add(data.atc.hf) + unique_map_frequencies.add(data.atc.vhf_fm) + unique_map_frequencies.add(data.atc.vhf_am) + unique_map_frequencies.add(data.atc.uhf) # No need to reserve ILS or TACAN because those are in the # beacon list. + for frequency in unique_map_frequencies: + self.radio_registry.reserve(frequency) + # Generate meteo if self.environment_settings is None: self.environment_settings = self.envgen.generate() From 474b6065248f138b31ba972e6e7d431b7e725cdc Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 8 Sep 2020 16:40:59 -0700 Subject: [PATCH 35/61] Add map data for Syria. --- gen/airfields.py | 279 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 279 insertions(+) diff --git a/gen/airfields.py b/gen/airfields.py index 1e881a98..c8464f0b 100644 --- a/gen/airfields.py +++ b/gen/airfields.py @@ -456,6 +456,18 @@ AIRFIELD_DATA = { # TODO : SYRIA MAP + "Adana Sakirpasa": AirfieldData( + theater="Syria", + icao="LTAF", + elevation=55, + runway_length=8115, + vor=("ADA", MHz(112, 700)), + atc=AtcData(MHz(4, 225), MHz(39, 350), MHz(121, 100), MHz(250, 900)), + ils={ + "05": ("IADA", MHz(108, 700)), + }, + ), + "Incirlik": AirfieldData( theater="Syria", icao="LTAG", @@ -471,6 +483,273 @@ AIRFIELD_DATA = { }, ), + "Minakh": AirfieldData( + theater="Syria", + icao="OS71", + elevation=1614, + runway_length=4648, + atc=AtcData(MHz(4, 125), MHz(39, 150), MHz(120, 600), MHz(250, 700)), + ), + + "Hatay": AirfieldData( + theater="Syria", + icao="LTDA", + elevation=253, + runway_length=9052, + vor=("HTY", MHz(112, 500)), + atc=AtcData(MHz(3, 825), MHz(38, 550), MHz(128, 500), MHz(250, 150)), + ils={ + "22": ("IHTY", MHz(108, 150)), + "04": ("IHAT", MHz(108, 900)), + }, + ), + + "Kuweires": AirfieldData( + theater="Syria", + icao="OS66", + elevation=1200, + runway_length=6662, + atc=AtcData(MHz(4, 275), MHz(39, 450), MHz(120, 500), MHz(251)), + ), + + "Aleppo": AirfieldData( + theater="Syria", + icao="OSAP", + elevation=1253, + runway_length=8332, + atc=AtcData(MHz(4, 150), MHz(39, 200), MHz(119, 100), MHz(250, 750)), + ils={ + "50": ("IDAN", MHz(109, 300)), + "23": ("DANM", MHz(111, 700)), + }, + ), + + "Jirah": AirfieldData( + theater="Syria", + icao="OS62", + elevation=1170, + runway_length=9090, + atc=AtcData(MHz(3, 875), MHz(38, 650), MHz(118, 100), MHz(250, 200)), + ), + + "Taftanaz": AirfieldData( + theater="Syria", + elevation=1020, + runway_length=2705, + atc=AtcData(MHz(4, 375), MHz(39, 650), MHz(122, 800), MHz(251, 200)), + ), + + "Tabqa": AirfieldData( + theater="Syria", + icao="OS59", + elevation=1083, + runway_length=9036, + atc=AtcData(MHz(4, 350), MHz(39, 600), MHz(118, 500), MHz(251, 150)), + ), + + "Abu al-Dahur": AirfieldData( + theater="Syria", + icao="OS57", + elevation=820, + runway_length=8728, + atc=AtcData(MHz(3, 950), MHz(38, 800), MHz(122, 200), MHz(250, 350)), + ), + + "Bassel Al-Assad": AirfieldData( + theater="Syria", + icao="OSLK", + elevation=93, + runway_length=7305, + vor=("LTK", MHz(114, 800)), + atc=AtcData(MHz(4), MHz(38, 900), MHz(118, 100), MHz(250, 450)), + ils={ + "17": ("IBA", MHz(109, 100)), + }, + ), + + "Hama": AirfieldData( + theater="Syria", + icao="OS58", + elevation=983, + runway_length=7957, + atc=AtcData(MHz(3, 800), MHz(38, 500), MHz(118, 50), MHz(250, 100)), + ), + + "Rene Mouawad": AirfieldData( + theater="Syria", + icao="OLKA", + elevation=14, + runway_length=8614, + atc=AtcData(MHz(4, 325), MHz(39, 550), MHz(129, 500), MHz(251, 100)), + ), + + "Al Quasayr": AirfieldData( + theater="Syria", + icao="OS70", + elevation=1729, + runway_length=8585, + atc=AtcData(MHz(4, 400), MHz(39, 700), MHz(119, 200), MHz(251, 250)), + ), + + "Palmyra": AirfieldData( + theater="Syria", + icao="OSPR", + elevation=1267, + runway_length=8704, + atc=AtcData(MHz(4, 175), MHz(39, 250), MHz(121, 900), MHz(250, 800)), + ), + + "Wujah Al Hajar": AirfieldData( + theater="Syria", + icao="Z19O", + elevation=619, + runway_length=4717, + vor=("CAK", MHz(116, 200)), + atc=AtcData(MHz(4, 425), MHz(39, 750), MHz(121, 500), MHz(251, 300)), + ), + + "An Nasiriyah": AirfieldData( + theater="Syria", + icao="OS64", + elevation=2746, + runway_length=8172, + atc=AtcData(MHz(4, 450), MHz(39, 800), MHz(122, 300), MHz(251, 350)), + ), + + "Rayak": AirfieldData( + theater="Syria", + icao="OLRA", + elevation=2934, + runway_length=8699, + vor=("HTY", MHz(124, 400)), + atc=AtcData(MHz(4, 300), MHz(39, 500), MHz(124, 400), MHz(251, 50)), + ), + + "Beirut-Rafic Hariri": AirfieldData( + theater="Syria", + icao="OLBA", + elevation=39, + runway_length=9463, + vor=("KAD", MHz(112, 600)), + atc=AtcData(MHz(4, 475), MHz(39, 850), MHz(118, 900), MHz(251, 400)), + ils={ + "17": ("BIL", MHz(109, 500)), + }, + ), + + "Al-Dumayr": AirfieldData( + theater="Syria", + icao="OS61", + elevation=2066, + runway_length=8902, + atc=AtcData(MHz(4, 550), MHz(40), MHz(120, 300), MHz(251, 550)), + ), + + "Marj as Sultan North": AirfieldData( + theater="Syria", + elevation=2007, + runway_length=268, + atc=AtcData(MHz(4, 25), MHz(38, 950), MHz(122, 700), MHz(250, 500)), + ), + + "Marj as Sultan South": AirfieldData( + theater="Syria", + elevation=2007, + runway_length=166, + atc=AtcData(MHz(4, 525), MHz(39, 950), MHz(122, 900), MHz(251, 500)), + ), + + "Mezzeh": AirfieldData( + theater="Syria", + icao="OS67", + elevation=2355, + runway_length=7522, + atc=AtcData(MHz(4, 100), MHz(39, 100), MHz(120, 700), MHz(250, 650)), + ), + + "Qabr as Sitt": AirfieldData( + theater="Syria", + elevation=2134, + runway_length=489, + atc=AtcData(MHz(4, 200), MHz(39, 300), MHz(122, 600), MHz(250, 850)), + ), + + "Damascus": AirfieldData( + theater="Syria", + icao="OSDI", + elevation=2007, + runway_length=11423, + vor=("DAM", MHz(116)), + atc=AtcData(MHz(4, 500), MHz(39, 900), MHz(118, 500), MHz(251, 450)), + ils={ + "24": ("IDA", MHz(109, 900)), + }, + ), + + "Marj Ruhayyil": AirfieldData( + theater="Syria", + icao="OS63", + elevation=2160, + runway_length=7576, + atc=AtcData(MHz(4, 50), MHz(39), MHz(120, 800), MHz(250, 550)), + ), + + "Kiryat Shmona": AirfieldData( + theater="Syria", + icao="LLKS", + elevation=328, + runway_length=3258, + atc=AtcData(MHz(3, 975), MHz(38, 850), MHz(118, 400), MHz(250, 400)), + ), + + "Khalkhalah": AirfieldData( + theater="Syria", + icao="OS69", + elevation=2337, + runway_length=8248, + atc=AtcData(MHz(3, 900), MHz(38, 700), MHz(122, 500), MHz(250, 250)), + ), + + "Haifa": AirfieldData( + theater="Syria", + icao="LLHA", + elevation=19, + runway_length=3253, + atc=AtcData(MHz(3, 775), MHz(38, 450), MHz(127, 800), MHz(250, 50)), + ), + + "Ramat David": AirfieldData( + theater="Syria", + icao="LLRD", + elevation=105, + runway_length=7037, + atc=AtcData(MHz(4, 250), MHz(39, 400), MHz(118, 600), MHz(250, 950)), + ), + + "Megiddo": AirfieldData( + theater="Syria", + icao="LLMG", + elevation=180, + runway_length=6098, + atc=AtcData(MHz(4, 75), MHz(39, 50), MHz(119, 900), MHz(250, 600)), + ), + + "Eyn Shemer": AirfieldData( + theater="Syria", + icao="LLES", + elevation=93, + runway_length=3562, + atc=AtcData(MHz(3, 750), MHz(38, 400), MHz(123, 400), MHz(250)), + ), + + "King Hussein Air College": AirfieldData( + theater="Syria", + icao="OJMF", + elevation=2204, + runway_length=8595, + atc=AtcData(MHz(3, 925), MHz(38, 750), MHz(118, 300), MHz(250, 300)), + ), + # NTTR "Mina Airport 3Q0": AirfieldData( theater="NTTR", From 8bc77bbf18c61cbb9745ff152ec74aae7f0b8439 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sun, 6 Sep 2020 22:39:59 -0700 Subject: [PATCH 36/61] Make waypoint types less error prone. Make the type of the waypoint a non-optional part of the constructor. Every waypoint needs a type, and there's no good default (the previous default, `TAKEOFF`, is actually unused). All of the target waypoints were mistakenly being set as `TAKEOFF`, so I've fixed that in the process. Also, fix the bug where only the last custom target of a SEAD objective was being added to the waypoint list because the append was scoped incorrectly. --- gen/flights/ai_flight_planner.py | 154 +++++++++++++----- gen/flights/flight.py | 7 +- .../QPredefinedWaypointSelectionComboBox.py | 31 +++- 3 files changed, 143 insertions(+), 49 deletions(-) diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index 90a91500..42787bfc 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -372,17 +372,26 @@ class FlightPlanner: egress_heading = heading - 180 - 25 ingress_pos = location.position.point_from_heading(ingress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"]) - ingress_point = FlightWaypoint(ingress_pos.x, ingress_pos.y, self.doctrine["INGRESS_ALT"]) + ingress_point = FlightWaypoint( + FlightWaypointType.INGRESS_STRIKE, + ingress_pos.x, + ingress_pos.y, + self.doctrine["INGRESS_ALT"] + ) ingress_point.pretty_name = "INGRESS on " + location.obj_name ingress_point.description = "INGRESS on " + location.obj_name ingress_point.name = "INGRESS" - ingress_point.waypoint_type = FlightWaypointType.INGRESS_STRIKE flight.points.append(ingress_point) if len(location.groups) > 0 and location.dcs_identifier == "AA": for g in location.groups: for j, u in enumerate(g.units): - point = FlightWaypoint(u.position.x, u.position.y, 0) + point = FlightWaypoint( + FlightWaypointType.TARGET_POINT, + u.position.x, + u.position.y, + 0 + ) point.description = "STRIKE " + "[" + str(location.obj_name) + "] : " + u.type + " #" + str(j) point.pretty_name = "STRIKE " + "[" + str(location.obj_name) + "] : " + u.type + " #" + str(j) point.name = location.obj_name + "#" + str(j) @@ -398,7 +407,12 @@ class FlightPlanner: if building.is_dead: continue - point = FlightWaypoint(building.position.x, building.position.y, 0) + point = FlightWaypoint( + FlightWaypointType.TARGET_POINT, + building.position.x, + building.position.y, + 0 + ) point.description = "STRIKE on " + building.obj_name + " " + building.category + " [" + str(building.dcs_identifier) + " ]" point.pretty_name = "STRIKE on " + building.obj_name + " " + building.category + " [" + str(building.dcs_identifier) + " ]" point.name = building.obj_name @@ -406,7 +420,12 @@ class FlightPlanner: ingress_point.targets.append(building) flight.points.append(point) else: - point = FlightWaypoint(location.position.x, location.position.y, 0) + point = FlightWaypoint( + FlightWaypointType.TARGET_GROUP_LOC, + location.position.x, + location.position.y, + 0 + ) point.description = "STRIKE on " + location.obj_name point.pretty_name = "STRIKE on " + location.obj_name point.name = location.obj_name @@ -415,11 +434,15 @@ class FlightPlanner: flight.points.append(point) egress_pos = location.position.point_from_heading(egress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"]) - egress_point = FlightWaypoint(egress_pos.x, egress_pos.y, self.doctrine["EGRESS_ALT"]) + egress_point = FlightWaypoint( + FlightWaypointType.EGRESS, + egress_pos.x, + egress_pos.y, + self.doctrine["EGRESS_ALT"] + ) egress_point.name = "EGRESS" egress_point.pretty_name = "EGRESS from " + location.obj_name egress_point.description = "EGRESS from " + location.obj_name - egress_point.waypoint_type = FlightWaypointType.EGRESS flight.points.append(egress_point) descend = self.generate_descend_point(flight.from_cp) @@ -454,18 +477,26 @@ class FlightPlanner: ascend = self.generate_ascend_point(flight.from_cp) flight.points.append(ascend) - orbit0 = FlightWaypoint(orbit0p.x, orbit0p.y, patrol_alt) + orbit0 = FlightWaypoint( + FlightWaypointType.PATROL_TRACK, + orbit0p.x, + orbit0p.y, + patrol_alt + ) orbit0.name = "ORBIT 0" orbit0.description = "Standby between this point and the next one" orbit0.pretty_name = "Race-track start" - orbit0.waypoint_type = FlightWaypointType.PATROL_TRACK flight.points.append(orbit0) - orbit1 = FlightWaypoint(orbit1p.x, orbit1p.y, patrol_alt) + orbit1 = FlightWaypoint( + FlightWaypointType.PATROL, + orbit1p.x, + orbit1p.y, + patrol_alt + ) orbit1.name = "ORBIT 1" orbit1.description = "Standby between this point and the previous one" orbit1.pretty_name = "Race-track end" - orbit1.waypoint_type = FlightWaypointType.PATROL flight.points.append(orbit1) orbit0.targets.append(for_cp) @@ -512,18 +543,26 @@ class FlightPlanner: ascend = self.generate_ascend_point(flight.from_cp) flight.points.append(ascend) - orbit0 = FlightWaypoint(orbit0p.x, orbit0p.y, patrol_alt) + orbit0 = FlightWaypoint( + FlightWaypointType.PATROL_TRACK, + orbit0p.x, + orbit0p.y, + patrol_alt + ) orbit0.name = "ORBIT 0" orbit0.description = "Standby between this point and the next one" orbit0.pretty_name = "Race-track start" - orbit0.waypoint_type = FlightWaypointType.PATROL_TRACK flight.points.append(orbit0) - orbit1 = FlightWaypoint(orbit1p.x, orbit1p.y, patrol_alt) + orbit1 = FlightWaypoint( + FlightWaypointType.PATROL, + orbit1p.x, + orbit1p.y, + patrol_alt + ) orbit1.name = "ORBIT 1" orbit1.description = "Standby between this point and the previous one" orbit1.pretty_name = "Race-track end" - orbit1.waypoint_type = FlightWaypointType.PATROL flight.points.append(orbit1) # Note : Targets of a PATROL TRACK waypoints are the points to be defended @@ -555,16 +594,25 @@ class FlightPlanner: egress_heading = heading - 180 - 25 ingress_pos = location.position.point_from_heading(ingress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"]) - ingress_point = FlightWaypoint(ingress_pos.x, ingress_pos.y, self.doctrine["INGRESS_ALT"]) + ingress_point = FlightWaypoint( + FlightWaypointType.INGRESS_SEAD, + ingress_pos.x, + ingress_pos.y, + self.doctrine["INGRESS_ALT"] + ) ingress_point.name = "INGRESS" ingress_point.pretty_name = "INGRESS on " + location.obj_name ingress_point.description = "INGRESS on " + location.obj_name - ingress_point.waypoint_type = FlightWaypointType.INGRESS_SEAD flight.points.append(ingress_point) if len(custom_targets) > 0: for target in custom_targets: - point = FlightWaypoint(target.position.x, target.position.y, 0) + point = FlightWaypoint( + FlightWaypointType.TARGET_POINT, + target.position.x, + target.position.y, + 0 + ) point.alt_type = "RADIO" if flight.flight_type == FlightType.DEAD: point.description = "SEAD on " + target.type @@ -574,11 +622,16 @@ class FlightPlanner: point.description = "DEAD on " + location.obj_name point.pretty_name = "DEAD on " + location.obj_name point.only_for_player = True + flight.points.append(point) ingress_point.targets.append(location) ingress_point.targetGroup = location - flight.points.append(point) else: - point = FlightWaypoint(location.position.x, location.position.y, 0) + point = FlightWaypoint( + FlightWaypointType.TARGET_GROUP_LOC, + location.position.x, + location.position.y, + 0 + ) point.alt_type = "RADIO" if flight.flight_type == FlightType.DEAD: point.description = "SEAD on " + location.obj_name @@ -593,11 +646,15 @@ class FlightPlanner: flight.points.append(point) egress_pos = location.position.point_from_heading(egress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"]) - egress_point = FlightWaypoint(egress_pos.x, egress_pos.y, self.doctrine["EGRESS_ALT"]) + egress_point = FlightWaypoint( + FlightWaypointType.EGRESS, + egress_pos.x, + egress_pos.y, + self.doctrine["EGRESS_ALT"] + ) egress_point.name = "EGRESS" egress_point.pretty_name = "EGRESS from " + location.obj_name egress_point.description = "EGRESS from " + location.obj_name - egress_point.waypoint_type = FlightWaypointType.EGRESS flight.points.append(egress_point) descend = self.generate_descend_point(flight.from_cp) @@ -628,28 +685,40 @@ class FlightPlanner: ascend.alt = 500 flight.points.append(ascend) - ingress_point = FlightWaypoint(ingress.x, ingress.y, cap_alt) + ingress_point = FlightWaypoint( + FlightWaypointType.INGRESS_CAS, + ingress.x, + ingress.y, + cap_alt + ) ingress_point.alt_type = "RADIO" ingress_point.name = "INGRESS" ingress_point.pretty_name = "INGRESS" ingress_point.description = "Ingress into CAS area" - ingress_point.waypoint_type = FlightWaypointType.INGRESS_CAS flight.points.append(ingress_point) - center_point = FlightWaypoint(center.x, center.y, cap_alt) + center_point = FlightWaypoint( + FlightWaypointType.CAS, + center.x, + center.y, + cap_alt + ) center_point.alt_type = "RADIO" center_point.description = "Provide CAS" center_point.name = "CAS" center_point.pretty_name = "CAS" - center_point.waypoint_type = FlightWaypointType.CAS flight.points.append(center_point) - egress_point = FlightWaypoint(egress.x, egress.y, cap_alt) + egress_point = FlightWaypoint( + FlightWaypointType.EGRESS, + egress.x, + egress.y, + cap_alt + ) egress_point.alt_type = "RADIO" egress_point.description = "Egress from CAS area" egress_point.name = "EGRESS" egress_point.pretty_name = "EGRESS" - egress_point.waypoint_type = FlightWaypointType.EGRESS flight.points.append(egress_point) descend = self.generate_descend_point(flight.from_cp) @@ -660,7 +729,6 @@ class FlightPlanner: rtb = self.generate_rtb_waypoint(flight.from_cp) flight.points.append(rtb) - def generate_ascend_point(self, from_cp): """ Generate ascend point @@ -669,15 +737,18 @@ class FlightPlanner: """ ascend_heading = from_cp.heading pos_ascend = from_cp.position.point_from_heading(ascend_heading, 10000) - ascend = FlightWaypoint(pos_ascend.x, pos_ascend.y, self.doctrine["PATTERN_ALTITUDE"]) + ascend = FlightWaypoint( + FlightWaypointType.ASCEND_POINT, + pos_ascend.x, + pos_ascend.y, + self.doctrine["PATTERN_ALTITUDE"] + ) ascend.name = "ASCEND" ascend.alt_type = "RADIO" ascend.description = "Ascend" ascend.pretty_name = "Ascend" - ascend.waypoint_type = FlightWaypointType.ASCEND_POINT return ascend - def generate_descend_point(self, from_cp): """ Generate approach/descend point @@ -686,15 +757,18 @@ class FlightPlanner: """ ascend_heading = from_cp.heading descend = from_cp.position.point_from_heading(ascend_heading - 180, 10000) - descend = FlightWaypoint(descend.x, descend.y, self.doctrine["PATTERN_ALTITUDE"]) + descend = FlightWaypoint( + FlightWaypointType.DESCENT_POINT, + descend.x, + descend.y, + self.doctrine["PATTERN_ALTITUDE"] + ) descend.name = "DESCEND" descend.alt_type = "RADIO" descend.description = "Descend to pattern alt" descend.pretty_name = "Descend to pattern alt" - descend.waypoint_type = FlightWaypointType.DESCENT_POINT return descend - def generate_rtb_waypoint(self, from_cp): """ Generate RTB landing point @@ -702,10 +776,14 @@ class FlightPlanner: :return: """ rtb = from_cp.position - rtb = FlightWaypoint(rtb.x, rtb.y, 0) + rtb = FlightWaypoint( + FlightWaypointType.LANDING_POINT, + rtb.x, + rtb.y, + 0 + ) rtb.name = "LANDING" rtb.alt_type = "RADIO" rtb.description = "RTB" rtb.pretty_name = "RTB" - rtb.waypoint_type = FlightWaypointType.LANDING_POINT - return rtb \ No newline at end of file + return rtb diff --git a/gen/flights/flight.py b/gen/flights/flight.py index 6e2522f5..b736247f 100644 --- a/gen/flights/flight.py +++ b/gen/flights/flight.py @@ -60,7 +60,9 @@ class PredefinedWaypointCategory(Enum): class FlightWaypoint: - def __init__(self, x: float, y: float, alt=0): + def __init__(self, waypoint_type: FlightWaypointType, x: float, y: float, + alt: int = 0) -> None: + self.waypoint_type = waypoint_type self.x = x self.y = y self.alt = alt @@ -71,8 +73,7 @@ class FlightWaypoint: self.targetGroup = None self.obj_name = "" self.pretty_name = "" - self.waypoint_type = FlightWaypointType.TAKEOFF # type: FlightWaypointType - self.category = PredefinedWaypointCategory.NOT_PREDEFINED# type: PredefinedWaypointCategory + self.category: PredefinedWaypointCategory = PredefinedWaypointCategory.NOT_PREDEFINED self.only_for_player = False self.data = None diff --git a/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py b/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py index c8721a17..079aab99 100644 --- a/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py +++ b/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py @@ -57,13 +57,16 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox): enemy_cp = [ecp for ecp in cp.connected_points if ecp.captured != cp.captured] for ecp in enemy_cp: pos = Conflict.frontline_position(self.game.theater, cp, ecp)[0] - wpt = FlightWaypoint(pos.x, pos.y, 800) + wpt = FlightWaypoint( + FlightWaypointType.CUSTOM, + pos.x, + pos.y, + 800) wpt.name = "Frontline " + cp.name + "/" + ecp.name + " [CAS]" wpt.alt_type = "RADIO" wpt.pretty_name = wpt.name wpt.description = "Frontline" wpt.data = [cp, ecp] - wpt.waypoint_type = FlightWaypointType.CUSTOM wpt.category = PredefinedWaypointCategory.FRONTLINE i = add_model_item(i, model, wpt.pretty_name, wpt) @@ -72,14 +75,18 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox): if (self.include_enemy and not cp.captured) or (self.include_friendly and cp.captured): for ground_object in cp.ground_objects: if not ground_object.is_dead and not ground_object.dcs_identifier == "AA": - wpt = FlightWaypoint(ground_object.position.x,ground_object.position.y, 0) + wpt = FlightWaypoint( + FlightWaypointType.CUSTOM, + ground_object.position.x, + ground_object.position.y, + 0 + ) wpt.alt_type = "RADIO" wpt.name = wpt.name = "[" + str(ground_object.obj_name) + "] : " + ground_object.category + " #" + str(ground_object.object_id) wpt.pretty_name = wpt.name wpt.obj_name = ground_object.obj_name wpt.targets.append(ground_object) wpt.data = ground_object - wpt.waypoint_type = FlightWaypointType.CUSTOM if cp.captured: wpt.description = "Friendly Building" wpt.category = PredefinedWaypointCategory.ALLY_BUILDING @@ -95,7 +102,12 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox): if not ground_object.is_dead and ground_object.dcs_identifier == "AA": for g in ground_object.groups: for j, u in enumerate(g.units): - wpt = FlightWaypoint(u.position.x, u.position.y, 0) + wpt = FlightWaypoint( + FlightWaypointType.CUSTOM, + u.position.x, + u.position.y, + 0 + ) wpt.alt_type = "RADIO" wpt.name = wpt.name = "[" + str(ground_object.obj_name) + "] : " + u.type + " #" + str(j) wpt.pretty_name = wpt.name @@ -114,11 +126,15 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox): if self.include_airbases: for cp in self.game.theater.controlpoints: if (self.include_enemy and not cp.captured) or (self.include_friendly and cp.captured): - wpt = FlightWaypoint(cp.position.x, cp.position.y, 0) + wpt = FlightWaypoint( + FlightWaypointType.CUSTOM, + cp.position.x, + cp.position.y, + 0 + ) wpt.alt_type = "RADIO" wpt.name = cp.name wpt.data = cp - wpt.waypoint_type = FlightWaypointType.CUSTOM if cp.captured: wpt.description = "Position of " + cp.name + " [Friendly Airbase]" wpt.category = PredefinedWaypointCategory.ALLY_CP @@ -133,7 +149,6 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox): else: wpt.pretty_name = cp.name + " (Airbase)" - i = add_model_item(i, model, wpt.pretty_name, wpt) self.setModel(model) From 180537cd4830f213bb1e0d3b91560d1c37d64764 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sun, 6 Sep 2020 22:50:30 -0700 Subject: [PATCH 37/61] Fix SEAD/DEAD reversal. --- gen/flights/ai_flight_planner.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index 42787bfc..99cf8427 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -615,12 +615,12 @@ class FlightPlanner: ) point.alt_type = "RADIO" if flight.flight_type == FlightType.DEAD: - point.description = "SEAD on " + target.type - point.pretty_name = "SEAD on " + location.obj_name + point.description = "DEAD on " + target.type + point.pretty_name = "DEAD on " + location.obj_name point.only_for_player = True else: - point.description = "DEAD on " + location.obj_name - point.pretty_name = "DEAD on " + location.obj_name + point.description = "SEAD on " + location.obj_name + point.pretty_name = "SEAD on " + location.obj_name point.only_for_player = True flight.points.append(point) ingress_point.targets.append(location) @@ -634,13 +634,13 @@ class FlightPlanner: ) point.alt_type = "RADIO" if flight.flight_type == FlightType.DEAD: - point.description = "SEAD on " + location.obj_name - point.pretty_name = "SEAD on " + location.obj_name - point.only_for_player = True - else: point.description = "DEAD on " + location.obj_name point.pretty_name = "DEAD on " + location.obj_name point.only_for_player = True + else: + point.description = "SEAD on " + location.obj_name + point.pretty_name = "SEAD on " + location.obj_name + point.only_for_player = True ingress_point.targets.append(location) ingress_point.targetGroup = location flight.points.append(point) From d4820b2435c6573ad643a2aea380150f719146bb Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Thu, 10 Sep 2020 01:33:54 -0700 Subject: [PATCH 38/61] Coalesce large runs of target waypoints. Since we create a target waypoint for every target in a strike/SEAD/DEAD objective area (including every ground vehicle), the kneeboard can quickly be overrun with target waypoints. When there are many target waypoints, collapse them all into a single row for brevity. --- gen/kneeboard.py | 62 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 6 deletions(-) diff --git a/gen/kneeboard.py b/gen/kneeboard.py index 4bc5d64d..d96dc466 100644 --- a/gen/kneeboard.py +++ b/gen/kneeboard.py @@ -23,19 +23,21 @@ only be added per airframe, so PvP missions where each side have the same aircraft will be able to see the enemy's kneeboard for the same airframe. """ from collections import defaultdict +from dataclasses import dataclass from pathlib import Path from typing import Dict, List, Optional, Tuple from PIL import Image, ImageDraw, ImageFont -from tabulate import tabulate - from dcs.mission import Mission from dcs.unittype import FlyingType +from tabulate import tabulate + from . import units from .aircraft import FlightData from .airfields import RunwayData from .airsupportgen import AwacsInfo, TankerInfo from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator +from .flights.flight import FlightWaypoint, FlightWaypointType from .radios import RadioFrequency @@ -95,6 +97,54 @@ class KneeboardPage: raise NotImplementedError +@dataclass(frozen=True) +class NumberedWaypoint: + number: int + waypoint: FlightWaypoint + + +class FlightPlanBuilder: + def __init__(self) -> None: + self.rows: List[List[str]] = [] + self.target_points: List[NumberedWaypoint] = [] + + def add_waypoint(self, waypoint_num: int, waypoint: FlightWaypoint) -> None: + if waypoint.waypoint_type == FlightWaypointType.TARGET_POINT: + self.target_points.append(NumberedWaypoint(waypoint_num, waypoint)) + return + + if self.target_points: + self.coalesce_target_points() + self.target_points = [] + + self.add_waypoint_row(NumberedWaypoint(waypoint_num, waypoint)) + + def coalesce_target_points(self) -> None: + if len(self.target_points) <= 4: + for steerpoint in self.target_points: + self.add_waypoint_row(steerpoint) + return + + first_waypoint_num = self.target_points[0].number + last_waypoint_num = self.target_points[-1].number + + self.rows.append([ + f"{first_waypoint_num}-{last_waypoint_num}", + "Target points", + "0" + ]) + + def add_waypoint_row(self, waypoint: NumberedWaypoint) -> None: + self.rows.append([ + waypoint.number, + waypoint.waypoint.pretty_name, + str(int(units.meters_to_feet(waypoint.waypoint.alt))) + ]) + + def build(self) -> List[List[str]]: + return self.rows + + class BriefingPage(KneeboardPage): """A kneeboard page containing briefing information.""" def __init__(self, flight: FlightData, comms: List[CommInfo], @@ -122,11 +172,11 @@ class BriefingPage(KneeboardPage): ], headers=["", "Airbase", "ATC", "TCN", "I(C)LS", "RWY"]) writer.heading("Flight Plan") - flight_plan = [] + flight_plan_builder = FlightPlanBuilder() for num, waypoint in enumerate(self.flight.waypoints): - alt = int(units.meters_to_feet(waypoint.alt)) - flight_plan.append([num, waypoint.pretty_name, str(alt)]) - writer.table(flight_plan, headers=["STPT", "Action", "Alt"]) + flight_plan_builder.add_waypoint(num, waypoint) + writer.table(flight_plan_builder.build(), + headers=["STPT", "Action", "Alt"]) writer.heading("Comm Ladder") comms = [] From 837795e87aa91a43a172d83f614c33fb18601679 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Thu, 10 Sep 2020 17:05:32 -0700 Subject: [PATCH 39/61] Fix TACAN/ILS info for airfields. --- gen/airfields.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/gen/airfields.py b/gen/airfields.py index 1e881a98..c1ae0dcc 100644 --- a/gen/airfields.py +++ b/gen/airfields.py @@ -712,21 +712,23 @@ class RunwayData: """ atc: Optional[RadioFrequency] = None tacan: Optional[TacanChannel] = None + tacan_callsign: Optional[str] = None ils: Optional[RadioFrequency] = None try: airfield = AIRFIELD_DATA[airport.name] atc = airfield.atc.uhf tacan = airfield.tacan - tacan = airfield.tacan_callsign + tacan_callsign = airfield.tacan_callsign ils = airfield.ils_freq(runway) except KeyError: logging.warning(f"No airfield data for {airport.name}") return cls( - airport.name, - runway, - atc, - tacan, - ils + airfield_name=airport.name, + runway_name=runway, + atc=atc, + tacan=tacan, + tacan_callsign=tacan_callsign, + ils=ils ) @classmethod From 51bfc9a59bc9d59ffea453441c87ca9810648c31 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Fri, 11 Sep 2020 01:47:00 -0700 Subject: [PATCH 40/61] Update pydcs. --- pydcs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydcs b/pydcs index 5c02bf8e..f0f9754b 160000 --- a/pydcs +++ b/pydcs @@ -1 +1 @@ -Subproject commit 5c02bf8ea5e3ec5afccc0135e31a3dd15e21342b +Subproject commit f0f9754b97cf08f71c2b7f5f9687b679b4f0d69d From 993bf50012357381e934b24e81992b165728f0c3 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Thu, 10 Sep 2020 16:59:24 -0700 Subject: [PATCH 41/61] Handle callsigns for flights. We don't configure the callsigns that pydcs uses, so instead read those from pydcs and use them where appropriate instead of just guessing. Fixes https://github.com/Khopa/dcs_liberation/issues/113. --- game/operation/operation.py | 11 +++---- gen/aircraft.py | 29 +++++++++++++++++ gen/airsupportgen.py | 62 ++++++++++++++++--------------------- gen/armor.py | 25 ++++++++++++--- gen/briefinggen.py | 11 ++----- gen/kneeboard.py | 4 +-- 6 files changed, 85 insertions(+), 57 deletions(-) diff --git a/game/operation/operation.py b/game/operation/operation.py index 7e75a305..93ad5765 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -199,7 +199,7 @@ class Operation: ) # Generate ground units on frontline everywhere - self.game.jtacs = [] + jtacs: List[JtacInfo] = [] for player_cp, enemy_cp in self.game.theater.conflicts(True): conflict = Conflict.frontline_cas_conflict(self.attacker_name, self.defender_name, self.current_mission.country(self.attacker_country), @@ -210,6 +210,7 @@ class Operation: enemy_gp = self.game.ground_planners[enemy_cp.id].units_per_cp[player_cp.id] groundConflictGen = GroundConflictGenerator(self.current_mission, conflict, self.game, player_gp, enemy_gp, player_cp.stances[enemy_cp.id]) groundConflictGen.generate() + jtacs.extend(groundConflictGen.jtacs) # Setup combined arms parameters self.current_mission.groundControl.pilot_can_control_vehicles = self.ca_slots > 0 @@ -250,8 +251,8 @@ class Operation: if not self.game.settings.jtac_smoke_on: smoke = "false" - for jtac in self.game.jtacs: - script = script + "\n" + "JTACAutoLase('" + str(jtac[2]) + "', " + str(jtac[1]) + ", " + smoke + ", \"vehicle\")" + "\n" + for jtac in jtacs: + script += f"\nJTACAutoLase('{jtac.unit_name}', {jtac.code}, {smoke}, 'vehicle')\n" load_autolase.add_action(DoScript(String(script))) self.current_mission.triggerrules.triggers.append(load_autolase) @@ -282,9 +283,7 @@ class Operation: self.briefinggen.add_awacs(awacs) kneeboard_generator.add_awacs(awacs) - for region, code, name in self.game.jtacs: - # TODO: Radio info? Type? - jtac = JtacInfo(name, region, code) + for jtac in jtacs: self.briefinggen.add_jtac(jtac) kneeboard_generator.add_jtac(jtac) diff --git a/gen/aircraft.py b/gen/aircraft.py index 82062b6e..7b9858c5 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -202,9 +202,27 @@ class FlightData: self.waypoints = waypoints self.intra_flight_channel = intra_flight_channel self.frequency_to_channel_map = {} + self.callsign = self.create_group_callsign() self.assign_intra_flight_channel() + def create_group_callsign(self) -> str: + lead = self.units[0] + raw_callsign = lead.callsign_as_str() + if not lead.callsign_is_western: + # Callsigns for non-Western countries are just a number per flight, + # similar to tail numbers. + return f"Flight {raw_callsign}" + + # Callsign from pydcs is in the format ``, + # where unit ID is guaranteed to be a single digit but the group ID may + # be more. + match = re.search(r"^(\D+)(\d+)(\d)$", raw_callsign) + if match is None: + logging.error(f"Could not parse unit callsign: {raw_callsign}") + return f"Flight {raw_callsign}" + return f"{match.group(1)} {match.group(2)}" + @property def client_units(self) -> List[FlyingUnit]: """List of playable units in the flight.""" @@ -254,6 +272,17 @@ class FlightData: ) +def callsign_for_support_unit(group: FlyingGroup) -> str: + # Either something like Overlord11 for Western AWACS, or else just a number. + # Convert to either "Overlord" or "Flight 123". + lead = group.units[0] + raw_callsign = lead.callsign_as_str() + try: + return f"Flight {int(raw_callsign)}" + except ValueError: + return raw_callsign.rstrip("1234567890") + + class AircraftConflictGenerator: escort_targets = [] # type: typing.List[typing.Tuple[FlyingGroup, int]] diff --git a/gen/airsupportgen.py b/gen/airsupportgen.py index e65b5a7a..5c1d830f 100644 --- a/gen/airsupportgen.py +++ b/gen/airsupportgen.py @@ -1,16 +1,11 @@ -from typing import List from dataclasses import dataclass, field +from .aircraft import callsign_for_support_unit from .conflictgen import * from .naming import * from .radios import RadioFrequency, RadioRegistry from .tacan import TacanBand, TacanChannel, TacanRegistry -from dcs.mission import * -from dcs.unitgroup import * -from dcs.unittype import * -from dcs.task import * - TANKER_DISTANCE = 15000 TANKER_ALT = 4572 TANKER_HEADING_OFFSET = 45 @@ -18,27 +13,6 @@ TANKER_HEADING_OFFSET = 45 AWACS_DISTANCE = 150000 AWACS_ALT = 13000 -AWACS_CALLSIGNS = [ - "Overlord", - "Magic", - "Wizard", - "Focus", - "Darkstar", -] - - -@dataclass -class TankerCallsign: - full: str - short: str - - -TANKER_CALLSIGNS = [ - TankerCallsign("Texaco", "TEX"), - TankerCallsign("Arco", "ARC"), - TankerCallsign("Shell", "SHL"), -] - @dataclass class AwacsInfo: @@ -81,8 +55,9 @@ class AirSupportConflictGenerator: def generate(self, is_awacs_enabled): player_cp = self.conflict.from_cp if self.conflict.from_cp.captured else self.conflict.to_cp + fallback_tanker_number = 0 + for i, tanker_unit_type in enumerate(db.find_unittype(Refueling, self.conflict.attackers_side)): - callsign = TANKER_CALLSIGNS[i] variant = db.unit_type_name(tanker_unit_type) freq = self.radio_registry.alloc_uhf() tacan = self.tacan_registry.alloc_for_band(TacanBand.Y) @@ -102,19 +77,35 @@ class AirSupportConflictGenerator: tacanchannel=str(tacan), ) + callsign = callsign_for_support_unit(tanker_group) + tacan_callsign = { + "Texaco": "TEX", + "Arco": "ARC", + "Shell": "SHL", + }.get(callsign) + if tacan_callsign is None: + # The dict above is all the callsigns currently in the game, but + # non-Western countries don't use the callsigns and instead just + # use numbers. It's possible that none of those nations have + # TACAN compatible refueling aircraft, but fallback just in + # case. + tacan_callsign = f"TK{fallback_tanker_number}" + fallback_tanker_number += 1 + if tanker_unit_type != IL_78M: - tanker_group.points[0].tasks.pop() # Override PyDCS tacan channel + # Override PyDCS tacan channel. + tanker_group.points[0].tasks.pop() tanker_group.points[0].tasks.append(ActivateBeaconCommand( - tacan.number, tacan.band.value, callsign.short, True, tanker_group.units[0].id, True)) + tacan.number, tacan.band.value, tacan_callsign, True, + tanker_group.units[0].id, True)) tanker_group.points[0].tasks.append(SetInvisibleCommand(True)) tanker_group.points[0].tasks.append(SetImmortalCommand(True)) - self.air_support.tankers.append(TankerInfo(callsign.full, variant, freq, tacan)) + self.air_support.tankers.append(TankerInfo(callsign, variant, freq, tacan)) if is_awacs_enabled: try: - callsign = AWACS_CALLSIGNS[0] freq = self.radio_registry.alloc_uhf() awacs_unit = db.find_unittype(AWACS, self.conflict.attackers_side)[0] awacs_flight = self.mission.awacs_flight( @@ -129,7 +120,8 @@ class AirSupportConflictGenerator: ) awacs_flight.points[0].tasks.append(SetInvisibleCommand(True)) awacs_flight.points[0].tasks.append(SetImmortalCommand(True)) - self.air_support.awacs.append(AwacsInfo(callsign, freq)) - except: - print("No AWACS for faction") + self.air_support.awacs.append(AwacsInfo( + callsign_for_support_unit(awacs_flight), freq)) + except: + print("No AWACS for faction") \ No newline at end of file diff --git a/gen/armor.py b/gen/armor.py index 5d9a8b29..82ee2f5e 100644 --- a/gen/armor.py +++ b/gen/armor.py @@ -1,10 +1,12 @@ -from dcs.action import AITaskPush, AITaskSet +from dataclasses import dataclass + +from dcs.action import AITaskPush from dcs.condition import TimeAfter, UnitDamaged, Or, GroupLifeLess -from dcs.task import * from dcs.triggers import TriggerOnce, Event from gen import namegen from gen.ground_forces.ai_ground_planner import CombatGroupRole, DISTANCE_FROM_FRONTLINE +from .aircraft import callsign_for_support_unit from .conflictgen import * SPREAD_DISTANCE_FACTOR = 0.1, 0.3 @@ -22,6 +24,17 @@ FIGHT_DISTANCE = 3500 RANDOM_OFFSET_ATTACK = 250 + +@dataclass(frozen=True) +class JtacInfo: + """JTAC information.""" + unit_name: str + callsign: str + region: str + code: str + # TODO: Radio info? Type? + + class GroundConflictGenerator: def __init__(self, mission: Mission, conflict: Conflict, game, player_planned_combat_groups, enemy_planned_combat_groups, player_stance): @@ -32,6 +45,7 @@ class GroundConflictGenerator: self.player_stance = CombatStance(player_stance) self.enemy_stance = random.choice([CombatStance.AGGRESIVE, CombatStance.AGGRESIVE, CombatStance.AGGRESIVE, CombatStance.ELIMINATION, CombatStance.BREAKTHROUGH]) if len(enemy_planned_combat_groups) > len(player_planned_combat_groups) else random.choice([CombatStance.DEFENSIVE, CombatStance.DEFENSIVE, CombatStance.DEFENSIVE, CombatStance.AMBUSH, CombatStance.AGGRESIVE]) self.game = game + self.jtacs: List[JtacInfo] = [] def _group_point(self, point) -> Point: distance = randint( @@ -100,7 +114,7 @@ class GroundConflictGenerator: # Add JTAC if "has_jtac" in self.game.player_faction and self.game.player_faction["has_jtac"] and self.game.settings.include_jtac_if_available: n = "JTAC" + str(self.conflict.from_cp.id) + str(self.conflict.to_cp.id) - code = 1688 - len(self.game.jtacs) + code = 1688 - len(self.jtacs) utype = MQ_9_Reaper if "jtac_unit" in self.game.player_faction: @@ -115,7 +129,10 @@ class GroundConflictGenerator: jtac.points[0].tasks.append(SetInvisibleCommand(True)) jtac.points[0].tasks.append(SetImmortalCommand(True)) jtac.points[0].tasks.append(OrbitAction(5000, 300, OrbitAction.OrbitPattern.Circle)) - self.game.jtacs.append(("Frontline " + self.conflict.from_cp.name + "/" + self.conflict.to_cp.name, code, n)) + frontline = f"Frontline {self.conflict.from_cp.name}/{self.conflict.to_cp.name}" + # Note: Will need to change if we ever add ground based JTAC. + callsign = callsign_for_support_unit(jtac) + self.jtacs.append(JtacInfo(n, callsign, frontline, str(code))) def gen_infantry_group_for_group(self, group, is_player, side:Country, forward_heading): diff --git a/gen/briefinggen.py b/gen/briefinggen.py index 0b494ac5..5100ed70 100644 --- a/gen/briefinggen.py +++ b/gen/briefinggen.py @@ -2,13 +2,14 @@ import os from collections import defaultdict from dataclasses import dataclass import random -from typing import List, Tuple +from typing import List from game import db from dcs.mission import Mission from .aircraft import FlightData from .airfields import RunwayData from .airsupportgen import AwacsInfo, TankerInfo +from .armor import JtacInfo from .conflictgen import Conflict from .ground_forces.combat_stance import CombatStance from .radios import RadioFrequency @@ -21,14 +22,6 @@ class CommInfo: freq: RadioFrequency -@dataclass -class JtacInfo: - """JTAC information for the kneeboard.""" - callsign: str - region: str - code: str - - class MissionInfoGenerator: """Base type for generators of mission information for the player. diff --git a/gen/kneeboard.py b/gen/kneeboard.py index 4bc5d64d..bca23746 100644 --- a/gen/kneeboard.py +++ b/gen/kneeboard.py @@ -109,9 +109,7 @@ class BriefingPage(KneeboardPage): def write(self, path: Path) -> None: writer = KneeboardPageWriter() - # TODO: Assign callsigns to flights and include that info. - # https://github.com/Khopa/dcs_liberation/issues/113 - writer.title(f"Mission Info") + writer.title(f"{self.flight.callsign} Mission Info") # TODO: Handle carriers. writer.heading("Airfield Info") From 2db740e1adc354e45ec0a9105750e2a8f90dae0c Mon Sep 17 00:00:00 2001 From: Khopa Date: Fri, 11 Sep 2020 22:00:22 +0200 Subject: [PATCH 42/61] Finished airfield export for PG map. --- gen/airfields.py | 257 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 249 insertions(+), 8 deletions(-) diff --git a/gen/airfields.py b/gen/airfields.py index 4025de33..8a007ced 100644 --- a/gen/airfields.py +++ b/gen/airfields.py @@ -418,11 +418,11 @@ AIRFIELD_DATA = { runway_length=11530, tacan=TacanChannel(96, TacanBand.X), tacan_callsign="MA", - vor=("MA", MHz(114, 90)), + vor=("MA", MHz(114, 900)), atc=AtcData(MHz(4, 250), MHz(39, 400), MHz(126, 500), MHz(251, 000)), ils={ - "13": ("MMA", MHz(111, 10)), - "31": ("IMA", MHz(109, 10)), + "13": ("MMA", MHz(111, 100)), + "31": ("IMA", MHz(109, 100)), }, ), @@ -431,7 +431,7 @@ AIRFIELD_DATA = { icao="OMAD", elevation=11, runway_length=6808, - vor=("ALB", MHz(114, 00)), + vor=("ALB", MHz(114, 0)), atc=AtcData(MHz(4, 25), MHz(38, 950), MHz(119, 900), MHz(250, 550)), ), @@ -440,7 +440,7 @@ AIRFIELD_DATA = { icao="OMNK", elevation=9, runway_length=5387, - vor=("SAS", MHz(128, 93)), + vor=("SAS", MHz(128, 930)), atc=AtcData(MHz(3, 975), MHz(38, 850), MHz(128, 900), MHz(250, 450)), ), @@ -449,13 +449,254 @@ AIRFIELD_DATA = { icao="OMAA", elevation=91, runway_length=12817, - vor=("ADV", MHz(114, 25)), + vor=("ADV", MHz(114, 250)), atc=AtcData(MHz(4, 000), MHz(38, 900), MHz(119, 200), MHz(250, 500)), ), - # TODO : finish persian gulf map - # TODO : SYRIA MAP + "Al Ain International Airport": AirfieldData( + theater="Persian Gulf", + icao="OMAL", + elevation=813, + runway_length=11267, + vor=("ALN", MHz(112, 600)), + atc=AtcData(MHz(4, 75), MHz(39, 50), MHz(119, 850), MHz(250, 650)), + ), + "Al Maktoum Intl": AirfieldData( + theater="Persian Gulf", + icao="OMDW", + elevation=123, + runway_length=11500, + atc=AtcData(MHz(4, 300), MHz(39, 500), MHz(118, 650), MHz(251, 100)), + ils={ + "30": ("IJWA", MHz(109, 750)), + "12": ("IMA", MHz(111, 750)), + }, + ), + + "Al Minhad Intl": AirfieldData( + theater="Persian Gulf", + icao="OMDM", + elevation=190, + runway_length=11865, + tacan=TacanChannel(99, TacanBand.X), + tacan_callsign="MIN", + atc=AtcData(MHz(3, 800), MHz(38, 500), MHz(121, 800), MHz(250, 100)), + ils={ + "27": ("IMNR", MHz(110, 750)), + "9": ("IMNW", MHz(110, 700)), + }, + ), + + "Dubai Intl": AirfieldData( + theater="Persian Gulf", + icao="OMDB", + elevation=16, + runway_length=11018, + atc=AtcData(MHz(4, 275), MHz(39, 450), MHz(118, 750), MHz(251, 50)), + ils={ + "30": ("IDBL", MHz(110, 900)), + "12": ("IDBR", MHz(110, 100)), + }, + ), + + "Sharjah Intl": AirfieldData( + theater="Persian Gulf", + icao="OMSJ", + elevation=98, + runway_length=10535, + atc=AtcData(MHz(3, 850), MHz(38, 600), MHz(118, 600), MHz(252, 200)), + ils={ + "30": ("ISHW", MHz(111, 950)), + "12": ("ISRE", MHz(108, 550)), + }, + ), + + "Fujairah Intl": AirfieldData( + theater="Persian Gulf", + icao="OMFJ", + elevation=60, + runway_length=9437, + vor=("FJV", MHz(113, 800)), + atc=AtcData(MHz(4, 325), MHz(39, 550), MHz(124, 600), MHz(251, 150)), + ils={ + "29": ("IFJR", MHz(111, 500)), + }, + ), + + "Ras AL Khaimah": AirfieldData( + theater="Persian Gulf", + icao="OMRK", + elevation=70, + runway_length=8406, + vor=("OMRK", MHz(113, 600)), + atc=AtcData(MHz(4, 150), MHz(39, 200), MHz(121, 600), MHz(250, 800)), + ), + + "Khasab": AirfieldData( + theater="Persian Gulf", + icao="OOKB", + elevation=47, + runway_length=7513, + atc=AtcData(MHz(3, 750), MHz(38, 400), MHz(124, 350), MHz(250, 000)), + ils={ + "19": ("IBKS", MHz(110, 300)), + }, + ), + + "Sir Abu Nuayr": AirfieldData( + theater="Persian Gulf", + icao="OMSN", + elevation=25, + runway_length=2229 + ), + + "Sirri Island": AirfieldData( + theater="Persian Gulf", + icao="OIBS", + elevation=17, + runway_length=7443, + vor=("SIR", MHz(113, 750)), + atc=AtcData(MHz(3, 875), MHz(38, 650), MHz(135, 50), MHz(250, 250)), + ), + + "Abu Musa Island Airport": AirfieldData( + theater="Persian Gulf", + icao="OIBA", + elevation=16, + runway_length=7616, + atc=AtcData(MHz(3, 950), MHz(38, 800), MHz(122, 900), MHz(250, 400)), + ), + + "Tunb Kochak": AirfieldData( + theater="Persian Gulf", + icao="OITK", + elevation=15, + runway_length=1481, + tacan=TacanChannel(89, TacanBand.X), + tacan_callsign="KCK", + ), + + "Tunb Island AFB": AirfieldData( + theater="Persian Gulf", + icao="OIGI", + elevation=42, + runway_length=6099, + ), + + "Qeshm Island": AirfieldData( + theater="Persian Gulf", + icao="OIKQ", + elevation=26, + runway_length=13287, + vor=("KHM", MHz(117, 100)), + atc=AtcData(MHz(3, 825), MHz(38, 550), MHz(118, 50), MHz(250, 150)), + ), + + "Bandar-e-Jask airfield": AirfieldData( + theater="Persian Gulf", + icao="OIZJ", + elevation=26, + runway_length=6842, + vor=("KHM", MHz(116, 300)), + atc=AtcData(MHz(3, 825), MHz(38, 550), MHz(118, 50), MHz(250, 150)), + ), + + "Bandar Lengeh": AirfieldData( + theater="Persian Gulf", + icao="OIBL", + elevation=80, + runway_length=7625, + vor=("LEN", MHz(114, 800)), + atc=AtcData(MHz(4, 225), MHz(39, 350), MHz(121, 700), MHz(250, 950)), + ), + + "Kish International Airport": AirfieldData( + theater="Persian Gulf", + icao="OIBK", + elevation=114, + runway_length=10617, + tacan=TacanChannel(112, TacanBand.X), + tacan_callsign="KIH", + atc=AtcData(MHz(4, 50), MHz(39, 000), MHz(121, 650), MHz(250, 600)), + ), + + "Lavan Island Airport": AirfieldData( + theater="Persian Gulf", + icao="OIBV", + elevation=75, + runway_length=8234, + vor=("LVA", MHz(116, 850)), + atc=AtcData(MHz(4, 100), MHz(39, 100), MHz(128, 550), MHz(250, 700)), + ), + + "Lar Airbase": AirfieldData( + theater="Persian Gulf", + icao="OISL", + elevation=2635, + runway_length=9600, + vor=("LAR", MHz(117, 900)), + atc=AtcData(MHz(3, 775), MHz(38, 450), MHz(127, 350), MHz(250, 50)), + ), + + "Havadarya": AirfieldData( + theater="Persian Gulf", + icao="OIKP", + elevation=50, + runway_length=7300, + tacan=TacanChannel(47, TacanBand.X), + tacan_callsign="HDR", + atc=AtcData(MHz(4, 350), MHz(39, 600), MHz(123, 150), MHz(251, 200)), + ils={ + "8": ("IBHD", MHz(108, 900)), + }, + ), + + "Bandar Abbas Intl": AirfieldData( + theater="Persian Gulf", + icao="OIKB", + elevation=18, + runway_length=11640, + tacan=TacanChannel(78, TacanBand.X), + tacan_callsign="BND", + vor=("BND", MHz(117, 200)), + atc=AtcData(MHz(4, 200), MHz(39, 300), MHz(118, 100), MHz(250, 900)), + ils={ + "21": ("IBND", MHz(333, 800)), + }, + ), + + "Jiroft Airport": AirfieldData( + theater="Persian Gulf", + icao="OIKJ", + elevation=2664, + runway_length=9160, + atc=AtcData(MHz(4, 125), MHz(39, 120), MHz(136, 0), MHz(250, 750)), + ), + + "Kerman Airport": AirfieldData( + theater="Persian Gulf", + icao="OIKK", + elevation=5746, + runway_length=11981, + tacan=TacanChannel(97, TacanBand.X), + tacan_callsign="KER", + vor=("KER", MHz(112, 0)), + atc=AtcData(MHz(3, 900), MHz(38, 700), MHz(118, 250), MHz(250, 300)), + ), + + "Shiraz International Airport": AirfieldData( + theater="Persian Gulf", + icao="OISS", + elevation=4878, + runway_length=13271, + tacan=TacanChannel(94, TacanBand.X), + tacan_callsign="SYZ1", + vor=("SYZ", MHz(112, 0)), + atc=AtcData(MHz(3, 925), MHz(38, 750), MHz(121, 900), MHz(250, 350)), + ), + + # Syria Map "Adana Sakirpasa": AirfieldData( theater="Syria", icao="LTAF", From 722ec000764847c2d0da880a407c54a4c4e2e58a Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Fri, 11 Sep 2020 01:00:49 -0700 Subject: [PATCH 43/61] Add radio information for more aircraft. Adds the following: * AJS37 * AV-8B * JF-17 This does move the preset channel allocation logic into its own class, since we need to customize that behavior for the AJS37 since it has a rather unique preset channel layout (see the comments in `ViggenRadioChannelAllocator` for details). --- game/operation/operation.py | 40 +---- gen/aircraft.py | 321 +++++++++++++++++++++++------------- gen/airsupportgen.py | 2 +- gen/armor.py | 2 +- gen/callsigns.py | 34 ++++ gen/radios.py | 6 + 6 files changed, 249 insertions(+), 156 deletions(-) create mode 100644 gen/callsigns.py diff --git a/game/operation/operation.py b/game/operation/operation.py index b80e6c54..f66eb124 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -312,41 +312,5 @@ class Operation: logging.warning(f"No aircraft data for {airframe.id}") return - # Intra-flight channel is set up when the flight is created, however we - # do need to make sure we don't overwrite it. For cases where the - # inter-flight and intra-flight radios share presets (the AV-8B only has - # one set of channels, even though it can use two channels - # simultaneously), start assigning channels at 2. - radio_id = aircraft_data.inter_flight_radio_index - if aircraft_data.intra_flight_radio_index == radio_id: - first_channel = 2 - else: - first_channel = 1 - - last_channel = flight.num_radio_channels(radio_id) - channel_alloc = iter(range(first_channel, last_channel + 1)) - - flight.assign_channel(radio_id, next(channel_alloc), flight.departure.atc) - - # TODO: If there ever are multiple AWACS, limit to mission relevant. - for awacs in self.airsupportgen.air_support.awacs: - flight.assign_channel(radio_id, next(channel_alloc), awacs.freq) - - # TODO: Fix departure/arrival to support carriers. - if flight.arrival != flight.departure: - flight.assign_channel(radio_id, next(channel_alloc), - flight.arrival.atc) - - try: - # TODO: Skip incompatible tankers. - for tanker in self.airsupportgen.air_support.tankers: - flight.assign_channel( - radio_id, next(channel_alloc), tanker.freq) - - if flight.divert is not None: - flight.assign_channel(radio_id, next(channel_alloc), - flight.divert.atc) - except StopIteration: - # Any remaining channels are nice-to-haves, but not necessary for - # the few aircraft with a small number of channels available. - pass + aircraft_data.channel_allocator.assign_channels_for_flight( + flight, self.airsupportgen.air_support) diff --git a/gen/aircraft.py b/gen/aircraft.py index 59e6b7a2..c9087838 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -12,6 +12,8 @@ from game.data.cap_capabilities_db import GUNFIGHTERS from game.settings import Settings from game.utils import nm_to_meter from gen.airfields import RunwayData +from gen.airsupportgen import AirSupport +from gen.callsigns import create_group_callsign_from_unit from gen.flights.ai_flight_planner import FlightPlanner from gen.flights.flight import ( Flight, @@ -45,70 +47,6 @@ HELICOPTER_CHANNEL = MHz(127) UHF_FALLBACK_CHANNEL = MHz(251) -@dataclass(frozen=True) -class AircraftData: - """Additional aircraft data not exposed by pydcs.""" - - #: The type of radio used for inter-flight communications. - inter_flight_radio: Radio - - #: The type of radio used for intra-flight communications. - intra_flight_radio: Radio - - #: Index of the radio used for intra-flight communications. Matches the - #: index of the panel_radio field of the pydcs.dcs.planes object. - inter_flight_radio_index: Optional[int] - - #: Index of the radio used for intra-flight communications. Matches the - #: index of the panel_radio field of the pydcs.dcs.planes object. - intra_flight_radio_index: Optional[int] - - -# Indexed by the id field of the pydcs PlaneType. -AIRCRAFT_DATA: Dict[str, AircraftData] = { - "A-10C": AircraftData( - inter_flight_radio=get_radio("AN/ARC-164"), - intra_flight_radio=get_radio("AN/ARC-186(V) AM"), - # The A-10's radio works differently than most aircraft. Doesn't seem to - # be a way to set these from the mission editor, let alone pydcs. - inter_flight_radio_index=None, - intra_flight_radio_index=None - ), - "F-16C_50": AircraftData( - inter_flight_radio=get_radio("AN/ARC-164"), - intra_flight_radio=get_radio("AN/ARC-222"), - # COM2 is the AN/ARC-222, which is the VHF radio we want to use for - # intra-flight communication to leave COM1 open for UHF inter-flight. - inter_flight_radio_index=1, - intra_flight_radio_index=2 - ), - "FA-18C_hornet": AircraftData( - inter_flight_radio=get_radio("AN/ARC-210"), - intra_flight_radio=get_radio("AN/ARC-210"), - # DCS will clobber channel 1 of the first radio compatible with the - # flight's assigned frequency. Since the F/A-18's two radios are both - # AN/ARC-210s, radio 1 will be compatible regardless of which frequency - # is assigned, so we must use radio 1 for the intra-flight radio. - inter_flight_radio_index=2, - intra_flight_radio_index=1 - ), - - "M-2000C": AircraftData( - inter_flight_radio=get_radio("TRT ERA 7000 V/UHF"), - intra_flight_radio=get_radio("TRT ERA 7200 UHF"), - inter_flight_radio_index=1, - intra_flight_radio_index=2 - ), - - "F-14B": AircraftData( - inter_flight_radio=get_radio("AN/ARC-159"), - intra_flight_radio=get_radio("AN/ARC-182"), - inter_flight_radio_index=1, - intra_flight_radio_index=2 - ) -} - - # TODO: Get radio information for all the special cases. def get_fallback_channel(unit_type: UnitType) -> RadioFrequency: if unit_type in helicopter_map.values() and unit_type != UH_1H: @@ -202,46 +140,13 @@ class FlightData: self.waypoints = waypoints self.intra_flight_channel = intra_flight_channel self.frequency_to_channel_map = {} - self.callsign = self.create_group_callsign() - - self.assign_intra_flight_channel() - - def create_group_callsign(self) -> str: - lead = self.units[0] - raw_callsign = lead.callsign_as_str() - if not lead.callsign_is_western: - # Callsigns for non-Western countries are just a number per flight, - # similar to tail numbers. - return f"Flight {raw_callsign}" - - # Callsign from pydcs is in the format ``, - # where unit ID is guaranteed to be a single digit but the group ID may - # be more. - match = re.search(r"^(\D+)(\d+)(\d)$", raw_callsign) - if match is None: - logging.error(f"Could not parse unit callsign: {raw_callsign}") - return f"Flight {raw_callsign}" - return f"{match.group(1)} {match.group(2)}" + self.callsign = create_group_callsign_from_unit(self.units[0]) @property def client_units(self) -> List[FlyingUnit]: """List of playable units in the flight.""" return [u for u in self.units if u.is_human()] - def assign_intra_flight_channel(self) -> None: - """Assigns a channel to the intra-flight frequency.""" - if not self.client_units: - return - - # pydcs will actually set up the channel for us, but we want to make - # sure that it ends up in frequency_to_channel_map. - try: - data = AIRCRAFT_DATA[self.aircraft_type.id] - self.assign_channel( - data.intra_flight_radio_index, 1, self.intra_flight_channel) - except KeyError: - logging.warning(f"No aircraft data for {self.aircraft_type.id}") - @property def aircraft_type(self) -> FlyingType: """Returns the type of aircraft in this flight.""" @@ -272,15 +177,202 @@ class FlightData: ) -def callsign_for_support_unit(group: FlyingGroup) -> str: - # Either something like Overlord11 for Western AWACS, or else just a number. - # Convert to either "Overlord" or "Flight 123". - lead = group.units[0] - raw_callsign = lead.callsign_as_str() - try: - return f"Flight {int(raw_callsign)}" - except ValueError: - return raw_callsign.rstrip("1234567890") +class RadioChannelAllocator: + """Base class for radio channel allocators.""" + + def assign_channels_for_flight(self, flight: FlightData, + air_support: AirSupport) -> None: + """Assigns mission frequencies to preset channels for the flight.""" + raise NotImplementedError + + +@dataclass(frozen=True) +class CommonRadioChannelAllocator(RadioChannelAllocator): + """Radio channel allocator suitable for most aircraft. + + Most of the aircraft with preset channels available have one or more radios + with 20 or more channels available (typically per-radio, but this is not the + case for the JF-17). + """ + + #: Index of the radio used for intra-flight communications. Matches the + #: index of the panel_radio field of the pydcs.dcs.planes object. + inter_flight_radio_index: Optional[int] + + #: Index of the radio used for intra-flight communications. Matches the + #: index of the panel_radio field of the pydcs.dcs.planes object. + intra_flight_radio_index: Optional[int] + + def assign_channels_for_flight(self, flight: FlightData, + air_support: AirSupport) -> None: + flight.assign_channel( + self.intra_flight_radio_index, 1, flight.intra_flight_channel) + + # For cases where the inter-flight and intra-flight radios share presets + # (the JF-17 only has one set of channels, even though it can use two + # channels simultaneously), start assigning inter-flight channels at 2. + radio_id = self.inter_flight_radio_index + if self.intra_flight_radio_index == radio_id: + first_channel = 2 + else: + first_channel = 1 + + last_channel = flight.num_radio_channels(radio_id) + channel_alloc = iter(range(first_channel, last_channel + 1)) + + flight.assign_channel(radio_id, next(channel_alloc), flight.departure.atc) + + # TODO: If there ever are multiple AWACS, limit to mission relevant. + for awacs in air_support.awacs: + flight.assign_channel(radio_id, next(channel_alloc), awacs.freq) + + if flight.arrival != flight.departure: + flight.assign_channel(radio_id, next(channel_alloc), + flight.arrival.atc) + + try: + # TODO: Skip incompatible tankers. + for tanker in air_support.tankers: + flight.assign_channel( + radio_id, next(channel_alloc), tanker.freq) + + if flight.divert is not None: + flight.assign_channel(radio_id, next(channel_alloc), + flight.divert.atc) + except StopIteration: + # Any remaining channels are nice-to-haves, but not necessary for + # the few aircraft with a small number of channels available. + pass + + +@dataclass(frozen=True) +class WarthogRadioChannelAllocator(RadioChannelAllocator): + """Preset channel allocator for the A-10C.""" + + def assign_channels_for_flight(self, flight: FlightData, + air_support: AirSupport) -> None: + # The A-10's radio works differently than most aircraft. Doesn't seem to + # be a way to set these from the mission editor, let alone pydcs. + pass + + +@dataclass(frozen=True) +class ViggenRadioChannelAllocator(RadioChannelAllocator): + """Preset channel allocator for the AJS37.""" + + def assign_channels_for_flight(self, flight: FlightData, + air_support: AirSupport) -> None: + # The Viggen's preset channels are handled differently from other + # aircraft. The aircraft automatically configures channels for every + # allied flight in the game (including AWACS) and for every airfield. As + # such, we don't need to allocate any of those. There are seven presets + # we can modify, however: three channels for the main radio intended for + # communication with wingmen, and four emergency channels for the backup + # radio. We'll set the first channel of the main radio to the + # intra-flight channel, and the first three emergency channels to each + # of the flight plan's airfields. The fourth emergency channel is always + # the guard channel. + radio_id = 1 + flight.assign_channel(radio_id, 1, flight.intra_flight_channel) + flight.assign_channel(radio_id, 4, flight.departure.atc) + flight.assign_channel(radio_id, 5, flight.arrival.atc) + # TODO: Assign divert to 6 when we support divert airfields. + + +@dataclass(frozen=True) +class AircraftData: + """Additional aircraft data not exposed by pydcs.""" + + #: The type of radio used for inter-flight communications. + inter_flight_radio: Radio + + #: The type of radio used for intra-flight communications. + intra_flight_radio: Radio + + #: The radio preset channel allocator, if the aircraft supports channel + #: presets. If the aircraft does not support preset channels, this will be + #: None. + channel_allocator: Optional[RadioChannelAllocator] + + +# Indexed by the id field of the pydcs PlaneType. +AIRCRAFT_DATA: Dict[str, AircraftData] = { + "A-10C": AircraftData( + inter_flight_radio=get_radio("AN/ARC-164"), + intra_flight_radio=get_radio("AN/ARC-186(V) AM"), + channel_allocator=WarthogRadioChannelAllocator() + ), + + "AJS37": AircraftData( + # The AJS37 has somewhat unique radio configuration. Two backup radio + # (FR 24) can only operate simultaneously with the main radio in guard + # mode. As such, we only use the main radio for both inter- and intra- + # flight communication. + inter_flight_radio=get_radio("FR 22"), + intra_flight_radio=get_radio("FR 22"), + channel_allocator=ViggenRadioChannelAllocator() + ), + + "AV8BNA": AircraftData( + inter_flight_radio=get_radio("AN/ARC-210"), + intra_flight_radio=get_radio("AN/ARC-210"), + channel_allocator=CommonRadioChannelAllocator( + inter_flight_radio_index=2, + intra_flight_radio_index=1 + ) + ), + + "F-14B": AircraftData( + inter_flight_radio=get_radio("AN/ARC-159"), + intra_flight_radio=get_radio("AN/ARC-182"), + channel_allocator=CommonRadioChannelAllocator( + inter_flight_radio_index=1, + intra_flight_radio_index=2 + ) + ), + + "F-16C_50": AircraftData( + inter_flight_radio=get_radio("AN/ARC-164"), + intra_flight_radio=get_radio("AN/ARC-222"), + # COM2 is the AN/ARC-222, which is the VHF radio we want to use for + # intra-flight communication to leave COM1 open for UHF inter-flight. + channel_allocator=CommonRadioChannelAllocator( + inter_flight_radio_index=1, + intra_flight_radio_index=2 + ) + ), + + "FA-18C_hornet": AircraftData( + inter_flight_radio=get_radio("AN/ARC-210"), + intra_flight_radio=get_radio("AN/ARC-210"), + # DCS will clobber channel 1 of the first radio compatible with the + # flight's assigned frequency. Since the F/A-18's two radios are both + # AN/ARC-210s, radio 1 will be compatible regardless of which frequency + # is assigned, so we must use radio 1 for the intra-flight radio. + channel_allocator=CommonRadioChannelAllocator( + inter_flight_radio_index=2, + intra_flight_radio_index=1 + ) + ), + + "JF-17": AircraftData( + inter_flight_radio=get_radio("R&S M3AR UHF"), + intra_flight_radio=get_radio("R&S M3AR VHF"), + channel_allocator=CommonRadioChannelAllocator( + inter_flight_radio_index=1, + intra_flight_radio_index=1 + ) + ), + + "M-2000C": AircraftData( + inter_flight_radio=get_radio("TRT ERA 7000 V/UHF"), + intra_flight_radio=get_radio("TRT ERA 7200 UHF"), + channel_allocator=CommonRadioChannelAllocator( + inter_flight_radio_index=1, + intra_flight_radio_index=2 + ) + ), +} class AircraftConflictGenerator: @@ -296,24 +388,21 @@ class AircraftConflictGenerator: self.escort_targets = [] self.flights: List[FlightData] = [] - def get_intra_flight_channel( - self, airframe: UnitType) -> Tuple[int, RadioFrequency]: + def get_intra_flight_channel(self, airframe: UnitType) -> RadioFrequency: """Allocates an intra-flight channel to a group. Args: airframe: The type of aircraft a channel should be allocated for. Returns: - A tuple of the radio index (for aircraft with multiple radios) and - the frequency of the intra-flight channel. + The frequency of the intra-flight channel. """ try: aircraft_data = AIRCRAFT_DATA[airframe.id] - channel = self.radio_registry.alloc_for_radio( + return self.radio_registry.alloc_for_radio( aircraft_data.intra_flight_radio) - return aircraft_data.intra_flight_radio_index, channel except KeyError: - return 1, get_fallback_channel(airframe) + return get_fallback_channel(airframe) def _start_type(self) -> StartType: return self.settings.cold_start and StartType.Cold or StartType.Warm @@ -372,8 +461,8 @@ class AircraftConflictGenerator: group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)) - radio_id, channel = self.get_intra_flight_channel(unit_type) - group.set_frequency(channel.mhz, radio_id) + channel = self.get_intra_flight_channel(unit_type) + group.set_frequency(channel.mhz) # TODO: Support for different departure/arrival airfields. cp = flight.from_cp diff --git a/gen/airsupportgen.py b/gen/airsupportgen.py index 5c1d830f..da0689e9 100644 --- a/gen/airsupportgen.py +++ b/gen/airsupportgen.py @@ -1,6 +1,6 @@ from dataclasses import dataclass, field -from .aircraft import callsign_for_support_unit +from .callsigns import callsign_for_support_unit from .conflictgen import * from .naming import * from .radios import RadioFrequency, RadioRegistry diff --git a/gen/armor.py b/gen/armor.py index 82ee2f5e..e3b6fcef 100644 --- a/gen/armor.py +++ b/gen/armor.py @@ -6,7 +6,7 @@ from dcs.triggers import TriggerOnce, Event from gen import namegen from gen.ground_forces.ai_ground_planner import CombatGroupRole, DISTANCE_FROM_FRONTLINE -from .aircraft import callsign_for_support_unit +from .callsigns import callsign_for_support_unit from .conflictgen import * SPREAD_DISTANCE_FACTOR = 0.1, 0.3 diff --git a/gen/callsigns.py b/gen/callsigns.py new file mode 100644 index 00000000..8ebda467 --- /dev/null +++ b/gen/callsigns.py @@ -0,0 +1,34 @@ +"""Support for working with DCS group callsigns.""" +import logging +import re + +from dcs.unitgroup import FlyingGroup +from dcs.flyingunit import FlyingUnit + + +def callsign_for_support_unit(group: FlyingGroup) -> str: + # Either something like Overlord11 for Western AWACS, or else just a number. + # Convert to either "Overlord" or "Flight 123". + lead = group.units[0] + raw_callsign = lead.callsign_as_str() + try: + return f"Flight {int(raw_callsign)}" + except ValueError: + return raw_callsign.rstrip("1234567890") + + +def create_group_callsign_from_unit(lead: FlyingUnit) -> str: + raw_callsign = lead.callsign_as_str() + if not lead.callsign_is_western: + # Callsigns for non-Western countries are just a number per flight, + # similar to tail numbers. + return f"Flight {raw_callsign}" + + # Callsign from pydcs is in the format ``, + # where unit ID is guaranteed to be a single digit but the group ID may + # be more. + match = re.search(r"^(\D+)(\d+)(\d)$", raw_callsign) + if match is None: + logging.error(f"Could not parse unit callsign: {raw_callsign}") + return f"Flight {raw_callsign}" + return f"{match.group(1)} {match.group(2)}" diff --git a/gen/radios.py b/gen/radios.py index 62790a4c..358b5c1b 100644 --- a/gen/radios.py +++ b/gen/radios.py @@ -116,6 +116,12 @@ RADIOS: List[Radio] = [ # to 400 MHz range, but we can't model gaps with the current implementation. # https://www.heatblur.se/F-14Manual/general.html#an-arc-182-v-uhf-2-radio Radio("AN/ARC-182", MHz(108), MHz(174), step=MHz(1)), + + # Also capable of [103, 156) at 25 kHz intervals, but we can't do gaps. + Radio("FR 22", MHz(225), MHz(400), step=kHz(50)), + + Radio("R&S M3AR VHF", MHz(108), MHz(174), step=MHz(1)), + Radio("R&S M3AR UHF", MHz(225), MHz(400), step=MHz(1)), ] From 5da1db91fd106e7388b38740aa69aba970afb10a Mon Sep 17 00:00:00 2001 From: Khopa Date: Sat, 12 Sep 2020 15:49:30 +0200 Subject: [PATCH 44/61] Normandy airfields data export. --- gen/airfields.py | 293 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 292 insertions(+), 1 deletion(-) diff --git a/gen/airfields.py b/gen/airfields.py index 8a007ced..36f126b3 100644 --- a/gen/airfields.py +++ b/gen/airfields.py @@ -1144,7 +1144,298 @@ AIRFIELD_DATA = { atc=AtcData(MHz(3, 750), MHz(123, 900), MHz(38, 400), MHz(250, 0)), ), - # TODO : NORMANDY MAP + # Normandy + + "Needs Oar Point": AirfieldData( + theater="Normandy", + elevation=30, + runway_length=5259, + atc=AtcData(MHz(4, 225), MHz(118, 950), MHz(39, 350), MHz(250, 950)), + ), + + "Funtington": AirfieldData( + theater="Normandy", + elevation=164, + runway_length=5080, + atc=AtcData(MHz(4, 250), MHz(119, 000), MHz(39, 400), MHz(251, 000)), + ), + + "Tangmere": AirfieldData( + theater="Normandy", + elevation=47, + runway_length=4296, + atc=AtcData(MHz(4, 300), MHz(119, 100), MHz(39, 500), MHz(251, 100)), + ), + + "Ford_AF": AirfieldData( + theater="Normandy", + elevation=29, + runway_length=4296, + atc=AtcData(MHz(4, 325), MHz(119, 150), MHz(39, 550), MHz(251, 150)), + ), + + "Chailey": AirfieldData( + theater="Normandy", + elevation=134, + runway_length=5080, + atc=AtcData(MHz(4, 200), MHz(118, 900), MHz(39, 300), MHz(250, 900)), + ), + + "Maupertus": AirfieldData( + theater="Normandy", + icao="A-15", + elevation=441, + runway_length=4666, + atc=AtcData(MHz(4, 550), MHz(119, 600), MHz(40, 000), MHz(251, 600)), + ), + + "Azeville": AirfieldData( + theater="Normandy", + icao="A-7", + elevation=74, + runway_length=3357, + atc=AtcData(MHz(3, 875), MHz(118, 250), MHz(38, 650), MHz(250, 250)), + ), + + "Biniville": AirfieldData( + theater="Normandy", + icao="A-24", + elevation=106, + runway_length=3283, + atc=AtcData(MHz(3, 750), MHz(118, 000), MHz(38, 400), MHz(250, 000)), + ), + + "Beuzeville": AirfieldData( + theater="Normandy", + icao="A-6", + elevation=114, + runway_length=3840, + atc=AtcData(MHz(3, 850), MHz(118, 200), MHz(38, 600), MHz(250, 200)), + ), + + "Picauville": AirfieldData( + theater="Normandy", + icao="A-8", + elevation=72, + runway_length=3840, + atc=AtcData(MHz(3, 900), MHz(118, 300), MHz(38, 700), MHz(250, 300)), + ), + + "Brucheville": AirfieldData( + theater="Normandy", + icao="A-16", + elevation=45, + runway_length=3413, + atc=AtcData(MHz(4, 575), MHz(119, 650), MHz(40, 50), MHz(251, 650)), + ), + + "Cretteville": AirfieldData( + theater="Normandy", + icao="A-14", + elevation=95, + runway_length=4594, + atc=AtcData(MHz(4, 500), MHz(119, 500), MHz(39, 900), MHz(251, 500)), + ), + + "Meautis": AirfieldData( + theater="Normandy", + icao="A-17", + elevation=83, + runway_length=3840, + atc=AtcData(MHz(4, 600), MHz(119, 700), MHz(40, 100), MHz(251, 700)), + ), + + "Lessay": AirfieldData( + theater="Normandy", + icao="A-20", + elevation=65, + runway_length=5080, + atc=AtcData(MHz(4, 650), MHz(119, 800), MHz(40, 200), MHz(251, 800)), + ), + + "Cardonville": AirfieldData( + theater="Normandy", + icao="A-3", + elevation=101, + runway_length=4541, + atc=AtcData(MHz(3, 775), MHz(118, 50), MHz(38, 450), MHz(250, 50)), + ), + + "Cricqueville-en-Bessin": AirfieldData( + theater="Normandy", + icao="A-2", + elevation=81, + runway_length=3459, + atc=AtcData(MHz(4, 625), MHz(119, 750), MHz(40, 150), MHz(251, 750)), + ), + + "Deux Jumeaux": AirfieldData( + theater="Normandy", + icao="A-4", + elevation=123, + runway_length=4628, + atc=AtcData(MHz(3, 800), MHz(118, 100), MHz(38, 500), MHz(250, 100)), + ), + + "Saint Pierre du Mont": AirfieldData( + theater="Normandy", + icao="A-1", + elevation=103, + runway_length=4737, + atc=AtcData(MHz(4, 000), MHz(118, 500), MHz(38, 900), MHz(250, 500)), + ), + + "Sainte-Laurent-sur-Mer": AirfieldData( + theater="Normandy", + icao="A-21", + elevation=145, + runway_length=4561, + atc=AtcData(MHz(4, 675), MHz(119, 850), MHz(40, 250), MHz(251, 850)), + ), + + "Longues-sur-Mer": AirfieldData( + theater="Normandy", + icao="B-11", + elevation=225, + runway_length=3155, + atc=AtcData(MHz(3, 950), MHz(118, 400), MHz(38, 800), MHz(250, 400)), + ), + + "Chippelle": AirfieldData( + theater="Normandy", + icao="A-5", + elevation=124, + runway_length=4643, + atc=AtcData(MHz(3, 825), MHz(118, 150), MHz(38, 550), MHz(250, 150)), + ), + + "Le Molay": AirfieldData( + theater="Normandy", + icao="A-9", + elevation=104, + runway_length=3840, + atc=AtcData(MHz(3, 925), MHz(118, 350), MHz(38, 750), MHz(250, 350)), + ), + + "Lignerolles": AirfieldData( + theater="Normandy", + icao="A-12", + elevation=404, + runway_length=3436, + atc=AtcData(MHz(4, 275), MHz(119, 50), MHz(39, 450), MHz(251, 50)), + ), + + "Sommervieu": AirfieldData( + theater="Normandy", + icao="B-8", + elevation=186, + runway_length=3840, + atc=AtcData(MHz(4, 125), MHz(118, 750), MHz(39, 150), MHz(250, 750)), + ), + + "Bazenville": AirfieldData( + theater="Normandy", + icao="B-2", + elevation=199, + runway_length=3800, + atc=AtcData(MHz(4, 25), MHz(118, 550), MHz(38, 950), MHz(250, 550)), + ), + + "Rucqueville": AirfieldData( + theater="Normandy", + icao="B-7", + elevation=192, + runway_length=4561, + atc=AtcData(MHz(4, 100), MHz(118, 700), MHz(39, 100), MHz(250, 700)), + ), + + "Lantheuil": AirfieldData( + theater="Normandy", + icao="B-9", + elevation=174, + runway_length=3597, + atc=AtcData(MHz(4, 150), MHz(118, 800), MHz(39, 200), MHz(250, 800)), + ), + + "Sainte-Croix-sur-Mer": AirfieldData( + theater="Normandy", + icao="B-3", + elevation=160, + runway_length=3840, + atc=AtcData(MHz(4, 50), MHz(118, 600), MHz(39, 000), MHz(250, 600)), + ), + + "Beny-sur-Mer": AirfieldData( + theater="Normandy", + icao="B-4", + elevation=199, + runway_length=3155, + atc=AtcData(MHz(4, 75), MHz(118, 650), MHz(39, 50), MHz(250, 650)), + ), + + "Carpiquet": AirfieldData( + theater="Normandy", + icao="B-17", + elevation=187, + runway_length=3799, + atc=AtcData(MHz(3, 975), MHz(118, 450), MHz(38, 850), MHz(250, 450)), + ), + + "Goulet": AirfieldData( + theater="Normandy", + elevation=616, + runway_length=3283, + atc=AtcData(MHz(4, 375), MHz(119, 250), MHz(39, 650), MHz(251, 250)), + ), + + "Argentan": AirfieldData( + theater="Normandy", + elevation=639, + runway_length=3283, + atc=AtcData(MHz(4, 350), MHz(119, 200), MHz(39, 600), MHz(251, 200)), + ), + + "Vrigny": AirfieldData( + theater="Normandy", + elevation=590, + runway_length=3283, + atc=AtcData(MHz(4, 475), MHz(119, 450), MHz(39, 850), MHz(251, 450)), + ), + + "Hauterive": AirfieldData( + theater="Normandy", + elevation=476, + runway_length=3283, + atc=AtcData(MHz(4, 450), MHz(119, 400), MHz(39, 800), MHz(251, 400)), + ), + + "Essay": AirfieldData( + theater="Normandy", + elevation=507, + runway_length=3283, + atc=AtcData(MHz(4, 425), MHz(119, 350), MHz(39, 750), MHz(251, 350)), + ), + + "Barville": AirfieldData( + theater="Normandy", + elevation=462, + runway_length=3493, + atc=AtcData(MHz(4, 400), MHz(119, 300), MHz(39, 700), MHz(251, 300)), + ), + + "Conches": AirfieldData( + theater="Normandy", + elevation=541, + runway_length=4199, + atc=AtcData(MHz(4, 525), MHz(119, 550), MHz(39, 950), MHz(251, 550)), + ), + + "Evreux": AirfieldData( + theater="Normandy", + elevation=423, + runway_length=4296, + atc=AtcData(MHz(4, 175), MHz(118, 850), MHz(39, 250), MHz(250, 850)), + ), # Channel Map "Detling": AirfieldData( From 9e98f05be052d17345e915452d7863c419e965a9 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sat, 12 Sep 2020 12:27:03 -0700 Subject: [PATCH 45/61] Ensure that allocated channels are reserved. This was previously mostly working because the allocator itself was moving forward, but since each radio has its own allocator, aircraft with different radios would often get overlapping intra-flight frequencies. --- gen/radios.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gen/radios.py b/gen/radios.py index 62790a4c..9462cf5d 100644 --- a/gen/radios.py +++ b/gen/radios.py @@ -183,6 +183,7 @@ class RadioRegistry: try: while (channel := next(allocator)) in self.allocated_channels: pass + self.reserve(channel) return channel except StopIteration: raise OutOfChannelsError(radio) From 6ceab69656f14e8f3c11a351fae97ddcc230bb73 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sat, 12 Sep 2020 12:25:59 -0700 Subject: [PATCH 46/61] Name radios appropriately for each aircraft. --- gen/aircraft.py | 75 +++++++++++++++++++++++++++++++++++++++++------- gen/kneeboard.py | 7 +++-- 2 files changed, 70 insertions(+), 12 deletions(-) diff --git a/gen/aircraft.py b/gen/aircraft.py index c9087838..1ff12b33 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from typing import Type from dcs import helicopters from dcs.action import ActivateGroup, AITaskPush, MessageToAll @@ -77,16 +78,61 @@ def get_fallback_channel(unit_type: UnitType) -> RadioFrequency: return UHF_FALLBACK_CHANNEL +class ChannelNamer: + """Base class allowing channel name customization per-aircraft. + + Most aircraft will want to customize this behavior, but the default is + reasonable for any aircraft with numbered radios. + """ + + @staticmethod + def channel_name(radio_id: int, channel_id: int) -> str: + """Returns the name of the channel for the given radio and channel.""" + return f"COMM{radio_id} Ch {channel_id}" + + +class MirageChannelNamer(ChannelNamer): + """Channel namer for the M-2000.""" + + @staticmethod + def channel_name(radio_id: int, channel_id: int) -> str: + radio_name = ["V/UHF", "UHF"][radio_id - 1] + return f"{radio_name} Ch {channel_id}" + + +class TomcatChannelNamer(ChannelNamer): + """Channel namer for the F-14.""" + + @staticmethod + def channel_name(radio_id: int, channel_id: int) -> str: + radio_name = ["UHF", "VHF/UHF"][radio_id - 1] + return f"{radio_name} Ch {channel_id}" + + +class ViggenChannelNamer(ChannelNamer): + """Channel namer for the AJS37.""" + + @staticmethod + def channel_name(radio_id: int, channel_id: int) -> str: + if channel_id >= 4: + channel_letter = "EFGH"[channel_id - 4] + return f"FR 24 {channel_letter}" + return f"FR 22 Special {channel_id}" + + +class ViperChannelNamer(ChannelNamer): + """Channel namer for the F-16.""" + + @staticmethod + def channel_name(radio_id: int, channel_id: int) -> str: + return f"COM{radio_id} Ch {channel_id}" + + @dataclass(frozen=True) class ChannelAssignment: radio_id: int channel: int - @property - def radio_name(self) -> str: - """Returns the name of the radio, i.e. COM1.""" - return f"COM{self.radio_id}" - @dataclass class FlightData: @@ -294,6 +340,9 @@ class AircraftData: #: None. channel_allocator: Optional[RadioChannelAllocator] + #: Defines how channels should be named when printed in the kneeboard. + channel_namer: Type[ChannelNamer] = ChannelNamer + # Indexed by the id field of the pydcs PlaneType. AIRCRAFT_DATA: Dict[str, AircraftData] = { @@ -310,7 +359,8 @@ AIRCRAFT_DATA: Dict[str, AircraftData] = { # flight communication. inter_flight_radio=get_radio("FR 22"), intra_flight_radio=get_radio("FR 22"), - channel_allocator=ViggenRadioChannelAllocator() + channel_allocator=ViggenRadioChannelAllocator(), + channel_namer=ViggenChannelNamer ), "AV8BNA": AircraftData( @@ -328,7 +378,8 @@ AIRCRAFT_DATA: Dict[str, AircraftData] = { channel_allocator=CommonRadioChannelAllocator( inter_flight_radio_index=1, intra_flight_radio_index=2 - ) + ), + channel_namer=TomcatChannelNamer ), "F-16C_50": AircraftData( @@ -339,7 +390,8 @@ AIRCRAFT_DATA: Dict[str, AircraftData] = { channel_allocator=CommonRadioChannelAllocator( inter_flight_radio_index=1, intra_flight_radio_index=2 - ) + ), + channel_namer=ViperChannelNamer ), "FA-18C_hornet": AircraftData( @@ -361,7 +413,9 @@ AIRCRAFT_DATA: Dict[str, AircraftData] = { channel_allocator=CommonRadioChannelAllocator( inter_flight_radio_index=1, intra_flight_radio_index=1 - ) + ), + # Same naming pattern as the Viper, so just reuse that. + channel_namer=ViperChannelNamer ), "M-2000C": AircraftData( @@ -370,7 +424,8 @@ AIRCRAFT_DATA: Dict[str, AircraftData] = { channel_allocator=CommonRadioChannelAllocator( inter_flight_radio_index=1, intra_flight_radio_index=2 - ) + ), + channel_namer=MirageChannelNamer ), } diff --git a/gen/kneeboard.py b/gen/kneeboard.py index 662c53ef..6843e395 100644 --- a/gen/kneeboard.py +++ b/gen/kneeboard.py @@ -33,7 +33,7 @@ from dcs.unittype import FlyingType from tabulate import tabulate from . import units -from .aircraft import FlightData +from .aircraft import AIRCRAFT_DATA, FlightData from .airfields import RunwayData from .airsupportgen import AwacsInfo, TankerInfo from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator @@ -238,7 +238,10 @@ class BriefingPage(KneeboardPage): channel = self.flight.channel_for(frequency) if channel is None: return str(frequency) - return f"{channel.radio_name} Ch {channel.channel}" + + namer = AIRCRAFT_DATA[self.flight.aircraft_type.id].channel_namer + channel_name = namer.channel_name(channel.radio_id, channel.channel) + return f"{channel_name} {frequency}" class KneeboardGenerator(MissionInfoGenerator): From e210dcb4df424c7fa17695ab2bb6e75c93d7315c Mon Sep 17 00:00:00 2001 From: Khopa Date: Sat, 19 Sep 2020 14:02:53 +0200 Subject: [PATCH 47/61] Added disband menu in ground object menu. --- gen/sam/aaa_bofors.py | 2 ++ gen/sam/aaa_flak.py | 2 ++ gen/sam/aaa_zu23_insurgent.py | 2 ++ gen/sam/sam_avenger.py | 2 ++ gen/sam/sam_chaparral.py | 2 ++ gen/sam/sam_gepard.py | 2 ++ gen/sam/sam_group_generator.py | 15 +++++++- gen/sam/sam_hawk.py | 2 ++ gen/sam/sam_hq7.py | 2 ++ gen/sam/sam_linebacker.py | 2 ++ gen/sam/sam_patriot.py | 2 ++ gen/sam/sam_rapier.py | 2 ++ gen/sam/sam_roland.py | 2 ++ gen/sam/sam_sa10.py | 2 ++ gen/sam/sam_sa11.py | 2 ++ gen/sam/sam_sa13.py | 2 ++ gen/sam/sam_sa15.py | 2 ++ gen/sam/sam_sa19.py | 2 ++ gen/sam/sam_sa2.py | 2 ++ gen/sam/sam_sa3.py | 2 ++ gen/sam/sam_sa6.py | 2 ++ gen/sam/sam_sa8.py | 2 ++ gen/sam/sam_sa9.py | 2 ++ gen/sam/sam_vulcan.py | 2 ++ gen/sam/sam_zsu23.py | 2 ++ gen/sam/sam_zu23.py | 2 ++ gen/sam/sam_zu23_ural.py | 2 ++ gen/sam/sam_zu23_ural_insurgent.py | 2 ++ .../windows/groundobject/QGroundObjectMenu.py | 34 ++++++++++++++++++- 29 files changed, 101 insertions(+), 2 deletions(-) diff --git a/gen/sam/aaa_bofors.py b/gen/sam/aaa_bofors.py index 7cab286a..ad515931 100644 --- a/gen/sam/aaa_bofors.py +++ b/gen/sam/aaa_bofors.py @@ -10,6 +10,8 @@ class BoforsGenerator(GroupGenerator): This generate a Bofors flak artillery group """ + name = "Bofors AAA" + def generate(self): grid_x = random.randint(2, 4) grid_y = random.randint(2, 4) diff --git a/gen/sam/aaa_flak.py b/gen/sam/aaa_flak.py index bd988910..896b48aa 100644 --- a/gen/sam/aaa_flak.py +++ b/gen/sam/aaa_flak.py @@ -11,6 +11,8 @@ class FlakGenerator(GroupGenerator): This generate a German flak artillery group """ + name = "Flak Site" + def generate(self): grid_x = random.randint(2, 4) grid_y = random.randint(2, 4) diff --git a/gen/sam/aaa_zu23_insurgent.py b/gen/sam/aaa_zu23_insurgent.py index ec207d84..f9c53620 100644 --- a/gen/sam/aaa_zu23_insurgent.py +++ b/gen/sam/aaa_zu23_insurgent.py @@ -10,6 +10,8 @@ class ZU23InsurgentGenerator(GroupGenerator): This generate a ZU23 insurgent flak artillery group """ + name = "Zu-23 Site" + def generate(self): grid_x = random.randint(2, 4) grid_y = random.randint(2, 4) diff --git a/gen/sam/sam_avenger.py b/gen/sam/sam_avenger.py index d2d7c52b..63844f7b 100644 --- a/gen/sam/sam_avenger.py +++ b/gen/sam/sam_avenger.py @@ -10,6 +10,8 @@ class AvengerGenerator(GroupGenerator): This generate an Avenger group """ + name = "Avenger Group" + def generate(self): num_launchers = random.randint(2, 3) diff --git a/gen/sam/sam_chaparral.py b/gen/sam/sam_chaparral.py index 09d0f2a8..6178df1c 100644 --- a/gen/sam/sam_chaparral.py +++ b/gen/sam/sam_chaparral.py @@ -10,6 +10,8 @@ class ChaparralGenerator(GroupGenerator): This generate a Chaparral group """ + name = "Chaparral Group" + def generate(self): num_launchers = random.randint(2, 4) diff --git a/gen/sam/sam_gepard.py b/gen/sam/sam_gepard.py index 15daa7d3..3ece2cc9 100644 --- a/gen/sam/sam_gepard.py +++ b/gen/sam/sam_gepard.py @@ -10,6 +10,8 @@ class GepardGenerator(GroupGenerator): This generate a Gepard group """ + name = "Gepard Group" + def generate(self): self.add_unit(AirDefence.SPAAA_Gepard, "SPAAA", self.position.x, self.position.y, self.heading) if random.randint(0, 1) == 1: diff --git a/gen/sam/sam_group_generator.py b/gen/sam/sam_group_generator.py index 7979cb16..2a27d752 100644 --- a/gen/sam/sam_group_generator.py +++ b/gen/sam/sam_group_generator.py @@ -1,5 +1,7 @@ import random +from typing import List +from dcs.unittype import UnitType from dcs.vehicles import AirDefence from game import db @@ -99,6 +101,17 @@ SAM_PRICES = { AirDefence.HQ_7_Self_Propelled_LN: 35 } +def get_faction_possible_sams_units(faction: str) -> List[UnitType]: + """ + Return the list + :param faction: Faction to search units for + :return: + """ + return [u for u in db.FACTIONS[faction]["units"] if u in AirDefence.__dict__.values()] + +def get_sam_names(): + pass + def generate_anti_air_group(game, parent_cp, ground_object, faction:str): """ This generate a SAM group @@ -107,7 +120,7 @@ def generate_anti_air_group(game, parent_cp, ground_object, faction:str): :param country: Owner country :return: Nothing, but put the group reference inside the ground object """ - possible_sams = [u for u in db.FACTIONS[faction]["units"] if u in AirDefence.__dict__.values()] + possible_sams = get_faction_possible_sams_units(faction) if len(possible_sams) > 0: sam = random.choice(possible_sams) generator = SAM_MAP[sam](game, ground_object) diff --git a/gen/sam/sam_hawk.py b/gen/sam/sam_hawk.py index 4b43b79d..f5273ff9 100644 --- a/gen/sam/sam_hawk.py +++ b/gen/sam/sam_hawk.py @@ -10,6 +10,8 @@ class HawkGenerator(GroupGenerator): This generate an HAWK group """ + name = "Hawk Site" + def generate(self): self.add_unit(AirDefence.SAM_Hawk_PCP, "PCP", self.position.x, self.position.y, self.heading) self.add_unit(AirDefence.SAM_Hawk_SR_AN_MPQ_50, "SR", self.position.x + 20, self.position.y, self.heading) diff --git a/gen/sam/sam_hq7.py b/gen/sam/sam_hq7.py index 8bd5f528..b32d0226 100644 --- a/gen/sam/sam_hq7.py +++ b/gen/sam/sam_hq7.py @@ -10,6 +10,8 @@ class HQ7Generator(GroupGenerator): This generate an HQ7 group """ + name = "HQ-7 Site" + def generate(self): self.add_unit(AirDefence.HQ_7_Self_Propelled_STR, "STR", self.position.x, self.position.y, self.heading) self.add_unit(AirDefence.HQ_7_Self_Propelled_LN, "LN", self.position.x + 20, self.position.y, self.heading) diff --git a/gen/sam/sam_linebacker.py b/gen/sam/sam_linebacker.py index be2dafb7..add33acb 100644 --- a/gen/sam/sam_linebacker.py +++ b/gen/sam/sam_linebacker.py @@ -10,6 +10,8 @@ class LinebackerGenerator(GroupGenerator): This generate an m6 linebacker group """ + name = "Linebacker Group" + def generate(self): num_launchers = random.randint(2, 4) diff --git a/gen/sam/sam_patriot.py b/gen/sam/sam_patriot.py index 5505ba2a..f042e3b1 100644 --- a/gen/sam/sam_patriot.py +++ b/gen/sam/sam_patriot.py @@ -10,6 +10,8 @@ class PatriotGenerator(GroupGenerator): This generate a Patriot group """ + name = "Patriot Battery" + def generate(self): # Command Post self.add_unit(AirDefence.SAM_Patriot_AMG_AN_MRC_137, "MRC", self.position.x, self.position.y, self.heading) diff --git a/gen/sam/sam_rapier.py b/gen/sam/sam_rapier.py index 2ff0d2bb..0cbffd9e 100644 --- a/gen/sam/sam_rapier.py +++ b/gen/sam/sam_rapier.py @@ -10,6 +10,8 @@ class RapierGenerator(GroupGenerator): This generate a Rapier Group """ + name = "Rapier AA Site" + def generate(self): self.add_unit(AirDefence.Rapier_FSA_Blindfire_Tracker, "BT", self.position.x, self.position.y, self.heading) self.add_unit(AirDefence.Rapier_FSA_Optical_Tracker, "OT", self.position.x + 20, self.position.y, self.heading) diff --git a/gen/sam/sam_roland.py b/gen/sam/sam_roland.py index 6ee438e6..22f60435 100644 --- a/gen/sam/sam_roland.py +++ b/gen/sam/sam_roland.py @@ -8,6 +8,8 @@ class RolandGenerator(GroupGenerator): This generate a Roland group """ + name = "Roland Site" + def generate(self): self.add_unit(AirDefence.SAM_Roland_ADS, "ADS", self.position.x, self.position.y, self.heading) self.add_unit(AirDefence.SAM_Roland_EWR, "EWR", self.position.x + 40, self.position.y, self.heading) diff --git a/gen/sam/sam_sa10.py b/gen/sam/sam_sa10.py index a5332546..c90d3ae9 100644 --- a/gen/sam/sam_sa10.py +++ b/gen/sam/sam_sa10.py @@ -10,6 +10,8 @@ class SA10Generator(GroupGenerator): This generate a SA-10 group """ + name = "SA-10/S-300PS Battery" + def generate(self): # Command Post self.add_unit(AirDefence.SAM_SA_10_S_300PS_CP_54K6, "CP", self.position.x, self.position.y, self.heading) diff --git a/gen/sam/sam_sa11.py b/gen/sam/sam_sa11.py index d3709550..c05fe21a 100644 --- a/gen/sam/sam_sa11.py +++ b/gen/sam/sam_sa11.py @@ -10,6 +10,8 @@ class SA11Generator(GroupGenerator): This generate a SA-11 group """ + name = "SA-11 Buk Battery" + def generate(self): self.add_unit(AirDefence.SAM_SA_11_Buk_CC_9S470M1, "CC", self.position.x, self.position.y, self.heading) self.add_unit(AirDefence.SAM_SA_11_Buk_SR_9S18M1, "SR", self.position.x+20, self.position.y, self.heading) diff --git a/gen/sam/sam_sa13.py b/gen/sam/sam_sa13.py index c3e45745..366746d7 100644 --- a/gen/sam/sam_sa13.py +++ b/gen/sam/sam_sa13.py @@ -10,6 +10,8 @@ class SA13Generator(GroupGenerator): This generate a SA-13 group """ + name = "SA-13 Strela Group" + def generate(self): self.add_unit(Unarmed.Transport_UAZ_469, "UAZ", self.position.x, self.position.y, self.heading) self.add_unit(Unarmed.Transport_KAMAZ_43101, "TRUCK", self.position.x+40, self.position.y, self.heading) diff --git a/gen/sam/sam_sa15.py b/gen/sam/sam_sa15.py index 592c1a34..0aa9c162 100644 --- a/gen/sam/sam_sa15.py +++ b/gen/sam/sam_sa15.py @@ -8,6 +8,8 @@ class SA15Generator(GroupGenerator): This generate a SA-15 group """ + name = "SA-15 Tor Group" + def generate(self): self.add_unit(AirDefence.SAM_SA_15_Tor_9A331, "ADS", self.position.x, self.position.y, self.heading) self.add_unit(Unarmed.Transport_UAZ_469, "EWR", self.position.x + 40, self.position.y, self.heading) diff --git a/gen/sam/sam_sa19.py b/gen/sam/sam_sa19.py index b81fd35d..10923a5a 100644 --- a/gen/sam/sam_sa19.py +++ b/gen/sam/sam_sa19.py @@ -10,6 +10,8 @@ class SA19Generator(GroupGenerator): This generate a SA-19 group """ + name = "SA-19 Tunguska Group" + def generate(self): num_launchers = random.randint(1, 3) diff --git a/gen/sam/sam_sa2.py b/gen/sam/sam_sa2.py index 384b3a06..eb7f014b 100644 --- a/gen/sam/sam_sa2.py +++ b/gen/sam/sam_sa2.py @@ -10,6 +10,8 @@ class SA2Generator(GroupGenerator): This generate a SA-2 group """ + name = "SA-2/S-75 Site" + def generate(self): self.add_unit(AirDefence.SAM_SR_P_19, "SR", self.position.x, self.position.y, self.heading) self.add_unit(AirDefence.SAM_SA_2_TR_SNR_75_Fan_Song, "TR", self.position.x + 20, self.position.y, self.heading) diff --git a/gen/sam/sam_sa3.py b/gen/sam/sam_sa3.py index d4095a7e..9a73de13 100644 --- a/gen/sam/sam_sa3.py +++ b/gen/sam/sam_sa3.py @@ -10,6 +10,8 @@ class SA3Generator(GroupGenerator): This generate a SA-3 group """ + name = "SA-3/S-125 Site" + def generate(self): self.add_unit(AirDefence.SAM_SR_P_19, "SR", self.position.x, self.position.y, self.heading) self.add_unit(AirDefence.SAM_SA_3_S_125_TR_SNR, "TR", self.position.x + 20, self.position.y, self.heading) diff --git a/gen/sam/sam_sa6.py b/gen/sam/sam_sa6.py index 64c6c15c..f96d3989 100644 --- a/gen/sam/sam_sa6.py +++ b/gen/sam/sam_sa6.py @@ -10,6 +10,8 @@ class SA6Generator(GroupGenerator): This generate a SA-6 group """ + name = "SA-6 Kub Site" + def generate(self): self.add_unit(AirDefence.SAM_SA_6_Kub_STR_9S91, "STR", self.position.x, self.position.y, self.heading) diff --git a/gen/sam/sam_sa8.py b/gen/sam/sam_sa8.py index d2e7e8d6..a5df4d85 100644 --- a/gen/sam/sam_sa8.py +++ b/gen/sam/sam_sa8.py @@ -10,6 +10,8 @@ class SA8Generator(GroupGenerator): This generate a SA-8 group """ + name = "SA-8 OSA Site" + def generate(self): self.add_unit(AirDefence.SAM_SA_8_Osa_9A33, "OSA", self.position.x, self.position.y, self.heading) self.add_unit(AirDefence.SAM_SA_8_Osa_LD_9T217, "LD", self.position.x + 20, self.position.y, self.heading) diff --git a/gen/sam/sam_sa9.py b/gen/sam/sam_sa9.py index f16b7cca..9eeffe3c 100644 --- a/gen/sam/sam_sa9.py +++ b/gen/sam/sam_sa9.py @@ -10,6 +10,8 @@ class SA9Generator(GroupGenerator): This generate a SA-9 group """ + name = "SA-9 Group" + def generate(self): self.add_unit(Unarmed.Transport_UAZ_469, "UAZ", self.position.x, self.position.y, self.heading) self.add_unit(Unarmed.Transport_KAMAZ_43101, "TRUCK", self.position.x+40, self.position.y, self.heading) diff --git a/gen/sam/sam_vulcan.py b/gen/sam/sam_vulcan.py index 7cd8d7aa..71789e62 100644 --- a/gen/sam/sam_vulcan.py +++ b/gen/sam/sam_vulcan.py @@ -10,6 +10,8 @@ class VulcanGenerator(GroupGenerator): This generate a Vulcan group """ + name = "Vulcan Group" + def generate(self): self.add_unit(AirDefence.AAA_Vulcan_M163, "SPAAA", self.position.x, self.position.y, self.heading) if random.randint(0, 1) == 1: diff --git a/gen/sam/sam_zsu23.py b/gen/sam/sam_zsu23.py index b0707416..33d1cde3 100644 --- a/gen/sam/sam_zsu23.py +++ b/gen/sam/sam_zsu23.py @@ -10,6 +10,8 @@ class ZSU23Generator(GroupGenerator): This generate a ZSU 23 group """ + name = "ZSU-23 Group" + def generate(self): num_launchers = random.randint(2, 5) diff --git a/gen/sam/sam_zu23.py b/gen/sam/sam_zu23.py index 34a892ab..1f4521cd 100644 --- a/gen/sam/sam_zu23.py +++ b/gen/sam/sam_zu23.py @@ -10,6 +10,8 @@ class ZU23Generator(GroupGenerator): This generate a ZU23 flak artillery group """ + name = "ZU-23 Group" + def generate(self): grid_x = random.randint(2, 4) grid_y = random.randint(2, 4) diff --git a/gen/sam/sam_zu23_ural.py b/gen/sam/sam_zu23_ural.py index 248b84a0..d6b08fac 100644 --- a/gen/sam/sam_zu23_ural.py +++ b/gen/sam/sam_zu23_ural.py @@ -10,6 +10,8 @@ class ZU23UralGenerator(GroupGenerator): This generate a Zu23 Ural group """ + name = "ZU-23 Ural Group" + def generate(self): num_launchers = random.randint(2, 8) diff --git a/gen/sam/sam_zu23_ural_insurgent.py b/gen/sam/sam_zu23_ural_insurgent.py index 282f3d20..55c72196 100644 --- a/gen/sam/sam_zu23_ural_insurgent.py +++ b/gen/sam/sam_zu23_ural_insurgent.py @@ -10,6 +10,8 @@ class ZU23UralInsurgentGenerator(GroupGenerator): This generate a Zu23 Ural group """ + name = "ZU-23 Ural Insurgent Group" + def generate(self): num_launchers = random.randint(2, 8) diff --git a/qt_ui/windows/groundobject/QGroundObjectMenu.py b/qt_ui/windows/groundobject/QGroundObjectMenu.py index 41073c2a..6e67d0c7 100644 --- a/qt_ui/windows/groundobject/QGroundObjectMenu.py +++ b/qt_ui/windows/groundobject/QGroundObjectMenu.py @@ -29,6 +29,7 @@ class QGroundObjectMenu(QDialog): self.buildingBox = QGroupBox("Buildings :") self.intelLayout = QGridLayout() self.buildingsLayout = QGridLayout() + self.total_value = 0 self.init_ui() def init_ui(self): @@ -43,9 +44,19 @@ class QGroundObjectMenu(QDialog): self.mainLayout.addWidget(self.intelBox) else: self.mainLayout.addWidget(self.buildingBox) + + self.actionLayout = QHBoxLayout() + sell_all_button = QPushButton("Disband (+" + str(self.total_value) + "M)") + sell_all_button.clicked.connect(self.sell_all) + self.actionLayout.addWidget(sell_all_button) + + if self.total_value > 0: + self.mainLayout.addLayout(self.actionLayout) self.setLayout(self.mainLayout) def doLayout(self): + + self.update_total_value() self.intelBox = QGroupBox("Units :") self.intelLayout = QGridLayout() i = 0 @@ -94,6 +105,18 @@ class QGroundObjectMenu(QDialog): self.mainLayout.addWidget(self.buildingBox) except Exception as e: print(e) + self.update_total_value() + + def update_total_value(self): + total_value = 0 + for group in self.ground_object.groups: + for u in group.units: + utype = unit_type_of(u) + if utype in PRICES: + total_value = total_value + PRICES[utype] + else: + total_value = total_value + 1 + self.total_value = total_value def repair_unit(self, group, unit, price): if self.game.budget > price: @@ -113,5 +136,14 @@ class QGroundObjectMenu(QDialog): self.do_refresh_layout() - def closeEvent(self, closeEvent: QCloseEvent): + def sell_all(self): + self.update_total_value() + self.game.budget = self.game.budget + self.total_value + self.ground_object.groups = [] GameUpdateSignal.get_instance().updateGame(self.game) + + def buy_group(self): + pass + + def closeEvent(self, closeEvent: QCloseEvent): + pass From 65dd9bc28650453aeced641d75c34d9571b71ac4 Mon Sep 17 00:00:00 2001 From: Khopa Date: Sat, 19 Sep 2020 17:10:56 +0200 Subject: [PATCH 48/61] Fix typo. --- gen/briefinggen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gen/briefinggen.py b/gen/briefinggen.py index bcf51dc2..0db4ac5e 100644 --- a/gen/briefinggen.py +++ b/gen/briefinggen.py @@ -237,7 +237,7 @@ class BriefingGenerator(MissionInfoGenerator): def __random_frontline_sentence(self, player_base_name, enemy_base_name): templates = [ "There are combats between {} and {}. ", - "The war on the ground is still going on between {} an {}. ", + "The war on the ground is still going on between {} and {}. ", "Our ground forces in {} are opposed to enemy forces based in {}. ", "Our forces from {} are fighting enemies based in {}. ", "There is an active frontline between {} and {}. ", From 4031c9b9783f741b73080bfe3d06862995b4698a Mon Sep 17 00:00:00 2001 From: Khopa Date: Sat, 19 Sep 2020 17:11:43 +0200 Subject: [PATCH 49/61] Possible to sell/buy units at SAM location and in airports. --- game/db.py | 16 +- gen/sam/aaa_bofors.py | 5 +- gen/sam/aaa_flak.py | 9 +- gen/sam/aaa_zu23_insurgent.py | 5 +- gen/sam/sam_avenger.py | 1 + gen/sam/sam_chaparral.py | 1 + gen/sam/sam_gepard.py | 1 + gen/sam/sam_group_generator.py | 12 +- gen/sam/sam_hawk.py | 1 + gen/sam/sam_hq7.py | 1 + gen/sam/sam_linebacker.py | 1 + gen/sam/sam_patriot.py | 5 +- gen/sam/sam_rapier.py | 1 + gen/sam/sam_roland.py | 1 + gen/sam/sam_sa10.py | 1 + gen/sam/sam_sa11.py | 1 + gen/sam/sam_sa13.py | 1 + gen/sam/sam_sa15.py | 1 + gen/sam/sam_sa19.py | 1 + gen/sam/sam_sa2.py | 1 + gen/sam/sam_sa3.py | 1 + gen/sam/sam_sa6.py | 1 + gen/sam/sam_sa8.py | 1 + gen/sam/sam_sa9.py | 1 + gen/sam/sam_vulcan.py | 1 + gen/sam/sam_zsu23.py | 3 +- gen/sam/sam_zu23.py | 5 +- gen/sam/sam_zu23_ural.py | 1 + gen/sam/sam_zu23_ural_insurgent.py | 1 + qt_ui/widgets/QTopPanel.py | 6 +- qt_ui/windows/GameUpdateSignal.py | 4 + .../base_defenses/QBaseDefenseGroupInfo.py | 19 +- .../windows/groundobject/QGroundObjectMenu.py | 209 +++++++++++++++++- 33 files changed, 274 insertions(+), 45 deletions(-) diff --git a/game/db.py b/game/db.py index a4cc3e4a..15024422 100644 --- a/game/db.py +++ b/game/db.py @@ -337,15 +337,15 @@ PRICES = { AirDefence.SAM_SA_11_Buk_LN_9A310M1: 30, AirDefence.SAM_SA_8_Osa_9A33: 28, AirDefence.SAM_SA_15_Tor_9A331: 40, - AirDefence.SAM_SA_13_Strela_10M3_9A35M3: 24, - AirDefence.SAM_SA_9_Strela_1_9P31: 16, + AirDefence.SAM_SA_13_Strela_10M3_9A35M3: 16, + AirDefence.SAM_SA_9_Strela_1_9P31: 12, AirDefence.SAM_SA_11_Buk_CC_9S470M1: 25, AirDefence.SAM_SA_8_Osa_LD_9T217: 22, AirDefence.SAM_Patriot_AMG_AN_MRC_137: 35, AirDefence.SAM_Patriot_ECS_AN_MSQ_104: 30, AirDefence.SPAAA_Gepard: 24, AirDefence.SAM_Hawk_PCP: 14, - AirDefence.AAA_Vulcan_M163: 12, + AirDefence.AAA_Vulcan_M163: 10, AirDefence.SAM_Hawk_LN_M192: 8, AirDefence.SAM_Chaparral_M48: 16, AirDefence.SAM_Linebacker_M6: 18, @@ -358,7 +358,7 @@ PRICES = { AirDefence.Stinger_MANPADS: 6, AirDefence.SAM_Stinger_comm_dsr: 4, AirDefence.SAM_Stinger_comm: 4, - AirDefence.SPAAA_ZSU_23_4_Shilka: 12, + AirDefence.SPAAA_ZSU_23_4_Shilka: 10, AirDefence.AAA_ZU_23_Closed: 6, AirDefence.AAA_ZU_23_Emplacement: 6, AirDefence.AAA_ZU_23_on_Ural_375: 8, @@ -387,19 +387,19 @@ PRICES = { AirDefence.SAM_SA_2_LN_SM_90: 8, AirDefence.SAM_SA_2_TR_SNR_75_Fan_Song: 12, AirDefence.Rapier_FSA_Launcher: 6, - AirDefence.Rapier_FSA_Optical_Tracker: 12, - AirDefence.Rapier_FSA_Blindfire_Tracker: 16, + AirDefence.Rapier_FSA_Optical_Tracker: 6, + AirDefence.Rapier_FSA_Blindfire_Tracker: 8, AirDefence.HQ_7_Self_Propelled_LN: 20, AirDefence.HQ_7_Self_Propelled_STR: 24, AirDefence.AAA_8_8cm_Flak_18: 6, AirDefence.AAA_Flak_38: 6, AirDefence.AAA_8_8cm_Flak_36: 8, - AirDefence.AAA_8_8cm_Flak_37: 10, + AirDefence.AAA_8_8cm_Flak_37: 9, AirDefence.AAA_Flak_Vierling_38:6, AirDefence.AAA_Kdo_G_40: 8, AirDefence.Flak_Searchlight_37: 4, AirDefence.Maschinensatz_33: 10, - AirDefence.AAA_8_8cm_Flak_41: 12, + AirDefence.AAA_8_8cm_Flak_41: 10, AirDefence.AAA_Bofors_40mm: 8, # FRENCH PACK MOD diff --git a/gen/sam/aaa_bofors.py b/gen/sam/aaa_bofors.py index ad515931..528edd8b 100644 --- a/gen/sam/aaa_bofors.py +++ b/gen/sam/aaa_bofors.py @@ -11,10 +11,11 @@ class BoforsGenerator(GroupGenerator): """ name = "Bofors AAA" + price = 75 def generate(self): - grid_x = random.randint(2, 4) - grid_y = random.randint(2, 4) + grid_x = random.randint(2, 3) + grid_y = random.randint(2, 3) spacing = random.randint(10,40) diff --git a/gen/sam/aaa_flak.py b/gen/sam/aaa_flak.py index 896b48aa..5a0d9121 100644 --- a/gen/sam/aaa_flak.py +++ b/gen/sam/aaa_flak.py @@ -12,12 +12,13 @@ class FlakGenerator(GroupGenerator): """ name = "Flak Site" + price = 135 def generate(self): - grid_x = random.randint(2, 4) - grid_y = random.randint(2, 4) + grid_x = random.randint(2, 3) + grid_y = random.randint(2, 3) - spacing = random.randint(30,60) + spacing = random.randint(30, 60) index = 0 mixed = random.choice([True, False]) @@ -34,7 +35,7 @@ class FlakGenerator(GroupGenerator): unit_type = random.choice(GFLAK) # Search lights - search_pos = self.get_circular_position(random.randint(2,5), 90) + search_pos = self.get_circular_position(random.randint(2,3), 90) for index, pos in enumerate(search_pos): self.add_unit(AirDefence.Flak_Searchlight_37, "SearchLight#" + str(index), pos[0], pos[1], self.heading) diff --git a/gen/sam/aaa_zu23_insurgent.py b/gen/sam/aaa_zu23_insurgent.py index f9c53620..ec659756 100644 --- a/gen/sam/aaa_zu23_insurgent.py +++ b/gen/sam/aaa_zu23_insurgent.py @@ -11,10 +11,11 @@ class ZU23InsurgentGenerator(GroupGenerator): """ name = "Zu-23 Site" + price = 56 def generate(self): - grid_x = random.randint(2, 4) - grid_y = random.randint(2, 4) + grid_x = random.randint(2, 3) + grid_y = random.randint(2, 3) spacing = random.randint(10,40) diff --git a/gen/sam/sam_avenger.py b/gen/sam/sam_avenger.py index 63844f7b..44d3aed9 100644 --- a/gen/sam/sam_avenger.py +++ b/gen/sam/sam_avenger.py @@ -11,6 +11,7 @@ class AvengerGenerator(GroupGenerator): """ name = "Avenger Group" + price = 62 def generate(self): num_launchers = random.randint(2, 3) diff --git a/gen/sam/sam_chaparral.py b/gen/sam/sam_chaparral.py index 6178df1c..a8d89181 100644 --- a/gen/sam/sam_chaparral.py +++ b/gen/sam/sam_chaparral.py @@ -11,6 +11,7 @@ class ChaparralGenerator(GroupGenerator): """ name = "Chaparral Group" + price = 66 def generate(self): num_launchers = random.randint(2, 4) diff --git a/gen/sam/sam_gepard.py b/gen/sam/sam_gepard.py index 3ece2cc9..501ed7b7 100644 --- a/gen/sam/sam_gepard.py +++ b/gen/sam/sam_gepard.py @@ -11,6 +11,7 @@ class GepardGenerator(GroupGenerator): """ name = "Gepard Group" + price = 50 def generate(self): self.add_unit(AirDefence.SPAAA_Gepard, "SPAAA", self.position.x, self.position.y, self.heading) diff --git a/gen/sam/sam_group_generator.py b/gen/sam/sam_group_generator.py index 2a27d752..add0c9fa 100644 --- a/gen/sam/sam_group_generator.py +++ b/gen/sam/sam_group_generator.py @@ -101,16 +101,22 @@ SAM_PRICES = { AirDefence.HQ_7_Self_Propelled_LN: 35 } + def get_faction_possible_sams_units(faction: str) -> List[UnitType]: """ Return the list :param faction: Faction to search units for - :return: """ return [u for u in db.FACTIONS[faction]["units"] if u in AirDefence.__dict__.values()] -def get_sam_names(): - pass + +def get_faction_possible_sams_generator(faction: str) -> List[UnitType]: + """ + Return the list of possible SAM generator for the given faction + :param faction: Faction to search units for + """ + return [SAM_MAP[u] for u in get_faction_possible_sams_units(faction)] + def generate_anti_air_group(game, parent_cp, ground_object, faction:str): """ diff --git a/gen/sam/sam_hawk.py b/gen/sam/sam_hawk.py index f5273ff9..89c11bc0 100644 --- a/gen/sam/sam_hawk.py +++ b/gen/sam/sam_hawk.py @@ -11,6 +11,7 @@ class HawkGenerator(GroupGenerator): """ name = "Hawk Site" + price = 115 def generate(self): self.add_unit(AirDefence.SAM_Hawk_PCP, "PCP", self.position.x, self.position.y, self.heading) diff --git a/gen/sam/sam_hq7.py b/gen/sam/sam_hq7.py index b32d0226..f8a531ea 100644 --- a/gen/sam/sam_hq7.py +++ b/gen/sam/sam_hq7.py @@ -11,6 +11,7 @@ class HQ7Generator(GroupGenerator): """ name = "HQ-7 Site" + price = 120 def generate(self): self.add_unit(AirDefence.HQ_7_Self_Propelled_STR, "STR", self.position.x, self.position.y, self.heading) diff --git a/gen/sam/sam_linebacker.py b/gen/sam/sam_linebacker.py index add33acb..946d14ed 100644 --- a/gen/sam/sam_linebacker.py +++ b/gen/sam/sam_linebacker.py @@ -11,6 +11,7 @@ class LinebackerGenerator(GroupGenerator): """ name = "Linebacker Group" + price = 75 def generate(self): num_launchers = random.randint(2, 4) diff --git a/gen/sam/sam_patriot.py b/gen/sam/sam_patriot.py index f042e3b1..b55dbaea 100644 --- a/gen/sam/sam_patriot.py +++ b/gen/sam/sam_patriot.py @@ -11,6 +11,7 @@ class PatriotGenerator(GroupGenerator): """ name = "Patriot Battery" + price = 240 def generate(self): # Command Post @@ -20,13 +21,13 @@ class PatriotGenerator(GroupGenerator): self.add_unit(AirDefence.SAM_Patriot_EPP_III, "EPP", self.position.x, self.position.y + 30, self.heading) self.add_unit(AirDefence.SAM_Patriot_STR_AN_MPQ_53, "ICC", self.position.x + 30, self.position.y + 30, self.heading) - num_launchers = random.randint(2, 4) + num_launchers = random.randint(3, 4) positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=360) for i, position in enumerate(positions): self.add_unit(AirDefence.SAM_Patriot_LN_M901, "LN#" + str(i), position[0], position[1], position[2]) # Short range protection for high value site - num_launchers = random.randint(2, 4) + num_launchers = random.randint(3, 4) positions = self.get_circular_position(num_launchers, launcher_distance=300, coverage=360) for i, position in enumerate(positions): self.add_unit(AirDefence.AAA_Vulcan_M163, "SPAAA#" + str(i), position[0], position[1], position[2]) diff --git a/gen/sam/sam_rapier.py b/gen/sam/sam_rapier.py index 0cbffd9e..99b7b205 100644 --- a/gen/sam/sam_rapier.py +++ b/gen/sam/sam_rapier.py @@ -11,6 +11,7 @@ class RapierGenerator(GroupGenerator): """ name = "Rapier AA Site" + price = 50 def generate(self): self.add_unit(AirDefence.Rapier_FSA_Blindfire_Tracker, "BT", self.position.x, self.position.y, self.heading) diff --git a/gen/sam/sam_roland.py b/gen/sam/sam_roland.py index 22f60435..9e31d5fe 100644 --- a/gen/sam/sam_roland.py +++ b/gen/sam/sam_roland.py @@ -9,6 +9,7 @@ class RolandGenerator(GroupGenerator): """ name = "Roland Site" + price = 40 def generate(self): self.add_unit(AirDefence.SAM_Roland_ADS, "ADS", self.position.x, self.position.y, self.heading) diff --git a/gen/sam/sam_sa10.py b/gen/sam/sam_sa10.py index c90d3ae9..ae2102f0 100644 --- a/gen/sam/sam_sa10.py +++ b/gen/sam/sam_sa10.py @@ -11,6 +11,7 @@ class SA10Generator(GroupGenerator): """ name = "SA-10/S-300PS Battery" + price = 450 def generate(self): # Command Post diff --git a/gen/sam/sam_sa11.py b/gen/sam/sam_sa11.py index c05fe21a..3af6c242 100644 --- a/gen/sam/sam_sa11.py +++ b/gen/sam/sam_sa11.py @@ -11,6 +11,7 @@ class SA11Generator(GroupGenerator): """ name = "SA-11 Buk Battery" + price = 180 def generate(self): self.add_unit(AirDefence.SAM_SA_11_Buk_CC_9S470M1, "CC", self.position.x, self.position.y, self.heading) diff --git a/gen/sam/sam_sa13.py b/gen/sam/sam_sa13.py index 366746d7..8fc069ad 100644 --- a/gen/sam/sam_sa13.py +++ b/gen/sam/sam_sa13.py @@ -11,6 +11,7 @@ class SA13Generator(GroupGenerator): """ name = "SA-13 Strela Group" + price = 50 def generate(self): self.add_unit(Unarmed.Transport_UAZ_469, "UAZ", self.position.x, self.position.y, self.heading) diff --git a/gen/sam/sam_sa15.py b/gen/sam/sam_sa15.py index 0aa9c162..09fda2ee 100644 --- a/gen/sam/sam_sa15.py +++ b/gen/sam/sam_sa15.py @@ -9,6 +9,7 @@ class SA15Generator(GroupGenerator): """ name = "SA-15 Tor Group" + price = 55 def generate(self): self.add_unit(AirDefence.SAM_SA_15_Tor_9A331, "ADS", self.position.x, self.position.y, self.heading) diff --git a/gen/sam/sam_sa19.py b/gen/sam/sam_sa19.py index 10923a5a..c4f710f4 100644 --- a/gen/sam/sam_sa19.py +++ b/gen/sam/sam_sa19.py @@ -11,6 +11,7 @@ class SA19Generator(GroupGenerator): """ name = "SA-19 Tunguska Group" + price = 90 def generate(self): num_launchers = random.randint(1, 3) diff --git a/gen/sam/sam_sa2.py b/gen/sam/sam_sa2.py index eb7f014b..c108c1e8 100644 --- a/gen/sam/sam_sa2.py +++ b/gen/sam/sam_sa2.py @@ -11,6 +11,7 @@ class SA2Generator(GroupGenerator): """ name = "SA-2/S-75 Site" + price = 74 def generate(self): self.add_unit(AirDefence.SAM_SR_P_19, "SR", self.position.x, self.position.y, self.heading) diff --git a/gen/sam/sam_sa3.py b/gen/sam/sam_sa3.py index 9a73de13..455bab19 100644 --- a/gen/sam/sam_sa3.py +++ b/gen/sam/sam_sa3.py @@ -11,6 +11,7 @@ class SA3Generator(GroupGenerator): """ name = "SA-3/S-125 Site" + price = 80 def generate(self): self.add_unit(AirDefence.SAM_SR_P_19, "SR", self.position.x, self.position.y, self.heading) diff --git a/gen/sam/sam_sa6.py b/gen/sam/sam_sa6.py index f96d3989..7ec2afca 100644 --- a/gen/sam/sam_sa6.py +++ b/gen/sam/sam_sa6.py @@ -11,6 +11,7 @@ class SA6Generator(GroupGenerator): """ name = "SA-6 Kub Site" + price = 102 def generate(self): self.add_unit(AirDefence.SAM_SA_6_Kub_STR_9S91, "STR", self.position.x, self.position.y, self.heading) diff --git a/gen/sam/sam_sa8.py b/gen/sam/sam_sa8.py index a5df4d85..1c09dd2e 100644 --- a/gen/sam/sam_sa8.py +++ b/gen/sam/sam_sa8.py @@ -11,6 +11,7 @@ class SA8Generator(GroupGenerator): """ name = "SA-8 OSA Site" + price = 55 def generate(self): self.add_unit(AirDefence.SAM_SA_8_Osa_9A33, "OSA", self.position.x, self.position.y, self.heading) diff --git a/gen/sam/sam_sa9.py b/gen/sam/sam_sa9.py index 9eeffe3c..d0045bea 100644 --- a/gen/sam/sam_sa9.py +++ b/gen/sam/sam_sa9.py @@ -11,6 +11,7 @@ class SA9Generator(GroupGenerator): """ name = "SA-9 Group" + price = 40 def generate(self): self.add_unit(Unarmed.Transport_UAZ_469, "UAZ", self.position.x, self.position.y, self.heading) diff --git a/gen/sam/sam_vulcan.py b/gen/sam/sam_vulcan.py index 71789e62..77cfc0a2 100644 --- a/gen/sam/sam_vulcan.py +++ b/gen/sam/sam_vulcan.py @@ -11,6 +11,7 @@ class VulcanGenerator(GroupGenerator): """ name = "Vulcan Group" + price = 25 def generate(self): self.add_unit(AirDefence.AAA_Vulcan_M163, "SPAAA", self.position.x, self.position.y, self.heading) diff --git a/gen/sam/sam_zsu23.py b/gen/sam/sam_zsu23.py index 33d1cde3..7c90cb4d 100644 --- a/gen/sam/sam_zsu23.py +++ b/gen/sam/sam_zsu23.py @@ -11,9 +11,10 @@ class ZSU23Generator(GroupGenerator): """ name = "ZSU-23 Group" + price = 50 def generate(self): - num_launchers = random.randint(2, 5) + num_launchers = random.randint(4, 5) positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=180) for i, position in enumerate(positions): diff --git a/gen/sam/sam_zu23.py b/gen/sam/sam_zu23.py index 1f4521cd..3134c3a7 100644 --- a/gen/sam/sam_zu23.py +++ b/gen/sam/sam_zu23.py @@ -11,10 +11,11 @@ class ZU23Generator(GroupGenerator): """ name = "ZU-23 Group" + price = 54 def generate(self): - grid_x = random.randint(2, 4) - grid_y = random.randint(2, 4) + grid_x = random.randint(2, 3) + grid_y = random.randint(2, 3) spacing = random.randint(10,40) diff --git a/gen/sam/sam_zu23_ural.py b/gen/sam/sam_zu23_ural.py index d6b08fac..1eb31b22 100644 --- a/gen/sam/sam_zu23_ural.py +++ b/gen/sam/sam_zu23_ural.py @@ -11,6 +11,7 @@ class ZU23UralGenerator(GroupGenerator): """ name = "ZU-23 Ural Group" + price = 64 def generate(self): num_launchers = random.randint(2, 8) diff --git a/gen/sam/sam_zu23_ural_insurgent.py b/gen/sam/sam_zu23_ural_insurgent.py index 55c72196..4512cfc7 100644 --- a/gen/sam/sam_zu23_ural_insurgent.py +++ b/gen/sam/sam_zu23_ural_insurgent.py @@ -11,6 +11,7 @@ class ZU23UralInsurgentGenerator(GroupGenerator): """ name = "ZU-23 Ural Insurgent Group" + price = 64 def generate(self): num_launchers = random.randint(2, 8) diff --git a/qt_ui/widgets/QTopPanel.py b/qt_ui/widgets/QTopPanel.py index 2a77cb23..30725095 100644 --- a/qt_ui/widgets/QTopPanel.py +++ b/qt_ui/widgets/QTopPanel.py @@ -21,6 +21,7 @@ class QTopPanel(QFrame): self.setMaximumHeight(70) self.init_ui() GameUpdateSignal.get_instance().gameupdated.connect(self.setGame) + GameUpdateSignal.get_instance().budgetupdated.connect(self.budget_update) def init_ui(self): @@ -101,4 +102,7 @@ class QTopPanel(QFrame): def proceed(self): self.subwindow = QMissionPlanning(self.game) - self.subwindow.show() \ No newline at end of file + self.subwindow.show() + + def budget_update(self, game:Game): + self.budgetBox.setGame(game) diff --git a/qt_ui/windows/GameUpdateSignal.py b/qt_ui/windows/GameUpdateSignal.py index 86546757..dd32dd58 100644 --- a/qt_ui/windows/GameUpdateSignal.py +++ b/qt_ui/windows/GameUpdateSignal.py @@ -16,6 +16,7 @@ class GameUpdateSignal(QObject): instance = None gameupdated = Signal(Game) + budgetupdated = Signal(Game) debriefingReceived = Signal(DebriefingSignal) def __init__(self): @@ -25,6 +26,9 @@ class GameUpdateSignal(QObject): def updateGame(self, game: Game): self.gameupdated.emit(game) + def updateBudget(self, game: Game): + self.budgetupdated.emit(game) + def sendDebriefing(self, game: Game, gameEvent: Event, debriefing: Debriefing): sig = DebriefingSignal(game, gameEvent, debriefing) self.gameupdated.emit(game) diff --git a/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py b/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py index 2b2abb67..836c1772 100644 --- a/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py +++ b/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py @@ -1,3 +1,4 @@ +from PySide2.QtCore import Qt from PySide2.QtWidgets import QGridLayout, QLabel, QGroupBox, QPushButton from qt_ui.uiconstants import VEHICLES_ICONS @@ -28,21 +29,21 @@ class QBaseDefenseGroupInfo(QGroupBox): unit_dict[u.type] = 1 i = 0 for k, v in unit_dict.items(): - #icon = QLabel() - #if k in VEHICLES_ICONS.keys(): - # icon.setPixmap(VEHICLES_ICONS[k]) - #else: - # icon.setText("" + k[:6] + "") - #icon.setProperty("style", "icon-plane") - #layout.addWidget(icon, i, 0) - layout.addWidget(QLabel(str(v) + " x " + "" + k + ""), i, 0) + icon = QLabel() + if k in VEHICLES_ICONS.keys(): + icon.setPixmap(VEHICLES_ICONS[k]) + else: + icon.setText("" + k[:6] + "") + icon.setProperty("style", "icon-plane") + layout.addWidget(icon, i, 0) + layout.addWidget(QLabel(str(v) + " x " + "" + k + ""), i, 1) i = i + 1 manage_button = QPushButton("Manage") manage_button.setProperty("style", "btn-success") manage_button.setMaximumWidth(180) manage_button.clicked.connect(self.onManage) - layout.addWidget(manage_button, i+1, 0) + layout.addWidget(manage_button, i, 0, Qt.AlignLeft) self.setLayout(layout) def onManage(self): diff --git a/qt_ui/windows/groundobject/QGroundObjectMenu.py b/qt_ui/windows/groundobject/QGroundObjectMenu.py index 6e67d0c7..6b8c9ed1 100644 --- a/qt_ui/windows/groundobject/QGroundObjectMenu.py +++ b/qt_ui/windows/groundobject/QGroundObjectMenu.py @@ -1,12 +1,16 @@ import logging -from PySide2.QtGui import QCloseEvent -from PySide2.QtWidgets import QHBoxLayout, QWidget, QDialog, QGridLayout, QLabel, QGroupBox, QVBoxLayout, QPushButton +from PySide2 import QtCore +from PySide2.QtGui import QCloseEvent, Qt +from PySide2.QtWidgets import QHBoxLayout, QDialog, QGridLayout, QLabel, QGroupBox, QVBoxLayout, QPushButton, \ + QComboBox, QSpinBox, QMessageBox from dcs import Point -from game import Game +from game import Game, db from game.data.building_data import FORTIFICATION_BUILDINGS -from game.db import PRICES, unit_type_of +from game.db import PRICES, unit_type_of, PinpointStrike +from gen.defenses.armor_group_generator import generate_armor_group +from gen.sam.sam_group_generator import get_faction_possible_sams_generator from qt_ui.uiconstants import EVENT_ICONS from qt_ui.widgets.QBudgetBox import QBudgetBox from qt_ui.windows.GameUpdateSignal import GameUpdateSignal @@ -29,6 +33,7 @@ class QGroundObjectMenu(QDialog): self.buildingBox = QGroupBox("Buildings :") self.intelLayout = QGridLayout() self.buildingsLayout = QGridLayout() + self.sell_all_button = None self.total_value = 0 self.init_ui() @@ -46,11 +51,20 @@ class QGroundObjectMenu(QDialog): self.mainLayout.addWidget(self.buildingBox) self.actionLayout = QHBoxLayout() - sell_all_button = QPushButton("Disband (+" + str(self.total_value) + "M)") - sell_all_button.clicked.connect(self.sell_all) - self.actionLayout.addWidget(sell_all_button) + + self.sell_all_button = QPushButton("Disband (+" + str(self.total_value) + "M)") + self.sell_all_button.clicked.connect(self.sell_all) + self.sell_all_button.setProperty("style", "btn-danger") + + self.buy_replace = QPushButton("Buy/Replace") + self.buy_replace.clicked.connect(self.buy_group) + self.buy_replace.setProperty("style", "btn-success") if self.total_value > 0: + self.actionLayout.addWidget(self.sell_all_button) + self.actionLayout.addWidget(self.buy_replace) + + if self.cp.captured: self.mainLayout.addLayout(self.actionLayout) self.setLayout(self.mainLayout) @@ -82,6 +96,9 @@ class QGroundObjectMenu(QDialog): repair.clicked.connect(lambda u=u, g=g, p=price: self.repair_unit(g, u, p)) self.intelLayout.addWidget(repair, i, 1) i = i + 1 + stretch = QVBoxLayout() + stretch.addStretch() + self.intelLayout.addLayout(stretch, i, 0) self.buildingBox = QGroupBox("Buildings :") self.buildingsLayout = QGridLayout() @@ -97,12 +114,27 @@ class QGroundObjectMenu(QDialog): def do_refresh_layout(self): try: for i in range(self.mainLayout.count()): - self.mainLayout.removeItem(self.mainLayout.itemAt(i)) + item = self.mainLayout.itemAt(i) + if item is not None and item.widget() is not None: + item.widget().setParent(None) + self.sell_all_button.setParent(None) + self.buy_replace.setParent(None) + self.actionLayout.setParent(None) + self.doLayout() - if len(self.ground_object.groups) > 0: + if self.ground_object.dcs_identifier == "AA": self.mainLayout.addWidget(self.intelBox) else: self.mainLayout.addWidget(self.buildingBox) + + self.actionLayout = QHBoxLayout() + if self.total_value > 0: + self.actionLayout.addWidget(self.sell_all_button) + self.actionLayout.addWidget(self.buy_replace) + + if self.cp.captured: + self.mainLayout.addLayout(self.actionLayout) + except Exception as e: print(e) self.update_total_value() @@ -116,6 +148,8 @@ class QGroundObjectMenu(QDialog): total_value = total_value + PRICES[utype] else: total_value = total_value + 1 + if self.sell_all_button is not None: + self.sell_all_button.setText("Disband (+$" + str(self.total_value) + "M)") self.total_value = total_value def repair_unit(self, group, unit, price): @@ -140,10 +174,163 @@ class QGroundObjectMenu(QDialog): self.update_total_value() self.game.budget = self.game.budget + self.total_value self.ground_object.groups = [] - GameUpdateSignal.get_instance().updateGame(self.game) + self.do_refresh_layout() + GameUpdateSignal.get_instance().updateBudget(self.game) def buy_group(self): - pass + self.subwindow = QBuyGroupForGroundObjectDialog(self, self.ground_object, self.cp, self.game, self.total_value) + self.subwindow.changed.connect(self.do_refresh_layout) + self.subwindow.show() + def closeEvent(self, closeEvent: QCloseEvent): pass + + +class QBuyGroupForGroundObjectDialog(QDialog): + + changed = QtCore.Signal() + + def __init__(self, parent, ground_object: TheaterGroundObject, cp: ControlPoint, game: Game, current_group_value: int): + super(QBuyGroupForGroundObjectDialog, self).__init__(parent) + + self.setMinimumWidth(350) + self.ground_object = ground_object + self.cp = cp + self.game = game + self.current_group_value = current_group_value + + self.setWindowTitle("Buy units @ " + self.ground_object.obj_name) + self.setWindowIcon(EVENT_ICONS["capture"]) + + self.buySamButton = QPushButton("Buy") + self.buyArmorButton = QPushButton("Buy") + self.buySamLayout = QGridLayout() + self.buyArmorLayout = QGridLayout() + self.amount = QSpinBox() + self.buyArmorCombo = QComboBox() + self.samCombo = QComboBox() + self.buySamBox = QGroupBox("Buy SAM site :") + self.buyArmorBox = QGroupBox("Buy defensive position :") + + + + self.init_ui() + + def init_ui(self): + faction = self.game.player_name + + # Sams + + possible_sams = get_faction_possible_sams_generator(faction) + for sam in possible_sams: + self.samCombo.addItem(sam.name + " [$" + str(sam.price) + "M]", userData=sam) + self.samCombo.currentIndexChanged.connect(self.samComboChanged) + + self.buySamLayout.addWidget(QLabel("Site Type :"), 0, 0, Qt.AlignLeft) + self.buySamLayout.addWidget(self.samCombo, 0, 1, alignment=Qt.AlignRight) + self.buySamLayout.addWidget(self.buySamButton, 1, 1, alignment=Qt.AlignRight) + stretch = QVBoxLayout() + stretch.addStretch() + self.buySamLayout.addLayout(stretch, 2, 0) + + self.buySamButton.clicked.connect(self.buySam) + + # Armored units + + armored_units = db.find_unittype(PinpointStrike, faction) # Todo : refactor this legacy nonsense + for unit in set(armored_units): + self.buyArmorCombo.addItem(db.unit_type_name_2(unit) + " [$" + str(db.PRICES[unit]) + "M]", userData=unit) + self.buyArmorCombo.currentIndexChanged.connect(self.armorComboChanged) + + self.amount.setMinimum(2) + self.amount.setMaximum(8) + self.amount.setValue(2) + self.amount.valueChanged.connect(self.amountComboChanged) + + self.buyArmorLayout.addWidget(QLabel("Unit type :"), 0, 0, Qt.AlignLeft) + self.buyArmorLayout.addWidget(self.buyArmorCombo, 0, 1, alignment=Qt.AlignRight) + self.buyArmorLayout.addWidget(QLabel("Group size :"), 1, 0, alignment=Qt.AlignLeft) + self.buyArmorLayout.addWidget(self.amount, 1, 1, alignment=Qt.AlignRight) + self.buyArmorLayout.addWidget(self.buyArmorButton, 2, 1, alignment=Qt.AlignRight) + stretch2 = QVBoxLayout() + stretch2.addStretch() + self.buyArmorLayout.addLayout(stretch2, 3, 0) + + self.buyArmorButton.clicked.connect(self.buyArmor) + + # Do layout + self.buySamBox.setLayout(self.buySamLayout) + self.buyArmorBox.setLayout(self.buyArmorLayout) + + self.mainLayout = QHBoxLayout() + self.mainLayout.addWidget(self.buySamBox) + + if self.ground_object.airbase_group: + self.mainLayout.addWidget(self.buyArmorBox) + + self.setLayout(self.mainLayout) + + try: + self.samComboChanged(0) + self.armorComboChanged(0) + except: + pass + + def samComboChanged(self, index): + self.buySamButton.setText("Buy [$" + str(self.samCombo.itemData(index).price) + "M] [-$" + str(self.current_group_value) + "M]") + + def armorComboChanged(self, index): + self.buyArmorButton.setText("Buy [$" + str(db.PRICES[self.buyArmorCombo.itemData(index)] * self.amount.value()) + "M][-$" + str(self.current_group_value) + "M]") + + def amountComboChanged(self): + self.buyArmorButton.setText("Buy [$" + str(db.PRICES[self.buyArmorCombo.itemData(self.buyArmorCombo.currentIndex())] * self.amount.value()) + "M][-$" + str(self.current_group_value) + "M]") + + def buyArmor(self): + utype = self.buyArmorCombo.itemData(self.buyArmorCombo.currentIndex()) + price = db.PRICES[utype] * self.amount.value() - self.current_group_value + if price > self.game.budget: + self.error_money() + self.close() + return + else: + self.game.budget -= price + + # Generate Armor + group = generate_armor_group(self.game.player_name, self.game, self.ground_object) + self.ground_object.groups = [group] + + GameUpdateSignal.get_instance().updateBudget(self.game) + + self.changed.emit() + self.close() + + def buySam(self): + sam_generator = self.samCombo.itemData(self.samCombo.currentIndex()) + price = sam_generator.price - self.current_group_value + if price > self.game.budget: + self.error_money() + return + else: + self.game.budget -= price + + # Generate SAM + generator = sam_generator(self.game, self.ground_object) + generator.generate() + generated_group = generator.get_generated_group() + self.ground_object.groups = [generated_group] + + GameUpdateSignal.get_instance().updateBudget(self.game) + + self.changed.emit() + self.close() + + def error_money(self): + msg = QMessageBox() + msg.setIcon(QMessageBox.Information) + msg.setText("Not enough money to buy these units !") + msg.setWindowTitle("Not enough money") + msg.setStandardButtons(QMessageBox.Ok) + msg.setWindowFlags(Qt.WindowStaysOnTopHint) + msg.exec_() + self.close() \ No newline at end of file From 5042ac1789b5f2fd7bc1fb9dd5189a3bb37a0746 Mon Sep 17 00:00:00 2001 From: Khopa Date: Sun, 20 Sep 2020 17:07:09 +0200 Subject: [PATCH 50/61] P-51/P-47 radios support. --- gen/aircraft.py | 38 ++++++++++++++++++++++++++++++++++++++ gen/radios.py | 4 ++++ 2 files changed, 42 insertions(+) diff --git a/gen/aircraft.py b/gen/aircraft.py index 1ff12b33..4911c916 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -128,6 +128,19 @@ class ViperChannelNamer(ChannelNamer): return f"COM{radio_id} Ch {channel_id}" +class SCR522ChannelNamer(ChannelNamer): + """ + Channel namer for P-51 & P-47D + """ + + @staticmethod + def channel_name(radio_id: int, channel_id: int) -> str: + if channel_id > 3: + return "?" + else: + return f"Button " + "ABCD"[channel_id - 1] + + @dataclass(frozen=True) class ChannelAssignment: radio_id: int @@ -325,6 +338,19 @@ class ViggenRadioChannelAllocator(RadioChannelAllocator): # TODO: Assign divert to 6 when we support divert airfields. +@dataclass(frozen=True) +class SCR522RadioChannelAllocator(RadioChannelAllocator): + """Preset channel allocator for the SCR522 WW2 radios. (4 channels)""" + + def assign_channels_for_flight(self, flight: FlightData, + air_support: AirSupport) -> None: + radio_id = 1 + flight.assign_channel(radio_id, 1, flight.intra_flight_channel) + flight.assign_channel(radio_id, 2, flight.departure.atc) + flight.assign_channel(radio_id, 3, flight.arrival.atc) + + # TODO : Some GCI on Channel 4 ? + @dataclass(frozen=True) class AircraftData: """Additional aircraft data not exposed by pydcs.""" @@ -427,7 +453,19 @@ AIRCRAFT_DATA: Dict[str, AircraftData] = { ), channel_namer=MirageChannelNamer ), + + "P-51D": AircraftData( + inter_flight_radio=get_radio("SCR522"), + intra_flight_radio=get_radio("SCR522"), + channel_allocator=CommonRadioChannelAllocator( + inter_flight_radio_index=1, + intra_flight_radio_index=1 + ), + channel_namer=SCR522ChannelNamer + ), } +AIRCRAFT_DATA["P-51D-30-NA"] = AIRCRAFT_DATA["P-51D"] +AIRCRAFT_DATA["P-47D-30"] = AIRCRAFT_DATA["P-51D"] class AircraftConflictGenerator: diff --git a/gen/radios.py b/gen/radios.py index 5210ba16..bf4f1447 100644 --- a/gen/radios.py +++ b/gen/radios.py @@ -120,6 +120,10 @@ RADIOS: List[Radio] = [ # Also capable of [103, 156) at 25 kHz intervals, but we can't do gaps. Radio("FR 22", MHz(225), MHz(400), step=kHz(50)), + # P-51 / P-47 Radio + # 4 preset channels (A/B/C/D) + Radio("SCR522", MHz(100), MHz(156), step=kHz(25)), + Radio("R&S M3AR VHF", MHz(108), MHz(174), step=MHz(1)), Radio("R&S M3AR UHF", MHz(225), MHz(400), step=MHz(1)), ] From 4019da8ba93457c9e435358952718b02223adb33 Mon Sep 17 00:00:00 2001 From: Khopa Date: Sun, 20 Sep 2020 17:15:43 +0200 Subject: [PATCH 51/61] Base defense armor unit icon style fix. --- .../basemenu/base_defenses/QBaseDefenseGroupInfo.py | 5 ++--- resources/stylesheets/style-dcs.css | 9 +++++++++ resources/stylesheets/style.css | 9 +++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py b/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py index 836c1772..4ace3577 100644 --- a/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py +++ b/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py @@ -17,7 +17,6 @@ class QBaseDefenseGroupInfo(QGroupBox): self.init_ui() - def init_ui(self): unit_dict = {} layout = QGridLayout() @@ -33,8 +32,8 @@ class QBaseDefenseGroupInfo(QGroupBox): if k in VEHICLES_ICONS.keys(): icon.setPixmap(VEHICLES_ICONS[k]) else: - icon.setText("" + k[:6] + "") - icon.setProperty("style", "icon-plane") + icon.setText("" + k[:9] + "") + icon.setProperty("style", "icon-armor") layout.addWidget(icon, i, 0) layout.addWidget(QLabel(str(v) + " x " + "" + k + ""), i, 1) i = i + 1 diff --git a/resources/stylesheets/style-dcs.css b/resources/stylesheets/style-dcs.css index 57b2478b..bd866559 100644 --- a/resources/stylesheets/style-dcs.css +++ b/resources/stylesheets/style-dcs.css @@ -198,6 +198,15 @@ QLabel[style="icon-plane"]{ color:white; } +QLabel[style="icon-armor"]{ + background-color:#48719D; + min-height:24px; + max-width: 64px; + border: 1px solid black; + text-align:center; + color:white; +} + QLabel[style="BARCAP"]{ border: 1px solid black; background-color: #445299; diff --git a/resources/stylesheets/style.css b/resources/stylesheets/style.css index b382f642..ea65ffd1 100644 --- a/resources/stylesheets/style.css +++ b/resources/stylesheets/style.css @@ -106,6 +106,15 @@ QLabel[style="icon-plane"]{ color:white; } +QLabel[style="icon-armor"]{ + background-color:#48719D; + min-height:24px; + max-width: 64px; + border: 1px solid black; + text-align:center; + color:white; +} + QLabel[style="bordered"]{ border: 1px solid black; } From a70e035e02f5c5313bc487b6dbfa953a023c301c Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sun, 20 Sep 2020 15:07:32 -0700 Subject: [PATCH 52/61] Update pydcs. This fixes the issue where delayed AI flights would not start if the mission was not first saved via the DCS mission editor. --- pydcs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydcs b/pydcs index f0f9754b..f46781b8 160000 --- a/pydcs +++ b/pydcs @@ -1 +1 @@ -Subproject commit f0f9754b97cf08f71c2b7f5f9687b679b4f0d69d +Subproject commit f46781b854102a9f06948c8fb81a40331b78459e From 0e2a449553c50873840385f8723c65a66de1432a Mon Sep 17 00:00:00 2001 From: Khopa Date: Wed, 23 Sep 2020 00:39:01 +0200 Subject: [PATCH 53/61] Fixes to Buy/Sell sam UI --- .../base_defenses/QBaseDefenseGroupInfo.py | 51 +++++++++++++------ .../base_defenses/QBaseInformation.py | 1 - .../windows/groundobject/QGroundObjectMenu.py | 11 ++-- 3 files changed, 41 insertions(+), 22 deletions(-) diff --git a/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py b/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py index 4ace3577..bafd70ee 100644 --- a/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py +++ b/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py @@ -1,5 +1,5 @@ from PySide2.QtCore import Qt -from PySide2.QtWidgets import QGridLayout, QLabel, QGroupBox, QPushButton +from PySide2.QtWidgets import QGridLayout, QLabel, QGroupBox, QPushButton, QVBoxLayout from qt_ui.uiconstants import VEHICLES_ICONS from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu @@ -8,18 +8,40 @@ from theater import ControlPoint, TheaterGroundObject class QBaseDefenseGroupInfo(QGroupBox): - def __init__(self, cp:ControlPoint, ground_object: TheaterGroundObject, game): + def __init__(self, cp: ControlPoint, ground_object: TheaterGroundObject, game): super(QBaseDefenseGroupInfo, self).__init__("Group : " + ground_object.obj_name) self.ground_object = ground_object self.cp = cp self.game = game self.buildings = game.theater.find_ground_objects_by_obj_name(self.ground_object.obj_name) + + self.main_layout = QVBoxLayout() + self.unit_layout = QGridLayout() + self.init_ui() - def init_ui(self): + + self.buildLayout() + manage_button = QPushButton("Manage") + manage_button.setProperty("style", "btn-success") + manage_button.setMaximumWidth(180) + manage_button.clicked.connect(self.onManage) + + self.main_layout.addLayout(self.unit_layout) + self.main_layout.addWidget(manage_button, 0, Qt.AlignLeft) + + self.setLayout(self.main_layout) + + def buildLayout(self): unit_dict = {} - layout = QGridLayout() + for i in range(self.unit_layout.count()): + item = self.unit_layout.itemAt(i) + if item is not None and item.widget() is not None: + self.unit_layout.removeItem(item) + item.widget().setParent(None) + item.widget().deleteLater() + for g in self.ground_object.groups: for u in g.units: if u.type in unit_dict.keys(): @@ -32,21 +54,18 @@ class QBaseDefenseGroupInfo(QGroupBox): if k in VEHICLES_ICONS.keys(): icon.setPixmap(VEHICLES_ICONS[k]) else: - icon.setText("" + k[:9] + "") + icon.setText("" + k[:8] + "") icon.setProperty("style", "icon-armor") - layout.addWidget(icon, i, 0) - layout.addWidget(QLabel(str(v) + " x " + "" + k + ""), i, 1) + self.unit_layout.addWidget(icon, i, 0) + self.unit_layout.addWidget(QLabel(str(v) + " x " + "" + k + ""), i, 1) i = i + 1 - manage_button = QPushButton("Manage") - manage_button.setProperty("style", "btn-success") - manage_button.setMaximumWidth(180) - manage_button.clicked.connect(self.onManage) - layout.addWidget(manage_button, i, 0, Qt.AlignLeft) - self.setLayout(layout) + self.setLayout(self.main_layout) def onManage(self): - self.editionMenu = QGroundObjectMenu(self.window(), self.ground_object, self.buildings, self.cp, self.game) - self.editionMenu.show() - + self.edition_menu = QGroundObjectMenu(self.window(), self.ground_object, self.buildings, self.cp, self.game) + self.edition_menu.show() + self.edition_menu.changed.connect(self.onEdition) + def onEdition(self): + self.buildLayout() \ No newline at end of file diff --git a/qt_ui/windows/basemenu/base_defenses/QBaseInformation.py b/qt_ui/windows/basemenu/base_defenses/QBaseInformation.py index c98113b6..f5325887 100644 --- a/qt_ui/windows/basemenu/base_defenses/QBaseInformation.py +++ b/qt_ui/windows/basemenu/base_defenses/QBaseInformation.py @@ -23,7 +23,6 @@ class QBaseInformation(QFrame): scroll_content = QWidget() task_box_layout = QGridLayout() scroll_content.setLayout(task_box_layout) - row = 0 for g in self.cp.ground_objects: if g.airbase_group: diff --git a/qt_ui/windows/groundobject/QGroundObjectMenu.py b/qt_ui/windows/groundobject/QGroundObjectMenu.py index 6b8c9ed1..6d28c357 100644 --- a/qt_ui/windows/groundobject/QGroundObjectMenu.py +++ b/qt_ui/windows/groundobject/QGroundObjectMenu.py @@ -20,6 +20,8 @@ from theater import ControlPoint, TheaterGroundObject class QGroundObjectMenu(QDialog): + changed = QtCore.Signal() + def __init__(self, parent, ground_object: TheaterGroundObject, buildings:[], cp: ControlPoint, game: Game): super(QGroundObjectMenu, self).__init__(parent) self.setMinimumWidth(350) @@ -64,7 +66,7 @@ class QGroundObjectMenu(QDialog): self.actionLayout.addWidget(self.sell_all_button) self.actionLayout.addWidget(self.buy_replace) - if self.cp.captured: + if self.cp.captured and self.ground_object.dcs_identifier == "AA": self.mainLayout.addLayout(self.actionLayout) self.setLayout(self.mainLayout) @@ -132,12 +134,13 @@ class QGroundObjectMenu(QDialog): self.actionLayout.addWidget(self.sell_all_button) self.actionLayout.addWidget(self.buy_replace) - if self.cp.captured: + if self.cp.captured and self.ground_object.dcs_identifier == "AA": self.mainLayout.addLayout(self.actionLayout) except Exception as e: print(e) self.update_total_value() + self.changed.emit() def update_total_value(self): total_value = 0 @@ -169,6 +172,7 @@ class QGroundObjectMenu(QDialog): logging.info("Repaired unit : " + str(unit.id) + " " + str(unit.type)) self.do_refresh_layout() + self.changed.emit() def sell_all(self): self.update_total_value() @@ -183,9 +187,6 @@ class QGroundObjectMenu(QDialog): self.subwindow.show() - def closeEvent(self, closeEvent: QCloseEvent): - pass - class QBuyGroupForGroundObjectDialog(QDialog): From 266927aa9a3d315c80fb49ffdfb6adad3e4b4dd3 Mon Sep 17 00:00:00 2001 From: Khopa Date: Thu, 24 Sep 2020 18:01:20 +0200 Subject: [PATCH 54/61] Added new payloads for F-16C for SEAD and CAS, added defaults payload for new P-47 variants. --- resources/customized_payloads/F-16C_50.lua | 132 +++++++++--------- resources/customized_payloads/P-47D-30.lua | 63 ++++++--- resources/customized_payloads/P-47D-30bl1.lua | 96 +++++++++++++ resources/customized_payloads/P-47D-40.lua | 88 ++++++++++++ 4 files changed, 289 insertions(+), 90 deletions(-) create mode 100644 resources/customized_payloads/P-47D-30bl1.lua create mode 100644 resources/customized_payloads/P-47D-40.lua diff --git a/resources/customized_payloads/F-16C_50.lua b/resources/customized_payloads/F-16C_50.lua index d48ad19e..285bccf3 100644 --- a/resources/customized_payloads/F-16C_50.lua +++ b/resources/customized_payloads/F-16C_50.lua @@ -2,41 +2,84 @@ local unitPayloads = { ["name"] = "F-16C_50", ["payloads"] = { [1] = { - ["name"] = "ANTISHIP", + ["name"] = "CAS", ["pylons"] = { [1] = { - ["CLSID"] = "LAU3_HE5", - ["num"] = 6, + ["CLSID"] = "{8A0BE8AE-58D4-4572-9263-3144C0D06364}", + ["num"] = 5, }, [2] = { - ["CLSID"] = "LAU3_HE5", + ["CLSID"] = "{DAC53A2F-79CA-42FF-A77A-F5649B601308}", ["num"] = 7, }, [3] = { - ["CLSID"] = "LAU3_HE5", - ["num"] = 4, - }, - [4] = { - ["CLSID"] = "LAU3_HE5", + ["CLSID"] = "{DAC53A2F-79CA-42FF-A77A-F5649B601308}", ["num"] = 3, }, - [5] = { + [4] = { ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", ["num"] = 2, }, - [6] = { + [5] = { ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", ["num"] = 1, }, - [7] = { + [6] = { ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", ["num"] = 8, }, - [8] = { + [7] = { ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", ["num"] = 9, }, + [8] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 4, + }, [9] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 6, + }, + [10] = { + ["CLSID"] = "{A111396E-D3E8-4b9c-8AC9-2432489304D5}", + ["num"] = 11, + }, + }, + ["tasks"] = { + }, + }, + [2] = { + ["name"] = "ANTISHIP", + ["pylons"] = { + [1] = { + ["CLSID"] = "{DAC53A2F-79CA-42FF-A77A-F5649B601308}", + ["num"] = 7, + }, + [2] = { + ["CLSID"] = "{DAC53A2F-79CA-42FF-A77A-F5649B601308}", + ["num"] = 3, + }, + [3] = { + ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", + ["num"] = 2, + }, + [4] = { + ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", + ["num"] = 1, + }, + [5] = { + ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", + ["num"] = 8, + }, + [6] = { + ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", + ["num"] = 9, + }, + [7] = { + ["CLSID"] = "{A111396E-D3E8-4b9c-8AC9-2432489304D5}", + ["num"] = 11, + }, + [8] = { ["CLSID"] = "{8A0BE8AE-58D4-4572-9263-3144C0D06364}", ["num"] = 5, }, @@ -44,7 +87,7 @@ local unitPayloads = { ["tasks"] = { }, }, - [2] = { + [3] = { ["name"] = "CAP", ["pylons"] = { [1] = { @@ -87,53 +130,6 @@ local unitPayloads = { ["tasks"] = { }, }, - [3] = { - ["name"] = "CAS", - ["pylons"] = { - [1] = { - ["CLSID"] = "{8A0BE8AE-58D4-4572-9263-3144C0D06364}", - ["num"] = 5, - }, - [2] = { - ["CLSID"] = "{DB769D48-67D7-42ED-A2BE-108D566C8B1E}", - ["num"] = 7, - }, - [3] = { - ["CLSID"] = "{DB769D48-67D7-42ED-A2BE-108D566C8B1E}", - ["num"] = 3, - }, - [4] = { - ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", - ["num"] = 2, - }, - [5] = { - ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", - ["num"] = 1, - }, - [6] = { - ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", - ["num"] = 8, - }, - [7] = { - ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", - ["num"] = 9, - }, - [8] = { - ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", - ["num"] = 4, - }, - [9] = { - ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", - ["num"] = 6, - }, - [11] = { - ["CLSID"] = "{A111396E-D3E8-4b9c-8AC9-2432489304D5}", - ["num"] = 11, - }, - }, - ["tasks"] = { - }, - }, [4] = { ["name"] = "STRIKE", ["pylons"] = { @@ -142,11 +138,11 @@ local unitPayloads = { ["num"] = 7, }, [2] = { - ["CLSID"] = "{TER_9A_2L*MK-82}", + ["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}", ["num"] = 4, }, [3] = { - ["CLSID"] = "{TER_9A_2R*MK-82}", + ["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}", ["num"] = 6, }, [4] = { @@ -173,7 +169,7 @@ local unitPayloads = { ["CLSID"] = "{8A0BE8AE-58D4-4572-9263-3144C0D06364}", ["num"] = 5, }, - [11] = { + [10] = { ["CLSID"] = "{A111396E-D3E8-4b9c-8AC9-2432489304D5}", ["num"] = 11, }, @@ -185,19 +181,19 @@ local unitPayloads = { ["name"] = "SEAD", ["pylons"] = { [1] = { - ["CLSID"] = "{DB769D48-67D7-42ED-A2BE-108D566C8B1E}", + ["CLSID"] = "{F376DBEE-4CAE-41BA-ADD9-B2910AC95DEC}", ["num"] = 6, }, [2] = { - ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["CLSID"] = "{B06DD79A-F21E-4EB9-BD9D-AB3844618C93}", ["num"] = 7, }, [3] = { - ["CLSID"] = "{DB769D48-67D7-42ED-A2BE-108D566C8B1E}", + ["CLSID"] = "{F376DBEE-4CAE-41BA-ADD9-B2910AC95DEC}", ["num"] = 4, }, [4] = { - ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["CLSID"] = "{B06DD79A-F21E-4EB9-BD9D-AB3844618C93}", ["num"] = 3, }, [5] = { diff --git a/resources/customized_payloads/P-47D-30.lua b/resources/customized_payloads/P-47D-30.lua index df5f21cd..71dc4891 100644 --- a/resources/customized_payloads/P-47D-30.lua +++ b/resources/customized_payloads/P-47D-30.lua @@ -2,26 +2,6 @@ local unitPayloads = { ["name"] = "P-47D-30", ["payloads"] = { [1] = { - ["name"] = "CAS", - ["pylons"] = { - [1] = { - ["CLSID"] = "{AN-M64}", - ["num"] = 3, - }, - [2] = { - ["CLSID"] = "{AN-M64}", - ["num"] = 2, - }, - [3] = { - ["CLSID"] = "{AN-M64}", - ["num"] = 1, - }, - }, - ["tasks"] = { - [1] = 11, - }, - }, - [2] = { ["name"] = "STRIKE", ["pylons"] = { [1] = { @@ -41,14 +21,34 @@ local unitPayloads = { [1] = 11, }, }, - [3] = { - ["name"] = "ANTISHIP", + [2] = { + ["name"] = "ANTISTRIKE", ["pylons"] = { }, ["tasks"] = { [1] = 11, }, }, + [3] = { + ["name"] = "CAS", + ["pylons"] = { + [1] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 3, + }, + [2] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 2, + }, + [3] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, [4] = { ["name"] = "CAP", ["pylons"] = { @@ -77,6 +77,25 @@ local unitPayloads = { [1] = 11, }, }, + [6] = { + ["name"] = "ANTISHIP", + ["pylons"] = { + [1] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 3, + }, + [2] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 2, + }, + [3] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 1, + }, + }, + ["tasks"] = { + }, + }, }, ["tasks"] = { }, diff --git a/resources/customized_payloads/P-47D-30bl1.lua b/resources/customized_payloads/P-47D-30bl1.lua new file mode 100644 index 00000000..50c84308 --- /dev/null +++ b/resources/customized_payloads/P-47D-30bl1.lua @@ -0,0 +1,96 @@ +local unitPayloads = { + ["name"] = "P-47D-30bl1", + ["payloads"] = { + [1] = { + ["name"] = "CAP", + ["pylons"] = { + }, + ["tasks"] = { + [1] = 11, + }, + }, + [2] = { + ["name"] = "CAS", + ["pylons"] = { + [1] = { + ["CLSID"] = "{AN_M57}", + ["num"] = 1, + }, + [2] = { + ["CLSID"] = "{AN_M57}", + ["num"] = 2, + }, + [3] = { + ["CLSID"] = "{AN_M57}", + ["num"] = 3, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, + [3] = { + ["name"] = "STRIKE", + ["pylons"] = { + [1] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 3, + }, + [2] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 2, + }, + [3] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, + [4] = { + ["name"] = "SEAD", + ["pylons"] = { + [1] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 3, + }, + [2] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 2, + }, + [3] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, + [5] = { + ["name"] = "ANTISHIP", + ["pylons"] = { + [1] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 3, + }, + [2] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 2, + }, + [3] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 1, + }, + }, + ["tasks"] = { + }, + }, + }, + ["tasks"] = { + }, + ["unitType"] = "P-47D-30bl1", +} +return unitPayloads diff --git a/resources/customized_payloads/P-47D-40.lua b/resources/customized_payloads/P-47D-40.lua new file mode 100644 index 00000000..fea43280 --- /dev/null +++ b/resources/customized_payloads/P-47D-40.lua @@ -0,0 +1,88 @@ +local unitPayloads = { + ["name"] = "P-47D-40", + ["payloads"] = { + [1] = { + ["name"] = "CAP", + ["pylons"] = { + }, + ["tasks"] = { + [1] = 11, + }, + }, + [2] = { + ["name"] = "CAS", + ["pylons"] = { + [1] = { + ["CLSID"] = "{P47_5_HVARS_ON_LEFT_WING_RAILS}", + ["num"] = 4, + }, + [2] = { + ["CLSID"] = "{P47_5_HVARS_ON_RIGHT_WING_RAILS}", + ["num"] = 5, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, + [3] = { + ["name"] = "SEAD", + ["pylons"] = { + [1] = { + ["CLSID"] = "{P47_5_HVARS_ON_LEFT_WING_RAILS}", + ["num"] = 4, + }, + [2] = { + ["CLSID"] = "{P47_5_HVARS_ON_RIGHT_WING_RAILS}", + ["num"] = 5, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, + [4] = { + ["name"] = "STRIKE", + ["pylons"] = { + [1] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 3, + }, + [2] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 2, + }, + [3] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, + [5] = { + ["name"] = "ANTISHIP", + ["pylons"] = { + [1] = { + ["CLSID"] = "{P47_5_HVARS_ON_RIGHT_WING_RAILS}", + ["num"] = 5, + }, + [2] = { + ["CLSID"] = "{P47_5_HVARS_ON_LEFT_WING_RAILS}", + ["num"] = 4, + }, + [3] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 1, + }, + }, + ["tasks"] = { + }, + }, + }, + ["tasks"] = { + }, + ["unitType"] = "P-47D-40", +} +return unitPayloads From b7dff59542790da14cb033aaa45956eb92723fd5 Mon Sep 17 00:00:00 2001 From: Khopa Date: Thu, 24 Sep 2020 18:03:10 +0200 Subject: [PATCH 55/61] P_47 variants added to db --- changelog.md | 5 ++++- game/db.py | 11 +++++++++-- gen/flights/ai_flight_planner_db.py | 6 ++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/changelog.md b/changelog.md index 69a72793..6569b827 100644 --- a/changelog.md +++ b/changelog.md @@ -2,7 +2,10 @@ ## Features/Improvements : * **[Other]** Added an installer option (thanks to contributor parithon) -* **[Cheat Menu]** Added possibility to replace destroyed SAM and base defenses units for the player (Click on a SAM site to fix it) +* **[Kneeboards]** Added custom kneeboards (thanks to contributor danalbert) +* **[Kneeboards]** Custom kneeboards contains ATC/ILS informations (thanks to contributor danalbert) +* **[Comms/Radio]** Relevant frequencies are assigned to comms channel (thanks to contributor danalbert) +* **[Base Menu]** Added possibility to replace destroyed SAM and base defenses units for the player (Click on a SAM site to fix it) * **[Cheat Menu]** Added recon images for buildings on strike targets, click on a Strike target to get detailled informations * **[Units/Factions]** Added F-16C to USA 1990 * **[Units/Factions]** Added MQ-9 Reaper as CAS unit for USA 2005 diff --git a/game/db.py b/game/db.py index 15024422..02120a45 100644 --- a/game/db.py +++ b/game/db.py @@ -223,13 +223,16 @@ PRICES = { KC130: 25, A_50: 50, + KJ_2000: 50, E_3A: 50, C_130: 25, # WW2 P_51D_30_NA: 18, P_51D: 16, - P_47D_30: 18, + P_47D_30: 17, + P_47D_30bl1: 16, + P_47D_40: 18, B_17G: 30, # Drones @@ -519,6 +522,8 @@ UNIT_BY_TASK = { MiG_27K, A_20G, P_47D_30, + P_47D_30bl1, + P_47D_40, Ju_88A4, B_17G, MB_339PAN, @@ -542,7 +547,7 @@ UNIT_BY_TASK = { KC130, S_3B_Tanker, ], - AWACS: [E_3A, A_50, ], + AWACS: [E_3A, A_50, KJ_2000], PinpointStrike: [ Armor.APC_MTLB, Armor.APC_MTLB, @@ -993,6 +998,8 @@ PLANE_PAYLOAD_OVERRIDES = { Su_17M4: COMMON_OVERRIDE, F_4E: COMMON_OVERRIDE, P_47D_30:COMMON_OVERRIDE, + P_47D_30bl1:COMMON_OVERRIDE, + P_47D_40:COMMON_OVERRIDE, B_17G: COMMON_OVERRIDE, P_51D: COMMON_OVERRIDE, P_51D_30_NA: COMMON_OVERRIDE, diff --git a/gen/flights/ai_flight_planner_db.py b/gen/flights/ai_flight_planner_db.py index dd924f64..c2824ef8 100644 --- a/gen/flights/ai_flight_planner_db.py +++ b/gen/flights/ai_flight_planner_db.py @@ -62,6 +62,8 @@ CAP_CAPABLE = [ P_51D_30_NA, P_51D, P_47D_30, + P_47D_30bl1, + P_47D_40, SpitfireLFMkIXCW, SpitfireLFMkIX, @@ -130,6 +132,8 @@ CAS_CAPABLE = [ P_51D_30_NA, P_51D, P_47D_30, + P_47D_30bl1, + P_47D_40, A_20G, SpitfireLFMkIXCW, @@ -204,6 +208,8 @@ STRIKE_CAPABLE = [ P_51D_30_NA, P_51D, P_47D_30, + P_47D_30bl1, + P_47D_40, A_20G, B_17G, From 42c9af102bc04c232536fe6deb1659150b4de5a1 Mon Sep 17 00:00:00 2001 From: Khopa Date: Thu, 24 Sep 2020 19:56:56 +0200 Subject: [PATCH 56/61] Added KJ 2000 to China as AWACS --- game/factions/china_2010.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game/factions/china_2010.py b/game/factions/china_2010.py index 148fdbd0..0d98d1b9 100644 --- a/game/factions/china_2010.py +++ b/game/factions/china_2010.py @@ -20,7 +20,7 @@ China_2010 = { An_30M, Yak_40, - A_50, + KJ_2000, Mi_8MT, Mi_28N, From fb40e9273dff8153a98e5dd163bf3588368a6946 Mon Sep 17 00:00:00 2001 From: Khopa Date: Thu, 24 Sep 2020 20:00:57 +0200 Subject: [PATCH 57/61] Added newest contributors to about dialog. --- qt_ui/windows/QLiberationWindow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qt_ui/windows/QLiberationWindow.py b/qt_ui/windows/QLiberationWindow.py index da50e03f..c51547e6 100644 --- a/qt_ui/windows/QLiberationWindow.py +++ b/qt_ui/windows/QLiberationWindow.py @@ -221,7 +221,7 @@ class QLiberationWindow(QMainWindow): "

Authors

" + \ "

DCS Liberation was originally developed by shdwp, DCS Liberation 2.0 is a partial rewrite based on this work by Khopa." \ "

Contributors

" + \ - "shdwp, Khopa, Wrycu, calvinmorrow, JohanAberg, Deus, root0fall, Captain Cody" + \ + "shdwp, Khopa, ColonelPanic, Wrycu, calvinmorrow, JohanAberg, Deus, root0fall, Captain Cody, steveveepee, pedromagueija, parithon, bwRavencl" + \ "

Special Thanks :

" \ "rp- for the pydcs framework
"\ "Grimes (mrSkortch) & Speed for the MIST framework
"\ From c96b5cf4d7add73112202c0e7c462c6f8e218209 Mon Sep 17 00:00:00 2001 From: Khopa Date: Fri, 25 Sep 2020 00:25:20 +0200 Subject: [PATCH 58/61] Fixed bug when buying armor at base --- gen/defenses/armor_group_generator.py | 30 ++++++++++++++----- gen/defenses/armored_group_generator.py | 17 +++++++++++ .../base_defenses/QBaseDefenseGroupInfo.py | 2 -- .../windows/groundobject/QGroundObjectMenu.py | 8 +++-- 4 files changed, 45 insertions(+), 12 deletions(-) diff --git a/gen/defenses/armor_group_generator.py b/gen/defenses/armor_group_generator.py index 3824f7da..7b772e31 100644 --- a/gen/defenses/armor_group_generator.py +++ b/gen/defenses/armor_group_generator.py @@ -3,22 +3,38 @@ import random from dcs.vehicles import Armor from game import db -from gen.defenses.armored_group_generator import ArmoredGroupGenerator +from gen.defenses.armored_group_generator import ArmoredGroupGenerator, FixedSizeArmorGroupGenerator def generate_armor_group(faction:str, game, ground_object): """ This generate a group of ground units - :param parentCp: The parent control point - :param ground_object: The ground object which will own the group - :param country: Owner country :return: Generated group """ possible_unit = [u for u in db.FACTIONS[faction]["units"] if u in Armor.__dict__.values()] if len(possible_unit) > 0: unit_type = random.choice(possible_unit) - generator = ArmoredGroupGenerator(game, ground_object, unit_type) - generator.generate() - return generator.get_generated_group() + return generate_armor_group_of_type(game, ground_object, unit_type) return None + + +def generate_armor_group_of_type(game, ground_object, unit_type): + """ + This generate a group of ground units of given type + :return: Generated group + """ + generator = ArmoredGroupGenerator(game, ground_object, unit_type) + generator.generate() + return generator.get_generated_group() + + +def generate_armor_group_of_type_and_size(game, ground_object, unit_type, size: int): + """ + This generate a group of ground units of given type and size + :return: Generated group + """ + generator = FixedSizeArmorGroupGenerator(game, ground_object, unit_type, size) + generator.generate() + return generator.get_generated_group() + diff --git a/gen/defenses/armored_group_generator.py b/gen/defenses/armored_group_generator.py index f678af81..3b81a1dd 100644 --- a/gen/defenses/armored_group_generator.py +++ b/gen/defenses/armored_group_generator.py @@ -25,3 +25,20 @@ class ArmoredGroupGenerator(GroupGenerator): self.position.y + spacing * j, self.heading) +class FixedSizeArmorGroupGenerator(GroupGenerator): + + def __init__(self, game, ground_object, unit_type, size): + super(FixedSizeArmorGroupGenerator, self).__init__(game, ground_object) + self.unit_type = unit_type + self.size = size + + def generate(self): + spacing = random.randint(20, 70) + + index = 0 + for i in range(self.size): + index = index + 1 + self.add_unit(self.unit_type, "Armor#" + str(index), + self.position.x + spacing * i, + self.position.y, self.heading) + diff --git a/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py b/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py index bafd70ee..94da80e5 100644 --- a/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py +++ b/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py @@ -39,8 +39,6 @@ class QBaseDefenseGroupInfo(QGroupBox): item = self.unit_layout.itemAt(i) if item is not None and item.widget() is not None: self.unit_layout.removeItem(item) - item.widget().setParent(None) - item.widget().deleteLater() for g in self.ground_object.groups: for u in g.units: diff --git a/qt_ui/windows/groundobject/QGroundObjectMenu.py b/qt_ui/windows/groundobject/QGroundObjectMenu.py index 6d28c357..dcfed0a3 100644 --- a/qt_ui/windows/groundobject/QGroundObjectMenu.py +++ b/qt_ui/windows/groundobject/QGroundObjectMenu.py @@ -1,7 +1,7 @@ import logging from PySide2 import QtCore -from PySide2.QtGui import QCloseEvent, Qt +from PySide2.QtGui import Qt from PySide2.QtWidgets import QHBoxLayout, QDialog, QGridLayout, QLabel, QGroupBox, QVBoxLayout, QPushButton, \ QComboBox, QSpinBox, QMessageBox from dcs import Point @@ -9,7 +9,7 @@ from dcs import Point from game import Game, db from game.data.building_data import FORTIFICATION_BUILDINGS from game.db import PRICES, unit_type_of, PinpointStrike -from gen.defenses.armor_group_generator import generate_armor_group +from gen.defenses.armor_group_generator import generate_armor_group_of_type_and_size from gen.sam.sam_group_generator import get_faction_possible_sams_generator from qt_ui.uiconstants import EVENT_ICONS from qt_ui.widgets.QBudgetBox import QBudgetBox @@ -288,7 +288,9 @@ class QBuyGroupForGroundObjectDialog(QDialog): self.buyArmorButton.setText("Buy [$" + str(db.PRICES[self.buyArmorCombo.itemData(self.buyArmorCombo.currentIndex())] * self.amount.value()) + "M][-$" + str(self.current_group_value) + "M]") def buyArmor(self): + print("Buy Armor ") utype = self.buyArmorCombo.itemData(self.buyArmorCombo.currentIndex()) + print(utype) price = db.PRICES[utype] * self.amount.value() - self.current_group_value if price > self.game.budget: self.error_money() @@ -298,7 +300,7 @@ class QBuyGroupForGroundObjectDialog(QDialog): self.game.budget -= price # Generate Armor - group = generate_armor_group(self.game.player_name, self.game, self.ground_object) + group = generate_armor_group_of_type_and_size(self.game, self.ground_object, utype, int(self.amount.value())) self.ground_object.groups = [group] GameUpdateSignal.get_instance().updateBudget(self.game) From ce257a31bb34362304e4ebdbdf177e253ebea500 Mon Sep 17 00:00:00 2001 From: Khopa Date: Fri, 25 Sep 2020 00:36:13 +0200 Subject: [PATCH 59/61] Fixed UI bugs when buying new units in base defense menu. --- .../base_defenses/QBaseDefenseGroupInfo.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py b/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py index 94da80e5..370cf65a 100644 --- a/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py +++ b/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py @@ -35,10 +35,12 @@ class QBaseDefenseGroupInfo(QGroupBox): def buildLayout(self): unit_dict = {} - for i in range(self.unit_layout.count()): - item = self.unit_layout.itemAt(i) - if item is not None and item.widget() is not None: - self.unit_layout.removeItem(item) + for i in range(self.unit_layout.rowCount()): + for j in range(self.unit_layout.columnCount()): + item = self.unit_layout.itemAtPosition(i, j) + if item is not None and item.widget() is not None: + item.widget().setParent(None) + print("Remove " + str(i) + ", " + str(j)) for g in self.ground_object.groups: for u in g.units: @@ -58,6 +60,11 @@ class QBaseDefenseGroupInfo(QGroupBox): self.unit_layout.addWidget(QLabel(str(v) + " x " + "" + k + ""), i, 1) i = i + 1 + if len(unit_dict.items()) == 0: + self.unit_layout.addWidget(QLabel("/"), 0, 0) + + + self.setLayout(self.main_layout) def onManage(self): From e9bad2c7eb861ec82fcac97fd6be81875858975a Mon Sep 17 00:00:00 2001 From: Khopa Date: Fri, 25 Sep 2020 00:56:12 +0200 Subject: [PATCH 60/61] Version number update and changelog update --- changelog.md | 23 ++++++++++++++++++----- qt_ui/uiconstants.py | 2 +- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/changelog.md b/changelog.md index 6569b827..aa1f7abc 100644 --- a/changelog.md +++ b/changelog.md @@ -2,11 +2,23 @@ ## Features/Improvements : * **[Other]** Added an installer option (thanks to contributor parithon) -* **[Kneeboards]** Added custom kneeboards (thanks to contributor danalbert) -* **[Kneeboards]** Custom kneeboards contains ATC/ILS informations (thanks to contributor danalbert) -* **[Comms/Radio]** Relevant frequencies are assigned to comms channel (thanks to contributor danalbert) -* **[Base Menu]** Added possibility to replace destroyed SAM and base defenses units for the player (Click on a SAM site to fix it) -* **[Cheat Menu]** Added recon images for buildings on strike targets, click on a Strike target to get detailled informations +* **[Kneeboards]** Generate mission kneeboards for player flights. Kneeboards include + airfield/carrier information (ATC frequencies, ILS, TACAN, and runway + assignments), assigned radio channels, waypoint lists, and AWACS/JTAC/tanker + information. +* **[Radios]** Allocate separate intra-flight channels for most aircraft to reduce global + chatter. +* **[Radios]** Configure radio channel presets for most aircraft. Currently supported are: + * AJS37 + * AV-8B + * F-14B + * F-16C + * F/A-18C + * JF-17 + * M-2000C +* **[Base Menu]** Added possibility to repair destroyed SAM and base defenses units for the player (Click on a SAM site to fix it) +* **[Base Menu]** Added possibility to buy/sell/replace SAM units +* **[Map]** Added recon images for buildings on strike targets, click on a Strike target to get detailled informations * **[Units/Factions]** Added F-16C to USA 1990 * **[Units/Factions]** Added MQ-9 Reaper as CAS unit for USA 2005 * **[Units/Factions]** Added Mig-21, Mig-23, SA-342L to Syria 2011 @@ -20,6 +32,7 @@ * **[Mission Generator]** AH-1W was not used by AI to generate CAS mission by default * **[Mission Generator]** Fixed F-16C targeting pod not being added to payload * **[Mission Generator]** AH-64A and AH-64D payloads fix. +* **[Units/Factions]** China will use KJ-2000 as awacs instead of A-50 # 2.1.0 diff --git a/qt_ui/uiconstants.py b/qt_ui/uiconstants.py index c0d52631..5920fa85 100644 --- a/qt_ui/uiconstants.py +++ b/qt_ui/uiconstants.py @@ -8,7 +8,7 @@ from game.event import UnitsDeliveryEvent, FrontlineAttackEvent from theater.theatergroundobject import CATEGORY_MAP from userdata.liberation_theme import get_theme_icons -VERSION_STRING = "2.1.0" +VERSION_STRING = "2.1.1 RC1" URLS : Dict[str, str] = { "Manual": "https://github.com/khopa/dcs_liberation/wiki", From aa9dcec0ad6b4a58a90e17877129bf6b37043f90 Mon Sep 17 00:00:00 2001 From: Khopa Date: Fri, 25 Sep 2020 00:58:11 +0200 Subject: [PATCH 61/61] Version number --- qt_ui/uiconstants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qt_ui/uiconstants.py b/qt_ui/uiconstants.py index 5920fa85..02b9d14a 100644 --- a/qt_ui/uiconstants.py +++ b/qt_ui/uiconstants.py @@ -8,7 +8,7 @@ from game.event import UnitsDeliveryEvent, FrontlineAttackEvent from theater.theatergroundobject import CATEGORY_MAP from userdata.liberation_theme import get_theme_icons -VERSION_STRING = "2.1.1 RC1" +VERSION_STRING = "2.1.1" URLS : Dict[str, str] = { "Manual": "https://github.com/khopa/dcs_liberation/wiki",