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]** 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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)] = []
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
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 .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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user