mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Add UI for setting flight properties like HMD.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/706
This commit is contained in:
parent
5684570880
commit
4528233830
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
12
game/dcs/propertyvalue.py
Normal 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
65
game/dcs/unitproperty.py
Normal 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__}"
|
||||
)
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
14
qt_ui/windows/mission/flight/payload/propertyeditor.py
Normal file
14
qt_ui/windows/mission/flight/payload/propertyeditor.py
Normal 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)
|
||||
23
qt_ui/windows/mission/flight/payload/propertyselector.py
Normal file
23
qt_ui/windows/mission/flight/payload/propertyselector.py
Normal 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()
|
||||
Loading…
x
Reference in New Issue
Block a user