mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +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
|
* **[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.
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
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:
|
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()
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
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