From 45282338304b5fbfcb6778c62cb61f0e067f8050 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sat, 25 Dec 2021 15:00:51 -0800 Subject: [PATCH] Add UI for setting flight properties like HMD. Fixes https://github.com/dcs-liberation/dcs_liberation/issues/706 --- changelog.md | 3 +- game/ato/flight.py | 13 ++++ game/dcs/aircrafttype.py | 28 ++++---- game/dcs/propertyvalue.py | 12 ++++ game/dcs/unitproperty.py | 65 +++++++++++++++++++ .../aircraft/flightgroupconfigurator.py | 6 ++ .../flight/payload/QFlightPayloadTab.py | 11 +++- .../mission/flight/payload/propertyeditor.py | 14 ++++ .../flight/payload/propertyselector.py | 23 +++++++ 9 files changed, 160 insertions(+), 15 deletions(-) create mode 100644 game/dcs/propertyvalue.py create mode 100644 game/dcs/unitproperty.py create mode 100644 qt_ui/windows/mission/flight/payload/propertyeditor.py create mode 100644 qt_ui/windows/mission/flight/payload/propertyselector.py diff --git a/changelog.md b/changelog.md index ad15384f..fe676e3a 100644 --- a/changelog.md +++ b/changelog.md @@ -10,7 +10,8 @@ Saves from 5.x are not compatible with 6.0. * **[Mission Generation]** Add Option to enforce the Easy Communication setting for the mission * **[Flight Planning]** Added the ability to plan tankers for recovery on package flights. AI does not plan. * **[Modding]** Add F-104 mod support -* +* **[UI]** Added options to the loadout editor for setting properties such as HMD choice. + ## Fixes * **[Mission Generator]** Fixed incorrect radio specification for the AN/ARC-222. diff --git a/game/ato/flight.py b/game/ato/flight.py index d5142d2b..823e5cf9 100644 --- a/game/ato/flight.py +++ b/game/ato/flight.py @@ -8,6 +8,7 @@ from dcs.planes import C_101CC, C_101EB, Su_33 from gen.flights.loadouts import Loadout from .flightroster import FlightRoster from .flightstate import FlightState, Uninitialized +from ..savecompat import has_save_compat_for if TYPE_CHECKING: from game.dcs.aircrafttype import AircraftType @@ -55,6 +56,15 @@ class Flight: # Only used by transport missions. self.cargo = cargo + # Flight properties that can be set in the mission editor. This is used for + # things like HMD selection, ripple quantity, etc. Any values set here will take + # the place of the defaults defined by DCS. + # + # This is a part of the Flight rather than the Loadout because DCS does not + # associate these choices with the loadout, and we don't want to reset these + # options when players switch loadouts. + self.props: dict[str, Any] = {} + # Used for simulating the travel to first contact. self.state: FlightState = Uninitialized(self, squadron.settings) @@ -76,8 +86,11 @@ class Flight: del state["state"] return state + @has_save_compat_for(6) def __setstate__(self, state: dict[str, Any]) -> None: state["state"] = Uninitialized(self, state["squadron"].settings) + if "props" not in state: + state["props"] = {} self.__dict__.update(state) @property diff --git a/game/dcs/aircrafttype.py b/game/dcs/aircrafttype.py index 33388eb0..4b581d42 100644 --- a/game/dcs/aircrafttype.py +++ b/game/dcs/aircrafttype.py @@ -5,37 +5,38 @@ from collections import defaultdict from dataclasses import dataclass from functools import cached_property from pathlib import Path -from typing import ClassVar, Type, Iterator, TYPE_CHECKING, Optional, Any +from typing import Any, ClassVar, Iterator, Optional, TYPE_CHECKING, Type import yaml from dcs.helicopters import helicopter_map from dcs.planes import plane_map from dcs.unittype import FlyingType +from game.dcs.unitproperty import UnitProperty from game.dcs.unittype import UnitType from game.radio.channels import ( ChannelNamer, - RadioChannelAllocator, CommonRadioChannelAllocator, - HueyChannelNamer, - SCR522ChannelNamer, - ViggenChannelNamer, - ViperChannelNamer, - TomcatChannelNamer, - MirageChannelNamer, - SingleRadioChannelNamer, FarmerRadioChannelAllocator, - SCR522RadioChannelAllocator, - ViggenRadioChannelAllocator, + HueyChannelNamer, + MirageChannelNamer, NoOpChannelAllocator, + RadioChannelAllocator, + SCR522ChannelNamer, + SCR522RadioChannelAllocator, + SingleRadioChannelNamer, + TomcatChannelNamer, + ViggenChannelNamer, + ViggenRadioChannelAllocator, + ViperChannelNamer, ) from game.utils import ( Distance, SPEED_OF_SOUND_AT_SEA_LEVEL, Speed, feet, - kph, knots, + kph, nautical_miles, ) @@ -287,6 +288,9 @@ class AircraftType(UnitType[Type[FlyingType]]): def channel_name(self, radio_id: int, channel_id: int) -> str: return self.channel_namer.channel_name(radio_id, channel_id) + def iter_props(self) -> Iterator[UnitProperty[Any]]: + return UnitProperty.for_aircraft(self.dcs_unit_type) + def __setstate__(self, state: dict[str, Any]) -> None: # Update any existing models with new data on load. updated = AircraftType.named(state["name"]) diff --git a/game/dcs/propertyvalue.py b/game/dcs/propertyvalue.py new file mode 100644 index 00000000..6a31cbac --- /dev/null +++ b/game/dcs/propertyvalue.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Generic, TypeVar + +ValueT = TypeVar("ValueT", bool, int) + + +@dataclass(frozen=True) +class PropertyValue(Generic[ValueT]): + id: str + value: ValueT diff --git a/game/dcs/unitproperty.py b/game/dcs/unitproperty.py new file mode 100644 index 00000000..d8702545 --- /dev/null +++ b/game/dcs/unitproperty.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +import inspect +from collections.abc import Iterator +from dataclasses import dataclass +from typing import Any, Generic, Type, TypeVar + +from dcs.unittype import FlyingType + +from .propertyvalue import PropertyValue + +ValueT = TypeVar("ValueT", bool, int) + + +@dataclass(frozen=True) +class UnitProperty(Generic[ValueT]): + id: str + default: ValueT + values: list[PropertyValue[Any]] + + @classmethod + def for_aircraft( + cls, unit_type: Type[FlyingType] + ) -> Iterator[UnitProperty[ValueT]]: + try: + props = unit_type.Properties # type: ignore + except AttributeError: + return + + if unit_type.property_defaults is None: + raise RuntimeError(f"{unit_type} has Properties but no defaults") + + for name, attr in inspect.getmembers(props, inspect.isclass): + if name.startswith("__"): + continue + yield cls.property_from(attr, unit_type.property_defaults[name]) + + @classmethod + def property_from(cls, attr: Type[ValueT], default: ValueT) -> UnitProperty[ValueT]: + prop_id = attr.id # type: ignore + values = getattr(attr, "Values", None) + if values is None: + prop_values = list(cls.default_values_for(prop_id, default)) + else: + prop_values = [] + for name, value in inspect.getmembers(values): + if name.startswith("__"): + continue + prop_values.append(PropertyValue(name, value)) + return UnitProperty(prop_id, default, prop_values) + + @classmethod + def default_values_for( + cls, prop_id: str, default: ValueT + ) -> Iterator[PropertyValue[ValueT]]: + if isinstance(default, bool): + yield PropertyValue("True", True) + yield PropertyValue("False", False) + elif isinstance(default, int): + for i in range(10): + yield PropertyValue(str(i), i) + else: + raise TypeError( + f"Unexpected property type for {prop_id}: {default.__class__}" + ) diff --git a/game/missiongenerator/aircraft/flightgroupconfigurator.py b/game/missiongenerator/aircraft/flightgroupconfigurator.py index 2fc72899..609eb5f7 100644 --- a/game/missiongenerator/aircraft/flightgroupconfigurator.py +++ b/game/missiongenerator/aircraft/flightgroupconfigurator.py @@ -59,6 +59,7 @@ class FlightGroupConfigurator: def configure(self) -> FlightData: AircraftBehavior(self.flight.flight_type).apply_to(self.flight, self.group) AircraftPainter(self.flight, self.group).apply_livery() + self.setup_props() self.setup_payload() self.setup_fuel() flight_channel = self.setup_radios() @@ -194,6 +195,11 @@ class FlightGroupConfigurator: ] return levels[new_level] + def setup_props(self) -> None: + for prop_id, value in self.flight.props.items(): + for unit in self.group.units: + unit.set_property(prop_id, value) + def setup_payload(self) -> None: for p in self.group.units: p.pylons.clear() diff --git a/qt_ui/windows/mission/flight/payload/QFlightPayloadTab.py b/qt_ui/windows/mission/flight/payload/QFlightPayloadTab.py index 28d5e697..aa09940f 100644 --- a/qt_ui/windows/mission/flight/payload/QFlightPayloadTab.py +++ b/qt_ui/windows/mission/flight/payload/QFlightPayloadTab.py @@ -1,10 +1,16 @@ from PySide2.QtCore import Qt -from PySide2.QtWidgets import QFrame, QLabel, QComboBox, QVBoxLayout +from PySide2.QtWidgets import ( + QComboBox, + QFrame, + QLabel, + QVBoxLayout, +) from game import Game from game.ato.flight import Flight from gen.flights.loadouts import Loadout -from qt_ui.windows.mission.flight.payload.QLoadoutEditor import QLoadoutEditor +from .QLoadoutEditor import QLoadoutEditor +from .propertyeditor import PropertyEditor class DcsLoadoutSelector(QComboBox): @@ -36,6 +42,7 @@ class QFlightPayloadTab(QFrame): docsText.setAlignment(Qt.AlignCenter) docsText.setOpenExternalLinks(True) + layout.addLayout(PropertyEditor(self.flight)) self.loadout_selector = DcsLoadoutSelector(flight) self.loadout_selector.currentIndexChanged.connect(self.on_new_loadout) layout.addWidget(self.loadout_selector) diff --git a/qt_ui/windows/mission/flight/payload/propertyeditor.py b/qt_ui/windows/mission/flight/payload/propertyeditor.py new file mode 100644 index 00000000..0b3beed9 --- /dev/null +++ b/qt_ui/windows/mission/flight/payload/propertyeditor.py @@ -0,0 +1,14 @@ +from PySide2.QtWidgets import QGridLayout, QLabel + +from game.ato import Flight +from .propertyselector import PropertySelector + + +class PropertyEditor(QGridLayout): + def __init__(self, flight: Flight) -> None: + super().__init__() + self.flight = flight + + for row, prop in enumerate(flight.unit_type.iter_props()): + self.addWidget(QLabel(prop.id), row, 0) + self.addWidget(PropertySelector(self.flight, prop), row, 1) diff --git a/qt_ui/windows/mission/flight/payload/propertyselector.py b/qt_ui/windows/mission/flight/payload/propertyselector.py new file mode 100644 index 00000000..c9e785c6 --- /dev/null +++ b/qt_ui/windows/mission/flight/payload/propertyselector.py @@ -0,0 +1,23 @@ +from PySide2.QtWidgets import QComboBox + +from game.ato import Flight +from game.dcs.unitproperty import UnitProperty + + +class PropertySelector(QComboBox): + def __init__(self, flight: Flight, prop: UnitProperty) -> None: + super().__init__() + self.flight = flight + self.prop = prop + + current_value = self.flight.props.get(self.prop.id, self.prop.default) + + for value in self.prop.values: + self.addItem(value.id, value.value) + if value.value == current_value: + self.setCurrentText(value.id) + + self.currentIndexChanged.connect(self.on_selection_changed) + + def on_selection_changed(self, _index: int) -> None: + self.flight.props[self.prop.id] = self.currentData()