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
13 changed files with 415 additions and 36 deletions

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)