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
* **[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.

View File

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

View File

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

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

View File

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

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