Add UI for setting flight properties like HMD.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/706
This commit is contained in:
Dan Albert 2021-12-25 15:00:51 -08:00
parent 5684570880
commit 4528233830
9 changed files with 160 additions and 15 deletions

View File

@ -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 * **[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. * **[Flight Planning]** Added the ability to plan tankers for recovery on package flights. AI does not plan.
* **[Modding]** Add F-104 mod support * **[Modding]** Add F-104 mod support
* * **[UI]** Added options to the loadout editor for setting properties such as HMD choice.
## Fixes ## Fixes
* **[Mission Generator]** Fixed incorrect radio specification for the AN/ARC-222. * **[Mission Generator]** Fixed incorrect radio specification for the AN/ARC-222.

View File

@ -8,6 +8,7 @@ from dcs.planes import C_101CC, C_101EB, Su_33
from gen.flights.loadouts import Loadout from gen.flights.loadouts import Loadout
from .flightroster import FlightRoster from .flightroster import FlightRoster
from .flightstate import FlightState, Uninitialized from .flightstate import FlightState, Uninitialized
from ..savecompat import has_save_compat_for
if TYPE_CHECKING: if TYPE_CHECKING:
from game.dcs.aircrafttype import AircraftType from game.dcs.aircrafttype import AircraftType
@ -55,6 +56,15 @@ class Flight:
# Only used by transport missions. # Only used by transport missions.
self.cargo = cargo 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. # Used for simulating the travel to first contact.
self.state: FlightState = Uninitialized(self, squadron.settings) self.state: FlightState = Uninitialized(self, squadron.settings)
@ -76,8 +86,11 @@ class Flight:
del state["state"] del state["state"]
return state return state
@has_save_compat_for(6)
def __setstate__(self, state: dict[str, Any]) -> None: def __setstate__(self, state: dict[str, Any]) -> None:
state["state"] = Uninitialized(self, state["squadron"].settings) state["state"] = Uninitialized(self, state["squadron"].settings)
if "props" not in state:
state["props"] = {}
self.__dict__.update(state) self.__dict__.update(state)
@property @property

View File

@ -5,37 +5,38 @@ from collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass
from functools import cached_property from functools import cached_property
from pathlib import Path 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 import yaml
from dcs.helicopters import helicopter_map from dcs.helicopters import helicopter_map
from dcs.planes import plane_map from dcs.planes import plane_map
from dcs.unittype import FlyingType from dcs.unittype import FlyingType
from game.dcs.unitproperty import UnitProperty
from game.dcs.unittype import UnitType from game.dcs.unittype import UnitType
from game.radio.channels import ( from game.radio.channels import (
ChannelNamer, ChannelNamer,
RadioChannelAllocator,
CommonRadioChannelAllocator, CommonRadioChannelAllocator,
HueyChannelNamer,
SCR522ChannelNamer,
ViggenChannelNamer,
ViperChannelNamer,
TomcatChannelNamer,
MirageChannelNamer,
SingleRadioChannelNamer,
FarmerRadioChannelAllocator, FarmerRadioChannelAllocator,
SCR522RadioChannelAllocator, HueyChannelNamer,
ViggenRadioChannelAllocator, MirageChannelNamer,
NoOpChannelAllocator, NoOpChannelAllocator,
RadioChannelAllocator,
SCR522ChannelNamer,
SCR522RadioChannelAllocator,
SingleRadioChannelNamer,
TomcatChannelNamer,
ViggenChannelNamer,
ViggenRadioChannelAllocator,
ViperChannelNamer,
) )
from game.utils import ( from game.utils import (
Distance, Distance,
SPEED_OF_SOUND_AT_SEA_LEVEL, SPEED_OF_SOUND_AT_SEA_LEVEL,
Speed, Speed,
feet, feet,
kph,
knots, knots,
kph,
nautical_miles, nautical_miles,
) )
@ -287,6 +288,9 @@ class AircraftType(UnitType[Type[FlyingType]]):
def channel_name(self, radio_id: int, channel_id: int) -> str: def channel_name(self, radio_id: int, channel_id: int) -> str:
return self.channel_namer.channel_name(radio_id, channel_id) 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: def __setstate__(self, state: dict[str, Any]) -> None:
# Update any existing models with new data on load. # Update any existing models with new data on load.
updated = AircraftType.named(state["name"]) updated = AircraftType.named(state["name"])

12
game/dcs/propertyvalue.py Normal file
View File

@ -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

65
game/dcs/unitproperty.py Normal file
View File

@ -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__}"
)

View File

@ -59,6 +59,7 @@ class FlightGroupConfigurator:
def configure(self) -> FlightData: def configure(self) -> FlightData:
AircraftBehavior(self.flight.flight_type).apply_to(self.flight, self.group) AircraftBehavior(self.flight.flight_type).apply_to(self.flight, self.group)
AircraftPainter(self.flight, self.group).apply_livery() AircraftPainter(self.flight, self.group).apply_livery()
self.setup_props()
self.setup_payload() self.setup_payload()
self.setup_fuel() self.setup_fuel()
flight_channel = self.setup_radios() flight_channel = self.setup_radios()
@ -194,6 +195,11 @@ class FlightGroupConfigurator:
] ]
return levels[new_level] 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: def setup_payload(self) -> None:
for p in self.group.units: for p in self.group.units:
p.pylons.clear() p.pylons.clear()

View File

@ -1,10 +1,16 @@
from PySide2.QtCore import Qt 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 import Game
from game.ato.flight import Flight from game.ato.flight import Flight
from gen.flights.loadouts import Loadout 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): class DcsLoadoutSelector(QComboBox):
@ -36,6 +42,7 @@ class QFlightPayloadTab(QFrame):
docsText.setAlignment(Qt.AlignCenter) docsText.setAlignment(Qt.AlignCenter)
docsText.setOpenExternalLinks(True) docsText.setOpenExternalLinks(True)
layout.addLayout(PropertyEditor(self.flight))
self.loadout_selector = DcsLoadoutSelector(flight) self.loadout_selector = DcsLoadoutSelector(flight)
self.loadout_selector.currentIndexChanged.connect(self.on_new_loadout) self.loadout_selector.currentIndexChanged.connect(self.on_new_loadout)
layout.addWidget(self.loadout_selector) layout.addWidget(self.loadout_selector)

View File

@ -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)

View File

@ -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()