dcs_liberation/game/radio/channels.py
zhexu14 9066fafcb1
DCS 55918 (#3401)
This PR adds support for DCS 2.9.5.55918. Some limited support for the
Kiowa Warrior is also implemented.
2024-06-06 22:41:08 +10:00

453 lines
14 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from game.missiongenerator.aircraft.flightdata import FlightData
from game.missiongenerator.missiondata import MissionData
class RadioChannelAllocator:
"""Base class for radio channel allocators."""
def assign_channels_for_flight(
self, flight: FlightData, mission_data: MissionData
) -> None:
"""Assigns mission frequencies to preset channels for the flight."""
raise NotImplementedError
@classmethod
def from_cfg(cls, cfg: dict[str, Any]) -> RadioChannelAllocator:
return cls()
@classmethod
def name(cls) -> str:
raise NotImplementedError
@dataclass(frozen=True)
class CommonRadioChannelAllocator(RadioChannelAllocator):
"""Radio channel allocator suitable for most aircraft.
Most of the aircraft with preset channels available have one or more radios
with 20 or more channels available (typically per-radio, but this is not the
case for the JF-17).
"""
#: Index of the radio used for intra-flight communications. Matches the
#: index of the panel_radio field of the pydcs.dcs.planes object.
inter_flight_radio_index: Optional[int]
#: Index of the radio used for intra-flight communications. Matches the
#: index of the panel_radio field of the pydcs.dcs.planes object.
intra_flight_radio_index: Optional[int]
def assign_channels_for_flight(
self, flight: FlightData, mission_data: MissionData
) -> None:
if self.intra_flight_radio_index is not None:
flight.assign_channel(
self.intra_flight_radio_index, 1, flight.intra_flight_channel
)
if self.inter_flight_radio_index is None:
return
# For cases where the inter-flight and intra-flight radios share presets
# (the JF-17 only has one set of channels, even though it can use two
# channels simultaneously), start assigning inter-flight channels at 2.
radio_id = self.inter_flight_radio_index
if self.intra_flight_radio_index == radio_id:
first_channel = 2
else:
first_channel = 1
last_channel = flight.num_radio_channels(radio_id)
channel_alloc = iter(range(first_channel, last_channel + 1))
if flight.departure.atc is not None:
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 mission_data.awacs:
flight.assign_channel(radio_id, next(channel_alloc), awacs.freq)
for jtac in mission_data.jtacs:
flight.assign_channel(radio_id, next(channel_alloc), jtac.freq)
if flight.arrival != flight.departure and flight.arrival.atc is not None:
flight.assign_channel(radio_id, next(channel_alloc), flight.arrival.atc)
try:
# TODO: Skip incompatible tankers.
for tanker in mission_data.tankers:
flight.assign_channel(radio_id, next(channel_alloc), tanker.freq)
if flight.divert is not None and flight.divert.atc 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
@classmethod
def from_cfg(cls, cfg: dict[str, Any]) -> CommonRadioChannelAllocator:
return CommonRadioChannelAllocator(
inter_flight_radio_index=cfg["inter_flight_radio_index"],
intra_flight_radio_index=cfg["intra_flight_radio_index"],
)
@classmethod
def name(cls) -> str:
return "common"
@dataclass(frozen=True)
class NoOpChannelAllocator(RadioChannelAllocator):
"""Channel allocator for aircraft that don't support preset channels."""
def assign_channels_for_flight(
self, flight: FlightData, mission_data: MissionData
) -> None:
pass
@classmethod
def name(cls) -> str:
return "noop"
@dataclass(frozen=True)
class FarmerRadioChannelAllocator(RadioChannelAllocator):
"""Preset channel allocator for the MiG-19P."""
def assign_channels_for_flight(
self, flight: FlightData, mission_data: MissionData
) -> None:
# The Farmer only has 6 preset channels. It also only has a VHF radio,
# and currently our ATC data and AWACS are only in the UHF band.
radio_id = 1
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
# TODO: Assign 4-6 to VHF frequencies of departure, arrival, and divert.
# TODO: Assign 2 and 3 to AWACS if it is VHF.
@classmethod
def name(cls) -> str:
return "farmer"
@dataclass(frozen=True)
class ViggenRadioChannelAllocator(RadioChannelAllocator):
"""Preset channel allocator for the AJS37."""
def assign_channels_for_flight(
self, flight: FlightData, mission_data: MissionData
) -> None:
# The Viggen's preset channels are handled differently from other
# aircraft. Since 2.7.9 the group channels will not be generated automatically
# anymore. So we have to set AWACS and JTAC manually. There are also seven
# special channels we can modify. We'll set the first channel of the main radio
# to the intra-flight channel, and the first three emergency channels to each
# of the flight plan's airfields. The fourth emergency channel is always
# the guard channel.
radio_id = 1
# Possible Group Channels (100-139)
channel_alloc = iter(range(1, 40))
# Intra-Flight channel on Special 1 and Group 100 (required by module)
flight.assign_channel(radio_id, 41, flight.intra_flight_channel) # Special 1
flight.assign_channel(
radio_id, next(channel_alloc), flight.intra_flight_channel
)
for awacs in mission_data.awacs:
flight.assign_channel(radio_id, next(channel_alloc), awacs.freq)
for jtac in mission_data.jtacs:
flight.assign_channel(radio_id, next(channel_alloc), jtac.freq)
if flight.departure.atc is not None:
flight.assign_channel(radio_id, 44, flight.departure.atc) # FR24 E
if flight.arrival.atc is not None:
flight.assign_channel(radio_id, 45, flight.arrival.atc) # FR24 F
if flight.divert is not None and flight.divert.atc is not None:
flight.assign_channel(radio_id, 46, flight.divert.atc) # FR24 G
@classmethod
def name(cls) -> str:
return "viggen"
@dataclass(frozen=True)
class SCR522RadioChannelAllocator(RadioChannelAllocator):
"""Preset channel allocator for the SCR522 WW2 radios. (4 channels)"""
def assign_channels_for_flight(
self, flight: FlightData, mission_data: MissionData
) -> None:
radio_id = 1
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
if flight.departure.atc is not None:
flight.assign_channel(radio_id, 2, flight.departure.atc)
if flight.arrival.atc is not None:
flight.assign_channel(radio_id, 3, flight.arrival.atc)
# TODO : Some GCI on Channel 4 ?
@classmethod
def name(cls) -> str:
return "SCR-522"
class ChannelNamer:
"""Base class allowing channel name customization per-aircraft.
Most aircraft will want to customize this behavior, but the default is
reasonable for any aircraft with numbered radios.
"""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
"""Returns the name of the channel for the given radio and channel."""
return f"COMM{radio_id} Ch {channel_id}"
@classmethod
def name(cls) -> str:
return "default"
class SingleRadioChannelNamer(ChannelNamer):
"""Channel namer for the aircraft with only a single radio.
Aircraft like the MiG-19P and the MiG-21bis only have a single radio, so
it's not necessary for us to name the radio when naming the channel.
"""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
return f"Ch {channel_id}"
@classmethod
def name(cls) -> str:
return "single"
class HueyChannelNamer(ChannelNamer):
"""Channel namer for the UH-1H."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
return f"COM3 Ch {channel_id}"
@classmethod
def name(cls) -> str:
return "huey"
class MirageChannelNamer(ChannelNamer):
"""Channel namer for the M-2000."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
radio_name = ["V/UHF", "UHF"][radio_id - 1]
return f"{radio_name} Ch {channel_id}"
@classmethod
def name(cls) -> str:
return "mirage"
class MirageF1CEChannelNamer(ChannelNamer):
"""Channel namer for the Mirage-F1CE."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
radio_name = ["V/UHF", "UHF"][radio_id - 1]
return f"{radio_name} Ch {channel_id}"
@classmethod
def name(cls) -> str:
return "mirage-f1CE"
class ApacheChannelNamer(ChannelNamer):
"""Channel namer for the AH-64D Apache"""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
# From the manual: Radio identifier (“VHF” for ARC-186, “UHF” for ARC-164,
# “FM1” for first ARC-201D, “FM2” for second ARC-201D, or “HF” for ARC-220).
radio_name = [
"VHF", # ARC-186
"UHF", # ARC-164
"FM1", # first ARC-201D
"FM2", # second ARC-201D
"HF", # ARC-220
][radio_id - 1]
return f"{radio_name} Ch {channel_id}"
@classmethod
def name(cls) -> str:
return "apache"
class TomcatChannelNamer(ChannelNamer):
"""Channel namer for the F-14."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
radio_name = ["UHF", "VHF/UHF"][radio_id - 1]
return f"{radio_name} Ch {channel_id}"
@classmethod
def name(cls) -> str:
return "tomcat"
class ViggenChannelNamer(ChannelNamer):
"""Channel namer for the AJS37."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
special_channels = [
"FR 22 Special 1",
"FR 22 Special 2",
"FR 22 Special 3",
"FR 24 E",
"FR 24 F",
"FR 24 G",
"FR 24 H",
]
if channel_id >= 41: # Special channels are 41-47
return special_channels[channel_id - 41]
return f"FR 22 Group {99 + channel_id}"
@classmethod
def name(cls) -> str:
return "viggen"
class ViperChannelNamer(ChannelNamer):
"""Channel namer for the F-16."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
return f"COM{radio_id} Ch {channel_id}"
@classmethod
def name(cls) -> str:
return "viper"
class SCR522ChannelNamer(ChannelNamer):
"""
Channel namer for P-51 & P-47D
"""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
if channel_id > 3:
return "?"
else:
return f"Button " + "ABCD"[channel_id - 1]
@classmethod
def name(cls) -> str:
return "SCR-522"
class LegacyWarthogChannelNamer(ChannelNamer):
"""Channel namer for the legacy A-10C."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
radio_name = {
1: "VHF AM",
2: "UHF",
3: "VHF FM",
}[radio_id]
return f"{radio_name} Ch {channel_id}"
@classmethod
def name(cls) -> str:
return "a10c-legacy"
class WarthogChannelNamer(ChannelNamer):
"""Channel namer for the legacy A-10C II"""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
radio_name = {
1: "COM 1",
2: "UHF",
3: "VHF FM",
}[radio_id]
return f"{radio_name} Ch {channel_id}"
@classmethod
def name(cls) -> str:
return "a10c-ii"
class PhantomChannelNamer(ChannelNamer):
"""Channel namer for the F4-E."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
radio_name = [
"COMM", # AN/ARC-164 COMM
"AUX", # AN/ARC-164 AUX
][radio_id - 1]
return f"{radio_name} Ch {channel_id}"
@classmethod
def name(cls) -> str:
return "phantom"
class HindChannelNamer(ChannelNamer):
"""Channel namer for Mi-24 Hind"""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
radio_name = [
"R863",
"R828",
][radio_id - 1]
return f"{radio_name} Ch {channel_id-1}"
@classmethod
def name(cls) -> str:
return "hind"
class HipChannelNamer(ChannelNamer):
"""Channel namer for Mi-8 Hip"""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
radio_name = [
"R863",
"R828",
][radio_id - 1]
return f"{radio_name} Ch {channel_id}"
@classmethod
def name(cls) -> str:
return "hip"
class KiowaChannelNamer(ChannelNamer):
"""Channel namer for OH58D Kiowa Warrior"""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
radio_name = ["UHF AM", "VHF AM", "VHF FM1", "VHF FM2"][radio_id - 1]
return f"{radio_name} Ch {channel_id}"
@classmethod
def name(cls) -> str:
return "kiowa"