Datalink support + pydcs update

Support for 2.9.10.4160, though without Iraq, but finally there's some basic support for datalink...
This commit is contained in:
Raffson 2024-12-16 03:07:26 +01:00
parent 0eb06f9af5
commit 64d60e5ccf
No known key found for this signature in database
GPG Key ID: B0402B2C9B764D99
13 changed files with 415 additions and 36 deletions

View File

@ -35,6 +35,8 @@
* **[UI/UX]** Sync package waypoints when primary flight's waypoints are updated and recreate other flights within the package to ensure JOIN, INGRESS & SPLIT are synced
* **[UI/UX]** Allow changing loadout on flight creation
* **[UI]** Display TOT for all waypoints in the flight plan
* **[UI]** Edit basic datalink properties for applicable aircraft
* **[Mission Generation]** Automatic datalink network setup for applicable aircraft (_should_ in theory avoid the need to re-save the mission)
## Fixes
* **[UI/UX]** A-10A flights can be edited again

View File

@ -16,7 +16,7 @@ class FlightMember:
self.use_custom_loadout = False
self.tgp_laser_code: LaserCode | None = None
self.weapon_laser_code: LaserCode | None = None
self.properties: dict[str, bool | float | int] = {}
self.properties: dict[str, bool | float | int | str] = {}
self.livery: Optional[str] = None
self.use_livery_set: bool = True

View File

@ -9,6 +9,9 @@ from dcs import Point
from dcs.action import AITaskPush
from dcs.condition import FlagIsTrue, GroupDead, Or, FlagIsFalse
from dcs.country import Country
from dcs.datalinks.datalink import DataLinkType
from dcs.datalinks.datalinkbase import DataLinkSettingsWithFlightLead
from dcs.datalinks.link16 import Link16Network, ViperLink16NetworkMemberLink
from dcs.mission import Mission
from dcs.terrain.terrain import NoParkingSlotError
from dcs.triggers import TriggerOnce, Event
@ -37,6 +40,7 @@ from .flightdata import FlightData
from .flightgroupconfigurator import FlightGroupConfigurator
from .flightgroupspawner import FlightGroupSpawner
from ...data.weapons import WeaponType
from ...radio.datalink import DataLinkRegistry
if TYPE_CHECKING:
from game import Game
@ -52,6 +56,7 @@ class AircraftGenerator:
time: datetime,
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,
datalink_registry: DataLinkRegistry,
unit_map: UnitMap,
mission_data: MissionData,
helipads: dict[ControlPoint, list[StaticGroup]],
@ -65,6 +70,7 @@ class AircraftGenerator:
self.time = time
self.radio_registry = radio_registry
self.tacan_registy = tacan_registry
self.datalink_registry = datalink_registry
self.unit_map = unit_map
self.flights: List[FlightData] = []
self.mission_data = mission_data
@ -115,6 +121,7 @@ class AircraftGenerator:
dynamic_runways: Runway data for carriers and FARPs.
"""
self._reserve_frequencies_and_tacan(ato)
self.mission_data.packages.clear()
for package in reversed(sorted(ato.packages, key=lambda x: x.time_over_target)):
logging.info(f"Generating package for target: {package.target.name}")
@ -153,6 +160,39 @@ class AircraftGenerator:
if len(splittrigger.actions) > 0:
self.mission.triggerrules.triggers.append(splittrigger)
# at this point all flights were generated, so now start setting up datalink...
self._link_datalink_on_package_level_and_awacs()
def _link_datalink_on_package_level_and_awacs(self) -> None:
for _, flights in self.mission_data.packages.items():
for f in flights:
if not f.aircraft_type.dcs_unit_type.networked_datalink:
continue
for awacs in self.mission_data.awacs:
if awacs.blue == f.friendly:
for u in f.units:
assert u.datalink is not None
if u.datalink.link_type == DataLinkType.LINK16:
u.datalink.network.add_donor(awacs.unit.id)
for unit in f.units:
assert unit.datalink is not None
link_type = unit.datalink.link_type
# check if there's room as team members
for package_flight in flights:
dcs_type = package_flight.aircraft_type.dcs_unit_type
if f is package_flight or not dcs_type.networked_datalink:
continue
default_link = dcs_type.get_default_datalink()
if default_link and link_type != default_link.link_type:
continue
pf_lead = package_flight.units[0]
if not (
unit.datalink.network.has_donors
and unit.datalink.network.add_donor(pf_lead.id)
or unit.datalink.network.add_member(pf_lead.id)
):
break
def spawn_unused_aircraft(
self, player_country: Country, enemy_country: Country
) -> None:
@ -246,26 +286,58 @@ class AircraftGenerator:
self.ground_spawns,
self.mission_data,
).create_flight_group()
self.flights.append(
FlightGroupConfigurator(
flight,
group,
self.game,
self.mission,
self.time,
self.radio_registry,
self.tacan_registy,
self.mission_data,
dynamic_runways,
self.use_client,
).configure()
)
flight_data = FlightGroupConfigurator(
flight,
group,
self.game,
self.mission,
self.time,
self.radio_registry,
self.tacan_registy,
self.datalink_registry,
self.mission_data,
dynamic_runways,
self.use_client,
).configure()
self.flights.append(flight_data)
if not self.mission_data.packages.get(id(flight.package)):
self.mission_data.packages[id(flight.package)] = []
self.mission_data.packages[id(flight.package)].append(flight_data)
dcs_type = flight.unit_type.dcs_unit_type
if dcs_type.networked_datalink:
self.setup_internal_datalink_network(group)
if self.ewrj:
self._track_ewrj_flight(flight, group)
return group
@staticmethod
def setup_internal_datalink_network(group: FlyingGroup[Any]) -> None:
link = group.units[0].datalink
if not link:
return
if isinstance(link.settings, DataLinkSettingsWithFlightLead):
link.settings.flight_lead = True
for u1 in group.units:
assert u1.datalink is not None
for u2 in group.units:
if u1 is u2:
continue
u1.datalink.network.add_member(u2.id)
net = u1.datalink.network
if (
isinstance(net, Link16Network)
and net.member_link_type == ViperLink16NetworkMemberLink
):
for member_link in net.team_members:
assert isinstance(member_link, ViperLink16NetworkMemberLink)
member_link.tdoa = True
def _track_ewrj_flight(self, flight: Flight, group: FlyingGroup[Any]) -> None:
if not self.ewrj_package_dict.get(id(flight.package)):
self.ewrj_package_dict[id(flight.package)] = []

View File

@ -37,6 +37,14 @@ from ...ato.flightmember import FlightMember
from ...ato.flightplans.aewc import AewcFlightPlan
from ...ato.flightplans.packagerefueling import PackageRefuelingFlightPlan
from ...ato.flightplans.theaterrefueling import TheaterRefuelingFlightPlan
from ...radio.datalink import (
DataLinkRegistry,
DataLinkKey,
DataLinkIdentifier,
VOICE_CALLSIGN_LABEL,
VOICE_CALLSIGN_NUMBER,
OWNSHIP_CALLSIGN,
)
from ...theater import Fob
if TYPE_CHECKING:
@ -53,6 +61,7 @@ class FlightGroupConfigurator:
time: datetime,
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,
datalink_registry: DataLinkRegistry,
mission_data: MissionData,
dynamic_runways: dict[str, RunwayData],
use_client: bool,
@ -64,6 +73,7 @@ class FlightGroupConfigurator:
self.time = time
self.radio_registry = radio_registry
self.tacan_registry = tacan_registry
self.datalink_registry = datalink_registry
self.mission_data = mission_data
self.dynamic_runways = dynamic_runways
self.use_client = use_client
@ -209,6 +219,7 @@ class FlightGroupConfigurator:
start_time=self.flight.flight_plan.patrol_start_time,
end_time=self.flight.flight_plan.patrol_end_time,
blue=self.flight.departure.captured,
unit=self.group.units[0],
)
)
elif isinstance(
@ -276,14 +287,50 @@ class FlightGroupConfigurator:
return levels[new_level]
def setup_props(self) -> None:
unit: FlyingUnit
member: FlightMember
for unit, member in zip(self.group.units, self.flight.iter_members()):
props = dict(member.properties)
if (code := member.weapon_laser_code) is not None:
for laser_code_config in self.flight.unit_type.laser_code_configs:
props.update(laser_code_config.property_dict_for_code(code.code))
if unit.unit_type.datalink_networkable() and self.no_datalink_set(props):
self.set_datalink(props, unit.callsign_as_str())
for prop_id, value in props.items():
unit.set_property(prop_id, value)
@staticmethod
def no_datalink_set(props: dict[str, bool | float | int | str]) -> bool:
for type in DataLinkKey:
if type.value in props:
return False
return True
def set_datalink(
self, props: dict[str, bool | float | int | str], callsign: str
) -> None:
vcl = callsign[:-2][0] + callsign[:-2][-1]
vcn = callsign[-2:]
identifier = self.datalink_registry.alloc_for_aircraft(self.flight.unit_type)
self.set_datalink_props(props, vcl.upper(), vcn, identifier)
@staticmethod
def set_datalink_props(
props: dict[str, bool | float | int | str],
label: str,
number: str,
identifier: DataLinkIdentifier,
) -> None:
if identifier.type in [DataLinkKey.LINK16, DataLinkKey.SADL]:
if not props.get(VOICE_CALLSIGN_LABEL):
props[VOICE_CALLSIGN_LABEL] = label
if not props.get(VOICE_CALLSIGN_NUMBER):
props[VOICE_CALLSIGN_NUMBER] = number
elif identifier.type in [DataLinkKey.IDM]:
if not props.get(OWNSHIP_CALLSIGN):
props[OWNSHIP_CALLSIGN] = f"G-{identifier.id}"
props[identifier.type.value] = identifier.id
def setup_payloads(self) -> None:
for unit, member in zip(self.group.units, self.flight.iter_members()):
self.setup_payload(unit, member)

View File

@ -4,6 +4,8 @@ from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional, TYPE_CHECKING
from dcs.flyingunit import FlyingUnit
from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
from game.missiongenerator.aircraft.flightdata import FlightData
@ -36,6 +38,7 @@ class AwacsInfo(GroupInfo):
depature_location: Optional[str]
start_time: datetime
end_time: datetime
unit: FlyingUnit # reference to be used as L16 donor
@dataclass
@ -102,6 +105,7 @@ class MissionData:
runways: list[RunwayData] = field(default_factory=list)
carriers: list[CarrierInfo] = field(default_factory=list)
flights: list[FlightData] = field(default_factory=list)
packages: dict[int, list[FlightData]] = field(default_factory=dict)
tankers: list[TankerInfo] = field(default_factory=list)
jtacs: list[JtacInfo] = field(default_factory=list)
logistics: list[LogisticsInfo] = field(default_factory=list)

View File

@ -40,6 +40,7 @@ from .tgogenerator import TgoGenerator
from .triggergenerator import TriggerGenerator
from .visualsgenerator import VisualsGenerator
from ..radio.TacanContainer import TacanContainer
from ..radio.datalink import DataLinkRegistry
if TYPE_CHECKING:
from game import Game
@ -56,6 +57,7 @@ class MissionGenerator:
self.radio_registry = RadioRegistry()
self.tacan_registry = TacanRegistry()
self.datalink_registry = DataLinkRegistry()
self.generation_started = False
@ -247,6 +249,7 @@ class MissionGenerator:
self.time,
self.radio_registry,
self.tacan_registry,
self.datalink_registry,
self.unit_map,
mission_data=self.mission_data,
helipads=tgo_generator.helipads,

View File

@ -28,6 +28,7 @@ from game.pretense.pretenseflightgroupconfigurator import (
PretenseFlightGroupConfigurator,
)
from game.pretense.pretenseflightgroupspawner import PretenseNameGenerator
from game.radio.datalink import DataLinkRegistry
from game.radio.radios import RadioRegistry
from game.radio.tacan import TacanRegistry
from game.runways import RunwayData
@ -64,6 +65,7 @@ class PretenseAircraftGenerator:
time: datetime,
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,
datalink_registry: DataLinkRegistry,
laser_code_registry: LaserCodeRegistry,
unit_map: UnitMap,
mission_data: MissionData,
@ -78,6 +80,7 @@ class PretenseAircraftGenerator:
self.time = time
self.radio_registry = radio_registry
self.tacan_registy = tacan_registry
self.datalink_registry = datalink_registry
self.laser_code_registry = laser_code_registry
self.unit_map = unit_map
self.flights: List[FlightData] = []
@ -1032,6 +1035,7 @@ class PretenseAircraftGenerator:
self.time,
self.radio_registry,
self.tacan_registy,
self.datalink_registry,
self.mission_data,
dynamic_runways,
self.use_client,

View File

@ -12,7 +12,6 @@ from game.ato.flightmember import FlightMember
from game.ato.flightwaypoint import FlightWaypoint
from game.ato.flightwaypointtype import FlightWaypointType
from game.data.weapons import Pylon
from game.lasercodes.lasercoderegistry import LaserCodeRegistry
from game.missiongenerator.aircraft.aircraftbehavior import AircraftBehavior
from game.missiongenerator.aircraft.aircraftpainter import AircraftPainter
from game.missiongenerator.aircraft.bingoestimator import BingoEstimator
@ -25,6 +24,7 @@ from game.missiongenerator.aircraft.waypoints.pydcswaypointbuilder import (
PydcsWaypointBuilder,
)
from game.missiongenerator.missiondata import MissionData
from game.radio.datalink import DataLinkRegistry
from game.radio.radios import RadioRegistry
from game.radio.tacan import (
TacanRegistry,
@ -45,6 +45,7 @@ class PretenseFlightGroupConfigurator(FlightGroupConfigurator):
time: datetime,
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,
datalink_registry: DataLinkRegistry,
mission_data: MissionData,
dynamic_runways: dict[str, RunwayData],
use_client: bool,
@ -57,6 +58,7 @@ class PretenseFlightGroupConfigurator(FlightGroupConfigurator):
time,
radio_registry,
tacan_registry,
datalink_registry,
mission_data,
dynamic_runways,
use_client,
@ -69,6 +71,7 @@ class PretenseFlightGroupConfigurator(FlightGroupConfigurator):
self.time = time
self.radio_registry = radio_registry
self.tacan_registry = tacan_registry
self.datalink_registry = datalink_registry
self.mission_data = mission_data
self.dynamic_runways = dynamic_runways
self.use_client = use_client

View File

@ -7,7 +7,7 @@ from pathlib import Path
from typing import TYPE_CHECKING
import dcs.lua
from dcs import Mission, Point
from dcs import Point
from dcs.coalition import Coalition
from dcs.countries import (
country_dict,
@ -16,21 +16,18 @@ from dcs.countries import (
)
from dcs.task import AFAC, FAC, SetInvisibleCommand, SetImmortalCommand, OrbitAction
from game.lasercodes.lasercoderegistry import LaserCodeRegistry
from game.missiongenerator.convoygenerator import ConvoyGenerator
from game.missiongenerator.environmentgenerator import EnvironmentGenerator
from game.missiongenerator.forcedoptionsgenerator import ForcedOptionsGenerator
from game.missiongenerator.frontlineconflictdescription import (
FrontLineConflictDescription,
)
from game.missiongenerator.missiondata import MissionData, JtacInfo
from game.missiongenerator.missiondata import JtacInfo
from game.missiongenerator.tgogenerator import TgoGenerator
from game.missiongenerator.visualsgenerator import VisualsGenerator
from game.naming import namegen
from game.persistency import pre_pretense_backups_dir
from game.pretense.pretenseaircraftgenerator import PretenseAircraftGenerator
from game.radio.radios import RadioRegistry
from game.radio.tacan import TacanRegistry
from game.theater.bullseye import Bullseye
from game.unitmap import UnitMap
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
@ -40,6 +37,7 @@ from .pretensetriggergenerator import PretenseTriggerGenerator
from ..ato.airtaaskingorder import AirTaskingOrder
from ..callsigns import callsign_for_support_unit
from ..dcs.aircrafttype import AircraftType
from ..lasercodes import LaserCodeRegistry
from ..missiongenerator import MissionGenerator
from ..theater import Airfield
@ -50,21 +48,8 @@ if TYPE_CHECKING:
class PretenseMissionGenerator(MissionGenerator):
def __init__(self, game: Game, time: datetime) -> None:
super().__init__(game, time)
self.game = game
self.time = time
self.mission = Mission(game.theater.terrain)
self.unit_map = UnitMap()
self.mission_data = MissionData()
self.laser_code_registry = LaserCodeRegistry()
self.radio_registry = RadioRegistry()
self.tacan_registry = TacanRegistry()
self.generation_started = False
self.p_country = country_dict[self.game.blue.faction.country.id]()
self.e_country = country_dict[self.game.red.faction.country.id]()
with open("resources/default_options.lua", "r", encoding="utf-8") as f:
options = dcs.lua.loads(f.read())["options"]
@ -262,6 +247,7 @@ class PretenseMissionGenerator(MissionGenerator):
self.time,
self.radio_registry,
self.tacan_registry,
self.datalink_registry,
self.laser_code_registry,
self.unit_map,
mission_data=self.mission_data,

107
game/radio/datalink.py Normal file
View File

@ -0,0 +1,107 @@
"""DATALINK handling."""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from typing import Dict, Iterator, Set
from game.dcs.aircrafttype import AircraftType
VOICE_CALLSIGN_LABEL = "VoiceCallsignLabel"
VOICE_CALLSIGN_NUMBER = "VoiceCallsignNumber"
OWNSHIP_CALLSIGN = "OwnshipCallSign"
class DataLinkKey(Enum):
LINK16 = "STN_L16"
SADL = "SADL_TN"
IDM = "TN_IDM_LB"
Unknown = "Unsupported type"
@staticmethod
def from_aircraft_type(ac_type: AircraftType) -> DataLinkKey:
dcs_type = ac_type.dcs_unit_type
if DataLinkKey.LINK16.value in dcs_type.properties:
return DataLinkKey.LINK16
elif DataLinkKey.SADL.value in dcs_type.properties:
return DataLinkKey.SADL
elif DataLinkKey.IDM.value in dcs_type.properties:
return DataLinkKey.IDM
return DataLinkKey.Unknown
def range(self) -> Iterator["DataLinkIdentifier"]:
match self.value:
case DataLinkKey.LINK16.value:
return (
DataLinkIdentifier(str(f"{x:05o}"), self) for x in range(1, 0o77777)
)
case DataLinkKey.SADL.value:
return (
DataLinkIdentifier(str(f"{x:04o}"), self) for x in range(1, 0o7777)
)
case DataLinkKey.IDM.value:
return (DataLinkIdentifier(x, self) for x in self._idm_ids())
raise RuntimeError(f"No range for datalink-type: {self.value}")
def valid_identifiers(self) -> Iterator["DataLinkIdentifier"]:
for x in self.range():
yield x
@staticmethod
def _idm_ids() -> Iterator[str]: # TODO: there must be a better place for this...
second_range = [str(x) for x in range(1, 10)]
for single in second_range:
yield f"{single}"
for first in range(1, 4):
second_range = [str(x) for x in range(1, 10)]
if first < 3:
second_range.extend([str(chr(x)) for x in range(65, 91)])
else:
second_range.extend([str(chr(x)) for x in range(65, 74)])
for second in second_range:
yield f"{first}{second}"
@dataclass
class DataLinkIdentifier:
id: str
type: DataLinkKey
def __hash__(self) -> int:
return f"{self.id} - {self.type.value}".__hash__()
class OutOfIdentifiersError(RuntimeError):
"""Raised when all channels in this band have been allocated."""
def __init__(self, type: DataLinkKey) -> None:
super().__init__(
f"No available identifiers left for datalink-type {type.value}"
)
class DataLinkRegistry:
"""Manages allocation of DATALINK identifiers."""
def __init__(self) -> None:
self.allocated_identifiers: Set[DataLinkIdentifier] = set()
self.allocators: Dict[DataLinkKey, Iterator[DataLinkIdentifier]] = {}
for type in DataLinkKey:
self.allocators[type] = type.valid_identifiers()
def alloc_for_aircraft(self, ac_type: AircraftType) -> DataLinkIdentifier:
datalink_type = DataLinkKey.from_aircraft_type(ac_type)
allocator = self.allocators[datalink_type]
try:
while (identifier := next(allocator)) in self.allocated_identifiers:
pass
self.mark_unavailable(identifier)
return identifier
except StopIteration:
raise OutOfIdentifiersError(datalink_type)
def mark_unavailable(self, identifier: DataLinkIdentifier) -> None:
self.allocated_identifiers.add(identifier)

View File

@ -0,0 +1,145 @@
import re
from typing import Dict, Callable, Union
from PySide6.QtGui import QFocusEvent
from PySide6.QtWidgets import QLineEdit
from dcs.unitpropertydescription import UnitPropertyDescription
from game.ato import Flight
from game.ato.flightmember import FlightMember
from game.radio.datalink import DataLinkKey
class PropertyEditBox(QLineEdit):
def __init__(
self,
flight_member: FlightMember,
prop: UnitPropertyDescription,
flight: Flight,
) -> None:
super().__init__()
self.flight_member = flight_member
self.prop = prop
self.flight = flight
self.setFixedWidth(100)
self.setText(
self.flight_member.properties.get(self.prop.identifier, self.prop.default)
)
self.textChanged.connect(self.on_value_changed)
self._datalink_handlers: Dict[
DataLinkKey,
Union[Callable[[int, str], str], Callable[[int, str, bool], str]],
] = {
DataLinkKey.LINK16: self._link16_input,
DataLinkKey.SADL: self._sadl_input,
DataLinkKey.IDM: self._idm_input,
}
@property
def datalink_type(self) -> DataLinkKey:
return DataLinkKey.from_aircraft_type(self.flight.unit_type)
def focusOutEvent(self, e: QFocusEvent) -> None:
super(PropertyEditBox, self).focusOutEvent(e)
link_type = self.datalink_type
if (handler := self._datalink_handlers.get(link_type)) is not None:
value = handler(0, self.text(), True)
self.setText(value)
def on_value_changed(self, value: str) -> None:
cursor = self.cursorPosition()
link_type = self.datalink_type
if (handler := self._datalink_handlers.get(link_type)) is not None:
value = handler(cursor, value.upper())
if not value and self.prop.identifier in self.flight_member.properties:
del self.flight_member.properties[self.prop.identifier]
else:
self.flight_member.properties[self.prop.identifier] = value
self.setText(value)
self.setCursorPosition(cursor)
if cursor > len(value):
self.setCursorPosition(0)
def _link16_input(self, cursor: int, value: str, focus_lost: bool = False) -> str:
max_input_length = 5
if "VoiceCallsign" in self.prop.identifier:
max_input_length = 2
value = self.common_link16_sadl_logic(
cursor, focus_lost, max_input_length, "STN_L16", value
)
return value
def common_link16_sadl_logic(
self,
cursor: int,
focus_lost: bool,
max_input_length: int,
identifier: str,
value: str,
) -> str:
value = value[:max_input_length]
if not (
("Label" in self.prop.identifier and value.isalpha())
or ("Number" in self.prop.identifier and value.isdigit())
or (identifier == self.prop.identifier and self._is_octal(value))
):
value = self._restore_from_property(cursor)
if focus_lost and 2 < max_input_length > len(value):
# STN_L16 --> prepend with zeroes
value = "0" * (max_input_length - len(value)) + value
return value
def _restore_from_property(self, cursor):
props = self.flight_member.properties
value = props.get(self.prop.identifier, "")[: cursor - 1]
return value
@staticmethod
def _is_octal(value: str) -> bool:
if re.match(r"^[0-7]+$", value):
return True
return False
def _sadl_input(self, cursor: int, value: str, focus_lost: bool = False) -> str:
max_input_length = 4
if "VoiceCallsign" in self.prop.identifier:
max_input_length = 2
value = self.common_link16_sadl_logic(
cursor, focus_lost, max_input_length, "SADL_TN", value
)
return value
def _idm_input(self, cursor: int, value: str, _: bool = False) -> str:
if "TN_IDM_LB" == self.prop.identifier:
value = value[:2] # Originator ID
if len(value) == 1 and not value.isalnum():
return ""
elif len(value) == 2 and not self._valid_2char_idm(value):
value = str(value[0])
else:
value = value[:5] # Ownship CallSign
if not re.match(r"^[A-Z0-9/*\-+.]{,5}$", value):
value = self._restore_from_property(cursor)
return value
@staticmethod
def _valid_2char_idm(value: str) -> bool:
if len(value) != 2:
return False
first = value[0]
second = value[1]
return (
first in ["1", "2"]
and second.isalnum()
or first == "3"
and (re.match(r"^[A-I]$", second) or second.isdigit())
)
def set_flight_member(self, flight_member: FlightMember) -> None:
self.flight_member = flight_member
self.setText(
self.flight_member.properties.get(self.prop.identifier, self.prop.default)
)

View File

@ -11,6 +11,7 @@ from game.ato.flightmember import FlightMember
from .missingpropertydataerror import MissingPropertyDataError
from .propertycheckbox import PropertyCheckBox
from .propertycombobox import PropertyComboBox
from .propertyeditbox import PropertyEditBox
from .propertyspinbox import PropertySpinBox
@ -22,6 +23,7 @@ class UnhandledControlTypeError(RuntimeError):
class PropertyEditor(QGridLayout):
def __init__(self, flight: Flight, flight_member: FlightMember) -> None:
super().__init__()
self.flight = flight
self.flight_member = flight_member
self.flight_member_update_listeners: list[Callable[[FlightMember], None]] = []
@ -78,7 +80,7 @@ class PropertyEditor(QGridLayout):
def control_for_property(self, prop: UnitPropertyDescription) -> Optional[QWidget]:
# Valid values are:
# "checkbox", "comboList", "groupbox", "label", "slider", "spinbox"
# "checkbox", "comboList", "groupbox", "label", "slider", "spinbox", "editbox"
match prop.control:
case "checkbox":
widget = PropertyCheckBox(self.flight_member, prop)
@ -94,6 +96,10 @@ class PropertyEditor(QGridLayout):
widget = PropertySpinBox(self.flight_member, prop)
self.flight_member_update_listeners.append(widget.set_flight_member)
return widget
case "editbox":
widget = PropertyEditBox(self.flight_member, prop, self.flight)
self.flight_member_update_listeners.append(widget.set_flight_member)
return widget
case _:
raise UnhandledControlTypeError(prop.control)

View File

@ -33,7 +33,7 @@ pluggy==1.5.0
pre-commit==3.7.1
pydantic==2.7.4
pydantic-settings==2.3.3
pydcs @ git+https://github.com/dcs-retribution/pydcs@ef9c72f1075546787b21da2bc25d228c32aca9ce
pydcs @ git+https://github.com/dcs-retribution/pydcs@4b63a60f410b5542c9654ebe4d74cfcb17e06eff
pyinstaller==5.13.2
pyinstaller-hooks-contrib==2024.0
pyparsing==3.1.2