Airlift support.

UI isn't finished. Bulk transfers where the player doesn't care what
aircraft get used work (though they're chosen with no thought at all),
but being able to plan your own airlift flight isn't here yet.

Cargo planes are not implemented yet.

No way to view the cargo of a flight (will come with the cargo flight
planning UI).

The airlift flight/package creation should probably be moved out of the
UI and into the game code.

AI doesn't use these yet.

https://github.com/Khopa/dcs_liberation/issues/825
This commit is contained in:
Dan Albert
2021-04-21 17:13:35 -07:00
parent 8e361a8776
commit 481f195725
12 changed files with 406 additions and 73 deletions

View File

@@ -8,6 +8,8 @@ example, the package to strike an enemy airfield may contain an escort flight,
a SEAD flight, and the strike aircraft themselves. CAP packages may contain only
the single CAP flight.
"""
from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass, field
@@ -172,6 +174,7 @@ class Package:
FlightType.OCA_RUNWAY,
FlightType.BAI,
FlightType.DEAD,
FlightType.TRANSPORT,
FlightType.SEAD,
FlightType.TARCAP,
FlightType.BARCAP,

View File

@@ -1,6 +1,5 @@
from __future__ import annotations
from collections import defaultdict
from datetime import timedelta
from enum import Enum
from typing import Dict, List, Optional, TYPE_CHECKING, Type
@@ -15,6 +14,7 @@ from game.theater.controlpoint import ControlPoint, MissionTarget
from game.utils import Distance, meters
if TYPE_CHECKING:
from game.transfers import AirliftOrder
from gen.ato import Package
from gen.flights.flightplan import FlightPlan
@@ -43,6 +43,7 @@ class FlightType(Enum):
OCA_RUNWAY = "OCA/Runway"
OCA_AIRCRAFT = "OCA/Aircraft"
AEWC = "AEW&C"
TRANSPORT = "Transport"
def __str__(self) -> str:
return self.value
@@ -75,6 +76,8 @@ class FlightWaypointType(Enum):
DIVERT = 23
INGRESS_OCA_RUNWAY = 24
INGRESS_OCA_AIRCRAFT = 25
PICKUP = 26
DROP_OFF = 27
class FlightWaypoint:
@@ -164,6 +167,7 @@ class Flight:
arrival: ControlPoint,
divert: Optional[ControlPoint],
custom_name: Optional[str] = None,
cargo: Optional[AirliftOrder] = None,
) -> None:
self.package = package
self.country = country
@@ -181,6 +185,9 @@ class Flight:
self.client_count = 0
self.custom_name = custom_name
# Only used by transport missions.
self.cargo = cargo
# Will be replaced with a more appropriate FlightPlan by
# FlightPlanBuilder, but an empty flight plan the flight begins with an
# empty flight plan.

View File

@@ -31,7 +31,6 @@ from game.theater import (
TheaterGroundObject,
)
from game.theater.theatergroundobject import EwrGroundObject
from game.transfers import Convoy
from game.utils import Distance, Speed, feet, meters, nautical_miles
from .closestairfields import ObjectiveDistanceCache
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
@@ -42,6 +41,7 @@ from ..conflictgen import Conflict, FRONTLINE_LENGTH
if TYPE_CHECKING:
from game import Game
from gen.ato import Package
from game.transfers import Convoy
INGRESS_TYPES = {
FlightWaypointType.INGRESS_CAS,
@@ -736,6 +736,46 @@ class AwacsFlightPlan(LoiterFlightPlan):
return self.push_time
@dataclass(frozen=True)
class AirliftFlightPlan(FlightPlan):
takeoff: FlightWaypoint
nav_to_pickup: List[FlightWaypoint]
pickup: Optional[FlightWaypoint]
nav_to_drop_off: List[FlightWaypoint]
drop_off: FlightWaypoint
nav_to_home: List[FlightWaypoint]
land: FlightWaypoint
divert: Optional[FlightWaypoint]
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.takeoff
yield from self.nav_to_pickup
if self.pickup:
yield self.pickup
yield from self.nav_to_drop_off
yield self.drop_off
yield from self.nav_to_home
yield self.land
if self.divert is not None:
yield self.divert
@property
def tot_waypoint(self) -> Optional[FlightWaypoint]:
return self.drop_off
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
# TOT planning isn't really useful for transports. They're behind the front
# lines so no need to wait for escorts or for other missions to complete.
return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
return None
@property
def mission_departure_time(self) -> timedelta:
return self.package.time_over_target
@dataclass(frozen=True)
class CustomFlightPlan(FlightPlan):
custom_waypoints: List[FlightWaypoint]
@@ -844,6 +884,8 @@ class FlightPlanBuilder:
return self.generate_tarcap(flight)
elif task == FlightType.AEWC:
return self.generate_aewc(flight)
elif task == FlightType.TRANSPORT:
return self.generate_transport(flight)
raise PlanningError(f"{task} flight plan generation not implemented")
def regenerate_package_waypoints(self) -> None:
@@ -1023,6 +1065,8 @@ class FlightPlanBuilder:
"""
location = self.package.target
from game.transfers import Convoy
targets: List[StrikeTarget] = []
if isinstance(location, TheaterGroundObject):
for group in location.groups:
@@ -1141,6 +1185,57 @@ class FlightPlanBuilder:
divert=builder.divert(flight.divert),
)
def generate_transport(self, flight: Flight) -> AirliftFlightPlan:
"""Generate an airlift flight at a given location.
Args:
flight: The flight to generate the flight plan for.
"""
cargo = flight.cargo
if cargo is None:
raise PlanningError(
"Cannot plan transport mission for flight with no cargo."
)
altitude = feet(1500)
altitude_is_agl = True
builder = WaypointBuilder(flight, self.game, self.is_player)
pickup = None
nav_to_pickup = []
if cargo.origin != flight.departure:
pickup = builder.pickup(cargo.origin)
nav_to_pickup = builder.nav_path(
flight.departure.position,
cargo.origin.position,
altitude,
altitude_is_agl,
)
return AirliftFlightPlan(
package=self.package,
flight=flight,
takeoff=builder.takeoff(flight.departure),
nav_to_pickup=nav_to_pickup,
pickup=pickup,
nav_to_drop_off=builder.nav_path(
cargo.origin.position,
cargo.destination.position,
altitude,
altitude_is_agl,
),
drop_off=builder.drop_off(cargo.destination),
nav_to_home=builder.nav_path(
cargo.origin.position,
flight.arrival.position,
altitude,
altitude_is_agl,
),
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert),
)
def racetrack_for_objective(
self, location: MissionTarget, barcap: bool
) -> Tuple[Point, Point]:

View File

@@ -16,10 +16,9 @@ from dcs.mapping import Point
from dcs.unit import Unit
from dcs.unitgroup import Group, VehicleGroup
from game.transfers import Convoy
if TYPE_CHECKING:
from game import Game
from game.transfers import Convoy
from game.theater import (
ControlPoint,
@@ -444,24 +443,69 @@ class WaypointBuilder:
return ingress, waypoint, egress
@staticmethod
def nav(position: Point, altitude: Distance) -> FlightWaypoint:
def pickup(control_point: ControlPoint) -> FlightWaypoint:
"""Creates a cargo pickup waypoint.
Args:
control_point: Pick up location.
"""
waypoint = FlightWaypoint(
FlightWaypointType.PICKUP,
control_point.position.x,
control_point.position.y,
meters(0),
)
waypoint.alt_type = "RADIO"
waypoint.name = "PICKUP"
waypoint.description = f"Pick up cargo from {control_point}"
waypoint.pretty_name = "Pick up location"
return waypoint
@staticmethod
def drop_off(control_point: ControlPoint) -> FlightWaypoint:
"""Creates a cargo drop-off waypoint.
Args:
control_point: Drop-off location.
"""
waypoint = FlightWaypoint(
FlightWaypointType.PICKUP,
control_point.position.x,
control_point.position.y,
meters(0),
)
waypoint.alt_type = "RADIO"
waypoint.name = "DROP OFF"
waypoint.description = f"Drop off cargo at {control_point}"
waypoint.pretty_name = "Drop off location"
return waypoint
@staticmethod
def nav(
position: Point, altitude: Distance, altitude_is_agl: bool = False
) -> FlightWaypoint:
"""Creates a navigation point.
Args:
position: Position of the waypoint.
altitude: Altitude of the waypoint.
altitude_is_agl: True for altitude is AGL. False if altitude is MSL.
"""
waypoint = FlightWaypoint(
FlightWaypointType.NAV, position.x, position.y, altitude
)
if altitude_is_agl:
waypoint.alt_type = "RADIO"
waypoint.name = "NAV"
waypoint.description = "NAV"
waypoint.pretty_name = "Nav"
return waypoint
def nav_path(self, a: Point, b: Point, altitude: Distance) -> List[FlightWaypoint]:
def nav_path(
self, a: Point, b: Point, altitude: Distance, altitude_is_agl: bool = False
) -> List[FlightWaypoint]:
path = self.clean_nav_points(self.navmesh.shortest_path(a, b))
return [self.nav(self.perturb(p), altitude) for p in path]
return [self.nav(self.perturb(p), altitude, altitude_is_agl) for p in path]
def clean_nav_points(self, points: Iterable[Point]) -> Iterator[Point]:
# Examine a sliding window of three waypoints. `current` is the waypoint