mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
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:
parent
0eb06f9af5
commit
64d60e5ccf
@ -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]** 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/UX]** Allow changing loadout on flight creation
|
||||||
* **[UI]** Display TOT for all waypoints in the flight plan
|
* **[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
|
## Fixes
|
||||||
* **[UI/UX]** A-10A flights can be edited again
|
* **[UI/UX]** A-10A flights can be edited again
|
||||||
|
|||||||
@ -16,7 +16,7 @@ class FlightMember:
|
|||||||
self.use_custom_loadout = False
|
self.use_custom_loadout = False
|
||||||
self.tgp_laser_code: LaserCode | None = None
|
self.tgp_laser_code: LaserCode | None = None
|
||||||
self.weapon_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.livery: Optional[str] = None
|
||||||
self.use_livery_set: bool = True
|
self.use_livery_set: bool = True
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,9 @@ from dcs import Point
|
|||||||
from dcs.action import AITaskPush
|
from dcs.action import AITaskPush
|
||||||
from dcs.condition import FlagIsTrue, GroupDead, Or, FlagIsFalse
|
from dcs.condition import FlagIsTrue, GroupDead, Or, FlagIsFalse
|
||||||
from dcs.country import Country
|
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.mission import Mission
|
||||||
from dcs.terrain.terrain import NoParkingSlotError
|
from dcs.terrain.terrain import NoParkingSlotError
|
||||||
from dcs.triggers import TriggerOnce, Event
|
from dcs.triggers import TriggerOnce, Event
|
||||||
@ -37,6 +40,7 @@ from .flightdata import FlightData
|
|||||||
from .flightgroupconfigurator import FlightGroupConfigurator
|
from .flightgroupconfigurator import FlightGroupConfigurator
|
||||||
from .flightgroupspawner import FlightGroupSpawner
|
from .flightgroupspawner import FlightGroupSpawner
|
||||||
from ...data.weapons import WeaponType
|
from ...data.weapons import WeaponType
|
||||||
|
from ...radio.datalink import DataLinkRegistry
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game import Game
|
from game import Game
|
||||||
@ -52,6 +56,7 @@ class AircraftGenerator:
|
|||||||
time: datetime,
|
time: datetime,
|
||||||
radio_registry: RadioRegistry,
|
radio_registry: RadioRegistry,
|
||||||
tacan_registry: TacanRegistry,
|
tacan_registry: TacanRegistry,
|
||||||
|
datalink_registry: DataLinkRegistry,
|
||||||
unit_map: UnitMap,
|
unit_map: UnitMap,
|
||||||
mission_data: MissionData,
|
mission_data: MissionData,
|
||||||
helipads: dict[ControlPoint, list[StaticGroup]],
|
helipads: dict[ControlPoint, list[StaticGroup]],
|
||||||
@ -65,6 +70,7 @@ class AircraftGenerator:
|
|||||||
self.time = time
|
self.time = time
|
||||||
self.radio_registry = radio_registry
|
self.radio_registry = radio_registry
|
||||||
self.tacan_registy = tacan_registry
|
self.tacan_registy = tacan_registry
|
||||||
|
self.datalink_registry = datalink_registry
|
||||||
self.unit_map = unit_map
|
self.unit_map = unit_map
|
||||||
self.flights: List[FlightData] = []
|
self.flights: List[FlightData] = []
|
||||||
self.mission_data = mission_data
|
self.mission_data = mission_data
|
||||||
@ -115,6 +121,7 @@ class AircraftGenerator:
|
|||||||
dynamic_runways: Runway data for carriers and FARPs.
|
dynamic_runways: Runway data for carriers and FARPs.
|
||||||
"""
|
"""
|
||||||
self._reserve_frequencies_and_tacan(ato)
|
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)):
|
for package in reversed(sorted(ato.packages, key=lambda x: x.time_over_target)):
|
||||||
logging.info(f"Generating package for target: {package.target.name}")
|
logging.info(f"Generating package for target: {package.target.name}")
|
||||||
@ -153,6 +160,39 @@ class AircraftGenerator:
|
|||||||
if len(splittrigger.actions) > 0:
|
if len(splittrigger.actions) > 0:
|
||||||
self.mission.triggerrules.triggers.append(splittrigger)
|
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(
|
def spawn_unused_aircraft(
|
||||||
self, player_country: Country, enemy_country: Country
|
self, player_country: Country, enemy_country: Country
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -246,26 +286,58 @@ class AircraftGenerator:
|
|||||||
self.ground_spawns,
|
self.ground_spawns,
|
||||||
self.mission_data,
|
self.mission_data,
|
||||||
).create_flight_group()
|
).create_flight_group()
|
||||||
self.flights.append(
|
|
||||||
FlightGroupConfigurator(
|
flight_data = FlightGroupConfigurator(
|
||||||
flight,
|
flight,
|
||||||
group,
|
group,
|
||||||
self.game,
|
self.game,
|
||||||
self.mission,
|
self.mission,
|
||||||
self.time,
|
self.time,
|
||||||
self.radio_registry,
|
self.radio_registry,
|
||||||
self.tacan_registy,
|
self.tacan_registy,
|
||||||
self.mission_data,
|
self.datalink_registry,
|
||||||
dynamic_runways,
|
self.mission_data,
|
||||||
self.use_client,
|
dynamic_runways,
|
||||||
).configure()
|
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:
|
if self.ewrj:
|
||||||
self._track_ewrj_flight(flight, group)
|
self._track_ewrj_flight(flight, group)
|
||||||
|
|
||||||
return 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:
|
def _track_ewrj_flight(self, flight: Flight, group: FlyingGroup[Any]) -> None:
|
||||||
if not self.ewrj_package_dict.get(id(flight.package)):
|
if not self.ewrj_package_dict.get(id(flight.package)):
|
||||||
self.ewrj_package_dict[id(flight.package)] = []
|
self.ewrj_package_dict[id(flight.package)] = []
|
||||||
|
|||||||
@ -37,6 +37,14 @@ from ...ato.flightmember import FlightMember
|
|||||||
from ...ato.flightplans.aewc import AewcFlightPlan
|
from ...ato.flightplans.aewc import AewcFlightPlan
|
||||||
from ...ato.flightplans.packagerefueling import PackageRefuelingFlightPlan
|
from ...ato.flightplans.packagerefueling import PackageRefuelingFlightPlan
|
||||||
from ...ato.flightplans.theaterrefueling import TheaterRefuelingFlightPlan
|
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
|
from ...theater import Fob
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -53,6 +61,7 @@ class FlightGroupConfigurator:
|
|||||||
time: datetime,
|
time: datetime,
|
||||||
radio_registry: RadioRegistry,
|
radio_registry: RadioRegistry,
|
||||||
tacan_registry: TacanRegistry,
|
tacan_registry: TacanRegistry,
|
||||||
|
datalink_registry: DataLinkRegistry,
|
||||||
mission_data: MissionData,
|
mission_data: MissionData,
|
||||||
dynamic_runways: dict[str, RunwayData],
|
dynamic_runways: dict[str, RunwayData],
|
||||||
use_client: bool,
|
use_client: bool,
|
||||||
@ -64,6 +73,7 @@ class FlightGroupConfigurator:
|
|||||||
self.time = time
|
self.time = time
|
||||||
self.radio_registry = radio_registry
|
self.radio_registry = radio_registry
|
||||||
self.tacan_registry = tacan_registry
|
self.tacan_registry = tacan_registry
|
||||||
|
self.datalink_registry = datalink_registry
|
||||||
self.mission_data = mission_data
|
self.mission_data = mission_data
|
||||||
self.dynamic_runways = dynamic_runways
|
self.dynamic_runways = dynamic_runways
|
||||||
self.use_client = use_client
|
self.use_client = use_client
|
||||||
@ -209,6 +219,7 @@ class FlightGroupConfigurator:
|
|||||||
start_time=self.flight.flight_plan.patrol_start_time,
|
start_time=self.flight.flight_plan.patrol_start_time,
|
||||||
end_time=self.flight.flight_plan.patrol_end_time,
|
end_time=self.flight.flight_plan.patrol_end_time,
|
||||||
blue=self.flight.departure.captured,
|
blue=self.flight.departure.captured,
|
||||||
|
unit=self.group.units[0],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
elif isinstance(
|
elif isinstance(
|
||||||
@ -276,14 +287,50 @@ class FlightGroupConfigurator:
|
|||||||
return levels[new_level]
|
return levels[new_level]
|
||||||
|
|
||||||
def setup_props(self) -> None:
|
def setup_props(self) -> None:
|
||||||
|
unit: FlyingUnit
|
||||||
|
member: FlightMember
|
||||||
for unit, member in zip(self.group.units, self.flight.iter_members()):
|
for unit, member in zip(self.group.units, self.flight.iter_members()):
|
||||||
props = dict(member.properties)
|
props = dict(member.properties)
|
||||||
if (code := member.weapon_laser_code) is not None:
|
if (code := member.weapon_laser_code) is not None:
|
||||||
for laser_code_config in self.flight.unit_type.laser_code_configs:
|
for laser_code_config in self.flight.unit_type.laser_code_configs:
|
||||||
props.update(laser_code_config.property_dict_for_code(code.code))
|
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():
|
for prop_id, value in props.items():
|
||||||
unit.set_property(prop_id, value)
|
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:
|
def setup_payloads(self) -> None:
|
||||||
for unit, member in zip(self.group.units, self.flight.iter_members()):
|
for unit, member in zip(self.group.units, self.flight.iter_members()):
|
||||||
self.setup_payload(unit, member)
|
self.setup_payload(unit, member)
|
||||||
|
|||||||
@ -4,6 +4,8 @@ from dataclasses import dataclass, field
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional, TYPE_CHECKING
|
from typing import Optional, TYPE_CHECKING
|
||||||
|
|
||||||
|
from dcs.flyingunit import FlyingUnit
|
||||||
|
|
||||||
from game.dcs.aircrafttype import AircraftType
|
from game.dcs.aircrafttype import AircraftType
|
||||||
from game.dcs.groundunittype import GroundUnitType
|
from game.dcs.groundunittype import GroundUnitType
|
||||||
from game.missiongenerator.aircraft.flightdata import FlightData
|
from game.missiongenerator.aircraft.flightdata import FlightData
|
||||||
@ -36,6 +38,7 @@ class AwacsInfo(GroupInfo):
|
|||||||
depature_location: Optional[str]
|
depature_location: Optional[str]
|
||||||
start_time: datetime
|
start_time: datetime
|
||||||
end_time: datetime
|
end_time: datetime
|
||||||
|
unit: FlyingUnit # reference to be used as L16 donor
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -102,6 +105,7 @@ class MissionData:
|
|||||||
runways: list[RunwayData] = field(default_factory=list)
|
runways: list[RunwayData] = field(default_factory=list)
|
||||||
carriers: list[CarrierInfo] = field(default_factory=list)
|
carriers: list[CarrierInfo] = field(default_factory=list)
|
||||||
flights: list[FlightData] = 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)
|
tankers: list[TankerInfo] = field(default_factory=list)
|
||||||
jtacs: list[JtacInfo] = field(default_factory=list)
|
jtacs: list[JtacInfo] = field(default_factory=list)
|
||||||
logistics: list[LogisticsInfo] = field(default_factory=list)
|
logistics: list[LogisticsInfo] = field(default_factory=list)
|
||||||
|
|||||||
@ -40,6 +40,7 @@ from .tgogenerator import TgoGenerator
|
|||||||
from .triggergenerator import TriggerGenerator
|
from .triggergenerator import TriggerGenerator
|
||||||
from .visualsgenerator import VisualsGenerator
|
from .visualsgenerator import VisualsGenerator
|
||||||
from ..radio.TacanContainer import TacanContainer
|
from ..radio.TacanContainer import TacanContainer
|
||||||
|
from ..radio.datalink import DataLinkRegistry
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game import Game
|
from game import Game
|
||||||
@ -56,6 +57,7 @@ class MissionGenerator:
|
|||||||
|
|
||||||
self.radio_registry = RadioRegistry()
|
self.radio_registry = RadioRegistry()
|
||||||
self.tacan_registry = TacanRegistry()
|
self.tacan_registry = TacanRegistry()
|
||||||
|
self.datalink_registry = DataLinkRegistry()
|
||||||
|
|
||||||
self.generation_started = False
|
self.generation_started = False
|
||||||
|
|
||||||
@ -247,6 +249,7 @@ class MissionGenerator:
|
|||||||
self.time,
|
self.time,
|
||||||
self.radio_registry,
|
self.radio_registry,
|
||||||
self.tacan_registry,
|
self.tacan_registry,
|
||||||
|
self.datalink_registry,
|
||||||
self.unit_map,
|
self.unit_map,
|
||||||
mission_data=self.mission_data,
|
mission_data=self.mission_data,
|
||||||
helipads=tgo_generator.helipads,
|
helipads=tgo_generator.helipads,
|
||||||
|
|||||||
@ -28,6 +28,7 @@ from game.pretense.pretenseflightgroupconfigurator import (
|
|||||||
PretenseFlightGroupConfigurator,
|
PretenseFlightGroupConfigurator,
|
||||||
)
|
)
|
||||||
from game.pretense.pretenseflightgroupspawner import PretenseNameGenerator
|
from game.pretense.pretenseflightgroupspawner import PretenseNameGenerator
|
||||||
|
from game.radio.datalink import DataLinkRegistry
|
||||||
from game.radio.radios import RadioRegistry
|
from game.radio.radios import RadioRegistry
|
||||||
from game.radio.tacan import TacanRegistry
|
from game.radio.tacan import TacanRegistry
|
||||||
from game.runways import RunwayData
|
from game.runways import RunwayData
|
||||||
@ -64,6 +65,7 @@ class PretenseAircraftGenerator:
|
|||||||
time: datetime,
|
time: datetime,
|
||||||
radio_registry: RadioRegistry,
|
radio_registry: RadioRegistry,
|
||||||
tacan_registry: TacanRegistry,
|
tacan_registry: TacanRegistry,
|
||||||
|
datalink_registry: DataLinkRegistry,
|
||||||
laser_code_registry: LaserCodeRegistry,
|
laser_code_registry: LaserCodeRegistry,
|
||||||
unit_map: UnitMap,
|
unit_map: UnitMap,
|
||||||
mission_data: MissionData,
|
mission_data: MissionData,
|
||||||
@ -78,6 +80,7 @@ class PretenseAircraftGenerator:
|
|||||||
self.time = time
|
self.time = time
|
||||||
self.radio_registry = radio_registry
|
self.radio_registry = radio_registry
|
||||||
self.tacan_registy = tacan_registry
|
self.tacan_registy = tacan_registry
|
||||||
|
self.datalink_registry = datalink_registry
|
||||||
self.laser_code_registry = laser_code_registry
|
self.laser_code_registry = laser_code_registry
|
||||||
self.unit_map = unit_map
|
self.unit_map = unit_map
|
||||||
self.flights: List[FlightData] = []
|
self.flights: List[FlightData] = []
|
||||||
@ -1032,6 +1035,7 @@ class PretenseAircraftGenerator:
|
|||||||
self.time,
|
self.time,
|
||||||
self.radio_registry,
|
self.radio_registry,
|
||||||
self.tacan_registy,
|
self.tacan_registy,
|
||||||
|
self.datalink_registry,
|
||||||
self.mission_data,
|
self.mission_data,
|
||||||
dynamic_runways,
|
dynamic_runways,
|
||||||
self.use_client,
|
self.use_client,
|
||||||
|
|||||||
@ -12,7 +12,6 @@ from game.ato.flightmember import FlightMember
|
|||||||
from game.ato.flightwaypoint import FlightWaypoint
|
from game.ato.flightwaypoint import FlightWaypoint
|
||||||
from game.ato.flightwaypointtype import FlightWaypointType
|
from game.ato.flightwaypointtype import FlightWaypointType
|
||||||
from game.data.weapons import Pylon
|
from game.data.weapons import Pylon
|
||||||
from game.lasercodes.lasercoderegistry import LaserCodeRegistry
|
|
||||||
from game.missiongenerator.aircraft.aircraftbehavior import AircraftBehavior
|
from game.missiongenerator.aircraft.aircraftbehavior import AircraftBehavior
|
||||||
from game.missiongenerator.aircraft.aircraftpainter import AircraftPainter
|
from game.missiongenerator.aircraft.aircraftpainter import AircraftPainter
|
||||||
from game.missiongenerator.aircraft.bingoestimator import BingoEstimator
|
from game.missiongenerator.aircraft.bingoestimator import BingoEstimator
|
||||||
@ -25,6 +24,7 @@ from game.missiongenerator.aircraft.waypoints.pydcswaypointbuilder import (
|
|||||||
PydcsWaypointBuilder,
|
PydcsWaypointBuilder,
|
||||||
)
|
)
|
||||||
from game.missiongenerator.missiondata import MissionData
|
from game.missiongenerator.missiondata import MissionData
|
||||||
|
from game.radio.datalink import DataLinkRegistry
|
||||||
from game.radio.radios import RadioRegistry
|
from game.radio.radios import RadioRegistry
|
||||||
from game.radio.tacan import (
|
from game.radio.tacan import (
|
||||||
TacanRegistry,
|
TacanRegistry,
|
||||||
@ -45,6 +45,7 @@ class PretenseFlightGroupConfigurator(FlightGroupConfigurator):
|
|||||||
time: datetime,
|
time: datetime,
|
||||||
radio_registry: RadioRegistry,
|
radio_registry: RadioRegistry,
|
||||||
tacan_registry: TacanRegistry,
|
tacan_registry: TacanRegistry,
|
||||||
|
datalink_registry: DataLinkRegistry,
|
||||||
mission_data: MissionData,
|
mission_data: MissionData,
|
||||||
dynamic_runways: dict[str, RunwayData],
|
dynamic_runways: dict[str, RunwayData],
|
||||||
use_client: bool,
|
use_client: bool,
|
||||||
@ -57,6 +58,7 @@ class PretenseFlightGroupConfigurator(FlightGroupConfigurator):
|
|||||||
time,
|
time,
|
||||||
radio_registry,
|
radio_registry,
|
||||||
tacan_registry,
|
tacan_registry,
|
||||||
|
datalink_registry,
|
||||||
mission_data,
|
mission_data,
|
||||||
dynamic_runways,
|
dynamic_runways,
|
||||||
use_client,
|
use_client,
|
||||||
@ -69,6 +71,7 @@ class PretenseFlightGroupConfigurator(FlightGroupConfigurator):
|
|||||||
self.time = time
|
self.time = time
|
||||||
self.radio_registry = radio_registry
|
self.radio_registry = radio_registry
|
||||||
self.tacan_registry = tacan_registry
|
self.tacan_registry = tacan_registry
|
||||||
|
self.datalink_registry = datalink_registry
|
||||||
self.mission_data = mission_data
|
self.mission_data = mission_data
|
||||||
self.dynamic_runways = dynamic_runways
|
self.dynamic_runways = dynamic_runways
|
||||||
self.use_client = use_client
|
self.use_client = use_client
|
||||||
|
|||||||
@ -7,7 +7,7 @@ from pathlib import Path
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import dcs.lua
|
import dcs.lua
|
||||||
from dcs import Mission, Point
|
from dcs import Point
|
||||||
from dcs.coalition import Coalition
|
from dcs.coalition import Coalition
|
||||||
from dcs.countries import (
|
from dcs.countries import (
|
||||||
country_dict,
|
country_dict,
|
||||||
@ -16,21 +16,18 @@ from dcs.countries import (
|
|||||||
)
|
)
|
||||||
from dcs.task import AFAC, FAC, SetInvisibleCommand, SetImmortalCommand, OrbitAction
|
from dcs.task import AFAC, FAC, SetInvisibleCommand, SetImmortalCommand, OrbitAction
|
||||||
|
|
||||||
from game.lasercodes.lasercoderegistry import LaserCodeRegistry
|
|
||||||
from game.missiongenerator.convoygenerator import ConvoyGenerator
|
from game.missiongenerator.convoygenerator import ConvoyGenerator
|
||||||
from game.missiongenerator.environmentgenerator import EnvironmentGenerator
|
from game.missiongenerator.environmentgenerator import EnvironmentGenerator
|
||||||
from game.missiongenerator.forcedoptionsgenerator import ForcedOptionsGenerator
|
from game.missiongenerator.forcedoptionsgenerator import ForcedOptionsGenerator
|
||||||
from game.missiongenerator.frontlineconflictdescription import (
|
from game.missiongenerator.frontlineconflictdescription import (
|
||||||
FrontLineConflictDescription,
|
FrontLineConflictDescription,
|
||||||
)
|
)
|
||||||
from game.missiongenerator.missiondata import MissionData, JtacInfo
|
from game.missiongenerator.missiondata import JtacInfo
|
||||||
from game.missiongenerator.tgogenerator import TgoGenerator
|
from game.missiongenerator.tgogenerator import TgoGenerator
|
||||||
from game.missiongenerator.visualsgenerator import VisualsGenerator
|
from game.missiongenerator.visualsgenerator import VisualsGenerator
|
||||||
from game.naming import namegen
|
from game.naming import namegen
|
||||||
from game.persistency import pre_pretense_backups_dir
|
from game.persistency import pre_pretense_backups_dir
|
||||||
from game.pretense.pretenseaircraftgenerator import PretenseAircraftGenerator
|
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.theater.bullseye import Bullseye
|
||||||
from game.unitmap import UnitMap
|
from game.unitmap import UnitMap
|
||||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||||
@ -40,6 +37,7 @@ from .pretensetriggergenerator import PretenseTriggerGenerator
|
|||||||
from ..ato.airtaaskingorder import AirTaskingOrder
|
from ..ato.airtaaskingorder import AirTaskingOrder
|
||||||
from ..callsigns import callsign_for_support_unit
|
from ..callsigns import callsign_for_support_unit
|
||||||
from ..dcs.aircrafttype import AircraftType
|
from ..dcs.aircrafttype import AircraftType
|
||||||
|
from ..lasercodes import LaserCodeRegistry
|
||||||
from ..missiongenerator import MissionGenerator
|
from ..missiongenerator import MissionGenerator
|
||||||
from ..theater import Airfield
|
from ..theater import Airfield
|
||||||
|
|
||||||
@ -50,21 +48,8 @@ if TYPE_CHECKING:
|
|||||||
class PretenseMissionGenerator(MissionGenerator):
|
class PretenseMissionGenerator(MissionGenerator):
|
||||||
def __init__(self, game: Game, time: datetime) -> None:
|
def __init__(self, game: Game, time: datetime) -> None:
|
||||||
super().__init__(game, time)
|
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.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:
|
with open("resources/default_options.lua", "r", encoding="utf-8") as f:
|
||||||
options = dcs.lua.loads(f.read())["options"]
|
options = dcs.lua.loads(f.read())["options"]
|
||||||
@ -262,6 +247,7 @@ class PretenseMissionGenerator(MissionGenerator):
|
|||||||
self.time,
|
self.time,
|
||||||
self.radio_registry,
|
self.radio_registry,
|
||||||
self.tacan_registry,
|
self.tacan_registry,
|
||||||
|
self.datalink_registry,
|
||||||
self.laser_code_registry,
|
self.laser_code_registry,
|
||||||
self.unit_map,
|
self.unit_map,
|
||||||
mission_data=self.mission_data,
|
mission_data=self.mission_data,
|
||||||
|
|||||||
107
game/radio/datalink.py
Normal file
107
game/radio/datalink.py
Normal 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)
|
||||||
145
qt_ui/windows/mission/flight/payload/propertyeditbox.py
Normal file
145
qt_ui/windows/mission/flight/payload/propertyeditbox.py
Normal 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)
|
||||||
|
)
|
||||||
@ -11,6 +11,7 @@ from game.ato.flightmember import FlightMember
|
|||||||
from .missingpropertydataerror import MissingPropertyDataError
|
from .missingpropertydataerror import MissingPropertyDataError
|
||||||
from .propertycheckbox import PropertyCheckBox
|
from .propertycheckbox import PropertyCheckBox
|
||||||
from .propertycombobox import PropertyComboBox
|
from .propertycombobox import PropertyComboBox
|
||||||
|
from .propertyeditbox import PropertyEditBox
|
||||||
from .propertyspinbox import PropertySpinBox
|
from .propertyspinbox import PropertySpinBox
|
||||||
|
|
||||||
|
|
||||||
@ -22,6 +23,7 @@ class UnhandledControlTypeError(RuntimeError):
|
|||||||
class PropertyEditor(QGridLayout):
|
class PropertyEditor(QGridLayout):
|
||||||
def __init__(self, flight: Flight, flight_member: FlightMember) -> None:
|
def __init__(self, flight: Flight, flight_member: FlightMember) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
self.flight = flight
|
||||||
self.flight_member = flight_member
|
self.flight_member = flight_member
|
||||||
self.flight_member_update_listeners: list[Callable[[FlightMember], None]] = []
|
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]:
|
def control_for_property(self, prop: UnitPropertyDescription) -> Optional[QWidget]:
|
||||||
# Valid values are:
|
# Valid values are:
|
||||||
# "checkbox", "comboList", "groupbox", "label", "slider", "spinbox"
|
# "checkbox", "comboList", "groupbox", "label", "slider", "spinbox", "editbox"
|
||||||
match prop.control:
|
match prop.control:
|
||||||
case "checkbox":
|
case "checkbox":
|
||||||
widget = PropertyCheckBox(self.flight_member, prop)
|
widget = PropertyCheckBox(self.flight_member, prop)
|
||||||
@ -94,6 +96,10 @@ class PropertyEditor(QGridLayout):
|
|||||||
widget = PropertySpinBox(self.flight_member, prop)
|
widget = PropertySpinBox(self.flight_member, prop)
|
||||||
self.flight_member_update_listeners.append(widget.set_flight_member)
|
self.flight_member_update_listeners.append(widget.set_flight_member)
|
||||||
return widget
|
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 _:
|
case _:
|
||||||
raise UnhandledControlTypeError(prop.control)
|
raise UnhandledControlTypeError(prop.control)
|
||||||
|
|
||||||
|
|||||||
@ -33,7 +33,7 @@ pluggy==1.5.0
|
|||||||
pre-commit==3.7.1
|
pre-commit==3.7.1
|
||||||
pydantic==2.7.4
|
pydantic==2.7.4
|
||||||
pydantic-settings==2.3.3
|
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==5.13.2
|
||||||
pyinstaller-hooks-contrib==2024.0
|
pyinstaller-hooks-contrib==2024.0
|
||||||
pyparsing==3.1.2
|
pyparsing==3.1.2
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user