diff --git a/changelog.md b/changelog.md index f0d07bdd..677f79e6 100644 --- a/changelog.md +++ b/changelog.md @@ -24,6 +24,8 @@ * **[Options]** New options in Settings: Spawn ground power trucks at ground starts in airbases/roadbases * **[Options]** Option for hiding TGOs (with IADS roles) on MFD * **[Plugins]** Splash Damage 2.1 with Clusters and Ship Radar effects. +* **[COMMs]** Aircraft-specific callsigns will now also be used. +* **[COMMs]** Ability to set a specific callsign to a flight. ## Fixes * **[Mission Generation]** Anti-ship strikes should use "group attack" in their attack-task diff --git a/game/ato/flight.py b/game/ato/flight.py index e5f3cb19..ef94b028 100644 --- a/game/ato/flight.py +++ b/game/ato/flight.py @@ -14,7 +14,9 @@ from .flightmembers import FlightMembers from .flightroster import FlightRoster from .flightstate import FlightState, Navigating, Uninitialized from .flightstate.killed import Killed +from .flighttype import FlightType from .loadouts import Weapon +from ..radio.CallsignContainer import CallsignContainer from ..radio.RadioFrequencyContainer import RadioFrequencyContainer from ..radio.TacanContainer import TacanContainer from ..radio.radios import RadioFrequency @@ -36,7 +38,6 @@ if TYPE_CHECKING: from game.data.weapons import WeaponType from .flightmember import FlightMember from .flightplans.flightplan import FlightPlan - from .flighttype import FlightType from .flightwaypoint import FlightWaypoint from .package import Package from .starttype import StartType @@ -44,7 +45,9 @@ if TYPE_CHECKING: F18_TGP_PYLON: int = 4 -class Flight(SidcDescribable, RadioFrequencyContainer, TacanContainer): +class Flight( + SidcDescribable, RadioFrequencyContainer, TacanContainer, CallsignContainer +): def __init__( self, package: Package, @@ -58,7 +61,7 @@ class Flight(SidcDescribable, RadioFrequencyContainer, TacanContainer): roster: Optional[FlightRoster] = None, frequency: Optional[RadioFrequency] = None, channel: Optional[TacanChannel] = None, - callsign: Optional[str] = None, + callsign_tcn: Optional[str] = None, claim_inv: bool = True, ) -> None: self.id = uuid.uuid4() @@ -81,7 +84,7 @@ class Flight(SidcDescribable, RadioFrequencyContainer, TacanContainer): self.frequency = frequency if self.unit_type.dcs_unit_type.tacan: self.tacan = channel - self.tcn_name = callsign + self.tcn_name = callsign_tcn self.initialize_fuel() self.use_same_loadout_for_all_members = True @@ -120,6 +123,22 @@ class Flight(SidcDescribable, RadioFrequencyContainer, TacanContainer): ) ) + @property + def available_callsigns(self) -> List[str]: + callsigns = set() + dcs_unit = self.squadron.aircraft.dcs_unit_type + category = dcs_unit.category + category = "Air" if category == "Interceptor" else category + for name in self.squadron.coalition.faction.country.callsign[category]: + callsigns.add(name) + if hasattr(dcs_unit, "callnames"): + country_name = self.squadron.coalition.faction.country.name + for c in dcs_unit.callnames: + if "Combined Joint Task Forces" in country_name or c == country_name: + for name in dcs_unit.callnames[c]: + callsigns.add(name) + return sorted(callsigns) + @property def flight_plan(self) -> FlightPlan[Any]: return self._flight_plan_builder.get_or_build() diff --git a/game/missiongenerator/aircraft/flightgroupspawner.py b/game/missiongenerator/aircraft/flightgroupspawner.py index 426d669e..96348fff 100644 --- a/game/missiongenerator/aircraft/flightgroupspawner.py +++ b/game/missiongenerator/aircraft/flightgroupspawner.py @@ -266,6 +266,8 @@ class FlightGroupSpawner: start_type=self._start_type_at_airfield(airfield), group_size=self.flight.count, parking_slots=None, + callsign_name=self.flight.callsign.name if self.flight.callsign else None, + callsign_nr=self.flight.callsign.nr if self.flight.callsign else None, ) def _generate_over_departure( @@ -299,6 +301,8 @@ class FlightGroupSpawner: speed=speed.kph, maintask=None, group_size=self.flight.count, + callsign_name=self.flight.callsign.name if self.flight.callsign else None, + callsign_nr=self.flight.callsign.nr if self.flight.callsign else None, ) group.points[0].alt_type = alt_type @@ -315,6 +319,8 @@ class FlightGroupSpawner: maintask=None, start_type=self._start_type_at_group(at), group_size=self.flight.count, + callsign_name=self.flight.callsign.name if self.flight.callsign else None, + callsign_nr=self.flight.callsign.nr if self.flight.callsign else None, ) def _generate_at_cp_helipad( diff --git a/game/radio/CallsignContainer.py b/game/radio/CallsignContainer.py new file mode 100644 index 00000000..2a51e424 --- /dev/null +++ b/game/radio/CallsignContainer.py @@ -0,0 +1,20 @@ +from abc import abstractmethod +from typing import Optional, List + + +class Callsign: + name: Optional[str] = None + nr: Optional[int] = None + + def __init__(self, name: Optional[str], nr: int) -> None: + self.name = name + self.nr = nr + + +class CallsignContainer: + callsign: Optional[Callsign] = None + + @property + @abstractmethod + def available_callsigns(self) -> List[str]: + ... diff --git a/qt_ui/widgets/QCallsignWidget.py b/qt_ui/widgets/QCallsignWidget.py new file mode 100644 index 00000000..7124c74f --- /dev/null +++ b/qt_ui/widgets/QCallsignWidget.py @@ -0,0 +1,70 @@ +from PySide6.QtCore import Signal +from PySide6.QtWidgets import ( + QHBoxLayout, + QLabel, + QPushButton, + QWidget, +) + +from game.radio.CallsignContainer import CallsignContainer, Callsign +from qt_ui.models import GameModel +from qt_ui.windows.QCallsignDialog import QCallsignDialog + + +class QCallsignWidget(QWidget): + callsign_changed = Signal(QWidget) + + def __init__(self, container: CallsignContainer, game_model: GameModel) -> None: + super().__init__() + + self.ct = container + self.gm = game_model + + columns = QHBoxLayout() + self.setLayout(columns) + + self.callsign = QLabel(self._get_label_text()) + columns.addWidget(self.callsign) + columns.addStretch() + + self.set_callsign_btn = QPushButton("Set Callsign") + self.set_callsign_btn.setProperty("class", "comms") + self.set_callsign_btn.setFixedWidth(100) + columns.addWidget(self.set_callsign_btn) + self.set_callsign_btn.clicked.connect(self.open_callsign_dialog) + + self.reset_callsign_btn = QPushButton("Reset Callsign") + self.reset_callsign_btn.setProperty("class", "btn-danger comms") + self.reset_callsign_btn.setFixedWidth(100) + columns.addWidget(self.reset_callsign_btn) + self.reset_callsign_btn.clicked.connect(self.reset_callsign) + + def _get_label_text(self) -> str: + cs = ( + "AUTO" + if self.ct.callsign is None + else f"{self.ct.callsign.name} {self.ct.callsign.nr}" + ) + return f"Callsign: {cs}" + + def open_callsign_dialog(self) -> None: + self.callsign_dialog = QCallsignDialog(self, self.ct) + self.callsign_dialog.accepted.connect(self.assign_callsign) + self.callsign_dialog.show() + + def assign_callsign(self) -> None: + name = self.callsign_dialog.callsign_name_input.currentText() + nr = self.callsign_dialog.callsign_nr_input.value() + self.ct.callsign = Callsign(name, nr) + self.callsign.setText(self._get_label_text()) + self.callsign_changed.emit(self) + + def reset_callsign(self) -> None: + self.ct.callsign = None + self.callsign.setText(self._get_label_text()) + self._reset_color_and_tooltip() + self.callsign_changed.emit(self) + + def _reset_color_and_tooltip(self): + self.callsign.setStyleSheet("color: white") + self.callsign.setToolTip(None) diff --git a/qt_ui/windows/QCallsignDialog.py b/qt_ui/windows/QCallsignDialog.py new file mode 100644 index 00000000..c2d1ca4a --- /dev/null +++ b/qt_ui/windows/QCallsignDialog.py @@ -0,0 +1,56 @@ +from typing import Optional + +from PySide6.QtCore import Qt +from PySide6.QtWidgets import ( + QDialog, + QPushButton, + QLabel, + QHBoxLayout, + QSpinBox, + QComboBox, +) + +from game.radio.CallsignContainer import CallsignContainer +from qt_ui.uiconstants import EVENT_ICONS + + +class QCallsignDialog(QDialog): + def __init__( + self, parent=None, container: Optional[CallsignContainer] = None + ) -> None: + super().__init__(parent=parent) + self.container = container + self.setMinimumWidth(400) + + # Make dialog modal to prevent background windows to close unexpectedly. + self.setModal(True) + + self.setWindowTitle("Assign Callsign") + self.setWindowIcon(EVENT_ICONS["strike"]) + + layout = QHBoxLayout() + + self.callsign_label = QLabel("Callsign:") + self.callsign_name_input = QComboBox() + self.callsign_name_input.addItems(container.available_callsigns) + self.callsign_nr_input = QSpinBox() + self.callsign_nr_input.setRange(1, 9) + self.callsign_nr_input.setSingleStep(1) + self.callsign_nr_input.setMaximumWidth(50) + layout.addWidget(self.callsign_label) + layout.addStretch() + layout.addWidget(self.callsign_name_input) + layout.addStretch() + layout.addWidget(self.callsign_nr_input) + layout.addStretch() + + self.create_button = QPushButton("Save") + self.create_button.clicked.connect(self.accept) + layout.addWidget(self.create_button, alignment=Qt.AlignmentFlag.AlignRight) + + self.setLayout(layout) + + if container is not None: + if container.callsign is not None: + self.callsign_name_input.setCurrentText(container.callsign.name) + self.callsign_nr_input.setValue(container.callsign.nr) diff --git a/qt_ui/windows/mission/flight/settings/QCommsEditor.py b/qt_ui/windows/mission/flight/settings/QCommsEditor.py index 8f06135b..74185ee1 100644 --- a/qt_ui/windows/mission/flight/settings/QCommsEditor.py +++ b/qt_ui/windows/mission/flight/settings/QCommsEditor.py @@ -2,13 +2,14 @@ from PySide6.QtWidgets import QGroupBox, QVBoxLayout from game.ato import Flight, FlightType from qt_ui.models import GameModel +from qt_ui.widgets.QCallsignWidget import QCallsignWidget from qt_ui.widgets.QFrequencyWidget import QFrequencyWidget from qt_ui.widgets.QTacanWidget import QTacanWidget class QCommsEditor(QGroupBox): def __init__(self, flight: Flight, game: GameModel): - title = "Intra-Flight Frequency" + title = "COMMs" layout = QVBoxLayout() @@ -16,9 +17,9 @@ class QCommsEditor(QGroupBox): has_tacan = flight.unit_type.dcs_unit_type.tacan layout.addWidget(QFrequencyWidget(flight, game)) + layout.addWidget(QCallsignWidget(flight, game)) if is_refuel and has_tacan: layout.addWidget(QTacanWidget(flight, game)) - title = title + " / TACAN" super(QCommsEditor, self).__init__(title) self.flight = flight