mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Merge pull request #119 from DanAlbert/radio-setup
Allocate per-flight radio channels, set up preset channels.
This commit is contained in:
commit
4446a7f060
@ -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()
|
||||
|
||||
@ -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 pydcs.dcs.countries import country_dict
|
||||
from pydcs.dcs.lua.parse import loads
|
||||
from pydcs.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,29 @@ class Operation:
|
||||
self.defenders_starting_position = self.to_cp.at
|
||||
|
||||
def generate(self):
|
||||
# Dedup beacon frequencies, since some maps have more than one beacon
|
||||
# per frequency.
|
||||
beacons = load_beacons_for_terrain(self.game.theater.terrain.name)
|
||||
unique_beacon_frequencies: Set[RadioFrequency] = set()
|
||||
for beacon in beacons:
|
||||
unique_beacon_frequencies.add(beacon.frequency)
|
||||
if beacon.is_tacan:
|
||||
if beacon.channel is None:
|
||||
logging.error(
|
||||
f"TACAN beacon has no channel: {beacon.callsign}")
|
||||
else:
|
||||
self.tacan_registry.reserve(beacon.tacan_channel)
|
||||
for frequency in unique_beacon_frequencies:
|
||||
self.radio_registry.reserve(frequency)
|
||||
|
||||
for airfield, data in AIRFIELD_DATA.items():
|
||||
if data.theater == self.game.theater.terrain.name:
|
||||
self.radio_registry.reserve(data.atc.hf)
|
||||
self.radio_registry.reserve(data.atc.vhf_fm)
|
||||
self.radio_registry.reserve(data.atc.vhf_am)
|
||||
self.radio_registry.reserve(data.atc.uhf)
|
||||
# No need to reserve ILS or TACAN because those are in the
|
||||
# beacon list.
|
||||
|
||||
# Generate meteo
|
||||
if self.environment_settings is None:
|
||||
@ -151,7 +191,12 @@ class Operation:
|
||||
else:
|
||||
country = self.current_mission.country(self.game.enemy_country)
|
||||
if cp.id in self.game.planners.keys():
|
||||
self.airgen.generate_flights(cp, country, self.game.planners[cp.id])
|
||||
self.airgen.generate_flights(
|
||||
cp,
|
||||
country,
|
||||
self.game.planners[cp.id],
|
||||
self.groundobjectgen.runways
|
||||
)
|
||||
|
||||
# Generate ground units on frontline everywhere
|
||||
self.game.jtacs = []
|
||||
@ -221,28 +266,87 @@ class Operation:
|
||||
load_dcs_libe.add_action(DoScript(String(script)))
|
||||
self.current_mission.triggerrules.triggers.append(load_dcs_libe)
|
||||
|
||||
kneeboard_generator = KneeboardGenerator(self.current_mission, self.game)
|
||||
self.assign_channels_to_flights()
|
||||
|
||||
# Briefing Generation
|
||||
for i, tanker_type in enumerate(self.airsupportgen.generated_tankers):
|
||||
callsign = TANKER_CALLSIGNS[i]
|
||||
tacan = f"{60 + i}X"
|
||||
freq = f"{130 + i} MHz AM"
|
||||
self.briefinggen.append_frequency(f"Tanker {callsign} ({tanker_type})", f"{tacan}/{freq}")
|
||||
kneeboard_generator.add_tanker(callsign, tanker_type, freq, tacan)
|
||||
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:
|
||||
callsign = "AWACS"
|
||||
freq = "233 MHz AM"
|
||||
self.briefinggen.append_frequency(callsign, freq)
|
||||
kneeboard_generator.add_awacs(callsign, freq)
|
||||
|
||||
self.briefinggen.append_frequency("Flight", "251 MHz AM")
|
||||
kneeboard_generator.add_comm("Flight", "251 MHz AM")
|
||||
|
||||
# Generate the briefing
|
||||
self.briefinggen.generate()
|
||||
for awacs in self.airsupportgen.air_support.awacs:
|
||||
self.briefinggen.add_awacs(awacs)
|
||||
kneeboard_generator.add_awacs(awacs)
|
||||
|
||||
for region, code, name in self.game.jtacs:
|
||||
kneeboard_generator.add_jtac(name, region, code)
|
||||
# TODO: Radio info? Type?
|
||||
jtac = JtacInfo(name, region, code)
|
||||
self.briefinggen.add_jtac(jtac)
|
||||
kneeboard_generator.add_jtac(jtac)
|
||||
|
||||
for flight in self.airgen.flights:
|
||||
self.briefinggen.add_flight(flight)
|
||||
kneeboard_generator.add_flight(flight)
|
||||
|
||||
self.briefinggen.generate()
|
||||
kneeboard_generator.generate()
|
||||
|
||||
def assign_channels_to_flights(self) -> None:
|
||||
"""Assigns preset radio channels for client flights."""
|
||||
for flight in self.airgen.flights:
|
||||
if not flight.client_units:
|
||||
continue
|
||||
self.assign_channels_to_flight(flight)
|
||||
|
||||
def assign_channels_to_flight(self, flight: FlightData) -> None:
|
||||
"""Assigns preset radio channels for a client flight."""
|
||||
airframe = flight.aircraft_type
|
||||
|
||||
try:
|
||||
aircraft_data = AIRCRAFT_DATA[airframe.id]
|
||||
except KeyError:
|
||||
logging.warning(f"No aircraft data for {airframe.id}")
|
||||
return
|
||||
|
||||
# Intra-flight channel is set up when the flight is created, however we
|
||||
# do need to make sure we don't overwrite it. For cases where the
|
||||
# inter-flight and intra-flight radios share presets (the AV-8B only has
|
||||
# one set of channels, even though it can use two channels
|
||||
# simultaneously), start assigning channels at 2.
|
||||
radio_id = aircraft_data.inter_flight_radio_index
|
||||
if aircraft_data.intra_flight_radio_index == radio_id:
|
||||
first_channel = 2
|
||||
else:
|
||||
first_channel = 1
|
||||
|
||||
last_channel = flight.num_radio_channels(radio_id)
|
||||
channel_alloc = iter(range(first_channel, last_channel + 1))
|
||||
|
||||
flight.assign_channel(radio_id, next(channel_alloc),flight.departure.atc)
|
||||
|
||||
# TODO: If there ever are multiple AWACS, limit to mission relevant.
|
||||
for awacs in self.airsupportgen.air_support.awacs:
|
||||
flight.assign_channel(radio_id, next(channel_alloc), awacs.freq)
|
||||
|
||||
# TODO: Fix departure/arrival to support carriers.
|
||||
if flight.arrival != flight.departure:
|
||||
flight.assign_channel(radio_id, next(channel_alloc),
|
||||
flight.arrival.atc)
|
||||
|
||||
try:
|
||||
# TODO: Skip incompatible tankers.
|
||||
for tanker in self.airsupportgen.air_support.tankers:
|
||||
flight.assign_channel(
|
||||
radio_id, next(channel_alloc), tanker.freq)
|
||||
|
||||
if flight.divert is not None:
|
||||
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
|
||||
|
||||
360
gen/aircraft.py
360
gen/aircraft.py
@ -1,14 +1,39 @@
|
||||
from dcs.action import ActivateGroup, AITaskPush, MessageToCoalition, MessageToAll
|
||||
from dcs.condition import TimeAfter, CoalitionHasAirdrome, PartOfCoalitionInZone
|
||||
from dcs.helicopters import UH_1H
|
||||
from dcs.terrain.terrain import NoParkingSlotError
|
||||
from dcs.triggers import TriggerOnce, Event
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from game.data.cap_capabilities_db import GUNFIGHTERS
|
||||
from game.settings import Settings
|
||||
from game.utils import nm_to_meter
|
||||
from gen.airfields import RunwayData
|
||||
from gen.flights.ai_flight_planner import FlightPlanner
|
||||
from gen.flights.flight import Flight, FlightType, FlightWaypointType
|
||||
from gen.flights.flight import (
|
||||
Flight,
|
||||
FlightType,
|
||||
FlightWaypoint,
|
||||
FlightWaypointType,
|
||||
)
|
||||
from gen.radios import get_radio, MHz, Radio, RadioFrequency, RadioRegistry
|
||||
from pydcs.dcs import helicopters
|
||||
from pydcs.dcs.action import ActivateGroup, AITaskPush, MessageToAll
|
||||
from pydcs.dcs.condition import TimeAfter, CoalitionHasAirdrome, PartOfCoalitionInZone
|
||||
from pydcs.dcs.flyingunit import FlyingUnit
|
||||
from pydcs.dcs.helicopters import helicopter_map, UH_1H
|
||||
from pydcs.dcs.mission import Mission, StartType
|
||||
from pydcs.dcs.planes import (
|
||||
Bf_109K_4,
|
||||
FW_190A8,
|
||||
FW_190D9,
|
||||
I_16,
|
||||
Ju_88A4,
|
||||
P_47D_30,
|
||||
P_51D,
|
||||
P_51D_30_NA,
|
||||
SpitfireLFMkIX,
|
||||
SpitfireLFMkIXCW,
|
||||
)
|
||||
from pydcs.dcs.terrain.terrain import Airport, NoParkingSlotError
|
||||
from pydcs.dcs.triggers import TriggerOnce, Event
|
||||
from pydcs.dcs.unittype import FlyingType, UnitType
|
||||
from .conflictgen import *
|
||||
from .naming import *
|
||||
|
||||
@ -23,22 +48,243 @@ RTB_ALTITUDE = 800
|
||||
RTB_DISTANCE = 5000
|
||||
HELI_ALT = 500
|
||||
|
||||
# Note that fallback radio channels will *not* be reserved. It's possible that
|
||||
# flights using these will overlap with other channels. This is because we would
|
||||
# need to make sure we fell back to a frequency that is not used by any beacon
|
||||
# or ATC, which we don't have the information to predict. Deal with the minor
|
||||
# annoyance for now since we'll be fleshing out radio info soon enough.
|
||||
ALLIES_WW2_CHANNEL = MHz(124)
|
||||
GERMAN_WW2_CHANNEL = MHz(40)
|
||||
HELICOPTER_CHANNEL = MHz(127)
|
||||
UHF_FALLBACK_CHANNEL = MHz(251)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AircraftData:
|
||||
"""Additional aircraft data not exposed by pydcs."""
|
||||
|
||||
#: The type of radio used for intra-flight communications.
|
||||
intra_flight_radio: Radio
|
||||
|
||||
#: Index of the radio used for intra-flight communications. Matches the
|
||||
#: index of the panel_radio field of the pydcs.dcs.planes object.
|
||||
inter_flight_radio_index: Optional[int]
|
||||
|
||||
#: Index of the radio used for intra-flight communications. Matches the
|
||||
#: index of the panel_radio field of the pydcs.dcs.planes object.
|
||||
intra_flight_radio_index: Optional[int]
|
||||
|
||||
|
||||
# Indexed by the id field of the pydcs PlaneType.
|
||||
AIRCRAFT_DATA: Dict[str, AircraftData] = {
|
||||
"A-10C": AircraftData(
|
||||
get_radio("AN/ARC-186(V) AM"),
|
||||
# The A-10's radio works differently than most aircraft. Doesn't seem to
|
||||
# be a way to set these from the mission editor, let alone pydcs.
|
||||
inter_flight_radio_index=None,
|
||||
intra_flight_radio_index=None
|
||||
),
|
||||
"F-16C_50": AircraftData(
|
||||
get_radio("AN/ARC-222"),
|
||||
# COM2 is the AN/ARC-222, which is the VHF radio we want to use for
|
||||
# intra-flight communication to leave COM1 open for UHF inter-flight.
|
||||
inter_flight_radio_index=1,
|
||||
intra_flight_radio_index=2
|
||||
),
|
||||
"FA-18C_hornet": AircraftData(
|
||||
get_radio("AN/ARC-210"),
|
||||
# DCS will clobber channel 1 of the first radio compatible with the
|
||||
# flight's assigned frequency. Since the F/A-18's two radios are both
|
||||
# AN/ARC-210s, radio 1 will be compatible regardless of which frequency
|
||||
# is assigned, so we must use radio 1 for the intra-flight radio.
|
||||
inter_flight_radio_index=2,
|
||||
intra_flight_radio_index=1
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ChannelAssignment:
|
||||
radio_id: int
|
||||
channel: int
|
||||
|
||||
@property
|
||||
def radio_name(self) -> str:
|
||||
"""Returns the name of the radio, i.e. COM1."""
|
||||
return f"COM{self.radio_id}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class FlightData:
|
||||
"""Details of a planned flight."""
|
||||
|
||||
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.assign_intra_flight_channel()
|
||||
|
||||
@property
|
||||
def client_units(self) -> List[FlyingUnit]:
|
||||
"""List of playable units in the flight."""
|
||||
return [u for u in self.units if u.is_human()]
|
||||
|
||||
def assign_intra_flight_channel(self) -> None:
|
||||
"""Assigns a channel to the intra-flight frequency."""
|
||||
if not self.client_units:
|
||||
return
|
||||
|
||||
# pydcs will actually set up the channel for us, but we want to make
|
||||
# sure that it ends up in frequency_to_channel_map.
|
||||
try:
|
||||
data = AIRCRAFT_DATA[self.aircraft_type.id]
|
||||
self.assign_channel(
|
||||
data.intra_flight_radio_index, 1, self.intra_flight_channel)
|
||||
except KeyError:
|
||||
logging.warning(f"No aircraft data for {self.aircraft_type.id}")
|
||||
|
||||
@property
|
||||
def aircraft_type(self) -> FlyingType:
|
||||
"""Returns the type of aircraft in this flight."""
|
||||
return self.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 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) -> Tuple[int, RadioFrequency]:
|
||||
"""Allocates an intra-flight channel to a group.
|
||||
|
||||
Args:
|
||||
airframe: The type of aircraft a channel should be allocated for.
|
||||
|
||||
Returns:
|
||||
A tuple of the radio index (for aircraft with multiple radios) and
|
||||
the frequency of the intra-flight channel.
|
||||
"""
|
||||
try:
|
||||
aircraft_data = AIRCRAFT_DATA[airframe.id]
|
||||
channel = self.radio_registry.alloc_for_radio(
|
||||
aircraft_data.intra_flight_radio)
|
||||
return aircraft_data.intra_flight_radio_index, channel
|
||||
except KeyError:
|
||||
return 1, 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 +320,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 +332,44 @@ 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 :(
|
||||
radio_id, channel = self.get_intra_flight_channel(unit_type)
|
||||
group.set_frequency(channel.mhz, radio_id)
|
||||
|
||||
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:
|
||||
# TODO: Implement logic for picking preferred runway.
|
||||
runway = flight.from_cp.airport.runways[0]
|
||||
runway_number = runway.heading // 10
|
||||
runway_side = ["", "L", "R"][runway.leftright]
|
||||
runway_name = f"{runway_number:02}{runway_side}"
|
||||
departure_runway = RunwayData.for_airfield(
|
||||
flight.from_cp.airport, runway_name)
|
||||
elif cp.is_fleet:
|
||||
departure_runway = dynamic_runways.get(cp.name, fallback_runway)
|
||||
else:
|
||||
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
|
||||
|
||||
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=flight.points,
|
||||
intra_flight_channel=channel
|
||||
))
|
||||
|
||||
# Special case so Su 33 carrier take off
|
||||
if unit_type is Su_33:
|
||||
@ -253,8 +520,8 @@ class AircraftConflictGenerator:
|
||||
logging.warning("Pylon not found ! => Pylon" + key + " on " + str(flight.unit_type))
|
||||
|
||||
|
||||
def generate_flights(self, cp, country, flight_planner:FlightPlanner):
|
||||
|
||||
def generate_flights(self, cp, country, flight_planner: FlightPlanner,
|
||||
dynamic_runways: Dict[str, RunwayData]):
|
||||
# Clear pydcs parking slots
|
||||
if cp.airport is not None:
|
||||
logging.info("CLEARING SLOTS @ " + cp.airport.name)
|
||||
@ -273,7 +540,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)
|
||||
|
||||
|
||||
@ -384,19 +652,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]))
|
||||
@ -408,7 +670,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))
|
||||
@ -417,7 +679,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))
|
||||
@ -426,14 +688,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))
|
||||
@ -512,23 +774,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
|
||||
|
||||
|
||||
|
||||
939
gen/airfields.py
939
gen/airfields.py
File diff suppressed because it is too large
Load Diff
@ -1,12 +1,15 @@
|
||||
from game import db
|
||||
from typing import List
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from .conflictgen import *
|
||||
from .naming import *
|
||||
from .radios import RadioFrequency, RadioRegistry
|
||||
from .tacan import TacanBand, TacanChannel, TacanRegistry
|
||||
|
||||
from dcs.mission import *
|
||||
from dcs.unitgroup import *
|
||||
from dcs.unittype import *
|
||||
from dcs.task import *
|
||||
from dcs.terrain.terrain import NoParkingSlotError
|
||||
|
||||
TANKER_DISTANCE = 15000
|
||||
TANKER_ALT = 4572
|
||||
@ -15,15 +18,61 @@ TANKER_HEADING_OFFSET = 45
|
||||
AWACS_DISTANCE = 150000
|
||||
AWACS_ALT = 13000
|
||||
|
||||
AWACS_CALLSIGNS = [
|
||||
"Overlord",
|
||||
"Magic",
|
||||
"Wizard",
|
||||
"Focus",
|
||||
"Darkstar",
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class TankerCallsign:
|
||||
full: str
|
||||
short: str
|
||||
|
||||
|
||||
TANKER_CALLSIGNS = [
|
||||
TankerCallsign("Texaco", "TEX"),
|
||||
TankerCallsign("Arco", "ARC"),
|
||||
TankerCallsign("Shell", "SHL"),
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class AwacsInfo:
|
||||
"""AWACS information for the kneeboard."""
|
||||
callsign: str
|
||||
freq: RadioFrequency
|
||||
|
||||
|
||||
@dataclass
|
||||
class TankerInfo:
|
||||
"""Tanker information for the kneeboard."""
|
||||
callsign: str
|
||||
variant: str
|
||||
freq: RadioFrequency
|
||||
tacan: TacanChannel
|
||||
|
||||
|
||||
@dataclass
|
||||
class AirSupport:
|
||||
awacs: List[AwacsInfo] = field(default_factory=list)
|
||||
tankers: List[TankerInfo] = field(default_factory=list)
|
||||
|
||||
|
||||
class AirSupportConflictGenerator:
|
||||
generated_tankers = None # type: typing.List[str]
|
||||
|
||||
def __init__(self, mission: Mission, conflict: Conflict, game):
|
||||
def __init__(self, mission: Mission, conflict: Conflict, game,
|
||||
radio_registry: RadioRegistry,
|
||||
tacan_registry: TacanRegistry) -> None:
|
||||
self.mission = mission
|
||||
self.conflict = conflict
|
||||
self.game = game
|
||||
self.generated_tankers = []
|
||||
self.air_support = AirSupport()
|
||||
self.radio_registry = radio_registry
|
||||
self.tacan_registry = tacan_registry
|
||||
|
||||
@classmethod
|
||||
def support_tasks(cls) -> typing.Collection[typing.Type[MainTask]]:
|
||||
@ -32,9 +81,11 @@ class AirSupportConflictGenerator:
|
||||
def generate(self, is_awacs_enabled):
|
||||
player_cp = self.conflict.from_cp if self.conflict.from_cp.captured else self.conflict.to_cp
|
||||
|
||||
CALLSIGNS = ["TKR", "TEX", "FUL", "FUE", ""]
|
||||
for i, tanker_unit_type in enumerate(db.find_unittype(Refueling, self.conflict.attackers_side)):
|
||||
self.generated_tankers.append(db.unit_type_name(tanker_unit_type))
|
||||
callsign = TANKER_CALLSIGNS[i]
|
||||
variant = db.unit_type_name(tanker_unit_type)
|
||||
freq = self.radio_registry.alloc_uhf()
|
||||
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y)
|
||||
tanker_heading = self.conflict.to_cp.position.heading_between_point(self.conflict.from_cp.position) + TANKER_HEADING_OFFSET * i
|
||||
tanker_position = player_cp.position.point_from_heading(tanker_heading, TANKER_DISTANCE)
|
||||
tanker_group = self.mission.refuel_flight(
|
||||
@ -45,21 +96,26 @@ class AirSupportConflictGenerator:
|
||||
position=tanker_position,
|
||||
altitude=TANKER_ALT,
|
||||
race_distance=58000,
|
||||
frequency=130 + i,
|
||||
frequency=freq.mhz,
|
||||
start_type=StartType.Warm,
|
||||
speed=574,
|
||||
tacanchannel="{}X".format(60 + i),
|
||||
tacanchannel=str(tacan),
|
||||
)
|
||||
|
||||
if tanker_unit_type != IL_78M:
|
||||
tanker_group.points[0].tasks.pop() # Override PyDCS tacan channel
|
||||
tanker_group.points[0].tasks.append(ActivateBeaconCommand(60 + i, "X", CALLSIGNS[i], True, tanker_group.units[0].id, True))
|
||||
tanker_group.points[0].tasks.append(ActivateBeaconCommand(
|
||||
tacan.number, tacan.band.value, callsign.short, True, tanker_group.units[0].id, True))
|
||||
|
||||
tanker_group.points[0].tasks.append(SetInvisibleCommand(True))
|
||||
tanker_group.points[0].tasks.append(SetImmortalCommand(True))
|
||||
|
||||
self.air_support.tankers.append(TankerInfo(callsign.full, variant, freq, tacan))
|
||||
|
||||
if is_awacs_enabled:
|
||||
try:
|
||||
callsign = AWACS_CALLSIGNS[0]
|
||||
freq = self.radio_registry.alloc_uhf()
|
||||
awacs_unit = db.find_unittype(AWACS, self.conflict.attackers_side)[0]
|
||||
awacs_flight = self.mission.awacs_flight(
|
||||
country=self.mission.country(self.game.player_country),
|
||||
@ -68,11 +124,12 @@ class AirSupportConflictGenerator:
|
||||
altitude=AWACS_ALT,
|
||||
airport=None,
|
||||
position=self.conflict.position.random_point_within(AWACS_DISTANCE, AWACS_DISTANCE),
|
||||
frequency=233,
|
||||
frequency=freq.mhz,
|
||||
start_type=StartType.Warm,
|
||||
)
|
||||
awacs_flight.points[0].tasks.append(SetInvisibleCommand(True))
|
||||
awacs_flight.points[0].tasks.append(SetImmortalCommand(True))
|
||||
self.air_support.awacs.append(AwacsInfo(callsign, freq))
|
||||
except:
|
||||
print("No AWACS for faction")
|
||||
|
||||
|
||||
74
gen/beacons.py
Normal file
74
gen/beacons.py
Normal file
@ -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)
|
||||
@ -1,68 +1,136 @@
|
||||
import logging
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
import random
|
||||
from typing import List, Tuple
|
||||
|
||||
from game import db
|
||||
from .conflictgen import *
|
||||
from .naming import *
|
||||
|
||||
from dcs.mission import *
|
||||
from pydcs.dcs.mission import Mission
|
||||
from .aircraft import FlightData
|
||||
from .airfields import RunwayData
|
||||
from .airsupportgen import AwacsInfo, TankerInfo
|
||||
from .conflictgen import Conflict
|
||||
from .ground_forces.combat_stance import CombatStance
|
||||
from .radios import RadioFrequency
|
||||
|
||||
|
||||
class BriefingGenerator:
|
||||
freqs = None # type: typing.List[typing.Tuple[str, str]]
|
||||
title = "" # type: str
|
||||
description = "" # type: str
|
||||
targets = None # type: typing.List[typing.Tuple[str, str]]
|
||||
waypoints = None # type: typing.List[str]
|
||||
@dataclass
|
||||
class CommInfo:
|
||||
"""Communications information for the kneeboard."""
|
||||
name: str
|
||||
freq: RadioFrequency
|
||||
|
||||
|
||||
@dataclass
|
||||
class JtacInfo:
|
||||
"""JTAC information for the kneeboard."""
|
||||
callsign: str
|
||||
region: str
|
||||
code: str
|
||||
|
||||
|
||||
class MissionInfoGenerator:
|
||||
"""Base type for generators of mission information for the player.
|
||||
|
||||
Examples of subtypes include briefing generators, kneeboard generators, etc.
|
||||
"""
|
||||
|
||||
def __init__(self, mission: Mission) -> None:
|
||||
self.mission = mission
|
||||
self.awacs: List[AwacsInfo] = []
|
||||
self.comms: List[CommInfo] = []
|
||||
self.flights: List[FlightData] = []
|
||||
self.jtacs: List[JtacInfo] = []
|
||||
self.tankers: List[TankerInfo] = []
|
||||
|
||||
def add_awacs(self, awacs: AwacsInfo) -> None:
|
||||
"""Adds an AWACS/GCI to the mission.
|
||||
|
||||
Args:
|
||||
awacs: AWACS information.
|
||||
"""
|
||||
self.awacs.append(awacs)
|
||||
|
||||
def add_comm(self, name: str, freq: RadioFrequency) -> None:
|
||||
"""Adds communications info to the mission.
|
||||
|
||||
Args:
|
||||
name: Name of the radio channel.
|
||||
freq: Frequency of the radio channel.
|
||||
"""
|
||||
self.comms.append(CommInfo(name, freq))
|
||||
|
||||
def add_flight(self, flight: FlightData) -> None:
|
||||
"""Adds flight info to the mission.
|
||||
|
||||
Args:
|
||||
flight: Flight information.
|
||||
"""
|
||||
self.flights.append(flight)
|
||||
|
||||
def add_jtac(self, jtac: JtacInfo) -> None:
|
||||
"""Adds a JTAC to the mission.
|
||||
|
||||
Args:
|
||||
jtac: JTAC information.
|
||||
"""
|
||||
self.jtacs.append(jtac)
|
||||
|
||||
def add_tanker(self, tanker: TankerInfo) -> None:
|
||||
"""Adds a tanker to the mission.
|
||||
|
||||
Args:
|
||||
tanker: Tanker information.
|
||||
"""
|
||||
self.tankers.append(tanker)
|
||||
|
||||
def generate(self) -> None:
|
||||
"""Generates the mission information."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class BriefingGenerator(MissionInfoGenerator):
|
||||
|
||||
def __init__(self, mission: Mission, conflict: Conflict, game):
|
||||
self.m = mission
|
||||
super().__init__(mission)
|
||||
self.conflict = conflict
|
||||
self.game = game
|
||||
self.title = ""
|
||||
self.description = ""
|
||||
self.dynamic_runways: List[RunwayData] = []
|
||||
|
||||
self.freqs = []
|
||||
self.targets = []
|
||||
self.waypoints = []
|
||||
def add_dynamic_runway(self, runway: RunwayData) -> None:
|
||||
"""Adds a dynamically generated runway to the briefing.
|
||||
|
||||
self.jtacs = []
|
||||
Dynamic runways are any valid landing point that is a unit rather than a
|
||||
map feature. These include carriers, ships with a helipad, and FARPs.
|
||||
"""
|
||||
self.dynamic_runways.append(runway)
|
||||
|
||||
def append_frequency(self, name: str, frequency: str):
|
||||
self.freqs.append((name, frequency))
|
||||
def add_flight_description(self, flight: FlightData):
|
||||
assert flight.client_units
|
||||
|
||||
def append_target(self, description: str, markpoint: str = None):
|
||||
self.targets.append((description, markpoint))
|
||||
|
||||
def append_waypoint(self, description: str):
|
||||
self.waypoints.append(description)
|
||||
|
||||
def add_flight_description(self, flight):
|
||||
|
||||
if flight.client_count <= 0:
|
||||
return
|
||||
|
||||
flight_unit_name = db.unit_type_name(flight.unit_type)
|
||||
aircraft = flight.aircraft_type
|
||||
flight_unit_name = db.unit_type_name(aircraft)
|
||||
self.description += "-" * 50 + "\n"
|
||||
self.description += flight_unit_name + " x " + str(flight.count) + 2 * "\n"
|
||||
self.description += f"{flight_unit_name} x {flight.size + 2}\n\n"
|
||||
|
||||
self.description += "#0 -- TAKEOFF : Take off from " + flight.from_cp.name + "\n"
|
||||
for i, wpt in enumerate(flight.points):
|
||||
self.description += "#" + str(1+i) + " -- " + wpt.name + " : " + wpt.description + "\n"
|
||||
self.description += "#" + str(len(flight.points) + 1) + " -- RTB\n\n"
|
||||
departure = flight.departure.airfield_name
|
||||
self.description += f"#0 -- TAKEOFF : Take off from {departure}\n"
|
||||
for i, wpt in enumerate(flight.waypoints):
|
||||
self.description += f"#{i + 1} -- {wpt.name} : {wpt.description}\n"
|
||||
self.description += f"#{len(flight.waypoints) + 1} -- RTB\n\n"
|
||||
|
||||
group = flight.group
|
||||
if group is not None:
|
||||
for i, nav_target in enumerate(group.nav_target_points):
|
||||
self.description += nav_target.text_comment + "\n"
|
||||
self.description += "\n"
|
||||
self.description += "-" * 50 + "\n"
|
||||
|
||||
def add_ally_flight_description(self, flight):
|
||||
if flight.client_count == 0:
|
||||
flight_unit_name = db.unit_type_name(flight.unit_type)
|
||||
self.description += flight.flight_type.name + " " + flight_unit_name + " x " + str(flight.count) + ", departing in " + str(flight.scheduled_in) + " minutes \n"
|
||||
def add_ally_flight_description(self, flight: FlightData):
|
||||
assert not flight.client_units
|
||||
aircraft = flight.aircraft_type
|
||||
flight_unit_name = db.unit_type_name(aircraft)
|
||||
self.description += (
|
||||
f"{flight.flight_type.name} {flight_unit_name} x {flight.size}, "
|
||||
f"departing in {flight.departure_delay} minutes\n"
|
||||
)
|
||||
|
||||
def generate(self):
|
||||
|
||||
self.description = ""
|
||||
|
||||
self.description += "DCS Liberation turn #" + str(self.game.turn) + "\n"
|
||||
@ -74,52 +142,50 @@ class BriefingGenerator:
|
||||
self.description += "Your flights:" + "\n"
|
||||
self.description += "=" * 15 + "\n\n"
|
||||
|
||||
for planner in self.game.planners.values():
|
||||
for flight in planner.flights:
|
||||
for flight in self.flights:
|
||||
if flight.client_units:
|
||||
self.add_flight_description(flight)
|
||||
|
||||
self.description += "\n"*2
|
||||
self.description += "Planned ally flights:" + "\n"
|
||||
self.description += "=" * 15 + "\n"
|
||||
for planner in self.game.planners.values():
|
||||
if planner.from_cp.captured and len(planner.flights) > 0:
|
||||
self.description += "\nFrom " + planner.from_cp.full_name + " \n"
|
||||
self.description += "-" * 50 + "\n\n"
|
||||
for flight in planner.flights:
|
||||
self.add_ally_flight_description(flight)
|
||||
allied_flights_by_departure = defaultdict(list)
|
||||
for flight in self.flights:
|
||||
if not flight.client_units and flight.friendly:
|
||||
name = flight.departure.airfield_name
|
||||
allied_flights_by_departure[name].append(flight)
|
||||
for departure, flights in allied_flights_by_departure.items():
|
||||
self.description += f"\nFrom {departure}\n"
|
||||
self.description += "-" * 50 + "\n\n"
|
||||
for flight in flights:
|
||||
self.add_ally_flight_description(flight)
|
||||
|
||||
if self.freqs:
|
||||
if self.comms:
|
||||
self.description += "\n\nComms Frequencies:\n"
|
||||
self.description += "=" * 15 + "\n"
|
||||
for name, freq in self.freqs:
|
||||
self.description += "{}: {}\n".format(name, freq)
|
||||
for comm_info in self.comms:
|
||||
self.description += f"{comm_info.name}: {comm_info.freq}\n"
|
||||
self.description += ("-" * 50) + "\n"
|
||||
|
||||
for cp in self.game.theater.controlpoints:
|
||||
if cp.captured and cp.cptype in [ControlPointType.LHA_GROUP, ControlPointType.AIRCRAFT_CARRIER_GROUP]:
|
||||
self.description += cp.name + "\n"
|
||||
self.description += "RADIO : 127.5 Mhz AM\n"
|
||||
self.description += "TACAN : "
|
||||
self.description += str(cp.tacanN)
|
||||
if cp.tacanY:
|
||||
self.description += "Y"
|
||||
else:
|
||||
self.description += "X"
|
||||
self.description += " " + str(cp.tacanI) + "\n"
|
||||
|
||||
if cp.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP and hasattr(cp, "icls"):
|
||||
self.description += "ICLS Channel : " + str(cp.icls) + "\n"
|
||||
self.description += "-" * 50 + "\n"
|
||||
for runway in self.dynamic_runways:
|
||||
self.description += f"{runway.airfield_name}\n"
|
||||
self.description += f"RADIO : {runway.atc}\n"
|
||||
if runway.tacan is not None:
|
||||
self.description += f"TACAN : {runway.tacan} {runway.tacan_callsign}\n"
|
||||
if runway.icls is not None:
|
||||
self.description += f"ICLS Channel : {runway.icls}\n"
|
||||
self.description += "-" * 50 + "\n"
|
||||
|
||||
|
||||
self.description += "JTACS [F-10 Menu] : \n"
|
||||
self.description += "===================\n\n"
|
||||
for jtac in self.game.jtacs:
|
||||
self.description += str(jtac[0]) + " -- Code : " + str(jtac[1]) + "\n"
|
||||
for jtac in self.jtacs:
|
||||
self.description += f"{jtac.region} -- Code : {jtac.code}\n"
|
||||
|
||||
self.m.set_description_text(self.description)
|
||||
self.mission.set_description_text(self.description)
|
||||
|
||||
self.m.add_picture_blue(os.path.abspath("./resources/ui/splash_screen.png"))
|
||||
self.mission.add_picture_blue(os.path.abspath(
|
||||
"./resources/ui/splash_screen.png"))
|
||||
|
||||
|
||||
def generate_ongoing_war_text(self):
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
from enum import Enum
|
||||
from typing import List
|
||||
|
||||
from dcs.mission import StartType
|
||||
from dcs.unittype import UnitType
|
||||
|
||||
from game import db
|
||||
from pydcs.dcs.unittype import UnitType
|
||||
|
||||
|
||||
class FlightType(Enum):
|
||||
@ -113,10 +111,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)
|
||||
|
||||
@ -1,13 +1,19 @@
|
||||
import logging
|
||||
|
||||
from game import db
|
||||
from game.data.building_data import FORTIFICATION_UNITS_ID, FORTIFICATION_UNITS
|
||||
from game.db import unit_type_from_name
|
||||
from pydcs.dcs.mission import *
|
||||
from pydcs.dcs.statics import *
|
||||
from pydcs.dcs.task import (
|
||||
ActivateBeaconCommand,
|
||||
ActivateICLSCommand,
|
||||
OptAlarmState,
|
||||
)
|
||||
from pydcs.dcs.unit import Ship, Vehicle
|
||||
from pydcs.dcs.unitgroup import StaticGroup
|
||||
from .airfields import RunwayData
|
||||
from .conflictgen import *
|
||||
from .naming import *
|
||||
|
||||
from dcs.mission import *
|
||||
from dcs.statics import *
|
||||
from .radios import RadioRegistry
|
||||
from .tacan import TacanBand, TacanRegistry
|
||||
|
||||
FARP_FRONTLINE_DISTANCE = 10000
|
||||
AA_CP_MIN_DISTANCE = 40000
|
||||
@ -16,10 +22,15 @@ AA_CP_MIN_DISTANCE = 40000
|
||||
class GroundObjectsGenerator:
|
||||
FARP_CAPACITY = 4
|
||||
|
||||
def __init__(self, mission: Mission, conflict: Conflict, game):
|
||||
def __init__(self, mission: Mission, conflict: Conflict, game,
|
||||
radio_registry: RadioRegistry, tacan_registry: TacanRegistry):
|
||||
self.m = mission
|
||||
self.conflict = conflict
|
||||
self.game = game
|
||||
self.radio_registry = radio_registry
|
||||
self.tacan_registry = tacan_registry
|
||||
self.icls_alloc = iter(range(1, 21))
|
||||
self.runways: Dict[str, RunwayData] = {}
|
||||
|
||||
def generate_farps(self, number_of_units=1) -> typing.Collection[StaticGroup]:
|
||||
if self.conflict.is_vector:
|
||||
@ -103,6 +114,8 @@ class GroundObjectsGenerator:
|
||||
utype = db.upgrade_to_supercarrier(utype, cp.name)
|
||||
|
||||
sg = self.m.ship_group(side, g.name, utype, position=g.position, heading=g.units[0].heading)
|
||||
atc_channel = self.radio_registry.alloc_uhf()
|
||||
sg.set_frequency(atc_channel.hertz)
|
||||
sg.units[0].name = self.m.string(g.units[0].name)
|
||||
|
||||
for i, u in enumerate(g.units):
|
||||
@ -111,6 +124,8 @@ class GroundObjectsGenerator:
|
||||
ship.position.x = u.position.x
|
||||
ship.position.y = u.position.y
|
||||
ship.heading = u.heading
|
||||
# TODO: Verify.
|
||||
ship.set_frequency(atc_channel.hertz)
|
||||
sg.add_unit(ship)
|
||||
|
||||
# Find carrier direction (In the wind)
|
||||
@ -125,10 +140,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:
|
||||
|
||||
|
||||
174
gen/kneeboard.py
174
gen/kneeboard.py
@ -23,7 +23,6 @@ only be added per airframe, so PvP missions where each side have the same
|
||||
aircraft will be able to see the enemy's kneeboard for the same airframe.
|
||||
"""
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
@ -31,11 +30,13 @@ from PIL import Image, ImageDraw, ImageFont
|
||||
from tabulate import tabulate
|
||||
|
||||
from pydcs.dcs.mission import Mission
|
||||
from pydcs.dcs.terrain.terrain import Airport
|
||||
from pydcs.dcs.unittype import FlyingType
|
||||
from .airfields import AIRFIELD_DATA
|
||||
from .flights.flight import Flight
|
||||
from . import units
|
||||
from .aircraft import FlightData
|
||||
from .airfields import RunwayData
|
||||
from .airsupportgen import AwacsInfo, TankerInfo
|
||||
from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator
|
||||
from .radios import RadioFrequency
|
||||
|
||||
|
||||
class KneeboardPageWriter:
|
||||
@ -94,68 +95,17 @@ class KneeboardPage:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class AirfieldInfo:
|
||||
def __init__(self, airfield: Airport) -> None:
|
||||
self.airport = airfield
|
||||
# TODO: Implement logic for picking preferred runway.
|
||||
runway = airfield.runways[0]
|
||||
runway_side = ["", "L", "R"][runway.leftright]
|
||||
self.runway = f"{runway.heading}{runway_side}"
|
||||
try:
|
||||
extra_data = AIRFIELD_DATA[airfield.name]
|
||||
self.atc = extra_data.atc.uhf or ""
|
||||
self.tacan = extra_data.tacan or ""
|
||||
self.ils = extra_data.ils_freq(self.runway) or ""
|
||||
except KeyError:
|
||||
self.atc = ""
|
||||
self.ils = ""
|
||||
self.tacan = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class CommInfo:
|
||||
"""Communications information for the kneeboard."""
|
||||
name: str
|
||||
freq: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class AwacsInfo:
|
||||
"""AWACS information for the kneeboard."""
|
||||
callsign: str
|
||||
freq: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class TankerInfo:
|
||||
"""Tanker information for the kneeboard."""
|
||||
callsign: str
|
||||
variant: str
|
||||
freq: str
|
||||
tacan: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class JtacInfo:
|
||||
"""JTAC information for the kneeboard."""
|
||||
callsign: str
|
||||
region: str
|
||||
code: str
|
||||
|
||||
|
||||
class BriefingPage(KneeboardPage):
|
||||
"""A kneeboard page containing briefing information."""
|
||||
def __init__(self, flight: Flight, comms: List[CommInfo],
|
||||
def __init__(self, flight: FlightData, comms: List[CommInfo],
|
||||
awacs: List[AwacsInfo], tankers: List[TankerInfo],
|
||||
jtacs: List[JtacInfo]) -> None:
|
||||
self.flight = flight
|
||||
self.comms = comms
|
||||
self.comms = list(comms)
|
||||
self.awacs = awacs
|
||||
self.tankers = tankers
|
||||
self.jtacs = jtacs
|
||||
self.departure = flight.from_cp.airport
|
||||
self.arrival = flight.from_cp.airport
|
||||
self.divert: Optional[Airport] = None
|
||||
self.comms.append(CommInfo("Flight", self.flight.intra_flight_channel))
|
||||
|
||||
def write(self, path: Path) -> None:
|
||||
writer = KneeboardPageWriter()
|
||||
@ -166,14 +116,14 @@ class BriefingPage(KneeboardPage):
|
||||
# TODO: Handle carriers.
|
||||
writer.heading("Airfield Info")
|
||||
writer.table([
|
||||
self.airfield_info_row("Departure", self.departure),
|
||||
self.airfield_info_row("Arrival", self.arrival),
|
||||
self.airfield_info_row("Divert", self.divert),
|
||||
], headers=["", "Airbase", "ATC", "TCN", "ILS", "RWY"])
|
||||
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 = []
|
||||
for num, waypoint in enumerate(self.flight.points):
|
||||
for num, waypoint in enumerate(self.flight.waypoints):
|
||||
alt = int(units.meters_to_feet(waypoint.alt))
|
||||
flight_plan.append([num, waypoint.pretty_name, str(alt)])
|
||||
writer.table(flight_plan, headers=["STPT", "Action", "Alt"])
|
||||
@ -181,13 +131,13 @@ class BriefingPage(KneeboardPage):
|
||||
writer.heading("Comm Ladder")
|
||||
comms = []
|
||||
for comm in self.comms:
|
||||
comms.append([comm.name, comm.freq])
|
||||
comms.append([comm.name, self.format_frequency(comm.freq)])
|
||||
writer.table(comms, headers=["Name", "UHF"])
|
||||
|
||||
writer.heading("AWACS")
|
||||
awacs = []
|
||||
for a in self.awacs:
|
||||
awacs.append([a.callsign, a.freq])
|
||||
awacs.append([a.callsign, self.format_frequency(a.freq)])
|
||||
writer.table(awacs, headers=["Callsign", "UHF"])
|
||||
|
||||
writer.heading("Tankers")
|
||||
@ -197,7 +147,7 @@ class BriefingPage(KneeboardPage):
|
||||
tanker.callsign,
|
||||
tanker.variant,
|
||||
tanker.tacan,
|
||||
tanker.freq,
|
||||
self.format_frequency(tanker.freq),
|
||||
])
|
||||
writer.table(tankers, headers=["Callsign", "Type", "TACAN", "UHF"])
|
||||
|
||||
@ -210,81 +160,44 @@ class BriefingPage(KneeboardPage):
|
||||
writer.write(path)
|
||||
|
||||
def airfield_info_row(self, row_title: str,
|
||||
airfield: Optional[Airport]) -> List[str]:
|
||||
runway: Optional[RunwayData]) -> List[str]:
|
||||
"""Creates a table row for a given airfield.
|
||||
|
||||
Args:
|
||||
row_title: Purpose of the airfield. e.g. "Departure", "Arrival" or
|
||||
"Divert".
|
||||
airfield: The airfield described by this row.
|
||||
runway: The runway described by this row.
|
||||
|
||||
Returns:
|
||||
A list of strings to be used as a row of the airfield table.
|
||||
"""
|
||||
if airfield is None:
|
||||
if runway is None:
|
||||
return [row_title, "", "", "", "", ""]
|
||||
info = AirfieldInfo(airfield)
|
||||
|
||||
atc = ""
|
||||
if runway.atc is not None:
|
||||
atc = self.format_frequency(runway.atc)
|
||||
return [
|
||||
row_title,
|
||||
airfield.name,
|
||||
info.atc,
|
||||
info.tacan,
|
||||
info.ils,
|
||||
info.runway,
|
||||
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)
|
||||
return f"{channel.radio_name} Ch {channel.channel}"
|
||||
|
||||
class KneeboardGenerator:
|
||||
|
||||
class KneeboardGenerator(MissionInfoGenerator):
|
||||
"""Creates kneeboard pages for each client flight in the mission."""
|
||||
|
||||
def __init__(self, mission: Mission, game) -> None:
|
||||
self.mission = mission
|
||||
self.game = game
|
||||
self.comms: List[CommInfo] = []
|
||||
self.awacs: List[AwacsInfo] = []
|
||||
self.tankers: List[TankerInfo] = []
|
||||
self.jtacs: List[JtacInfo] = []
|
||||
|
||||
def add_comm(self, name: str, freq: str) -> None:
|
||||
"""Adds communications info to the kneeboard.
|
||||
|
||||
Args:
|
||||
name: Name of the radio channel.
|
||||
freq: Frequency of the radio channel.
|
||||
"""
|
||||
self.comms.append(CommInfo(name, freq))
|
||||
|
||||
def add_awacs(self, callsign: str, freq: str) -> None:
|
||||
"""Adds an AWACS/GCI to the kneeboard.
|
||||
|
||||
Args:
|
||||
callsign: Callsign of the AWACS/GCI.
|
||||
freq: Radio frequency used by the AWACS/GCI.
|
||||
"""
|
||||
self.awacs.append(AwacsInfo(callsign, freq))
|
||||
|
||||
def add_tanker(self, callsign: str, variant: str, freq: str,
|
||||
tacan: str) -> None:
|
||||
"""Adds a tanker to the kneeboard.
|
||||
|
||||
Args:
|
||||
callsign: Callsign of the tanker.
|
||||
variant: Aircraft type.
|
||||
freq: Radio frequency used by the tanker.
|
||||
tacan: TACAN channel of the tanker.
|
||||
"""
|
||||
self.tankers.append(TankerInfo(callsign, variant, freq, tacan))
|
||||
|
||||
def add_jtac(self, callsign: str, region: str, code: str) -> None:
|
||||
"""Adds a JTAC to the kneeboard.
|
||||
|
||||
Args:
|
||||
callsign: Callsign of the JTAC.
|
||||
region: JTAC's area of responsibility.
|
||||
code: Laser code used by the JTAC.
|
||||
"""
|
||||
# TODO: Radio info? Type?
|
||||
self.jtacs.append(JtacInfo(callsign, region, code))
|
||||
def __init__(self, mission: Mission) -> None:
|
||||
super().__init__(mission)
|
||||
|
||||
def generate(self) -> None:
|
||||
"""Generates a kneeboard per client flight."""
|
||||
@ -310,15 +223,14 @@ class KneeboardGenerator:
|
||||
that aircraft.
|
||||
"""
|
||||
all_flights: Dict[FlyingType, List[KneeboardPage]] = defaultdict(list)
|
||||
for cp in self.game.theater.controlpoints:
|
||||
if cp.id in self.game.planners.keys():
|
||||
for flight in self.game.planners[cp.id].flights:
|
||||
if flight.client_count > 0:
|
||||
all_flights[flight.unit_type].extend(
|
||||
self.generate_flight_kneeboard(flight))
|
||||
for flight in 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: Flight) -> List[KneeboardPage]:
|
||||
def generate_flight_kneeboard(self, flight: FlightData) -> List[KneeboardPage]:
|
||||
"""Returns a list of kneeboard pages for the given flight."""
|
||||
return [
|
||||
BriefingPage(
|
||||
|
||||
200
gen/radios.py
Normal file
200
gen/radios.py
Normal file
@ -0,0 +1,200 @@
|
||||
"""Radio frequency types and allocators."""
|
||||
import itertools
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Iterator, List, Set
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RadioFrequency:
|
||||
"""A radio frequency.
|
||||
|
||||
Not currently concerned with tracking modulation, just the frequency.
|
||||
"""
|
||||
|
||||
#: The frequency in kilohertz.
|
||||
hertz: int
|
||||
|
||||
def __str__(self):
|
||||
if self.hertz >= 1000000:
|
||||
return self.format("MHz", 1000000)
|
||||
return self.format("kHz", 1000)
|
||||
|
||||
def format(self, units: str, divisor: int) -> str:
|
||||
converted = self.hertz / divisor
|
||||
if converted.is_integer():
|
||||
return f"{int(converted)} {units}"
|
||||
return f"{converted:0.3f} {units}"
|
||||
|
||||
@property
|
||||
def mhz(self) -> float:
|
||||
"""Returns the frequency in megahertz.
|
||||
|
||||
Returns:
|
||||
The frequency in megahertz.
|
||||
"""
|
||||
return self.hertz / 1000000
|
||||
|
||||
|
||||
def MHz(num: int, khz: int = 0) -> RadioFrequency:
|
||||
return RadioFrequency(num * 1000000 + khz * 1000)
|
||||
|
||||
|
||||
def kHz(num: int) -> RadioFrequency:
|
||||
return RadioFrequency(num * 1000)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Radio:
|
||||
"""A radio.
|
||||
|
||||
Defines the minimum (inclusive) and maximum (exclusive) range of the radio.
|
||||
"""
|
||||
|
||||
#: The name of the radio.
|
||||
name: str
|
||||
|
||||
#: The minimum (inclusive) frequency tunable by this radio.
|
||||
minimum: RadioFrequency
|
||||
|
||||
#: The maximum (exclusive) frequency tunable by this radio.
|
||||
maximum: RadioFrequency
|
||||
|
||||
#: The spacing between adjacent frequencies.
|
||||
step: RadioFrequency
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
def range(self) -> Iterator[RadioFrequency]:
|
||||
"""Returns an iterator over the usable frequencies of this radio."""
|
||||
return (RadioFrequency(x) for x in range(
|
||||
self.minimum.hertz, self.maximum.hertz, self.step.hertz
|
||||
))
|
||||
|
||||
|
||||
class OutOfChannelsError(RuntimeError):
|
||||
"""Raised when all channels usable by this radio have been allocated."""
|
||||
|
||||
def __init__(self, radio: Radio) -> None:
|
||||
super().__init__(f"No available channels for {radio}")
|
||||
|
||||
|
||||
class ChannelInUseError(RuntimeError):
|
||||
"""Raised when attempting to reserve an in-use frequency."""
|
||||
|
||||
def __init__(self, frequency: RadioFrequency) -> None:
|
||||
super().__init__(f"{frequency} is already in use")
|
||||
|
||||
|
||||
# TODO: Figure out appropriate steps for each radio. These are just guesses.
|
||||
#: List of all known radios used by aircraft in the game.
|
||||
RADIOS: List[Radio] = [
|
||||
Radio("AN/ARC-164", MHz(225), MHz(400), step=MHz(1)),
|
||||
Radio("AN/ARC-186(V) AM", MHz(116), MHz(152), step=MHz(1)),
|
||||
Radio("AN/ARC-186(V) FM", MHz(30), MHz(76), step=MHz(1)),
|
||||
# The AN/ARC-210 can also use [30, 88) and [108, 118), but the current
|
||||
# implementation can't implement the gap and the radio can't transmit on the
|
||||
# latter. There's still plenty of channels between 118 MHz and 400 MHz, so
|
||||
# not worth worrying about.
|
||||
Radio("AN/ARC-210", MHz(118), MHz(400), step=MHz(1)),
|
||||
Radio("AN/ARC-222", MHz(116), MHz(174), step=MHz(1)),
|
||||
Radio("SCR-522", MHz(100), MHz(156), step=MHz(1)),
|
||||
Radio("A.R.I. 1063", MHz(100), MHz(156), step=MHz(1)),
|
||||
Radio("BC-1206", kHz(200), kHz(400), step=kHz(10)),
|
||||
]
|
||||
|
||||
|
||||
def get_radio(name: str) -> Radio:
|
||||
"""Returns the radio with the given name.
|
||||
|
||||
Args:
|
||||
name: Name of the radio to return.
|
||||
|
||||
Returns:
|
||||
The radio matching name.
|
||||
|
||||
Raises:
|
||||
KeyError: No matching radio was found.
|
||||
"""
|
||||
for radio in RADIOS:
|
||||
if radio.name == name:
|
||||
return radio
|
||||
raise KeyError
|
||||
|
||||
|
||||
class RadioRegistry:
|
||||
"""Manages allocation of radio channels.
|
||||
|
||||
There's some room for improvement here. We could prefer to allocate
|
||||
frequencies that are available to the fewest number of radios first, so
|
||||
radios with wide bands like the AN/ARC-210 don't exhaust all the channels
|
||||
available to narrower radios like the AN/ARC-186(V). In practice there are
|
||||
probably plenty of channels, so we can deal with that later if we need to.
|
||||
|
||||
We could also allocate using a larger increment, returning to smaller
|
||||
increments each time the range is exhausted. This would help with the
|
||||
previous problem, as the AN/ARC-186(V) would still have plenty of 25 kHz
|
||||
increment channels left after the AN/ARC-210 moved on to the higher
|
||||
frequencies. This would also look a little nicer than having every flight
|
||||
allocated in the 30 MHz range.
|
||||
"""
|
||||
|
||||
# Not a real radio, but useful for allocating a channel usable for
|
||||
# inter-flight communications.
|
||||
BLUFOR_UHF = Radio("BLUFOR UHF", MHz(225), MHz(400), step=MHz(1))
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.allocated_channels: Set[RadioFrequency] = set()
|
||||
self.radio_allocators: Dict[Radio, Iterator[RadioFrequency]] = {}
|
||||
|
||||
radios = itertools.chain(RADIOS, [self.BLUFOR_UHF])
|
||||
for radio in radios:
|
||||
self.radio_allocators[radio] = radio.range()
|
||||
|
||||
def alloc_for_radio(self, radio: Radio) -> RadioFrequency:
|
||||
"""Allocates a radio channel tunable by the given radio.
|
||||
|
||||
Args:
|
||||
radio: The radio to allocate a channel for.
|
||||
|
||||
Returns:
|
||||
A radio channel compatible with the given radio.
|
||||
|
||||
Raises:
|
||||
OutOfChannelsError: All channels compatible with the given radio are
|
||||
already allocated.
|
||||
"""
|
||||
allocator = self.radio_allocators[radio]
|
||||
try:
|
||||
while (channel := next(allocator)) in self.allocated_channels:
|
||||
pass
|
||||
return channel
|
||||
except StopIteration:
|
||||
raise OutOfChannelsError(radio)
|
||||
|
||||
def alloc_uhf(self) -> RadioFrequency:
|
||||
"""Allocates a UHF radio channel suitable for inter-flight comms.
|
||||
|
||||
Returns:
|
||||
A UHF radio channel suitable for inter-flight comms.
|
||||
|
||||
Raises:
|
||||
OutOfChannelsError: All channels compatible with the given radio are
|
||||
already allocated.
|
||||
"""
|
||||
return self.alloc_for_radio(self.BLUFOR_UHF)
|
||||
|
||||
def reserve(self, frequency: RadioFrequency) -> None:
|
||||
"""Reserves the given channel.
|
||||
|
||||
Reserving a channel ensures that it will not be allocated in the future.
|
||||
|
||||
Args:
|
||||
frequency: The channel to reserve.
|
||||
|
||||
Raises:
|
||||
ChannelInUseError: The given frequency is already in use.
|
||||
"""
|
||||
if frequency in self.allocated_channels:
|
||||
raise ChannelInUseError(frequency)
|
||||
self.allocated_channels.add(frequency)
|
||||
83
gen/tacan.py
Normal file
83
gen/tacan.py
Normal file
@ -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)
|
||||
2
pydcs
2
pydcs
@ -1 +1 @@
|
||||
Subproject commit d278df68eee2f486f840c178e17893f58313efb8
|
||||
Subproject commit 5c02bf8ea5e3ec5afccc0135e31a3dd15e21342b
|
||||
1157
resources/dcs/beacons/caucasus.json
Normal file
1157
resources/dcs/beacons/caucasus.json
Normal file
File diff suppressed because it is too large
Load Diff
317
resources/dcs/beacons/nevada.json
Normal file
317
resources/dcs/beacons/nevada.json
Normal file
@ -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
|
||||
}
|
||||
]
|
||||
1
resources/dcs/beacons/normandy.json
Normal file
1
resources/dcs/beacons/normandy.json
Normal file
@ -0,0 +1 @@
|
||||
[]
|
||||
709
resources/dcs/beacons/persiangulf.json
Normal file
709
resources/dcs/beacons/persiangulf.json
Normal file
@ -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
|
||||
}
|
||||
]
|
||||
408
resources/dcs/beacons/syria.json
Normal file
408
resources/dcs/beacons/syria.json
Normal file
@ -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
|
||||
}
|
||||
]
|
||||
@ -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")
|
||||
|
||||
183
resources/tools/import_beacons.py
Normal file
183
resources/tools/import_beacons.py
Normal file
@ -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()
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user