mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
This update includes expanded runway data that lets us clean up some code. It also include beacon IDs for runways and airfields that will let us get ILS and TACAN info from pydcs, but that's a bigger change that I'll land separately.
129 lines
4.2 KiB
Python
129 lines
4.2 KiB
Python
"""Runway information and selection."""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from dataclasses import dataclass
|
|
from typing import Iterator, Optional, TYPE_CHECKING
|
|
|
|
from dcs.terrain.terrain import Airport, RunwayApproach
|
|
|
|
from game.airfields import AirfieldData, AtcData
|
|
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
|
|
|
|
try:
|
|
airfield = AirfieldData.for_airport(theater, airport)
|
|
tacan = airfield.tacan
|
|
tacan_callsign = airfield.tacan_callsign
|
|
ils = airfield.ils_freq(runway.name)
|
|
except KeyError:
|
|
logging.warning(f"No airfield data for {airport.name} ({airport.id}")
|
|
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]
|