Pick runways and ascent/descent based on headwind.

This commit is contained in:
Dan Albert 2020-10-17 16:36:49 -07:00
parent aa7ffdabb0
commit dd4c37cde3
8 changed files with 177 additions and 105 deletions

View File

@ -64,7 +64,6 @@ from game import db
from game.data.cap_capabilities_db import GUNFIGHTERS from game.data.cap_capabilities_db import GUNFIGHTERS
from game.settings import Settings from game.settings import Settings
from game.utils import nm_to_meter from game.utils import nm_to_meter
from gen.airfields import RunwayData
from gen.airsupportgen import AirSupport from gen.airsupportgen import AirSupport
from gen.ato import AirTaskingOrder, Package from gen.ato import AirTaskingOrder, Package
from gen.callsigns import create_group_callsign_from_unit from gen.callsigns import create_group_callsign_from_unit
@ -75,11 +74,13 @@ from gen.flights.flight import (
FlightWaypointType, FlightWaypointType,
) )
from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
from gen.runways import RunwayData
from theater import TheaterGroundObject from theater import TheaterGroundObject
from theater.controlpoint import ControlPoint, ControlPointType from theater.controlpoint import ControlPoint, ControlPointType
from .conflictgen import Conflict from .conflictgen import Conflict
from .flights.traveltime import PackageWaypointTiming, TotEstimator from .flights.traveltime import PackageWaypointTiming, TotEstimator
from .naming import namegen from .naming import namegen
from .runways import RunwayAssigner
WARM_START_HELI_AIRSPEED = 120 WARM_START_HELI_AIRSPEED = 120
WARM_START_HELI_ALT = 500 WARM_START_HELI_ALT = 500
@ -621,9 +622,12 @@ class AircraftConflictGenerator:
# TODO: Support for different departure/arrival airfields. # TODO: Support for different departure/arrival airfields.
cp = flight.from_cp cp = flight.from_cp
fallback_runway = RunwayData(cp.full_name, runway_name="") fallback_runway = RunwayData(cp.full_name, runway_heading=0,
runway_name="")
if cp.cptype == ControlPointType.AIRBASE: if cp.cptype == ControlPointType.AIRBASE:
departure_runway = self.get_preferred_runway(flight.from_cp.airport) assigner = RunwayAssigner(self.game.conditions)
departure_runway = assigner.get_preferred_runway(
flight.from_cp.airport)
elif cp.is_fleet: elif cp.is_fleet:
departure_runway = dynamic_runways.get(cp.name, fallback_runway) departure_runway = dynamic_runways.get(cp.name, fallback_runway)
else: else:
@ -655,22 +659,6 @@ class AircraftConflictGenerator:
for unit in group.units: for unit in group.units:
unit.fuel = Su_33.fuel_max * 0.8 unit.fuel = Su_33.fuel_max * 0.8
def get_preferred_runway(self, 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(airport))
for runway in runways:
# Prefer any runway with ILS.
if runway.ils is not None:
return runway
# Otherwise we lack the mission information to pick more usefully,
# so just use the first runway.
return runways[0]
def _generate_at_airport(self, name: str, side: Country, def _generate_at_airport(self, name: str, side: Country,
unit_type: FlyingType, count: int, start_type: str, unit_type: FlyingType, count: int, start_type: str,
airport: Optional[Airport] = None) -> FlyingGroup: airport: Optional[Airport] = None) -> FlyingGroup:

View File

@ -3,11 +3,11 @@
Remove once https://github.com/pydcs/dcs/issues/69 tracks getting the missing 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. data added to pydcs. Until then, missing data can be manually filled in here.
""" """
from dataclasses import dataclass, field from __future__ import annotations
import logging
from typing import Dict, Iterator, Optional, Tuple from dataclasses import dataclass, field
from typing import Dict, Optional, Tuple
from dcs.terrain.terrain import Airport
from .radios import MHz, RadioFrequency from .radios import MHz, RadioFrequency
from .tacan import TacanBand, TacanChannel from .tacan import TacanBand, TacanChannel
@ -1503,61 +1503,3 @@ AIRFIELD_DATA = {
atc=AtcData(MHz(3, 775), MHz(118, 50), MHz(38, 450), MHz(250, 50)), atc=AtcData(MHz(3, 775), MHz(118, 50), MHz(38, 450), MHz(250, 50)),
), ),
} }
@dataclass(frozen=True)
class RunwayData:
airfield_name: str
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_airfield(cls, airport: Airport, runway: str) -> "RunwayData":
"""Creates RunwayData for the given runway of an airfield.
Args:
airport: The airfield the runway belongs to.
runway: Identifier of the runway to use. e.g. "03" or "20L".
"""
atc: Optional[RadioFrequency] = None
tacan: Optional[TacanChannel] = None
tacan_callsign: Optional[str] = None
ils: Optional[RadioFrequency] = None
try:
airfield = AIRFIELD_DATA[airport.name]
if airfield.atc is not None:
atc = airfield.atc.uhf
else:
atc = None
tacan = airfield.tacan
tacan_callsign = airfield.tacan_callsign
ils = airfield.ils_freq(runway)
except KeyError:
logging.warning(f"No airfield data for {airport.name}")
return cls(
airfield_name=airport.name,
runway_name=runway,
atc=atc,
tacan=tacan,
tacan_callsign=tacan_callsign,
ils=ils
)
@classmethod
def for_pydcs_airport(cls, airport: Airport) -> Iterator["RunwayData"]:
for runway in airport.runways:
runway_number = runway.heading // 10
runway_side = ["", "L", "R"][runway.leftright]
runway_name = f"{runway_number:02}{runway_side}"
yield cls.for_airfield(airport, runway_name)
# pydcs only exposes one runway per physical runway, so to expose
# both sides of the runway we need to generate the other.
runway_number = ((runway.heading + 180) % 360) // 10
runway_side = ["", "R", "L"][runway.leftright]
runway_name = f"{runway_number:02}{runway_side}"
yield cls.for_airfield(airport, runway_name)

View File

@ -1,19 +1,20 @@
import datetime import datetime
import os import os
import random
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass
import random
from typing import List from typing import List
from game import db
from dcs.mission import Mission from dcs.mission import Mission
from game import db
from .aircraft import FlightData from .aircraft import FlightData
from .airfields import RunwayData
from .airsupportgen import AwacsInfo, TankerInfo from .airsupportgen import AwacsInfo, TankerInfo
from .armor import JtacInfo from .armor import JtacInfo
from .conflictgen import Conflict from .conflictgen import Conflict
from .ground_forces.combat_stance import CombatStance from .ground_forces.combat_stance import CombatStance
from .radios import RadioFrequency from .radios import RadioFrequency
from .runways import RunwayData
@dataclass @dataclass

View File

@ -132,7 +132,7 @@ class FlightPlanBuilder:
if not isinstance(location, TheaterGroundObject): if not isinstance(location, TheaterGroundObject):
raise InvalidObjectiveLocation(flight.flight_type, location) raise InvalidObjectiveLocation(flight.flight_type, location)
builder = WaypointBuilder(flight, self.doctrine) builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
builder.ascent(flight.from_cp) builder.ascent(flight.from_cp)
builder.hold(self._hold_point(flight)) builder.hold(self._hold_point(flight))
builder.join(self.package.waypoints.join) builder.join(self.package.waypoints.join)
@ -222,7 +222,7 @@ class FlightPlanBuilder:
) )
start = end.point_from_heading(heading - 180, diameter) start = end.point_from_heading(heading - 180, diameter)
builder = WaypointBuilder(flight, self.doctrine) builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
builder.ascent(flight.from_cp) builder.ascent(flight.from_cp)
builder.race_track(start, end, patrol_alt) builder.race_track(start, end, patrol_alt)
builder.rtb(flight.from_cp) builder.rtb(flight.from_cp)
@ -264,7 +264,7 @@ class FlightPlanBuilder:
orbit1p = orbit_center.point_from_heading(heading + 180, radius) orbit1p = orbit_center.point_from_heading(heading + 180, radius)
# Create points # Create points
builder = WaypointBuilder(flight, self.doctrine) builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
builder.ascent(flight.from_cp) builder.ascent(flight.from_cp)
builder.hold(self._hold_point(flight)) builder.hold(self._hold_point(flight))
builder.join(self.package.waypoints.join) builder.join(self.package.waypoints.join)
@ -290,7 +290,7 @@ class FlightPlanBuilder:
if custom_targets is None: if custom_targets is None:
custom_targets = [] custom_targets = []
builder = WaypointBuilder(flight, self.doctrine) builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
builder.ascent(flight.from_cp) builder.ascent(flight.from_cp)
builder.hold(self._hold_point(flight)) builder.hold(self._hold_point(flight))
builder.join(self.package.waypoints.join) builder.join(self.package.waypoints.join)
@ -328,7 +328,7 @@ class FlightPlanBuilder:
def generate_escort(self, flight: Flight) -> None: def generate_escort(self, flight: Flight) -> None:
assert self.package.waypoints is not None assert self.package.waypoints is not None
builder = WaypointBuilder(flight, self.doctrine) builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
builder.ascent(flight.from_cp) builder.ascent(flight.from_cp)
builder.hold(self._hold_point(flight)) builder.hold(self._hold_point(flight))
builder.join(self.package.waypoints.join) builder.join(self.package.waypoints.join)
@ -361,7 +361,7 @@ class FlightPlanBuilder:
center = ingress.point_from_heading(heading, distance / 2) center = ingress.point_from_heading(heading, distance / 2)
egress = ingress.point_from_heading(heading, distance) egress = ingress.point_from_heading(heading, distance)
builder = WaypointBuilder(flight, self.doctrine) builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
builder.ascent(flight.from_cp, is_helo) builder.ascent(flight.from_cp, is_helo)
builder.hold(self._hold_point(flight)) builder.hold(self._hold_point(flight))
builder.join(self.package.waypoints.join) builder.join(self.package.waypoints.join)
@ -382,7 +382,7 @@ class FlightPlanBuilder:
flight: The flight to generate the descend point for. flight: The flight to generate the descend point for.
departure: Departure airfield or carrier. departure: Departure airfield or carrier.
""" """
builder = WaypointBuilder(flight, self.doctrine) builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
builder.ascent(departure) builder.ascent(departure)
return builder.build()[0] return builder.build()[0]
@ -394,7 +394,7 @@ class FlightPlanBuilder:
flight: The flight to generate the descend point for. flight: The flight to generate the descend point for.
arrival: Arrival airfield or carrier. arrival: Arrival airfield or carrier.
""" """
builder = WaypointBuilder(flight, self.doctrine) builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
builder.descent(arrival) builder.descent(arrival)
return builder.build()[0] return builder.build()[0]
@ -406,7 +406,7 @@ class FlightPlanBuilder:
flight: The flight to generate the landing waypoint for. flight: The flight to generate the landing waypoint for.
arrival: Arrival airfield or carrier. arrival: Arrival airfield or carrier.
""" """
builder = WaypointBuilder(flight, self.doctrine) builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
builder.land(arrival) builder.land(arrival)
return builder.build()[0] return builder.build()[0]

View File

@ -7,12 +7,16 @@ from dcs.unit import Unit
from game.data.doctrine import Doctrine from game.data.doctrine import Doctrine
from game.utils import nm_to_meter from game.utils import nm_to_meter
from game.weather import Conditions
from theater import ControlPoint, MissionTarget, TheaterGroundObject from theater import ControlPoint, MissionTarget, TheaterGroundObject
from .flight import Flight, FlightWaypoint, FlightWaypointType from .flight import Flight, FlightWaypoint, FlightWaypointType
from ..runways import RunwayAssigner
class WaypointBuilder: class WaypointBuilder:
def __init__(self, flight: Flight, doctrine: Doctrine) -> None: def __init__(self, conditions: Conditions, flight: Flight,
doctrine: Doctrine) -> None:
self.conditions = conditions
self.flight = flight self.flight = flight
self.doctrine = doctrine self.doctrine = doctrine
self.waypoints: List[FlightWaypoint] = [] self.waypoints: List[FlightWaypoint] = []
@ -28,8 +32,7 @@ class WaypointBuilder:
departure: Departure airfield or carrier. departure: Departure airfield or carrier.
is_helo: True if the flight is a helicopter. is_helo: True if the flight is a helicopter.
""" """
# TODO: Pick runway based on wind direction. heading = RunwayAssigner(self.conditions).takeoff_heading(departure)
heading = departure.heading
position = departure.position.point_from_heading( position = departure.position.point_from_heading(
heading, nm_to_meter(5) heading, nm_to_meter(5)
) )
@ -52,9 +55,8 @@ class WaypointBuilder:
arrival: Arrival airfield or carrier. arrival: Arrival airfield or carrier.
is_helo: True if the flight is a helicopter. is_helo: True if the flight is a helicopter.
""" """
# TODO: Pick runway based on wind direction. landing_heading = RunwayAssigner(self.conditions).landing_heading(arrival)
# ControlPoint.heading is the departure heading. heading = (landing_heading + 180) % 360
heading = (arrival.heading + 180) % 360
position = arrival.position.point_from_heading( position = arrival.position.point_from_heading(
heading, nm_to_meter(5) heading, nm_to_meter(5)
) )

View File

@ -16,9 +16,9 @@ from dcs.unitgroup import StaticGroup
from game import db from game import db
from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID
from game.db import unit_type_from_name from game.db import unit_type_from_name
from .airfields import RunwayData
from .conflictgen import Conflict from .conflictgen import Conflict
from .radios import RadioRegistry from .radios import RadioRegistry
from .runways import RunwayData
from .tacan import TacanBand, TacanRegistry from .tacan import TacanBand, TacanRegistry
FARP_FRONTLINE_DISTANCE = 10000 FARP_FRONTLINE_DISTANCE = 10000
@ -141,8 +141,9 @@ class GroundObjectsGenerator:
# Find carrier direction (In the wind) # Find carrier direction (In the wind)
found_carrier_destination = False found_carrier_destination = False
attempt = 0 attempt = 0
brc = self.m.weather.wind_at_ground.direction + 180
while not found_carrier_destination and attempt < 5: while not found_carrier_destination and attempt < 5:
point = sg.points[0].position.point_from_heading(self.m.weather.wind_at_ground.direction + 180, 100000-attempt*20000) point = sg.points[0].position.point_from_heading(brc, 100000-attempt*20000)
if self.game.theater.is_in_sea(point): if self.game.theater.is_in_sea(point):
found_carrier_destination = True found_carrier_destination = True
sg.add_waypoint(point) sg.add_waypoint(point)
@ -196,6 +197,7 @@ class GroundObjectsGenerator:
# unit name since it's an arbitrary ID. # unit name since it's an arbitrary ID.
self.runways[cp.name] = RunwayData( self.runways[cp.name] = RunwayData(
cp.name, cp.name,
brc,
"N/A", "N/A",
atc=atc_channel, atc=atc_channel,
tacan=tacan, tacan=tacan,

View File

@ -22,14 +22,13 @@ https://forums.eagle.ru/showthread.php?t=206360 claims that kneeboard pages can
only be added per airframe, so PvP missions where each side have the same 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. aircraft will be able to see the enemy's kneeboard for the same airframe.
""" """
import datetime
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass
import datetime
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional, Tuple from typing import Dict, List, Optional, Tuple
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
from dcs.mapping import Point
from dcs.mission import Mission from dcs.mission import Mission
from dcs.unittype import FlyingType from dcs.unittype import FlyingType
from tabulate import tabulate from tabulate import tabulate
@ -37,12 +36,11 @@ from tabulate import tabulate
from game.utils import meter_to_nm from game.utils import meter_to_nm
from . import units from . import units
from .aircraft import AIRCRAFT_DATA, FlightData from .aircraft import AIRCRAFT_DATA, FlightData
from .airfields import RunwayData
from .airsupportgen import AwacsInfo, TankerInfo from .airsupportgen import AwacsInfo, TankerInfo
from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator
from .flights.flight import FlightWaypoint, FlightWaypointType from .flights.flight import FlightWaypoint, FlightWaypointType
from .flights.traveltime import TravelTime
from .radios import RadioFrequency from .radios import RadioFrequency
from .runways import RunwayData
class KneeboardPageWriter: class KneeboardPageWriter:

139
gen/runways.py Normal file
View File

@ -0,0 +1,139 @@
"""Runway information and selection."""
from __future__ import annotations
import logging
from dataclasses import dataclass
from typing import Iterator, Optional
from dcs.terrain.terrain import Airport
from game.weather import Conditions
from theater import ControlPoint, ControlPointType
from .airfields import AIRFIELD_DATA
from .radios import RadioFrequency
from .tacan import TacanChannel
@dataclass(frozen=True)
class RunwayData:
airfield_name: str
runway_heading: int
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_airfield(cls, airport: Airport, runway_heading: int,
runway_name: str) -> RunwayData:
"""Creates RunwayData for the given runway of an airfield.
Args:
airport: The airfield the runway belongs to.
runway_heading: Heading of the runway.
runway_name: Identifier of the runway to use. e.g. "03" or "20L".
"""
atc: Optional[RadioFrequency] = None
tacan: Optional[TacanChannel] = None
tacan_callsign: Optional[str] = None
ils: Optional[RadioFrequency] = None
try:
airfield = AIRFIELD_DATA[airport.name]
if airfield.atc is not None:
atc = airfield.atc.uhf
else:
atc = None
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}")
return cls(
airfield_name=airport.name,
runway_heading=runway_heading,
runway_name=runway_name,
atc=atc,
tacan=tacan,
tacan_callsign=tacan_callsign,
ils=ils
)
@classmethod
def for_pydcs_airport(cls, airport: Airport) -> Iterator[RunwayData]:
for runway in airport.runways:
runway_number = runway.heading // 10
runway_side = ["", "L", "R"][runway.leftright]
runway_name = f"{runway_number:02}{runway_side}"
yield cls.for_airfield(airport, runway.heading, runway_name)
# pydcs only exposes one runway per physical runway, so to expose
# both sides of the runway we need to generate the other.
heading = (runway.heading + 180) % 360
runway_number = heading // 10
runway_side = ["", "R", "L"][runway.leftright]
runway_name = f"{runway_number:02}{runway_side}"
yield cls.for_airfield(airport, heading, runway_name)
class RunwayAssigner:
def __init__(self, conditions: Conditions):
self.conditions = conditions
def angle_off_headwind(self, runway: RunwayData) -> int:
wind = self.conditions.weather.wind.at_0m.direction
ideal_heading = (wind + 180) % 360
return abs(runway.runway_heading - ideal_heading)
def get_preferred_runway(self, 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(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]
def takeoff_heading(self, departure: ControlPoint) -> int:
if departure.cptype == ControlPointType.AIRBASE:
return self.get_preferred_runway(departure.airport).runway_heading
elif departure.is_fleet:
# The carrier will be angled into the wind automatically.
return (self.conditions.weather.wind.at_0m.direction + 180) % 360
logging.warning(
f"Unhandled departure control point: {departure.cptype}")
return 0
def landing_heading(self, arrival: ControlPoint) -> int:
if arrival.cptype == ControlPointType.AIRBASE:
return self.get_preferred_runway(arrival.airport).runway_heading
elif arrival.is_fleet:
# The carrier will be angled into the wind automatically.
return (self.conditions.weather.wind.at_0m.direction + 180) % 360
logging.warning(
f"Unhandled departure control point: {arrival.cptype}")
return 0