mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Pick runways and ascent/descent based on headwind.
This commit is contained in:
parent
aa7ffdabb0
commit
dd4c37cde3
@ -64,7 +64,6 @@ from game import db
|
||||
from game.data.cap_capabilities_db import GUNFIGHTERS
|
||||
from game.settings import Settings
|
||||
from game.utils import nm_to_meter
|
||||
from gen.airfields import RunwayData
|
||||
from gen.airsupportgen import AirSupport
|
||||
from gen.ato import AirTaskingOrder, Package
|
||||
from gen.callsigns import create_group_callsign_from_unit
|
||||
@ -75,11 +74,13 @@ from gen.flights.flight import (
|
||||
FlightWaypointType,
|
||||
)
|
||||
from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
|
||||
from gen.runways import RunwayData
|
||||
from theater import TheaterGroundObject
|
||||
from theater.controlpoint import ControlPoint, ControlPointType
|
||||
from .conflictgen import Conflict
|
||||
from .flights.traveltime import PackageWaypointTiming, TotEstimator
|
||||
from .naming import namegen
|
||||
from .runways import RunwayAssigner
|
||||
|
||||
WARM_START_HELI_AIRSPEED = 120
|
||||
WARM_START_HELI_ALT = 500
|
||||
@ -621,9 +622,12 @@ class AircraftConflictGenerator:
|
||||
|
||||
# TODO: Support for different departure/arrival airfields.
|
||||
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:
|
||||
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:
|
||||
departure_runway = dynamic_runways.get(cp.name, fallback_runway)
|
||||
else:
|
||||
@ -655,22 +659,6 @@ class AircraftConflictGenerator:
|
||||
for unit in group.units:
|
||||
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,
|
||||
unit_type: FlyingType, count: int, start_type: str,
|
||||
airport: Optional[Airport] = None) -> FlyingGroup:
|
||||
|
||||
@ -3,11 +3,11 @@
|
||||
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 dataclasses import dataclass, field
|
||||
import logging
|
||||
from typing import Dict, Iterator, Optional, Tuple
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
from dcs.terrain.terrain import Airport
|
||||
from .radios import MHz, RadioFrequency
|
||||
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)),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
@ -1,19 +1,20 @@
|
||||
import datetime
|
||||
import os
|
||||
import random
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
import random
|
||||
from typing import List
|
||||
|
||||
from game import db
|
||||
from dcs.mission import Mission
|
||||
|
||||
from game import db
|
||||
from .aircraft import FlightData
|
||||
from .airfields import RunwayData
|
||||
from .airsupportgen import AwacsInfo, TankerInfo
|
||||
from .armor import JtacInfo
|
||||
from .conflictgen import Conflict
|
||||
from .ground_forces.combat_stance import CombatStance
|
||||
from .radios import RadioFrequency
|
||||
from .runways import RunwayData
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@ -132,7 +132,7 @@ class FlightPlanBuilder:
|
||||
if not isinstance(location, TheaterGroundObject):
|
||||
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.hold(self._hold_point(flight))
|
||||
builder.join(self.package.waypoints.join)
|
||||
@ -222,7 +222,7 @@ class FlightPlanBuilder:
|
||||
)
|
||||
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.race_track(start, end, patrol_alt)
|
||||
builder.rtb(flight.from_cp)
|
||||
@ -264,7 +264,7 @@ class FlightPlanBuilder:
|
||||
orbit1p = orbit_center.point_from_heading(heading + 180, radius)
|
||||
|
||||
# Create points
|
||||
builder = WaypointBuilder(flight, self.doctrine)
|
||||
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
||||
builder.ascent(flight.from_cp)
|
||||
builder.hold(self._hold_point(flight))
|
||||
builder.join(self.package.waypoints.join)
|
||||
@ -290,7 +290,7 @@ class FlightPlanBuilder:
|
||||
if custom_targets is None:
|
||||
custom_targets = []
|
||||
|
||||
builder = WaypointBuilder(flight, self.doctrine)
|
||||
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
||||
builder.ascent(flight.from_cp)
|
||||
builder.hold(self._hold_point(flight))
|
||||
builder.join(self.package.waypoints.join)
|
||||
@ -328,7 +328,7 @@ class FlightPlanBuilder:
|
||||
def generate_escort(self, flight: Flight) -> 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.hold(self._hold_point(flight))
|
||||
builder.join(self.package.waypoints.join)
|
||||
@ -361,7 +361,7 @@ class FlightPlanBuilder:
|
||||
center = ingress.point_from_heading(heading, distance / 2)
|
||||
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.hold(self._hold_point(flight))
|
||||
builder.join(self.package.waypoints.join)
|
||||
@ -382,7 +382,7 @@ class FlightPlanBuilder:
|
||||
flight: The flight to generate the descend point for.
|
||||
departure: Departure airfield or carrier.
|
||||
"""
|
||||
builder = WaypointBuilder(flight, self.doctrine)
|
||||
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
||||
builder.ascent(departure)
|
||||
return builder.build()[0]
|
||||
|
||||
@ -394,7 +394,7 @@ class FlightPlanBuilder:
|
||||
flight: The flight to generate the descend point for.
|
||||
arrival: Arrival airfield or carrier.
|
||||
"""
|
||||
builder = WaypointBuilder(flight, self.doctrine)
|
||||
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
||||
builder.descent(arrival)
|
||||
return builder.build()[0]
|
||||
|
||||
@ -406,7 +406,7 @@ class FlightPlanBuilder:
|
||||
flight: The flight to generate the landing waypoint for.
|
||||
arrival: Arrival airfield or carrier.
|
||||
"""
|
||||
builder = WaypointBuilder(flight, self.doctrine)
|
||||
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
||||
builder.land(arrival)
|
||||
return builder.build()[0]
|
||||
|
||||
|
||||
@ -7,12 +7,16 @@ from dcs.unit import Unit
|
||||
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.utils import nm_to_meter
|
||||
from game.weather import Conditions
|
||||
from theater import ControlPoint, MissionTarget, TheaterGroundObject
|
||||
from .flight import Flight, FlightWaypoint, FlightWaypointType
|
||||
from ..runways import RunwayAssigner
|
||||
|
||||
|
||||
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.doctrine = doctrine
|
||||
self.waypoints: List[FlightWaypoint] = []
|
||||
@ -28,8 +32,7 @@ class WaypointBuilder:
|
||||
departure: Departure airfield or carrier.
|
||||
is_helo: True if the flight is a helicopter.
|
||||
"""
|
||||
# TODO: Pick runway based on wind direction.
|
||||
heading = departure.heading
|
||||
heading = RunwayAssigner(self.conditions).takeoff_heading(departure)
|
||||
position = departure.position.point_from_heading(
|
||||
heading, nm_to_meter(5)
|
||||
)
|
||||
@ -52,9 +55,8 @@ class WaypointBuilder:
|
||||
arrival: Arrival airfield or carrier.
|
||||
is_helo: True if the flight is a helicopter.
|
||||
"""
|
||||
# TODO: Pick runway based on wind direction.
|
||||
# ControlPoint.heading is the departure heading.
|
||||
heading = (arrival.heading + 180) % 360
|
||||
landing_heading = RunwayAssigner(self.conditions).landing_heading(arrival)
|
||||
heading = (landing_heading + 180) % 360
|
||||
position = arrival.position.point_from_heading(
|
||||
heading, nm_to_meter(5)
|
||||
)
|
||||
|
||||
@ -16,9 +16,9 @@ from dcs.unitgroup import StaticGroup
|
||||
from game import db
|
||||
from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID
|
||||
from game.db import unit_type_from_name
|
||||
from .airfields import RunwayData
|
||||
from .conflictgen import Conflict
|
||||
from .radios import RadioRegistry
|
||||
from .runways import RunwayData
|
||||
from .tacan import TacanBand, TacanRegistry
|
||||
|
||||
FARP_FRONTLINE_DISTANCE = 10000
|
||||
@ -141,8 +141,9 @@ class GroundObjectsGenerator:
|
||||
# Find carrier direction (In the wind)
|
||||
found_carrier_destination = False
|
||||
attempt = 0
|
||||
brc = self.m.weather.wind_at_ground.direction + 180
|
||||
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):
|
||||
found_carrier_destination = True
|
||||
sg.add_waypoint(point)
|
||||
@ -196,6 +197,7 @@ class GroundObjectsGenerator:
|
||||
# unit name since it's an arbitrary ID.
|
||||
self.runways[cp.name] = RunwayData(
|
||||
cp.name,
|
||||
brc,
|
||||
"N/A",
|
||||
atc=atc_channel,
|
||||
tacan=tacan,
|
||||
|
||||
@ -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
|
||||
aircraft will be able to see the enemy's kneeboard for the same airframe.
|
||||
"""
|
||||
import datetime
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from dcs.mapping import Point
|
||||
from dcs.mission import Mission
|
||||
from dcs.unittype import FlyingType
|
||||
from tabulate import tabulate
|
||||
@ -37,12 +36,11 @@ from tabulate import tabulate
|
||||
from game.utils import meter_to_nm
|
||||
from . import units
|
||||
from .aircraft import AIRCRAFT_DATA, FlightData
|
||||
from .airfields import RunwayData
|
||||
from .airsupportgen import AwacsInfo, TankerInfo
|
||||
from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator
|
||||
from .flights.flight import FlightWaypoint, FlightWaypointType
|
||||
from .flights.traveltime import TravelTime
|
||||
from .radios import RadioFrequency
|
||||
from .runways import RunwayData
|
||||
|
||||
|
||||
class KneeboardPageWriter:
|
||||
|
||||
139
gen/runways.py
Normal file
139
gen/runways.py
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user