dcs-retribution/game/runways.py
Dan Albert 868c38b782
Remove unused data.
We get TACAN, ILS, and ATC data from pydcs now. The rest of this
manually curated data is unused.
2022-10-02 19:56:48 +02:00

133 lines
4.4 KiB
Python

"""Runway information and selection."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Iterator, Optional, TYPE_CHECKING
from dcs.terrain.terrain import Airport, RunwayApproach
from game.atcdata import AtcData
from game.dcs.beacons import BeaconType, Beacons
from game.radio.radios import RadioFrequency
from game.radio.tacan import TacanChannel
from game.utils import Heading
from game.weather import Conditions
if TYPE_CHECKING:
from game.theater import ConflictTheater
@dataclass(frozen=True)
class RunwayData:
airfield_name: str
runway_heading: Heading
runway_name: str
atc: Optional[RadioFrequency] = None
tacan: Optional[TacanChannel] = None
tacan_callsign: Optional[str] = None
ils: Optional[RadioFrequency] = None
icls: Optional[int] = None
@classmethod
def for_pydcs_runway_runway(
cls,
theater: ConflictTheater,
airport: Airport,
runway: RunwayApproach,
) -> RunwayData:
"""Creates RunwayData for the given runway of an airfield.
Args:
theater: The theater the airport is in.
airport: The airfield the runway belongs to.
runway: The pydcs runway.
"""
atc: Optional[RadioFrequency] = None
tacan: Optional[TacanChannel] = None
tacan_callsign: Optional[str] = None
ils: Optional[RadioFrequency] = None
atc_radio = AtcData.from_pydcs(airport)
if atc_radio is not None:
atc = atc_radio.uhf
for beacon_data in airport.beacons:
beacon = Beacons.with_id(beacon_data.id, theater)
if beacon.is_tacan:
tacan = beacon.tacan_channel
tacan_callsign = beacon.callsign
for beacon_data in runway.beacons:
beacon = Beacons.with_id(beacon_data.id, theater)
if beacon.beacon_type is BeaconType.BEACON_TYPE_ILS_GLIDESLOPE:
ils = beacon.frequency
return cls(
airfield_name=airport.name,
runway_heading=Heading(runway.heading),
runway_name=runway.name,
atc=atc,
tacan=tacan,
tacan_callsign=tacan_callsign,
ils=ils,
)
@classmethod
def for_pydcs_airport(
cls, theater: ConflictTheater, airport: Airport
) -> Iterator[RunwayData]:
for runway in airport.runways:
yield cls.for_pydcs_runway_runway(
theater,
airport,
runway.main,
)
yield cls.for_pydcs_runway_runway(
theater,
airport,
runway.opposite,
)
class RunwayAssigner:
def __init__(self, conditions: Conditions):
self.conditions = conditions
def angle_off_headwind(self, runway: RunwayData) -> Heading:
wind = Heading.from_degrees(self.conditions.weather.wind.at_0m.direction)
ideal_heading = wind.opposite
return runway.runway_heading.angle_between(ideal_heading)
def get_preferred_runway(
self, theater: ConflictTheater, airport: Airport
) -> RunwayData:
"""Returns the preferred runway for the given airport.
Right now we're only selecting runways based on whether or not
they have
ILS, but we could also choose based on wind conditions, or which
direction flight plans should follow.
"""
runways = list(RunwayData.for_pydcs_airport(theater, airport))
# Find the runway with the best headwind first.
best_runways = [runways[0]]
best_angle_off_headwind = self.angle_off_headwind(best_runways[0])
for runway in runways[1:]:
angle_off_headwind = self.angle_off_headwind(runway)
if angle_off_headwind == best_angle_off_headwind:
best_runways.append(runway)
elif angle_off_headwind < best_angle_off_headwind:
best_runways = [runway]
best_angle_off_headwind = angle_off_headwind
for runway in best_runways:
# But if there are multiple runways with the same heading,
# prefer
# and ILS capable runway.
if runway.ils is not None:
return runway
# Otherwise the only difference between the two is the distance from
# parking, which we don't know, so just pick the first one.
return best_runways[0]