mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Merge branch 'develop' into faction_refactor
# Conflicts: # game/factions/bluefor_coldwar.py # game/factions/bluefor_coldwar_a4.py # game/factions/bluefor_coldwar_mods.py # game/factions/bluefor_modern.py
This commit is contained in:
108
gen/aircraft.py
108
gen/aircraft.py
@@ -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
|
||||
@@ -204,6 +205,9 @@ class ChannelAssignment:
|
||||
class FlightData:
|
||||
"""Details of a planned flight."""
|
||||
|
||||
#: The package that the flight belongs to.
|
||||
package: Package
|
||||
|
||||
flight_type: FlightType
|
||||
|
||||
#: All units in the flight.
|
||||
@@ -236,14 +240,13 @@ class FlightData:
|
||||
#: Map of radio frequencies to their assigned radio and channel, if any.
|
||||
frequency_to_channel_map: Dict[RadioFrequency, ChannelAssignment]
|
||||
|
||||
#: Data concerning the target of a CAS/Strike/SEAD flight, or None else
|
||||
targetPoint = None
|
||||
|
||||
def __init__(self, flight_type: FlightType, units: List[FlyingUnit],
|
||||
size: int, friendly: bool, departure_delay: int,
|
||||
departure: RunwayData, arrival: RunwayData,
|
||||
divert: Optional[RunwayData], waypoints: List[FlightWaypoint],
|
||||
intra_flight_channel: RadioFrequency, targetPoint: Optional) -> None:
|
||||
def __init__(self, package: Package, flight_type: FlightType,
|
||||
units: List[FlyingUnit], size: int, friendly: bool,
|
||||
departure_delay: int, departure: RunwayData,
|
||||
arrival: RunwayData, divert: Optional[RunwayData],
|
||||
waypoints: List[FlightWaypoint],
|
||||
intra_flight_channel: RadioFrequency) -> None:
|
||||
self.package = package
|
||||
self.flight_type = flight_type
|
||||
self.units = units
|
||||
self.size = size
|
||||
@@ -256,7 +259,6 @@ class FlightData:
|
||||
self.intra_flight_channel = intra_flight_channel
|
||||
self.frequency_to_channel_map = {}
|
||||
self.callsign = create_group_callsign_from_unit(self.units[0])
|
||||
self.targetPoint = targetPoint
|
||||
|
||||
@property
|
||||
def client_units(self) -> List[FlyingUnit]:
|
||||
@@ -574,12 +576,12 @@ class AircraftConflictGenerator:
|
||||
return StartType.Warm
|
||||
|
||||
def _setup_group(self, group: FlyingGroup, for_task: Type[Task],
|
||||
flight: Flight, dynamic_runways: Dict[str, RunwayData]):
|
||||
package: Package, flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> None:
|
||||
did_load_loadout = False
|
||||
unit_type = group.units[0].unit_type
|
||||
|
||||
if unit_type in db.PLANE_PAYLOAD_OVERRIDES:
|
||||
override_loadout = db.PLANE_PAYLOAD_OVERRIDES[unit_type]
|
||||
# Clear pylons
|
||||
for p in group.units:
|
||||
p.pylons.clear()
|
||||
@@ -622,9 +624,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:
|
||||
@@ -632,6 +637,7 @@ class AircraftConflictGenerator:
|
||||
departure_runway = fallback_runway
|
||||
|
||||
self.flights.append(FlightData(
|
||||
package=package,
|
||||
flight_type=flight.flight_type,
|
||||
units=group.units,
|
||||
size=len(group.units),
|
||||
@@ -643,8 +649,7 @@ class AircraftConflictGenerator:
|
||||
divert=None,
|
||||
# Waypoints are added later, after they've had their TOTs set.
|
||||
waypoints=[],
|
||||
intra_flight_channel=channel,
|
||||
targetPoint=flight.targetPoint,
|
||||
intra_flight_channel=channel
|
||||
))
|
||||
|
||||
# Special case so Su 33 carrier take off
|
||||
@@ -656,22 +661,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:
|
||||
@@ -802,7 +791,7 @@ class AircraftConflictGenerator:
|
||||
logging.info(f"Generating flight: {flight.unit_type}")
|
||||
group = self.generate_planned_flight(flight.from_cp, country,
|
||||
flight)
|
||||
self.setup_flight_group(group, flight, dynamic_runways)
|
||||
self.setup_flight_group(group, package, flight, dynamic_runways)
|
||||
self.create_waypoints(group, package, flight, timing)
|
||||
|
||||
def set_activation_time(self, flight: Flight, group: FlyingGroup,
|
||||
@@ -919,10 +908,11 @@ class AircraftConflictGenerator:
|
||||
if flight.unit_type.eplrs:
|
||||
group.points[0].tasks.append(EPLRS(group.id))
|
||||
|
||||
def configure_cap(self, group: FlyingGroup, flight: Flight,
|
||||
def configure_cap(self, group: FlyingGroup, package: Package,
|
||||
flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> None:
|
||||
group.task = CAP.name
|
||||
self._setup_group(group, CAP, flight, dynamic_runways)
|
||||
self._setup_group(group, CAP, package, flight, dynamic_runways)
|
||||
|
||||
if flight.unit_type not in GUNFIGHTERS:
|
||||
ammo_type = OptRTBOnOutOfAmmo.Values.AAM
|
||||
@@ -934,10 +924,11 @@ class AircraftConflictGenerator:
|
||||
group.points[0].tasks.append(EngageTargets(max_distance=nm_to_meter(50),
|
||||
targets=[Targets.All.Air]))
|
||||
|
||||
def configure_cas(self, group: FlyingGroup, flight: Flight,
|
||||
def configure_cas(self, group: FlyingGroup, package: Package,
|
||||
flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> None:
|
||||
group.task = CAS.name
|
||||
self._setup_group(group, CAS, flight, dynamic_runways)
|
||||
self._setup_group(group, CAS, package, flight, dynamic_runways)
|
||||
self.configure_behavior(
|
||||
group,
|
||||
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||
@@ -949,10 +940,11 @@ class AircraftConflictGenerator:
|
||||
targets=[Targets.All.GroundUnits.GroundVehicles])
|
||||
)
|
||||
|
||||
def configure_sead(self, group: FlyingGroup, flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> None:
|
||||
def configure_sead(self, group: FlyingGroup, package: Package,
|
||||
flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> None:
|
||||
group.task = SEAD.name
|
||||
self._setup_group(group, SEAD, flight, dynamic_runways)
|
||||
self._setup_group(group, SEAD, package, flight, dynamic_runways)
|
||||
self.configure_behavior(
|
||||
group,
|
||||
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||
@@ -960,33 +952,37 @@ class AircraftConflictGenerator:
|
||||
rtb_winchester=OptRTBOnOutOfAmmo.Values.ASM,
|
||||
restrict_jettison=True)
|
||||
|
||||
def configure_strike(self, group: FlyingGroup, flight: Flight,
|
||||
def configure_strike(self, group: FlyingGroup, package: Package,
|
||||
flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> None:
|
||||
group.task = PinpointStrike.name
|
||||
self._setup_group(group, GroundAttack, flight, dynamic_runways)
|
||||
self._setup_group(group, GroundAttack, package, flight, dynamic_runways)
|
||||
self.configure_behavior(
|
||||
group,
|
||||
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||
roe=OptROE.Values.OpenFire,
|
||||
restrict_jettison=True)
|
||||
|
||||
def configure_anti_ship(self, group: FlyingGroup, flight: Flight,
|
||||
def configure_anti_ship(self, group: FlyingGroup, package: Package,
|
||||
flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> None:
|
||||
group.task = AntishipStrike.name
|
||||
self._setup_group(group, AntishipStrike, flight, dynamic_runways)
|
||||
self._setup_group(group, AntishipStrike, package, flight,
|
||||
dynamic_runways)
|
||||
self.configure_behavior(
|
||||
group,
|
||||
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||
roe=OptROE.Values.OpenFire,
|
||||
restrict_jettison=True)
|
||||
|
||||
def configure_escort(self, group: FlyingGroup, flight: Flight,
|
||||
def configure_escort(self, group: FlyingGroup, package: Package,
|
||||
flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> None:
|
||||
# Escort groups are actually given the CAP task so they can perform the
|
||||
# Search Then Engage task, which we have to use instead of the Escort
|
||||
# task for the reasons explained in JoinPointBuilder.
|
||||
group.task = CAP.name
|
||||
self._setup_group(group, CAP, flight, dynamic_runways)
|
||||
self._setup_group(group, CAP, package, flight, dynamic_runways)
|
||||
self.configure_behavior(group, roe=OptROE.Values.OpenFire,
|
||||
restrict_jettison=True)
|
||||
|
||||
@@ -995,22 +991,23 @@ class AircraftConflictGenerator:
|
||||
logging.error(f"Unhandled flight type: {flight.flight_type.name}")
|
||||
self.configure_behavior(group)
|
||||
|
||||
def setup_flight_group(self, group: FlyingGroup, flight: Flight,
|
||||
def setup_flight_group(self, group: FlyingGroup, package: Package,
|
||||
flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> None:
|
||||
flight_type = flight.flight_type
|
||||
if flight_type in [FlightType.BARCAP, FlightType.TARCAP,
|
||||
FlightType.INTERCEPTION]:
|
||||
self.configure_cap(group, flight, dynamic_runways)
|
||||
self.configure_cap(group, package, flight, dynamic_runways)
|
||||
elif flight_type in [FlightType.CAS, FlightType.BAI]:
|
||||
self.configure_cas(group, flight, dynamic_runways)
|
||||
self.configure_cas(group, package, flight, dynamic_runways)
|
||||
elif flight_type in [FlightType.SEAD, FlightType.DEAD]:
|
||||
self.configure_sead(group, flight, dynamic_runways)
|
||||
self.configure_sead(group, package, flight, dynamic_runways)
|
||||
elif flight_type in [FlightType.STRIKE]:
|
||||
self.configure_strike(group, flight, dynamic_runways)
|
||||
self.configure_strike(group, package, flight, dynamic_runways)
|
||||
elif flight_type in [FlightType.ANTISHIP]:
|
||||
self.configure_anti_ship(group, flight, dynamic_runways)
|
||||
self.configure_anti_ship(group, package, flight, dynamic_runways)
|
||||
elif flight_type == FlightType.ESCORT:
|
||||
self.configure_escort(group, flight, dynamic_runways)
|
||||
self.configure_escort(group, package, flight, dynamic_runways)
|
||||
else:
|
||||
self.configure_unknown_task(group, flight)
|
||||
|
||||
@@ -1301,8 +1298,9 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
|
||||
pattern=OrbitAction.OrbitPattern.RaceTrack
|
||||
))
|
||||
|
||||
self.set_waypoint_tot(waypoint, self.timing.race_track_start)
|
||||
racetrack.stop_after_time(self.timing.race_track_end)
|
||||
self.set_waypoint_tot(waypoint,
|
||||
self.timing.race_track_start(self.flight))
|
||||
racetrack.stop_after_time(self.timing.race_track_end(self.flight))
|
||||
waypoint.add_task(racetrack)
|
||||
return waypoint
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -206,10 +206,9 @@ class PackageBuilder:
|
||||
if assignment is None:
|
||||
return False
|
||||
airfield, aircraft = assignment
|
||||
flight = Flight(aircraft, plan.num_aircraft, airfield, plan.task,
|
||||
self.start_type)
|
||||
flight = Flight(self.package, aircraft, plan.num_aircraft, airfield,
|
||||
plan.task, self.start_type)
|
||||
self.package.add_flight(flight)
|
||||
flight.targetPoint = self.package.target
|
||||
return True
|
||||
|
||||
def build(self) -> Package:
|
||||
@@ -222,7 +221,7 @@ class PackageBuilder:
|
||||
for flight in flights:
|
||||
self.global_inventory.return_from_flight(flight)
|
||||
self.package.remove_flight(flight)
|
||||
flight.targetPoint = None
|
||||
|
||||
|
||||
class ObjectiveFinder:
|
||||
"""Identifies potential objectives for the mission planner."""
|
||||
|
||||
@@ -19,11 +19,14 @@ from dcs.planes import (
|
||||
A_10C_2,
|
||||
A_20G,
|
||||
B_17G,
|
||||
B_1B,
|
||||
B_52H,
|
||||
Bf_109K_4,
|
||||
C_101CC,
|
||||
FA_18C_hornet,
|
||||
FW_190A8,
|
||||
FW_190D9,
|
||||
F_117A,
|
||||
F_14B,
|
||||
F_15C,
|
||||
F_15E,
|
||||
@@ -71,6 +74,9 @@ from dcs.planes import (
|
||||
Su_34,
|
||||
Tornado_GR4,
|
||||
Tornado_IDS,
|
||||
Tu_160,
|
||||
Tu_22M3,
|
||||
Tu_95MS,
|
||||
WingLoong_I,
|
||||
)
|
||||
|
||||
@@ -226,6 +232,8 @@ CAS_CAPABLE = [
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
|
||||
B_1B,
|
||||
|
||||
Tornado_IDS,
|
||||
Tornado_GR4,
|
||||
|
||||
@@ -367,6 +375,10 @@ STRIKE_CAPABLE = [
|
||||
Su_25T,
|
||||
Su_34,
|
||||
|
||||
Tu_160,
|
||||
Tu_22M3,
|
||||
Tu_95MS,
|
||||
|
||||
JF_17,
|
||||
|
||||
M_2000C,
|
||||
@@ -384,6 +396,10 @@ STRIKE_CAPABLE = [
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
|
||||
B_1B,
|
||||
B_52H,
|
||||
F_117A,
|
||||
|
||||
Tornado_IDS,
|
||||
Tornado_GR4,
|
||||
|
||||
@@ -413,11 +429,16 @@ STRIKE_CAPABLE = [
|
||||
|
||||
STRIKE_PREFERRED = [
|
||||
AJS37,
|
||||
F_15E,
|
||||
Tornado_GR4,
|
||||
|
||||
A_20G,
|
||||
B_17G,
|
||||
B_1B,
|
||||
B_52H,
|
||||
F_117A,
|
||||
F_15E,
|
||||
Tornado_GR4,
|
||||
Tu_160,
|
||||
Tu_22M3,
|
||||
Tu_95MS,
|
||||
]
|
||||
|
||||
ANTISHIP_CAPABLE = [
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from typing import Dict, Iterable, List, Optional
|
||||
from typing import Dict, Iterable, List, Optional, TYPE_CHECKING
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.point import MovingPoint, PointAction
|
||||
@@ -8,6 +10,9 @@ from dcs.unittype import UnitType
|
||||
from game import db
|
||||
from theater.controlpoint import ControlPoint, MissionTarget
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gen.ato import Package
|
||||
|
||||
|
||||
class FlightType(Enum):
|
||||
CAP = 0 # Do not use. Use BARCAP or TARCAP.
|
||||
@@ -138,10 +143,11 @@ class Flight:
|
||||
use_custom_loadout = False
|
||||
preset_loadout_name = ""
|
||||
group = False # Contains DCS Mission group data after mission has been generated
|
||||
targetPoint = None # Contains either None or a Strike/SEAD target point location
|
||||
|
||||
def __init__(self, unit_type: UnitType, count: int, from_cp: ControlPoint,
|
||||
flight_type: FlightType, start_type: str) -> None:
|
||||
def __init__(self, package: Package, unit_type: UnitType, count: int,
|
||||
from_cp: ControlPoint, flight_type: FlightType,
|
||||
start_type: str) -> None:
|
||||
self.package = package
|
||||
self.unit_type = unit_type
|
||||
self.count = count
|
||||
self.from_cp = from_cp
|
||||
|
||||
@@ -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)
|
||||
@@ -157,11 +157,7 @@ class FlightPlanBuilder:
|
||||
if building.is_dead:
|
||||
continue
|
||||
|
||||
builder.strike_point(
|
||||
building,
|
||||
f"{building.obj_name} {building.category}",
|
||||
location
|
||||
)
|
||||
builder.strike_point(building, building.category, location)
|
||||
|
||||
builder.egress(self.package.waypoints.egress, location)
|
||||
builder.split(self.package.waypoints.split)
|
||||
@@ -222,7 +218,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 +260,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 +286,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 +324,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)
|
||||
@@ -351,9 +347,6 @@ class FlightPlanBuilder:
|
||||
if not isinstance(location, FrontLine):
|
||||
raise InvalidObjectiveLocation(flight.flight_type, location)
|
||||
|
||||
is_helo = getattr(flight.unit_type, "helicopter", False)
|
||||
cap_alt = 500 if is_helo else 1000
|
||||
|
||||
ingress, heading, distance = Conflict.frontline_vector(
|
||||
location.control_points[0], location.control_points[1],
|
||||
self.game.theater
|
||||
@@ -361,15 +354,15 @@ class FlightPlanBuilder:
|
||||
center = ingress.point_from_heading(heading, distance / 2)
|
||||
egress = ingress.point_from_heading(heading, distance)
|
||||
|
||||
builder = WaypointBuilder(flight, self.doctrine)
|
||||
builder.ascent(flight.from_cp, is_helo)
|
||||
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)
|
||||
builder.ingress_cas(ingress, location)
|
||||
builder.cas(center, cap_alt)
|
||||
builder.cas(center)
|
||||
builder.egress(egress, location)
|
||||
builder.split(self.package.waypoints.split)
|
||||
builder.rtb(flight.from_cp, is_helo)
|
||||
builder.rtb(flight.from_cp)
|
||||
|
||||
flight.points = builder.build()
|
||||
|
||||
@@ -382,7 +375,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 +387,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 +399,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]
|
||||
|
||||
|
||||
@@ -27,21 +27,22 @@ INGRESS_TYPES = {
|
||||
FlightWaypointType.INGRESS_STRIKE,
|
||||
}
|
||||
|
||||
IP_TYPES = {
|
||||
FlightWaypointType.INGRESS_CAS,
|
||||
FlightWaypointType.INGRESS_ESCORT,
|
||||
FlightWaypointType.INGRESS_SEAD,
|
||||
FlightWaypointType.INGRESS_STRIKE,
|
||||
FlightWaypointType.PATROL_TRACK,
|
||||
}
|
||||
|
||||
|
||||
class GroundSpeed:
|
||||
@staticmethod
|
||||
def mission_speed(package: Package) -> int:
|
||||
speeds = set()
|
||||
for flight in package.flights:
|
||||
waypoint = flight.waypoint_with_type(IP_TYPES)
|
||||
# Find a waypoint that matches the mission start waypoint and use
|
||||
# that for the altitude of the mission. That may not be true for the
|
||||
# whole mission, but it's probably good enough for now.
|
||||
waypoint = flight.waypoint_with_type({
|
||||
FlightWaypointType.INGRESS_CAS,
|
||||
FlightWaypointType.INGRESS_ESCORT,
|
||||
FlightWaypointType.INGRESS_SEAD,
|
||||
FlightWaypointType.INGRESS_STRIKE,
|
||||
FlightWaypointType.PATROL_TRACK,
|
||||
})
|
||||
if waypoint is None:
|
||||
logging.error(f"Could not find ingress point for {flight}.")
|
||||
if flight.points:
|
||||
@@ -152,8 +153,10 @@ class TotEstimator:
|
||||
# Takeoff immediately.
|
||||
return 0
|
||||
|
||||
if self.package.primary_task == FlightType.BARCAP:
|
||||
start_time = self.timing.race_track_start
|
||||
# BARCAP flights do not coordinate with the rest of the package on join
|
||||
# or ingress points.
|
||||
if flight.flight_type == FlightType.BARCAP:
|
||||
start_time = self.timing.race_track_start(flight)
|
||||
else:
|
||||
start_time = self.timing.join
|
||||
return start_time - travel_time - self.HOLD_TIME
|
||||
@@ -166,7 +169,9 @@ class TotEstimator:
|
||||
def earliest_tot_for_flight(self, flight: Flight) -> int:
|
||||
"""Estimate fastest time from mission start to the target position.
|
||||
|
||||
For CAP missions, this is time to race track start.
|
||||
For BARCAP flights, this is time to race track start. This ensures that
|
||||
they are on station at the same time any other package members reach
|
||||
their ingress point.
|
||||
|
||||
For other mission types this is the time to the mission target.
|
||||
|
||||
@@ -177,27 +182,34 @@ class TotEstimator:
|
||||
The earliest possible TOT for the given flight in seconds. Returns 0
|
||||
if an ingress point cannot be found.
|
||||
"""
|
||||
time_to_ingress = self.estimate_waypoints_to_target(flight, IP_TYPES)
|
||||
if time_to_ingress is None:
|
||||
logging.warning(
|
||||
f"Found no ingress types. Cannot estimate TOT for {flight}")
|
||||
# Return 0 so this flight's travel time does not affect the rest of
|
||||
# the package.
|
||||
return 0
|
||||
|
||||
if self.package.primary_task == FlightType.BARCAP:
|
||||
# The racetrack start *is* the target. The package target is the
|
||||
# protected objective.
|
||||
time_to_target = 0
|
||||
if flight.flight_type == FlightType.BARCAP:
|
||||
time_to_target = self.estimate_waypoints_to_target(flight, {
|
||||
FlightWaypointType.PATROL_TRACK
|
||||
})
|
||||
if time_to_target is None:
|
||||
logging.warning(
|
||||
f"Found no race track. Cannot estimate TOT for {flight}")
|
||||
# Return 0 so this flight's travel time does not affect the rest
|
||||
# of the package.
|
||||
return 0
|
||||
else:
|
||||
time_to_ingress = self.estimate_waypoints_to_target(
|
||||
flight, INGRESS_TYPES
|
||||
)
|
||||
if time_to_ingress is None:
|
||||
logging.warning(
|
||||
f"Found no ingress types. Cannot estimate TOT for {flight}")
|
||||
# Return 0 so this flight's travel time does not affect the rest
|
||||
# of the package.
|
||||
return 0
|
||||
|
||||
assert self.package.waypoints is not None
|
||||
time_to_target = TravelTime.between_points(
|
||||
time_to_target = time_to_ingress + TravelTime.between_points(
|
||||
self.package.waypoints.ingress, self.package.target.position,
|
||||
GroundSpeed.mission_speed(self.package))
|
||||
return sum([
|
||||
self.estimate_startup(flight),
|
||||
self.estimate_ground_ops(flight),
|
||||
time_to_ingress,
|
||||
time_to_target,
|
||||
])
|
||||
|
||||
@@ -281,18 +293,22 @@ class PackageWaypointTiming:
|
||||
assert self.package.time_over_target is not None
|
||||
return self.package.time_over_target
|
||||
|
||||
@property
|
||||
def race_track_start(self) -> int:
|
||||
if self.package.primary_task == FlightType.BARCAP:
|
||||
return self.package.time_over_target
|
||||
def race_track_start(self, flight: Flight) -> int:
|
||||
if flight.flight_type == FlightType.BARCAP:
|
||||
return self.target
|
||||
else:
|
||||
# The only other type that (currently) uses race tracks is TARCAP,
|
||||
# which is sort of in need of cleanup. TARCAP is only valid on front
|
||||
# lines and they participate in join points and patrol between the
|
||||
# ingress and egress points rather than on a race track actually
|
||||
# pointed at the enemy.
|
||||
return self.ingress
|
||||
|
||||
@property
|
||||
def race_track_end(self) -> int:
|
||||
if self.package.primary_task == FlightType.BARCAP:
|
||||
def race_track_end(self, flight: Flight) -> int:
|
||||
if flight.flight_type == FlightType.BARCAP:
|
||||
return self.target + CAP_DURATION * 60
|
||||
else:
|
||||
# For TARCAP. See the explanation in race_track_start.
|
||||
return self.egress
|
||||
|
||||
def push_time(self, flight: Flight, hold_point: FlightWaypoint) -> int:
|
||||
@@ -303,7 +319,8 @@ class PackageWaypointTiming:
|
||||
GroundSpeed.for_flight(flight, hold_point.alt)
|
||||
)
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[int]:
|
||||
def tot_for_waypoint(self, flight: Flight,
|
||||
waypoint: FlightWaypoint) -> Optional[int]:
|
||||
target_types = (
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
FlightWaypointType.TARGET_POINT,
|
||||
@@ -321,7 +338,7 @@ class PackageWaypointTiming:
|
||||
elif waypoint.waypoint_type == FlightWaypointType.SPLIT:
|
||||
return self.split
|
||||
elif waypoint.waypoint_type == FlightWaypointType.PATROL_TRACK:
|
||||
return self.race_track_start
|
||||
return self.race_track_start(flight)
|
||||
return None
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint,
|
||||
@@ -329,7 +346,7 @@ class PackageWaypointTiming:
|
||||
if waypoint.waypoint_type == FlightWaypointType.LOITER:
|
||||
return self.push_time(flight, waypoint)
|
||||
elif waypoint.waypoint_type == FlightWaypointType.PATROL:
|
||||
return self.race_track_end
|
||||
return self.race_track_end(flight)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -7,29 +7,35 @@ 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] = []
|
||||
self.ingress_point: Optional[FlightWaypoint] = None
|
||||
|
||||
@property
|
||||
def is_helo(self) -> bool:
|
||||
return getattr(self.flight.unit_type, "helicopter", False)
|
||||
|
||||
def build(self) -> List[FlightWaypoint]:
|
||||
return self.waypoints
|
||||
|
||||
def ascent(self, departure: ControlPoint, is_helo: bool = False) -> None:
|
||||
def ascent(self, departure: ControlPoint) -> None:
|
||||
"""Create ascent waypoint for the given departure airfield or carrier.
|
||||
|
||||
Args:
|
||||
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)
|
||||
)
|
||||
@@ -37,7 +43,7 @@ class WaypointBuilder:
|
||||
FlightWaypointType.ASCEND_POINT,
|
||||
position.x,
|
||||
position.y,
|
||||
500 if is_helo else self.doctrine.pattern_altitude
|
||||
500 if self.is_helo else self.doctrine.pattern_altitude
|
||||
)
|
||||
waypoint.name = "ASCEND"
|
||||
waypoint.alt_type = "RADIO"
|
||||
@@ -45,16 +51,15 @@ class WaypointBuilder:
|
||||
waypoint.pretty_name = "Ascend"
|
||||
self.waypoints.append(waypoint)
|
||||
|
||||
def descent(self, arrival: ControlPoint, is_helo: bool = False) -> None:
|
||||
def descent(self, arrival: ControlPoint) -> None:
|
||||
"""Create descent waypoint for the given arrival airfield or carrier.
|
||||
|
||||
Args:
|
||||
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)
|
||||
)
|
||||
@@ -62,7 +67,7 @@ class WaypointBuilder:
|
||||
FlightWaypointType.DESCENT_POINT,
|
||||
position.x,
|
||||
position.y,
|
||||
300 if is_helo else self.doctrine.pattern_altitude
|
||||
300 if self.is_helo else self.doctrine.pattern_altitude
|
||||
)
|
||||
waypoint.name = "DESCEND"
|
||||
waypoint.alt_type = "RADIO"
|
||||
@@ -94,7 +99,7 @@ class WaypointBuilder:
|
||||
FlightWaypointType.LOITER,
|
||||
position.x,
|
||||
position.y,
|
||||
self.doctrine.rendezvous_altitude
|
||||
500 if self.is_helo else self.doctrine.rendezvous_altitude
|
||||
)
|
||||
waypoint.pretty_name = "Hold"
|
||||
waypoint.description = "Wait until push time"
|
||||
@@ -106,7 +111,7 @@ class WaypointBuilder:
|
||||
FlightWaypointType.JOIN,
|
||||
position.x,
|
||||
position.y,
|
||||
self.doctrine.ingress_altitude
|
||||
500 if self.is_helo else self.doctrine.ingress_altitude
|
||||
)
|
||||
waypoint.pretty_name = "Join"
|
||||
waypoint.description = "Rendezvous with package"
|
||||
@@ -118,7 +123,7 @@ class WaypointBuilder:
|
||||
FlightWaypointType.SPLIT,
|
||||
position.x,
|
||||
position.y,
|
||||
self.doctrine.ingress_altitude
|
||||
500 if self.is_helo else self.doctrine.ingress_altitude
|
||||
)
|
||||
waypoint.pretty_name = "Split"
|
||||
waypoint.description = "Depart from package"
|
||||
@@ -146,7 +151,7 @@ class WaypointBuilder:
|
||||
ingress_type,
|
||||
position.x,
|
||||
position.y,
|
||||
self.doctrine.ingress_altitude
|
||||
500 if self.is_helo else self.doctrine.ingress_altitude
|
||||
)
|
||||
waypoint.pretty_name = "INGRESS on " + objective.name
|
||||
waypoint.description = "INGRESS on " + objective.name
|
||||
@@ -159,7 +164,7 @@ class WaypointBuilder:
|
||||
FlightWaypointType.EGRESS,
|
||||
position.x,
|
||||
position.y,
|
||||
self.doctrine.ingress_altitude
|
||||
500 if self.is_helo else self.doctrine.ingress_altitude
|
||||
)
|
||||
waypoint.pretty_name = "EGRESS from " + target.name
|
||||
waypoint.description = "EGRESS from " + target.name
|
||||
@@ -168,24 +173,21 @@ class WaypointBuilder:
|
||||
|
||||
def dead_point(self, target: Union[TheaterGroundObject, Unit], name: str,
|
||||
location: MissionTarget) -> None:
|
||||
self._target_point(target, name, f"STRIKE [{location.name}]: {name}",
|
||||
location)
|
||||
self._target_point(target, name, f"STRIKE {name}", location)
|
||||
# TODO: Seems fishy.
|
||||
if self.ingress_point is not None:
|
||||
self.ingress_point.targetGroup = location
|
||||
|
||||
def sead_point(self, target: Union[TheaterGroundObject, Unit], name: str,
|
||||
location: MissionTarget) -> None:
|
||||
self._target_point(target, name, f"STRIKE [{location.name}]: {name}",
|
||||
location)
|
||||
self._target_point(target, name, f"STRIKE {name}", location)
|
||||
# TODO: Seems fishy.
|
||||
if self.ingress_point is not None:
|
||||
self.ingress_point.targetGroup = location
|
||||
|
||||
def strike_point(self, target: Union[TheaterGroundObject, Unit], name: str,
|
||||
location: MissionTarget) -> None:
|
||||
self._target_point(target, name, f"STRIKE [{location.name}]: {name}",
|
||||
location)
|
||||
self._target_point(target, name, f"STRIKE {name}", location)
|
||||
|
||||
def _target_point(self, target: Union[TheaterGroundObject, Unit], name: str,
|
||||
description: str, location: MissionTarget) -> None:
|
||||
@@ -246,12 +248,12 @@ class WaypointBuilder:
|
||||
# TODO: This seems wrong, but it's what was there before.
|
||||
self.ingress_point.targets.append(location)
|
||||
|
||||
def cas(self, position: Point, altitude: int) -> None:
|
||||
def cas(self, position: Point) -> None:
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.CAS,
|
||||
position.x,
|
||||
position.y,
|
||||
altitude
|
||||
500 if self.is_helo else 1000
|
||||
)
|
||||
waypoint.alt_type = "RADIO"
|
||||
waypoint.description = "Provide CAS"
|
||||
@@ -306,14 +308,13 @@ class WaypointBuilder:
|
||||
self.race_track_start(start, altitude)
|
||||
self.race_track_end(end, altitude)
|
||||
|
||||
def rtb(self, arrival: ControlPoint, is_helo: bool = False) -> None:
|
||||
def rtb(self, arrival: ControlPoint) -> None:
|
||||
"""Creates descent ant landing waypoints for the given control point.
|
||||
|
||||
Args:
|
||||
arrival: Arrival airfield or carrier.
|
||||
is_helo: True if the flight is a helicopter.
|
||||
"""
|
||||
self.descent(arrival, is_helo)
|
||||
self.descent(arrival)
|
||||
self.land(arrival)
|
||||
|
||||
def escort(self, ingress: Point, target: MissionTarget,
|
||||
@@ -337,7 +338,7 @@ class WaypointBuilder:
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
target.position.x,
|
||||
target.position.y,
|
||||
self.doctrine.ingress_altitude
|
||||
500 if self.is_helo else self.doctrine.ingress_altitude
|
||||
)
|
||||
waypoint.name = "TARGET"
|
||||
waypoint.description = "Escort the package"
|
||||
|
||||
@@ -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:
|
||||
@@ -126,6 +124,7 @@ class FlightPlanBuilder:
|
||||
self.target_points = []
|
||||
|
||||
self.add_waypoint_row(NumberedWaypoint(waypoint_num, waypoint))
|
||||
self.last_waypoint = waypoint
|
||||
|
||||
def coalesce_target_points(self) -> None:
|
||||
if len(self.target_points) <= 4:
|
||||
@@ -157,7 +156,6 @@ class FlightPlanBuilder:
|
||||
self._format_time(waypoint.waypoint.tot),
|
||||
self._format_time(waypoint.waypoint.departure_time),
|
||||
])
|
||||
self.last_waypoint = waypoint.waypoint
|
||||
|
||||
def _format_time(self, time: Optional[int]) -> str:
|
||||
if time is None:
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user