diff --git a/.gitignore b/.gitignore index bdd27ca3..68c604e4 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,9 @@ tests/** # User-specific stuff .idea/ -liberation_preferences.json +/kneeboards +/liberation_preferences.json +/state.json logs/liberation.log diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..646c8768 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,19 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Main", + "type": "python", + "request": "launch", + "program": "qt_ui\\main.py", + "console": "integratedTerminal", + "env": { + "PYTHONPATH": ".;./pydcs" + }, + "preLaunchTask": "Prepare Environment" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..d8c82a7e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python.pythonPath": "g:\\python\\dcs_liberation\\venv\\Scripts\\python.exe", + "vsintellicode.python.completionsEnabled": true +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..84bd2413 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,35 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Prepare Environment", + "type": "shell", + "isBackground": false, + "problemMatcher": [], + "command": "powershell", + "args": [ + "-Command", + "& {if (-not (Test-Path ${workspaceFolder}\\venv)) { python -m venv ${workspaceFolder}\\venv; . ${workspaceFolder}\\venv\\scripts\\activate.ps1; pip install -r requirements.txt; Copy-Item ${workspaceFolder}\\venv\\Lib\\site-packages\\shiboken2\\shiboken2.abi3.dll ${workspaceFolder}\\venv\\Lib\\site-packages\\PySide2 } }", + ], + "group": "build", + "options": { + "env": { + "PYTHONPATH": ".;./pydcs" + } + }, + "presentation": { + "echo": true, + "reveal": "never", + "focus": false, + "panel": "shared", + "showReuseMessage": true, + "clear": false + }, + "runOptions": { + "runOn": "folderOpen" + } + } + ] +} \ No newline at end of file diff --git a/changelog.md b/changelog.md index 69a72793..aa1f7abc 100644 --- a/changelog.md +++ b/changelog.md @@ -2,8 +2,23 @@ ## Features/Improvements : * **[Other]** Added an installer option (thanks to contributor parithon) -* **[Cheat Menu]** Added possibility to replace destroyed SAM and base defenses units for the player (Click on a SAM site to fix it) -* **[Cheat Menu]** Added recon images for buildings on strike targets, click on a Strike target to get detailled informations +* **[Kneeboards]** Generate mission kneeboards for player flights. Kneeboards include + airfield/carrier information (ATC frequencies, ILS, TACAN, and runway + assignments), assigned radio channels, waypoint lists, and AWACS/JTAC/tanker + information. +* **[Radios]** Allocate separate intra-flight channels for most aircraft to reduce global + chatter. +* **[Radios]** Configure radio channel presets for most aircraft. Currently supported are: + * AJS37 + * AV-8B + * F-14B + * F-16C + * F/A-18C + * JF-17 + * M-2000C +* **[Base Menu]** Added possibility to repair destroyed SAM and base defenses units for the player (Click on a SAM site to fix it) +* **[Base Menu]** Added possibility to buy/sell/replace SAM units +* **[Map]** Added recon images for buildings on strike targets, click on a Strike target to get detailled informations * **[Units/Factions]** Added F-16C to USA 1990 * **[Units/Factions]** Added MQ-9 Reaper as CAS unit for USA 2005 * **[Units/Factions]** Added Mig-21, Mig-23, SA-342L to Syria 2011 @@ -17,6 +32,7 @@ * **[Mission Generator]** AH-1W was not used by AI to generate CAS mission by default * **[Mission Generator]** Fixed F-16C targeting pod not being added to payload * **[Mission Generator]** AH-64A and AH-64D payloads fix. +* **[Units/Factions]** China will use KJ-2000 as awacs instead of A-50 # 2.1.0 diff --git a/game/data/building_data.py b/game/data/building_data.py index f88415ce..bd6ab666 100644 --- a/game/data/building_data.py +++ b/game/data/building_data.py @@ -1,5 +1,5 @@ import inspect -from pydcs import dcs +import dcs DEFAULT_AVAILABLE_BUILDINGS = ['fuel', 'ammo', 'comms', 'oil', 'ware', 'farp', 'fob', 'power', 'factory', 'derrick', 'aa'] diff --git a/game/db.py b/game/db.py index a4cc3e4a..02120a45 100644 --- a/game/db.py +++ b/game/db.py @@ -223,13 +223,16 @@ PRICES = { KC130: 25, A_50: 50, + KJ_2000: 50, E_3A: 50, C_130: 25, # WW2 P_51D_30_NA: 18, P_51D: 16, - P_47D_30: 18, + P_47D_30: 17, + P_47D_30bl1: 16, + P_47D_40: 18, B_17G: 30, # Drones @@ -337,15 +340,15 @@ PRICES = { AirDefence.SAM_SA_11_Buk_LN_9A310M1: 30, AirDefence.SAM_SA_8_Osa_9A33: 28, AirDefence.SAM_SA_15_Tor_9A331: 40, - AirDefence.SAM_SA_13_Strela_10M3_9A35M3: 24, - AirDefence.SAM_SA_9_Strela_1_9P31: 16, + AirDefence.SAM_SA_13_Strela_10M3_9A35M3: 16, + AirDefence.SAM_SA_9_Strela_1_9P31: 12, AirDefence.SAM_SA_11_Buk_CC_9S470M1: 25, AirDefence.SAM_SA_8_Osa_LD_9T217: 22, AirDefence.SAM_Patriot_AMG_AN_MRC_137: 35, AirDefence.SAM_Patriot_ECS_AN_MSQ_104: 30, AirDefence.SPAAA_Gepard: 24, AirDefence.SAM_Hawk_PCP: 14, - AirDefence.AAA_Vulcan_M163: 12, + AirDefence.AAA_Vulcan_M163: 10, AirDefence.SAM_Hawk_LN_M192: 8, AirDefence.SAM_Chaparral_M48: 16, AirDefence.SAM_Linebacker_M6: 18, @@ -358,7 +361,7 @@ PRICES = { AirDefence.Stinger_MANPADS: 6, AirDefence.SAM_Stinger_comm_dsr: 4, AirDefence.SAM_Stinger_comm: 4, - AirDefence.SPAAA_ZSU_23_4_Shilka: 12, + AirDefence.SPAAA_ZSU_23_4_Shilka: 10, AirDefence.AAA_ZU_23_Closed: 6, AirDefence.AAA_ZU_23_Emplacement: 6, AirDefence.AAA_ZU_23_on_Ural_375: 8, @@ -387,19 +390,19 @@ PRICES = { AirDefence.SAM_SA_2_LN_SM_90: 8, AirDefence.SAM_SA_2_TR_SNR_75_Fan_Song: 12, AirDefence.Rapier_FSA_Launcher: 6, - AirDefence.Rapier_FSA_Optical_Tracker: 12, - AirDefence.Rapier_FSA_Blindfire_Tracker: 16, + AirDefence.Rapier_FSA_Optical_Tracker: 6, + AirDefence.Rapier_FSA_Blindfire_Tracker: 8, AirDefence.HQ_7_Self_Propelled_LN: 20, AirDefence.HQ_7_Self_Propelled_STR: 24, AirDefence.AAA_8_8cm_Flak_18: 6, AirDefence.AAA_Flak_38: 6, AirDefence.AAA_8_8cm_Flak_36: 8, - AirDefence.AAA_8_8cm_Flak_37: 10, + AirDefence.AAA_8_8cm_Flak_37: 9, AirDefence.AAA_Flak_Vierling_38:6, AirDefence.AAA_Kdo_G_40: 8, AirDefence.Flak_Searchlight_37: 4, AirDefence.Maschinensatz_33: 10, - AirDefence.AAA_8_8cm_Flak_41: 12, + AirDefence.AAA_8_8cm_Flak_41: 10, AirDefence.AAA_Bofors_40mm: 8, # FRENCH PACK MOD @@ -519,6 +522,8 @@ UNIT_BY_TASK = { MiG_27K, A_20G, P_47D_30, + P_47D_30bl1, + P_47D_40, Ju_88A4, B_17G, MB_339PAN, @@ -542,7 +547,7 @@ UNIT_BY_TASK = { KC130, S_3B_Tanker, ], - AWACS: [E_3A, A_50, ], + AWACS: [E_3A, A_50, KJ_2000], PinpointStrike: [ Armor.APC_MTLB, Armor.APC_MTLB, @@ -993,6 +998,8 @@ PLANE_PAYLOAD_OVERRIDES = { Su_17M4: COMMON_OVERRIDE, F_4E: COMMON_OVERRIDE, P_47D_30:COMMON_OVERRIDE, + P_47D_30bl1:COMMON_OVERRIDE, + P_47D_40:COMMON_OVERRIDE, B_17G: COMMON_OVERRIDE, P_51D: COMMON_OVERRIDE, P_51D_30_NA: COMMON_OVERRIDE, diff --git a/game/factions/china_2010.py b/game/factions/china_2010.py index 148fdbd0..0d98d1b9 100644 --- a/game/factions/china_2010.py +++ b/game/factions/china_2010.py @@ -20,7 +20,7 @@ China_2010 = { An_30M, Yak_40, - A_50, + KJ_2000, Mi_8MT, Mi_28N, diff --git a/game/factions/sweden_1990.py b/game/factions/sweden_1990.py index 5bafb20b..ebc754f9 100644 --- a/game/factions/sweden_1990.py +++ b/game/factions/sweden_1990.py @@ -11,7 +11,7 @@ Sweden_1990 = { UH_1H, - AirDefence.SAM_Hawk_LN_M192, + AirDefence.SAM_Hawk_PCP, Armor.IFV_MCV_80, # Standing as Strf 90 Armor.MBT_Leopard_2, 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 033d3c34..f66eb124 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -1,12 +1,15 @@ -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 gen.airfields import AIRFIELD_DATA +from gen.beacons import load_beacons_for_terrain +from gen.radios import RadioRegistry +from gen.tacan import TacanRegistry +from dcs.countries import country_dict +from dcs.lua.parse import loads +from dcs.terrain.terrain import Terrain from userdata.debriefing import * -TANKER_CALLSIGNS = ["Texaco", "Arco", "Shell"] - class Operation: attackers_starting_position = None # type: db.StartingPosition @@ -25,6 +28,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,13 +68,25 @@ 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) 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): @@ -110,6 +127,30 @@ class Operation: self.defenders_starting_position = self.to_cp.at def generate(self): + # Dedup beacon/radio frequencies, since some maps have some frequencies + # used multiple times. + beacons = load_beacons_for_terrain(self.game.theater.terrain.name) + unique_map_frequencies: Set[RadioFrequency] = set() + for beacon in beacons: + unique_map_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 airfield, data in AIRFIELD_DATA.items(): + if data.theater == self.game.theater.terrain.name: + unique_map_frequencies.add(data.atc.hf) + unique_map_frequencies.add(data.atc.vhf_fm) + unique_map_frequencies.add(data.atc.vhf_am) + unique_map_frequencies.add(data.atc.uhf) + # No need to reserve ILS or TACAN because those are in the + # beacon list. + + for frequency in unique_map_frequencies: + self.radio_registry.reserve(frequency) # Generate meteo if self.environment_settings is None: @@ -151,10 +192,15 @@ 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 = [] + jtacs: List[JtacInfo] = [] for player_cp, enemy_cp in self.game.theater.conflicts(True): conflict = Conflict.frontline_cas_conflict(self.attacker_name, self.defender_name, self.current_mission.country(self.attacker_country), @@ -165,6 +211,7 @@ class Operation: enemy_gp = self.game.ground_planners[enemy_cp.id].units_per_cp[player_cp.id] groundConflictGen = GroundConflictGenerator(self.current_mission, conflict, self.game, player_gp, enemy_gp, player_cp.stances[enemy_cp.id]) groundConflictGen.generate() + jtacs.extend(groundConflictGen.jtacs) # Setup combined arms parameters self.current_mission.groundControl.pilot_can_control_vehicles = self.ca_slots > 0 @@ -205,8 +252,8 @@ class Operation: if not self.game.settings.jtac_smoke_on: smoke = "false" - for jtac in self.game.jtacs: - script = script + "\n" + "JTACAutoLase('" + str(jtac[2]) + "', " + str(jtac[1]) + ", " + smoke + ", \"vehicle\")" + "\n" + for jtac in jtacs: + script += f"\nJTACAutoLase('{jtac.unit_name}', {jtac.code}, {smoke}, 'vehicle')\n" load_autolase.add_action(DoScript(String(script))) self.current_mission.triggerrules.triggers.append(load_autolase) @@ -221,17 +268,49 @@ class Operation: load_dcs_libe.add_action(DoScript(String(script))) self.current_mission.triggerrules.triggers.append(load_dcs_libe) - # Briefing Generation - for i, tanker_type in enumerate(self.airsupportgen.generated_tankers): - self.briefinggen.append_frequency("Tanker {} ({})".format(TANKER_CALLSIGNS[i], tanker_type), "{}X/{} MHz AM".format(60+i, 130+i)) + self.assign_channels_to_flights() + + kneeboard_generator = KneeboardGenerator(self.current_mission) + + 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.add_tanker(tanker) + kneeboard_generator.add_tanker(tanker) if self.is_awacs_enabled: - self.briefinggen.append_frequency("AWACS", "233 MHz AM") + for awacs in self.airsupportgen.air_support.awacs: + self.briefinggen.add_awacs(awacs) + kneeboard_generator.add_awacs(awacs) - self.briefinggen.append_frequency("Flight", "251 MHz AM") + for jtac in jtacs: + 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) - # Generate the briefing self.briefinggen.generate() + kneeboard_generator.generate() + 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 + + aircraft_data.channel_allocator.assign_channels_for_flight( + flight, self.airsupportgen.air_support) diff --git a/gen/__init__.py b/gen/__init__.py index d910a19d..ad11614f 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -9,6 +9,7 @@ from .environmentgen import * from .groundobjectsgen import * from .briefinggen import * from .forcedoptionsgen import * +from .kneeboard import * from . import naming diff --git a/gen/aircraft.py b/gen/aircraft.py index cdcf77c9..4911c916 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -1,14 +1,28 @@ -from dcs.action import ActivateGroup, AITaskPush, MessageToCoalition, MessageToAll +from dataclasses import dataclass +from typing import Type + +from dcs import helicopters +from dcs.action import ActivateGroup, AITaskPush, MessageToAll from dcs.condition import TimeAfter, CoalitionHasAirdrome, PartOfCoalitionInZone -from dcs.helicopters import UH_1H -from dcs.terrain.terrain import NoParkingSlotError +from dcs.flyingunit import FlyingUnit +from dcs.helicopters import helicopter_map, UH_1H +from dcs.terrain.terrain import Airport, NoParkingSlotError from dcs.triggers import TriggerOnce, Event 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.airsupportgen import AirSupport +from gen.callsigns import create_group_callsign_from_unit 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 .conflictgen import * from .naming import * @@ -23,22 +37,471 @@ 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) + + +# 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 ChannelNamer: + """Base class allowing channel name customization per-aircraft. + + Most aircraft will want to customize this behavior, but the default is + reasonable for any aircraft with numbered radios. + """ + + @staticmethod + def channel_name(radio_id: int, channel_id: int) -> str: + """Returns the name of the channel for the given radio and channel.""" + return f"COMM{radio_id} Ch {channel_id}" + + +class MirageChannelNamer(ChannelNamer): + """Channel namer for the M-2000.""" + + @staticmethod + def channel_name(radio_id: int, channel_id: int) -> str: + radio_name = ["V/UHF", "UHF"][radio_id - 1] + return f"{radio_name} Ch {channel_id}" + + +class TomcatChannelNamer(ChannelNamer): + """Channel namer for the F-14.""" + + @staticmethod + def channel_name(radio_id: int, channel_id: int) -> str: + radio_name = ["UHF", "VHF/UHF"][radio_id - 1] + return f"{radio_name} Ch {channel_id}" + + +class ViggenChannelNamer(ChannelNamer): + """Channel namer for the AJS37.""" + + @staticmethod + def channel_name(radio_id: int, channel_id: int) -> str: + if channel_id >= 4: + channel_letter = "EFGH"[channel_id - 4] + return f"FR 24 {channel_letter}" + return f"FR 22 Special {channel_id}" + + +class ViperChannelNamer(ChannelNamer): + """Channel namer for the F-16.""" + + @staticmethod + def channel_name(radio_id: int, channel_id: int) -> str: + return f"COM{radio_id} Ch {channel_id}" + + +class SCR522ChannelNamer(ChannelNamer): + """ + Channel namer for P-51 & P-47D + """ + + @staticmethod + def channel_name(radio_id: int, channel_id: int) -> str: + if channel_id > 3: + return "?" + else: + return f"Button " + "ABCD"[channel_id - 1] + + +@dataclass(frozen=True) +class ChannelAssignment: + radio_id: int + channel: int + + +@dataclass +class FlightData: + """Details of a planned flight.""" + + 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 + + #: Departure airport. + departure: RunwayData + + #: Diver airport. + divert: Optional[RunwayData] + + #: 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, 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.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 + self.frequency_to_channel_map = {} + self.callsign = create_group_callsign_from_unit(self.units[0]) + + @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()] + + @property + def aircraft_type(self) -> FlyingType: + """Returns the type of aircraft in this flight.""" + 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( + 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 RadioChannelAllocator: + """Base class for radio channel allocators.""" + + def assign_channels_for_flight(self, flight: FlightData, + air_support: AirSupport) -> None: + """Assigns mission frequencies to preset channels for the flight.""" + raise NotImplementedError + + +@dataclass(frozen=True) +class CommonRadioChannelAllocator(RadioChannelAllocator): + """Radio channel allocator suitable for most aircraft. + + Most of the aircraft with preset channels available have one or more radios + with 20 or more channels available (typically per-radio, but this is not the + case for the JF-17). + """ + + #: 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] + + def assign_channels_for_flight(self, flight: FlightData, + air_support: AirSupport) -> None: + flight.assign_channel( + self.intra_flight_radio_index, 1, flight.intra_flight_channel) + + # For cases where the inter-flight and intra-flight radios share presets + # (the JF-17 only has one set of channels, even though it can use two + # channels simultaneously), start assigning inter-flight channels at 2. + radio_id = self.inter_flight_radio_index + if self.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)) + + 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 air_support.awacs: + flight.assign_channel(radio_id, next(channel_alloc), awacs.freq) + + if flight.arrival != flight.departure: + flight.assign_channel(radio_id, next(channel_alloc), + flight.arrival.atc) + + try: + # TODO: Skip incompatible tankers. + for tanker in air_support.tankers: + flight.assign_channel( + radio_id, next(channel_alloc), tanker.freq) + + if flight.divert is not None: + 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. + pass + + +@dataclass(frozen=True) +class WarthogRadioChannelAllocator(RadioChannelAllocator): + """Preset channel allocator for the A-10C.""" + + def assign_channels_for_flight(self, flight: FlightData, + air_support: AirSupport) -> None: + # 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. + pass + + +@dataclass(frozen=True) +class ViggenRadioChannelAllocator(RadioChannelAllocator): + """Preset channel allocator for the AJS37.""" + + def assign_channels_for_flight(self, flight: FlightData, + air_support: AirSupport) -> None: + # The Viggen's preset channels are handled differently from other + # aircraft. The aircraft automatically configures channels for every + # allied flight in the game (including AWACS) and for every airfield. As + # such, we don't need to allocate any of those. There are seven presets + # we can modify, however: three channels for the main radio intended for + # communication with wingmen, and four emergency channels for the backup + # radio. We'll set the first channel of the main radio to the + # intra-flight channel, and the first three emergency channels to each + # of the flight plan's airfields. The fourth emergency channel is always + # the guard channel. + radio_id = 1 + flight.assign_channel(radio_id, 1, flight.intra_flight_channel) + flight.assign_channel(radio_id, 4, flight.departure.atc) + flight.assign_channel(radio_id, 5, flight.arrival.atc) + # TODO: Assign divert to 6 when we support divert airfields. + + +@dataclass(frozen=True) +class SCR522RadioChannelAllocator(RadioChannelAllocator): + """Preset channel allocator for the SCR522 WW2 radios. (4 channels)""" + + def assign_channels_for_flight(self, flight: FlightData, + air_support: AirSupport) -> None: + radio_id = 1 + flight.assign_channel(radio_id, 1, flight.intra_flight_channel) + flight.assign_channel(radio_id, 2, flight.departure.atc) + flight.assign_channel(radio_id, 3, flight.arrival.atc) + + # TODO : Some GCI on Channel 4 ? + +@dataclass(frozen=True) +class AircraftData: + """Additional aircraft data not exposed by pydcs.""" + + #: The type of radio used for inter-flight communications. + inter_flight_radio: Radio + + #: The type of radio used for intra-flight communications. + intra_flight_radio: Radio + + #: The radio preset channel allocator, if the aircraft supports channel + #: presets. If the aircraft does not support preset channels, this will be + #: None. + channel_allocator: Optional[RadioChannelAllocator] + + #: Defines how channels should be named when printed in the kneeboard. + channel_namer: Type[ChannelNamer] = ChannelNamer + + +# Indexed by the id field of the pydcs PlaneType. +AIRCRAFT_DATA: Dict[str, AircraftData] = { + "A-10C": AircraftData( + inter_flight_radio=get_radio("AN/ARC-164"), + intra_flight_radio=get_radio("AN/ARC-186(V) AM"), + channel_allocator=WarthogRadioChannelAllocator() + ), + + "AJS37": AircraftData( + # The AJS37 has somewhat unique radio configuration. Two backup radio + # (FR 24) can only operate simultaneously with the main radio in guard + # mode. As such, we only use the main radio for both inter- and intra- + # flight communication. + inter_flight_radio=get_radio("FR 22"), + intra_flight_radio=get_radio("FR 22"), + channel_allocator=ViggenRadioChannelAllocator(), + channel_namer=ViggenChannelNamer + ), + + "AV8BNA": AircraftData( + inter_flight_radio=get_radio("AN/ARC-210"), + intra_flight_radio=get_radio("AN/ARC-210"), + channel_allocator=CommonRadioChannelAllocator( + inter_flight_radio_index=2, + intra_flight_radio_index=1 + ) + ), + + "F-14B": AircraftData( + inter_flight_radio=get_radio("AN/ARC-159"), + intra_flight_radio=get_radio("AN/ARC-182"), + channel_allocator=CommonRadioChannelAllocator( + inter_flight_radio_index=1, + intra_flight_radio_index=2 + ), + channel_namer=TomcatChannelNamer + ), + + "F-16C_50": AircraftData( + inter_flight_radio=get_radio("AN/ARC-164"), + intra_flight_radio=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. + channel_allocator=CommonRadioChannelAllocator( + inter_flight_radio_index=1, + intra_flight_radio_index=2 + ), + channel_namer=ViperChannelNamer + ), + + "FA-18C_hornet": AircraftData( + inter_flight_radio=get_radio("AN/ARC-210"), + intra_flight_radio=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. + channel_allocator=CommonRadioChannelAllocator( + inter_flight_radio_index=2, + intra_flight_radio_index=1 + ) + ), + + "JF-17": AircraftData( + inter_flight_radio=get_radio("R&S M3AR UHF"), + intra_flight_radio=get_radio("R&S M3AR VHF"), + channel_allocator=CommonRadioChannelAllocator( + inter_flight_radio_index=1, + intra_flight_radio_index=1 + ), + # Same naming pattern as the Viper, so just reuse that. + channel_namer=ViperChannelNamer + ), + + "M-2000C": AircraftData( + inter_flight_radio=get_radio("TRT ERA 7000 V/UHF"), + intra_flight_radio=get_radio("TRT ERA 7200 UHF"), + channel_allocator=CommonRadioChannelAllocator( + inter_flight_radio_index=1, + intra_flight_radio_index=2 + ), + channel_namer=MirageChannelNamer + ), + + "P-51D": AircraftData( + inter_flight_radio=get_radio("SCR522"), + intra_flight_radio=get_radio("SCR522"), + channel_allocator=CommonRadioChannelAllocator( + inter_flight_radio_index=1, + intra_flight_radio_index=1 + ), + channel_namer=SCR522ChannelNamer + ), +} +AIRCRAFT_DATA["P-51D-30-NA"] = AIRCRAFT_DATA["P-51D"] +AIRCRAFT_DATA["P-47D-30"] = AIRCRAFT_DATA["P-51D"] + 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 = [] + self.flights: List[FlightData] = [] + + def get_intra_flight_channel(self, airframe: UnitType) -> RadioFrequency: + """Allocates an intra-flight channel to a group. + + Args: + airframe: The type of aircraft a channel should be allocated for. + + Returns: + The frequency of the intra-flight channel. + """ + 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 - - 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 @@ -74,10 +537,11 @@ class AircraftConflictGenerator: single_client = flight.client_count == 1 for idx in range(0, min(len(group.units), flight.client_count)): + unit = group.units[idx] 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: @@ -85,24 +549,42 @@ 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)) - # TODO : refactor this following bad specific special case code :( + channel = self.get_intra_flight_channel(unit_type) + group.set_frequency(channel.mhz) - if unit_type in helicopters.helicopter_map.values() and unit_type not in [UH_1H]: - group.set_frequency(127.5) + # TODO: Support for different departure/arrival airfields. + cp = flight.from_cp + fallback_runway = RunwayData(cp.full_name, runway_name="") + if cp.cptype == ControlPointType.AIRBASE: + departure_runway = self.get_preferred_runway(flight.from_cp.airport) + elif cp.is_fleet: + departure_runway = dynamic_runways.get(cp.name, fallback_runway) 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) + logging.warning(f"Unhandled departure control point: {cp.cptype}") + departure_runway = fallback_runway + + # The first waypoint is set automatically by pydcs, so it's not in our + # list. Convert the pydcs MovingPoint to a FlightWaypoint so it shows up + # in our FlightData. + first_point = FlightWaypoint.from_pydcs(group.points[0], flight.from_cp) + self.flights.append(FlightData( + 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. + divert=None, + waypoints=[first_point] + flight.points, + intra_flight_channel=channel + )) # Special case so Su 33 carrier take off if unit_type is Su_33: @@ -113,6 +595,21 @@ class AircraftConflictGenerator: for unit in group.units: unit.fuel = Su_33.fuel_max * 0.8 + def get_preferred_runway(self, airport: Airport) -> RunwayData: + """Returns the preferred runway for the given airport. + + Right now we're only selecting runways based on whether or not they have + ILS, but we could also choose based on wind conditions, or which + direction flight plans should follow. + """ + runways = list(RunwayData.for_pydcs_airport(airport)) + for runway in runways: + # Prefer any runway with ILS. + if runway.ils is not None: + return runway + # Otherwise we lack the mission information to pick more usefully, + # so just use the first runway. + return runways[0] def _generate_at_airport(self, name: str, side: Country, unit_type: FlyingType, count: int, client_count: int, airport: Airport = None, start_type = None) -> FlyingGroup: assert count > 0 @@ -253,17 +750,18 @@ 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 - logging.info("CLEARING SLOTS @ " + cp.airport.name) - logging.info("===============") if cp.airport is not None: - for ps in cp.airport.parking_slots: - logging.info("SLOT : " + str(ps.unit_id)) - ps.unit_id = None - logging.info("----------------") - logging.info("===============") + logging.info("CLEARING SLOTS @ " + cp.airport.name) + logging.info("===============") + if cp.airport is not None: + for ps in cp.airport.parking_slots: + logging.info("SLOT : " + str(ps.unit_id)) + ps.unit_id = None + logging.info("----------------") + logging.info("===============") for flight in flight_planner.flights: @@ -272,7 +770,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) @@ -383,19 +882,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])) @@ -407,7 +900,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)) @@ -416,7 +909,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)) @@ -425,14 +918,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)) @@ -511,23 +1004,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 new file mode 100644 index 00000000..36f126b3 --- /dev/null +++ b/gen/airfields.py @@ -0,0 +1,1558 @@ +"""Extra airfield data that is not exposed by pydcs. + +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, Iterator, Optional, Tuple + +from dcs.terrain.terrain import Airport +from .radios import MHz, RadioFrequency +from .tacan import TacanBand, TacanChannel + + +@dataclass +class AtcData: + hf: RadioFrequency + vhf_fm: RadioFrequency + vhf_am: RadioFrequency + uhf: RadioFrequency + + +@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 + + #: Elevation (in ft). + elevation: int = 0 + + #: Runway length (in ft). + runway_length: int = 0 + + #: TACAN channel for the airfield. + tacan: Optional[TacanChannel] = None + + #: TACAN callsign + tacan_callsign: Optional[str] = None + + #: VOR as a tuple of (callsign, frequency). + vor: Optional[Tuple[str, RadioFrequency]] = None + + #: RSBN channel as a tuple of (callsign, channel). + rsbn: Optional[Tuple[str, int]] = None + + #: Radio channels used by the airfield's ATC. Note that not all airfields + #: have ATCs. + atc: Optional[AtcData] = None + + #: Dict of runway heading -> ILS tuple of (callsign, frequency). + ils: Dict[str, Tuple[str, RadioFrequency]] = 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 tuple of (callsign, frequency). + outer_ndb: Dict[str, Tuple[str, RadioFrequency]] = 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]: + ils = self.ils.get(runway) + if ils is not None: + return ils[1] + return None + + +# TODO: Add more airfields. +AIRFIELD_DATA = { + # Caucasus + + "Batumi": AirfieldData( + 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( + 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={ + "07": ("IKB", MHz(111, 50)), + }, + outer_ndb={ + "07": ("KT", MHz(870, 0)), + }, + inner_ndb={ + "07": ("T", MHz(490, 0)), + }, + ), + + "Senaki-Kolkhi": AirfieldData( + 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={ + "09": ("ITS", MHz(108, 90)), + }, + outer_ndb={ + "09": ("BI", MHz(335, 0)), + }, + inner_ndb={ + "09": ("I", MHz(688, 0)), + }, + ), + + "Kutaisi": AirfieldData( + 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={ + "08": ("IKS", MHz(109, 75)), + }, + ), + + "Sukhumi-Babushara": AirfieldData( + 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( + 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( + theater="Caucasus", + icao="URSS", + elevation=98, + runway_length=9686, + atc=AtcData(MHz(4, 50), MHz(127, 0), MHz(39, 600), MHz(256, 0)), + ils={ + "06": ("ISO", MHz(111, 10)), + }, + ), + + "Gelendzhik": AirfieldData( + 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( + 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( + 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( + 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={ + "04": ("OX", 26), + "22": ("KW", 26), + }, + outer_ndb={ + "04": ("OX", MHz(408, 0)), + "22": ("KW", MHz(408, 0)), + }, + inner_ndb={ + "04": ("O", MHz(803, 0)), + "22": ("K", MHz(803, 0)), + }, + ), + + "Krasnodar-Center": AirfieldData( + 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={ + "09": ("MB", 38), + }, + outer_ndb={ + "09": ("MB", MHz(625, 0)), + "27": ("OC", MHz(625, 0)), + }, + inner_ndb={ + "09": ("M", MHz(303, 0)), + "27": ("C", MHz(303, 0)), + }, + ), + + "Krasnodar-Pashkovsky": AirfieldData( + 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)), + "05": ("KR", MHz(493, 0)), + }, + inner_ndb={ + "23": ("L", MHz(240, 0)), + "05": ("K", MHz(240, 0)), + }, + ), + + "Maykop-Khanskaya": AirfieldData( + 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={ + "04": ("DG", 36), + }, + outer_ndb={ + "04": ("DG", MHz(289, 0)), + "22": ("RK", MHz(289, 0)), + }, + inner_ndb={ + "4": ("D", MHz(591, 0)), + "22": ("R", MHz(591, 0)), + }, + ), + + "Mineralnye Vody": AirfieldData( + 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( + 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( + 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( + 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( + 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( + 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( + 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 + "Liwa Airbase": AirfieldData( + theater="Persian Gulf", + icao="OMLW", + elevation=400, + runway_length=10768, + atc=AtcData(MHz(4, 175), MHz(39, 250), MHz(119, 300), MHz(250, 850)), + ), + + "Al Dhafra AB": AirfieldData( + theater="Persian Gulf", + icao="OMAM", + elevation=52, + runway_length=11530, + tacan=TacanChannel(96, TacanBand.X), + tacan_callsign="MA", + vor=("MA", MHz(114, 900)), + atc=AtcData(MHz(4, 250), MHz(39, 400), MHz(126, 500), MHz(251, 000)), + ils={ + "13": ("MMA", MHz(111, 100)), + "31": ("IMA", MHz(109, 100)), + }, + ), + + "Al-Bateen Airport": AirfieldData( + theater="Persian Gulf", + icao="OMAD", + elevation=11, + runway_length=6808, + vor=("ALB", MHz(114, 0)), + atc=AtcData(MHz(4, 25), MHz(38, 950), MHz(119, 900), MHz(250, 550)), + ), + + "Sas Al Nakheel Airport": AirfieldData( + theater="Persian Gulf", + icao="OMNK", + elevation=9, + runway_length=5387, + vor=("SAS", MHz(128, 930)), + atc=AtcData(MHz(3, 975), MHz(38, 850), MHz(128, 900), MHz(250, 450)), + ), + + "Abu Dhabi International Airport": AirfieldData( + theater="Persian Gulf", + icao="OMAA", + elevation=91, + runway_length=12817, + vor=("ADV", MHz(114, 250)), + atc=AtcData(MHz(4, 000), MHz(38, 900), MHz(119, 200), MHz(250, 500)), + ), + + "Al Ain International Airport": AirfieldData( + theater="Persian Gulf", + icao="OMAL", + elevation=813, + runway_length=11267, + vor=("ALN", MHz(112, 600)), + atc=AtcData(MHz(4, 75), MHz(39, 50), MHz(119, 850), MHz(250, 650)), + ), + + "Al Maktoum Intl": AirfieldData( + theater="Persian Gulf", + icao="OMDW", + elevation=123, + runway_length=11500, + atc=AtcData(MHz(4, 300), MHz(39, 500), MHz(118, 650), MHz(251, 100)), + ils={ + "30": ("IJWA", MHz(109, 750)), + "12": ("IMA", MHz(111, 750)), + }, + ), + + "Al Minhad Intl": AirfieldData( + theater="Persian Gulf", + icao="OMDM", + elevation=190, + runway_length=11865, + tacan=TacanChannel(99, TacanBand.X), + tacan_callsign="MIN", + atc=AtcData(MHz(3, 800), MHz(38, 500), MHz(121, 800), MHz(250, 100)), + ils={ + "27": ("IMNR", MHz(110, 750)), + "9": ("IMNW", MHz(110, 700)), + }, + ), + + "Dubai Intl": AirfieldData( + theater="Persian Gulf", + icao="OMDB", + elevation=16, + runway_length=11018, + atc=AtcData(MHz(4, 275), MHz(39, 450), MHz(118, 750), MHz(251, 50)), + ils={ + "30": ("IDBL", MHz(110, 900)), + "12": ("IDBR", MHz(110, 100)), + }, + ), + + "Sharjah Intl": AirfieldData( + theater="Persian Gulf", + icao="OMSJ", + elevation=98, + runway_length=10535, + atc=AtcData(MHz(3, 850), MHz(38, 600), MHz(118, 600), MHz(252, 200)), + ils={ + "30": ("ISHW", MHz(111, 950)), + "12": ("ISRE", MHz(108, 550)), + }, + ), + + "Fujairah Intl": AirfieldData( + theater="Persian Gulf", + icao="OMFJ", + elevation=60, + runway_length=9437, + vor=("FJV", MHz(113, 800)), + atc=AtcData(MHz(4, 325), MHz(39, 550), MHz(124, 600), MHz(251, 150)), + ils={ + "29": ("IFJR", MHz(111, 500)), + }, + ), + + "Ras AL Khaimah": AirfieldData( + theater="Persian Gulf", + icao="OMRK", + elevation=70, + runway_length=8406, + vor=("OMRK", MHz(113, 600)), + atc=AtcData(MHz(4, 150), MHz(39, 200), MHz(121, 600), MHz(250, 800)), + ), + + "Khasab": AirfieldData( + theater="Persian Gulf", + icao="OOKB", + elevation=47, + runway_length=7513, + atc=AtcData(MHz(3, 750), MHz(38, 400), MHz(124, 350), MHz(250, 000)), + ils={ + "19": ("IBKS", MHz(110, 300)), + }, + ), + + "Sir Abu Nuayr": AirfieldData( + theater="Persian Gulf", + icao="OMSN", + elevation=25, + runway_length=2229 + ), + + "Sirri Island": AirfieldData( + theater="Persian Gulf", + icao="OIBS", + elevation=17, + runway_length=7443, + vor=("SIR", MHz(113, 750)), + atc=AtcData(MHz(3, 875), MHz(38, 650), MHz(135, 50), MHz(250, 250)), + ), + + "Abu Musa Island Airport": AirfieldData( + theater="Persian Gulf", + icao="OIBA", + elevation=16, + runway_length=7616, + atc=AtcData(MHz(3, 950), MHz(38, 800), MHz(122, 900), MHz(250, 400)), + ), + + "Tunb Kochak": AirfieldData( + theater="Persian Gulf", + icao="OITK", + elevation=15, + runway_length=1481, + tacan=TacanChannel(89, TacanBand.X), + tacan_callsign="KCK", + ), + + "Tunb Island AFB": AirfieldData( + theater="Persian Gulf", + icao="OIGI", + elevation=42, + runway_length=6099, + ), + + "Qeshm Island": AirfieldData( + theater="Persian Gulf", + icao="OIKQ", + elevation=26, + runway_length=13287, + vor=("KHM", MHz(117, 100)), + atc=AtcData(MHz(3, 825), MHz(38, 550), MHz(118, 50), MHz(250, 150)), + ), + + "Bandar-e-Jask airfield": AirfieldData( + theater="Persian Gulf", + icao="OIZJ", + elevation=26, + runway_length=6842, + vor=("KHM", MHz(116, 300)), + atc=AtcData(MHz(3, 825), MHz(38, 550), MHz(118, 50), MHz(250, 150)), + ), + + "Bandar Lengeh": AirfieldData( + theater="Persian Gulf", + icao="OIBL", + elevation=80, + runway_length=7625, + vor=("LEN", MHz(114, 800)), + atc=AtcData(MHz(4, 225), MHz(39, 350), MHz(121, 700), MHz(250, 950)), + ), + + "Kish International Airport": AirfieldData( + theater="Persian Gulf", + icao="OIBK", + elevation=114, + runway_length=10617, + tacan=TacanChannel(112, TacanBand.X), + tacan_callsign="KIH", + atc=AtcData(MHz(4, 50), MHz(39, 000), MHz(121, 650), MHz(250, 600)), + ), + + "Lavan Island Airport": AirfieldData( + theater="Persian Gulf", + icao="OIBV", + elevation=75, + runway_length=8234, + vor=("LVA", MHz(116, 850)), + atc=AtcData(MHz(4, 100), MHz(39, 100), MHz(128, 550), MHz(250, 700)), + ), + + "Lar Airbase": AirfieldData( + theater="Persian Gulf", + icao="OISL", + elevation=2635, + runway_length=9600, + vor=("LAR", MHz(117, 900)), + atc=AtcData(MHz(3, 775), MHz(38, 450), MHz(127, 350), MHz(250, 50)), + ), + + "Havadarya": AirfieldData( + theater="Persian Gulf", + icao="OIKP", + elevation=50, + runway_length=7300, + tacan=TacanChannel(47, TacanBand.X), + tacan_callsign="HDR", + atc=AtcData(MHz(4, 350), MHz(39, 600), MHz(123, 150), MHz(251, 200)), + ils={ + "8": ("IBHD", MHz(108, 900)), + }, + ), + + "Bandar Abbas Intl": AirfieldData( + theater="Persian Gulf", + icao="OIKB", + elevation=18, + runway_length=11640, + tacan=TacanChannel(78, TacanBand.X), + tacan_callsign="BND", + vor=("BND", MHz(117, 200)), + atc=AtcData(MHz(4, 200), MHz(39, 300), MHz(118, 100), MHz(250, 900)), + ils={ + "21": ("IBND", MHz(333, 800)), + }, + ), + + "Jiroft Airport": AirfieldData( + theater="Persian Gulf", + icao="OIKJ", + elevation=2664, + runway_length=9160, + atc=AtcData(MHz(4, 125), MHz(39, 120), MHz(136, 0), MHz(250, 750)), + ), + + "Kerman Airport": AirfieldData( + theater="Persian Gulf", + icao="OIKK", + elevation=5746, + runway_length=11981, + tacan=TacanChannel(97, TacanBand.X), + tacan_callsign="KER", + vor=("KER", MHz(112, 0)), + atc=AtcData(MHz(3, 900), MHz(38, 700), MHz(118, 250), MHz(250, 300)), + ), + + "Shiraz International Airport": AirfieldData( + theater="Persian Gulf", + icao="OISS", + elevation=4878, + runway_length=13271, + tacan=TacanChannel(94, TacanBand.X), + tacan_callsign="SYZ1", + vor=("SYZ", MHz(112, 0)), + atc=AtcData(MHz(3, 925), MHz(38, 750), MHz(121, 900), MHz(250, 350)), + ), + + # Syria Map + "Adana Sakirpasa": AirfieldData( + theater="Syria", + icao="LTAF", + elevation=55, + runway_length=8115, + vor=("ADA", MHz(112, 700)), + atc=AtcData(MHz(4, 225), MHz(39, 350), MHz(121, 100), MHz(250, 900)), + ils={ + "05": ("IADA", MHz(108, 700)), + }, + ), + + "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={ + "50": ("IDAN", MHz(109, 300)), + "23": ("DANM", MHz(111, 700)), + }, + ), + + "Minakh": AirfieldData( + theater="Syria", + icao="OS71", + elevation=1614, + runway_length=4648, + atc=AtcData(MHz(4, 125), MHz(39, 150), MHz(120, 600), MHz(250, 700)), + ), + + "Hatay": AirfieldData( + theater="Syria", + icao="LTDA", + elevation=253, + runway_length=9052, + vor=("HTY", MHz(112, 500)), + atc=AtcData(MHz(3, 825), MHz(38, 550), MHz(128, 500), MHz(250, 150)), + ils={ + "22": ("IHTY", MHz(108, 150)), + "04": ("IHAT", MHz(108, 900)), + }, + ), + + "Kuweires": AirfieldData( + theater="Syria", + icao="OS66", + elevation=1200, + runway_length=6662, + atc=AtcData(MHz(4, 275), MHz(39, 450), MHz(120, 500), MHz(251)), + ), + + "Aleppo": AirfieldData( + theater="Syria", + icao="OSAP", + elevation=1253, + runway_length=8332, + atc=AtcData(MHz(4, 150), MHz(39, 200), MHz(119, 100), MHz(250, 750)), + ils={ + "50": ("IDAN", MHz(109, 300)), + "23": ("DANM", MHz(111, 700)), + }, + ), + + "Jirah": AirfieldData( + theater="Syria", + icao="OS62", + elevation=1170, + runway_length=9090, + atc=AtcData(MHz(3, 875), MHz(38, 650), MHz(118, 100), MHz(250, 200)), + ), + + "Taftanaz": AirfieldData( + theater="Syria", + elevation=1020, + runway_length=2705, + atc=AtcData(MHz(4, 375), MHz(39, 650), MHz(122, 800), MHz(251, 200)), + ), + + "Tabqa": AirfieldData( + theater="Syria", + icao="OS59", + elevation=1083, + runway_length=9036, + atc=AtcData(MHz(4, 350), MHz(39, 600), MHz(118, 500), MHz(251, 150)), + ), + + "Abu al-Dahur": AirfieldData( + theater="Syria", + icao="OS57", + elevation=820, + runway_length=8728, + atc=AtcData(MHz(3, 950), MHz(38, 800), MHz(122, 200), MHz(250, 350)), + ), + + "Bassel Al-Assad": AirfieldData( + theater="Syria", + icao="OSLK", + elevation=93, + runway_length=7305, + vor=("LTK", MHz(114, 800)), + atc=AtcData(MHz(4), MHz(38, 900), MHz(118, 100), MHz(250, 450)), + ils={ + "17": ("IBA", MHz(109, 100)), + }, + ), + + "Hama": AirfieldData( + theater="Syria", + icao="OS58", + elevation=983, + runway_length=7957, + atc=AtcData(MHz(3, 800), MHz(38, 500), MHz(118, 50), MHz(250, 100)), + ), + + "Rene Mouawad": AirfieldData( + theater="Syria", + icao="OLKA", + elevation=14, + runway_length=8614, + atc=AtcData(MHz(4, 325), MHz(39, 550), MHz(129, 500), MHz(251, 100)), + ), + + "Al Quasayr": AirfieldData( + theater="Syria", + icao="OS70", + elevation=1729, + runway_length=8585, + atc=AtcData(MHz(4, 400), MHz(39, 700), MHz(119, 200), MHz(251, 250)), + ), + + "Palmyra": AirfieldData( + theater="Syria", + icao="OSPR", + elevation=1267, + runway_length=8704, + atc=AtcData(MHz(4, 175), MHz(39, 250), MHz(121, 900), MHz(250, 800)), + ), + + "Wujah Al Hajar": AirfieldData( + theater="Syria", + icao="Z19O", + elevation=619, + runway_length=4717, + vor=("CAK", MHz(116, 200)), + atc=AtcData(MHz(4, 425), MHz(39, 750), MHz(121, 500), MHz(251, 300)), + ), + + "An Nasiriyah": AirfieldData( + theater="Syria", + icao="OS64", + elevation=2746, + runway_length=8172, + atc=AtcData(MHz(4, 450), MHz(39, 800), MHz(122, 300), MHz(251, 350)), + ), + + "Rayak": AirfieldData( + theater="Syria", + icao="OLRA", + elevation=2934, + runway_length=8699, + vor=("HTY", MHz(124, 400)), + atc=AtcData(MHz(4, 300), MHz(39, 500), MHz(124, 400), MHz(251, 50)), + ), + + "Beirut-Rafic Hariri": AirfieldData( + theater="Syria", + icao="OLBA", + elevation=39, + runway_length=9463, + vor=("KAD", MHz(112, 600)), + atc=AtcData(MHz(4, 475), MHz(39, 850), MHz(118, 900), MHz(251, 400)), + ils={ + "17": ("BIL", MHz(109, 500)), + }, + ), + + "Al-Dumayr": AirfieldData( + theater="Syria", + icao="OS61", + elevation=2066, + runway_length=8902, + atc=AtcData(MHz(4, 550), MHz(40), MHz(120, 300), MHz(251, 550)), + ), + + "Marj as Sultan North": AirfieldData( + theater="Syria", + elevation=2007, + runway_length=268, + atc=AtcData(MHz(4, 25), MHz(38, 950), MHz(122, 700), MHz(250, 500)), + ), + + "Marj as Sultan South": AirfieldData( + theater="Syria", + elevation=2007, + runway_length=166, + atc=AtcData(MHz(4, 525), MHz(39, 950), MHz(122, 900), MHz(251, 500)), + ), + + "Mezzeh": AirfieldData( + theater="Syria", + icao="OS67", + elevation=2355, + runway_length=7522, + atc=AtcData(MHz(4, 100), MHz(39, 100), MHz(120, 700), MHz(250, 650)), + ), + + "Qabr as Sitt": AirfieldData( + theater="Syria", + elevation=2134, + runway_length=489, + atc=AtcData(MHz(4, 200), MHz(39, 300), MHz(122, 600), MHz(250, 850)), + ), + + "Damascus": AirfieldData( + theater="Syria", + icao="OSDI", + elevation=2007, + runway_length=11423, + vor=("DAM", MHz(116)), + atc=AtcData(MHz(4, 500), MHz(39, 900), MHz(118, 500), MHz(251, 450)), + ils={ + "24": ("IDA", MHz(109, 900)), + }, + ), + + "Marj Ruhayyil": AirfieldData( + theater="Syria", + icao="OS63", + elevation=2160, + runway_length=7576, + atc=AtcData(MHz(4, 50), MHz(39), MHz(120, 800), MHz(250, 550)), + ), + + "Kiryat Shmona": AirfieldData( + theater="Syria", + icao="LLKS", + elevation=328, + runway_length=3258, + atc=AtcData(MHz(3, 975), MHz(38, 850), MHz(118, 400), MHz(250, 400)), + ), + + "Khalkhalah": AirfieldData( + theater="Syria", + icao="OS69", + elevation=2337, + runway_length=8248, + atc=AtcData(MHz(3, 900), MHz(38, 700), MHz(122, 500), MHz(250, 250)), + ), + + "Haifa": AirfieldData( + theater="Syria", + icao="LLHA", + elevation=19, + runway_length=3253, + atc=AtcData(MHz(3, 775), MHz(38, 450), MHz(127, 800), MHz(250, 50)), + ), + + "Ramat David": AirfieldData( + theater="Syria", + icao="LLRD", + elevation=105, + runway_length=7037, + atc=AtcData(MHz(4, 250), MHz(39, 400), MHz(118, 600), MHz(250, 950)), + ), + + "Megiddo": AirfieldData( + theater="Syria", + icao="LLMG", + elevation=180, + runway_length=6098, + atc=AtcData(MHz(4, 75), MHz(39, 50), MHz(119, 900), MHz(250, 600)), + ), + + "Eyn Shemer": AirfieldData( + theater="Syria", + icao="LLES", + elevation=93, + runway_length=3562, + atc=AtcData(MHz(3, 750), MHz(38, 400), MHz(123, 400), MHz(250)), + ), + + "King Hussein Air College": AirfieldData( + theater="Syria", + icao="OJMF", + elevation=2204, + runway_length=8595, + atc=AtcData(MHz(3, 925), MHz(38, 750), MHz(118, 300), MHz(250, 300)), + ), + + # NTTR + "Mina Airport 3Q0": AirfieldData( + theater="NTTR", + elevation=4562, + runway_length=4222, + ), + + "Tonopah Airport": AirfieldData( + theater="NTTR", + icao="KTPH", + elevation=5394, + runway_length=6715, + ), + + "Tonopah Test Range Airfield": AirfieldData( + 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( + theater="NTTR", + icao="KBTY", + elevation=3173, + runway_length=5380, + ), + + "Pahute Mesa Airstrip": AirfieldData( + theater="NTTR", + elevation=5056, + runway_length=5420, + ), + + "Groom Lake AFB": AirfieldData( + 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( + theater="NTTR", + elevation=4815, + runway_length=4408, + ), + + "Mesquite": AirfieldData( + theater="NTTR", + icao="67L", + elevation=1858, + runway_length=4937, + ), + + "Creech AFB": AirfieldData( + 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( + 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( + 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( + 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( + 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( + 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( + theater="NTTR", + icao="KBVU", + elevation=2121, + runway_length=4612, + ), + + "Jean Airport": AirfieldData( + theater="NTTR", + elevation=2824, + runway_length=4053, + ), + + "Laughlin Airport": AirfieldData( + theater="NTTR", + icao="KIFP", + elevation=656, + runway_length=7139, + atc=AtcData(MHz(3, 750), MHz(123, 900), MHz(38, 400), MHz(250, 0)), + ), + + # Normandy + + "Needs Oar Point": AirfieldData( + theater="Normandy", + elevation=30, + runway_length=5259, + atc=AtcData(MHz(4, 225), MHz(118, 950), MHz(39, 350), MHz(250, 950)), + ), + + "Funtington": AirfieldData( + theater="Normandy", + elevation=164, + runway_length=5080, + atc=AtcData(MHz(4, 250), MHz(119, 000), MHz(39, 400), MHz(251, 000)), + ), + + "Tangmere": AirfieldData( + theater="Normandy", + elevation=47, + runway_length=4296, + atc=AtcData(MHz(4, 300), MHz(119, 100), MHz(39, 500), MHz(251, 100)), + ), + + "Ford_AF": AirfieldData( + theater="Normandy", + elevation=29, + runway_length=4296, + atc=AtcData(MHz(4, 325), MHz(119, 150), MHz(39, 550), MHz(251, 150)), + ), + + "Chailey": AirfieldData( + theater="Normandy", + elevation=134, + runway_length=5080, + atc=AtcData(MHz(4, 200), MHz(118, 900), MHz(39, 300), MHz(250, 900)), + ), + + "Maupertus": AirfieldData( + theater="Normandy", + icao="A-15", + elevation=441, + runway_length=4666, + atc=AtcData(MHz(4, 550), MHz(119, 600), MHz(40, 000), MHz(251, 600)), + ), + + "Azeville": AirfieldData( + theater="Normandy", + icao="A-7", + elevation=74, + runway_length=3357, + atc=AtcData(MHz(3, 875), MHz(118, 250), MHz(38, 650), MHz(250, 250)), + ), + + "Biniville": AirfieldData( + theater="Normandy", + icao="A-24", + elevation=106, + runway_length=3283, + atc=AtcData(MHz(3, 750), MHz(118, 000), MHz(38, 400), MHz(250, 000)), + ), + + "Beuzeville": AirfieldData( + theater="Normandy", + icao="A-6", + elevation=114, + runway_length=3840, + atc=AtcData(MHz(3, 850), MHz(118, 200), MHz(38, 600), MHz(250, 200)), + ), + + "Picauville": AirfieldData( + theater="Normandy", + icao="A-8", + elevation=72, + runway_length=3840, + atc=AtcData(MHz(3, 900), MHz(118, 300), MHz(38, 700), MHz(250, 300)), + ), + + "Brucheville": AirfieldData( + theater="Normandy", + icao="A-16", + elevation=45, + runway_length=3413, + atc=AtcData(MHz(4, 575), MHz(119, 650), MHz(40, 50), MHz(251, 650)), + ), + + "Cretteville": AirfieldData( + theater="Normandy", + icao="A-14", + elevation=95, + runway_length=4594, + atc=AtcData(MHz(4, 500), MHz(119, 500), MHz(39, 900), MHz(251, 500)), + ), + + "Meautis": AirfieldData( + theater="Normandy", + icao="A-17", + elevation=83, + runway_length=3840, + atc=AtcData(MHz(4, 600), MHz(119, 700), MHz(40, 100), MHz(251, 700)), + ), + + "Lessay": AirfieldData( + theater="Normandy", + icao="A-20", + elevation=65, + runway_length=5080, + atc=AtcData(MHz(4, 650), MHz(119, 800), MHz(40, 200), MHz(251, 800)), + ), + + "Cardonville": AirfieldData( + theater="Normandy", + icao="A-3", + elevation=101, + runway_length=4541, + atc=AtcData(MHz(3, 775), MHz(118, 50), MHz(38, 450), MHz(250, 50)), + ), + + "Cricqueville-en-Bessin": AirfieldData( + theater="Normandy", + icao="A-2", + elevation=81, + runway_length=3459, + atc=AtcData(MHz(4, 625), MHz(119, 750), MHz(40, 150), MHz(251, 750)), + ), + + "Deux Jumeaux": AirfieldData( + theater="Normandy", + icao="A-4", + elevation=123, + runway_length=4628, + atc=AtcData(MHz(3, 800), MHz(118, 100), MHz(38, 500), MHz(250, 100)), + ), + + "Saint Pierre du Mont": AirfieldData( + theater="Normandy", + icao="A-1", + elevation=103, + runway_length=4737, + atc=AtcData(MHz(4, 000), MHz(118, 500), MHz(38, 900), MHz(250, 500)), + ), + + "Sainte-Laurent-sur-Mer": AirfieldData( + theater="Normandy", + icao="A-21", + elevation=145, + runway_length=4561, + atc=AtcData(MHz(4, 675), MHz(119, 850), MHz(40, 250), MHz(251, 850)), + ), + + "Longues-sur-Mer": AirfieldData( + theater="Normandy", + icao="B-11", + elevation=225, + runway_length=3155, + atc=AtcData(MHz(3, 950), MHz(118, 400), MHz(38, 800), MHz(250, 400)), + ), + + "Chippelle": AirfieldData( + theater="Normandy", + icao="A-5", + elevation=124, + runway_length=4643, + atc=AtcData(MHz(3, 825), MHz(118, 150), MHz(38, 550), MHz(250, 150)), + ), + + "Le Molay": AirfieldData( + theater="Normandy", + icao="A-9", + elevation=104, + runway_length=3840, + atc=AtcData(MHz(3, 925), MHz(118, 350), MHz(38, 750), MHz(250, 350)), + ), + + "Lignerolles": AirfieldData( + theater="Normandy", + icao="A-12", + elevation=404, + runway_length=3436, + atc=AtcData(MHz(4, 275), MHz(119, 50), MHz(39, 450), MHz(251, 50)), + ), + + "Sommervieu": AirfieldData( + theater="Normandy", + icao="B-8", + elevation=186, + runway_length=3840, + atc=AtcData(MHz(4, 125), MHz(118, 750), MHz(39, 150), MHz(250, 750)), + ), + + "Bazenville": AirfieldData( + theater="Normandy", + icao="B-2", + elevation=199, + runway_length=3800, + atc=AtcData(MHz(4, 25), MHz(118, 550), MHz(38, 950), MHz(250, 550)), + ), + + "Rucqueville": AirfieldData( + theater="Normandy", + icao="B-7", + elevation=192, + runway_length=4561, + atc=AtcData(MHz(4, 100), MHz(118, 700), MHz(39, 100), MHz(250, 700)), + ), + + "Lantheuil": AirfieldData( + theater="Normandy", + icao="B-9", + elevation=174, + runway_length=3597, + atc=AtcData(MHz(4, 150), MHz(118, 800), MHz(39, 200), MHz(250, 800)), + ), + + "Sainte-Croix-sur-Mer": AirfieldData( + theater="Normandy", + icao="B-3", + elevation=160, + runway_length=3840, + atc=AtcData(MHz(4, 50), MHz(118, 600), MHz(39, 000), MHz(250, 600)), + ), + + "Beny-sur-Mer": AirfieldData( + theater="Normandy", + icao="B-4", + elevation=199, + runway_length=3155, + atc=AtcData(MHz(4, 75), MHz(118, 650), MHz(39, 50), MHz(250, 650)), + ), + + "Carpiquet": AirfieldData( + theater="Normandy", + icao="B-17", + elevation=187, + runway_length=3799, + atc=AtcData(MHz(3, 975), MHz(118, 450), MHz(38, 850), MHz(250, 450)), + ), + + "Goulet": AirfieldData( + theater="Normandy", + elevation=616, + runway_length=3283, + atc=AtcData(MHz(4, 375), MHz(119, 250), MHz(39, 650), MHz(251, 250)), + ), + + "Argentan": AirfieldData( + theater="Normandy", + elevation=639, + runway_length=3283, + atc=AtcData(MHz(4, 350), MHz(119, 200), MHz(39, 600), MHz(251, 200)), + ), + + "Vrigny": AirfieldData( + theater="Normandy", + elevation=590, + runway_length=3283, + atc=AtcData(MHz(4, 475), MHz(119, 450), MHz(39, 850), MHz(251, 450)), + ), + + "Hauterive": AirfieldData( + theater="Normandy", + elevation=476, + runway_length=3283, + atc=AtcData(MHz(4, 450), MHz(119, 400), MHz(39, 800), MHz(251, 400)), + ), + + "Essay": AirfieldData( + theater="Normandy", + elevation=507, + runway_length=3283, + atc=AtcData(MHz(4, 425), MHz(119, 350), MHz(39, 750), MHz(251, 350)), + ), + + "Barville": AirfieldData( + theater="Normandy", + elevation=462, + runway_length=3493, + atc=AtcData(MHz(4, 400), MHz(119, 300), MHz(39, 700), MHz(251, 300)), + ), + + "Conches": AirfieldData( + theater="Normandy", + elevation=541, + runway_length=4199, + atc=AtcData(MHz(4, 525), MHz(119, 550), MHz(39, 950), MHz(251, 550)), + ), + + "Evreux": AirfieldData( + theater="Normandy", + elevation=423, + runway_length=4296, + atc=AtcData(MHz(4, 175), MHz(118, 850), MHz(39, 250), MHz(250, 850)), + ), + + # Channel Map + "Detling": AirfieldData( + theater="Channel", + elevation=623, + runway_length=2557, + atc=AtcData(MHz(3, 950), MHz(118, 400), MHz(38, 800), MHz(250, 400)), + ), + + "High Halden": AirfieldData( + theater="Channel", + elevation=104, + runway_length=3296, + atc=AtcData(MHz(3, 750), MHz(118, 800), MHz(38, 400), MHz(250, 0)), + ), + + "Lympne": AirfieldData( + theater="Channel", + elevation=351, + runway_length=2548, + atc=AtcData(MHz(3, 925), MHz(118, 350), MHz(38, 750), MHz(250, 350)), + ), + + "Hawkinge": AirfieldData( + theater="Channel", + elevation=524, + runway_length=3013, + atc=AtcData(MHz(3, 900), MHz(118, 300), MHz(38, 700), MHz(250, 300)), + ), + + "Manston": AirfieldData( + theater="Channel", + elevation=160, + runway_length=8626, + atc=AtcData(MHz(3, 875), MHz(118, 250), MHz(38, 650), MHz(250, 250)), + ), + + "Dunkirk Mardyck": AirfieldData( + 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( + theater="Channel", + elevation=219, + runway_length=1929, + atc=AtcData(MHz(3, 825), MHz(118, 150), MHz(38, 550), MHz(250, 150)), + ), + + "Merville Calonne": AirfieldData( + theater="Channel", + elevation=52, + runway_length=7580, + atc=AtcData(MHz(3, 800), MHz(118, 100), MHz(38, 500), MHz(250, 100)), + ), + + "Abbeville Drucat": AirfieldData( + theater="Channel", + elevation=183, + runway_length=4726, + atc=AtcData(MHz(3, 775), MHz(118, 50), MHz(38, 450), MHz(250, 50)), + ), +} + + +@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 + + @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. "03" or "20L". + """ + atc: Optional[RadioFrequency] = None + tacan: Optional[TacanChannel] = None + tacan_callsign: Optional[str] = None + ils: Optional[RadioFrequency] = None + try: + airfield = AIRFIELD_DATA[airport.name] + atc = airfield.atc.uhf + tacan = airfield.tacan + tacan_callsign = airfield.tacan_callsign + ils = airfield.ils_freq(runway) + except KeyError: + logging.warning(f"No airfield data for {airport.name}") + return cls( + airfield_name=airport.name, + runway_name=runway, + atc=atc, + tacan=tacan, + tacan_callsign=tacan_callsign, + ils=ils + ) + + @classmethod + def for_pydcs_airport(cls, airport: Airport) -> Iterator["RunwayData"]: + for runway in airport.runways: + runway_number = runway.heading // 10 + runway_side = ["", "L", "R"][runway.leftright] + runway_name = f"{runway_number:02}{runway_side}" + yield cls.for_airfield(airport, runway_name) + + # pydcs only exposes one runway per physical runway, so to expose + # both sides of the runway we need to generate the other. + runway_number = ((runway.heading + 180) % 360) // 10 + runway_side = ["", "R", "L"][runway.leftright] + runway_name = f"{runway_number:02}{runway_side}" + yield cls.for_airfield(airport, runway_name) diff --git a/gen/airsupportgen.py b/gen/airsupportgen.py index 2e0ef249..da0689e9 100644 --- a/gen/airsupportgen.py +++ b/gen/airsupportgen.py @@ -1,12 +1,10 @@ -from game import db +from dataclasses import dataclass, field + +from .callsigns import callsign_for_support_unit from .conflictgen import * from .naming import * - -from dcs.mission import * -from dcs.unitgroup import * -from dcs.unittype import * -from dcs.task import * -from dcs.terrain.terrain import NoParkingSlotError +from .radios import RadioFrequency, RadioRegistry +from .tacan import TacanBand, TacanChannel, TacanRegistry TANKER_DISTANCE = 15000 TANKER_ALT = 4572 @@ -16,14 +14,39 @@ AWACS_DISTANCE = 150000 AWACS_ALT = 13000 -class AirSupportConflictGenerator: - generated_tankers = None # type: typing.List[str] +@dataclass +class AwacsInfo: + """AWACS information for the kneeboard.""" + callsign: str + freq: RadioFrequency - def __init__(self, mission: Mission, conflict: Conflict, game): + +@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: + + 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 +55,12 @@ 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", ""] + fallback_tanker_number = 0 + 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)) + 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 +71,42 @@ 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), ) + callsign = callsign_for_support_unit(tanker_group) + tacan_callsign = { + "Texaco": "TEX", + "Arco": "ARC", + "Shell": "SHL", + }.get(callsign) + if tacan_callsign is None: + # The dict above is all the callsigns currently in the game, but + # non-Western countries don't use the callsigns and instead just + # use numbers. It's possible that none of those nations have + # TACAN compatible refueling aircraft, but fallback just in + # case. + tacan_callsign = f"TK{fallback_tanker_number}" + fallback_tanker_number += 1 + 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)) + # Override PyDCS tacan channel. + tanker_group.points[0].tasks.pop() + tanker_group.points[0].tasks.append(ActivateBeaconCommand( + tacan.number, tacan.band.value, tacan_callsign, 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, variant, freq, tacan)) + if is_awacs_enabled: try: + 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 +115,13 @@ 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)) - except: - print("No AWACS for faction") + self.air_support.awacs.append(AwacsInfo( + callsign_for_support_unit(awacs_flight), freq)) + except: + print("No AWACS for faction") \ No newline at end of file diff --git a/gen/armor.py b/gen/armor.py index 07fafa67..5042149f 100644 --- a/gen/armor.py +++ b/gen/armor.py @@ -1,10 +1,12 @@ -from dcs.action import AITaskPush, AITaskSet +from dataclasses import dataclass + +from dcs.action import AITaskPush from dcs.condition import TimeAfter, UnitDamaged, Or, GroupLifeLess -from dcs.task import * from dcs.triggers import TriggerOnce, Event from gen import namegen from gen.ground_forces.ai_ground_planner import CombatGroupRole, DISTANCE_FROM_FRONTLINE +from .callsigns import callsign_for_support_unit from .conflictgen import * SPREAD_DISTANCE_FACTOR = 0.1, 0.3 @@ -22,6 +24,17 @@ FIGHT_DISTANCE = 3500 RANDOM_OFFSET_ATTACK = 250 + +@dataclass(frozen=True) +class JtacInfo: + """JTAC information.""" + unit_name: str + callsign: str + region: str + code: str + # TODO: Radio info? Type? + + class GroundConflictGenerator: def __init__(self, mission: Mission, conflict: Conflict, game, player_planned_combat_groups, enemy_planned_combat_groups, player_stance): @@ -32,6 +45,7 @@ class GroundConflictGenerator: self.player_stance = CombatStance(player_stance) self.enemy_stance = random.choice([CombatStance.AGGRESSIVE, CombatStance.AGGRESSIVE, CombatStance.AGGRESSIVE, CombatStance.ELIMINATION, CombatStance.BREAKTHROUGH]) if len(enemy_planned_combat_groups) > len(player_planned_combat_groups) else random.choice([CombatStance.DEFENSIVE, CombatStance.DEFENSIVE, CombatStance.DEFENSIVE, CombatStance.AMBUSH, CombatStance.AGGRESSIVE]) self.game = game + self.jtacs: List[JtacInfo] = [] def _group_point(self, point) -> Point: distance = randint( @@ -100,7 +114,7 @@ class GroundConflictGenerator: # Add JTAC if "has_jtac" in self.game.player_faction and self.game.player_faction["has_jtac"] and self.game.settings.include_jtac_if_available: n = "JTAC" + str(self.conflict.from_cp.id) + str(self.conflict.to_cp.id) - code = 1688 - len(self.game.jtacs) + code = 1688 - len(self.jtacs) utype = MQ_9_Reaper if "jtac_unit" in self.game.player_faction: @@ -115,7 +129,10 @@ class GroundConflictGenerator: jtac.points[0].tasks.append(SetInvisibleCommand(True)) jtac.points[0].tasks.append(SetImmortalCommand(True)) jtac.points[0].tasks.append(OrbitAction(5000, 300, OrbitAction.OrbitPattern.Circle)) - self.game.jtacs.append(("Frontline " + self.conflict.from_cp.name + "/" + self.conflict.to_cp.name, code, n)) + frontline = f"Frontline {self.conflict.from_cp.name}/{self.conflict.to_cp.name}" + # Note: Will need to change if we ever add ground based JTAC. + callsign = callsign_for_support_unit(jtac) + self.jtacs.append(JtacInfo(n, callsign, frontline, str(code))) def gen_infantry_group_for_group(self, group, is_player, side:Country, forward_heading): 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/gen/briefinggen.py b/gen/briefinggen.py index 47e43ce6..10e07001 100644 --- a/gen/briefinggen.py +++ b/gen/briefinggen.py @@ -1,68 +1,127 @@ -import logging +import os +from collections import defaultdict +from dataclasses import dataclass +import random +from typing import List from game import db -from .conflictgen import * -from .naming import * - -from dcs.mission import * +from dcs.mission import Mission +from .aircraft import FlightData +from .airfields import RunwayData +from .airsupportgen import AwacsInfo, TankerInfo +from .armor import JtacInfo +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 + + +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" + 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 +133,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): @@ -180,7 +237,7 @@ class BriefingGenerator: def __random_frontline_sentence(self, player_base_name, enemy_base_name): templates = [ "There are combats between {} and {}. ", - "The war on the ground is still going on between {} an {}. ", + "The war on the ground is still going on between {} and {}. ", "Our ground forces in {} are opposed to enemy forces based in {}. ", "Our forces from {} are fighting enemies based in {}. ", "There is an active frontline between {} and {}. ", diff --git a/gen/callsigns.py b/gen/callsigns.py new file mode 100644 index 00000000..8ebda467 --- /dev/null +++ b/gen/callsigns.py @@ -0,0 +1,34 @@ +"""Support for working with DCS group callsigns.""" +import logging +import re + +from dcs.unitgroup import FlyingGroup +from dcs.flyingunit import FlyingUnit + + +def callsign_for_support_unit(group: FlyingGroup) -> str: + # Either something like Overlord11 for Western AWACS, or else just a number. + # Convert to either "Overlord" or "Flight 123". + lead = group.units[0] + raw_callsign = lead.callsign_as_str() + try: + return f"Flight {int(raw_callsign)}" + except ValueError: + return raw_callsign.rstrip("1234567890") + + +def create_group_callsign_from_unit(lead: FlyingUnit) -> str: + raw_callsign = lead.callsign_as_str() + if not lead.callsign_is_western: + # Callsigns for non-Western countries are just a number per flight, + # similar to tail numbers. + return f"Flight {raw_callsign}" + + # Callsign from pydcs is in the format ``, + # where unit ID is guaranteed to be a single digit but the group ID may + # be more. + match = re.search(r"^(\D+)(\d+)(\d)$", raw_callsign) + if match is None: + logging.error(f"Could not parse unit callsign: {raw_callsign}") + return f"Flight {raw_callsign}" + return f"{match.group(1)} {match.group(2)}" diff --git a/gen/conflictgen.py b/gen/conflictgen.py index 1a5dabdb..9b83b51e 100644 --- a/gen/conflictgen.py +++ b/gen/conflictgen.py @@ -1,7 +1,7 @@ import logging import typing import pdb -from pydcs import dcs +import dcs from random import randint from dcs import Mission diff --git a/gen/defenses/armor_group_generator.py b/gen/defenses/armor_group_generator.py index 3824f7da..7b772e31 100644 --- a/gen/defenses/armor_group_generator.py +++ b/gen/defenses/armor_group_generator.py @@ -3,22 +3,38 @@ import random from dcs.vehicles import Armor from game import db -from gen.defenses.armored_group_generator import ArmoredGroupGenerator +from gen.defenses.armored_group_generator import ArmoredGroupGenerator, FixedSizeArmorGroupGenerator def generate_armor_group(faction:str, game, ground_object): """ This generate a group of ground units - :param parentCp: The parent control point - :param ground_object: The ground object which will own the group - :param country: Owner country :return: Generated group """ possible_unit = [u for u in db.FACTIONS[faction]["units"] if u in Armor.__dict__.values()] if len(possible_unit) > 0: unit_type = random.choice(possible_unit) - generator = ArmoredGroupGenerator(game, ground_object, unit_type) - generator.generate() - return generator.get_generated_group() + return generate_armor_group_of_type(game, ground_object, unit_type) return None + + +def generate_armor_group_of_type(game, ground_object, unit_type): + """ + This generate a group of ground units of given type + :return: Generated group + """ + generator = ArmoredGroupGenerator(game, ground_object, unit_type) + generator.generate() + return generator.get_generated_group() + + +def generate_armor_group_of_type_and_size(game, ground_object, unit_type, size: int): + """ + This generate a group of ground units of given type and size + :return: Generated group + """ + generator = FixedSizeArmorGroupGenerator(game, ground_object, unit_type, size) + generator.generate() + return generator.get_generated_group() + diff --git a/gen/defenses/armored_group_generator.py b/gen/defenses/armored_group_generator.py index f678af81..3b81a1dd 100644 --- a/gen/defenses/armored_group_generator.py +++ b/gen/defenses/armored_group_generator.py @@ -25,3 +25,20 @@ class ArmoredGroupGenerator(GroupGenerator): self.position.y + spacing * j, self.heading) +class FixedSizeArmorGroupGenerator(GroupGenerator): + + def __init__(self, game, ground_object, unit_type, size): + super(FixedSizeArmorGroupGenerator, self).__init__(game, ground_object) + self.unit_type = unit_type + self.size = size + + def generate(self): + spacing = random.randint(20, 70) + + index = 0 + for i in range(self.size): + index = index + 1 + self.add_unit(self.unit_type, "Armor#" + str(index), + self.position.x + spacing * i, + self.position.y, self.heading) + diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index 90a91500..99cf8427 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -372,17 +372,26 @@ class FlightPlanner: egress_heading = heading - 180 - 25 ingress_pos = location.position.point_from_heading(ingress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"]) - ingress_point = FlightWaypoint(ingress_pos.x, ingress_pos.y, self.doctrine["INGRESS_ALT"]) + ingress_point = FlightWaypoint( + FlightWaypointType.INGRESS_STRIKE, + ingress_pos.x, + ingress_pos.y, + self.doctrine["INGRESS_ALT"] + ) ingress_point.pretty_name = "INGRESS on " + location.obj_name ingress_point.description = "INGRESS on " + location.obj_name ingress_point.name = "INGRESS" - ingress_point.waypoint_type = FlightWaypointType.INGRESS_STRIKE flight.points.append(ingress_point) if len(location.groups) > 0 and location.dcs_identifier == "AA": for g in location.groups: for j, u in enumerate(g.units): - point = FlightWaypoint(u.position.x, u.position.y, 0) + point = FlightWaypoint( + FlightWaypointType.TARGET_POINT, + u.position.x, + u.position.y, + 0 + ) point.description = "STRIKE " + "[" + str(location.obj_name) + "] : " + u.type + " #" + str(j) point.pretty_name = "STRIKE " + "[" + str(location.obj_name) + "] : " + u.type + " #" + str(j) point.name = location.obj_name + "#" + str(j) @@ -398,7 +407,12 @@ class FlightPlanner: if building.is_dead: continue - point = FlightWaypoint(building.position.x, building.position.y, 0) + point = FlightWaypoint( + FlightWaypointType.TARGET_POINT, + building.position.x, + building.position.y, + 0 + ) point.description = "STRIKE on " + building.obj_name + " " + building.category + " [" + str(building.dcs_identifier) + " ]" point.pretty_name = "STRIKE on " + building.obj_name + " " + building.category + " [" + str(building.dcs_identifier) + " ]" point.name = building.obj_name @@ -406,7 +420,12 @@ class FlightPlanner: ingress_point.targets.append(building) flight.points.append(point) else: - point = FlightWaypoint(location.position.x, location.position.y, 0) + point = FlightWaypoint( + FlightWaypointType.TARGET_GROUP_LOC, + location.position.x, + location.position.y, + 0 + ) point.description = "STRIKE on " + location.obj_name point.pretty_name = "STRIKE on " + location.obj_name point.name = location.obj_name @@ -415,11 +434,15 @@ class FlightPlanner: flight.points.append(point) egress_pos = location.position.point_from_heading(egress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"]) - egress_point = FlightWaypoint(egress_pos.x, egress_pos.y, self.doctrine["EGRESS_ALT"]) + egress_point = FlightWaypoint( + FlightWaypointType.EGRESS, + egress_pos.x, + egress_pos.y, + self.doctrine["EGRESS_ALT"] + ) egress_point.name = "EGRESS" egress_point.pretty_name = "EGRESS from " + location.obj_name egress_point.description = "EGRESS from " + location.obj_name - egress_point.waypoint_type = FlightWaypointType.EGRESS flight.points.append(egress_point) descend = self.generate_descend_point(flight.from_cp) @@ -454,18 +477,26 @@ class FlightPlanner: ascend = self.generate_ascend_point(flight.from_cp) flight.points.append(ascend) - orbit0 = FlightWaypoint(orbit0p.x, orbit0p.y, patrol_alt) + orbit0 = FlightWaypoint( + FlightWaypointType.PATROL_TRACK, + orbit0p.x, + orbit0p.y, + patrol_alt + ) orbit0.name = "ORBIT 0" orbit0.description = "Standby between this point and the next one" orbit0.pretty_name = "Race-track start" - orbit0.waypoint_type = FlightWaypointType.PATROL_TRACK flight.points.append(orbit0) - orbit1 = FlightWaypoint(orbit1p.x, orbit1p.y, patrol_alt) + orbit1 = FlightWaypoint( + FlightWaypointType.PATROL, + orbit1p.x, + orbit1p.y, + patrol_alt + ) orbit1.name = "ORBIT 1" orbit1.description = "Standby between this point and the previous one" orbit1.pretty_name = "Race-track end" - orbit1.waypoint_type = FlightWaypointType.PATROL flight.points.append(orbit1) orbit0.targets.append(for_cp) @@ -512,18 +543,26 @@ class FlightPlanner: ascend = self.generate_ascend_point(flight.from_cp) flight.points.append(ascend) - orbit0 = FlightWaypoint(orbit0p.x, orbit0p.y, patrol_alt) + orbit0 = FlightWaypoint( + FlightWaypointType.PATROL_TRACK, + orbit0p.x, + orbit0p.y, + patrol_alt + ) orbit0.name = "ORBIT 0" orbit0.description = "Standby between this point and the next one" orbit0.pretty_name = "Race-track start" - orbit0.waypoint_type = FlightWaypointType.PATROL_TRACK flight.points.append(orbit0) - orbit1 = FlightWaypoint(orbit1p.x, orbit1p.y, patrol_alt) + orbit1 = FlightWaypoint( + FlightWaypointType.PATROL, + orbit1p.x, + orbit1p.y, + patrol_alt + ) orbit1.name = "ORBIT 1" orbit1.description = "Standby between this point and the previous one" orbit1.pretty_name = "Race-track end" - orbit1.waypoint_type = FlightWaypointType.PATROL flight.points.append(orbit1) # Note : Targets of a PATROL TRACK waypoints are the points to be defended @@ -555,49 +594,67 @@ class FlightPlanner: egress_heading = heading - 180 - 25 ingress_pos = location.position.point_from_heading(ingress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"]) - ingress_point = FlightWaypoint(ingress_pos.x, ingress_pos.y, self.doctrine["INGRESS_ALT"]) + ingress_point = FlightWaypoint( + FlightWaypointType.INGRESS_SEAD, + ingress_pos.x, + ingress_pos.y, + self.doctrine["INGRESS_ALT"] + ) ingress_point.name = "INGRESS" ingress_point.pretty_name = "INGRESS on " + location.obj_name ingress_point.description = "INGRESS on " + location.obj_name - ingress_point.waypoint_type = FlightWaypointType.INGRESS_SEAD flight.points.append(ingress_point) if len(custom_targets) > 0: for target in custom_targets: - point = FlightWaypoint(target.position.x, target.position.y, 0) + point = FlightWaypoint( + FlightWaypointType.TARGET_POINT, + target.position.x, + target.position.y, + 0 + ) point.alt_type = "RADIO" if flight.flight_type == FlightType.DEAD: - point.description = "SEAD on " + target.type - point.pretty_name = "SEAD on " + location.obj_name - point.only_for_player = True - else: - point.description = "DEAD on " + location.obj_name + point.description = "DEAD on " + target.type point.pretty_name = "DEAD on " + location.obj_name point.only_for_player = True + else: + point.description = "SEAD on " + location.obj_name + point.pretty_name = "SEAD on " + location.obj_name + point.only_for_player = True + flight.points.append(point) ingress_point.targets.append(location) ingress_point.targetGroup = location - flight.points.append(point) else: - point = FlightWaypoint(location.position.x, location.position.y, 0) + point = FlightWaypoint( + FlightWaypointType.TARGET_GROUP_LOC, + location.position.x, + location.position.y, + 0 + ) point.alt_type = "RADIO" if flight.flight_type == FlightType.DEAD: - point.description = "SEAD on " + location.obj_name - point.pretty_name = "SEAD on " + location.obj_name - point.only_for_player = True - else: point.description = "DEAD on " + location.obj_name point.pretty_name = "DEAD on " + location.obj_name point.only_for_player = True + else: + point.description = "SEAD on " + location.obj_name + point.pretty_name = "SEAD on " + location.obj_name + point.only_for_player = True ingress_point.targets.append(location) ingress_point.targetGroup = location flight.points.append(point) egress_pos = location.position.point_from_heading(egress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"]) - egress_point = FlightWaypoint(egress_pos.x, egress_pos.y, self.doctrine["EGRESS_ALT"]) + egress_point = FlightWaypoint( + FlightWaypointType.EGRESS, + egress_pos.x, + egress_pos.y, + self.doctrine["EGRESS_ALT"] + ) egress_point.name = "EGRESS" egress_point.pretty_name = "EGRESS from " + location.obj_name egress_point.description = "EGRESS from " + location.obj_name - egress_point.waypoint_type = FlightWaypointType.EGRESS flight.points.append(egress_point) descend = self.generate_descend_point(flight.from_cp) @@ -628,28 +685,40 @@ class FlightPlanner: ascend.alt = 500 flight.points.append(ascend) - ingress_point = FlightWaypoint(ingress.x, ingress.y, cap_alt) + ingress_point = FlightWaypoint( + FlightWaypointType.INGRESS_CAS, + ingress.x, + ingress.y, + cap_alt + ) ingress_point.alt_type = "RADIO" ingress_point.name = "INGRESS" ingress_point.pretty_name = "INGRESS" ingress_point.description = "Ingress into CAS area" - ingress_point.waypoint_type = FlightWaypointType.INGRESS_CAS flight.points.append(ingress_point) - center_point = FlightWaypoint(center.x, center.y, cap_alt) + center_point = FlightWaypoint( + FlightWaypointType.CAS, + center.x, + center.y, + cap_alt + ) center_point.alt_type = "RADIO" center_point.description = "Provide CAS" center_point.name = "CAS" center_point.pretty_name = "CAS" - center_point.waypoint_type = FlightWaypointType.CAS flight.points.append(center_point) - egress_point = FlightWaypoint(egress.x, egress.y, cap_alt) + egress_point = FlightWaypoint( + FlightWaypointType.EGRESS, + egress.x, + egress.y, + cap_alt + ) egress_point.alt_type = "RADIO" egress_point.description = "Egress from CAS area" egress_point.name = "EGRESS" egress_point.pretty_name = "EGRESS" - egress_point.waypoint_type = FlightWaypointType.EGRESS flight.points.append(egress_point) descend = self.generate_descend_point(flight.from_cp) @@ -660,7 +729,6 @@ class FlightPlanner: rtb = self.generate_rtb_waypoint(flight.from_cp) flight.points.append(rtb) - def generate_ascend_point(self, from_cp): """ Generate ascend point @@ -669,15 +737,18 @@ class FlightPlanner: """ ascend_heading = from_cp.heading pos_ascend = from_cp.position.point_from_heading(ascend_heading, 10000) - ascend = FlightWaypoint(pos_ascend.x, pos_ascend.y, self.doctrine["PATTERN_ALTITUDE"]) + ascend = FlightWaypoint( + FlightWaypointType.ASCEND_POINT, + pos_ascend.x, + pos_ascend.y, + self.doctrine["PATTERN_ALTITUDE"] + ) ascend.name = "ASCEND" ascend.alt_type = "RADIO" ascend.description = "Ascend" ascend.pretty_name = "Ascend" - ascend.waypoint_type = FlightWaypointType.ASCEND_POINT return ascend - def generate_descend_point(self, from_cp): """ Generate approach/descend point @@ -686,15 +757,18 @@ class FlightPlanner: """ ascend_heading = from_cp.heading descend = from_cp.position.point_from_heading(ascend_heading - 180, 10000) - descend = FlightWaypoint(descend.x, descend.y, self.doctrine["PATTERN_ALTITUDE"]) + descend = FlightWaypoint( + FlightWaypointType.DESCENT_POINT, + descend.x, + descend.y, + self.doctrine["PATTERN_ALTITUDE"] + ) descend.name = "DESCEND" descend.alt_type = "RADIO" descend.description = "Descend to pattern alt" descend.pretty_name = "Descend to pattern alt" - descend.waypoint_type = FlightWaypointType.DESCENT_POINT return descend - def generate_rtb_waypoint(self, from_cp): """ Generate RTB landing point @@ -702,10 +776,14 @@ class FlightPlanner: :return: """ rtb = from_cp.position - rtb = FlightWaypoint(rtb.x, rtb.y, 0) + rtb = FlightWaypoint( + FlightWaypointType.LANDING_POINT, + rtb.x, + rtb.y, + 0 + ) rtb.name = "LANDING" rtb.alt_type = "RADIO" rtb.description = "RTB" rtb.pretty_name = "RTB" - rtb.waypoint_type = FlightWaypointType.LANDING_POINT - return rtb \ No newline at end of file + return rtb diff --git a/gen/flights/ai_flight_planner_db.py b/gen/flights/ai_flight_planner_db.py index dd924f64..c2824ef8 100644 --- a/gen/flights/ai_flight_planner_db.py +++ b/gen/flights/ai_flight_planner_db.py @@ -62,6 +62,8 @@ CAP_CAPABLE = [ P_51D_30_NA, P_51D, P_47D_30, + P_47D_30bl1, + P_47D_40, SpitfireLFMkIXCW, SpitfireLFMkIX, @@ -130,6 +132,8 @@ CAS_CAPABLE = [ P_51D_30_NA, P_51D, P_47D_30, + P_47D_30bl1, + P_47D_40, A_20G, SpitfireLFMkIXCW, @@ -204,6 +208,8 @@ STRIKE_CAPABLE = [ P_51D_30_NA, P_51D, P_47D_30, + P_47D_30bl1, + P_47D_40, A_20G, B_17G, diff --git a/gen/flights/flight.py b/gen/flights/flight.py index 57c7097f..0c5c7956 100644 --- a/gen/flights/flight.py +++ b/gen/flights/flight.py @@ -1,10 +1,10 @@ from enum import Enum from typing import List -from dcs.mission import StartType -from dcs.unittype import UnitType - from game import db +from dcs.unittype import UnitType +from dcs.point import MovingPoint, PointAction +from theater.controlpoint import ControlPoint class FlightType(Enum): @@ -62,7 +62,9 @@ class PredefinedWaypointCategory(Enum): class FlightWaypoint: - def __init__(self, x: float, y: float, alt=0): + def __init__(self, waypoint_type: FlightWaypointType, x: float, y: float, + alt: int = 0) -> None: + self.waypoint_type = waypoint_type self.x = x self.y = y self.alt = alt @@ -73,12 +75,38 @@ class FlightWaypoint: self.targetGroup = None self.obj_name = "" self.pretty_name = "" - self.waypoint_type = FlightWaypointType.TAKEOFF # type: FlightWaypointType - self.category = PredefinedWaypointCategory.NOT_PREDEFINED# type: PredefinedWaypointCategory + self.category: PredefinedWaypointCategory = PredefinedWaypointCategory.NOT_PREDEFINED self.only_for_player = False self.data = None + @classmethod + def from_pydcs(cls, point: MovingPoint, + from_cp: ControlPoint) -> "FlightWaypoint": + waypoint = FlightWaypoint(point.position.x, point.position.y, + point.alt) + waypoint.alt_type = point.alt_type + # Other actions exist... but none of them *should* be the first + # waypoint for a flight. + waypoint.waypoint_type = { + PointAction.TurningPoint: FlightWaypointType.NAV, + PointAction.FlyOverPoint: FlightWaypointType.NAV, + PointAction.FromParkingArea: FlightWaypointType.TAKEOFF, + PointAction.FromParkingAreaHot: FlightWaypointType.TAKEOFF, + PointAction.FromRunway: FlightWaypointType.TAKEOFF, + }[point.action] + if waypoint.waypoint_type == FlightWaypointType.NAV: + waypoint.name = "NAV" + waypoint.pretty_name = "Nav" + waypoint.description = "Nav" + else: + waypoint.name = "TAKEOFF" + waypoint.pretty_name = "Takeoff" + waypoint.description = "Takeoff" + waypoint.description = f"Takeoff from {from_cp.name}" + return waypoint + + class Flight: unit_type: UnitType = None from_cp = None @@ -113,10 +141,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/flights/radio_generator.py b/gen/flights/radio_generator.py deleted file mode 100644 index 1e647287..00000000 --- a/gen/flights/radio_generator.py +++ /dev/null @@ -1,4 +0,0 @@ -from dcs.unitgroup import FlyingGroup - - - diff --git a/gen/groundobjectsgen.py b/gen/groundobjectsgen.py index 89a09293..d1cd5378 100644 --- a/gen/groundobjectsgen.py +++ b/gen/groundobjectsgen.py @@ -1,13 +1,13 @@ -import logging +from dcs.statics import * +from dcs.unit import Ship, Vehicle -from game import db from game.data.building_data import FORTIFICATION_UNITS_ID, FORTIFICATION_UNITS from game.db import unit_type_from_name +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 +16,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 +108,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 +118,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 +134,58 @@ 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, + tacan_callsign=tacan_callsign, + icls=icls_channel, + ) else: diff --git a/gen/kneeboard.py b/gen/kneeboard.py new file mode 100644 index 00000000..6843e395 --- /dev/null +++ b/gen/kneeboard.py @@ -0,0 +1,290 @@ +"""Generates kneeboard pages relevant to the player's mission. + +The player kneeboard includes the following information: + +* Airfield (departure, arrival, divert) info. +* Flight plan (waypoint numbers, names, altitudes). +* Comm channels. +* AWACS info. +* Tanker info. +* JTAC info. + +Things we should add: + +* Flight plan ToT and fuel ladder (current have neither available). +* Support for planning an arrival/divert airfield separate from departure. +* Mission package infrastructure to include information about the larger + mission, i.e. information about the escort flight for a strike package. +* Target information. Steerpoints, preplanned objectives, ToT, etc. + +For multiplayer missions, a kneeboard will be generated per flight. +https://forums.eagle.ru/showthread.php?t=206360 claims that kneeboard pages can +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 + +from PIL import Image, ImageDraw, ImageFont +from dcs.mission import Mission +from dcs.unittype import FlyingType +from tabulate import tabulate + +from . import units +from .aircraft import AIRCRAFT_DATA, FlightData +from .airfields import RunwayData +from .airsupportgen import AwacsInfo, TankerInfo +from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator +from .flights.flight import FlightWaypoint, FlightWaypointType +from .radios import RadioFrequency + + +class KneeboardPageWriter: + """Creates kneeboard images.""" + + def __init__(self, page_margin: int = 24, line_spacing: int = 12) -> None: + self.image = Image.new('RGB', (768, 1024), (0xff, 0xff, 0xff)) + # These font sizes create a relatively full page for current sorties. If + # we start generating more complicated flight plans, or start including + # more information in the comm ladder (the latter of which we should + # probably do), we'll need to split some of this information off into a + # second page. + self.title_font = ImageFont.truetype("arial.ttf", 32) + self.heading_font = ImageFont.truetype("arial.ttf", 24) + self.content_font = ImageFont.truetype("arial.ttf", 20) + self.table_font = ImageFont.truetype( + "resources/fonts/Inconsolata.otf", 20) + self.draw = ImageDraw.Draw(self.image) + self.x = page_margin + self.y = page_margin + self.line_spacing = line_spacing + + @property + def position(self) -> Tuple[int, int]: + return self.x, self.y + + def text(self, text: str, font=None, + fill: Tuple[int, int, int] = (0, 0, 0)) -> None: + if font is None: + font = self.content_font + + self.draw.text(self.position, text, font=font, fill=fill) + width, height = self.draw.textsize(text, font=font) + self.y += height + self.line_spacing + + def title(self, title: str) -> None: + self.text(title, font=self.title_font) + + def heading(self, text: str) -> None: + self.text(text, font=self.heading_font) + + def table(self, cells: List[List[str]], + headers: Optional[List[str]] = None) -> None: + table = tabulate(cells, headers=headers, numalign="right") + self.text(table, font=self.table_font) + + def write(self, path: Path) -> None: + self.image.save(path) + + +class KneeboardPage: + """Base class for all kneeboard pages.""" + + def write(self, path: Path) -> None: + """Writes the kneeboard page to the given path.""" + raise NotImplementedError + + +@dataclass(frozen=True) +class NumberedWaypoint: + number: int + waypoint: FlightWaypoint + + +class FlightPlanBuilder: + def __init__(self) -> None: + self.rows: List[List[str]] = [] + self.target_points: List[NumberedWaypoint] = [] + + def add_waypoint(self, waypoint_num: int, waypoint: FlightWaypoint) -> None: + if waypoint.waypoint_type == FlightWaypointType.TARGET_POINT: + self.target_points.append(NumberedWaypoint(waypoint_num, waypoint)) + return + + if self.target_points: + self.coalesce_target_points() + self.target_points = [] + + self.add_waypoint_row(NumberedWaypoint(waypoint_num, waypoint)) + + def coalesce_target_points(self) -> None: + if len(self.target_points) <= 4: + for steerpoint in self.target_points: + self.add_waypoint_row(steerpoint) + return + + first_waypoint_num = self.target_points[0].number + last_waypoint_num = self.target_points[-1].number + + self.rows.append([ + f"{first_waypoint_num}-{last_waypoint_num}", + "Target points", + "0" + ]) + + def add_waypoint_row(self, waypoint: NumberedWaypoint) -> None: + self.rows.append([ + waypoint.number, + waypoint.waypoint.pretty_name, + str(int(units.meters_to_feet(waypoint.waypoint.alt))) + ]) + + def build(self) -> List[List[str]]: + return self.rows + + +class BriefingPage(KneeboardPage): + """A kneeboard page containing briefing information.""" + def __init__(self, flight: FlightData, comms: List[CommInfo], + awacs: List[AwacsInfo], tankers: List[TankerInfo], + jtacs: List[JtacInfo]) -> None: + self.flight = flight + self.comms = list(comms) + self.awacs = awacs + self.tankers = tankers + self.jtacs = jtacs + self.comms.append(CommInfo("Flight", self.flight.intra_flight_channel)) + + def write(self, path: Path) -> None: + writer = KneeboardPageWriter() + writer.title(f"{self.flight.callsign} Mission Info") + + # TODO: Handle carriers. + writer.heading("Airfield Info") + writer.table([ + 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", "I(C)LS", "RWY"]) + + writer.heading("Flight Plan") + flight_plan_builder = FlightPlanBuilder() + for num, waypoint in enumerate(self.flight.waypoints): + flight_plan_builder.add_waypoint(num, waypoint) + writer.table(flight_plan_builder.build(), + headers=["STPT", "Action", "Alt"]) + + writer.heading("Comm Ladder") + comms = [] + for comm in self.comms: + 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, self.format_frequency(a.freq)]) + writer.table(awacs, headers=["Callsign", "UHF"]) + + writer.heading("Tankers") + tankers = [] + for tanker in self.tankers: + tankers.append([ + tanker.callsign, + tanker.variant, + tanker.tacan, + self.format_frequency(tanker.freq), + ]) + writer.table(tankers, headers=["Callsign", "Type", "TACAN", "UHF"]) + + writer.heading("JTAC") + jtacs = [] + for jtac in self.jtacs: + jtacs.append([jtac.callsign, jtac.region, jtac.code]) + writer.table(jtacs, headers=["Callsign", "Region", "Laser Code"]) + + writer.write(path) + + def airfield_info_row(self, row_title: 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". + runway: The runway described by this row. + + Returns: + A list of strings to be used as a row of the airfield table. + """ + if runway is None: + return [row_title, "", "", "", "", ""] + + atc = "" + if runway.atc is not None: + atc = self.format_frequency(runway.atc) + return [ + row_title, + runway.airfield_name, + atc, + runway.tacan or "", + runway.ils or runway.icls or "", + runway.runway_name, + ] + + def format_frequency(self, frequency: RadioFrequency) -> str: + channel = self.flight.channel_for(frequency) + if channel is None: + return str(frequency) + + namer = AIRCRAFT_DATA[self.flight.aircraft_type.id].channel_namer + channel_name = namer.channel_name(channel.radio_id, channel.channel) + return f"{channel_name} {frequency}" + + +class KneeboardGenerator(MissionInfoGenerator): + """Creates kneeboard pages for each client flight in the mission.""" + + def __init__(self, mission: Mission) -> None: + super().__init__(mission) + + 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().items(): + aircraft_dir = temp_dir / aircraft.id + aircraft_dir.mkdir(exist_ok=True) + for idx, page in enumerate(pages): + page_path = aircraft_dir / f"page{idx:02}.png" + page.write(page_path) + self.mission.add_aircraft_kneeboard(aircraft, page_path) + + 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 + group-specific kneeboard pages, flights (possibly from opposing sides) + will be able to see the kneeboards of all aircraft of the same type. + + Returns: + A dict mapping aircraft types to the list of kneeboard pages for + that aircraft. + """ + all_flights: Dict[FlyingType, List[KneeboardPage]] = defaultdict(list) + for flight in self.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: FlightData) -> List[KneeboardPage]: + """Returns a list of kneeboard pages for the given flight.""" + return [ + BriefingPage( + flight, self.comms, self.awacs, self.tankers, self.jtacs + ), + ] diff --git a/gen/radios.py b/gen/radios.py new file mode 100644 index 00000000..bf4f1447 --- /dev/null +++ b/gen/radios.py @@ -0,0 +1,226 @@ +"""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)), + + # Note: The M2000C V/UHF can operate in both ranges, but has a gap between + # 150 MHz and 225 MHz. We can't allocate in that gap, and the current + # system doesn't model gaps, so just pretend it ends at 150 MHz for now. We + # can model gaps later if needed. + Radio("TRT ERA 7000 V/UHF", MHz(118), MHz(150), step=MHz(1)), + Radio("TRT ERA 7200 UHF", MHz(225), MHz(400), step=MHz(1)), + + # Tomcat radios + # # https://www.heatblur.se/F-14Manual/general.html#an-arc-159-uhf-1-radio + Radio("AN/ARC-159", MHz(225), MHz(400), step=MHz(1)), + # AN/ARC-182 can also operate from 30 MHz to 88 MHz, as well as from 225 MHz + # to 400 MHz range, but we can't model gaps with the current implementation. + # https://www.heatblur.se/F-14Manual/general.html#an-arc-182-v-uhf-2-radio + Radio("AN/ARC-182", MHz(108), MHz(174), step=MHz(1)), + + # Also capable of [103, 156) at 25 kHz intervals, but we can't do gaps. + Radio("FR 22", MHz(225), MHz(400), step=kHz(50)), + + # P-51 / P-47 Radio + # 4 preset channels (A/B/C/D) + Radio("SCR522", MHz(100), MHz(156), step=kHz(25)), + + Radio("R&S M3AR VHF", MHz(108), MHz(174), step=MHz(1)), + Radio("R&S M3AR UHF", MHz(225), MHz(400), step=MHz(1)), +] + + +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 + self.reserve(channel) + 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/sam/aaa_bofors.py b/gen/sam/aaa_bofors.py index 7cab286a..528edd8b 100644 --- a/gen/sam/aaa_bofors.py +++ b/gen/sam/aaa_bofors.py @@ -10,9 +10,12 @@ class BoforsGenerator(GroupGenerator): This generate a Bofors flak artillery group """ + name = "Bofors AAA" + price = 75 + def generate(self): - grid_x = random.randint(2, 4) - grid_y = random.randint(2, 4) + grid_x = random.randint(2, 3) + grid_y = random.randint(2, 3) spacing = random.randint(10,40) diff --git a/gen/sam/aaa_flak.py b/gen/sam/aaa_flak.py index bd988910..5a0d9121 100644 --- a/gen/sam/aaa_flak.py +++ b/gen/sam/aaa_flak.py @@ -11,11 +11,14 @@ class FlakGenerator(GroupGenerator): This generate a German flak artillery group """ - def generate(self): - grid_x = random.randint(2, 4) - grid_y = random.randint(2, 4) + name = "Flak Site" + price = 135 - spacing = random.randint(30,60) + def generate(self): + grid_x = random.randint(2, 3) + grid_y = random.randint(2, 3) + + spacing = random.randint(30, 60) index = 0 mixed = random.choice([True, False]) @@ -32,7 +35,7 @@ class FlakGenerator(GroupGenerator): unit_type = random.choice(GFLAK) # Search lights - search_pos = self.get_circular_position(random.randint(2,5), 90) + search_pos = self.get_circular_position(random.randint(2,3), 90) for index, pos in enumerate(search_pos): self.add_unit(AirDefence.Flak_Searchlight_37, "SearchLight#" + str(index), pos[0], pos[1], self.heading) diff --git a/gen/sam/aaa_zu23_insurgent.py b/gen/sam/aaa_zu23_insurgent.py index ec207d84..ec659756 100644 --- a/gen/sam/aaa_zu23_insurgent.py +++ b/gen/sam/aaa_zu23_insurgent.py @@ -10,9 +10,12 @@ class ZU23InsurgentGenerator(GroupGenerator): This generate a ZU23 insurgent flak artillery group """ + name = "Zu-23 Site" + price = 56 + def generate(self): - grid_x = random.randint(2, 4) - grid_y = random.randint(2, 4) + grid_x = random.randint(2, 3) + grid_y = random.randint(2, 3) spacing = random.randint(10,40) diff --git a/gen/sam/sam_avenger.py b/gen/sam/sam_avenger.py index d2d7c52b..44d3aed9 100644 --- a/gen/sam/sam_avenger.py +++ b/gen/sam/sam_avenger.py @@ -10,6 +10,9 @@ class AvengerGenerator(GroupGenerator): This generate an Avenger group """ + name = "Avenger Group" + price = 62 + def generate(self): num_launchers = random.randint(2, 3) diff --git a/gen/sam/sam_chaparral.py b/gen/sam/sam_chaparral.py index 09d0f2a8..a8d89181 100644 --- a/gen/sam/sam_chaparral.py +++ b/gen/sam/sam_chaparral.py @@ -10,6 +10,9 @@ class ChaparralGenerator(GroupGenerator): This generate a Chaparral group """ + name = "Chaparral Group" + price = 66 + def generate(self): num_launchers = random.randint(2, 4) diff --git a/gen/sam/sam_gepard.py b/gen/sam/sam_gepard.py index 15daa7d3..501ed7b7 100644 --- a/gen/sam/sam_gepard.py +++ b/gen/sam/sam_gepard.py @@ -10,6 +10,9 @@ class GepardGenerator(GroupGenerator): This generate a Gepard group """ + name = "Gepard Group" + price = 50 + def generate(self): self.add_unit(AirDefence.SPAAA_Gepard, "SPAAA", self.position.x, self.position.y, self.heading) if random.randint(0, 1) == 1: diff --git a/gen/sam/sam_group_generator.py b/gen/sam/sam_group_generator.py index 7979cb16..add0c9fa 100644 --- a/gen/sam/sam_group_generator.py +++ b/gen/sam/sam_group_generator.py @@ -1,5 +1,7 @@ import random +from typing import List +from dcs.unittype import UnitType from dcs.vehicles import AirDefence from game import db @@ -99,6 +101,23 @@ SAM_PRICES = { AirDefence.HQ_7_Self_Propelled_LN: 35 } + +def get_faction_possible_sams_units(faction: str) -> List[UnitType]: + """ + Return the list + :param faction: Faction to search units for + """ + return [u for u in db.FACTIONS[faction]["units"] if u in AirDefence.__dict__.values()] + + +def get_faction_possible_sams_generator(faction: str) -> List[UnitType]: + """ + Return the list of possible SAM generator for the given faction + :param faction: Faction to search units for + """ + return [SAM_MAP[u] for u in get_faction_possible_sams_units(faction)] + + def generate_anti_air_group(game, parent_cp, ground_object, faction:str): """ This generate a SAM group @@ -107,7 +126,7 @@ def generate_anti_air_group(game, parent_cp, ground_object, faction:str): :param country: Owner country :return: Nothing, but put the group reference inside the ground object """ - possible_sams = [u for u in db.FACTIONS[faction]["units"] if u in AirDefence.__dict__.values()] + possible_sams = get_faction_possible_sams_units(faction) if len(possible_sams) > 0: sam = random.choice(possible_sams) generator = SAM_MAP[sam](game, ground_object) diff --git a/gen/sam/sam_hawk.py b/gen/sam/sam_hawk.py index 4b43b79d..89c11bc0 100644 --- a/gen/sam/sam_hawk.py +++ b/gen/sam/sam_hawk.py @@ -10,6 +10,9 @@ class HawkGenerator(GroupGenerator): This generate an HAWK group """ + name = "Hawk Site" + price = 115 + def generate(self): self.add_unit(AirDefence.SAM_Hawk_PCP, "PCP", self.position.x, self.position.y, self.heading) self.add_unit(AirDefence.SAM_Hawk_SR_AN_MPQ_50, "SR", self.position.x + 20, self.position.y, self.heading) diff --git a/gen/sam/sam_hq7.py b/gen/sam/sam_hq7.py index 8bd5f528..f8a531ea 100644 --- a/gen/sam/sam_hq7.py +++ b/gen/sam/sam_hq7.py @@ -10,6 +10,9 @@ class HQ7Generator(GroupGenerator): This generate an HQ7 group """ + name = "HQ-7 Site" + price = 120 + def generate(self): self.add_unit(AirDefence.HQ_7_Self_Propelled_STR, "STR", self.position.x, self.position.y, self.heading) self.add_unit(AirDefence.HQ_7_Self_Propelled_LN, "LN", self.position.x + 20, self.position.y, self.heading) diff --git a/gen/sam/sam_linebacker.py b/gen/sam/sam_linebacker.py index be2dafb7..946d14ed 100644 --- a/gen/sam/sam_linebacker.py +++ b/gen/sam/sam_linebacker.py @@ -10,6 +10,9 @@ class LinebackerGenerator(GroupGenerator): This generate an m6 linebacker group """ + name = "Linebacker Group" + price = 75 + def generate(self): num_launchers = random.randint(2, 4) diff --git a/gen/sam/sam_patriot.py b/gen/sam/sam_patriot.py index 5505ba2a..b55dbaea 100644 --- a/gen/sam/sam_patriot.py +++ b/gen/sam/sam_patriot.py @@ -10,6 +10,9 @@ class PatriotGenerator(GroupGenerator): This generate a Patriot group """ + name = "Patriot Battery" + price = 240 + def generate(self): # Command Post self.add_unit(AirDefence.SAM_Patriot_AMG_AN_MRC_137, "MRC", self.position.x, self.position.y, self.heading) @@ -18,13 +21,13 @@ class PatriotGenerator(GroupGenerator): self.add_unit(AirDefence.SAM_Patriot_EPP_III, "EPP", self.position.x, self.position.y + 30, self.heading) self.add_unit(AirDefence.SAM_Patriot_STR_AN_MPQ_53, "ICC", self.position.x + 30, self.position.y + 30, self.heading) - num_launchers = random.randint(2, 4) + num_launchers = random.randint(3, 4) positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=360) for i, position in enumerate(positions): self.add_unit(AirDefence.SAM_Patriot_LN_M901, "LN#" + str(i), position[0], position[1], position[2]) # Short range protection for high value site - num_launchers = random.randint(2, 4) + num_launchers = random.randint(3, 4) positions = self.get_circular_position(num_launchers, launcher_distance=300, coverage=360) for i, position in enumerate(positions): self.add_unit(AirDefence.AAA_Vulcan_M163, "SPAAA#" + str(i), position[0], position[1], position[2]) diff --git a/gen/sam/sam_rapier.py b/gen/sam/sam_rapier.py index 2ff0d2bb..99b7b205 100644 --- a/gen/sam/sam_rapier.py +++ b/gen/sam/sam_rapier.py @@ -10,6 +10,9 @@ class RapierGenerator(GroupGenerator): This generate a Rapier Group """ + name = "Rapier AA Site" + price = 50 + def generate(self): self.add_unit(AirDefence.Rapier_FSA_Blindfire_Tracker, "BT", self.position.x, self.position.y, self.heading) self.add_unit(AirDefence.Rapier_FSA_Optical_Tracker, "OT", self.position.x + 20, self.position.y, self.heading) diff --git a/gen/sam/sam_roland.py b/gen/sam/sam_roland.py index 6ee438e6..9e31d5fe 100644 --- a/gen/sam/sam_roland.py +++ b/gen/sam/sam_roland.py @@ -8,6 +8,9 @@ class RolandGenerator(GroupGenerator): This generate a Roland group """ + name = "Roland Site" + price = 40 + def generate(self): self.add_unit(AirDefence.SAM_Roland_ADS, "ADS", self.position.x, self.position.y, self.heading) self.add_unit(AirDefence.SAM_Roland_EWR, "EWR", self.position.x + 40, self.position.y, self.heading) diff --git a/gen/sam/sam_sa10.py b/gen/sam/sam_sa10.py index a5332546..ae2102f0 100644 --- a/gen/sam/sam_sa10.py +++ b/gen/sam/sam_sa10.py @@ -10,6 +10,9 @@ class SA10Generator(GroupGenerator): This generate a SA-10 group """ + name = "SA-10/S-300PS Battery" + price = 450 + def generate(self): # Command Post self.add_unit(AirDefence.SAM_SA_10_S_300PS_CP_54K6, "CP", self.position.x, self.position.y, self.heading) diff --git a/gen/sam/sam_sa11.py b/gen/sam/sam_sa11.py index d3709550..3af6c242 100644 --- a/gen/sam/sam_sa11.py +++ b/gen/sam/sam_sa11.py @@ -10,6 +10,9 @@ class SA11Generator(GroupGenerator): This generate a SA-11 group """ + name = "SA-11 Buk Battery" + price = 180 + def generate(self): self.add_unit(AirDefence.SAM_SA_11_Buk_CC_9S470M1, "CC", self.position.x, self.position.y, self.heading) self.add_unit(AirDefence.SAM_SA_11_Buk_SR_9S18M1, "SR", self.position.x+20, self.position.y, self.heading) diff --git a/gen/sam/sam_sa13.py b/gen/sam/sam_sa13.py index c3e45745..8fc069ad 100644 --- a/gen/sam/sam_sa13.py +++ b/gen/sam/sam_sa13.py @@ -10,6 +10,9 @@ class SA13Generator(GroupGenerator): This generate a SA-13 group """ + name = "SA-13 Strela Group" + price = 50 + def generate(self): self.add_unit(Unarmed.Transport_UAZ_469, "UAZ", self.position.x, self.position.y, self.heading) self.add_unit(Unarmed.Transport_KAMAZ_43101, "TRUCK", self.position.x+40, self.position.y, self.heading) diff --git a/gen/sam/sam_sa15.py b/gen/sam/sam_sa15.py index 592c1a34..09fda2ee 100644 --- a/gen/sam/sam_sa15.py +++ b/gen/sam/sam_sa15.py @@ -8,6 +8,9 @@ class SA15Generator(GroupGenerator): This generate a SA-15 group """ + name = "SA-15 Tor Group" + price = 55 + def generate(self): self.add_unit(AirDefence.SAM_SA_15_Tor_9A331, "ADS", self.position.x, self.position.y, self.heading) self.add_unit(Unarmed.Transport_UAZ_469, "EWR", self.position.x + 40, self.position.y, self.heading) diff --git a/gen/sam/sam_sa19.py b/gen/sam/sam_sa19.py index b81fd35d..c4f710f4 100644 --- a/gen/sam/sam_sa19.py +++ b/gen/sam/sam_sa19.py @@ -10,6 +10,9 @@ class SA19Generator(GroupGenerator): This generate a SA-19 group """ + name = "SA-19 Tunguska Group" + price = 90 + def generate(self): num_launchers = random.randint(1, 3) diff --git a/gen/sam/sam_sa2.py b/gen/sam/sam_sa2.py index 384b3a06..c108c1e8 100644 --- a/gen/sam/sam_sa2.py +++ b/gen/sam/sam_sa2.py @@ -10,6 +10,9 @@ class SA2Generator(GroupGenerator): This generate a SA-2 group """ + name = "SA-2/S-75 Site" + price = 74 + def generate(self): self.add_unit(AirDefence.SAM_SR_P_19, "SR", self.position.x, self.position.y, self.heading) self.add_unit(AirDefence.SAM_SA_2_TR_SNR_75_Fan_Song, "TR", self.position.x + 20, self.position.y, self.heading) diff --git a/gen/sam/sam_sa3.py b/gen/sam/sam_sa3.py index d4095a7e..455bab19 100644 --- a/gen/sam/sam_sa3.py +++ b/gen/sam/sam_sa3.py @@ -10,6 +10,9 @@ class SA3Generator(GroupGenerator): This generate a SA-3 group """ + name = "SA-3/S-125 Site" + price = 80 + def generate(self): self.add_unit(AirDefence.SAM_SR_P_19, "SR", self.position.x, self.position.y, self.heading) self.add_unit(AirDefence.SAM_SA_3_S_125_TR_SNR, "TR", self.position.x + 20, self.position.y, self.heading) diff --git a/gen/sam/sam_sa6.py b/gen/sam/sam_sa6.py index 64c6c15c..7ec2afca 100644 --- a/gen/sam/sam_sa6.py +++ b/gen/sam/sam_sa6.py @@ -10,6 +10,9 @@ class SA6Generator(GroupGenerator): This generate a SA-6 group """ + name = "SA-6 Kub Site" + price = 102 + def generate(self): self.add_unit(AirDefence.SAM_SA_6_Kub_STR_9S91, "STR", self.position.x, self.position.y, self.heading) diff --git a/gen/sam/sam_sa8.py b/gen/sam/sam_sa8.py index d2e7e8d6..1c09dd2e 100644 --- a/gen/sam/sam_sa8.py +++ b/gen/sam/sam_sa8.py @@ -10,6 +10,9 @@ class SA8Generator(GroupGenerator): This generate a SA-8 group """ + name = "SA-8 OSA Site" + price = 55 + def generate(self): self.add_unit(AirDefence.SAM_SA_8_Osa_9A33, "OSA", self.position.x, self.position.y, self.heading) self.add_unit(AirDefence.SAM_SA_8_Osa_LD_9T217, "LD", self.position.x + 20, self.position.y, self.heading) diff --git a/gen/sam/sam_sa9.py b/gen/sam/sam_sa9.py index f16b7cca..d0045bea 100644 --- a/gen/sam/sam_sa9.py +++ b/gen/sam/sam_sa9.py @@ -10,6 +10,9 @@ class SA9Generator(GroupGenerator): This generate a SA-9 group """ + name = "SA-9 Group" + price = 40 + def generate(self): self.add_unit(Unarmed.Transport_UAZ_469, "UAZ", self.position.x, self.position.y, self.heading) self.add_unit(Unarmed.Transport_KAMAZ_43101, "TRUCK", self.position.x+40, self.position.y, self.heading) diff --git a/gen/sam/sam_vulcan.py b/gen/sam/sam_vulcan.py index 7cd8d7aa..77cfc0a2 100644 --- a/gen/sam/sam_vulcan.py +++ b/gen/sam/sam_vulcan.py @@ -10,6 +10,9 @@ class VulcanGenerator(GroupGenerator): This generate a Vulcan group """ + name = "Vulcan Group" + price = 25 + def generate(self): self.add_unit(AirDefence.AAA_Vulcan_M163, "SPAAA", self.position.x, self.position.y, self.heading) if random.randint(0, 1) == 1: diff --git a/gen/sam/sam_zsu23.py b/gen/sam/sam_zsu23.py index b0707416..7c90cb4d 100644 --- a/gen/sam/sam_zsu23.py +++ b/gen/sam/sam_zsu23.py @@ -10,8 +10,11 @@ class ZSU23Generator(GroupGenerator): This generate a ZSU 23 group """ + name = "ZSU-23 Group" + price = 50 + def generate(self): - num_launchers = random.randint(2, 5) + num_launchers = random.randint(4, 5) positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=180) for i, position in enumerate(positions): diff --git a/gen/sam/sam_zu23.py b/gen/sam/sam_zu23.py index 34a892ab..3134c3a7 100644 --- a/gen/sam/sam_zu23.py +++ b/gen/sam/sam_zu23.py @@ -10,9 +10,12 @@ class ZU23Generator(GroupGenerator): This generate a ZU23 flak artillery group """ + name = "ZU-23 Group" + price = 54 + def generate(self): - grid_x = random.randint(2, 4) - grid_y = random.randint(2, 4) + grid_x = random.randint(2, 3) + grid_y = random.randint(2, 3) spacing = random.randint(10,40) diff --git a/gen/sam/sam_zu23_ural.py b/gen/sam/sam_zu23_ural.py index 248b84a0..1eb31b22 100644 --- a/gen/sam/sam_zu23_ural.py +++ b/gen/sam/sam_zu23_ural.py @@ -10,6 +10,9 @@ class ZU23UralGenerator(GroupGenerator): This generate a Zu23 Ural group """ + name = "ZU-23 Ural Group" + price = 64 + def generate(self): num_launchers = random.randint(2, 8) diff --git a/gen/sam/sam_zu23_ural_insurgent.py b/gen/sam/sam_zu23_ural_insurgent.py index 282f3d20..4512cfc7 100644 --- a/gen/sam/sam_zu23_ural_insurgent.py +++ b/gen/sam/sam_zu23_ural_insurgent.py @@ -10,6 +10,9 @@ class ZU23UralInsurgentGenerator(GroupGenerator): This generate a Zu23 Ural group """ + name = "ZU-23 Ural Insurgent Group" + price = 64 + def generate(self): num_launchers = random.randint(2, 8) 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) diff --git a/gen/units.py b/gen/units.py new file mode 100644 index 00000000..005e1576 --- /dev/null +++ b/gen/units.py @@ -0,0 +1,6 @@ +"""Unit conversions.""" + + +def meters_to_feet(meters: float) -> float: + """Convers meters to feet.""" + return meters * 3.28084 \ No newline at end of file diff --git a/pydcs b/pydcs index dcc3d846..f46781b8 160000 --- a/pydcs +++ b/pydcs @@ -1 +1 @@ -Subproject commit dcc3d846316af2925c93ae09840c3ab4a1150e59 +Subproject commit f46781b854102a9f06948c8fb81a40331b78459e diff --git a/qt_ui/main.py b/qt_ui/main.py index dff7e51b..e019d32c 100644 --- a/qt_ui/main.py +++ b/qt_ui/main.py @@ -4,7 +4,7 @@ import logging import os import sys -from pydcs import dcs +import dcs from PySide2 import QtWidgets from PySide2.QtGui import QPixmap from PySide2.QtWidgets import QApplication, QSplashScreen diff --git a/qt_ui/uiconstants.py b/qt_ui/uiconstants.py index c0d52631..02b9d14a 100644 --- a/qt_ui/uiconstants.py +++ b/qt_ui/uiconstants.py @@ -8,7 +8,7 @@ from game.event import UnitsDeliveryEvent, FrontlineAttackEvent from theater.theatergroundobject import CATEGORY_MAP from userdata.liberation_theme import get_theme_icons -VERSION_STRING = "2.1.0" +VERSION_STRING = "2.1.1" URLS : Dict[str, str] = { "Manual": "https://github.com/khopa/dcs_liberation/wiki", diff --git a/qt_ui/widgets/QTopPanel.py b/qt_ui/widgets/QTopPanel.py index 2a77cb23..30725095 100644 --- a/qt_ui/widgets/QTopPanel.py +++ b/qt_ui/widgets/QTopPanel.py @@ -21,6 +21,7 @@ class QTopPanel(QFrame): self.setMaximumHeight(70) self.init_ui() GameUpdateSignal.get_instance().gameupdated.connect(self.setGame) + GameUpdateSignal.get_instance().budgetupdated.connect(self.budget_update) def init_ui(self): @@ -101,4 +102,7 @@ class QTopPanel(QFrame): def proceed(self): self.subwindow = QMissionPlanning(self.game) - self.subwindow.show() \ No newline at end of file + self.subwindow.show() + + def budget_update(self, game:Game): + self.budgetBox.setGame(game) diff --git a/qt_ui/widgets/combos/QFilteredComboBox.py b/qt_ui/widgets/combos/QFilteredComboBox.py index 9597c42b..7d152b2e 100644 --- a/qt_ui/widgets/combos/QFilteredComboBox.py +++ b/qt_ui/widgets/combos/QFilteredComboBox.py @@ -35,6 +35,7 @@ class QFilteredComboBox(QComboBox): super(QFilteredComboBox, self).setModel(model) self.pFilterModel.setSourceModel(model) self.completer.setModel(self.pFilterModel) + self.model().sort(0) def setModelColumn(self, column): self.completer.setCompletionColumn(column) diff --git a/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py b/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py index c8721a17..079aab99 100644 --- a/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py +++ b/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py @@ -57,13 +57,16 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox): enemy_cp = [ecp for ecp in cp.connected_points if ecp.captured != cp.captured] for ecp in enemy_cp: pos = Conflict.frontline_position(self.game.theater, cp, ecp)[0] - wpt = FlightWaypoint(pos.x, pos.y, 800) + wpt = FlightWaypoint( + FlightWaypointType.CUSTOM, + pos.x, + pos.y, + 800) wpt.name = "Frontline " + cp.name + "/" + ecp.name + " [CAS]" wpt.alt_type = "RADIO" wpt.pretty_name = wpt.name wpt.description = "Frontline" wpt.data = [cp, ecp] - wpt.waypoint_type = FlightWaypointType.CUSTOM wpt.category = PredefinedWaypointCategory.FRONTLINE i = add_model_item(i, model, wpt.pretty_name, wpt) @@ -72,14 +75,18 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox): if (self.include_enemy and not cp.captured) or (self.include_friendly and cp.captured): for ground_object in cp.ground_objects: if not ground_object.is_dead and not ground_object.dcs_identifier == "AA": - wpt = FlightWaypoint(ground_object.position.x,ground_object.position.y, 0) + wpt = FlightWaypoint( + FlightWaypointType.CUSTOM, + ground_object.position.x, + ground_object.position.y, + 0 + ) wpt.alt_type = "RADIO" wpt.name = wpt.name = "[" + str(ground_object.obj_name) + "] : " + ground_object.category + " #" + str(ground_object.object_id) wpt.pretty_name = wpt.name wpt.obj_name = ground_object.obj_name wpt.targets.append(ground_object) wpt.data = ground_object - wpt.waypoint_type = FlightWaypointType.CUSTOM if cp.captured: wpt.description = "Friendly Building" wpt.category = PredefinedWaypointCategory.ALLY_BUILDING @@ -95,7 +102,12 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox): if not ground_object.is_dead and ground_object.dcs_identifier == "AA": for g in ground_object.groups: for j, u in enumerate(g.units): - wpt = FlightWaypoint(u.position.x, u.position.y, 0) + wpt = FlightWaypoint( + FlightWaypointType.CUSTOM, + u.position.x, + u.position.y, + 0 + ) wpt.alt_type = "RADIO" wpt.name = wpt.name = "[" + str(ground_object.obj_name) + "] : " + u.type + " #" + str(j) wpt.pretty_name = wpt.name @@ -114,11 +126,15 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox): if self.include_airbases: for cp in self.game.theater.controlpoints: if (self.include_enemy and not cp.captured) or (self.include_friendly and cp.captured): - wpt = FlightWaypoint(cp.position.x, cp.position.y, 0) + wpt = FlightWaypoint( + FlightWaypointType.CUSTOM, + cp.position.x, + cp.position.y, + 0 + ) wpt.alt_type = "RADIO" wpt.name = cp.name wpt.data = cp - wpt.waypoint_type = FlightWaypointType.CUSTOM if cp.captured: wpt.description = "Position of " + cp.name + " [Friendly Airbase]" wpt.category = PredefinedWaypointCategory.ALLY_CP @@ -133,7 +149,6 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox): else: wpt.pretty_name = cp.name + " (Airbase)" - i = add_model_item(i, model, wpt.pretty_name, wpt) self.setModel(model) diff --git a/qt_ui/windows/GameUpdateSignal.py b/qt_ui/windows/GameUpdateSignal.py index 86546757..dd32dd58 100644 --- a/qt_ui/windows/GameUpdateSignal.py +++ b/qt_ui/windows/GameUpdateSignal.py @@ -16,6 +16,7 @@ class GameUpdateSignal(QObject): instance = None gameupdated = Signal(Game) + budgetupdated = Signal(Game) debriefingReceived = Signal(DebriefingSignal) def __init__(self): @@ -25,6 +26,9 @@ class GameUpdateSignal(QObject): def updateGame(self, game: Game): self.gameupdated.emit(game) + def updateBudget(self, game: Game): + self.budgetupdated.emit(game) + def sendDebriefing(self, game: Game, gameEvent: Event, debriefing: Debriefing): sig = DebriefingSignal(game, gameEvent, debriefing) self.gameupdated.emit(game) diff --git a/qt_ui/windows/QLiberationWindow.py b/qt_ui/windows/QLiberationWindow.py index 382b3289..c51547e6 100644 --- a/qt_ui/windows/QLiberationWindow.py +++ b/qt_ui/windows/QLiberationWindow.py @@ -29,7 +29,7 @@ class QLiberationWindow(QMainWindow): self.setGame(persistency.restore_game()) self.setGeometry(300, 100, 270, 100) - self.setWindowTitle("DCS Liberation") + self.setWindowTitle("DCS Liberation - v" + CONST.VERSION_STRING) self.setWindowIcon(QIcon("./resources/icon.png")) self.statusBar().showMessage('Ready') @@ -69,27 +69,31 @@ class QLiberationWindow(QMainWindow): GameUpdateSignal.get_instance().debriefingReceived.connect(self.onDebriefing) def initActions(self): - self.newGameAction = QAction("New Game", self) + self.newGameAction = QAction("&New Game", self) self.newGameAction.setIcon(QIcon(CONST.ICONS["New"])) self.newGameAction.triggered.connect(self.newGame) + self.newGameAction.setShortcut('CTRL+N') - self.openAction = QAction("Open", self) + self.openAction = QAction("&Open", self) self.openAction.setIcon(QIcon(CONST.ICONS["Open"])) self.openAction.triggered.connect(self.openFile) + self.openAction.setShortcut('CTRL+O') - self.saveGameAction = QAction("Save", self) + self.saveGameAction = QAction("&Save", self) self.saveGameAction.setIcon(QIcon(CONST.ICONS["Save"])) self.saveGameAction.triggered.connect(self.saveGame) + self.saveGameAction.setShortcut('CTRL+S') - self.saveAsAction = QAction("Save As", self) + self.saveAsAction = QAction("Save &As", self) self.saveAsAction.setIcon(QIcon(CONST.ICONS["Save"])) self.saveAsAction.triggered.connect(self.saveGameAs) + self.saveAsAction.setShortcut('CTRL+A') - self.showAboutDialogAction = QAction("About DCS Liberation", self) + self.showAboutDialogAction = QAction("&About DCS Liberation", self) self.showAboutDialogAction.setIcon(QIcon.fromTheme("help-about")) self.showAboutDialogAction.triggered.connect(self.showAboutDialog) - self.showLiberationPrefDialogAction = QAction("Preferences", self) + self.showLiberationPrefDialogAction = QAction("&Preferences", self) self.showLiberationPrefDialogAction.setIcon(QIcon.fromTheme("help-about")) self.showLiberationPrefDialogAction.triggered.connect(self.showLiberationDialog) @@ -102,57 +106,47 @@ class QLiberationWindow(QMainWindow): def initMenuBar(self): self.menu = self.menuBar() - file_menu = self.menu.addMenu("File") + file_menu = self.menu.addMenu("&File") file_menu.addAction(self.newGameAction) file_menu.addAction(self.openAction) + file_menu.addSeparator() file_menu.addAction(self.saveGameAction) file_menu.addAction(self.saveAsAction) file_menu.addSeparator() file_menu.addAction(self.showLiberationPrefDialogAction) file_menu.addSeparator() #file_menu.addAction("Close Current Game", lambda: self.closeGame()) # Not working - file_menu.addAction("Exit" , lambda: self.exit()) + file_menu.addAction("E&xit" , lambda: self.exit()) - help_menu = self.menu.addMenu("Help") - help_menu.addAction("Discord Server", lambda: webbrowser.open_new_tab("https://" + "discord.gg" + "/" + "bKrt" + "rkJ")) - help_menu.addAction("Github Repository", lambda: webbrowser.open_new_tab("https://github.com/khopa/dcs_liberation")) - help_menu.addAction("Releases", lambda: webbrowser.open_new_tab("https://github.com/Khopa/dcs_liberation/releases")) - help_menu.addAction("Online Manual", lambda: webbrowser.open_new_tab(URLS["Manual"])) - help_menu.addAction("ED Forum Thread", lambda: webbrowser.open_new_tab(URLS["ForumThread"])) - help_menu.addAction("Report an issue", lambda: webbrowser.open_new_tab(URLS["Issues"])) + displayMenu = self.menu.addMenu("&Display") - help_menu.addSeparator() - help_menu.addAction(self.showAboutDialogAction) - - displayMenu = self.menu.addMenu("Display") - - tg_cp_visibility = QAction('Control Point', displayMenu) + tg_cp_visibility = QAction('&Control Point', displayMenu) tg_cp_visibility.setCheckable(True) tg_cp_visibility.setChecked(True) tg_cp_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("cp", tg_cp_visibility.isChecked())) - tg_go_visibility = QAction('Ground Objects', displayMenu) + tg_go_visibility = QAction('&Ground Objects', displayMenu) tg_go_visibility.setCheckable(True) tg_go_visibility.setChecked(True) tg_go_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("go", tg_go_visibility.isChecked())) - tg_line_visibility = QAction('Lines', displayMenu) + tg_line_visibility = QAction('&Lines', displayMenu) tg_line_visibility.setCheckable(True) tg_line_visibility.setChecked(True) tg_line_visibility.toggled.connect( lambda: QLiberationMap.set_display_rule("lines", tg_line_visibility.isChecked())) - tg_event_visibility = QAction('Events', displayMenu) + tg_event_visibility = QAction('&Events', displayMenu) tg_event_visibility.setCheckable(True) tg_event_visibility.setChecked(True) tg_event_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("events", tg_event_visibility.isChecked())) - tg_sam_visibility = QAction('SAM Range', displayMenu) + tg_sam_visibility = QAction('&SAM Range', displayMenu) tg_sam_visibility.setCheckable(True) tg_sam_visibility.setChecked(True) tg_sam_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("sam", tg_sam_visibility.isChecked())) - tg_flight_path_visibility = QAction('Flight Paths', displayMenu) + tg_flight_path_visibility = QAction('&Flight Paths', displayMenu) tg_flight_path_visibility.setCheckable(True) tg_flight_path_visibility.setChecked(False) tg_flight_path_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("flight_paths", tg_flight_path_visibility.isChecked())) @@ -164,6 +158,17 @@ class QLiberationWindow(QMainWindow): displayMenu.addAction(tg_sam_visibility) displayMenu.addAction(tg_flight_path_visibility) + help_menu = self.menu.addMenu("&Help") + help_menu.addAction("&Discord Server", lambda: webbrowser.open_new_tab("https://" + "discord.gg" + "/" + "bKrt" + "rkJ")) + help_menu.addAction("&Github Repository", lambda: webbrowser.open_new_tab("https://github.com/khopa/dcs_liberation")) + help_menu.addAction("&Releases", lambda: webbrowser.open_new_tab("https://github.com/Khopa/dcs_liberation/releases")) + help_menu.addAction("&Online Manual", lambda: webbrowser.open_new_tab(URLS["Manual"])) + help_menu.addAction("&ED Forum Thread", lambda: webbrowser.open_new_tab(URLS["ForumThread"])) + help_menu.addAction("Report an &issue", lambda: webbrowser.open_new_tab(URLS["Issues"])) + + help_menu.addSeparator() + help_menu.addAction(self.showAboutDialogAction) + def newGame(self): wizard = NewGameWizard(self) wizard.show() @@ -216,7 +221,7 @@ class QLiberationWindow(QMainWindow): "

Authors

" + \ "

DCS Liberation was originally developed by shdwp, DCS Liberation 2.0 is a partial rewrite based on this work by Khopa." \ "

Contributors

" + \ - "shdwp, Khopa, Wrycu, calvinmorrow, JohanAberg, Deus, root0fall, Captain Cody" + \ + "shdwp, Khopa, ColonelPanic, Wrycu, calvinmorrow, JohanAberg, Deus, root0fall, Captain Cody, steveveepee, pedromagueija, parithon, bwRavencl" + \ "

Special Thanks :

" \ "rp- for the pydcs framework
"\ "Grimes (mrSkortch) & Speed for the MIST framework
"\ diff --git a/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py b/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py index 2b2abb67..370cf65a 100644 --- a/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py +++ b/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py @@ -1,4 +1,5 @@ -from PySide2.QtWidgets import QGridLayout, QLabel, QGroupBox, QPushButton +from PySide2.QtCore import Qt +from PySide2.QtWidgets import QGridLayout, QLabel, QGroupBox, QPushButton, QVBoxLayout from qt_ui.uiconstants import VEHICLES_ICONS from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu @@ -7,19 +8,40 @@ from theater import ControlPoint, TheaterGroundObject class QBaseDefenseGroupInfo(QGroupBox): - def __init__(self, cp:ControlPoint, ground_object: TheaterGroundObject, game): + def __init__(self, cp: ControlPoint, ground_object: TheaterGroundObject, game): super(QBaseDefenseGroupInfo, self).__init__("Group : " + ground_object.obj_name) self.ground_object = ground_object self.cp = cp self.game = game self.buildings = game.theater.find_ground_objects_by_obj_name(self.ground_object.obj_name) + + self.main_layout = QVBoxLayout() + self.unit_layout = QGridLayout() + self.init_ui() - - def init_ui(self): + + self.buildLayout() + manage_button = QPushButton("Manage") + manage_button.setProperty("style", "btn-success") + manage_button.setMaximumWidth(180) + manage_button.clicked.connect(self.onManage) + + self.main_layout.addLayout(self.unit_layout) + self.main_layout.addWidget(manage_button, 0, Qt.AlignLeft) + + self.setLayout(self.main_layout) + + def buildLayout(self): unit_dict = {} - layout = QGridLayout() + for i in range(self.unit_layout.rowCount()): + for j in range(self.unit_layout.columnCount()): + item = self.unit_layout.itemAtPosition(i, j) + if item is not None and item.widget() is not None: + item.widget().setParent(None) + print("Remove " + str(i) + ", " + str(j)) + for g in self.ground_object.groups: for u in g.units: if u.type in unit_dict.keys(): @@ -28,25 +50,27 @@ class QBaseDefenseGroupInfo(QGroupBox): unit_dict[u.type] = 1 i = 0 for k, v in unit_dict.items(): - #icon = QLabel() - #if k in VEHICLES_ICONS.keys(): - # icon.setPixmap(VEHICLES_ICONS[k]) - #else: - # icon.setText("" + k[:6] + "") - #icon.setProperty("style", "icon-plane") - #layout.addWidget(icon, i, 0) - layout.addWidget(QLabel(str(v) + " x " + "" + k + ""), i, 0) + icon = QLabel() + if k in VEHICLES_ICONS.keys(): + icon.setPixmap(VEHICLES_ICONS[k]) + else: + icon.setText("" + k[:8] + "") + icon.setProperty("style", "icon-armor") + self.unit_layout.addWidget(icon, i, 0) + self.unit_layout.addWidget(QLabel(str(v) + " x " + "" + k + ""), i, 1) i = i + 1 - manage_button = QPushButton("Manage") - manage_button.setProperty("style", "btn-success") - manage_button.setMaximumWidth(180) - manage_button.clicked.connect(self.onManage) - layout.addWidget(manage_button, i+1, 0) - self.setLayout(layout) + if len(unit_dict.items()) == 0: + self.unit_layout.addWidget(QLabel("/"), 0, 0) + + + + self.setLayout(self.main_layout) def onManage(self): - self.editionMenu = QGroundObjectMenu(self.window(), self.ground_object, self.buildings, self.cp, self.game) - self.editionMenu.show() - + self.edition_menu = QGroundObjectMenu(self.window(), self.ground_object, self.buildings, self.cp, self.game) + self.edition_menu.show() + self.edition_menu.changed.connect(self.onEdition) + def onEdition(self): + self.buildLayout() \ No newline at end of file diff --git a/qt_ui/windows/basemenu/base_defenses/QBaseInformation.py b/qt_ui/windows/basemenu/base_defenses/QBaseInformation.py index c98113b6..f5325887 100644 --- a/qt_ui/windows/basemenu/base_defenses/QBaseInformation.py +++ b/qt_ui/windows/basemenu/base_defenses/QBaseInformation.py @@ -23,7 +23,6 @@ class QBaseInformation(QFrame): scroll_content = QWidget() task_box_layout = QGridLayout() scroll_content.setLayout(task_box_layout) - row = 0 for g in self.cp.ground_objects: if g.airbase_group: diff --git a/qt_ui/windows/groundobject/QGroundObjectMenu.py b/qt_ui/windows/groundobject/QGroundObjectMenu.py index 41073c2a..dcfed0a3 100644 --- a/qt_ui/windows/groundobject/QGroundObjectMenu.py +++ b/qt_ui/windows/groundobject/QGroundObjectMenu.py @@ -1,12 +1,16 @@ import logging -from PySide2.QtGui import QCloseEvent -from PySide2.QtWidgets import QHBoxLayout, QWidget, QDialog, QGridLayout, QLabel, QGroupBox, QVBoxLayout, QPushButton +from PySide2 import QtCore +from PySide2.QtGui import Qt +from PySide2.QtWidgets import QHBoxLayout, QDialog, QGridLayout, QLabel, QGroupBox, QVBoxLayout, QPushButton, \ + QComboBox, QSpinBox, QMessageBox from dcs import Point -from game import Game +from game import Game, db from game.data.building_data import FORTIFICATION_BUILDINGS -from game.db import PRICES, unit_type_of +from game.db import PRICES, unit_type_of, PinpointStrike +from gen.defenses.armor_group_generator import generate_armor_group_of_type_and_size +from gen.sam.sam_group_generator import get_faction_possible_sams_generator from qt_ui.uiconstants import EVENT_ICONS from qt_ui.widgets.QBudgetBox import QBudgetBox from qt_ui.windows.GameUpdateSignal import GameUpdateSignal @@ -16,6 +20,8 @@ from theater import ControlPoint, TheaterGroundObject class QGroundObjectMenu(QDialog): + changed = QtCore.Signal() + def __init__(self, parent, ground_object: TheaterGroundObject, buildings:[], cp: ControlPoint, game: Game): super(QGroundObjectMenu, self).__init__(parent) self.setMinimumWidth(350) @@ -29,6 +35,8 @@ class QGroundObjectMenu(QDialog): self.buildingBox = QGroupBox("Buildings :") self.intelLayout = QGridLayout() self.buildingsLayout = QGridLayout() + self.sell_all_button = None + self.total_value = 0 self.init_ui() def init_ui(self): @@ -43,9 +51,28 @@ class QGroundObjectMenu(QDialog): self.mainLayout.addWidget(self.intelBox) else: self.mainLayout.addWidget(self.buildingBox) + + self.actionLayout = QHBoxLayout() + + self.sell_all_button = QPushButton("Disband (+" + str(self.total_value) + "M)") + self.sell_all_button.clicked.connect(self.sell_all) + self.sell_all_button.setProperty("style", "btn-danger") + + self.buy_replace = QPushButton("Buy/Replace") + self.buy_replace.clicked.connect(self.buy_group) + self.buy_replace.setProperty("style", "btn-success") + + if self.total_value > 0: + self.actionLayout.addWidget(self.sell_all_button) + self.actionLayout.addWidget(self.buy_replace) + + if self.cp.captured and self.ground_object.dcs_identifier == "AA": + self.mainLayout.addLayout(self.actionLayout) self.setLayout(self.mainLayout) def doLayout(self): + + self.update_total_value() self.intelBox = QGroupBox("Units :") self.intelLayout = QGridLayout() i = 0 @@ -71,6 +98,9 @@ class QGroundObjectMenu(QDialog): repair.clicked.connect(lambda u=u, g=g, p=price: self.repair_unit(g, u, p)) self.intelLayout.addWidget(repair, i, 1) i = i + 1 + stretch = QVBoxLayout() + stretch.addStretch() + self.intelLayout.addLayout(stretch, i, 0) self.buildingBox = QGroupBox("Buildings :") self.buildingsLayout = QGridLayout() @@ -86,14 +116,44 @@ class QGroundObjectMenu(QDialog): def do_refresh_layout(self): try: for i in range(self.mainLayout.count()): - self.mainLayout.removeItem(self.mainLayout.itemAt(i)) + item = self.mainLayout.itemAt(i) + if item is not None and item.widget() is not None: + item.widget().setParent(None) + self.sell_all_button.setParent(None) + self.buy_replace.setParent(None) + self.actionLayout.setParent(None) + self.doLayout() - if len(self.ground_object.groups) > 0: + if self.ground_object.dcs_identifier == "AA": self.mainLayout.addWidget(self.intelBox) else: self.mainLayout.addWidget(self.buildingBox) + + self.actionLayout = QHBoxLayout() + if self.total_value > 0: + self.actionLayout.addWidget(self.sell_all_button) + self.actionLayout.addWidget(self.buy_replace) + + if self.cp.captured and self.ground_object.dcs_identifier == "AA": + self.mainLayout.addLayout(self.actionLayout) + except Exception as e: print(e) + self.update_total_value() + self.changed.emit() + + def update_total_value(self): + total_value = 0 + for group in self.ground_object.groups: + for u in group.units: + utype = unit_type_of(u) + if utype in PRICES: + total_value = total_value + PRICES[utype] + else: + total_value = total_value + 1 + if self.sell_all_button is not None: + self.sell_all_button.setText("Disband (+$" + str(self.total_value) + "M)") + self.total_value = total_value def repair_unit(self, group, unit, price): if self.game.budget > price: @@ -112,6 +172,168 @@ class QGroundObjectMenu(QDialog): logging.info("Repaired unit : " + str(unit.id) + " " + str(unit.type)) self.do_refresh_layout() + self.changed.emit() - def closeEvent(self, closeEvent: QCloseEvent): - GameUpdateSignal.get_instance().updateGame(self.game) + def sell_all(self): + self.update_total_value() + self.game.budget = self.game.budget + self.total_value + self.ground_object.groups = [] + self.do_refresh_layout() + GameUpdateSignal.get_instance().updateBudget(self.game) + + def buy_group(self): + self.subwindow = QBuyGroupForGroundObjectDialog(self, self.ground_object, self.cp, self.game, self.total_value) + self.subwindow.changed.connect(self.do_refresh_layout) + self.subwindow.show() + + + +class QBuyGroupForGroundObjectDialog(QDialog): + + changed = QtCore.Signal() + + def __init__(self, parent, ground_object: TheaterGroundObject, cp: ControlPoint, game: Game, current_group_value: int): + super(QBuyGroupForGroundObjectDialog, self).__init__(parent) + + self.setMinimumWidth(350) + self.ground_object = ground_object + self.cp = cp + self.game = game + self.current_group_value = current_group_value + + self.setWindowTitle("Buy units @ " + self.ground_object.obj_name) + self.setWindowIcon(EVENT_ICONS["capture"]) + + self.buySamButton = QPushButton("Buy") + self.buyArmorButton = QPushButton("Buy") + self.buySamLayout = QGridLayout() + self.buyArmorLayout = QGridLayout() + self.amount = QSpinBox() + self.buyArmorCombo = QComboBox() + self.samCombo = QComboBox() + self.buySamBox = QGroupBox("Buy SAM site :") + self.buyArmorBox = QGroupBox("Buy defensive position :") + + + + self.init_ui() + + def init_ui(self): + faction = self.game.player_name + + # Sams + + possible_sams = get_faction_possible_sams_generator(faction) + for sam in possible_sams: + self.samCombo.addItem(sam.name + " [$" + str(sam.price) + "M]", userData=sam) + self.samCombo.currentIndexChanged.connect(self.samComboChanged) + + self.buySamLayout.addWidget(QLabel("Site Type :"), 0, 0, Qt.AlignLeft) + self.buySamLayout.addWidget(self.samCombo, 0, 1, alignment=Qt.AlignRight) + self.buySamLayout.addWidget(self.buySamButton, 1, 1, alignment=Qt.AlignRight) + stretch = QVBoxLayout() + stretch.addStretch() + self.buySamLayout.addLayout(stretch, 2, 0) + + self.buySamButton.clicked.connect(self.buySam) + + # Armored units + + armored_units = db.find_unittype(PinpointStrike, faction) # Todo : refactor this legacy nonsense + for unit in set(armored_units): + self.buyArmorCombo.addItem(db.unit_type_name_2(unit) + " [$" + str(db.PRICES[unit]) + "M]", userData=unit) + self.buyArmorCombo.currentIndexChanged.connect(self.armorComboChanged) + + self.amount.setMinimum(2) + self.amount.setMaximum(8) + self.amount.setValue(2) + self.amount.valueChanged.connect(self.amountComboChanged) + + self.buyArmorLayout.addWidget(QLabel("Unit type :"), 0, 0, Qt.AlignLeft) + self.buyArmorLayout.addWidget(self.buyArmorCombo, 0, 1, alignment=Qt.AlignRight) + self.buyArmorLayout.addWidget(QLabel("Group size :"), 1, 0, alignment=Qt.AlignLeft) + self.buyArmorLayout.addWidget(self.amount, 1, 1, alignment=Qt.AlignRight) + self.buyArmorLayout.addWidget(self.buyArmorButton, 2, 1, alignment=Qt.AlignRight) + stretch2 = QVBoxLayout() + stretch2.addStretch() + self.buyArmorLayout.addLayout(stretch2, 3, 0) + + self.buyArmorButton.clicked.connect(self.buyArmor) + + # Do layout + self.buySamBox.setLayout(self.buySamLayout) + self.buyArmorBox.setLayout(self.buyArmorLayout) + + self.mainLayout = QHBoxLayout() + self.mainLayout.addWidget(self.buySamBox) + + if self.ground_object.airbase_group: + self.mainLayout.addWidget(self.buyArmorBox) + + self.setLayout(self.mainLayout) + + try: + self.samComboChanged(0) + self.armorComboChanged(0) + except: + pass + + def samComboChanged(self, index): + self.buySamButton.setText("Buy [$" + str(self.samCombo.itemData(index).price) + "M] [-$" + str(self.current_group_value) + "M]") + + def armorComboChanged(self, index): + self.buyArmorButton.setText("Buy [$" + str(db.PRICES[self.buyArmorCombo.itemData(index)] * self.amount.value()) + "M][-$" + str(self.current_group_value) + "M]") + + def amountComboChanged(self): + self.buyArmorButton.setText("Buy [$" + str(db.PRICES[self.buyArmorCombo.itemData(self.buyArmorCombo.currentIndex())] * self.amount.value()) + "M][-$" + str(self.current_group_value) + "M]") + + def buyArmor(self): + print("Buy Armor ") + utype = self.buyArmorCombo.itemData(self.buyArmorCombo.currentIndex()) + print(utype) + price = db.PRICES[utype] * self.amount.value() - self.current_group_value + if price > self.game.budget: + self.error_money() + self.close() + return + else: + self.game.budget -= price + + # Generate Armor + group = generate_armor_group_of_type_and_size(self.game, self.ground_object, utype, int(self.amount.value())) + self.ground_object.groups = [group] + + GameUpdateSignal.get_instance().updateBudget(self.game) + + self.changed.emit() + self.close() + + def buySam(self): + sam_generator = self.samCombo.itemData(self.samCombo.currentIndex()) + price = sam_generator.price - self.current_group_value + if price > self.game.budget: + self.error_money() + return + else: + self.game.budget -= price + + # Generate SAM + generator = sam_generator(self.game, self.ground_object) + generator.generate() + generated_group = generator.get_generated_group() + self.ground_object.groups = [generated_group] + + GameUpdateSignal.get_instance().updateBudget(self.game) + + self.changed.emit() + self.close() + + def error_money(self): + msg = QMessageBox() + msg.setIcon(QMessageBox.Information) + msg.setText("Not enough money to buy these units !") + msg.setWindowTitle("Not enough money") + msg.setStandardButtons(QMessageBox.Ok) + msg.setWindowFlags(Qt.WindowStaysOnTopHint) + msg.exec_() + self.close() \ No newline at end of file diff --git a/qt_ui/windows/mission/QMissionPlanning.py b/qt_ui/windows/mission/QMissionPlanning.py index 7dcc2231..04ffdeb3 100644 --- a/qt_ui/windows/mission/QMissionPlanning.py +++ b/qt_ui/windows/mission/QMissionPlanning.py @@ -17,7 +17,6 @@ class QMissionPlanning(QDialog): self.game = game self.setWindowFlags(Qt.WindowStaysOnTopHint) self.setMinimumSize(1000, 440) - self.setModal(True) self.setWindowTitle("Mission Preparation") self.setWindowIcon(EVENT_ICONS["strike"]) self.init_ui() diff --git a/qt_ui/windows/newgame/QNewGameWizard.py b/qt_ui/windows/newgame/QNewGameWizard.py index cb88fc21..3daace57 100644 --- a/qt_ui/windows/newgame/QNewGameWizard.py +++ b/qt_ui/windows/newgame/QNewGameWizard.py @@ -12,7 +12,7 @@ import qt_ui.uiconstants as CONST from game import db, Game from game.settings import Settings from gen import namegen -from qt_ui.windows.newgame.QCampaignList import QCampaignList +from qt_ui.windows.newgame.QCampaignList import QCampaignList, CAMPAIGNS from theater import start_generator, persiangulf, nevada, caucasus, ConflictTheater, normandy, thechannel @@ -42,6 +42,8 @@ class NewGameWizard(QtWidgets.QWizard): redFaction = [c for c in db.FACTIONS][self.field("redFaction")] selectedCampaign = self.field("selectedCampaign") + if selectedCampaign is None: + selectedCampaign = CAMPAIGNS[0] conflictTheater = selectedCampaign[1]() timePeriod = db.TIME_PERIODS[list(db.TIME_PERIODS.keys())[self.field("timePeriod")]] diff --git a/requirements.txt b/requirements.txt index 6f314f97..12d48655 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,6 @@ Pyside2>=5.13.0 pyinstaller==3.6 pyproj==2.6.1.post1 + +Pillow~=7.2.0 +tabulate~=0.8.7 \ No newline at end of file diff --git a/resources/customized_payloads/F-16C_50.lua b/resources/customized_payloads/F-16C_50.lua index d48ad19e..285bccf3 100644 --- a/resources/customized_payloads/F-16C_50.lua +++ b/resources/customized_payloads/F-16C_50.lua @@ -2,41 +2,84 @@ local unitPayloads = { ["name"] = "F-16C_50", ["payloads"] = { [1] = { - ["name"] = "ANTISHIP", + ["name"] = "CAS", ["pylons"] = { [1] = { - ["CLSID"] = "LAU3_HE5", - ["num"] = 6, + ["CLSID"] = "{8A0BE8AE-58D4-4572-9263-3144C0D06364}", + ["num"] = 5, }, [2] = { - ["CLSID"] = "LAU3_HE5", + ["CLSID"] = "{DAC53A2F-79CA-42FF-A77A-F5649B601308}", ["num"] = 7, }, [3] = { - ["CLSID"] = "LAU3_HE5", - ["num"] = 4, - }, - [4] = { - ["CLSID"] = "LAU3_HE5", + ["CLSID"] = "{DAC53A2F-79CA-42FF-A77A-F5649B601308}", ["num"] = 3, }, - [5] = { + [4] = { ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", ["num"] = 2, }, - [6] = { + [5] = { ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", ["num"] = 1, }, - [7] = { + [6] = { ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", ["num"] = 8, }, - [8] = { + [7] = { ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", ["num"] = 9, }, + [8] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 4, + }, [9] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 6, + }, + [10] = { + ["CLSID"] = "{A111396E-D3E8-4b9c-8AC9-2432489304D5}", + ["num"] = 11, + }, + }, + ["tasks"] = { + }, + }, + [2] = { + ["name"] = "ANTISHIP", + ["pylons"] = { + [1] = { + ["CLSID"] = "{DAC53A2F-79CA-42FF-A77A-F5649B601308}", + ["num"] = 7, + }, + [2] = { + ["CLSID"] = "{DAC53A2F-79CA-42FF-A77A-F5649B601308}", + ["num"] = 3, + }, + [3] = { + ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", + ["num"] = 2, + }, + [4] = { + ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", + ["num"] = 1, + }, + [5] = { + ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", + ["num"] = 8, + }, + [6] = { + ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", + ["num"] = 9, + }, + [7] = { + ["CLSID"] = "{A111396E-D3E8-4b9c-8AC9-2432489304D5}", + ["num"] = 11, + }, + [8] = { ["CLSID"] = "{8A0BE8AE-58D4-4572-9263-3144C0D06364}", ["num"] = 5, }, @@ -44,7 +87,7 @@ local unitPayloads = { ["tasks"] = { }, }, - [2] = { + [3] = { ["name"] = "CAP", ["pylons"] = { [1] = { @@ -87,53 +130,6 @@ local unitPayloads = { ["tasks"] = { }, }, - [3] = { - ["name"] = "CAS", - ["pylons"] = { - [1] = { - ["CLSID"] = "{8A0BE8AE-58D4-4572-9263-3144C0D06364}", - ["num"] = 5, - }, - [2] = { - ["CLSID"] = "{DB769D48-67D7-42ED-A2BE-108D566C8B1E}", - ["num"] = 7, - }, - [3] = { - ["CLSID"] = "{DB769D48-67D7-42ED-A2BE-108D566C8B1E}", - ["num"] = 3, - }, - [4] = { - ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", - ["num"] = 2, - }, - [5] = { - ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", - ["num"] = 1, - }, - [6] = { - ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", - ["num"] = 8, - }, - [7] = { - ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", - ["num"] = 9, - }, - [8] = { - ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", - ["num"] = 4, - }, - [9] = { - ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", - ["num"] = 6, - }, - [11] = { - ["CLSID"] = "{A111396E-D3E8-4b9c-8AC9-2432489304D5}", - ["num"] = 11, - }, - }, - ["tasks"] = { - }, - }, [4] = { ["name"] = "STRIKE", ["pylons"] = { @@ -142,11 +138,11 @@ local unitPayloads = { ["num"] = 7, }, [2] = { - ["CLSID"] = "{TER_9A_2L*MK-82}", + ["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}", ["num"] = 4, }, [3] = { - ["CLSID"] = "{TER_9A_2R*MK-82}", + ["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}", ["num"] = 6, }, [4] = { @@ -173,7 +169,7 @@ local unitPayloads = { ["CLSID"] = "{8A0BE8AE-58D4-4572-9263-3144C0D06364}", ["num"] = 5, }, - [11] = { + [10] = { ["CLSID"] = "{A111396E-D3E8-4b9c-8AC9-2432489304D5}", ["num"] = 11, }, @@ -185,19 +181,19 @@ local unitPayloads = { ["name"] = "SEAD", ["pylons"] = { [1] = { - ["CLSID"] = "{DB769D48-67D7-42ED-A2BE-108D566C8B1E}", + ["CLSID"] = "{F376DBEE-4CAE-41BA-ADD9-B2910AC95DEC}", ["num"] = 6, }, [2] = { - ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["CLSID"] = "{B06DD79A-F21E-4EB9-BD9D-AB3844618C93}", ["num"] = 7, }, [3] = { - ["CLSID"] = "{DB769D48-67D7-42ED-A2BE-108D566C8B1E}", + ["CLSID"] = "{F376DBEE-4CAE-41BA-ADD9-B2910AC95DEC}", ["num"] = 4, }, [4] = { - ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["CLSID"] = "{B06DD79A-F21E-4EB9-BD9D-AB3844618C93}", ["num"] = 3, }, [5] = { diff --git a/resources/customized_payloads/P-47D-30.lua b/resources/customized_payloads/P-47D-30.lua index df5f21cd..71dc4891 100644 --- a/resources/customized_payloads/P-47D-30.lua +++ b/resources/customized_payloads/P-47D-30.lua @@ -2,26 +2,6 @@ local unitPayloads = { ["name"] = "P-47D-30", ["payloads"] = { [1] = { - ["name"] = "CAS", - ["pylons"] = { - [1] = { - ["CLSID"] = "{AN-M64}", - ["num"] = 3, - }, - [2] = { - ["CLSID"] = "{AN-M64}", - ["num"] = 2, - }, - [3] = { - ["CLSID"] = "{AN-M64}", - ["num"] = 1, - }, - }, - ["tasks"] = { - [1] = 11, - }, - }, - [2] = { ["name"] = "STRIKE", ["pylons"] = { [1] = { @@ -41,14 +21,34 @@ local unitPayloads = { [1] = 11, }, }, - [3] = { - ["name"] = "ANTISHIP", + [2] = { + ["name"] = "ANTISTRIKE", ["pylons"] = { }, ["tasks"] = { [1] = 11, }, }, + [3] = { + ["name"] = "CAS", + ["pylons"] = { + [1] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 3, + }, + [2] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 2, + }, + [3] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, [4] = { ["name"] = "CAP", ["pylons"] = { @@ -77,6 +77,25 @@ local unitPayloads = { [1] = 11, }, }, + [6] = { + ["name"] = "ANTISHIP", + ["pylons"] = { + [1] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 3, + }, + [2] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 2, + }, + [3] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 1, + }, + }, + ["tasks"] = { + }, + }, }, ["tasks"] = { }, diff --git a/resources/customized_payloads/P-47D-30bl1.lua b/resources/customized_payloads/P-47D-30bl1.lua new file mode 100644 index 00000000..50c84308 --- /dev/null +++ b/resources/customized_payloads/P-47D-30bl1.lua @@ -0,0 +1,96 @@ +local unitPayloads = { + ["name"] = "P-47D-30bl1", + ["payloads"] = { + [1] = { + ["name"] = "CAP", + ["pylons"] = { + }, + ["tasks"] = { + [1] = 11, + }, + }, + [2] = { + ["name"] = "CAS", + ["pylons"] = { + [1] = { + ["CLSID"] = "{AN_M57}", + ["num"] = 1, + }, + [2] = { + ["CLSID"] = "{AN_M57}", + ["num"] = 2, + }, + [3] = { + ["CLSID"] = "{AN_M57}", + ["num"] = 3, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, + [3] = { + ["name"] = "STRIKE", + ["pylons"] = { + [1] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 3, + }, + [2] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 2, + }, + [3] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, + [4] = { + ["name"] = "SEAD", + ["pylons"] = { + [1] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 3, + }, + [2] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 2, + }, + [3] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, + [5] = { + ["name"] = "ANTISHIP", + ["pylons"] = { + [1] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 3, + }, + [2] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 2, + }, + [3] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 1, + }, + }, + ["tasks"] = { + }, + }, + }, + ["tasks"] = { + }, + ["unitType"] = "P-47D-30bl1", +} +return unitPayloads diff --git a/resources/customized_payloads/P-47D-40.lua b/resources/customized_payloads/P-47D-40.lua new file mode 100644 index 00000000..fea43280 --- /dev/null +++ b/resources/customized_payloads/P-47D-40.lua @@ -0,0 +1,88 @@ +local unitPayloads = { + ["name"] = "P-47D-40", + ["payloads"] = { + [1] = { + ["name"] = "CAP", + ["pylons"] = { + }, + ["tasks"] = { + [1] = 11, + }, + }, + [2] = { + ["name"] = "CAS", + ["pylons"] = { + [1] = { + ["CLSID"] = "{P47_5_HVARS_ON_LEFT_WING_RAILS}", + ["num"] = 4, + }, + [2] = { + ["CLSID"] = "{P47_5_HVARS_ON_RIGHT_WING_RAILS}", + ["num"] = 5, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, + [3] = { + ["name"] = "SEAD", + ["pylons"] = { + [1] = { + ["CLSID"] = "{P47_5_HVARS_ON_LEFT_WING_RAILS}", + ["num"] = 4, + }, + [2] = { + ["CLSID"] = "{P47_5_HVARS_ON_RIGHT_WING_RAILS}", + ["num"] = 5, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, + [4] = { + ["name"] = "STRIKE", + ["pylons"] = { + [1] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 3, + }, + [2] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 2, + }, + [3] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, + [5] = { + ["name"] = "ANTISHIP", + ["pylons"] = { + [1] = { + ["CLSID"] = "{P47_5_HVARS_ON_RIGHT_WING_RAILS}", + ["num"] = 5, + }, + [2] = { + ["CLSID"] = "{P47_5_HVARS_ON_LEFT_WING_RAILS}", + ["num"] = 4, + }, + [3] = { + ["CLSID"] = "{AN-M64}", + ["num"] = 1, + }, + }, + ["tasks"] = { + }, + }, + }, + ["tasks"] = { + }, + ["unitType"] = "P-47D-40", +} +return unitPayloads 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 diff --git a/resources/fonts/Inconsolata.otf b/resources/fonts/Inconsolata.otf new file mode 100644 index 00000000..e7e1fa0c Binary files /dev/null and b/resources/fonts/Inconsolata.otf differ diff --git a/resources/fonts/OFL.txt b/resources/fonts/OFL.txt new file mode 100644 index 00000000..6fe694fc --- /dev/null +++ b/resources/fonts/OFL.txt @@ -0,0 +1,38 @@ +—————————————————————————————- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +—————————————————————————————- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. + +The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. + +DEFINITIONS +“Font Software” refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. + +“Reserved Font Name” refers to any names specified as such after the copyright statement(s). + +“Original Version” refers to the collection of Font Software components as distributed by the Copyright Holder(s). + +“Modified Version” refers to any derivative made by adding to, deleting, or substituting—in part or in whole—any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. + +“Author” refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. + +5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file diff --git a/resources/stylesheets/style-dcs.css b/resources/stylesheets/style-dcs.css index 57b2478b..bd866559 100644 --- a/resources/stylesheets/style-dcs.css +++ b/resources/stylesheets/style-dcs.css @@ -198,6 +198,15 @@ QLabel[style="icon-plane"]{ color:white; } +QLabel[style="icon-armor"]{ + background-color:#48719D; + min-height:24px; + max-width: 64px; + border: 1px solid black; + text-align:center; + color:white; +} + QLabel[style="BARCAP"]{ border: 1px solid black; background-color: #445299; diff --git a/resources/stylesheets/style.css b/resources/stylesheets/style.css index b382f642..ea65ffd1 100644 --- a/resources/stylesheets/style.css +++ b/resources/stylesheets/style.css @@ -106,6 +106,15 @@ QLabel[style="icon-plane"]{ color:white; } +QLabel[style="icon-armor"]{ + background-color:#48719D; + min-height:24px; + max-width: 64px; + border: 1px solid black; + text-align:center; + color:white; +} + QLabel[style="bordered"]{ border: 1px solid black; } diff --git a/resources/tools/generate_loadout_check.py b/resources/tools/generate_loadout_check.py index ae299a0f..09835265 100644 --- a/resources/tools/generate_loadout_check.py +++ b/resources/tools/generate_loadout_check.py @@ -1,6 +1,6 @@ import os import sys -from pydcs import dcs +import dcs from game import db from gen.aircraft import AircraftConflictGenerator @@ -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/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() diff --git a/theater/conflicttheater.py b/theater/conflicttheater.py index 2934f15e..621f106a 100644 --- a/theater/conflicttheater.py +++ b/theater/conflicttheater.py @@ -1,6 +1,6 @@ import typing -from pydcs import dcs +import dcs from dcs.mapping import Point from .controlpoint import ControlPoint 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 diff --git a/userdata/liberation_install.py b/userdata/liberation_install.py index 440fe29f..5f19ec0a 100644 --- a/userdata/liberation_install.py +++ b/userdata/liberation_install.py @@ -2,7 +2,7 @@ import json import os from shutil import copyfile -from pydcs import dcs +import dcs from userdata import persistency