From b4e3067718e2fb77c64c6499e72b7268be376a22 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Wed, 2 Sep 2020 16:34:42 -0700 Subject: [PATCH 1/9] Update pydcs. --- pydcs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydcs b/pydcs index d278df68..5c02bf8e 160000 --- a/pydcs +++ b/pydcs @@ -1 +1 @@ -Subproject commit d278df68eee2f486f840c178e17893f58313efb8 +Subproject commit 5c02bf8ea5e3ec5afccc0135e31a3dd15e21342b From af596c58c32f24d16f95da6105d3a7879c5fbbc1 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sun, 30 Aug 2020 17:46:02 -0700 Subject: [PATCH 2/9] Control radio/TACAN allocation, set flight radios. Add central registries for allocating TACAN/radio channels to the Operation. These ensure that each channel is allocated uniquely, and removes the caller's need to think about which frequency to use. The registry allocates frequencies based on the radio it is given, which ensures that the allocated frequency will be compatible with the radio that needs it. A mapping from aircraft to the radio used by that aircraft for intra-flight comms (i.e. the F-16 uses the AN/ARC-222) exists for creating infra-flight channels appropriate for the aircraft. Inter-flight channels are allocated by a generic UHF radio. I've moved the inter-flight radio channels from the VHF to UHF range, since that's the most easily allocated band, and inter-flight will be in the highest demand. Intra-flight radios are now generally not shared. For aircraft where the radio type is not known we will still fall back to the shared channel, but that will stop being the case as we gain more data. Tankers have been moved to the Y TACAN band. Not completely needed, but seems typical for most missions and deconflicts the tankers from any unknown airfields (which always use the X band in DCS). --- game/operation/operation.py | 49 +- gen/aircraft.py | 112 ++++- gen/airfields.py | 907 ++++++++++++++++++------------------ gen/airsupportgen.py | 79 +++- gen/flights/flight.py | 18 +- gen/kneeboard.py | 47 +- gen/radios.py | 200 ++++++++ gen/tacan.py | 83 ++++ 8 files changed, 960 insertions(+), 535 deletions(-) create mode 100644 gen/radios.py create mode 100644 gen/tacan.py diff --git a/game/operation/operation.py b/game/operation/operation.py index 5f7a9503..09242628 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -4,8 +4,9 @@ from dcs.terrain import Terrain from gen import * from userdata.debriefing import * - -TANKER_CALLSIGNS = ["Texaco", "Arco", "Shell"] +from gen.airfields import AIRFIELD_DATA +from gen.radios import RadioRegistry +from gen.tacan import TacanRegistry class Operation: @@ -25,6 +26,8 @@ class Operation: groundobjectgen = None # type: GroundObjectsGenerator briefinggen = None # type: BriefingGenerator forcedoptionsgen = None # type: ForcedOptionsGenerator + radio_registry: Optional[RadioRegistry] = None + tacan_registry: Optional[TacanRegistry] = None environment_settings = None trigger_radius = TRIGGER_RADIUS_MEDIUM @@ -63,8 +66,14 @@ class Operation: def initialize(self, mission: Mission, conflict: Conflict): self.current_mission = mission self.conflict = conflict - self.airgen = AircraftConflictGenerator(mission, conflict, self.game.settings, self.game) - self.airsupportgen = AirSupportConflictGenerator(mission, conflict, self.game) + self.radio_registry = RadioRegistry() + self.tacan_registry = TacanRegistry() + self.airgen = AircraftConflictGenerator( + mission, conflict, self.game.settings, self.game, + self.radio_registry) + self.airsupportgen = AirSupportConflictGenerator( + mission, conflict, self.game, self.radio_registry, + self.tacan_registry) self.triggersgen = TriggersGenerator(mission, conflict, self.game) self.visualgen = VisualGenerator(mission, conflict, self.game) self.envgen = EnviromentGenerator(mission, conflict, self.game) @@ -120,6 +129,17 @@ class Operation: # Generate ground object first self.groundobjectgen.generate() + for airfield, data in AIRFIELD_DATA.items(): + if data.theater == self.game.theater.terrain.name: + self.radio_registry.reserve(data.atc.hf) + self.radio_registry.reserve(data.atc.vhf_fm) + self.radio_registry.reserve(data.atc.vhf_am) + self.radio_registry.reserve(data.atc.uhf) + for ils in data.ils.values(): + self.radio_registry.reserve(ils) + if data.tacan is not None: + self.tacan_registry.reserve(data.tacan) + # Generate destroyed units for d in self.game.get_destroyed_units(): try: @@ -224,21 +244,16 @@ class Operation: kneeboard_generator = KneeboardGenerator(self.current_mission, self.game) # Briefing Generation - for i, tanker_type in enumerate(self.airsupportgen.generated_tankers): - callsign = TANKER_CALLSIGNS[i] - tacan = f"{60 + i}X" - freq = f"{130 + i} MHz AM" - self.briefinggen.append_frequency(f"Tanker {callsign} ({tanker_type})", f"{tacan}/{freq}") - kneeboard_generator.add_tanker(callsign, tanker_type, freq, tacan) + for tanker in self.airsupportgen.air_support.tankers: + self.briefinggen.append_frequency( + f"Tanker {tanker.callsign} ({tanker.variant})", + f"{tanker.tacan}/{tanker.freq}") + kneeboard_generator.add_tanker(tanker) if self.is_awacs_enabled: - callsign = "AWACS" - freq = "233 MHz AM" - self.briefinggen.append_frequency(callsign, freq) - kneeboard_generator.add_awacs(callsign, freq) - - self.briefinggen.append_frequency("Flight", "251 MHz AM") - kneeboard_generator.add_comm("Flight", "251 MHz AM") + for awacs in self.airsupportgen.air_support.awacs: + self.briefinggen.append_frequency(awacs.callsign, awacs.freq) + kneeboard_generator.add_awacs(awacs) # Generate the briefing self.briefinggen.generate() diff --git a/gen/aircraft.py b/gen/aircraft.py index c5eb8ae8..aa5a62e4 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -1,14 +1,32 @@ -from dcs.action import ActivateGroup, AITaskPush, MessageToCoalition, MessageToAll -from dcs.condition import TimeAfter, CoalitionHasAirdrome, PartOfCoalitionInZone -from dcs.helicopters import UH_1H -from dcs.terrain.terrain import NoParkingSlotError -from dcs.triggers import TriggerOnce, Event +from dataclasses import dataclass +from typing import Dict from game.data.cap_capabilities_db import GUNFIGHTERS from game.settings import Settings from game.utils import nm_to_meter from gen.flights.ai_flight_planner import FlightPlanner from gen.flights.flight import Flight, FlightType, FlightWaypointType +from gen.radios import get_radio, MHz, Radio, RadioFrequency, RadioRegistry +from pydcs.dcs import helicopters +from pydcs.dcs.action import ActivateGroup, AITaskPush, MessageToAll +from pydcs.dcs.condition import TimeAfter, CoalitionHasAirdrome, PartOfCoalitionInZone +from pydcs.dcs.helicopters import helicopter_map, UH_1H +from pydcs.dcs.mission import Mission, StartType +from pydcs.dcs.planes import ( + Bf_109K_4, + FW_190A8, + FW_190D9, + I_16, + Ju_88A4, + P_47D_30, + P_51D, + P_51D_30_NA, + SpitfireLFMkIX, + SpitfireLFMkIXCW, +) +from pydcs.dcs.terrain.terrain import NoParkingSlotError +from pydcs.dcs.triggers import TriggerOnce, Event +from pydcs.dcs.unittype import UnitType from .conflictgen import * from .naming import * @@ -23,17 +41,83 @@ RTB_ALTITUDE = 800 RTB_DISTANCE = 5000 HELI_ALT = 500 +# Note that fallback radio channels will *not* be reserved. It's possible that +# flights using these will overlap with other channels. This is because we would +# need to make sure we fell back to a frequency that is not used by any beacon +# or ATC, which we don't have the information to predict. Deal with the minor +# annoyance for now since we'll be fleshing out radio info soon enough. +ALLIES_WW2_CHANNEL = MHz(124) +GERMAN_WW2_CHANNEL = MHz(40) +HELICOPTER_CHANNEL = MHz(127) +UHF_FALLBACK_CHANNEL = MHz(251) + + +@dataclass(frozen=True) +class AircraftData: + """Additional aircraft data not exposed by pydcs.""" + + #: The type of radio used for intra-flight communications. + intra_flight_radio: Radio + + +# Indexed by the id field of the pydcs PlaneType. +AIRCRAFT_DATA: Dict[str, AircraftData] = { + "A-10C": AircraftData(get_radio("AN/ARC-186(V) AM")), + "F-16C_50": AircraftData(get_radio("AN/ARC-222")), + "F/A-18C": AircraftData(get_radio("AN/ARC-210")), +} + + +# TODO: Get radio information for all the special cases. +def get_fallback_channel(unit_type: UnitType) -> RadioFrequency: + if unit_type in helicopter_map.values() and unit_type != UH_1H: + return HELICOPTER_CHANNEL + + german_ww2_aircraft = [ + Bf_109K_4, + FW_190A8, + FW_190D9, + Ju_88A4, + ] + + if unit_type in german_ww2_aircraft: + return GERMAN_WW2_CHANNEL + + allied_ww2_aircraft = [ + I_16, + P_47D_30, + P_51D, + P_51D_30_NA, + SpitfireLFMkIX, + SpitfireLFMkIXCW, + ] + + if unit_type in allied_ww2_aircraft: + return ALLIES_WW2_CHANNEL + + return UHF_FALLBACK_CHANNEL + class AircraftConflictGenerator: escort_targets = [] # type: typing.List[typing.Tuple[FlyingGroup, int]] - def __init__(self, mission: Mission, conflict: Conflict, settings: Settings, game): + def __init__(self, mission: Mission, conflict: Conflict, settings: Settings, + game, radio_registry: RadioRegistry): self.m = mission self.game = game self.settings = settings self.conflict = conflict + self.radio_registry = radio_registry self.escort_targets = [] + def get_intra_flight_channel(self, airframe: UnitType) -> RadioFrequency: + try: + aircraft_data = AIRCRAFT_DATA[airframe.id] + return self.radio_registry.alloc_for_radio( + aircraft_data.intra_flight_radio) + except KeyError: + return get_fallback_channel(airframe) + def _start_type(self) -> StartType: return self.settings.cold_start and StartType.Cold or StartType.Warm @@ -90,19 +174,9 @@ class AircraftConflictGenerator: group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)) - # TODO : refactor this following bad specific special case code :( - - if unit_type in helicopters.helicopter_map.values() and unit_type not in [UH_1H]: - group.set_frequency(127.5) - else: - if unit_type not in [P_51D_30_NA, P_51D, SpitfireLFMkIX, SpitfireLFMkIXCW, P_47D_30, I_16, FW_190A8, FW_190D9, Bf_109K_4]: - group.set_frequency(251.0) - else: - # WW2 - if unit_type in [FW_190A8, FW_190D9, Bf_109K_4, Ju_88A4]: - group.set_frequency(40) - else: - group.set_frequency(124.0) + channel = self.get_intra_flight_channel(unit_type) + group.set_frequency(channel.mhz) + flight.intra_flight_channel = channel # Special case so Su 33 carrier take off if unit_type is Su_33: diff --git a/gen/airfields.py b/gen/airfields.py index 2b51995a..38d4ccdb 100644 --- a/gen/airfields.py +++ b/gen/airfields.py @@ -4,10 +4,10 @@ Remove once https://github.com/pydcs/dcs/issues/69 tracks getting the missing data added to pydcs. Until then, missing data can be manually filled in here. """ from dataclasses import dataclass, field -from typing import Dict, List, Optional +from typing import Dict, Optional, Tuple - -RadioFrequency = str +from .radios import MHz, RadioFrequency +from .tacan import TacanBand, TacanChannel @dataclass @@ -21,6 +21,8 @@ class AtcData: @dataclass class AirfieldData: """Additional airfield data not included in pydcs.""" + #: Name of the theater the airport is in. + theater: str #: ICAO airport code icao: Optional[str] = None @@ -31,614 +33,615 @@ class AirfieldData: #: Runway length (in ft). runway_length: int = 0 - #: TACAN channel as a string, i.e. "74X". - tacan: Optional[str] = None + #: TACAN channel for the airfield. + tacan: Optional[TacanChannel] = None #: TACAN callsign tacan_callsign: Optional[str] = None - #: VOR channel as a string, i.e. "114.90 (MA)". - vor: Optional[str] = None + #: VOR as a tuple of (callsign, frequency). + vor: Optional[Tuple[str, RadioFrequency]] = None - #: RSBN channel as a string, i.e. "ch 28 (KW)". - rsbn: Optional[str] = None + #: RSBN channel as a tuple of (callsign, channel). + rsbn: Optional[Tuple[str, int]] = None - #: Radio channels used by the airfield's ATC. - atc: AtcData = AtcData("", "", "", "") + #: Radio channels used by the airfield's ATC. Note that not all airfields + #: have ATCs. + atc: Optional[AtcData] = None - #: Dict of runway heading -> ILS frequency. - ils: Dict[str, RadioFrequency] = field(default_factory=dict) + #: Dict of runway heading -> ILS tuple of (callsign, frequency). + ils: Dict[str, Tuple[str, RadioFrequency]] = field(default_factory=dict) - #: Dict of runway heading -> PRMG info, i.e "ch 26 (KW)" - prmg: Dict[str, str] = field(default_factory=dict) + #: Dict of runway heading -> PRMG tuple of (callsign, channel). + prmg: Dict[str, Tuple[str, int]] = field(default_factory=dict) - #: Dict of runway heading -> outer ndb, i.e "408.00 (KW)" - outer_ndb: Dict[str, str] = field(default_factory=dict) + #: Dict of runway heading -> outer NDB tuple of (callsign, frequency). + outer_ndb: Dict[str, Tuple[str, RadioFrequency]] = field(default_factory=dict) - #: Dict of runway heading -> inner ndb, i.e "803.00 (K) - inner_ndb: Dict[str, str] = field(default_factory=dict) + #: Dict of runway heading -> inner NDB tuple of (callsign, frequency). + inner_ndb: Dict[str, Tuple[str, RadioFrequency]] = field(default_factory=dict) def ils_freq(self, runway: str) -> Optional[RadioFrequency]: - return self.ils.get(runway) + ils = self.ils.get(runway) + if ils is not None: + return ils[1] + return None # TODO: Add more airfields. AIRFIELD_DATA = { + # Caucasus - # TODO : CAUCASUS MAP "Batumi": AirfieldData( - "UGSB", - 32, 6792, - "16X", "BTM", - "", "", - AtcData("4.250", "131.000", "40.400", "260.000"), - {"13": "110.30 (ILU)"}, - {}, - {}, - {} + theater="Caucasus", + icao="UGSB", + elevation=32, + runway_length=6792, + tacan=TacanChannel(16, TacanBand.X), + tacan_callsign="BTM", + atc=AtcData(MHz(4, 250), MHz(131, 0), MHz(40, 400), MHz(260, 0)), + ils={ + "13": ("ILU", MHz(110, 30)), + }, ), "Kobuleti": AirfieldData( - "UG5X", - 59, 7406, - "67X", "KBL", - "", "", - AtcData("4.350", "133.000", "40.800", "262.000"), - {"7": "111.50 (IKB)"}, - {}, - {"7": "870.00 (KT)"}, - {"7": "490.00 (T)"}, + theater="Caucasus", + icao="UG5X", + elevation=59, + runway_length=7406, + tacan=TacanChannel(67, TacanBand.X), + tacan_callsign="KBL", + atc=AtcData(MHz(4, 350), MHz(133, 0), MHz(40, 800), MHz(262, 0)), + ils={ + "7": ("IKB", MHz(111, 50)), + }, + outer_ndb={ + "7": ("KT", MHz(870, 0)), + }, + inner_ndb={ + "7": ("T", MHz(490, 0)), + }, ), "Senaki-Kolkhi": AirfieldData( - "UGKS", - 43, 7256, - "31X", "TSK", - "", "", - AtcData("4.300", "132.000", "40.600", "261.000"), - {"9": "108.90 (ITS)"}, - {}, - {"9": "335.00 (BI)"}, - {"9": "688.00 (I)"}, + theater="Caucasus", + icao="UGKS", + elevation=43, + runway_length=7256, + tacan=TacanChannel(31, TacanBand.X), + tacan_callsign="TSK", + atc=AtcData(MHz(4, 300), MHz(132, 0), MHz(40, 600), MHz(261, 0)), + ils={ + "9": ("ITS", MHz(108, 90)), + }, + outer_ndb={ + "9": ("BI", MHz(335, 0)), + }, + inner_ndb={ + "9": ("I", MHz(688, 0)), + }, ), "Kutaisi": AirfieldData( - "UGKO", - 147, 7937, - "44X", "KTS", - "113.60 (KT)", "", - AtcData("4.400", "134.000", "41.000", "263.000"), - {"8": "109.75 (IKS)"}, - {}, - {}, - {}, + theater="Caucasus", + icao="UGKO", + elevation=147, + runway_length=7937, + tacan=TacanChannel(44, TacanBand.X), + tacan_callsign="KTS", + atc=AtcData(MHz(4, 400), MHz(134, 0), MHz(41, 0), MHz(263, 0)), + ils={ + "8": ("IKS", MHz(109, 75)), + }, ), "Sukhumi-Babushara": AirfieldData( - "UGSS", - 43, 11217, - "", "", - "", "", - AtcData("4.150", "129.000", "40.000", "258.000"), - {}, - {}, - {"30": "489.00 (AV)"}, - {"30": "995.00 (A)"}, + theater="Caucasus", + icao="UGSS", + elevation=43, + runway_length=11217, + atc=AtcData(MHz(4, 150), MHz(129, 0), MHz(40, 0), MHz(258, 0)), + outer_ndb={ + "30": ("AV", MHz(489, 0)), + }, + inner_ndb={ + "30": ("A", MHz(995, 0)), + }, ), "Gudauta": AirfieldData( - "UG23", - 68, 7839, - "", "", - "", "", - AtcData("4.200", "120.000", "40.200", "259.000"), - {}, - {}, - {}, - {}, + theater="Caucasus", + icao="UG23", + elevation=68, + runway_length=7839, + atc=AtcData(MHz(4, 200), MHz(120, 0), MHz(40, 200), MHz(259, 0)), ), "Sochi-Adler": AirfieldData( - "URSS", - 98, 9686, - "", "", - "", "", - AtcData("4.050", "127.000", "39.600", "256.000"), - {"6": "111.10 (ISO)"}, - {}, - {}, - {}, + theater="Caucasus", + icao="URSS", + elevation=98, + runway_length=9686, + atc=AtcData(MHz(4, 50), MHz(127, 0), MHz(39, 600), MHz(256, 0)), + ils={ + "6": ("ISO", MHz(111, 10)), + }, ), "Gelendzhik": AirfieldData( - "URKG", - 72, 5452, - "", "", - "114.30 (GN)", "", - AtcData("4.000", "126.000", "39.400", "255.000"), - {}, - {}, - {}, - {}, + theater="Caucasus", + icao="URKG", + elevation=72, + runway_length=5452, + vor=("GN", MHz(114, 30)), + atc=AtcData(MHz(4, 0), MHz(126, 0), MHz(39, 400), MHz(255, 0)), ), "Novorossiysk": AirfieldData( - "URKN", - 131, 5639, - "", "", - "", "", - AtcData("3.850", "123.000", "38.800", "252.000"), - {}, - {}, - {}, - {}, + theater="Caucasus", + icao="URKN", + elevation=131, + runway_length=5639, + atc=AtcData(MHz(3, 850), MHz(123, 0), MHz(38, 800), MHz(252, 0)), ), "Anapa-Vityazevo": AirfieldData( - "URKA", - 141, 8623, - "", "", - "", "", - AtcData("3.750", "121.000", "38.400", "250.000"), - {}, - {}, - {"22": "443.00 (AP)", "4": "443.00 (AN)"}, - {"22": "215.00 (P)", "4": "215.00 (N)"}, + theater="Caucasus", + icao="URKA", + elevation=141, + runway_length=8623, + atc=AtcData(MHz(3, 750), MHz(121, 0), MHz(38, 400), MHz(250, 0)), + outer_ndb={ + "22": ("AP", MHz(443, 0)), "4": "443.00 (AN)" + }, + inner_ndb={ + "22": ("P", MHz(215, 0)), "4": "215.00 (N)" + }, ), "Krymsk": AirfieldData( - "URKW", - 65, 6733, - "", "", - "", "ch 28 (KW)", - AtcData("3.900", "124.000", "39.000", "253.000"), - {}, - {"4": "ch 26 (OX)", "22": "ch 26 (KW)"}, - {"4": "408.00 (OX)", "22": "408.00 (KW)"}, - {"4": "803.00 (O)", "22": "803.00 (K)"}, + theater="Caucasus", + icao="URKW", + elevation=65, + runway_length=6733, + rsbn=("KW", 28), + atc=AtcData(MHz(3, 900), MHz(124, 0), MHz(39, 0), MHz(253, 0)), + prmg={ + "4": ("OX", 26), + "22": ("KW", 26), + }, + outer_ndb={ + "4": ("OX", MHz(408, 0)), + "22": ("KW", MHz(408, 0)), + }, + inner_ndb={ + "4": ("O", MHz(803, 0)), + "22": ("K", MHz(803, 0)), + }, ), "Krasnodar-Center": AirfieldData( - "URKL", - 98, 7659, - "", "", - "", "ch 40 (MB)", - AtcData("3.800", "122.000", "38.600", "251.000"), - {}, - {"9": "ch 38 (MB)"}, - {"9": "625.00 (MB)", "27": "625.00 (OC)"}, - {"9": "303.00 (M)", "27": "303.00 (C)"}, + theater="Caucasus", + icao="URKL", + elevation=98, + runway_length=7659, + rsbn=("MB", 40), + atc=AtcData(MHz(3, 800), MHz(122, 0), MHz(38, 600), MHz(251, 0)), + prmg={ + "9": ("MB", 38), + }, + outer_ndb={ + "9": ("MB", MHz(625, 0)), + "27": ("OC", MHz(625, 0)), + }, + inner_ndb={ + "9": ("M", MHz(303, 0)), + "27": ("C", MHz(303, 0)), + }, ), "Krasnodar-Pashkovsky": AirfieldData( - "URKK", - 111, 9738, - "", "", - "115.80 (KN)", "", - AtcData("4.100", "128.000", "39.800", "257.000"), - {}, - {}, - {"23": "493.00 (LD)", "5": "493.00 (KR)"}, - {"23": "240.00 (L)", "5": "240.00 (K)"}, + theater="Caucasus", + icao="URKK", + elevation=111, + runway_length=9738, + vor=("KN", MHz(115, 80)), + atc=AtcData(MHz(4, 100), MHz(128, 0), MHz(39, 800), MHz(257, 0)), + outer_ndb={ + "23": ("LD", MHz(493, 0)), + "5": ("KR", MHz(493, 0)), + }, + inner_ndb={ + "23": ("L", MHz(240, 0)), + "5": ("K", MHz(240, 0)), + }, ), "Maykop-Khanskaya": AirfieldData( - "URKH", - 590, 10195, - "", "", - "", "ch 34 (DG)", - AtcData("3.950", "125.000", "39.200", "254.000"), - {}, - {"4": "ch 36 (DG)"}, - {"4": "289.00 (DG)", "22": "289.00 (RK)"}, - {"4": "591.00 (D)", "22": "591.00 (R)"}, + theater="Caucasus", + icao="URKH", + elevation=590, + runway_length=10195, + rsbn=("DG", 34), + atc=AtcData(MHz(3, 950), MHz(125, 0), MHz(39, 200), MHz(254, 0)), + prmg={ + "4": ("DG", 36), + }, + outer_ndb={ + "4": ("DG", MHz(289, 0)), + "22": ("RK", MHz(289, 0)), + }, + inner_ndb={ + "4": ("D", MHz(591, 0)), + "22": ("R", MHz(591, 0)), + }, ), "Mineralnye Vody": AirfieldData( - "URMM", - 1049, 12316, - "", "", - "117.10 (MN)", "", - AtcData("4.450", "135.000", "41.200", "264.000"), - {"30": "109.30 (IMW)", "12": "111.70 (IMD)"}, - {}, - {"30": "583.00 (NR)", "12": "583.00 (MD)"}, - {"30": "283.00 (N)", "12": "283.00 (D)"}, + theater="Caucasus", + icao="URMM", + elevation=1049, + runway_length=12316, + vor=("MN", MHz(117, 10)), + atc=AtcData(MHz(4, 450), MHz(135, 0), MHz(41, 200), MHz(264, 0)), + ils={ + "30": ("IMW", MHz(109, 30)), + "12": ("IMD", MHz(111, 70)), + }, + outer_ndb={ + "30": ("NR", MHz(583, 0)), + "12": ("MD", MHz(583, 0)), + }, + inner_ndb={ + "30": ("N", MHz(283, 0)), + "12": ("D", MHz(283, 0)), + }, ), "Nalchik": AirfieldData( - "URMN", - 1410, 7082, - "", "", - "", "", - AtcData("4.500", "136.000", "41.400", "265.000"), - {"24": "110.50 (INL)"}, - {}, - {"24": "718.00 (NL)"}, - {"24": "350.00 (N)"}, + theater="Caucasus", + icao="URMN", + elevation=1410, + runway_length=7082, + atc=AtcData(MHz(4, 500), MHz(136, 0), MHz(41, 400), MHz(265, 0)), + ils={ + "24": ("INL", MHz(110, 50)), + }, + outer_ndb={ + "24": ("NL", MHz(718, 0)), + }, + inner_ndb={ + "24": ("N", MHz(350, 0)), + }, ), "Mozdok": AirfieldData( - "XRMF", - 507, 7734, - "", "", - "", "ch 20 (MZ)", - AtcData("4.550", "137.000", "41.600", "266.000"), - {}, - {"26": "ch 22 (MZ)", "8": "ch 22 (MZ)"}, - {"26": "525.00 (RM)", "8": "525.00 (DO)"}, - {"26": "1.06 (R)", "8": "1.06 (D)"} + theater="Caucasus", + icao="XRMF", + elevation=507, + runway_length=7734, + rsbn=("MZ", 20), + atc=AtcData(MHz(4, 550), MHz(137, 0), MHz(41, 600), MHz(266, 0)), + prmg={ + "26": ("MZ", 22), + "8": ("MZ", 22), + }, + outer_ndb={ + "26": ("RM", MHz(525, 0)), + "8": ("DO", MHz(525, 0)), + }, + inner_ndb={ + "26": ("R", MHz(1, 6)), + "8": ("D", MHz(1, 6)), + } ), "Beslan": AirfieldData( - "URMO", - 1719, 9327, - "", "", - "", "", - AtcData("4.750", "141.000", "42.400", "270.000"), - {"10": "110.50 (ICH)"}, - {}, - {"10": "1.05 (CX)"}, - {"10": "250.00 (C)"} + theater="Caucasus", + icao="URMO", + elevation=1719, + runway_length=9327, + atc=AtcData(MHz(4, 750), MHz(141, 0), MHz(42, 400), MHz(270, 0)), + ils={ + "10": ("ICH", MHz(110, 50)), + }, + outer_ndb={ + "10": ("CX", MHz(1, 5)), + }, + inner_ndb={ + "10": ("C", MHz(250, 0)), + } ), "Tbilisi-Lochini": AirfieldData( - "UGTB", - 1573, 7692, - "25X", "GTB", - "113.70 (INA)", "", - AtcData("4.600", "138.000", "41.800", "267.000"), - {"13": "110.30 (INA)", "30": "108.90 (INA)"}, - {}, - {"13": "342.00 (BP)", "30": "211.00 (NA)"}, - {"13": "923.00 (B)", "30": "435.00 (N)"}, + theater="Caucasus", + icao="UGTB", + elevation=1573, + runway_length=7692, + tacan=TacanChannel(25, TacanBand.X), + tacan_callsign="GTB", + atc=AtcData(MHz(4, 600), MHz(138, 0), MHz(41, 800), MHz(267, 0)), + ils={ + "13": ("INA", MHz(110, 30)), + "30": ("INA", MHz(108, 90)), + }, + outer_ndb={ + "13": ("BP", MHz(342, 0)), + "30": ("NA", MHz(211, 0)), + }, + inner_ndb={ + "13": ("B", MHz(923, 0)), + "30": ("N", MHz(435, 0)), + }, ), "Soganlung": AirfieldData( - "UG24", - 1474, 7871, - "25X", "GTB", - "113.70 (INA)", "", - AtcData("4.650", "139.000", "42.000", "268.000"), - {}, - {}, - {}, - {}, + theater="Caucasus", + icao="UG24", + elevation=1474, + runway_length=7871, + tacan=TacanChannel(25, TacanBand.X), + tacan_callsign="GTB", + atc=AtcData(MHz(4, 650), MHz(139, 0), MHz(42, 0), MHz(268, 0)), ), "Vaziani": AirfieldData( - "UG27", - 1523, 7842, - "22X", "VAS", - "", "", - AtcData("4.700", "140.000", "42.200", "269.000"), - {"13": "108.75 (IVZ)", "31": "108.75 (IVZ)"}, - {}, - {}, - {}, + theater="Caucasus", + icao="UG27", + elevation=1523, + runway_length=7842, + tacan=TacanChannel(22, TacanBand.X), + tacan_callsign="VAS", + atc=AtcData(MHz(4, 700), MHz(140, 0), MHz(42, 200), MHz(269, 0)), + ils={ + "13": ("IVZ", MHz(108, 75)), + "31": ("IVZ", MHz(108, 75)), + }, ), # TODO : PERSIAN GULF MAP # TODO : SYRIA MAP - # "Incirlik": AirfieldData( - # AtcData("3.85", "38.6", "129.4", "360.1"), - # "21X", - # {"050": "109.3", "230": "111.7"} - # ), - # TODO : NEVADA MAP + + "Incirlik": AirfieldData( + theater="Syria", + icao="LTAG", + elevation=156, + runway_length=9662, + tacan=TacanChannel(21, TacanBand.X), + tacan_callsign="DAN", + vor=("DAN", MHz(108, 400)), + atc=AtcData(MHz(3, 850), MHz(38, 600), MHz(129, 400), MHz(360, 100)), + ils={ + "050": ("IDAN", MHz(109, 300)), + "230": ("DANM", MHz(111, 700)), + }, + ), + + # NTTR "Mina Airport 3Q0": AirfieldData( - "", - 4562, 4222, - "", "", - "", "", - AtcData("", "", "", ""), - {}, - {}, - {}, - {}, + theater="NTTR", + elevation=4562, + runway_length=4222, ), "Tonopah Airport": AirfieldData( - "KTPH", - 5394, 6715, - "", "", - "", "", - AtcData("", "", "", ""), - {}, - {}, - {}, - {}, + theater="NTTR", + icao="KTPH", + elevation=5394, + runway_length=6715, ), "Tonopah Test Range Airfield": AirfieldData( - "KTNX", - 5534, 11633, - "77X", "TQQ", - "113.00 (TQQ)", "", - AtcData("3.800", "124.750", "38.500", "257.950"), - {"32": "111.70 (I-UVV)", "14": "108.30 (I-RVP)"}, - {}, - {}, - {}, + theater="NTTR", + icao="KTNX", + elevation=5534, + runway_length=11633, + tacan=TacanChannel(77, TacanBand.X), + tacan_callsign="TQQ", + atc=AtcData(MHz(3, 800), MHz(124, 750), MHz(38, 500), MHz(257, 950)), + ils={ + "32": ("I-UVV", MHz(111, 70)), + "14": ("I-RVP", MHz(108, 30)), + }, ), "Beatty Airport": AirfieldData( - "KBTY", - 3173, 5380, - "", "", - "", "", - AtcData("", "", "", ""), - {}, - {}, - {}, - {}, + theater="NTTR", + icao="KBTY", + elevation=3173, + runway_length=5380, ), "Pahute Mesa Airstrip": AirfieldData( - "", - 5056, 5420, - "", "", - "", "", - AtcData("", "", "", ""), - {}, - {}, - {}, - {}, + theater="NTTR", + elevation=5056, + runway_length=5420, ), "Groom Lake AFB": AirfieldData( - "KXTA", - 4494, 11008, - "18X", "GRL", - "", "", - AtcData("3.850", "118.000", "38.600", "250.050"), - {"32": "109.30 (GLRI)"}, - {}, - {}, - {}, + theater="NTTR", + icao="KXTA", + elevation=4494, + runway_length=11008, + tacan=TacanChannel(18, TacanBand.X), + tacan_callsign="GRL", + atc=AtcData(MHz(3, 850), MHz(118, 0), MHz(38, 600), MHz(250, 50)), + ils={ + "32": ("GLRI", MHz(109, 30)), + }, ), "Lincoln County": AirfieldData( - "", - 4815, 4408, - "", "", - "", "", - AtcData("", "", "", ""), - {}, - {}, - {}, - {}, + theater="NTTR", + elevation=4815, + runway_length=4408, ), "Mesquite": AirfieldData( - "67L", - 1858, 4937, - "", "", - "", "", - AtcData("", "", "", ""), - {}, - {}, - {}, - {}, + theater="NTTR", + icao="67L", + elevation=1858, + runway_length=4937, ), "Creech AFB": AirfieldData( - "KINS", - 3126, 6100, - "87X", "INS", - "", "", - AtcData("3.825", "118.300", "38.550", "360.600"), - {"8": "108.70 (ICRR)"}, - {}, - {}, - {}, + theater="NTTR", + icao="KINS", + elevation=3126, + runway_length=6100, + tacan=TacanChannel(87, TacanBand.X), + tacan_callsign="INS", + atc=AtcData(MHz(3, 825), MHz(118, 300), MHz(38, 550), MHz(360, 600)), + ils={ + "8": ("ICRR", MHz(108, 70)), + }, ), "Echo Bay": AirfieldData( - "OL9", - 3126, 6100, - "87X", "INS", - "", "", - AtcData("3.825", "118.300", "38.550", "360.600"), - {}, - {}, - {}, - {}, + theater="NTTR", + icao="OL9", + elevation=3126, + runway_length=6100, + tacan=TacanChannel(87, TacanBand.X), + tacan_callsign="INS", + atc=AtcData(MHz(3, 825), MHz(118, 300), MHz(38, 550), MHz(360, 600)), ), "Nellis AFB": AirfieldData( - "KLSV", - 1841, 9454, - "12X", "LSV", - "", "", - AtcData("3.900", "132.550", "38.700", "327.000"), - {"21": "109.10 (IDIQ)"}, - {}, - {}, - {}, + theater="NTTR", + icao="KLSV", + elevation=1841, + runway_length=9454, + tacan=TacanChannel(12, TacanBand.X), + tacan_callsign="LSV", + atc=AtcData(MHz(3, 900), MHz(132, 550), MHz(38, 700), MHz(327, 0)), + ils={ + "21": ("IDIQ", MHz(109, 10)), + }, ), "North Las Vegas": AirfieldData( - "KVGT", - 2228, 4734, - "", "", - "", "", - AtcData("3.775", "125.700", "38.450", "360.750"), - {}, - {}, - {}, - {}, + theater="NTTR", + icao="KVGT", + elevation=2228, + runway_length=4734, + atc=AtcData(MHz(3, 775), MHz(125, 700), MHz(38, 450), MHz(360, 750)), ), "McCarran International Airport": AirfieldData( - "KLAS", - 2169, 10377, - "116X", "LAS", - "116.90 (LAS)", "", - AtcData("3.875", "119.900", "38.650", "257.800"), - {"25": "110.30 (I-LAS)"}, - {}, - {}, - {}, + theater="NTTR", + icao="KLAS", + elevation=2169, + runway_length=10377, + tacan=TacanChannel(116, TacanBand.X), + tacan_callsign="LAS", + atc=AtcData(MHz(3, 875), MHz(119, 900), MHz(38, 650), MHz(257, 800)), + ils={ + "25": ("I-LAS", MHz(110, 30)), + }, ), "Henderson Executive Airport": AirfieldData( - "KHND", - 2491, 5999, - "", "", - "", "", - AtcData("3.925", "125.100", "38.750", "250.100"), - {}, - {}, - {}, - {}, + theater="NTTR", + icao="KHND", + elevation=2491, + runway_length=5999, + atc=AtcData(MHz(3, 925), MHz(125, 100), MHz(38, 750), MHz(250, 100)), ), "Boulder City Airport": AirfieldData( - "KBVU", - 2121, 4612, - "", "", - "", "", - AtcData("", "", "", ""), - {}, - {}, - {}, - {}, + theater="NTTR", + icao="KBVU", + elevation=2121, + runway_length=4612, ), "Jean Airport": AirfieldData( - "", - 2824, 4053, - "", "", - "", "", - AtcData("", "", "", ""), - {}, - {}, - {}, - {}, + theater="NTTR", + elevation=2824, + runway_length=4053, ), "Laughlin Airport": AirfieldData( - "KIFP", - 656, 7139, - "", "", - "", "", - AtcData("3.750", "123.900", "38.400", "250.000"), - {}, - {}, - {}, - {}, + theater="NTTR", + icao="KIFP", + elevation=656, + runway_length=7139, + atc=AtcData(MHz(3, 750), MHz(123, 900), MHz(38, 400), MHz(250, 0)), ), # TODO : NORMANDY MAP # Channel Map "Detling": AirfieldData( - "", - 623, 2557, - "", "", - "", "", - AtcData("3.950", "118.400", "38.800", "250.400"), - {}, - {}, - {}, - {}, + theater="Channel", + elevation=623, + runway_length=2557, + atc=AtcData(MHz(3, 950), MHz(118, 400), MHz(38, 800), MHz(250, 400)), ), "High Halden": AirfieldData( - "", - 104, 3296, - "", "", - "", "", - AtcData("3.750", "118.800", "38.400", "250.000"), - {}, - {}, - {}, - {}, + theater="Channel", + elevation=104, + runway_length=3296, + atc=AtcData(MHz(3, 750), MHz(118, 800), MHz(38, 400), MHz(250, 0)), ), "Lympne": AirfieldData( - "", - 351, 2548, - "", "", - "", "", - AtcData("3.925", "118.350", "38.750", "250.350"), - {}, - {}, - {}, - {}, + theater="Channel", + elevation=351, + runway_length=2548, + atc=AtcData(MHz(3, 925), MHz(118, 350), MHz(38, 750), MHz(250, 350)), ), "Hawkinge": AirfieldData( - "", - 524, 3013, - "", "", - "", "", - AtcData("3.900", "118.300", "38.700", "250.300"), - {}, - {}, - {}, - {}, + theater="Channel", + elevation=524, + runway_length=3013, + atc=AtcData(MHz(3, 900), MHz(118, 300), MHz(38, 700), MHz(250, 300)), ), "Manston": AirfieldData( - "", - 160, 8626, - "", "", - "", "", - AtcData("3.875", "118.250", "38.650", "250.250"), - {}, - {}, - {}, - {}, + theater="Channel", + elevation=160, + runway_length=8626, + atc=AtcData(MHz(3, 875), MHz(118, 250), MHz(38, 650), MHz(250, 250)), ), "Dunkirk Mardyck": AirfieldData( - "", - 16, 1737, - "", "", - "", "", - AtcData("3.850", "118.200", "38.600", "250.200"), - {}, - {}, - {}, - {}, + theater="Channel", + elevation=16, + runway_length=1737, + atc=AtcData(MHz(3, 850), MHz(118, 200), MHz(38, 600), MHz(250, 200)), ), "Saint Omer Longuenesse": AirfieldData( - "", - 219, 1929, - "", "", - "", "", - AtcData("3.825", "118.150", "38.550" "250.150"), - {}, - {}, - {}, - {}, + theater="Channel", + elevation=219, + runway_length=1929, + atc=AtcData(MHz(3, 825), MHz(118, 150), MHz(38, 550), MHz(250, 150)), ), "Merville Calonne": AirfieldData( - "", - 52, 7580, - "", "", - "", "", - AtcData("3.800", "118.100", "38.500", "250.100"), - {}, - {}, - {}, - {}, + theater="Channel", + elevation=52, + runway_length=7580, + atc=AtcData(MHz(3, 800), MHz(118, 100), MHz(38, 500), MHz(250, 100)), ), "Abbeville Drucat": AirfieldData( - "", - 183, 4726, - "", "", - "", "", - AtcData("3.775", "118.050", "38.450", "250.050"), - {}, - {}, - {}, - {}, - ) - + theater="Channel", + elevation=183, + runway_length=4726, + atc=AtcData(MHz(3, 775), MHz(118, 50), MHz(38, 450), MHz(250, 50)), + ), } + +# TODO: Add list of all beacons on the map so we can reserve those frequencies. +# This list could be generated from the beasons.lua file in the terrain mod +# directory. As-is, we're allocating channels that might include VOR beacons, +# and those will broadcast their callsign consistently (probably with a lot of +# static, depending on how far away the beacon is. The F-16's VHF radio starts +# at 116 MHz, which happens to be the Damascus VOR beacon, so this is more or +# less guaranteed to happen. diff --git a/gen/airsupportgen.py b/gen/airsupportgen.py index 2e0ef249..e65b5a7a 100644 --- a/gen/airsupportgen.py +++ b/gen/airsupportgen.py @@ -1,12 +1,15 @@ -from game import db +from typing import List +from dataclasses import dataclass, field + from .conflictgen import * from .naming import * +from .radios import RadioFrequency, RadioRegistry +from .tacan import TacanBand, TacanChannel, TacanRegistry from dcs.mission import * from dcs.unitgroup import * from dcs.unittype import * from dcs.task import * -from dcs.terrain.terrain import NoParkingSlotError TANKER_DISTANCE = 15000 TANKER_ALT = 4572 @@ -15,15 +18,61 @@ TANKER_HEADING_OFFSET = 45 AWACS_DISTANCE = 150000 AWACS_ALT = 13000 +AWACS_CALLSIGNS = [ + "Overlord", + "Magic", + "Wizard", + "Focus", + "Darkstar", +] + + +@dataclass +class TankerCallsign: + full: str + short: str + + +TANKER_CALLSIGNS = [ + TankerCallsign("Texaco", "TEX"), + TankerCallsign("Arco", "ARC"), + TankerCallsign("Shell", "SHL"), +] + + +@dataclass +class AwacsInfo: + """AWACS information for the kneeboard.""" + callsign: str + freq: RadioFrequency + + +@dataclass +class TankerInfo: + """Tanker information for the kneeboard.""" + callsign: str + variant: str + freq: RadioFrequency + tacan: TacanChannel + + +@dataclass +class AirSupport: + awacs: List[AwacsInfo] = field(default_factory=list) + tankers: List[TankerInfo] = field(default_factory=list) + class AirSupportConflictGenerator: - generated_tankers = None # type: typing.List[str] - def __init__(self, mission: Mission, conflict: Conflict, game): + def __init__(self, mission: Mission, conflict: Conflict, game, + radio_registry: RadioRegistry, + tacan_registry: TacanRegistry) -> None: self.mission = mission self.conflict = conflict self.game = game - self.generated_tankers = [] + self.air_support = AirSupport() + self.radio_registry = radio_registry + self.tacan_registry = tacan_registry @classmethod def support_tasks(cls) -> typing.Collection[typing.Type[MainTask]]: @@ -32,9 +81,11 @@ class AirSupportConflictGenerator: def generate(self, is_awacs_enabled): player_cp = self.conflict.from_cp if self.conflict.from_cp.captured else self.conflict.to_cp - CALLSIGNS = ["TKR", "TEX", "FUL", "FUE", ""] for i, tanker_unit_type in enumerate(db.find_unittype(Refueling, self.conflict.attackers_side)): - self.generated_tankers.append(db.unit_type_name(tanker_unit_type)) + callsign = TANKER_CALLSIGNS[i] + variant = db.unit_type_name(tanker_unit_type) + freq = self.radio_registry.alloc_uhf() + tacan = self.tacan_registry.alloc_for_band(TacanBand.Y) tanker_heading = self.conflict.to_cp.position.heading_between_point(self.conflict.from_cp.position) + TANKER_HEADING_OFFSET * i tanker_position = player_cp.position.point_from_heading(tanker_heading, TANKER_DISTANCE) tanker_group = self.mission.refuel_flight( @@ -45,21 +96,26 @@ class AirSupportConflictGenerator: position=tanker_position, altitude=TANKER_ALT, race_distance=58000, - frequency=130 + i, + frequency=freq.mhz, start_type=StartType.Warm, speed=574, - tacanchannel="{}X".format(60 + i), + tacanchannel=str(tacan), ) if tanker_unit_type != IL_78M: tanker_group.points[0].tasks.pop() # Override PyDCS tacan channel - tanker_group.points[0].tasks.append(ActivateBeaconCommand(60 + i, "X", CALLSIGNS[i], True, tanker_group.units[0].id, True)) + tanker_group.points[0].tasks.append(ActivateBeaconCommand( + tacan.number, tacan.band.value, callsign.short, True, tanker_group.units[0].id, True)) tanker_group.points[0].tasks.append(SetInvisibleCommand(True)) tanker_group.points[0].tasks.append(SetImmortalCommand(True)) + self.air_support.tankers.append(TankerInfo(callsign.full, variant, freq, tacan)) + if is_awacs_enabled: try: + callsign = AWACS_CALLSIGNS[0] + freq = self.radio_registry.alloc_uhf() awacs_unit = db.find_unittype(AWACS, self.conflict.attackers_side)[0] awacs_flight = self.mission.awacs_flight( country=self.mission.country(self.game.player_country), @@ -68,11 +124,12 @@ class AirSupportConflictGenerator: altitude=AWACS_ALT, airport=None, position=self.conflict.position.random_point_within(AWACS_DISTANCE, AWACS_DISTANCE), - frequency=233, + frequency=freq.mhz, start_type=StartType.Warm, ) awacs_flight.points[0].tasks.append(SetInvisibleCommand(True)) awacs_flight.points[0].tasks.append(SetImmortalCommand(True)) + self.air_support.awacs.append(AwacsInfo(callsign, freq)) except: print("No AWACS for faction") diff --git a/gen/flights/flight.py b/gen/flights/flight.py index 57c7097f..912946e4 100644 --- a/gen/flights/flight.py +++ b/gen/flights/flight.py @@ -1,10 +1,10 @@ from enum import Enum -from typing import List +from typing import List, Optional -from dcs.mission import StartType -from dcs.unittype import UnitType +from pydcs.dcs.unittype import UnitType from game import db +from gen.radios import RadioFrequency class FlightType(Enum): @@ -96,6 +96,13 @@ class Flight: # How long before this flight should take off scheduled_in = 0 + # Populated during mission generation time by AircraftConflictGenerator. + # TODO: Decouple radio planning from the Flight. + # Make AircraftConflictGenerator generate a FlightData object that is + # returned to the Operation rather than relying on the Flight object, which + # represents a game UI flight rather than a fully planned flight. + intra_flight_channel: Optional[RadioFrequency] + def __init__(self, unit_type: UnitType, count: int, from_cp, flight_type: FlightType): self.unit_type = unit_type self.count = count @@ -105,6 +112,7 @@ class Flight: self.targets = [] self.loadout = {} self.start_type = "Runway" + self.intra_flight_channel = None def __repr__(self): return self.flight_type.name + " | " + str(self.count) + "x" + db.unit_type_name(self.unit_type) \ @@ -113,10 +121,10 @@ class Flight: # Test if __name__ == '__main__': - from dcs.planes import A_10C + from pydcs.dcs.planes import A_10C from theater import ControlPoint, Point, List from_cp = ControlPoint(0, "AA", Point(0, 0), None, [], 0, 0) - f = Flight(A_10C, 4, from_cp, FlightType.CAS) + f = Flight(A_10C(), 4, from_cp, FlightType.CAS) f.scheduled_in = 50 print(f) diff --git a/gen/kneeboard.py b/gen/kneeboard.py index 1d679e4a..1241078e 100644 --- a/gen/kneeboard.py +++ b/gen/kneeboard.py @@ -33,9 +33,11 @@ from tabulate import tabulate from pydcs.dcs.mission import Mission from pydcs.dcs.terrain.terrain import Airport from pydcs.dcs.unittype import FlyingType -from .airfields import AIRFIELD_DATA -from .flights.flight import Flight from . import units +from .airfields import AIRFIELD_DATA +from .airsupportgen import AwacsInfo, TankerInfo +from .flights.flight import Flight +from .radios import RadioFrequency class KneeboardPageWriter: @@ -116,23 +118,7 @@ class AirfieldInfo: class CommInfo: """Communications information for the kneeboard.""" name: str - freq: str - - -@dataclass -class AwacsInfo: - """AWACS information for the kneeboard.""" - callsign: str - freq: str - - -@dataclass -class TankerInfo: - """Tanker information for the kneeboard.""" - callsign: str - variant: str - freq: str - tacan: str + freq: RadioFrequency @dataclass @@ -153,6 +139,10 @@ class BriefingPage(KneeboardPage): self.awacs = awacs self.tankers = tankers self.jtacs = jtacs + if self.flight.intra_flight_channel is not None: + self.comms.append( + CommInfo("Flight", self.flight.intra_flight_channel) + ) self.departure = flight.from_cp.airport self.arrival = flight.from_cp.airport self.divert: Optional[Airport] = None @@ -245,7 +235,7 @@ class KneeboardGenerator: self.tankers: List[TankerInfo] = [] self.jtacs: List[JtacInfo] = [] - def add_comm(self, name: str, freq: str) -> None: + def add_comm(self, name: str, freq: RadioFrequency) -> None: """Adds communications info to the kneeboard. Args: @@ -254,26 +244,21 @@ class KneeboardGenerator: """ self.comms.append(CommInfo(name, freq)) - def add_awacs(self, callsign: str, freq: str) -> None: + def add_awacs(self, awacs: AwacsInfo) -> None: """Adds an AWACS/GCI to the kneeboard. Args: - callsign: Callsign of the AWACS/GCI. - freq: Radio frequency used by the AWACS/GCI. + awacs: AWACS information. """ - self.awacs.append(AwacsInfo(callsign, freq)) + self.awacs.append(awacs) - def add_tanker(self, callsign: str, variant: str, freq: str, - tacan: str) -> None: + def add_tanker(self, tanker: TankerInfo) -> None: """Adds a tanker to the kneeboard. Args: - callsign: Callsign of the tanker. - variant: Aircraft type. - freq: Radio frequency used by the tanker. - tacan: TACAN channel of the tanker. + tanker: Tanker information. """ - self.tankers.append(TankerInfo(callsign, variant, freq, tacan)) + self.tankers.append(tanker) def add_jtac(self, callsign: str, region: str, code: str) -> None: """Adds a JTAC to the kneeboard. diff --git a/gen/radios.py b/gen/radios.py new file mode 100644 index 00000000..2cfeac8a --- /dev/null +++ b/gen/radios.py @@ -0,0 +1,200 @@ +"""Radio frequency types and allocators.""" +import itertools +from dataclasses import dataclass +from typing import Dict, Iterator, List, Set + + +@dataclass(frozen=True) +class RadioFrequency: + """A radio frequency. + + Not currently concerned with tracking modulation, just the frequency. + """ + + #: The frequency in kilohertz. + hertz: int + + def __str__(self): + if self.hertz >= 1000000: + return self.format("MHz", 1000000) + return self.format("kHz", 1000) + + def format(self, units: str, divisor: int) -> str: + converted = self.hertz / divisor + if converted.is_integer(): + return f"{int(converted)} {units}" + return f"{converted:0.3f} {units}" + + @property + def mhz(self) -> float: + """Returns the frequency in megahertz. + + Returns: + The frequency in megahertz. + """ + return self.hertz / 1000000 + + +def MHz(num: int, khz: int = 0) -> RadioFrequency: + return RadioFrequency(num * 1000000 + khz * 1000) + + +def kHz(num: int) -> RadioFrequency: + return RadioFrequency(num * 1000) + + +@dataclass(frozen=True) +class Radio: + """A radio. + + Defines the minimum (inclusive) and maximum (exclusive) range of the radio. + """ + + #: The name of the radio. + name: str + + #: The minimum (inclusive) frequency tunable by this radio. + minimum: RadioFrequency + + #: The maximum (exclusive) frequency tunable by this radio. + maximum: RadioFrequency + + #: The spacing between adjacent frequencies. + step: RadioFrequency + + def __str__(self) -> str: + return self.name + + def range(self) -> Iterator[RadioFrequency]: + """Returns an iterator over the usable frequencies of this radio.""" + return (RadioFrequency(x) for x in range( + self.minimum.hertz, self.maximum.hertz, self.step.hertz + )) + + +class OutOfChannelsError(RuntimeError): + """Raised when all channels usable by this radio have been allocated.""" + + def __init__(self, radio: Radio) -> None: + super().__init__(f"No available channels for {radio}") + + +class ChannelInUseError(RuntimeError): + """Raised when attempting to reserve an in-use frequency.""" + + def __init__(self, frequency: RadioFrequency) -> None: + super().__init__(f"{frequency} is already in use") + + +# TODO: Figure out appropriate steps for each radio. These are just guesses. +#: List of all known radios used by aircraft in the game. +RADIOS: List[Radio] = [ + Radio("AN/ARC-164", MHz(225), MHz(400), step=MHz(1)), + Radio("AN/ARC-186(V) AM", MHz(116), MHz(152), step=MHz(1)), + Radio("AN/ARC-186(V) FM", MHz(30), MHz(76), step=MHz(1)), + # The AN/ARC-210 can also use [30, 88) and [108, 118), but the current + # implementation can't implement the gap and the radio can't transmit on the + # latter. There's still plenty of channels between 118 MHz and 400 MHz, so + # not worth worrying about. + Radio("AN/ARC-210", MHz(118), MHz(400), step=MHz(1)), + Radio("AN/ARC-222", MHz(116), MHz(174), step=MHz(1)), + Radio("SCR-522", MHz(100), MHz(156), step=MHz(1)), + Radio("A.R.I. 1063", MHz(100), MHz(156), step=MHz(1)), + Radio("BC-1206", kHz(200), kHz(400), step=kHz(10)), +] + + +def get_radio(name: str) -> Radio: + """Returns the radio with the given name. + + Args: + name: Name of the radio to return. + + Returns: + The radio matching name. + + Raises: + KeyError: No matching radio was found. + """ + for radio in RADIOS: + if radio.name == name: + return radio + raise KeyError + + +class RadioRegistry: + """Manages allocation of radio channels. + + There's some room for improvement here. We could prefer to allocate + frequencies that are available to the fewest number of radios first, so + radios with wide bands like the AN/ARC-210 don't exhaust all the channels + available to narrower radios like the AN/ARC-186(V). In practice there are + probably plenty of channels, so we can deal with that later if we need to. + + We could also allocate using a larger increment, returning to smaller + increments each time the range is exhausted. This would help with the + previous problem, as the AN/ARC-186(V) would still have plenty of 25 kHz + increment channels left after the AN/ARC-210 moved on to the higher + frequencies. This would also look a little nicer than having every flight + allocated in the 30 MHz range. + """ + + # Not a real radio, but useful for allocating a channel usable for + # inter-flight communications. + BLUFOR_UHF = Radio("BLUFOR UHF", MHz(225), MHz(400), step=MHz(1)) + + def __init__(self) -> None: + self.allocated_channels: Set[RadioFrequency] = set() + self.radio_allocators: Dict[Radio, Iterator[RadioFrequency]] = {} + + radios = itertools.chain(RADIOS, [self.BLUFOR_UHF]) + for radio in radios: + self.radio_allocators[radio] = radio.range() + + def alloc_for_radio(self, radio: Radio) -> RadioFrequency: + """Allocates a radio channel tunable by the given radio. + + Args: + radio: The radio to allocate a channel for. + + Returns: + A radio channel compatible with the given radio. + + Raises: + OutOfChannelsError: All channels compatible with the given radio are + already allocated. + """ + allocator = self.radio_allocators[radio] + try: + while (channel := next(allocator)) in self.allocated_channels: + pass + return channel + except StopIteration: + raise OutOfChannelsError(radio) + + def alloc_uhf(self) -> RadioFrequency: + """Allocates a UHF radio channel suitable for inter-flight comms. + + Returns: + A UHF radio channel suitable for inter-flight comms. + + Raises: + OutOfChannelsError: All channels compatible with the given radio are + already allocated. + """ + return self.alloc_for_radio(self.BLUFOR_UHF) + + def reserve(self, frequency: RadioFrequency) -> None: + """Reserves the given channel. + + Reserving a channel ensures that it will not be allocated in the future. + + Args: + frequency: The channel to reserve. + + Raises: + ChannelInUseError: The given frequency is already in use. + """ + if frequency in self.allocated_channels: + raise ChannelInUseError(frequency) + self.allocated_channels.add(frequency) diff --git a/gen/tacan.py b/gen/tacan.py new file mode 100644 index 00000000..5e43202a --- /dev/null +++ b/gen/tacan.py @@ -0,0 +1,83 @@ +"""TACAN channel handling.""" +from dataclasses import dataclass +from enum import Enum +from typing import Dict, Iterator, Set + + +class TacanBand(Enum): + X = "X" + Y = "Y" + + def range(self) -> Iterator["TacanChannel"]: + """Returns an iterator over the channels in this band.""" + return (TacanChannel(x, self) for x in range(1, 100)) + + +@dataclass(frozen=True) +class TacanChannel: + number: int + band: TacanBand + + def __str__(self) -> str: + return f"{self.number}{self.band.value}" + + +class OutOfTacanChannelsError(RuntimeError): + """Raised when all channels in this band have been allocated.""" + + def __init__(self, band: TacanBand) -> None: + super().__init__(f"No available channels in TACAN {band.value} band") + + +class TacanChannelInUseError(RuntimeError): + """Raised when attempting to reserve an in-use channel.""" + + def __init__(self, channel: TacanChannel) -> None: + super().__init__(f"{channel} is already in use") + + +class TacanRegistry: + """Manages allocation of TACAN channels.""" + + def __init__(self) -> None: + self.allocated_channels: Set[TacanChannel] = set() + self.band_allocators: Dict[TacanBand, Iterator[TacanChannel]] = {} + + for band in TacanBand: + self.band_allocators[band] = band.range() + + def alloc_for_band(self, band: TacanBand) -> TacanChannel: + """Allocates a TACAN channel in the given band. + + Args: + band: The TACAN band to allocate a channel for. + + Returns: + A TACAN channel in the given band. + + Raises: + OutOfChannelsError: All channels compatible with the given radio are + already allocated. + """ + allocator = self.band_allocators[band] + try: + while (channel := next(allocator)) in self.allocated_channels: + pass + return channel + except StopIteration: + raise OutOfTacanChannelsError(band) + + def reserve(self, channel: TacanChannel) -> None: + """Reserves the given channel. + + Reserving a channel ensures that it will not be allocated in the future. + + Args: + channel: The channel to reserve. + + Raises: + ChannelInUseError: The given frequency is already in use. + """ + if channel in self.allocated_channels: + raise TacanChannelInUseError(channel) + self.allocated_channels.add(channel) From d0518593712f229a0448bd9ed8240e87e3bc80a2 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Mon, 31 Aug 2020 14:21:00 -0700 Subject: [PATCH 3/9] Add beacon list importer. --- gen/airfields.py | 8 -- gen/beacons.py | 74 ++++++++++++ resources/tools/import_beacons.py | 183 ++++++++++++++++++++++++++++++ 3 files changed, 257 insertions(+), 8 deletions(-) create mode 100644 gen/beacons.py create mode 100644 resources/tools/import_beacons.py diff --git a/gen/airfields.py b/gen/airfields.py index 38d4ccdb..8b98668d 100644 --- a/gen/airfields.py +++ b/gen/airfields.py @@ -637,11 +637,3 @@ AIRFIELD_DATA = { atc=AtcData(MHz(3, 775), MHz(118, 50), MHz(38, 450), MHz(250, 50)), ), } - -# TODO: Add list of all beacons on the map so we can reserve those frequencies. -# This list could be generated from the beasons.lua file in the terrain mod -# directory. As-is, we're allocating channels that might include VOR beacons, -# and those will broadcast their callsign consistently (probably with a lot of -# static, depending on how far away the beacon is. The F-16's VHF radio starts -# at 116 MHz, which happens to be the Damascus VOR beacon, so this is more or -# less guaranteed to happen. diff --git a/gen/beacons.py b/gen/beacons.py new file mode 100644 index 00000000..b54eacb1 --- /dev/null +++ b/gen/beacons.py @@ -0,0 +1,74 @@ +from dataclasses import dataclass +from enum import auto, IntEnum +import json +from pathlib import Path +from typing import Iterable, Optional + +from gen.radios import RadioFrequency +from gen.tacan import TacanBand, TacanChannel + + +BEACONS_RESOURCE_PATH = Path("resources/dcs/beacons") + + +class BeaconType(IntEnum): + BEACON_TYPE_NULL = auto() + BEACON_TYPE_VOR = auto() + BEACON_TYPE_DME = auto() + BEACON_TYPE_VOR_DME = auto() + BEACON_TYPE_TACAN = auto() + BEACON_TYPE_VORTAC = auto() + BEACON_TYPE_RSBN = auto() + BEACON_TYPE_BROADCAST_STATION = auto() + + BEACON_TYPE_HOMER = auto() + BEACON_TYPE_AIRPORT_HOMER = auto() + BEACON_TYPE_AIRPORT_HOMER_WITH_MARKER = auto() + BEACON_TYPE_ILS_FAR_HOMER = auto() + BEACON_TYPE_ILS_NEAR_HOMER = auto() + + BEACON_TYPE_ILS_LOCALIZER = auto() + BEACON_TYPE_ILS_GLIDESLOPE = auto() + + BEACON_TYPE_PRMG_LOCALIZER = auto() + BEACON_TYPE_PRMG_GLIDESLOPE = auto() + + BEACON_TYPE_ICLS_LOCALIZER = auto() + BEACON_TYPE_ICLS_GLIDESLOPE = auto() + + BEACON_TYPE_NAUTICAL_HOMER = auto() + + +@dataclass(frozen=True) +class Beacon: + name: str + callsign: str + beacon_type: BeaconType + hertz: int + channel: Optional[int] + + @property + def frequency(self) -> RadioFrequency: + return RadioFrequency(self.hertz) + + @property + def is_tacan(self) -> bool: + return self.beacon_type in ( + BeaconType.BEACON_TYPE_VORTAC, + BeaconType.BEACON_TYPE_TACAN, + ) + + @property + def tacan_channel(self) -> TacanChannel: + assert self.is_tacan + assert self.channel is not None + return TacanChannel(self.channel, TacanBand.X) + + +def load_beacons_for_terrain(name: str) -> Iterable[Beacon]: + beacons_file = BEACONS_RESOURCE_PATH / f"{name.lower()}.json" + if not beacons_file.exists(): + raise RuntimeError(f"Beacon file {beacons_file.resolve()} is missing") + + for beacon in json.loads(beacons_file.read_text()): + yield Beacon(**beacon) diff --git a/resources/tools/import_beacons.py b/resources/tools/import_beacons.py new file mode 100644 index 00000000..9f3dd38e --- /dev/null +++ b/resources/tools/import_beacons.py @@ -0,0 +1,183 @@ +"""Generates resources/dcs/beacons.json from the DCS installation. + +DCS has a beacons.lua file for each terrain mod that includes information about +the radio beacons present on the map: + +beacons = { + { + display_name = _('INCIRLIC'); + beaconId = 'airfield16_0'; + type = BEACON_TYPE_VORTAC; + callsign = 'DAN'; + frequency = 108400000.000000; + channel = 21; + position = { 222639.437500, 73.699811, -33216.257813 }; + direction = 0.000000; + positionGeo = { latitude = 37.015611, longitude = 35.448194 }; + sceneObjects = {'t:124814096'}; + }; + ... +} + +""" +import argparse +from contextlib import contextmanager +import dataclasses +import gettext +import os +from pathlib import Path +import textwrap +from typing import Dict, Iterable, Union + +import lupa + +import game # Needed to resolve cyclic import, for some reason. +from gen.beacons import Beacon, BeaconType, BEACONS_RESOURCE_PATH + +THIS_DIR = Path(__file__).parent.resolve() +SRC_DIR = THIS_DIR.parents[1] +EXPORT_DIR = SRC_DIR / BEACONS_RESOURCE_PATH + + +@contextmanager +def cd(path: Path): + cwd = os.getcwd() + os.chdir(path) + try: + yield + finally: + os.chdir(cwd) + + +def convert_lua_frequency(raw: Union[float, int]) -> int: + if isinstance(raw, float): + if not raw.is_integer(): + # The values are in hertz, and everything should be a whole number. + raise ValueError(f"Unexpected non-integer frequency: {raw}") + return int(raw) + else: + return raw + + +def beacons_from_terrain(dcs_path: Path, path: Path) -> Iterable[Beacon]: + # TODO: Fix case-sensitive issues. + # The beacons.lua file differs by case in some terrains. Will need to be + # fixed if the tool is to be run on Linux, but presumably the server + # wouldn't be able to find these anyway. + beacons_lua = path / "beacons.lua" + with cd(dcs_path): + lua = lupa.LuaRuntime() + + lua.execute(textwrap.dedent("""\ + function module(name) + end + + """)) + + bind_gettext = lua.eval(textwrap.dedent("""\ + function(py_gettext) + package.preload["i_18n"] = function() + return { + translate = py_gettext + } + end + end + + """)) + translator = gettext.translation( + "messages", path / "l10n", languages=["en"]) + + def translate(message_name: str) -> str: + if not message_name: + return message_name + return translator.gettext(message_name) + bind_gettext(translate) + + src = beacons_lua.read_text() + lua.execute(src) + + beacon_types_map: Dict[int, BeaconType] = {} + for beacon_type in BeaconType: + beacon_value = lua.eval(beacon_type.name) + beacon_types_map[beacon_value] = beacon_type + + beacons = lua.eval("beacons") + for beacon in beacons.values(): + beacon_type_lua = beacon["type"] + if beacon_type_lua not in beacon_types_map: + raise KeyError( + f"Unknown beacon type {beacon_type_lua}. Check that all " + f"beacon types in {beacon_types_path} are present in " + f"{BeaconType.__class__.__name__}" + ) + beacon_type = beacon_types_map[beacon_type_lua] + + yield Beacon( + beacon["display_name"], + beacon["callsign"], + beacon_type, + convert_lua_frequency(beacon["frequency"]), + getattr(beacon, "channel", None) + ) + + +class Importer: + """Imports beacon definitions from each available terrain mod. + + Only beacons for maps owned by the user can be imported. Other maps that + have been previously imported will not be disturbed. + """ + + def __init__(self, dcs_path: Path, export_dir: Path) -> None: + self.dcs_path = dcs_path + self.export_dir = export_dir + + def run(self) -> None: + """Exports the beacons for each available terrain mod.""" + terrains_path = self.dcs_path / "Mods" / "terrains" + self.export_dir.mkdir(parents=True, exist_ok=True) + for terrain in terrains_path.iterdir(): + beacons = beacons_from_terrain(self.dcs_path, terrain) + self.export_beacons(terrain.name, beacons) + + def export_beacons(self, terrain: str, beacons: Iterable[Beacon]) -> None: + terrain_py_path = self.export_dir / f"{terrain.lower()}.json" + import json + terrain_py_path.write_text(json.dumps([ + dataclasses.asdict(b) for b in beacons + ], indent=True)) + + + +def parse_args() -> argparse.Namespace: + """Parses and returns command line arguments.""" + parser = argparse.ArgumentParser() + + def resolved_path(val: str) -> Path: + """Returns the given string as a fully resolved Path.""" + return Path(val).resolve() + + parser.add_argument( + "--export-to", + type=resolved_path, + default=EXPORT_DIR, + help="Output directory for generated JSON files.") + + parser.add_argument( + "dcs_path", + metavar="DCS_PATH", + type=resolved_path, + help="Path to DCS installation." + ) + + return parser.parse_args() + + +def main() -> None: + """Program entry point.""" + args = parse_args() + Importer(args.dcs_path, args.export_to).run() + + +if __name__ == "__main__": + main() From 95b9a3e1aafe1813ff57672f2f97f9445a04ce47 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Mon, 31 Aug 2020 15:19:55 -0700 Subject: [PATCH 4/9] Check in beacon lists for most maps. --- resources/dcs/beacons/caucasus.json | 1157 ++++++++++++++++++++++++ resources/dcs/beacons/nevada.json | 317 +++++++ resources/dcs/beacons/normandy.json | 1 + resources/dcs/beacons/persiangulf.json | 709 +++++++++++++++ resources/dcs/beacons/syria.json | 408 +++++++++ 5 files changed, 2592 insertions(+) create mode 100644 resources/dcs/beacons/caucasus.json create mode 100644 resources/dcs/beacons/nevada.json create mode 100644 resources/dcs/beacons/normandy.json create mode 100644 resources/dcs/beacons/persiangulf.json create mode 100644 resources/dcs/beacons/syria.json diff --git a/resources/dcs/beacons/caucasus.json b/resources/dcs/beacons/caucasus.json new file mode 100644 index 00000000..d84fefc0 --- /dev/null +++ b/resources/dcs/beacons/caucasus.json @@ -0,0 +1,1157 @@ +[ + { + "name": "", + "callsign": "AP", + "beacon_type": 12, + "hertz": 443000, + "channel": null + }, + { + "name": "", + "callsign": "P", + "beacon_type": 13, + "hertz": 215000, + "channel": null + }, + { + "name": "", + "callsign": "AN", + "beacon_type": 12, + "hertz": 443000, + "channel": null + }, + { + "name": "", + "callsign": "N", + "beacon_type": 13, + "hertz": 215000, + "channel": null + }, + { + "name": "", + "callsign": "ILU", + "beacon_type": 14, + "hertz": 110300000, + "channel": null + }, + { + "name": "", + "callsign": "", + "beacon_type": 15, + "hertz": 110300000, + "channel": null + }, + { + "name": "", + "callsign": "BTM", + "beacon_type": 5, + "hertz": 977000000, + "channel": 16 + }, + { + "name": "", + "callsign": "LU", + "beacon_type": 10, + "hertz": 430000, + "channel": null + }, + { + "name": "", + "callsign": "CX", + "beacon_type": 12, + "hertz": 1050000, + "channel": null + }, + { + "name": "", + "callsign": "C", + "beacon_type": 13, + "hertz": 250000, + "channel": null + }, + { + "name": "", + "callsign": "ICH", + "beacon_type": 14, + "hertz": 110500000, + "channel": null + }, + { + "name": "", + "callsign": "", + "beacon_type": 15, + "hertz": 110500000, + "channel": null + }, + { + "name": "", + "callsign": "GN", + "beacon_type": 10, + "hertz": 1000000, + "channel": null + }, + { + "name": "", + "callsign": "GN", + "beacon_type": 2, + "hertz": 114300000, + "channel": 90 + }, + { + "name": "", + "callsign": "XC", + "beacon_type": 11, + "hertz": 395000, + "channel": null + }, + { + "name": "", + "callsign": "KT", + "beacon_type": 12, + "hertz": 870000, + "channel": null + }, + { + "name": "", + "callsign": "T", + "beacon_type": 13, + "hertz": 490000, + "channel": null + }, + { + "name": "", + "callsign": "IKB", + "beacon_type": 14, + "hertz": 111500000, + "channel": null + }, + { + "name": "", + "callsign": "", + "beacon_type": 15, + "hertz": 111500000, + "channel": null + }, + { + "name": "", + "callsign": "KBL", + "beacon_type": 5, + "hertz": 1154000000, + "channel": 67 + }, + { + "name": "", + "callsign": "OC", + "beacon_type": 12, + "hertz": 625000, + "channel": null + }, + { + "name": "", + "callsign": "O", + "beacon_type": 13, + "hertz": 303000, + "channel": null + }, + { + "name": "", + "callsign": "MB", + "beacon_type": 12, + "hertz": 625000, + "channel": null + }, + { + "name": "", + "callsign": "M", + "beacon_type": 13, + "hertz": 303000, + "channel": null + }, + { + "name": "", + "callsign": "MB", + "beacon_type": 16, + "hertz": 838000000, + "channel": 38 + }, + { + "name": "", + "callsign": "MB", + "beacon_type": 17, + "hertz": 838000000, + "channel": 38 + }, + { + "name": "", + "callsign": "MB", + "beacon_type": 7, + "hertz": 840000000, + "channel": 40 + }, + { + "name": "", + "callsign": "KR", + "beacon_type": 12, + "hertz": 493000, + "channel": null + }, + { + "name": "", + "callsign": "K", + "beacon_type": 13, + "hertz": 240000, + "channel": null + }, + { + "name": "", + "callsign": "LD", + "beacon_type": 12, + "hertz": 493000, + "channel": null + }, + { + "name": "", + "callsign": "L", + "beacon_type": 13, + "hertz": 240000, + "channel": null + }, + { + "name": "", + "callsign": "KN", + "beacon_type": 2, + "hertz": 115800000, + "channel": 105 + }, + { + "name": "", + "callsign": "KW", + "beacon_type": 12, + "hertz": 408000, + "channel": null + }, + { + "name": "", + "callsign": "K", + "beacon_type": 13, + "hertz": 803000, + "channel": null + }, + { + "name": "", + "callsign": "OX", + "beacon_type": 12, + "hertz": 408000, + "channel": null + }, + { + "name": "", + "callsign": "O", + "beacon_type": 13, + "hertz": 803000, + "channel": null + }, + { + "name": "", + "callsign": "", + "beacon_type": 17, + "hertz": 826000000, + "channel": null + }, + { + "name": "", + "callsign": "", + "beacon_type": 17, + "hertz": 826000000, + "channel": null + }, + { + "name": "", + "callsign": "KW", + "beacon_type": 16, + "hertz": 826000000, + "channel": 26 + }, + { + "name": "", + "callsign": "OX", + "beacon_type": 16, + "hertz": 826000000, + "channel": 26 + }, + { + "name": "", + "callsign": "KW", + "beacon_type": 7, + "hertz": 828000000, + "channel": 28 + }, + { + "name": "", + "callsign": "IKS", + "beacon_type": 14, + "hertz": 109750000, + "channel": null + }, + { + "name": "", + "callsign": "", + "beacon_type": 15, + "hertz": 109750000, + "channel": null + }, + { + "name": "", + "callsign": "TI", + "beacon_type": 11, + "hertz": 477000, + "channel": null + }, + { + "name": "", + "callsign": "KTS", + "beacon_type": 5, + "hertz": 1005000000, + "channel": 44 + }, + { + "name": "KUTAISI", + "callsign": "KT", + "beacon_type": 2, + "hertz": 113600000, + "channel": 83 + }, + { + "name": "", + "callsign": "BP", + "beacon_type": 12, + "hertz": 342000, + "channel": null + }, + { + "name": "", + "callsign": "B", + "beacon_type": 13, + "hertz": 923000, + "channel": null + }, + { + "name": "", + "callsign": "INA", + "beacon_type": 14, + "hertz": 110300000, + "channel": null + }, + { + "name": "", + "callsign": "", + "beacon_type": 15, + "hertz": 110300000, + "channel": null + }, + { + "name": "", + "callsign": "NA", + "beacon_type": 12, + "hertz": 211000, + "channel": null + }, + { + "name": "", + "callsign": "N", + "beacon_type": 13, + "hertz": 435000, + "channel": null + }, + { + "name": "", + "callsign": "INA", + "beacon_type": 14, + "hertz": 108900000, + "channel": null + }, + { + "name": "", + "callsign": "", + "beacon_type": 15, + "hertz": 108900000, + "channel": null + }, + { + "name": "", + "callsign": "TB", + "beacon_type": 2, + "hertz": 113700000, + "channel": 84 + }, + { + "name": "", + "callsign": "GTB", + "beacon_type": 5, + "hertz": 986000000, + "channel": 25 + }, + { + "name": "", + "callsign": "RK", + "beacon_type": 12, + "hertz": 289000, + "channel": null + }, + { + "name": "", + "callsign": "R", + "beacon_type": 13, + "hertz": 591000, + "channel": null + }, + { + "name": "", + "callsign": "DG", + "beacon_type": 12, + "hertz": 289000, + "channel": null + }, + { + "name": "", + "callsign": "D", + "beacon_type": 13, + "hertz": 591000, + "channel": null + }, + { + "name": "", + "callsign": "DG", + "beacon_type": 16, + "hertz": 836000000, + "channel": 36 + }, + { + "name": "", + "callsign": "DG", + "beacon_type": 17, + "hertz": 836000000, + "channel": 36 + }, + { + "name": "", + "callsign": "DG", + "beacon_type": 7, + "hertz": 834000000, + "channel": 34 + }, + { + "name": "", + "callsign": "NR", + "beacon_type": 12, + "hertz": 583000, + "channel": null + }, + { + "name": "", + "callsign": "N", + "beacon_type": 13, + "hertz": 283000, + "channel": null + }, + { + "name": "", + "callsign": "IMW", + "beacon_type": 14, + "hertz": 109300000, + "channel": null + }, + { + "name": "", + "callsign": "", + "beacon_type": 15, + "hertz": 109300000, + "channel": null + }, + { + "name": "", + "callsign": "MD", + "beacon_type": 12, + "hertz": 583000, + "channel": null + }, + { + "name": "", + "callsign": "D", + "beacon_type": 13, + "hertz": 283000, + "channel": null + }, + { + "name": "", + "callsign": "IMD", + "beacon_type": 14, + "hertz": 111700000, + "channel": null + }, + { + "name": "", + "callsign": "", + "beacon_type": 15, + "hertz": 111700000, + "channel": null + }, + { + "name": "", + "callsign": "MN", + "beacon_type": 2, + "hertz": 117100000, + "channel": 118 + }, + { + "name": "", + "callsign": "DO", + "beacon_type": 12, + "hertz": 525000, + "channel": null + }, + { + "name": "", + "callsign": "D", + "beacon_type": 13, + "hertz": 1065000, + "channel": null + }, + { + "name": "", + "callsign": "RM", + "beacon_type": 12, + "hertz": 525000, + "channel": null + }, + { + "name": "", + "callsign": "R", + "beacon_type": 13, + "hertz": 1065000, + "channel": null + }, + { + "name": "", + "callsign": "MZ", + "beacon_type": 16, + "hertz": 822000000, + "channel": 22 + }, + { + "name": "", + "callsign": "", + "beacon_type": 17, + "hertz": 822000000, + "channel": 22 + }, + { + "name": "", + "callsign": "MZ", + "beacon_type": 16, + "hertz": 822000000, + "channel": 22 + }, + { + "name": "", + "callsign": "", + "beacon_type": 17, + "hertz": 822000000, + "channel": 22 + }, + { + "name": "", + "callsign": "MZ", + "beacon_type": 7, + "hertz": 820000000, + "channel": 20 + }, + { + "name": "", + "callsign": "NL", + "beacon_type": 12, + "hertz": 718000, + "channel": null + }, + { + "name": "", + "callsign": "N", + "beacon_type": 13, + "hertz": 350000, + "channel": null + }, + { + "name": "", + "callsign": "INL", + "beacon_type": 14, + "hertz": 110500000, + "channel": null + }, + { + "name": "", + "callsign": "", + "beacon_type": 15, + "hertz": 110500000, + "channel": null + }, + { + "name": "", + "callsign": "BI", + "beacon_type": 12, + "hertz": 335000, + "channel": null + }, + { + "name": "", + "callsign": "B", + "beacon_type": 13, + "hertz": 688000, + "channel": null + }, + { + "name": "", + "callsign": "ITS", + "beacon_type": 14, + "hertz": 108900000, + "channel": null + }, + { + "name": "", + "callsign": "", + "beacon_type": 15, + "hertz": 108900000, + "channel": null + }, + { + "name": "", + "callsign": "TSK", + "beacon_type": 5, + "hertz": 992000000, + "channel": 31 + }, + { + "name": "", + "callsign": "CO", + "beacon_type": 11, + "hertz": 761000, + "channel": null + }, + { + "name": "", + "callsign": "ISO", + "beacon_type": 14, + "hertz": 111100000, + "channel": null + }, + { + "name": "", + "callsign": "", + "beacon_type": 15, + "hertz": 111100000, + "channel": null + }, + { + "name": "", + "callsign": "AV", + "beacon_type": 12, + "hertz": 489000, + "channel": null + }, + { + "name": "", + "callsign": "A", + "beacon_type": 13, + "hertz": 995000, + "channel": null + }, + { + "name": "", + "callsign": "IVZ", + "beacon_type": 14, + "hertz": 108750000, + "channel": null + }, + { + "name": "", + "callsign": "", + "beacon_type": 15, + "hertz": 108750000, + "channel": null + }, + { + "name": "", + "callsign": "IVZ", + "beacon_type": 14, + "hertz": 108750000, + "channel": null + }, + { + "name": "", + "callsign": "", + "beacon_type": 15, + "hertz": 108750000, + "channel": null + }, + { + "name": "", + "callsign": "VAS", + "beacon_type": 5, + "hertz": 983000000, + "channel": 22 + }, + { + "name": "", + "callsign": "NZ", + "beacon_type": 9, + "hertz": 330000, + "channel": null + }, + { + "name": "", + "callsign": "AR", + "beacon_type": 9, + "hertz": 440000, + "channel": null + }, + { + "name": "", + "callsign": "DM", + "beacon_type": 9, + "hertz": 690000, + "channel": null + }, + { + "name": "", + "callsign": "AG", + "beacon_type": 9, + "hertz": 381000, + "channel": null + }, + { + "name": "", + "callsign": "MA", + "beacon_type": 9, + "hertz": 682000, + "channel": null + }, + { + "name": "", + "callsign": "HS", + "beacon_type": 9, + "hertz": 1065000, + "channel": null + }, + { + "name": "", + "callsign": "SM", + "beacon_type": 9, + "hertz": 662000, + "channel": null + }, + { + "name": "", + "callsign": "KW", + "beacon_type": 9, + "hertz": 995000, + "channel": null + }, + { + "name": "", + "callsign": "TC", + "beacon_type": 9, + "hertz": 470000, + "channel": null + }, + { + "name": "", + "callsign": "IL", + "beacon_type": 9, + "hertz": 300500, + "channel": null + }, + { + "name": "", + "callsign": "SH", + "beacon_type": 9, + "hertz": 389000, + "channel": null + }, + { + "name": "", + "callsign": "OD", + "beacon_type": 9, + "hertz": 348000, + "channel": null + }, + { + "name": "", + "callsign": "BS", + "beacon_type": 9, + "hertz": 300500, + "channel": null + }, + { + "name": "", + "callsign": "KT", + "beacon_type": 9, + "hertz": 730000, + "channel": null + }, + { + "name": "", + "callsign": "ER", + "beacon_type": 9, + "hertz": 435000, + "channel": null + }, + { + "name": "", + "callsign": "KM", + "beacon_type": 9, + "hertz": 950000, + "channel": null + }, + { + "name": "", + "callsign": "SK", + "beacon_type": 9, + "hertz": 680000, + "channel": null + }, + { + "name": "", + "callsign": "DA", + "beacon_type": 9, + "hertz": 525000, + "channel": null + }, + { + "name": "", + "callsign": "DF", + "beacon_type": 9, + "hertz": 520000, + "channel": null + }, + { + "name": "", + "callsign": "RF", + "beacon_type": 9, + "hertz": 324000, + "channel": null + }, + { + "name": "", + "callsign": "TP", + "beacon_type": 9, + "hertz": 1182000, + "channel": null + }, + { + "name": "", + "callsign": "BJ", + "beacon_type": 9, + "hertz": 735000, + "channel": null + }, + { + "name": "", + "callsign": "NK", + "beacon_type": 9, + "hertz": 1030000, + "channel": null + }, + { + "name": "", + "callsign": "MN", + "beacon_type": 9, + "hertz": 705000, + "channel": null + }, + { + "name": "", + "callsign": "KC", + "beacon_type": 9, + "hertz": 1050000, + "channel": null + }, + { + "name": "", + "callsign": "TY", + "beacon_type": 9, + "hertz": 720000, + "channel": null + }, + { + "name": "", + "callsign": "AL", + "beacon_type": 9, + "hertz": 353000, + "channel": null + }, + { + "name": "", + "callsign": "CA", + "beacon_type": 9, + "hertz": 311000, + "channel": null + }, + { + "name": "", + "callsign": "XT", + "beacon_type": 9, + "hertz": 312000, + "channel": null + }, + { + "name": "", + "callsign": "KH", + "beacon_type": 9, + "hertz": 485000, + "channel": null + }, + { + "name": "", + "callsign": "WS", + "beacon_type": 9, + "hertz": 641000, + "channel": null + }, + { + "name": "", + "callsign": "WR", + "beacon_type": 9, + "hertz": 309500, + "channel": null + }, + { + "name": "", + "callsign": "VM", + "beacon_type": 9, + "hertz": 740000, + "channel": null + }, + { + "name": "", + "callsign": "WK", + "beacon_type": 9, + "hertz": 830000, + "channel": null + }, + { + "name": "", + "callsign": "TH", + "beacon_type": 9, + "hertz": 515000, + "channel": null + }, + { + "name": "", + "callsign": "KC", + "beacon_type": 9, + "hertz": 580000, + "channel": null + }, + { + "name": "", + "callsign": "SN", + "beacon_type": 9, + "hertz": 866000, + "channel": null + }, + { + "name": "", + "callsign": "DW", + "beacon_type": 9, + "hertz": 625000, + "channel": null + }, + { + "name": "", + "callsign": "SR", + "beacon_type": 9, + "hertz": 907000, + "channel": null + }, + { + "name": "", + "callsign": "TD", + "beacon_type": 9, + "hertz": 309500, + "channel": null + }, + { + "name": "", + "callsign": "SH", + "beacon_type": 9, + "hertz": 862000, + "channel": null + }, + { + "name": "", + "callsign": "SH", + "beacon_type": 9, + "hertz": 396000, + "channel": null + }, + { + "name": "", + "callsign": "DV", + "beacon_type": 9, + "hertz": 420000, + "channel": null + }, + { + "name": "", + "callsign": "GE", + "beacon_type": 9, + "hertz": 300500, + "channel": null + }, + { + "name": "", + "callsign": "GW", + "beacon_type": 9, + "hertz": 920000, + "channel": null + }, + { + "name": "", + "callsign": "QG", + "beacon_type": 9, + "hertz": 435000, + "channel": null + }, + { + "name": "", + "callsign": "AL", + "beacon_type": 9, + "hertz": 384000, + "channel": null + }, + { + "name": "", + "callsign": "DO", + "beacon_type": 9, + "hertz": 1175000, + "channel": null + }, + { + "name": "", + "callsign": "ND", + "beacon_type": 9, + "hertz": 507000, + "channel": null + }, + { + "name": "", + "callsign": "PR", + "beacon_type": 9, + "hertz": 1210000, + "channel": null + }, + { + "name": "", + "callsign": "PA", + "beacon_type": 9, + "hertz": 905000, + "channel": null + }, + { + "name": "", + "callsign": "OE", + "beacon_type": 9, + "hertz": 462000, + "channel": null + }, + { + "name": "", + "callsign": "LY", + "beacon_type": 9, + "hertz": 670000, + "channel": null + }, + { + "name": "", + "callsign": "MA", + "beacon_type": 9, + "hertz": 770000, + "channel": null + }, + { + "name": "", + "callsign": "AH", + "beacon_type": 9, + "hertz": 300500, + "channel": null + }, + { + "name": "", + "callsign": "NK", + "beacon_type": 9, + "hertz": 1030000, + "channel": null + }, + { + "name": "", + "callsign": "NE", + "beacon_type": 9, + "hertz": 740000, + "channel": null + }, + { + "name": "", + "callsign": "LE", + "beacon_type": 9, + "hertz": 395000, + "channel": null + }, + { + "name": "", + "callsign": "UH", + "beacon_type": 9, + "hertz": 528000, + "channel": null + }, + { + "name": "", + "callsign": "RE", + "beacon_type": 9, + "hertz": 320000, + "channel": null + }, + { + "name": "", + "callsign": "LA", + "beacon_type": 9, + "hertz": 307000, + "channel": null + }, + { + "name": "", + "callsign": "BD", + "beacon_type": 9, + "hertz": 342000, + "channel": null + }, + { + "name": "", + "callsign": "KP", + "beacon_type": 9, + "hertz": 214000, + "channel": null + }, + { + "name": "", + "callsign": "LA", + "beacon_type": 9, + "hertz": 750000, + "channel": null + }, + { + "name": "", + "callsign": "KS", + "beacon_type": 9, + "hertz": 1025000, + "channel": null + } +] \ No newline at end of file diff --git a/resources/dcs/beacons/nevada.json b/resources/dcs/beacons/nevada.json new file mode 100644 index 00000000..4c93b5c5 --- /dev/null +++ b/resources/dcs/beacons/nevada.json @@ -0,0 +1,317 @@ +[ + { + "name": "", + "callsign": "ICRR", + "beacon_type": 15, + "hertz": 108700000, + "channel": 24 + }, + { + "name": "", + "callsign": "ICRR", + "beacon_type": 14, + "hertz": 108700000, + "channel": 24 + }, + { + "name": "", + "callsign": "ICRS", + "beacon_type": 14, + "hertz": 108500000, + "channel": 22 + }, + { + "name": "", + "callsign": "ICRS", + "beacon_type": 15, + "hertz": 108500000, + "channel": 22 + }, + { + "name": "Indian Springs", + "callsign": "INS", + "beacon_type": 5, + "hertz": null, + "channel": 87 + }, + { + "name": "", + "callsign": "GLRI", + "beacon_type": 14, + "hertz": 109300000, + "channel": 30 + }, + { + "name": "", + "callsign": "GLRI", + "beacon_type": 15, + "hertz": 109300000, + "channel": 30 + }, + { + "name": "Groom Lake", + "callsign": "GRL", + "beacon_type": 5, + "hertz": null, + "channel": 18 + }, + { + "name": "", + "callsign": "I-RLE", + "beacon_type": 15, + "hertz": 111750000, + "channel": null + }, + { + "name": "", + "callsign": "I-LAS", + "beacon_type": 15, + "hertz": 110300000, + "channel": 40 + }, + { + "name": "", + "callsign": "I-RLE", + "beacon_type": 14, + "hertz": 111750000, + "channel": null + }, + { + "name": "", + "callsign": "I-LAS", + "beacon_type": 14, + "hertz": 110300000, + "channel": 40 + }, + { + "name": "Las Vegas", + "callsign": "LAS", + "beacon_type": 6, + "hertz": 116900000, + "channel": 116 + }, + { + "name": "", + "callsign": "IDIQ", + "beacon_type": 15, + "hertz": 109100000, + "channel": null + }, + { + "name": "Nellis", + "callsign": "LSV", + "beacon_type": 5, + "hertz": null, + "channel": 12 + }, + { + "name": "", + "callsign": "IDIQ", + "beacon_type": 14, + "hertz": 109100000, + "channel": null + }, + { + "name": "", + "callsign": "I-HWG", + "beacon_type": 14, + "hertz": 110700000, + "channel": null + }, + { + "name": "", + "callsign": "I-HWG", + "beacon_type": 15, + "hertz": 110700000, + "channel": null + }, + { + "name": "", + "callsign": "I-RVP", + "beacon_type": 14, + "hertz": 108300000, + "channel": null + }, + { + "name": "", + "callsign": "I-UVV", + "beacon_type": 14, + "hertz": 111700000, + "channel": null + }, + { + "name": "", + "callsign": "I-UVV", + "beacon_type": 15, + "hertz": 111700000, + "channel": null + }, + { + "name": "", + "callsign": "I-RVP", + "beacon_type": 15, + "hertz": 108300000, + "channel": null + }, + { + "name": "Silverbow", + "callsign": "TQQ", + "beacon_type": 6, + "hertz": 113000000, + "channel": 77 + }, + { + "name": "St George", + "callsign": "UTI", + "beacon_type": 4, + "hertz": 108600000, + "channel": 23 + }, + { + "name": "Grand Canyon", + "callsign": "GCN", + "beacon_type": 4, + "hertz": 113100000, + "channel": 78 + }, + { + "name": "Kingman", + "callsign": "IGM", + "beacon_type": 4, + "hertz": 108800000, + "channel": 25 + }, + { + "name": "Colorado City", + "callsign": "AZC", + "beacon_type": 10, + "hertz": 403000, + "channel": null + }, + { + "name": "Meggi", + "callsign": "EC", + "beacon_type": 10, + "hertz": 217000, + "channel": null + }, + { + "name": "Daggett", + "callsign": "DAG", + "beacon_type": 6, + "hertz": 113200000, + "channel": 79 + }, + { + "name": "Hector", + "callsign": "HEC", + "beacon_type": 6, + "hertz": 112700000, + "channel": 74 + }, + { + "name": "Needles", + "callsign": "EED", + "beacon_type": 6, + "hertz": 115200000, + "channel": 99 + }, + { + "name": "Milford", + "callsign": "MLF", + "beacon_type": 6, + "hertz": 112100000, + "channel": 58 + }, + { + "name": "GOFFS", + "callsign": "GFS", + "beacon_type": 6, + "hertz": 114400000, + "channel": 91 + }, + { + "name": "Tonopah", + "callsign": "TPH", + "beacon_type": 6, + "hertz": 117200000, + "channel": 119 + }, + { + "name": "Mina", + "callsign": "MVA", + "beacon_type": 6, + "hertz": 115100000, + "channel": 98 + }, + { + "name": "Wilson Creek", + "callsign": "ILC", + "beacon_type": 6, + "hertz": 116300000, + "channel": 110 + }, + { + "name": "Cedar City", + "callsign": "CDC", + "beacon_type": 6, + "hertz": 117300000, + "channel": 120 + }, + { + "name": "Bryce Canyon", + "callsign": "BCE", + "beacon_type": 6, + "hertz": 112800000, + "channel": 75 + }, + { + "name": "Mormon Mesa", + "callsign": "MMM", + "beacon_type": 6, + "hertz": 114300000, + "channel": 90 + }, + { + "name": "Beatty", + "callsign": "BTY", + "beacon_type": 6, + "hertz": 114700000, + "channel": 94 + }, + { + "name": "Bishop", + "callsign": "BIH", + "beacon_type": 6, + "hertz": 109600000, + "channel": 33 + }, + { + "name": "Coaldale", + "callsign": "OAL", + "beacon_type": 6, + "hertz": 117700000, + "channel": 124 + }, + { + "name": "Peach Springs", + "callsign": "PGS", + "beacon_type": 6, + "hertz": 112000000, + "channel": 57 + }, + { + "name": "Boulder City", + "callsign": "BLD", + "beacon_type": 6, + "hertz": 116700000, + "channel": 114 + }, + { + "name": "Mercury", + "callsign": "MCY", + "beacon_type": 10, + "hertz": 326000, + "channel": null + } +] \ No newline at end of file diff --git a/resources/dcs/beacons/normandy.json b/resources/dcs/beacons/normandy.json new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/resources/dcs/beacons/normandy.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/resources/dcs/beacons/persiangulf.json b/resources/dcs/beacons/persiangulf.json new file mode 100644 index 00000000..3f0f870a --- /dev/null +++ b/resources/dcs/beacons/persiangulf.json @@ -0,0 +1,709 @@ +[ + { + "name": "ABUDHABI", + "callsign": "ADV", + "beacon_type": 2, + "hertz": 114250000, + "channel": null + }, + { + "name": "AbuDhabiInt", + "callsign": "ADV", + "beacon_type": 3, + "hertz": 114250000, + "channel": 119 + }, + { + "name": "Abumusa", + "callsign": "ABM", + "beacon_type": 3, + "hertz": 285000, + "channel": 101 + }, + { + "name": "AlAinInt", + "callsign": "ALN", + "beacon_type": 4, + "hertz": 112600000, + "channel": 119 + }, + { + "name": "AlBateenInt", + "callsign": "ALB", + "beacon_type": 2, + "hertz": 114000000, + "channel": 119 + }, + { + "name": "BandarAbbas", + "callsign": "BND", + "beacon_type": 4, + "hertz": 117200000, + "channel": 119 + }, + { + "name": "BandarAbbas", + "callsign": "BND", + "beacon_type": 9, + "hertz": 250000, + "channel": null + }, + { + "name": "", + "callsign": "IBND", + "beacon_type": 14, + "hertz": 333800000, + "channel": null + }, + { + "name": "", + "callsign": "IBND", + "beacon_type": 15, + "hertz": 333800000, + "channel": null + }, + { + "name": "BandarAbbas", + "callsign": "BND", + "beacon_type": 5, + "hertz": null, + "channel": 78 + }, + { + "name": "BandarEJask", + "callsign": "KHM", + "beacon_type": 4, + "hertz": 116300000, + "channel": null + }, + { + "name": "BandarEJask", + "callsign": "JSK", + "beacon_type": 9, + "hertz": 349000000, + "channel": null + }, + { + "name": "BandarLengeh", + "callsign": "LEN", + "beacon_type": 9, + "hertz": 408000, + "channel": null + }, + { + "name": "BandarLengeh", + "callsign": "LEN", + "beacon_type": 4, + "hertz": 114800000, + "channel": 95 + }, + { + "name": "", + "callsign": "MMA", + "beacon_type": 15, + "hertz": 111100000, + "channel": 48 + }, + { + "name": "", + "callsign": "LMA", + "beacon_type": 15, + "hertz": 108700000, + "channel": 24 + }, + { + "name": "", + "callsign": "IMA", + "beacon_type": 15, + "hertz": 109100000, + "channel": 28 + }, + { + "name": "", + "callsign": "RMA", + "beacon_type": 15, + "hertz": 114900000, + "channel": 24 + }, + { + "name": "", + "callsign": "MMA", + "beacon_type": 14, + "hertz": 111100000, + "channel": 48 + }, + { + "name": "", + "callsign": "RMA", + "beacon_type": 14, + "hertz": 114900000, + "channel": 24 + }, + { + "name": "", + "callsign": "LMA", + "beacon_type": 14, + "hertz": 108700000, + "channel": 24 + }, + { + "name": "", + "callsign": "IMA", + "beacon_type": 14, + "hertz": 109100000, + "channel": 28 + }, + { + "name": "AlDhafra", + "callsign": "MA", + "beacon_type": 6, + "hertz": 114900000, + "channel": 96 + }, + { + "name": "", + "callsign": "IDBW", + "beacon_type": 14, + "hertz": 109500000, + "channel": null + }, + { + "name": "", + "callsign": "IDBR", + "beacon_type": 14, + "hertz": 110100000, + "channel": null + }, + { + "name": "", + "callsign": "IDBE", + "beacon_type": 14, + "hertz": 111300000, + "channel": null + }, + { + "name": "", + "callsign": "IDBL", + "beacon_type": 14, + "hertz": 110900000, + "channel": null + }, + { + "name": "", + "callsign": "IDBL", + "beacon_type": 15, + "hertz": 110900000, + "channel": null + }, + { + "name": "", + "callsign": "IDBR", + "beacon_type": 15, + "hertz": 110100000, + "channel": null + }, + { + "name": "", + "callsign": "IDBE", + "beacon_type": 15, + "hertz": 111300000, + "channel": null + }, + { + "name": "", + "callsign": "IDBW", + "beacon_type": 15, + "hertz": 109500000, + "channel": null + }, + { + "name": "", + "callsign": "IJEA", + "beacon_type": 14, + "hertz": 111750000, + "channel": null + }, + { + "name": "", + "callsign": "IJWA", + "beacon_type": 15, + "hertz": 109750000, + "channel": null + }, + { + "name": "", + "callsign": "IJEA", + "beacon_type": 15, + "hertz": 111750000, + "channel": null + }, + { + "name": "", + "callsign": "IJWA", + "beacon_type": 14, + "hertz": 109750000, + "channel": null + }, + { + "name": "Fujairah", + "callsign": "FJV", + "beacon_type": 4, + "hertz": 113800000, + "channel": 85 + }, + { + "name": "", + "callsign": "IFJR", + "beacon_type": 15, + "hertz": 111500000, + "channel": null + }, + { + "name": "", + "callsign": "IFJR", + "beacon_type": 14, + "hertz": 111500000, + "channel": null + }, + { + "name": "Havadarya", + "callsign": "HDR", + "beacon_type": 5, + "hertz": 111000000, + "channel": 47 + }, + { + "name": "", + "callsign": "IBHD", + "beacon_type": 14, + "hertz": 108900000, + "channel": null + }, + { + "name": "", + "callsign": "IBHD", + "beacon_type": 15, + "hertz": 108900000, + "channel": null + }, + { + "name": "Jiroft", + "callsign": "JIR", + "beacon_type": 10, + "hertz": 276000, + "channel": null + }, + { + "name": "KERMAN", + "callsign": "KER", + "beacon_type": 5, + "hertz": 122500000, + "channel": 97 + }, + { + "name": "KERMAN", + "callsign": "KER", + "beacon_type": 4, + "hertz": 112000000, + "channel": 57 + }, + { + "name": "KERMAN", + "callsign": "KER", + "beacon_type": 3, + "hertz": 290000000, + "channel": null + }, + { + "name": "", + "callsign": "IBKS", + "beacon_type": 14, + "hertz": 110300000, + "channel": null + }, + { + "name": "", + "callsign": "IBKS", + "beacon_type": 15, + "hertz": 110300000, + "channel": null + }, + { + "name": "KishIsland", + "callsign": "KIH", + "beacon_type": 9, + "hertz": 201000000, + "channel": null + }, + { + "name": "KishIsland", + "callsign": "KIH", + "beacon_type": 5, + "hertz": null, + "channel": 112 + }, + { + "name": "LAR", + "callsign": "LAR", + "beacon_type": 4, + "hertz": 117900000, + "channel": null + }, + { + "name": "LAR", + "callsign": "OISL", + "beacon_type": 9, + "hertz": 224000, + "channel": null + }, + { + "name": "LavanIsland", + "callsign": "LVA", + "beacon_type": 4, + "hertz": 116850000, + "channel": 115 + }, + { + "name": "LavanIsland", + "callsign": "LVA", + "beacon_type": 9, + "hertz": 310000000, + "channel": 0 + }, + { + "name": "LiwaAirbase", + "callsign": "\u00c4\u00bc", + "beacon_type": 7, + "hertz": null, + "channel": 121 + }, + { + "name": "Minhad", + "callsign": "MIN", + "beacon_type": 5, + "hertz": 115200000, + "channel": 99 + }, + { + "name": "", + "callsign": "IMNW", + "beacon_type": 14, + "hertz": 110700000, + "channel": null + }, + { + "name": "", + "callsign": "IMNW", + "beacon_type": 15, + "hertz": 110700000, + "channel": null + }, + { + "name": "", + "callsign": "IMNR", + "beacon_type": 14, + "hertz": 110750000, + "channel": null + }, + { + "name": "", + "callsign": "IMNR", + "beacon_type": 15, + "hertz": 110750000, + "channel": null + }, + { + "name": "GheshmIsland", + "callsign": "KHM", + "beacon_type": 9, + "hertz": 233000, + "channel": null + }, + { + "name": "GheshmIsland", + "callsign": "KHM", + "beacon_type": 4, + "hertz": 117100000, + "channel": null + }, + { + "name": "RasAlKhaimah", + "callsign": "OMRK", + "beacon_type": 4, + "hertz": 113600000, + "channel": 83 + }, + { + "name": "SasAlNakheelAirport", + "callsign": "SAS", + "beacon_type": 10, + "hertz": 128925, + "channel": null + }, + { + "name": "SasAlNakheel", + "callsign": "SAS", + "beacon_type": 4, + "hertz": 128925000, + "channel": 119 + }, + { + "name": "", + "callsign": "ISRE", + "beacon_type": 14, + "hertz": 108550000, + "channel": null + }, + { + "name": "", + "callsign": "ISHW", + "beacon_type": 14, + "hertz": 111950000, + "channel": null + }, + { + "name": "", + "callsign": "ISHW", + "beacon_type": 15, + "hertz": 111950000, + "channel": null + }, + { + "name": "", + "callsign": "ISRE", + "beacon_type": 15, + "hertz": 108550000, + "channel": null + }, + { + "name": "SHIRAZ", + "callsign": "SYZ", + "beacon_type": 4, + "hertz": 117800000, + "channel": 125 + }, + { + "name": "SHIRAZ", + "callsign": "SYZ1", + "beacon_type": 5, + "hertz": 114700000, + "channel": 94 + }, + { + "name": "SHIRAZ", + "callsign": "SR", + "beacon_type": 9, + "hertz": 205000, + "channel": null + }, + { + "name": "", + "callsign": "ISYZ", + "beacon_type": 15, + "hertz": 109900000, + "channel": null + }, + { + "name": "", + "callsign": "ISYZ", + "beacon_type": 14, + "hertz": 109900000, + "channel": null + }, + { + "name": "SirriIsland", + "callsign": "SIR", + "beacon_type": 9, + "hertz": 300000, + "channel": null + }, + { + "name": "SirriIsland", + "callsign": "SIR", + "beacon_type": 4, + "hertz": 113750000, + "channel": null + }, + { + "name": "Kochak", + "callsign": "KCK", + "beacon_type": 5, + "hertz": 114200000, + "channel": 89 + }, + { + "name": "Kish", + "callsign": "KIS", + "beacon_type": 4, + "hertz": 117400000, + "channel": 121 + }, + { + "name": "DohaAirport", + "callsign": "DIA", + "beacon_type": 4, + "hertz": 112400000, + "channel": 71 + }, + { + "name": "HamadInternationalAirport", + "callsign": "DOH", + "beacon_type": 4, + "hertz": 114400000, + "channel": 91 + }, + { + "name": "DezfulAirport", + "callsign": "DZF", + "beacon_type": 9, + "hertz": 293000000, + "channel": null + }, + { + "name": "AbadanIntAirport", + "callsign": "ABD", + "beacon_type": 4, + "hertz": 115100000, + "channel": 98 + }, + { + "name": "AhvazIntAirport", + "callsign": "AWZ", + "beacon_type": 4, + "hertz": 114000000, + "channel": 87 + }, + { + "name": "AghajariAirport", + "callsign": "AJR", + "beacon_type": 4, + "hertz": 114900000, + "channel": 96 + }, + { + "name": "BirjandIntAirport", + "callsign": "BJD", + "beacon_type": 4, + "hertz": 113500000, + "channel": 82 + }, + { + "name": "BushehrIntAirport", + "callsign": "BUZ", + "beacon_type": 4, + "hertz": 117450000, + "channel": 121 + }, + { + "name": "KonarakAirport", + "callsign": "CBH", + "beacon_type": 4, + "hertz": 115600000, + "channel": 103 + }, + { + "name": "IsfahanIntAirport", + "callsign": "ISN", + "beacon_type": 4, + "hertz": 113200000, + "channel": 79 + }, + { + "name": "KhoramabadAirport", + "callsign": "KRD", + "beacon_type": 4, + "hertz": 113750000, + "channel": 84 + }, + { + "name": "PersianGulfIntAirport", + "callsign": "PRG", + "beacon_type": 4, + "hertz": 112100000, + "channel": 58 + }, + { + "name": "YasoujAirport", + "callsign": "YSJ", + "beacon_type": 4, + "hertz": 116550000, + "channel": 112 + }, + { + "name": "BamAirport", + "callsign": "BAM", + "beacon_type": 4, + "hertz": 114900000, + "channel": 96 + }, + { + "name": "MahshahrAirport", + "callsign": "MAH", + "beacon_type": 4, + "hertz": 115800000, + "channel": 105 + }, + { + "name": "IranShahrAirport", + "callsign": "ISR", + "beacon_type": 4, + "hertz": 117000000, + "channel": 117 + }, + { + "name": "LamerdAirport", + "callsign": "LAM", + "beacon_type": 4, + "hertz": 117000000, + "channel": 117 + }, + { + "name": "SirjanAirport", + "callsign": "SRJ", + "beacon_type": 4, + "hertz": 114600000, + "channel": 93 + }, + { + "name": "YazdIntAirport", + "callsign": "YZD", + "beacon_type": 4, + "hertz": 117700000, + "channel": 124 + }, + { + "name": "ZabolAirport", + "callsign": "ZAL", + "beacon_type": 4, + "hertz": 113100000, + "channel": 78 + }, + { + "name": "ZahedanIntAirport", + "callsign": "ZDN", + "beacon_type": 4, + "hertz": 116000000, + "channel": 107 + }, + { + "name": "RafsanjanAirport", + "callsign": "RAF", + "beacon_type": 4, + "hertz": 112300000, + "channel": 70 + }, + { + "name": "SaravanAirport", + "callsign": "SRN", + "beacon_type": 4, + "hertz": 114100000, + "channel": 88 + }, + { + "name": "BuHasa", + "callsign": "BH", + "beacon_type": 3, + "hertz": 309000000, + "channel": null + } +] \ No newline at end of file diff --git a/resources/dcs/beacons/syria.json b/resources/dcs/beacons/syria.json new file mode 100644 index 00000000..d5ee97cd --- /dev/null +++ b/resources/dcs/beacons/syria.json @@ -0,0 +1,408 @@ +[ + { + "name": "Deir ez-Zor", + "callsign": "DRZ", + "beacon_type": 10, + "hertz": 295000, + "channel": null + }, + { + "name": "GAZIANTEP", + "callsign": "GAZ", + "beacon_type": 10, + "hertz": 432000, + "channel": null + }, + { + "name": "BANIAS", + "callsign": "BAN", + "beacon_type": 10, + "hertz": 304000, + "channel": null + }, + { + "name": "ALEPPO", + "callsign": "ALE", + "beacon_type": 10, + "hertz": 396000, + "channel": null + }, + { + "name": "KAHRAMANMARAS", + "callsign": "KHM", + "beacon_type": 10, + "hertz": 374000, + "channel": null + }, + { + "name": "MEZZEH", + "callsign": "MEZ", + "beacon_type": 10, + "hertz": 358000, + "channel": null + }, + { + "name": "KLEYATE", + "callsign": "RA", + "beacon_type": 10, + "hertz": 450000, + "channel": null + }, + { + "name": "KARIATAIN", + "callsign": "KTN", + "beacon_type": 10, + "hertz": 372500, + "channel": null + }, + { + "name": "ALEPPO", + "callsign": "MER", + "beacon_type": 10, + "hertz": 365000, + "channel": null + }, + { + "name": "TURAIF", + "callsign": "TRF", + "beacon_type": 4, + "hertz": 116100000, + "channel": null + }, + { + "name": "Deir ez-Zor", + "callsign": "DRZ", + "beacon_type": 4, + "hertz": 117000000, + "channel": null + }, + { + "name": "BAYSUR", + "callsign": "BAR", + "beacon_type": 4, + "hertz": 113900000, + "channel": null + }, + { + "name": "ALEPPO", + "callsign": "ALE", + "beacon_type": 4, + "hertz": 114500000, + "channel": null + }, + { + "name": "MARKA", + "callsign": "AMN", + "beacon_type": 4, + "hertz": 116300000, + "channel": null + }, + { + "name": "GAZIANTEP", + "callsign": "GAZ", + "beacon_type": 4, + "hertz": 116700000, + "channel": null + }, + { + "name": "ROSH-PINA", + "callsign": "ROP", + "beacon_type": 4, + "hertz": 115300000, + "channel": null + }, + { + "name": "TANF", + "callsign": "TAN", + "beacon_type": 4, + "hertz": 114000000, + "channel": null + }, + { + "name": "NATANIA", + "callsign": "NAT", + "beacon_type": 4, + "hertz": 112400000, + "channel": null + }, + { + "name": "KAHRAMANMARAS", + "callsign": "KHM", + "beacon_type": 4, + "hertz": 113900000, + "channel": null + }, + { + "name": "KARIATAIN", + "callsign": "KTN", + "beacon_type": 4, + "hertz": 117700000, + "channel": null + }, + { + "name": "", + "callsign": "IADA", + "beacon_type": 14, + "hertz": 108700000, + "channel": null + }, + { + "name": "", + "callsign": "IADA", + "beacon_type": 15, + "hertz": 108700000, + "channel": null + }, + { + "name": "ADANA", + "callsign": "ADN", + "beacon_type": 11, + "hertz": 395000000, + "channel": null + }, + { + "name": "ADANA", + "callsign": "ADA", + "beacon_type": 4, + "hertz": 112700000, + "channel": null + }, + { + "name": "KALDE", + "callsign": "KAD", + "beacon_type": 4, + "hertz": 112600000, + "channel": null + }, + { + "name": "", + "callsign": "IBB", + "beacon_type": 15, + "hertz": 110100000, + "channel": null + }, + { + "name": "", + "callsign": "IKK", + "beacon_type": 14, + "hertz": 110700000, + "channel": null + }, + { + "name": "", + "callsign": "BIL", + "beacon_type": 14, + "hertz": 109500000, + "channel": null + }, + { + "name": "", + "callsign": "IBB", + "beacon_type": 14, + "hertz": 110100000, + "channel": null + }, + { + "name": "", + "callsign": "BIL", + "beacon_type": 15, + "hertz": 109500000, + "channel": null + }, + { + "name": "", + "callsign": "IKK", + "beacon_type": 15, + "hertz": 110700000, + "channel": null + }, + { + "name": "BEIRUT", + "callsign": "BOD", + "beacon_type": 11, + "hertz": 351000000, + "channel": null + }, + { + "name": "", + "callsign": "IDA", + "beacon_type": 15, + "hertz": 109900000, + "channel": null + }, + { + "name": "", + "callsign": "IDA", + "beacon_type": 14, + "hertz": 109900000, + "channel": null + }, + { + "name": "Damascus", + "callsign": "DAM", + "beacon_type": 4, + "hertz": 116000000, + "channel": null + }, + { + "name": "", + "callsign": "DAML", + "beacon_type": 14, + "hertz": 111100000, + "channel": null + }, + { + "name": "DAMASCUS", + "callsign": "DAL", + "beacon_type": 11, + "hertz": 342000000, + "channel": null + }, + { + "name": "ABYAD", + "callsign": "ABD", + "beacon_type": 10, + "hertz": 264000, + "channel": null + }, + { + "name": "", + "callsign": "DAML", + "beacon_type": 15, + "hertz": 111100000, + "channel": null + }, + { + "name": "HATAY", + "callsign": "HTY", + "beacon_type": 4, + "hertz": 112050000, + "channel": null + }, + { + "name": "", + "callsign": "IHAT", + "beacon_type": 14, + "hertz": 108900000, + "channel": null + }, + { + "name": "", + "callsign": "IHAT", + "beacon_type": 15, + "hertz": 108900000, + "channel": null + }, + { + "name": "HATAY", + "callsign": "HTY", + "beacon_type": 10, + "hertz": 336000, + "channel": null + }, + { + "name": "", + "callsign": "IHTY", + "beacon_type": 15, + "hertz": 108150000, + "channel": null + }, + { + "name": "", + "callsign": "IHTY", + "beacon_type": 14, + "hertz": 108150000, + "channel": null + }, + { + "name": "INCIRLIC", + "callsign": "DAN", + "beacon_type": 6, + "hertz": 108400000, + "channel": 21 + }, + { + "name": "", + "callsign": "IDAN", + "beacon_type": 14, + "hertz": 109300000, + "channel": null + }, + { + "name": "", + "callsign": "IDAN", + "beacon_type": 15, + "hertz": 109300000, + "channel": null + }, + { + "name": "", + "callsign": "DANM", + "beacon_type": 15, + "hertz": 111700000, + "channel": null + }, + { + "name": "", + "callsign": "DANM", + "beacon_type": 14, + "hertz": 111700000, + "channel": null + }, + { + "name": "", + "callsign": "IBA", + "beacon_type": 15, + "hertz": 109100000, + "channel": null + }, + { + "name": "", + "callsign": "IBA", + "beacon_type": 14, + "hertz": 109100000, + "channel": null + }, + { + "name": "LATAKIA", + "callsign": "LTK", + "beacon_type": 4, + "hertz": 114800000, + "channel": null + }, + { + "name": "LATAKIA", + "callsign": "LTK", + "beacon_type": 11, + "hertz": 414000000, + "channel": null + }, + { + "name": "PALMYRA", + "callsign": "PLR", + "beacon_type": 10, + "hertz": 363000, + "channel": null + }, + { + "name": "PALMYRA", + "callsign": "PAL", + "beacon_type": 10, + "hertz": 337000, + "channel": null + }, + { + "name": "RAMATDAVID", + "callsign": "RMD", + "beacon_type": 10, + "hertz": 368000, + "channel": null + }, + { + "name": "Cheka", + "callsign": "CAK", + "beacon_type": 4, + "hertz": 116200000, + "channel": null + } +] \ No newline at end of file From 010d505f04ad94012f801ae20bfe123141d32e09 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Mon, 31 Aug 2020 16:30:11 -0700 Subject: [PATCH 5/9] Reserve frequencies used by beacons. --- game/operation/operation.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/game/operation/operation.py b/game/operation/operation.py index 09242628..141f5b5b 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -1,12 +1,14 @@ -from dcs.countries import country_dict -from dcs.lua.parse import loads -from dcs.terrain import Terrain +from typing import Set from gen import * -from userdata.debriefing import * from gen.airfields import AIRFIELD_DATA +from gen.beacons import load_beacons_for_terrain from gen.radios import RadioRegistry from gen.tacan import TacanRegistry +from pydcs.dcs.countries import country_dict +from pydcs.dcs.lua.parse import loads +from pydcs.dcs.terrain.terrain import Terrain +from userdata.debriefing import * class Operation: @@ -119,6 +121,20 @@ class Operation: self.defenders_starting_position = self.to_cp.at def generate(self): + # Dedup beacon frequencies, since some maps have more than one beacon + # per frequency. + beacons = load_beacons_for_terrain(self.game.theater.terrain.name) + unique_beacon_frequencies: Set[RadioFrequency] = set() + for beacon in beacons: + unique_beacon_frequencies.add(beacon.frequency) + if beacon.is_tacan: + if beacon.channel is None: + logging.error( + f"TACAN beacon has no channel: {beacon.callsign}") + else: + self.tacan_registry.reserve(beacon.tacan_channel) + for frequency in unique_beacon_frequencies: + self.radio_registry.reserve(frequency) # Generate meteo if self.environment_settings is None: @@ -135,10 +151,8 @@ class Operation: self.radio_registry.reserve(data.atc.vhf_fm) self.radio_registry.reserve(data.atc.vhf_am) self.radio_registry.reserve(data.atc.uhf) - for ils in data.ils.values(): - self.radio_registry.reserve(ils) - if data.tacan is not None: - self.tacan_registry.reserve(data.tacan) + # No need to reserve ILS or TACAN because those are in the + # beacon list. # Generate destroyed units for d in self.game.get_destroyed_units(): From a9e65cc83d3ce036c57623cf18c56b8426395d53 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 1 Sep 2020 00:03:37 -0700 Subject: [PATCH 6/9] Setup default radio channels for player flights. --- game/operation/operation.py | 77 ++++++++++++++- gen/aircraft.py | 182 ++++++++++++++++++++++++++++++++---- gen/flights/flight.py | 14 +-- gen/kneeboard.py | 98 +++++++++---------- 4 files changed, 289 insertions(+), 82 deletions(-) diff --git a/game/operation/operation.py b/game/operation/operation.py index 141f5b5b..09b5eb0a 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -255,7 +255,7 @@ class Operation: load_dcs_libe.add_action(DoScript(String(script))) self.current_mission.triggerrules.triggers.append(load_dcs_libe) - kneeboard_generator = KneeboardGenerator(self.current_mission, self.game) + kneeboard_generator = KneeboardGenerator(self.current_mission) # Briefing Generation for tanker in self.airsupportgen.air_support.tankers: @@ -269,9 +269,82 @@ class Operation: self.briefinggen.append_frequency(awacs.callsign, awacs.freq) kneeboard_generator.add_awacs(awacs) + self.assign_channels_to_flights() + # Generate the briefing self.briefinggen.generate() for region, code, name in self.game.jtacs: kneeboard_generator.add_jtac(name, region, code) - kneeboard_generator.generate() + kneeboard_generator.generate(self.airgen.flights) + + def assign_channels_to_flights(self) -> None: + """Assigns preset radio channels for client flights.""" + for flight in self.airgen.flights: + if not flight.client_units: + continue + self.assign_channels_to_flight(flight) + + def assign_channels_to_flight(self, flight: FlightData) -> None: + """Assigns preset radio channels for a client flight.""" + airframe = flight.aircraft_type + + try: + aircraft_data = AIRCRAFT_DATA[airframe.id] + except KeyError: + logging.warning(f"No aircraft data for {airframe.id}") + return + + # Intra-flight channel is set up when the flight is created, however we + # do need to make sure we don't overwrite it. For cases where the + # inter-flight and intra-flight radios share presets (the AV-8B only has + # one set of channels, even though it can use two channels + # simultaneously), start assigning channels at 2. + radio_id = aircraft_data.inter_flight_radio_index + if aircraft_data.intra_flight_radio_index == radio_id: + first_channel = 2 + else: + first_channel = 1 + + last_channel = flight.num_radio_channels(radio_id) + channel_alloc = iter(range(first_channel, last_channel + 1)) + + # TODO: Fix departure/arrival to support carriers. + if flight.departure is not None: + try: + departure = AIRFIELD_DATA[flight.departure.name] + flight.assign_channel( + radio_id, next(channel_alloc), departure.atc.uhf) + except KeyError: + pass + + # TODO: If there ever are multiple AWACS, limit to mission relevant. + for awacs in self.airsupportgen.air_support.awacs: + flight.assign_channel(radio_id, next(channel_alloc), awacs.freq) + + # TODO: Fix departure/arrival to support carriers. + if flight.arrival is not None and flight.arrival != flight.departure: + try: + arrival = AIRFIELD_DATA[flight.arrival.name] + flight.assign_channel( + radio_id, next(channel_alloc), arrival.atc.uhf) + except KeyError: + pass + + try: + # TODO: Skip incompatible tankers. + for tanker in self.airsupportgen.air_support.tankers: + flight.assign_channel( + radio_id, next(channel_alloc), tanker.freq) + + if flight.divert is not None: + try: + divert = AIRFIELD_DATA[flight.divert.name] + flight.assign_channel( + radio_id, next(channel_alloc), divert.atc.uhf) + except KeyError: + pass + except StopIteration: + # Any remaining channels are nice-to-haves, but not necessary for + # the few aircraft with a small number of channels available. + pass diff --git a/gen/aircraft.py b/gen/aircraft.py index aa5a62e4..e0e8460e 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -1,15 +1,21 @@ from dataclasses import dataclass -from typing import Dict +from typing import Dict, List, Optional, Tuple from game.data.cap_capabilities_db import GUNFIGHTERS from game.settings import Settings from game.utils import nm_to_meter from gen.flights.ai_flight_planner import FlightPlanner -from gen.flights.flight import Flight, FlightType, FlightWaypointType +from gen.flights.flight import ( + Flight, + FlightType, + FlightWaypoint, + FlightWaypointType, +) from gen.radios import get_radio, MHz, Radio, RadioFrequency, RadioRegistry from pydcs.dcs import helicopters from pydcs.dcs.action import ActivateGroup, AITaskPush, MessageToAll from pydcs.dcs.condition import TimeAfter, CoalitionHasAirdrome, PartOfCoalitionInZone +from pydcs.dcs.flyingunit import FlyingUnit from pydcs.dcs.helicopters import helicopter_map, UH_1H from pydcs.dcs.mission import Mission, StartType from pydcs.dcs.planes import ( @@ -24,9 +30,9 @@ from pydcs.dcs.planes import ( SpitfireLFMkIX, SpitfireLFMkIXCW, ) -from pydcs.dcs.terrain.terrain import NoParkingSlotError +from pydcs.dcs.terrain.terrain import Airport, NoParkingSlotError from pydcs.dcs.triggers import TriggerOnce, Event -from pydcs.dcs.unittype import UnitType +from pydcs.dcs.unittype import FlyingType, UnitType from .conflictgen import * from .naming import * @@ -59,12 +65,40 @@ class AircraftData: #: The type of radio used for intra-flight communications. intra_flight_radio: Radio + #: Index of the radio used for intra-flight communications. Matches the + #: index of the panel_radio field of the pydcs.dcs.planes object. + inter_flight_radio_index: Optional[int] + + #: Index of the radio used for intra-flight communications. Matches the + #: index of the panel_radio field of the pydcs.dcs.planes object. + intra_flight_radio_index: Optional[int] + # Indexed by the id field of the pydcs PlaneType. AIRCRAFT_DATA: Dict[str, AircraftData] = { - "A-10C": AircraftData(get_radio("AN/ARC-186(V) AM")), - "F-16C_50": AircraftData(get_radio("AN/ARC-222")), - "F/A-18C": AircraftData(get_radio("AN/ARC-210")), + "A-10C": AircraftData( + get_radio("AN/ARC-186(V) AM"), + # The A-10's radio works differently than most aircraft. Doesn't seem to + # be a way to set these from the mission editor, let alone pydcs. + inter_flight_radio_index=None, + intra_flight_radio_index=None + ), + "F-16C_50": AircraftData( + get_radio("AN/ARC-222"), + # COM2 is the AN/ARC-222, which is the VHF radio we want to use for + # intra-flight communication to leave COM1 open for UHF inter-flight. + inter_flight_radio_index=1, + intra_flight_radio_index=2 + ), + "FA-18C_hornet": AircraftData( + get_radio("AN/ARC-210"), + # DCS will clobber channel 1 of the first radio compatible with the + # flight's assigned frequency. Since the F/A-18's two radios are both + # AN/ARC-210s, radio 1 will be compatible regardless of which frequency + # is assigned, so we must use radio 1 for the intra-flight radio. + inter_flight_radio_index=2, + intra_flight_radio_index=1 + ), } @@ -98,6 +132,100 @@ def get_fallback_channel(unit_type: UnitType) -> RadioFrequency: return UHF_FALLBACK_CHANNEL +@dataclass(frozen=True) +class ChannelAssignment: + radio_id: int + channel: int + + @property + def radio_name(self) -> str: + """Returns the name of the radio, i.e. COM1.""" + return f"COM{self.radio_id}" + + +@dataclass +class FlightData: + """Details of a planned flight.""" + + #: List of playable units in the flight. + client_units: List[FlyingUnit] + + # TODO: Arrival and departure should not be optional, but carriers don't count. + #: Arrival airport. + arrival: Optional[Airport] + + #: Departure airport. + departure: Optional[Airport] + + #: Diver airport. + divert: Optional[Airport] + + #: Waypoints of the flight plan. + waypoints: List[FlightWaypoint] + + #: Radio frequency for intra-flight communications. + intra_flight_channel: RadioFrequency + + #: Map of radio frequencies to their assigned radio and channel, if any. + frequency_to_channel_map: Dict[RadioFrequency, ChannelAssignment] + + def __init__(self, client_units: List[FlyingUnit], arrival: Airport, + departure: Airport, divert: Optional[Airport], + waypoints: List[FlightWaypoint], + intra_flight_channel: RadioFrequency) -> None: + self.client_units = client_units + self.arrival = arrival + self.departure = departure + self.divert = divert + self.waypoints = waypoints + self.intra_flight_channel = intra_flight_channel + self.frequency_to_channel_map = {} + + self.assign_intra_flight_channel() + + def assign_intra_flight_channel(self) -> None: + """Assigns a channel to the intra-flight frequency.""" + if not self.client_units: + return + + # pydcs will actually set up the channel for us, but we want to make + # sure that it ends up in frequency_to_channel_map. + try: + data = AIRCRAFT_DATA[self.aircraft_type.id] + self.assign_channel( + data.intra_flight_radio_index, 1, self.intra_flight_channel) + except KeyError: + logging.warning(f"No aircraft data for {self.aircraft_type.id}") + + @property + def aircraft_type(self) -> FlyingType: + """Returns the type of aircraft in this flight.""" + return self.client_units[0].unit_type + + def num_radio_channels(self, radio_id: int) -> int: + """Returns the number of preset channels for the given radio.""" + return self.client_units[0].num_radio_channels(radio_id) + + def channel_for( + self, frequency: RadioFrequency) -> Optional[ChannelAssignment]: + """Returns the radio and channel number for the given frequency.""" + return self.frequency_to_channel_map.get(frequency, None) + + def assign_channel(self, radio_id: int, channel_id: int, + frequency: RadioFrequency) -> None: + """Assigns a preset radio channel to the given frequency.""" + for unit in self.client_units: + unit.set_radio_channel_preset(radio_id, channel_id, frequency.mhz) + + # One frequency could be bound to multiple channels. Prefer the first, + # since with the current implementation it will be the lowest numbered + # channel. + if frequency not in self.frequency_to_channel_map: + self.frequency_to_channel_map[frequency] = ChannelAssignment( + radio_id, channel_id + ) + + class AircraftConflictGenerator: escort_targets = [] # type: typing.List[typing.Tuple[FlyingGroup, int]] @@ -109,14 +237,26 @@ class AircraftConflictGenerator: self.conflict = conflict self.radio_registry = radio_registry self.escort_targets = [] + self.flights: List[FlightData] = [] - def get_intra_flight_channel(self, airframe: UnitType) -> RadioFrequency: + def get_intra_flight_channel( + self, airframe: UnitType) -> Tuple[int, RadioFrequency]: + """Allocates an intra-flight channel to a group. + + Args: + airframe: The type of aircraft a channel should be allocated for. + + Returns: + A tuple of the radio index (for aircraft with multiple radios) and + the frequency of the intra-flight channel. + """ try: aircraft_data = AIRCRAFT_DATA[airframe.id] - return self.radio_registry.alloc_for_radio( + channel = self.radio_registry.alloc_for_radio( aircraft_data.intra_flight_radio) + return aircraft_data.intra_flight_radio_index, channel except KeyError: - return get_fallback_channel(airframe) + return 1, get_fallback_channel(airframe) def _start_type(self) -> StartType: return self.settings.cold_start and StartType.Cold or StartType.Warm @@ -156,12 +296,15 @@ class AircraftConflictGenerator: for unit_instance in group.units: unit_instance.livery_id = db.PLANE_LIVERY_OVERRIDES[unit_type] + clients: List[FlyingUnit] = [] single_client = flight.client_count == 1 for idx in range(0, min(len(group.units), flight.client_count)): + unit = group.units[idx] + clients.append(unit) if single_client: - group.units[idx].set_player() + unit.set_player() else: - group.units[idx].set_client() + unit.set_client() # Do not generate player group with late activation. if group.late_activation: @@ -169,14 +312,21 @@ class AircraftConflictGenerator: # Set up F-14 Client to have pre-stored alignement if unit_type is F_14B: - group.units[idx].set_property(F_14B.Properties.INSAlignmentStored.id, True) + unit.set_property(F_14B.Properties.INSAlignmentStored.id, True) group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)) - channel = self.get_intra_flight_channel(unit_type) - group.set_frequency(channel.mhz) - flight.intra_flight_channel = channel + radio_id, channel = self.get_intra_flight_channel(unit_type) + group.set_frequency(channel.mhz, radio_id) + self.flights.append(FlightData( + client_units=clients, + departure=flight.from_cp.airport, + arrival=flight.from_cp.airport, + divert=None, + waypoints=flight.points, + intra_flight_channel=channel + )) # Special case so Su 33 carrier take off if unit_type is Su_33: diff --git a/gen/flights/flight.py b/gen/flights/flight.py index 912946e4..a9b5e9f8 100644 --- a/gen/flights/flight.py +++ b/gen/flights/flight.py @@ -1,10 +1,8 @@ from enum import Enum -from typing import List, Optional - -from pydcs.dcs.unittype import UnitType +from typing import List from game import db -from gen.radios import RadioFrequency +from pydcs.dcs.unittype import UnitType class FlightType(Enum): @@ -96,13 +94,6 @@ class Flight: # How long before this flight should take off scheduled_in = 0 - # Populated during mission generation time by AircraftConflictGenerator. - # TODO: Decouple radio planning from the Flight. - # Make AircraftConflictGenerator generate a FlightData object that is - # returned to the Operation rather than relying on the Flight object, which - # represents a game UI flight rather than a fully planned flight. - intra_flight_channel: Optional[RadioFrequency] - def __init__(self, unit_type: UnitType, count: int, from_cp, flight_type: FlightType): self.unit_type = unit_type self.count = count @@ -112,7 +103,6 @@ class Flight: self.targets = [] self.loadout = {} self.start_type = "Runway" - self.intra_flight_channel = None def __repr__(self): return self.flight_type.name + " | " + str(self.count) + "x" + db.unit_type_name(self.unit_type) \ diff --git a/gen/kneeboard.py b/gen/kneeboard.py index 1241078e..4c652757 100644 --- a/gen/kneeboard.py +++ b/gen/kneeboard.py @@ -34,9 +34,9 @@ from pydcs.dcs.mission import Mission from pydcs.dcs.terrain.terrain import Airport from pydcs.dcs.unittype import FlyingType from . import units +from .aircraft import FlightData from .airfields import AIRFIELD_DATA from .airsupportgen import AwacsInfo, TankerInfo -from .flights.flight import Flight from .radios import RadioFrequency @@ -96,24 +96,6 @@ class KneeboardPage: raise NotImplementedError -class AirfieldInfo: - def __init__(self, airfield: Airport) -> None: - self.airport = airfield - # TODO: Implement logic for picking preferred runway. - runway = airfield.runways[0] - runway_side = ["", "L", "R"][runway.leftright] - self.runway = f"{runway.heading}{runway_side}" - try: - extra_data = AIRFIELD_DATA[airfield.name] - self.atc = extra_data.atc.uhf or "" - self.tacan = extra_data.tacan or "" - self.ils = extra_data.ils_freq(self.runway) or "" - except KeyError: - self.atc = "" - self.ils = "" - self.tacan = "" - - @dataclass class CommInfo: """Communications information for the kneeboard.""" @@ -131,21 +113,15 @@ class JtacInfo: class BriefingPage(KneeboardPage): """A kneeboard page containing briefing information.""" - def __init__(self, flight: Flight, comms: List[CommInfo], + def __init__(self, flight: FlightData, comms: List[CommInfo], awacs: List[AwacsInfo], tankers: List[TankerInfo], jtacs: List[JtacInfo]) -> None: self.flight = flight - self.comms = comms + self.comms = list(comms) self.awacs = awacs self.tankers = tankers self.jtacs = jtacs - if self.flight.intra_flight_channel is not None: - self.comms.append( - CommInfo("Flight", self.flight.intra_flight_channel) - ) - self.departure = flight.from_cp.airport - self.arrival = flight.from_cp.airport - self.divert: Optional[Airport] = None + self.comms.append(CommInfo("Flight", self.flight.intra_flight_channel)) def write(self, path: Path) -> None: writer = KneeboardPageWriter() @@ -156,14 +132,14 @@ class BriefingPage(KneeboardPage): # TODO: Handle carriers. writer.heading("Airfield Info") writer.table([ - self.airfield_info_row("Departure", self.departure), - self.airfield_info_row("Arrival", self.arrival), - self.airfield_info_row("Divert", self.divert), + self.airfield_info_row("Departure", self.flight.departure), + self.airfield_info_row("Arrival", self.flight.arrival), + self.airfield_info_row("Divert", self.flight.divert), ], headers=["", "Airbase", "ATC", "TCN", "ILS", "RWY"]) writer.heading("Flight Plan") flight_plan = [] - for num, waypoint in enumerate(self.flight.points): + for num, waypoint in enumerate(self.flight.waypoints): alt = int(units.meters_to_feet(waypoint.alt)) flight_plan.append([num, waypoint.pretty_name, str(alt)]) writer.table(flight_plan, headers=["STPT", "Action", "Alt"]) @@ -171,13 +147,13 @@ class BriefingPage(KneeboardPage): writer.heading("Comm Ladder") comms = [] for comm in self.comms: - comms.append([comm.name, comm.freq]) + comms.append([comm.name, self.format_frequency(comm.freq)]) writer.table(comms, headers=["Name", "UHF"]) writer.heading("AWACS") awacs = [] for a in self.awacs: - awacs.append([a.callsign, a.freq]) + awacs.append([a.callsign, self.format_frequency(a.freq)]) writer.table(awacs, headers=["Callsign", "UHF"]) writer.heading("Tankers") @@ -187,7 +163,7 @@ class BriefingPage(KneeboardPage): tanker.callsign, tanker.variant, tanker.tacan, - tanker.freq, + self.format_frequency(tanker.freq), ]) writer.table(tankers, headers=["Callsign", "Type", "TACAN", "UHF"]) @@ -213,23 +189,42 @@ class BriefingPage(KneeboardPage): """ if airfield is None: return [row_title, "", "", "", "", ""] - info = AirfieldInfo(airfield) + + # TODO: Implement logic for picking preferred runway. + runway = airfield.runways[0] + runway_side = ["", "L", "R"][runway.leftright] + runway_text = f"{runway.heading}{runway_side}" + + try: + extra_data = AIRFIELD_DATA[airfield.name] + atc = self.format_frequency(extra_data.atc.uhf) + tacan = extra_data.tacan or "" + ils = extra_data.ils_freq(runway) or "" + except KeyError: + atc = "" + ils = "" + tacan = "" return [ row_title, airfield.name, - info.atc, - info.tacan, - info.ils, - info.runway, + atc, + tacan, + ils, + runway_text, ] + def format_frequency(self, frequency: RadioFrequency) -> str: + channel = self.flight.channel_for(frequency) + if channel is None: + return str(frequency) + return f"{channel.radio_name} Ch {channel.channel}" + class KneeboardGenerator: """Creates kneeboard pages for each client flight in the mission.""" - def __init__(self, mission: Mission, game) -> None: + def __init__(self, mission: Mission) -> None: self.mission = mission - self.game = game self.comms: List[CommInfo] = [] self.awacs: List[AwacsInfo] = [] self.tankers: List[TankerInfo] = [] @@ -271,11 +266,11 @@ class KneeboardGenerator: # TODO: Radio info? Type? self.jtacs.append(JtacInfo(callsign, region, code)) - def generate(self) -> None: + def generate(self, flights: List[FlightData]) -> None: """Generates a kneeboard per client flight.""" temp_dir = Path("kneeboards") temp_dir.mkdir(exist_ok=True) - for aircraft, pages in self.pages_by_airframe().items(): + for aircraft, pages in self.pages_by_airframe(flights).items(): aircraft_dir = temp_dir / aircraft.id aircraft_dir.mkdir(exist_ok=True) for idx, page in enumerate(pages): @@ -283,7 +278,7 @@ class KneeboardGenerator: page.write(page_path) self.mission.add_aircraft_kneeboard(aircraft, page_path) - def pages_by_airframe(self) -> Dict[FlyingType, List[KneeboardPage]]: + def pages_by_airframe(self, flights: List[FlightData]) -> Dict[FlyingType, List[KneeboardPage]]: """Returns a list of kneeboard pages per airframe in the mission. Only client flights will be included, but because DCS does not support @@ -295,15 +290,14 @@ class KneeboardGenerator: that aircraft. """ all_flights: Dict[FlyingType, List[KneeboardPage]] = defaultdict(list) - for cp in self.game.theater.controlpoints: - if cp.id in self.game.planners.keys(): - for flight in self.game.planners[cp.id].flights: - if flight.client_count > 0: - all_flights[flight.unit_type].extend( - self.generate_flight_kneeboard(flight)) + for flight in flights: + if not flight.client_units: + continue + all_flights[flight.aircraft_type].extend( + self.generate_flight_kneeboard(flight)) return all_flights - def generate_flight_kneeboard(self, flight: Flight) -> List[KneeboardPage]: + def generate_flight_kneeboard(self, flight: FlightData) -> List[KneeboardPage]: """Returns a list of kneeboard pages for the given flight.""" return [ BriefingPage( From d02a3a0d3f8f067095da8f4dd1a9ec558f962f50 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 1 Sep 2020 14:10:57 -0700 Subject: [PATCH 7/9] Add carrier support to kneeboards. --- game/operation/operation.py | 60 ++++++++-------- gen/aircraft.py | 85 +++++++++++------------ gen/airfields.py | 38 ++++++++++ gen/groundobjectsgen.py | 84 +++++++++++++++++++--- gen/kneeboard.py | 36 ++++------ resources/tools/generate_loadout_check.py | 2 +- theater/controlpoint.py | 14 ---- 7 files changed, 191 insertions(+), 128 deletions(-) diff --git a/game/operation/operation.py b/game/operation/operation.py index 09b5eb0a..051df639 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -80,7 +80,13 @@ class Operation: self.visualgen = VisualGenerator(mission, conflict, self.game) self.envgen = EnviromentGenerator(mission, conflict, self.game) self.forcedoptionsgen = ForcedOptionsGenerator(mission, conflict, self.game) - self.groundobjectgen = GroundObjectsGenerator(mission, conflict, self.game) + self.groundobjectgen = GroundObjectsGenerator( + mission, + conflict, + self.game, + self.radio_registry, + self.tacan_registry + ) self.briefinggen = BriefingGenerator(mission, conflict, self.game) def prepare(self, terrain: Terrain, is_quick: bool): @@ -136,15 +142,6 @@ class Operation: for frequency in unique_beacon_frequencies: self.radio_registry.reserve(frequency) - # Generate meteo - if self.environment_settings is None: - self.environment_settings = self.envgen.generate() - else: - self.envgen.load(self.environment_settings) - - # Generate ground object first - self.groundobjectgen.generate() - for airfield, data in AIRFIELD_DATA.items(): if data.theater == self.game.theater.terrain.name: self.radio_registry.reserve(data.atc.hf) @@ -154,6 +151,15 @@ class Operation: # No need to reserve ILS or TACAN because those are in the # beacon list. + # Generate meteo + if self.environment_settings is None: + self.environment_settings = self.envgen.generate() + else: + self.envgen.load(self.environment_settings) + + # Generate ground object first + self.groundobjectgen.generate() + # Generate destroyed units for d in self.game.get_destroyed_units(): try: @@ -185,7 +191,12 @@ class Operation: else: country = self.current_mission.country(self.game.enemy_country) if cp.id in self.game.planners.keys(): - self.airgen.generate_flights(cp, country, self.game.planners[cp.id]) + self.airgen.generate_flights( + cp, + country, + self.game.planners[cp.id], + self.groundobjectgen.runways + ) # Generate ground units on frontline everywhere self.game.jtacs = [] @@ -309,27 +320,16 @@ class Operation: last_channel = flight.num_radio_channels(radio_id) channel_alloc = iter(range(first_channel, last_channel + 1)) - # TODO: Fix departure/arrival to support carriers. - if flight.departure is not None: - try: - departure = AIRFIELD_DATA[flight.departure.name] - flight.assign_channel( - radio_id, next(channel_alloc), departure.atc.uhf) - except KeyError: - pass + flight.assign_channel(radio_id, next(channel_alloc),flight.departure.atc) # TODO: If there ever are multiple AWACS, limit to mission relevant. for awacs in self.airsupportgen.air_support.awacs: flight.assign_channel(radio_id, next(channel_alloc), awacs.freq) # TODO: Fix departure/arrival to support carriers. - if flight.arrival is not None and flight.arrival != flight.departure: - try: - arrival = AIRFIELD_DATA[flight.arrival.name] - flight.assign_channel( - radio_id, next(channel_alloc), arrival.atc.uhf) - except KeyError: - pass + if flight.arrival != flight.departure: + flight.assign_channel(radio_id, next(channel_alloc), + flight.arrival.atc) try: # TODO: Skip incompatible tankers. @@ -338,12 +338,8 @@ class Operation: radio_id, next(channel_alloc), tanker.freq) if flight.divert is not None: - try: - divert = AIRFIELD_DATA[flight.divert.name] - flight.assign_channel( - radio_id, next(channel_alloc), divert.atc.uhf) - except KeyError: - pass + flight.assign_channel(radio_id, next(channel_alloc), + flight.divert.atc) except StopIteration: # Any remaining channels are nice-to-haves, but not necessary for # the few aircraft with a small number of channels available. diff --git a/gen/aircraft.py b/gen/aircraft.py index e0e8460e..941bcd38 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -4,6 +4,7 @@ from typing import Dict, List, Optional, Tuple from game.data.cap_capabilities_db import GUNFIGHTERS from game.settings import Settings from game.utils import nm_to_meter +from gen.airfields import RunwayData from gen.flights.ai_flight_planner import FlightPlanner from gen.flights.flight import ( Flight, @@ -150,15 +151,14 @@ class FlightData: #: List of playable units in the flight. client_units: List[FlyingUnit] - # TODO: Arrival and departure should not be optional, but carriers don't count. #: Arrival airport. - arrival: Optional[Airport] + arrival: RunwayData #: Departure airport. - departure: Optional[Airport] + departure: RunwayData #: Diver airport. - divert: Optional[Airport] + divert: Optional[RunwayData] #: Waypoints of the flight plan. waypoints: List[FlightWaypoint] @@ -169,8 +169,8 @@ class FlightData: #: Map of radio frequencies to their assigned radio and channel, if any. frequency_to_channel_map: Dict[RadioFrequency, ChannelAssignment] - def __init__(self, client_units: List[FlyingUnit], arrival: Airport, - departure: Airport, divert: Optional[Airport], + def __init__(self, client_units: List[FlyingUnit], arrival: RunwayData, + departure: RunwayData, divert: Optional[RunwayData], waypoints: List[FlightWaypoint], intra_flight_channel: RadioFrequency) -> None: self.client_units = client_units @@ -261,8 +261,8 @@ class AircraftConflictGenerator: def _start_type(self) -> StartType: return self.settings.cold_start and StartType.Cold or StartType.Warm - - def _setup_group(self, group: FlyingGroup, for_task: typing.Type[Task], flight: Flight): + def _setup_group(self, group: FlyingGroup, for_task: typing.Type[Task], + flight: Flight, dynamic_runways: Dict[str, RunwayData]): did_load_loadout = False unit_type = group.units[0].unit_type @@ -319,10 +319,28 @@ class AircraftConflictGenerator: radio_id, channel = self.get_intra_flight_channel(unit_type) group.set_frequency(channel.mhz, radio_id) + + # TODO: Support for different departure/arrival airfields. + cp = flight.from_cp + fallback_runway = RunwayData(cp.full_name, runway_name="") + if cp.cptype == ControlPointType.AIRBASE: + # TODO: Implement logic for picking preferred runway. + runway = flight.from_cp.airport.runways[0] + runway_side = ["", "L", "R"][runway.leftright] + runway_name = f"{runway.heading}{runway_side}" + departure_runway = RunwayData.for_airfield( + flight.from_cp.airport, runway_name) + elif cp.is_fleet: + departure_runway = dynamic_runways.get(cp.name, fallback_runway) + else: + logging.warning(f"Unhandled departure control point: {cp.cptype}") + departure_runway = fallback_runway + self.flights.append(FlightData( client_units=clients, - departure=flight.from_cp.airport, - arrival=flight.from_cp.airport, + departure=departure_runway, + arrival=departure_runway, + # TODO: Support for divert airfields. divert=None, waypoints=flight.points, intra_flight_channel=channel @@ -477,8 +495,8 @@ class AircraftConflictGenerator: logging.warning("Pylon not found ! => Pylon" + key + " on " + str(flight.unit_type)) - def generate_flights(self, cp, country, flight_planner:FlightPlanner): - + def generate_flights(self, cp, country, flight_planner: FlightPlanner, + dynamic_runways: Dict[str, RunwayData]): # Clear pydcs parking slots if cp.airport is not None: logging.info("CLEARING SLOTS @ " + cp.airport.name) @@ -497,7 +515,8 @@ class AircraftConflictGenerator: continue logging.info("Generating flight : " + str(flight.unit_type)) group = self.generate_planned_flight(cp, country, flight) - self.setup_flight_group(group, flight, flight.flight_type) + self.setup_flight_group(group, flight, flight.flight_type, + dynamic_runways) self.setup_group_activation_trigger(flight, group) @@ -608,19 +627,13 @@ class AircraftConflictGenerator: flight.group = group return group - def setup_group_as_intercept_flight(self, group, flight): - group.points[0].ETA = 0 - group.late_activation = True - self._setup_group(group, Intercept, flight) - for point in flight.points: - group.add_waypoint(Point(point.x,point.y), point.alt) - - def setup_flight_group(self, group, flight, flight_type): + def setup_flight_group(self, group, flight, flight_type, + dynamic_runways: Dict[str, RunwayData]): if flight_type in [FlightType.CAP, FlightType.BARCAP, FlightType.TARCAP, FlightType.INTERCEPTION]: group.task = CAP.name - self._setup_group(group, CAP, flight) + self._setup_group(group, CAP, flight, dynamic_runways) # group.points[0].tasks.clear() group.points[0].tasks.clear() group.points[0].tasks.append(EngageTargets(max_distance=nm_to_meter(50), targets=[Targets.All.Air])) @@ -632,7 +645,7 @@ class AircraftConflictGenerator: elif flight_type in [FlightType.CAS, FlightType.BAI]: group.task = CAS.name - self._setup_group(group, CAS, flight) + self._setup_group(group, CAS, flight, dynamic_runways) group.points[0].tasks.clear() group.points[0].tasks.append(EngageTargets(max_distance=nm_to_meter(10), targets=[Targets.All.GroundUnits.GroundVehicles])) group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)) @@ -641,7 +654,7 @@ class AircraftConflictGenerator: group.points[0].tasks.append(OptRestrictJettison(True)) elif flight_type in [FlightType.SEAD, FlightType.DEAD]: group.task = SEAD.name - self._setup_group(group, SEAD, flight) + self._setup_group(group, SEAD, flight, dynamic_runways) group.points[0].tasks.clear() group.points[0].tasks.append(NoTask()) group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)) @@ -650,14 +663,14 @@ class AircraftConflictGenerator: group.points[0].tasks.append(OptRTBOnOutOfAmmo(OptRTBOnOutOfAmmo.Values.ASM)) elif flight_type in [FlightType.STRIKE]: group.task = PinpointStrike.name - self._setup_group(group, GroundAttack, flight) + self._setup_group(group, GroundAttack, flight, dynamic_runways) group.points[0].tasks.clear() group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)) group.points[0].tasks.append(OptROE(OptROE.Values.OpenFire)) group.points[0].tasks.append(OptRestrictJettison(True)) elif flight_type in [FlightType.ANTISHIP]: group.task = AntishipStrike.name - self._setup_group(group, AntishipStrike, flight) + self._setup_group(group, AntishipStrike, flight, dynamic_runways) group.points[0].tasks.clear() group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)) group.points[0].tasks.append(OptROE(OptROE.Values.OpenFire)) @@ -736,23 +749,3 @@ class AircraftConflictGenerator: pt.name = String(point.name) self._setup_custom_payload(flight, group) - - - def setup_group_as_antiship_flight(self, group, flight): - group.task = AntishipStrike.name - self._setup_group(group, AntishipStrike, flight) - - group.points[0].tasks.clear() - group.points[0].tasks.append(AntishipStrikeTaskAction()) - group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)) - group.points[0].tasks.append(OptROE(OptROE.Values.OpenFireWeaponFree)) - group.points[0].tasks.append(OptRestrictJettison(True)) - - for point in flight.points: - group.add_waypoint(Point(point.x, point.y), point.alt) - - - def setup_radio_preset(self, flight, group): - pass - - diff --git a/gen/airfields.py b/gen/airfields.py index 8b98668d..12680c40 100644 --- a/gen/airfields.py +++ b/gen/airfields.py @@ -4,8 +4,10 @@ Remove once https://github.com/pydcs/dcs/issues/69 tracks getting the missing data added to pydcs. Until then, missing data can be manually filled in here. """ from dataclasses import dataclass, field +import logging from typing import Dict, Optional, Tuple +from pydcs.dcs.terrain.terrain import Airport from .radios import MHz, RadioFrequency from .tacan import TacanBand, TacanChannel @@ -637,3 +639,39 @@ AIRFIELD_DATA = { atc=AtcData(MHz(3, 775), MHz(118, 50), MHz(38, 450), MHz(250, 50)), ), } + + +@dataclass +class RunwayData: + airfield_name: str + runway_name: str + atc: Optional[RadioFrequency] = None + tacan: Optional[TacanChannel] = None + ils: Optional[RadioFrequency] = None + icls: Optional[int] = None + + @classmethod + def for_airfield(cls, airport: Airport, runway: str) -> "RunwayData": + """Creates RunwayData for the given runway of an airfield. + + Args: + airport: The airfield the runway belongs to. + runway: Identifier of the runway to use. e.g. "030" or "200L". + """ + atc: Optional[RadioFrequency] = None + tacan: Optional[TacanChannel] = None + ils: Optional[RadioFrequency] = None + try: + airfield = AIRFIELD_DATA[airport.name] + atc = airfield.atc.uhf + tacan = airfield.tacan + ils = airfield.ils_freq(runway) + except KeyError: + logging.warning(f"No airfield data for {airport.name}") + return cls( + airport.name, + runway, + atc, + tacan, + ils + ) diff --git a/gen/groundobjectsgen.py b/gen/groundobjectsgen.py index 89a09293..6a12579e 100644 --- a/gen/groundobjectsgen.py +++ b/gen/groundobjectsgen.py @@ -1,13 +1,19 @@ -import logging - -from game import db from game.data.building_data import FORTIFICATION_UNITS_ID, FORTIFICATION_UNITS from game.db import unit_type_from_name +from pydcs.dcs.mission import * +from pydcs.dcs.statics import * +from pydcs.dcs.task import ( + ActivateBeaconCommand, + ActivateICLSCommand, + OptAlarmState, +) +from pydcs.dcs.unit import Ship, Vehicle +from pydcs.dcs.unitgroup import StaticGroup +from .airfields import RunwayData from .conflictgen import * from .naming import * - -from dcs.mission import * -from dcs.statics import * +from .radios import RadioRegistry +from .tacan import TacanBand, TacanRegistry FARP_FRONTLINE_DISTANCE = 10000 AA_CP_MIN_DISTANCE = 40000 @@ -16,10 +22,15 @@ AA_CP_MIN_DISTANCE = 40000 class GroundObjectsGenerator: FARP_CAPACITY = 4 - def __init__(self, mission: Mission, conflict: Conflict, game): + def __init__(self, mission: Mission, conflict: Conflict, game, + radio_registry: RadioRegistry, tacan_registry: TacanRegistry): self.m = mission self.conflict = conflict self.game = game + self.radio_registry = radio_registry + self.tacan_registry = tacan_registry + self.icls_alloc = iter(range(1, 21)) + self.runways: Dict[str, RunwayData] = {} def generate_farps(self, number_of_units=1) -> typing.Collection[StaticGroup]: if self.conflict.is_vector: @@ -103,6 +114,8 @@ class GroundObjectsGenerator: utype = db.upgrade_to_supercarrier(utype, cp.name) sg = self.m.ship_group(side, g.name, utype, position=g.position, heading=g.units[0].heading) + atc_channel = self.radio_registry.alloc_uhf() + sg.set_frequency(atc_channel.hertz) sg.units[0].name = self.m.string(g.units[0].name) for i, u in enumerate(g.units): @@ -111,6 +124,8 @@ class GroundObjectsGenerator: ship.position.x = u.position.x ship.position.y = u.position.y ship.heading = u.heading + # TODO: Verify. + ship.set_frequency(atc_channel.hertz) sg.add_unit(ship) # Find carrier direction (In the wind) @@ -125,10 +140,57 @@ class GroundObjectsGenerator: attempt = attempt + 1 # Set UP TACAN and ICLS - modeChannel = "X" if not cp.tacanY else "Y" - sg.points[0].tasks.append(ActivateBeaconCommand(channel=cp.tacanN, modechannel=modeChannel, callsign=cp.tacanI, unit_id=sg.units[0].id, aa=False)) - if ground_object.dcs_identifier == "CARRIER" and hasattr(cp, "icls"): - sg.points[0].tasks.append(ActivateICLSCommand(cp.icls, unit_id=sg.units[0].id)) + tacan = self.tacan_registry.alloc_for_band(TacanBand.X) + icls_channel = next(self.icls_alloc) + # TODO: Assign these properly. + if ground_object.dcs_identifier == "CARRIER": + tacan_callsign = random.choice([ + "STE", + "CVN", + "CVH", + "CCV", + "ACC", + "ARC", + "GER", + "ABR", + "LIN", + "TRU", + ]) + else: + tacan_callsign = random.choice([ + "LHD", + "LHA", + "LHB", + "LHC", + "LHD", + "LDS", + ]) + sg.points[0].tasks.append(ActivateBeaconCommand( + channel=tacan.number, + modechannel=tacan.band.value, + callsign=tacan_callsign, + unit_id=sg.units[0].id, + aa=False + )) + sg.points[0].tasks.append(ActivateICLSCommand( + icls_channel, + unit_id=sg.units[0].id + )) + # TODO: Make unit name usable. + # This relies on one control point mapping exactly + # to one LHA, carrier, or other usable "runway". + # This isn't wholly true, since the DD escorts of + # the carrier group are valid for helicopters, but + # they aren't exposed as such to the game. Should + # clean this up so that's possible. We can't use the + # unit name since it's an arbitrary ID. + self.runways[cp.name] = RunwayData( + cp.name, + "N/A", + atc=atc_channel, + tacan=tacan, + icls=icls_channel, + ) else: diff --git a/gen/kneeboard.py b/gen/kneeboard.py index 4c652757..bb7bb4da 100644 --- a/gen/kneeboard.py +++ b/gen/kneeboard.py @@ -31,11 +31,10 @@ from PIL import Image, ImageDraw, ImageFont from tabulate import tabulate from pydcs.dcs.mission import Mission -from pydcs.dcs.terrain.terrain import Airport from pydcs.dcs.unittype import FlyingType from . import units from .aircraft import FlightData -from .airfields import AIRFIELD_DATA +from .airfields import RunwayData from .airsupportgen import AwacsInfo, TankerInfo from .radios import RadioFrequency @@ -135,7 +134,7 @@ class BriefingPage(KneeboardPage): self.airfield_info_row("Departure", self.flight.departure), self.airfield_info_row("Arrival", self.flight.arrival), self.airfield_info_row("Divert", self.flight.divert), - ], headers=["", "Airbase", "ATC", "TCN", "ILS", "RWY"]) + ], headers=["", "Airbase", "ATC", "TCN", "I(C)LS", "RWY"]) writer.heading("Flight Plan") flight_plan = [] @@ -176,41 +175,30 @@ class BriefingPage(KneeboardPage): writer.write(path) def airfield_info_row(self, row_title: str, - airfield: Optional[Airport]) -> List[str]: + runway: Optional[RunwayData]) -> List[str]: """Creates a table row for a given airfield. Args: row_title: Purpose of the airfield. e.g. "Departure", "Arrival" or "Divert". - airfield: The airfield described by this row. + runway: The runway described by this row. Returns: A list of strings to be used as a row of the airfield table. """ - if airfield is None: + if runway is None: return [row_title, "", "", "", "", ""] - # TODO: Implement logic for picking preferred runway. - runway = airfield.runways[0] - runway_side = ["", "L", "R"][runway.leftright] - runway_text = f"{runway.heading}{runway_side}" - - try: - extra_data = AIRFIELD_DATA[airfield.name] - atc = self.format_frequency(extra_data.atc.uhf) - tacan = extra_data.tacan or "" - ils = extra_data.ils_freq(runway) or "" - except KeyError: - atc = "" - ils = "" - tacan = "" + atc = "" + if runway.atc is not None: + atc = self.format_frequency(runway.atc) return [ row_title, - airfield.name, + runway.airfield_name, atc, - tacan, - ils, - runway_text, + runway.tacan or "", + runway.ils or runway.icls or "", + runway.runway_name, ] def format_frequency(self, frequency: RadioFrequency) -> str: diff --git a/resources/tools/generate_loadout_check.py b/resources/tools/generate_loadout_check.py index ae299a0f..165da58f 100644 --- a/resources/tools/generate_loadout_check.py +++ b/resources/tools/generate_loadout_check.py @@ -30,6 +30,6 @@ for t, uts in db.UNIT_BY_TASK.items(): altitude=10000 ) g.task = t.name - airgen._setup_group(g, t, 0) + airgen._setup_group(g, t, 0, {}) mis.save("loadout_test.miz") diff --git a/theater/controlpoint.py b/theater/controlpoint.py index 96b6605c..d7a726e7 100644 --- a/theater/controlpoint.py +++ b/theater/controlpoint.py @@ -27,7 +27,6 @@ class ControlPoint: full_name = None # type: str base = None # type: theater.base.Base at = None # type: db.StartPosition - icls = 1 allow_sea_units = True connected_points = None # type: typing.List[ControlPoint] @@ -38,7 +37,6 @@ class ControlPoint: frontline_offset = 0.0 cptype: ControlPointType = None - ICLS_counter = 1 alt = 0 def __init__(self, id: int, name: str, position: Point, at, radials: typing.Collection[int], size: int, importance: float, @@ -63,10 +61,6 @@ class ControlPoint: self.base = theater.base.Base() self.cptype = cptype self.stances = {} - self.tacanY = False - self.tacanN = None - self.tacanI = "TAC" - self.icls = 0 self.airport = None @classmethod @@ -81,11 +75,6 @@ class ControlPoint: import theater.conflicttheater cp = cls(id, name, at, at, theater.conflicttheater.LAND, theater.conflicttheater.SIZE_SMALL, 1, has_frontline=False, cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP) - cp.tacanY = False - cp.tacanN = random.randint(26, 49) - cp.tacanI = random.choice(["STE", "CVN", "CVH", "CCV", "ACC", "ARC", "GER", "ABR", "LIN", "TRU"]) - ControlPoint.ICLS_counter = ControlPoint.ICLS_counter + 1 - cp.icls = ControlPoint.ICLS_counter return cp @classmethod @@ -93,9 +82,6 @@ class ControlPoint: import theater.conflicttheater cp = cls(id, name, at, at, theater.conflicttheater.LAND, theater.conflicttheater.SIZE_SMALL, 1, has_frontline=False, cptype=ControlPointType.LHA_GROUP) - cp.tacanY = False - cp.tacanN = random.randint(1,25) - cp.tacanI = random.choice(["LHD", "LHA", "LHB", "LHC", "LHD", "LDS"]) return cp @property From b31e186d1d70430e42ab705a2a6e9e6b9ecc7344 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Wed, 2 Sep 2020 17:45:14 -0700 Subject: [PATCH 8/9] Fix inconsistent runway numbering. pydcs gives us a 3-digit runway, but most of our data is 2-digit runway numbers, so we weren't finding any runways for those airfields. --- gen/aircraft.py | 3 ++- gen/airfields.py | 42 +++++++++++++++++++++--------------------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/gen/aircraft.py b/gen/aircraft.py index 941bcd38..6e0c450d 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -326,8 +326,9 @@ class AircraftConflictGenerator: if cp.cptype == ControlPointType.AIRBASE: # TODO: Implement logic for picking preferred runway. runway = flight.from_cp.airport.runways[0] + runway_number = runway.heading // 10 runway_side = ["", "L", "R"][runway.leftright] - runway_name = f"{runway.heading}{runway_side}" + runway_name = f"{runway_number:02}{runway_side}" departure_runway = RunwayData.for_airfield( flight.from_cp.airport, runway_name) elif cp.is_fleet: diff --git a/gen/airfields.py b/gen/airfields.py index 12680c40..fcd71760 100644 --- a/gen/airfields.py +++ b/gen/airfields.py @@ -96,13 +96,13 @@ AIRFIELD_DATA = { tacan_callsign="KBL", atc=AtcData(MHz(4, 350), MHz(133, 0), MHz(40, 800), MHz(262, 0)), ils={ - "7": ("IKB", MHz(111, 50)), + "07": ("IKB", MHz(111, 50)), }, outer_ndb={ - "7": ("KT", MHz(870, 0)), + "07": ("KT", MHz(870, 0)), }, inner_ndb={ - "7": ("T", MHz(490, 0)), + "07": ("T", MHz(490, 0)), }, ), @@ -115,13 +115,13 @@ AIRFIELD_DATA = { tacan_callsign="TSK", atc=AtcData(MHz(4, 300), MHz(132, 0), MHz(40, 600), MHz(261, 0)), ils={ - "9": ("ITS", MHz(108, 90)), + "09": ("ITS", MHz(108, 90)), }, outer_ndb={ - "9": ("BI", MHz(335, 0)), + "09": ("BI", MHz(335, 0)), }, inner_ndb={ - "9": ("I", MHz(688, 0)), + "09": ("I", MHz(688, 0)), }, ), @@ -134,7 +134,7 @@ AIRFIELD_DATA = { tacan_callsign="KTS", atc=AtcData(MHz(4, 400), MHz(134, 0), MHz(41, 0), MHz(263, 0)), ils={ - "8": ("IKS", MHz(109, 75)), + "08": ("IKS", MHz(109, 75)), }, ), @@ -167,7 +167,7 @@ AIRFIELD_DATA = { runway_length=9686, atc=AtcData(MHz(4, 50), MHz(127, 0), MHz(39, 600), MHz(256, 0)), ils={ - "6": ("ISO", MHz(111, 10)), + "06": ("ISO", MHz(111, 10)), }, ), @@ -210,15 +210,15 @@ AIRFIELD_DATA = { rsbn=("KW", 28), atc=AtcData(MHz(3, 900), MHz(124, 0), MHz(39, 0), MHz(253, 0)), prmg={ - "4": ("OX", 26), + "04": ("OX", 26), "22": ("KW", 26), }, outer_ndb={ - "4": ("OX", MHz(408, 0)), + "04": ("OX", MHz(408, 0)), "22": ("KW", MHz(408, 0)), }, inner_ndb={ - "4": ("O", MHz(803, 0)), + "04": ("O", MHz(803, 0)), "22": ("K", MHz(803, 0)), }, ), @@ -231,14 +231,14 @@ AIRFIELD_DATA = { rsbn=("MB", 40), atc=AtcData(MHz(3, 800), MHz(122, 0), MHz(38, 600), MHz(251, 0)), prmg={ - "9": ("MB", 38), + "09": ("MB", 38), }, outer_ndb={ - "9": ("MB", MHz(625, 0)), + "09": ("MB", MHz(625, 0)), "27": ("OC", MHz(625, 0)), }, inner_ndb={ - "9": ("M", MHz(303, 0)), + "09": ("M", MHz(303, 0)), "27": ("C", MHz(303, 0)), }, ), @@ -252,11 +252,11 @@ AIRFIELD_DATA = { atc=AtcData(MHz(4, 100), MHz(128, 0), MHz(39, 800), MHz(257, 0)), outer_ndb={ "23": ("LD", MHz(493, 0)), - "5": ("KR", MHz(493, 0)), + "05": ("KR", MHz(493, 0)), }, inner_ndb={ "23": ("L", MHz(240, 0)), - "5": ("K", MHz(240, 0)), + "05": ("K", MHz(240, 0)), }, ), @@ -268,10 +268,10 @@ AIRFIELD_DATA = { rsbn=("DG", 34), atc=AtcData(MHz(3, 950), MHz(125, 0), MHz(39, 200), MHz(254, 0)), prmg={ - "4": ("DG", 36), + "04": ("DG", 36), }, outer_ndb={ - "4": ("DG", MHz(289, 0)), + "04": ("DG", MHz(289, 0)), "22": ("RK", MHz(289, 0)), }, inner_ndb={ @@ -415,8 +415,8 @@ AIRFIELD_DATA = { vor=("DAN", MHz(108, 400)), atc=AtcData(MHz(3, 850), MHz(38, 600), MHz(129, 400), MHz(360, 100)), ils={ - "050": ("IDAN", MHz(109, 300)), - "230": ("DANM", MHz(111, 700)), + "50": ("IDAN", MHz(109, 300)), + "23": ("DANM", MHz(111, 700)), }, ), @@ -656,7 +656,7 @@ class RunwayData: Args: airport: The airfield the runway belongs to. - runway: Identifier of the runway to use. e.g. "030" or "200L". + runway: Identifier of the runway to use. e.g. "03" or "20L". """ atc: Optional[RadioFrequency] = None tacan: Optional[TacanChannel] = None From 9d31c478d3e9cef4ce47bc5f5550d3689e688c17 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Fri, 4 Sep 2020 00:35:58 -0700 Subject: [PATCH 9/9] Fix briefing generation. I removed the nav target info from the briefing because that doesn't seem to have been doing what it was intended to do. It didn't give any actual target information, all it would show was (example is a JF-17 strike mission): PP1 PP2 PP3 PP4 Without any additional context that doesn't seem too helpful to me. I'll be following up (hopefully) shortly by adding target information (type, coordinates, STPT/PP, etc) to both the briefing and the kneeboard that will cover that. Refactor a bunch to share some code with the kneeboard generator as well. --- game/operation/frontlineattack.py | 2 - game/operation/operation.py | 30 ++-- gen/aircraft.py | 46 +++++-- gen/airfields.py | 4 +- gen/briefinggen.py | 220 +++++++++++++++++++----------- gen/groundobjectsgen.py | 1 + gen/kneeboard.py | 69 +--------- 7 files changed, 207 insertions(+), 165 deletions(-) diff --git a/game/operation/frontlineattack.py b/game/operation/frontlineattack.py index 902ff6c9..48c5965c 100644 --- a/game/operation/frontlineattack.py +++ b/game/operation/frontlineattack.py @@ -36,6 +36,4 @@ class FrontlineAttackOperation(Operation): def generate(self): self.briefinggen.title = "Frontline CAS" self.briefinggen.description = "Provide CAS for the ground forces attacking enemy lines. Operation will be considered successful if total number of enemy units will be lower than your own by a factor of 1.5 (i.e. with 12 units from both sides, enemy forces need to be reduced to at least 8), meaning that you (and, probably, your wingmans) should concentrate on destroying the enemy units. Target base strength will be lowered as a result. Be advised that your flight will not attack anything until you explicitly tell them so by comms menu." - self.briefinggen.append_waypoint("CAS AREA IP") - self.briefinggen.append_waypoint("CAS AREA EGRESS") super(FrontlineAttackOperation, self).generate() diff --git a/game/operation/operation.py b/game/operation/operation.py index 051df639..f87c13e3 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -266,28 +266,34 @@ class Operation: load_dcs_libe.add_action(DoScript(String(script))) self.current_mission.triggerrules.triggers.append(load_dcs_libe) + self.assign_channels_to_flights() + kneeboard_generator = KneeboardGenerator(self.current_mission) - # Briefing Generation + for dynamic_runway in self.groundobjectgen.runways.values(): + self.briefinggen.add_dynamic_runway(dynamic_runway) + for tanker in self.airsupportgen.air_support.tankers: - self.briefinggen.append_frequency( - f"Tanker {tanker.callsign} ({tanker.variant})", - f"{tanker.tacan}/{tanker.freq}") + self.briefinggen.add_tanker(tanker) kneeboard_generator.add_tanker(tanker) if self.is_awacs_enabled: for awacs in self.airsupportgen.air_support.awacs: - self.briefinggen.append_frequency(awacs.callsign, awacs.freq) + self.briefinggen.add_awacs(awacs) kneeboard_generator.add_awacs(awacs) - self.assign_channels_to_flights() - - # Generate the briefing - self.briefinggen.generate() - for region, code, name in self.game.jtacs: - kneeboard_generator.add_jtac(name, region, code) - kneeboard_generator.generate(self.airgen.flights) + # TODO: Radio info? Type? + jtac = JtacInfo(name, region, code) + self.briefinggen.add_jtac(jtac) + kneeboard_generator.add_jtac(jtac) + + for flight in self.airgen.flights: + self.briefinggen.add_flight(flight) + kneeboard_generator.add_flight(flight) + + self.briefinggen.generate() + kneeboard_generator.generate() def assign_channels_to_flights(self) -> None: """Assigns preset radio channels for client flights.""" diff --git a/gen/aircraft.py b/gen/aircraft.py index 6e0c450d..02451ab5 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -148,8 +148,19 @@ class ChannelAssignment: class FlightData: """Details of a planned flight.""" - #: List of playable units in the flight. - client_units: List[FlyingUnit] + flight_type: FlightType + + #: All units in the flight. + units: List[FlyingUnit] + + #: Total number of aircraft in the flight. + size: int + + #: True if this flight belongs to the player's coalition. + friendly: bool + + #: Number of minutes after mission start the flight is set to depart. + departure_delay: int #: Arrival airport. arrival: RunwayData @@ -169,13 +180,18 @@ class FlightData: #: Map of radio frequencies to their assigned radio and channel, if any. frequency_to_channel_map: Dict[RadioFrequency, ChannelAssignment] - def __init__(self, client_units: List[FlyingUnit], arrival: RunwayData, - departure: RunwayData, divert: Optional[RunwayData], - waypoints: List[FlightWaypoint], + def __init__(self, flight_type: FlightType, units: List[FlyingUnit], + size: int, friendly: bool, departure_delay: int, + departure: RunwayData, arrival: RunwayData, + divert: Optional[RunwayData], waypoints: List[FlightWaypoint], intra_flight_channel: RadioFrequency) -> None: - self.client_units = client_units - self.arrival = arrival + self.flight_type = flight_type + self.units = units + self.size = size + self.friendly = friendly + self.departure_delay = departure_delay self.departure = departure + self.arrival = arrival self.divert = divert self.waypoints = waypoints self.intra_flight_channel = intra_flight_channel @@ -183,6 +199,11 @@ class FlightData: self.assign_intra_flight_channel() + @property + def client_units(self) -> List[FlyingUnit]: + """List of playable units in the flight.""" + return [u for u in self.units if u.is_human()] + def assign_intra_flight_channel(self) -> None: """Assigns a channel to the intra-flight frequency.""" if not self.client_units: @@ -200,10 +221,11 @@ class FlightData: @property def aircraft_type(self) -> FlyingType: """Returns the type of aircraft in this flight.""" - return self.client_units[0].unit_type + return self.units[0].unit_type def num_radio_channels(self, radio_id: int) -> int: """Returns the number of preset channels for the given radio.""" + # Note: pydcs only initializes the radio presets for client slots. return self.client_units[0].num_radio_channels(radio_id) def channel_for( @@ -296,11 +318,9 @@ class AircraftConflictGenerator: for unit_instance in group.units: unit_instance.livery_id = db.PLANE_LIVERY_OVERRIDES[unit_type] - clients: List[FlyingUnit] = [] single_client = flight.client_count == 1 for idx in range(0, min(len(group.units), flight.client_count)): unit = group.units[idx] - clients.append(unit) if single_client: unit.set_player() else: @@ -338,7 +358,11 @@ class AircraftConflictGenerator: departure_runway = fallback_runway self.flights.append(FlightData( - client_units=clients, + flight_type=flight.flight_type, + units=group.units, + size=len(group.units), + friendly=flight.from_cp.captured, + departure_delay=flight.scheduled_in, departure=departure_runway, arrival=departure_runway, # TODO: Support for divert airfields. diff --git a/gen/airfields.py b/gen/airfields.py index fcd71760..e4c5f1fb 100644 --- a/gen/airfields.py +++ b/gen/airfields.py @@ -641,12 +641,13 @@ AIRFIELD_DATA = { } -@dataclass +@dataclass(frozen=True) class RunwayData: airfield_name: str runway_name: str atc: Optional[RadioFrequency] = None tacan: Optional[TacanChannel] = None + tacan_callsign: Optional[str] = None ils: Optional[RadioFrequency] = None icls: Optional[int] = None @@ -665,6 +666,7 @@ class RunwayData: airfield = AIRFIELD_DATA[airport.name] atc = airfield.atc.uhf tacan = airfield.tacan + tacan = airfield.tacan_callsign ils = airfield.ils_freq(runway) except KeyError: logging.warning(f"No airfield data for {airport.name}") diff --git a/gen/briefinggen.py b/gen/briefinggen.py index c3e10931..9eed3af2 100644 --- a/gen/briefinggen.py +++ b/gen/briefinggen.py @@ -1,68 +1,136 @@ -import logging +import os +from collections import defaultdict +from dataclasses import dataclass +import random +from typing import List, Tuple from game import db -from .conflictgen import * -from .naming import * - -from dcs.mission import * +from pydcs.dcs.mission import Mission +from .aircraft import FlightData +from .airfields import RunwayData +from .airsupportgen import AwacsInfo, TankerInfo +from .conflictgen import Conflict +from .ground_forces.combat_stance import CombatStance +from .radios import RadioFrequency -class BriefingGenerator: - freqs = None # type: typing.List[typing.Tuple[str, str]] - title = "" # type: str - description = "" # type: str - targets = None # type: typing.List[typing.Tuple[str, str]] - waypoints = None # type: typing.List[str] +@dataclass +class CommInfo: + """Communications information for the kneeboard.""" + name: str + freq: RadioFrequency + + +@dataclass +class JtacInfo: + """JTAC information for the kneeboard.""" + callsign: str + region: str + code: str + + +class MissionInfoGenerator: + """Base type for generators of mission information for the player. + + Examples of subtypes include briefing generators, kneeboard generators, etc. + """ + + def __init__(self, mission: Mission) -> None: + self.mission = mission + self.awacs: List[AwacsInfo] = [] + self.comms: List[CommInfo] = [] + self.flights: List[FlightData] = [] + self.jtacs: List[JtacInfo] = [] + self.tankers: List[TankerInfo] = [] + + def add_awacs(self, awacs: AwacsInfo) -> None: + """Adds an AWACS/GCI to the mission. + + Args: + awacs: AWACS information. + """ + self.awacs.append(awacs) + + def add_comm(self, name: str, freq: RadioFrequency) -> None: + """Adds communications info to the mission. + + Args: + name: Name of the radio channel. + freq: Frequency of the radio channel. + """ + self.comms.append(CommInfo(name, freq)) + + def add_flight(self, flight: FlightData) -> None: + """Adds flight info to the mission. + + Args: + flight: Flight information. + """ + self.flights.append(flight) + + def add_jtac(self, jtac: JtacInfo) -> None: + """Adds a JTAC to the mission. + + Args: + jtac: JTAC information. + """ + self.jtacs.append(jtac) + + def add_tanker(self, tanker: TankerInfo) -> None: + """Adds a tanker to the mission. + + Args: + tanker: Tanker information. + """ + self.tankers.append(tanker) + + def generate(self) -> None: + """Generates the mission information.""" + raise NotImplementedError + + +class BriefingGenerator(MissionInfoGenerator): def __init__(self, mission: Mission, conflict: Conflict, game): - self.m = mission + super().__init__(mission) self.conflict = conflict self.game = game + self.title = "" self.description = "" + self.dynamic_runways: List[RunwayData] = [] - self.freqs = [] - self.targets = [] - self.waypoints = [] + def add_dynamic_runway(self, runway: RunwayData) -> None: + """Adds a dynamically generated runway to the briefing. - self.jtacs = [] + Dynamic runways are any valid landing point that is a unit rather than a + map feature. These include carriers, ships with a helipad, and FARPs. + """ + self.dynamic_runways.append(runway) - def append_frequency(self, name: str, frequency: str): - self.freqs.append((name, frequency)) + def add_flight_description(self, flight: FlightData): + assert flight.client_units - def append_target(self, description: str, markpoint: str = None): - self.targets.append((description, markpoint)) - - def append_waypoint(self, description: str): - self.waypoints.append(description) - - def add_flight_description(self, flight): - - if flight.client_count <= 0: - return - - flight_unit_name = db.unit_type_name(flight.unit_type) + aircraft = flight.aircraft_type + flight_unit_name = db.unit_type_name(aircraft) self.description += "-" * 50 + "\n" - self.description += flight_unit_name + " x " + str(flight.count) + 2 * "\n" + self.description += f"{flight_unit_name} x {flight.size + 2}\n\n" - self.description += "#0 -- TAKEOFF : Take off from " + flight.from_cp.name + "\n" - for i, wpt in enumerate(flight.points): - self.description += "#" + str(1+i) + " -- " + wpt.name + " : " + wpt.description + "\n" - self.description += "#" + str(len(flight.points) + 1) + " -- RTB\n\n" + departure = flight.departure.airfield_name + self.description += f"#0 -- TAKEOFF : Take off from {departure}\n" + for i, wpt in enumerate(flight.waypoints): + self.description += f"#{i + 1} -- {wpt.name} : {wpt.description}\n" + self.description += f"#{len(flight.waypoints) + 1} -- RTB\n\n" - group = flight.group - if group is not None: - for i, nav_target in enumerate(group.nav_target_points): - self.description += nav_target.text_comment + "\n" - self.description += "\n" - self.description += "-" * 50 + "\n" - - def add_ally_flight_description(self, flight): - if flight.client_count == 0: - flight_unit_name = db.unit_type_name(flight.unit_type) - self.description += flight.flight_type.name + " " + flight_unit_name + " x " + str(flight.count) + ", departing in " + str(flight.scheduled_in) + " minutes \n" + def add_ally_flight_description(self, flight: FlightData): + assert not flight.client_units + aircraft = flight.aircraft_type + flight_unit_name = db.unit_type_name(aircraft) + self.description += ( + f"{flight.flight_type.name} {flight_unit_name} x {flight.size}, " + f"departing in {flight.departure_delay} minutes\n" + ) def generate(self): - self.description = "" self.description += "DCS Liberation turn #" + str(self.game.turn) + "\n" @@ -74,52 +142,50 @@ class BriefingGenerator: self.description += "Your flights:" + "\n" self.description += "=" * 15 + "\n\n" - for planner in self.game.planners.values(): - for flight in planner.flights: + for flight in self.flights: + if flight.client_units: self.add_flight_description(flight) self.description += "\n"*2 self.description += "Planned ally flights:" + "\n" self.description += "=" * 15 + "\n" - for planner in self.game.planners.values(): - if planner.from_cp.captured and len(planner.flights) > 0: - self.description += "\nFrom " + planner.from_cp.full_name + " \n" - self.description += "-" * 50 + "\n\n" - for flight in planner.flights: - self.add_ally_flight_description(flight) + allied_flights_by_departure = defaultdict(list) + for flight in self.flights: + if not flight.client_units and flight.friendly: + name = flight.departure.airfield_name + allied_flights_by_departure[name].append(flight) + for departure, flights in allied_flights_by_departure.items(): + self.description += f"\nFrom {departure}\n" + self.description += "-" * 50 + "\n\n" + for flight in flights: + self.add_ally_flight_description(flight) - if self.freqs: + if self.comms: self.description += "\n\nComms Frequencies:\n" self.description += "=" * 15 + "\n" - for name, freq in self.freqs: - self.description += "{}: {}\n".format(name, freq) + for comm_info in self.comms: + self.description += f"{comm_info.name}: {comm_info.freq}\n" self.description += ("-" * 50) + "\n" - for cp in self.game.theater.controlpoints: - if cp.captured and cp.cptype in [ControlPointType.LHA_GROUP, ControlPointType.AIRCRAFT_CARRIER_GROUP]: - self.description += cp.name + "\n" - self.description += "RADIO : 127.5 Mhz AM\n" - self.description += "TACAN : " - self.description += str(cp.tacanN) - if cp.tacanY: - self.description += "Y" - else: - self.description += "X" - self.description += " " + str(cp.tacanI) + "\n" - - if cp.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP and hasattr(cp, "icls"): - self.description += "ICLS Channel : " + str(cp.icls) + "\n" - self.description += "-" * 50 + "\n" + for runway in self.dynamic_runways: + self.description += f"{runway.airfield_name}\n" + self.description += f"RADIO : {runway.atc}\n" + if runway.tacan is not None: + self.description += f"TACAN : {runway.tacan} {runway.tacan_callsign}\n" + if runway.icls is not None: + self.description += f"ICLS Channel : {runway.icls}\n" + self.description += "-" * 50 + "\n" self.description += "JTACS [F-10 Menu] : \n" self.description += "===================\n\n" - for jtac in self.game.jtacs: - self.description += str(jtac[0]) + " -- Code : " + str(jtac[1]) + "\n" + for jtac in self.jtacs: + self.description += f"{jtac.region} -- Code : {jtac.code}\n" - self.m.set_description_text(self.description) + self.mission.set_description_text(self.description) - self.m.add_picture_blue(os.path.abspath("./resources/ui/splash_screen.png")) + self.mission.add_picture_blue(os.path.abspath( + "./resources/ui/splash_screen.png")) def generate_ongoing_war_text(self): diff --git a/gen/groundobjectsgen.py b/gen/groundobjectsgen.py index 6a12579e..3163aa3b 100644 --- a/gen/groundobjectsgen.py +++ b/gen/groundobjectsgen.py @@ -189,6 +189,7 @@ class GroundObjectsGenerator: "N/A", atc=atc_channel, tacan=tacan, + tacan_callsign=tacan_callsign, icls=icls_channel, ) diff --git a/gen/kneeboard.py b/gen/kneeboard.py index bb7bb4da..8383b392 100644 --- a/gen/kneeboard.py +++ b/gen/kneeboard.py @@ -23,7 +23,6 @@ only be added per airframe, so PvP missions where each side have the same aircraft will be able to see the enemy's kneeboard for the same airframe. """ from collections import defaultdict -from dataclasses import dataclass from pathlib import Path from typing import Dict, List, Optional, Tuple @@ -36,6 +35,7 @@ from . import units from .aircraft import FlightData from .airfields import RunwayData from .airsupportgen import AwacsInfo, TankerInfo +from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator from .radios import RadioFrequency @@ -95,21 +95,6 @@ class KneeboardPage: raise NotImplementedError -@dataclass -class CommInfo: - """Communications information for the kneeboard.""" - name: str - freq: RadioFrequency - - -@dataclass -class JtacInfo: - """JTAC information for the kneeboard.""" - callsign: str - region: str - code: str - - class BriefingPage(KneeboardPage): """A kneeboard page containing briefing information.""" def __init__(self, flight: FlightData, comms: List[CommInfo], @@ -208,57 +193,17 @@ class BriefingPage(KneeboardPage): return f"{channel.radio_name} Ch {channel.channel}" -class KneeboardGenerator: +class KneeboardGenerator(MissionInfoGenerator): """Creates kneeboard pages for each client flight in the mission.""" def __init__(self, mission: Mission) -> None: - self.mission = mission - self.comms: List[CommInfo] = [] - self.awacs: List[AwacsInfo] = [] - self.tankers: List[TankerInfo] = [] - self.jtacs: List[JtacInfo] = [] + super().__init__(mission) - def add_comm(self, name: str, freq: RadioFrequency) -> None: - """Adds communications info to the kneeboard. - - Args: - name: Name of the radio channel. - freq: Frequency of the radio channel. - """ - self.comms.append(CommInfo(name, freq)) - - def add_awacs(self, awacs: AwacsInfo) -> None: - """Adds an AWACS/GCI to the kneeboard. - - Args: - awacs: AWACS information. - """ - self.awacs.append(awacs) - - def add_tanker(self, tanker: TankerInfo) -> None: - """Adds a tanker to the kneeboard. - - Args: - tanker: Tanker information. - """ - self.tankers.append(tanker) - - def add_jtac(self, callsign: str, region: str, code: str) -> None: - """Adds a JTAC to the kneeboard. - - Args: - callsign: Callsign of the JTAC. - region: JTAC's area of responsibility. - code: Laser code used by the JTAC. - """ - # TODO: Radio info? Type? - self.jtacs.append(JtacInfo(callsign, region, code)) - - def generate(self, flights: List[FlightData]) -> None: + def generate(self) -> None: """Generates a kneeboard per client flight.""" temp_dir = Path("kneeboards") temp_dir.mkdir(exist_ok=True) - for aircraft, pages in self.pages_by_airframe(flights).items(): + for aircraft, pages in self.pages_by_airframe().items(): aircraft_dir = temp_dir / aircraft.id aircraft_dir.mkdir(exist_ok=True) for idx, page in enumerate(pages): @@ -266,7 +211,7 @@ class KneeboardGenerator: page.write(page_path) self.mission.add_aircraft_kneeboard(aircraft, page_path) - def pages_by_airframe(self, flights: List[FlightData]) -> Dict[FlyingType, List[KneeboardPage]]: + def pages_by_airframe(self) -> Dict[FlyingType, List[KneeboardPage]]: """Returns a list of kneeboard pages per airframe in the mission. Only client flights will be included, but because DCS does not support @@ -278,7 +223,7 @@ class KneeboardGenerator: that aircraft. """ all_flights: Dict[FlyingType, List[KneeboardPage]] = defaultdict(list) - for flight in flights: + for flight in self.flights: if not flight.client_units: continue all_flights[flight.aircraft_type].extend(