Initial implementation of AEW&C missions.

Still a work in progress (the missions don't actually perform their task, just orbit). Currently:

* AEW&C aircraft can be bought.
* AEW&C missions can be planned at any control point and at front lines.
* AEW&C will return after 4H or Bingo.
This commit is contained in:
Simon Krüger 2021-02-06 02:18:50 +01:00 committed by Simon Clark
parent 4a0ccc4c2f
commit e0501e46e3
13 changed files with 213 additions and 43 deletions

View File

@ -4,6 +4,8 @@ Saves from 2.4 are not compatible with 2.5.
## Features/Improvements
* **[Flight Planner]** (WIP) Added AEW&C missions.
## Fixes
# 2.4.1

View File

@ -1117,7 +1117,8 @@ COMMON_OVERRIDE = {
GroundAttack: "STRIKE",
Escort: "CAP",
RunwayAttack: "RUNWAY_ATTACK",
FighterSweep: "CAP"
FighterSweep: "CAP",
AWACS: "AEW&C",
}
"""
@ -1328,6 +1329,7 @@ CARRIER_CAPABLE = [
A_4E_C,
Rafale_M,
S_3B,
E_2C,
UH_1H,
Mi_8MT,

View File

@ -4,7 +4,7 @@ import math
import typing
from typing import Dict, Type
from dcs.task import CAP, CAS, Embarking, PinpointStrike, Task
from dcs.task import AWACS, CAP, CAS, Embarking, PinpointStrike, Task
from dcs.unittype import FlyingType, UnitType, VehicleType
from dcs.vehicles import AirDefence, Armor
@ -122,7 +122,7 @@ class Base:
for_task = db.unit_task(unit_type)
target_dict = None
if for_task == CAS or for_task == CAP or for_task == Embarking:
if for_task == AWACS or for_task == CAS or for_task == CAP or for_task == Embarking:
target_dict = self.aircraft
elif for_task == PinpointStrike:
target_dict = self.armor

View File

@ -812,6 +812,7 @@ class FrontLine(MissionTarget):
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
yield from [
FlightType.CAS,
FlightType.AEWC,
# TODO: FlightType.TROOP_TRANSPORT
# TODO: FlightType.EVAC
]

View File

@ -603,6 +603,14 @@ class ControlPoint(MissionTarget, ABC):
def income_per_turn(self) -> int:
return 0
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType
if self.is_friendly(for_player):
yield from [
FlightType.AEWC,
]
yield from super().mission_types(for_player)
class Airfield(ControlPoint):

View File

@ -41,6 +41,7 @@ from dcs.planes import (
)
from dcs.point import MovingPoint, PointAction
from dcs.task import (
AWACS,
AntishipStrike,
AttackGroup,
Bombing,
@ -90,7 +91,6 @@ from game.utils import Distance, meters, nautical_miles
from gen.airsupportgen import AirSupport
from gen.ato import AirTaskingOrder, Package
from gen.callsigns import create_group_callsign_from_unit
from gen.conflictgen import FRONTLINE_LENGTH
from gen.flights.flight import (
Flight,
FlightType,
@ -129,10 +129,11 @@ HELICOPTER_CHANNEL = MHz(127)
UHF_FALLBACK_CHANNEL = MHz(251)
TARGET_WAYPOINTS = (
FlightWaypointType.TARGET_GROUP_LOC,
FlightWaypointType.TARGET_POINT,
FlightWaypointType.TARGET_SHIP,
)
FlightWaypointType.TARGET_GROUP_LOC,
FlightWaypointType.TARGET_POINT,
FlightWaypointType.TARGET_SHIP,
)
# TODO: Get radio information for all the special cases.
def get_fallback_channel(unit_type: UnitType) -> RadioFrequency:
@ -731,11 +732,14 @@ class AircraftConflictGenerator:
group.load_loadout(payload_name)
if not group.units[0].pylons and for_task == RunwayAttack:
if PinpointStrike in db.PLANE_PAYLOAD_OVERRIDES[unit_type]:
logging.warning("No loadout for \"Runway Attack\" for the {}, defaulting to Strike loadout".format(str(unit_type)))
logging.warning(
"No loadout for \"Runway Attack\" for the {}, defaulting to Strike loadout".format(
str(unit_type)))
payload_name = db.PLANE_PAYLOAD_OVERRIDES[unit_type][PinpointStrike]
group.load_loadout(payload_name)
did_load_loadout = True
logging.info("Loaded overridden payload for {} - {} for task {}".format(unit_type, payload_name, for_task))
logging.info(
"Loaded overridden payload for {} - {} for task {}".format(unit_type, payload_name, for_task))
if not did_load_loadout:
group.load_task_default_loadout(for_task)
@ -995,7 +999,7 @@ class AircraftConflictGenerator:
group = self._generate_at_airport(
name=namegen.next_aircraft_name(country, control_point.id,
flight),
flight),
side=country,
unit_type=aircraft,
count=1,
@ -1058,7 +1062,7 @@ class AircraftConflictGenerator:
trigger.add_condition(
CoalitionHasAirdrome(coalition, flight.from_cp.id))
def generate_planned_flight(self, cp, country, flight:Flight):
def generate_planned_flight(self, cp, country, flight: Flight):
name = namegen.next_aircraft_name(country, cp.id, flight)
try:
if flight.start_type == "In Flight":
@ -1249,6 +1253,17 @@ class AircraftConflictGenerator:
roe=OptROE.Values.OpenFire,
restrict_jettison=True)
def configure_awacs(
self, group: FlyingGroup, package: Package, flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None:
group.task = AWACS.name
self._setup_group(group, AWACS, package, flight, dynamic_runways)
self.configure_behavior(
group,
react_on_threat=OptReactOnThreat.Values.EvadeFire,
roe=OptROE.Values.WeaponHold,
restrict_jettison=True)
def configure_escort(self, group: FlyingGroup, package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None:
@ -1274,6 +1289,8 @@ class AircraftConflictGenerator:
self.configure_cap(group, package, flight, dynamic_runways)
elif flight_type == FlightType.SWEEP:
self.configure_sweep(group, package, flight, dynamic_runways)
elif flight_type == FlightType.AEWC:
self.configure_awacs(group, package, flight, dynamic_runways)
elif flight_type in [FlightType.CAS, FlightType.BAI]:
self.configure_cas(group, package, flight, dynamic_runways)
elif flight_type == FlightType.DEAD:
@ -1326,8 +1343,8 @@ class AircraftConflictGenerator:
filtered_points = [
point for idx, point in enumerate(filtered_points) if (
point.waypoint_type not in TARGET_WAYPOINTS or idx == keep_target[0]
)
]
)
]
for idx, point in enumerate(filtered_points):
PydcsWaypointBuilder.for_waypoint(
@ -1458,8 +1475,8 @@ class PydcsWaypointBuilder:
If the flight is a player controlled Viggen flight, no TOT should be set on any waypoint except actual target waypoints.
"""
if (
(self.flight.client_count > 0 and self.flight.unit_type == AJS37) and
(self.waypoint.waypoint_type not in TARGET_WAYPOINTS)
(self.flight.client_count > 0 and self.flight.unit_type == AJS37) and
(self.waypoint.waypoint_type not in TARGET_WAYPOINTS)
):
return True
else:
@ -1622,12 +1639,12 @@ class SeadIngressBuilder(PydcsWaypointBuilder):
tgroup = self.mission.find_group(target_group.group_name)
if tgroup is not None:
waypoint.add_task(EngageTargetsInZone(
position=tgroup.position,
radius=int(nautical_miles(30).meters),
targets=[
Targets.All.GroundUnits.AirDefence,
])
)
position=tgroup.position,
radius=int(nautical_miles(30).meters),
targets=[
Targets.All.GroundUnits.AirDefence,
])
)
else:
logging.error(f"Could not find group for DEAD mission {target_group.group_name}")
self.register_special_waypoints(self.waypoint.targets)

View File

@ -174,6 +174,7 @@ class Package:
FlightType.TARCAP,
FlightType.BARCAP,
FlightType.SWEEP,
FlightType.AEWC,
FlightType.ESCORT,
]
for task in task_priorities:

View File

@ -12,8 +12,8 @@ from dcs.helicopters import (
OH_58D,
SA342L,
SA342M,
SH_60B,
UH_1H,
SH_60B
)
from dcs.planes import (
AJS37,
@ -22,11 +22,14 @@ from dcs.planes import (
A_10C,
A_10C_2,
A_20G,
A_50,
B_17G,
B_1B,
B_52H,
Bf_109K_4,
C_101CC,
E_2C,
E_3A,
FA_18C_hornet,
FW_190A8,
FW_190D9,
@ -40,9 +43,11 @@ from dcs.planes import (
F_4E,
F_5E_3,
F_86F_Sabre,
I_16,
JF_17,
J_11A,
Ju_88A4,
KJ_2000,
L_39ZA,
MQ_9_Reaper,
M_2000C,
@ -83,18 +88,16 @@ from dcs.planes import (
Tu_22M3,
Tu_95MS,
WingLoong_I,
I_16
)
from dcs.unittype import FlyingType
from gen.flights.flight import FlightType
from pydcs_extensions.a4ec.a4ec import A_4E_C
from pydcs_extensions.f22a.f22a import F_22A
from pydcs_extensions.mb339.mb339 import MB_339PAN
from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_M, Rafale_B
from pydcs_extensions.su57.su57 import Su_57
from pydcs_extensions.hercules.hercules import Hercules
from pydcs_extensions.mb339.mb339 import MB_339PAN
from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_B, Rafale_M
from pydcs_extensions.su57.su57 import Su_57
# All aircraft lists are in priority order. Aircraft higher in the list will be
# preferred over those lower in the list.
@ -155,8 +158,8 @@ CAP_CAPABLE = [
# Used for CAS (Close air support) and BAI (Battlefield Interdiction)
CAS_CAPABLE = [
A_10C_2,
A_10C,
B_1B,
A_10C,
F_14B,
F_14A_135_GR,
Su_25TM,
@ -373,6 +376,13 @@ DRONES = [
WingLoong_I
]
AEWC_CAPABLE = [
E_3A,
E_2C,
A_50,
KJ_2000,
]
def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
cap_missions = (FlightType.BARCAP, FlightType.TARCAP)
@ -396,6 +406,8 @@ def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
return STRIKE_CAPABLE
elif task == FlightType.ESCORT:
return CAP_CAPABLE
elif task == FlightType.AEWC:
return AEWC_CAPABLE
else:
logging.error(f"Unplannable flight type: {task}")
return []

View File

@ -20,6 +20,14 @@ if TYPE_CHECKING:
class FlightType(Enum):
"""Enumeration of mission types.
The value of each enumeration is the name that will be shown in the UI.
These values are persisted to the save game as well since they are a part of
each flight and thus a part of the ATO, so changing these values will break
save compat.
"""
TARCAP = "TARCAP"
BARCAP = "BARCAP"
CAS = "CAS"
@ -33,6 +41,7 @@ class FlightType(Enum):
SWEEP = "Fighter sweep"
OCA_RUNWAY = "OCA/Runway"
OCA_AIRCRAFT = "OCA/Aircraft"
AEWC = "AEW&C"
def __str__(self) -> str:
return self.value

View File

@ -15,6 +15,13 @@ from datetime import timedelta
from functools import cached_property
from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple
from dcs.planes import (
E_3A,
E_2C,
A_50,
KJ_2000
)
from dcs.mapping import Point
from dcs.unit import Unit
from shapely.geometry import Point as ShapelyPoint
@ -29,7 +36,7 @@ from game.theater import (
TheaterGroundObject,
)
from game.theater.theatergroundobject import EwrGroundObject
from game.utils import Distance, Speed, meters, nautical_miles
from game.utils import Distance, Speed, feet, meters, nautical_miles
from .closestairfields import ObjectiveDistanceCache
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
from .traveltime import GroundSpeed, TravelTime
@ -121,7 +128,7 @@ class FlightPlan:
failed to generate. Nevertheless, we have to defend against it.
"""
raise NotImplementedError
@cached_property
def bingo_fuel(self) -> int:
"""Bingo fuel value for the FlightPlan
@ -145,7 +152,7 @@ class FlightPlan:
"""Joker fuel value for the FlightPlan
"""
return self.bingo_fuel + 1000
def max_distance_from(self, cp: ControlPoint) -> Distance:
"""Returns the farthest waypoint of the flight plan from a ControlPoint.
:arg cp The ControlPoint to measure distance from.
@ -280,11 +287,11 @@ class LoiterFlightPlan(FlightPlan):
travel_time = super().travel_time_between_waypoints(a, b)
if a != self.hold:
return travel_time
try:
return travel_time + self.hold_duration
except AttributeError:
# Save compat for 2.3.
return travel_time + timedelta(minutes=5)
return travel_time + self.hold_duration
@property
def mission_departure_time(self) -> timedelta:
raise NotImplementedError
@dataclass(frozen=True)
@ -542,10 +549,10 @@ class StrikeFlightPlan(FormationFlightPlan):
@property
def package_speed_waypoints(self) -> Set[FlightWaypoint]:
return {
self.ingress,
self.egress,
self.split,
} | set(self.targets)
self.ingress,
self.egress,
self.split,
} | set(self.targets)
def speed_between_waypoints(self, a: FlightWaypoint,
b: FlightWaypoint) -> Speed:
@ -696,6 +703,41 @@ class SweepFlightPlan(LoiterFlightPlan):
return self.sweep_end_time
@dataclass(frozen=True)
class AwacsFlightPlan(LoiterFlightPlan):
takeoff: FlightWaypoint
nav_to: List[FlightWaypoint]
nav_from: List[FlightWaypoint]
land: FlightWaypoint
divert: Optional[FlightWaypoint]
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.takeoff
yield from self.nav_to
yield self.hold
yield from self.nav_from
yield self.land
if self.divert is not None:
yield self.divert
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
if waypoint == self.hold:
return self.package.time_over_target
return None
@property
def tot_waypoint(self) -> Optional[FlightWaypoint]:
return self.hold
@property
def push_time(self) -> timedelta:
return self.package.time_over_target + self.hold_duration
@property
def mission_departure_time(self) -> timedelta:
return self.push_time
@dataclass(frozen=True)
class CustomFlightPlan(FlightPlan):
custom_waypoints: List[FlightWaypoint]
@ -791,6 +833,8 @@ class FlightPlanBuilder:
return self.generate_sweep(flight)
elif task == FlightType.TARCAP:
return self.generate_tarcap(flight)
elif task == FlightType.AEWC:
return self.generate_aewc(flight)
raise PlanningError(
f"{task} flight plan generation not implemented")
@ -921,6 +965,45 @@ class FlightPlanBuilder:
FlightWaypointType.INGRESS_STRIKE,
targets)
def generate_aewc(self, flight: Flight) -> AwacsFlightPlan:
"""Generate a AWACS flight at a given location.
Args:
flight: The flight to generate the flight plan for.
"""
location = self.package.target
start = self.aewc_orbit(location)
# As high as possible to maximize detection and on-station time.
if flight.unit_type == E_2C:
patrol_alt = feet(30000)
elif flight.unit_type == E_3A:
patrol_alt = feet(35000)
elif flight.unit_type == A_50:
patrol_alt = feet(33000)
elif flight.unit_type == KJ_2000:
patrol_alt = feet(40000)
else:
patrol_alt = feet(25000)
builder = WaypointBuilder(flight, self.game, self.is_player)
start = builder.orbit(start, patrol_alt)
return AwacsFlightPlan(
package=self.package,
flight=flight,
takeoff=builder.takeoff(flight.departure),
nav_to=builder.nav_path(flight.departure.position, start.position,
patrol_alt),
nav_from=builder.nav_path(start.position, flight.arrival.position,
patrol_alt),
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert),
hold=start,
hold_duration=timedelta(hours=4),
)
def generate_bai(self, flight: Flight) -> StrikeFlightPlan:
"""Generates a BAI flight plan.
@ -1095,6 +1178,18 @@ class FlightPlanBuilder:
start = end.point_from_heading(heading - 180, diameter)
return start, end
@staticmethod
def aewc_orbit(location: MissionTarget) -> Point:
closest_airfield = location
# TODO: This is a heading to itself.
# Place this either over the target or as close as possible outside the
# threat zone: https://github.com/Khopa/dcs_liberation/issues/842.
heading = location.position.heading_between_point(closest_airfield.position)
return location.position.point_from_heading(
heading,
5000
)
def racetrack_for_frontline(self, origin: Point,
front_line: FrontLine) -> Tuple[Point, Point]:
ally_cp, enemy_cp = front_line.control_points

View File

@ -366,6 +366,26 @@ class WaypointBuilder:
return (self.race_track_start(start, altitude),
self.race_track_end(end, altitude))
@staticmethod
def orbit(start: Point, altitude: Distance) -> FlightWaypoint:
"""Creates an circular orbit point.
Args:
start: Position of the waypoint.
altitude: Altitude of the racetrack.
"""
waypoint = FlightWaypoint(
FlightWaypointType.LOITER,
start.x,
start.y,
altitude
)
waypoint.name = "ORBIT"
waypoint.description = "Anchor and hold at this point"
waypoint.pretty_name = "Orbit"
return waypoint
@staticmethod
def sweep_start(position: Point, altitude: Distance) -> FlightWaypoint:
"""Creates a sweep start waypoint.

View File

@ -47,6 +47,9 @@ class QAircraftTypeSelector(QComboBox):
elif mission_type in [FlightType.OCA_RUNWAY]:
if aircraft in gen.flights.ai_flight_planner_db.RUNWAY_ATTACK_CAPABLE:
self.addItem(f"{db.unit_get_expanded_info(self.country, aircraft, 'name')}", userData=aircraft)
elif mission_type in [FlightType.AEWC]:
if aircraft in gen.flights.ai_flight_planner_db.AEWC_CAPABLE:
self.addItem(f"{db.unit_get_expanded_info(self.country, aircraft, 'name')}", userData=aircraft)
current_aircraft_index = self.findData(current_aircraft)
if current_aircraft_index != -1:
self.setCurrentIndex(current_aircraft_index)

View File

@ -12,7 +12,7 @@ from PySide2.QtWidgets import (
QVBoxLayout,
QWidget,
)
from dcs.task import CAP, CAS
from dcs.task import CAP, CAS, AWACS
from dcs.unittype import FlyingType, UnitType
from game import db
@ -45,7 +45,7 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
def init_ui(self):
main_layout = QVBoxLayout()
tasks = [CAP, CAS]
tasks = [CAP, CAS, AWACS]
scroll_content = QWidget()
task_box_layout = QGridLayout()