Merge pull request #119 from DanAlbert/radio-setup

Allocate per-flight radio channels, set up preset channels.
This commit is contained in:
C. Perreau 2020-09-04 12:50:44 +02:00 committed by GitHub
commit 4446a7f060
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 4385 additions and 792 deletions

View File

@ -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()

View File

@ -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

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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
View 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)

View File

@ -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):

View File

@ -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)

View File

@ -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:

View File

@ -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
View 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
View 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

@ -1 +1 @@
Subproject commit d278df68eee2f486f840c178e17893f58313efb8
Subproject commit 5c02bf8ea5e3ec5afccc0135e31a3dd15e21342b

File diff suppressed because it is too large Load Diff

View 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
}
]

View File

@ -0,0 +1 @@
[]

View 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
}
]

View 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
}
]

View File

@ -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")

View 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()

View File

@ -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