mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
This shouldn't be the case for anything shipped, but is typical when new theaters are still being developed. We could potentially add an `in_progress` flag to the theater definition to make this only optionally tolerant, but since that code path would rarely be exercised it's just likely to bitrot. This data isn't critical to mission generation anyway, so this is fine. What we should do is add some linters that document all the data that is missing though (and ideally publish that to our docs).
188 lines
6.0 KiB
Python
188 lines
6.0 KiB
Python
"""Extra airfield data that is not exposed by pydcs.
|
|
|
|
Remove once https://github.com/pydcs/dcs/issues/69 tracks getting the missing
|
|
data added to pydcs. Until then, missing data can be manually filled in here.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from collections.abc import Iterator
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Any, ClassVar, Dict, Optional, TYPE_CHECKING, Tuple
|
|
|
|
import yaml
|
|
from dcs.task import Modulation
|
|
from dcs.terrain import Airport
|
|
|
|
from game.radio.radios import RadioFrequency
|
|
from game.radio.tacan import TacanChannel
|
|
|
|
if TYPE_CHECKING:
|
|
from game.theater import ConflictTheater
|
|
|
|
|
|
@dataclass
|
|
class AtcData:
|
|
hf: RadioFrequency
|
|
vhf_fm: RadioFrequency
|
|
vhf_am: RadioFrequency
|
|
uhf: RadioFrequency
|
|
|
|
@classmethod
|
|
def from_yaml(cls, data: dict[str, Any]) -> Optional[AtcData]:
|
|
atc_data = data.get("atc")
|
|
if atc_data is None:
|
|
return None
|
|
return AtcData(
|
|
RadioFrequency.parse(atc_data["hf"], Modulation.FM),
|
|
RadioFrequency.parse(atc_data["vhf_low"], Modulation.FM),
|
|
RadioFrequency.parse(atc_data["vhf_high"], Modulation.AM),
|
|
RadioFrequency.parse(atc_data["uhf"], Modulation.AM),
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class AirfieldData:
|
|
"""Additional airfield data not included in pydcs."""
|
|
|
|
#: Airfield name for the UI. Not stable.
|
|
name: str
|
|
|
|
#: pydcs airport ID
|
|
id: int
|
|
|
|
#: ICAO airport code
|
|
icao: Optional[str] = None
|
|
|
|
#: Elevation (in ft).
|
|
elevation: int = 0
|
|
|
|
#: Runway length (in ft).
|
|
runway_length: int = 0
|
|
|
|
#: TACAN channel for the airfield.
|
|
tacan: Optional[TacanChannel] = None
|
|
|
|
#: TACAN callsign
|
|
tacan_callsign: Optional[str] = None
|
|
|
|
#: VOR as a tuple of (callsign, frequency).
|
|
vor: Optional[Tuple[str, RadioFrequency]] = None
|
|
|
|
#: RSBN channel as a tuple of (callsign, channel).
|
|
rsbn: Optional[Tuple[str, int]] = None
|
|
|
|
#: Radio channels used by the airfield's ATC. Note that not all airfields
|
|
#: have ATCs.
|
|
atc: Optional[AtcData] = None
|
|
|
|
#: Dict of runway heading -> ILS tuple of (callsign, frequency).
|
|
ils: Dict[str, Tuple[str, RadioFrequency]] = field(default_factory=dict)
|
|
|
|
#: Dict of runway heading -> PRMG tuple of (callsign, channel).
|
|
prmg: Dict[str, Tuple[str, int]] = field(default_factory=dict)
|
|
|
|
#: Dict of runway heading -> outer NDB tuple of (callsign, frequency).
|
|
outer_ndb: Dict[str, Tuple[str, RadioFrequency]] = field(default_factory=dict)
|
|
|
|
#: Dict of runway heading -> inner NDB tuple of (callsign, frequency).
|
|
inner_ndb: Dict[str, Tuple[str, RadioFrequency]] = field(default_factory=dict)
|
|
|
|
_airfields: ClassVar[dict[str, dict[int, AirfieldData]]] = {}
|
|
|
|
def ils_freq(self, runway: str) -> Optional[RadioFrequency]:
|
|
ils = self.ils.get(runway)
|
|
if ils is not None:
|
|
return ils[1]
|
|
return None
|
|
|
|
@classmethod
|
|
def from_file(cls, airfield_yaml: Path) -> AirfieldData:
|
|
with airfield_yaml.open() as yaml_file:
|
|
data = yaml.safe_load(yaml_file)
|
|
|
|
tacan_channel = None
|
|
tacan_callsign = None
|
|
if (tacan := data.get("tacan")) is not None:
|
|
tacan_channel = TacanChannel.parse(tacan["channel"])
|
|
tacan_callsign = tacan["callsign"]
|
|
|
|
vor = None
|
|
if (vor_data := data.get("vor")) is not None:
|
|
vor = (
|
|
vor_data["callsign"],
|
|
RadioFrequency.parse(vor_data["frequency"], Modulation.FM),
|
|
)
|
|
|
|
rsbn = None
|
|
if (rsbn_data := data.get("rsbn")) is not None:
|
|
rsbn = (rsbn_data["callsign"], rsbn_data["channel"])
|
|
|
|
ils = {}
|
|
prmg = {}
|
|
outer_ndb = {}
|
|
inner_ndb = {}
|
|
for name, runway_data in data.get("runways", {}).items():
|
|
if (ils_data := runway_data.get("ils")) is not None:
|
|
ils[name] = (
|
|
ils_data["callsign"],
|
|
RadioFrequency.parse(ils_data["frequency"], Modulation.FM),
|
|
)
|
|
|
|
if (prmg_data := runway_data.get("prmg")) is not None:
|
|
prmg[name] = (prmg_data["callsign"], prmg_data["channel"])
|
|
|
|
if (outer_ndb_data := runway_data.get("outer_ndb")) is not None:
|
|
outer_ndb[name] = (
|
|
outer_ndb_data["callsign"],
|
|
RadioFrequency.parse(outer_ndb_data["frequency"], Modulation.AM),
|
|
)
|
|
|
|
if (inner_ndb_data := runway_data.get("inner_ndb")) is not None:
|
|
inner_ndb[name] = (
|
|
inner_ndb_data["callsign"],
|
|
RadioFrequency.parse(inner_ndb_data["frequency"], Modulation.AM),
|
|
)
|
|
|
|
return AirfieldData(
|
|
data["name"],
|
|
data["id"],
|
|
data.get("icao"),
|
|
data["elevation"],
|
|
data["runway_length"],
|
|
tacan_channel,
|
|
tacan_callsign,
|
|
vor,
|
|
rsbn,
|
|
AtcData.from_yaml(data),
|
|
ils,
|
|
prmg,
|
|
outer_ndb,
|
|
inner_ndb,
|
|
)
|
|
|
|
@classmethod
|
|
def _load_for_theater_if_needed(cls, theater: ConflictTheater) -> None:
|
|
if theater.terrain.name in cls._airfields:
|
|
return
|
|
|
|
airfields = {}
|
|
base_path = Path("resources/airfields") / theater.terrain.name
|
|
if base_path.is_dir():
|
|
for airfield_yaml in base_path.iterdir():
|
|
data = cls.from_file(airfield_yaml)
|
|
airfields[data.id] = data
|
|
else:
|
|
logging.warning("No airfield data available for %s", theater.terrain.name)
|
|
cls._airfields[theater.terrain.name] = airfields
|
|
|
|
@classmethod
|
|
def for_airport(cls, theater: ConflictTheater, airport: Airport) -> AirfieldData:
|
|
return cls._airfields[theater.terrain.name][airport.id]
|
|
|
|
@classmethod
|
|
def for_theater(cls, theater: ConflictTheater) -> Iterator[AirfieldData]:
|
|
cls._load_for_theater_if_needed(theater)
|
|
yield from cls._airfields[theater.terrain.name].values()
|