Add escort tasks for the AI.

This commit is contained in:
Dan Albert 2020-10-18 15:05:26 -07:00
parent 95f486870d
commit fd969020af
6 changed files with 133 additions and 41 deletions

View File

@ -3,11 +3,11 @@ from __future__ import annotations
import logging import logging
import random import random
from dataclasses import dataclass from dataclasses import dataclass
from typing import Dict, List, Optional, Tuple, Type, Union from typing import Dict, List, Optional, Type, Union
from dcs import helicopters from dcs import helicopters
from dcs.action import AITaskPush, ActivateGroup, MessageToAll from dcs.action import AITaskPush, ActivateGroup
from dcs.condition import CoalitionHasAirdrome, PartOfCoalitionInZone, TimeAfter from dcs.condition import CoalitionHasAirdrome, TimeAfter
from dcs.country import Country from dcs.country import Country
from dcs.flyingunit import FlyingUnit from dcs.flyingunit import FlyingUnit
from dcs.helicopters import UH_1H, helicopter_map from dcs.helicopters import UH_1H, helicopter_map
@ -40,7 +40,6 @@ from dcs.task import (
ControlledTask, ControlledTask,
EPLRS, EPLRS,
EngageTargets, EngageTargets,
Escort,
GroundAttack, GroundAttack,
OptROE, OptROE,
OptRTBOnBingoFuel, OptRTBOnBingoFuel,
@ -55,10 +54,10 @@ from dcs.task import (
Targets, Targets,
Task, Task,
) )
from dcs.terrain.terrain import Airport, NoParkingSlotError from dcs.terrain.terrain import Airport
from dcs.translation import String from dcs.translation import String
from dcs.triggers import Event, TriggerOnce, TriggerRule from dcs.triggers import Event, TriggerOnce, TriggerRule
from dcs.unitgroup import FlyingGroup, Group, ShipGroup, StaticGroup from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup
from dcs.unittype import FlyingType, UnitType from dcs.unittype import FlyingType, UnitType
from game import db from game import db
@ -548,7 +547,6 @@ class AircraftConflictGenerator:
self.settings = settings self.settings = settings
self.conflict = conflict self.conflict = conflict
self.radio_registry = radio_registry self.radio_registry = radio_registry
self.escort_targets: List[Tuple[FlyingGroup, int]] = []
self.flights: List[FlightData] = [] self.flights: List[FlightData] = []
def get_intra_flight_channel(self, airframe: UnitType) -> RadioFrequency: def get_intra_flight_channel(self, airframe: UnitType) -> RadioFrequency:
@ -633,10 +631,6 @@ class AircraftConflictGenerator:
logging.warning(f"Unhandled departure control point: {cp.cptype}") logging.warning(f"Unhandled departure control point: {cp.cptype}")
departure_runway = fallback_runway departure_runway = fallback_runway
# The first waypoint is set automatically by pydcs, so it's not in our
# list. Convert the pydcs MovingPoint to a FlightWaypoint so it shows up
# in our FlightData.
first_point = FlightWaypoint.from_pydcs(group.points[0], flight.from_cp)
self.flights.append(FlightData( self.flights.append(FlightData(
flight_type=flight.flight_type, flight_type=flight.flight_type,
units=group.units, units=group.units,
@ -808,8 +802,8 @@ class AircraftConflictGenerator:
logging.info(f"Generating flight: {flight.unit_type}") logging.info(f"Generating flight: {flight.unit_type}")
group = self.generate_planned_flight(flight.from_cp, country, group = self.generate_planned_flight(flight.from_cp, country,
flight) flight)
self.setup_flight_group(group, package, flight, timing, self.setup_flight_group(group, flight, dynamic_runways)
dynamic_runways) self.create_waypoints(group, package, flight, timing)
def set_activation_time(self, flight: Flight, group: FlyingGroup, def set_activation_time(self, flight: Flight, group: FlyingGroup,
delay: int) -> None: delay: int) -> None:
@ -988,8 +982,11 @@ class AircraftConflictGenerator:
def configure_escort(self, group: FlyingGroup, flight: Flight, def configure_escort(self, group: FlyingGroup, flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None: dynamic_runways: Dict[str, RunwayData]) -> None:
group.task = Escort.name # Escort groups are actually given the CAP task so they can perform the
self._setup_group(group, Escort, flight, dynamic_runways) # 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.configure_behavior(group, roe=OptROE.Values.OpenFire, self.configure_behavior(group, roe=OptROE.Values.OpenFire,
restrict_jettison=True) restrict_jettison=True)
@ -998,8 +995,7 @@ class AircraftConflictGenerator:
logging.error(f"Unhandled flight type: {flight.flight_type.name}") logging.error(f"Unhandled flight type: {flight.flight_type.name}")
self.configure_behavior(group) self.configure_behavior(group)
def setup_flight_group(self, group: FlyingGroup, package: Package, def setup_flight_group(self, group: FlyingGroup, flight: Flight,
flight: Flight, timing: PackageWaypointTiming,
dynamic_runways: Dict[str, RunwayData]) -> None: dynamic_runways: Dict[str, RunwayData]) -> None:
flight_type = flight.flight_type flight_type = flight.flight_type
if flight_type in [FlightType.BARCAP, FlightType.TARCAP, if flight_type in [FlightType.BARCAP, FlightType.TARCAP,
@ -1020,16 +1016,23 @@ class AircraftConflictGenerator:
self.configure_eplrs(group, flight) self.configure_eplrs(group, flight)
def create_waypoints(self, group: FlyingGroup, package: Package,
flight: Flight, timing: PackageWaypointTiming) -> None:
for waypoint in flight.points: for waypoint in flight.points:
waypoint.tot = None waypoint.tot = None
takeoff_point = FlightWaypoint.from_pydcs(group.points[0], takeoff_point = FlightWaypoint.from_pydcs(group.points[0],
flight.from_cp) flight.from_cp)
self.set_takeoff_time(takeoff_point, package, flight, group) self.set_takeoff_time(takeoff_point, package, flight, group)
filtered_points = []
for point in flight.points: for point in flight.points:
if point.only_for_player and not flight.client_count: if point.only_for_player and not flight.client_count:
continue continue
filtered_points.append(point)
for idx, point in enumerate(filtered_points):
PydcsWaypointBuilder.for_waypoint( PydcsWaypointBuilder.for_waypoint(
point, group, flight, timing, self.m point, group, flight, timing, self.m
).build() ).build()
@ -1114,6 +1117,8 @@ class PydcsWaypointBuilder:
mission: Mission) -> PydcsWaypointBuilder: mission: Mission) -> PydcsWaypointBuilder:
builders = { builders = {
FlightWaypointType.EGRESS: EgressPointBuilder, FlightWaypointType.EGRESS: EgressPointBuilder,
FlightWaypointType.INGRESS_CAS: IngressBuilder,
FlightWaypointType.INGRESS_ESCORT: IngressBuilder,
FlightWaypointType.INGRESS_SEAD: SeadIngressBuilder, FlightWaypointType.INGRESS_SEAD: SeadIngressBuilder,
FlightWaypointType.INGRESS_STRIKE: StrikeIngressBuilder, FlightWaypointType.INGRESS_STRIKE: StrikeIngressBuilder,
FlightWaypointType.JOIN: JoinPointBuilder, FlightWaypointType.JOIN: JoinPointBuilder,
@ -1236,8 +1241,48 @@ class JoinPointBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint: def build(self) -> MovingPoint:
waypoint = super().build() waypoint = super().build()
self.set_waypoint_tot(waypoint, self.timing.join) self.set_waypoint_tot(waypoint, self.timing.join)
if self.flight.flight_type == FlightType.ESCORT:
self.configure_escort_tasks(waypoint)
return waypoint return waypoint
@staticmethod
def configure_escort_tasks(waypoint: MovingPoint) -> None:
# Ideally we would use the escort mission type and escort task to have
# the AI automatically but the AI only escorts AI flights while they are
# traveling between waypoints. When an AI flight performs an attack
# (such as attacking the mission target), AI escorts wander aimlessly
# until the escorted group resumes its flight plan.
#
# As such, we instead use the Search Then Engage task, which is an
# enroute task that causes the AI to follow their flight plan and engage
# enemies of the set type within a certain distance. The downside to
# this approach is that AI escorts are no longer related to the group
# they are escorting, aside from the fact that they fly a similar flight
# plan at the same time. With Escort, the escorts will follow the
# escorted group out of the area. The strike element may or may not fly
# directly over the target, and they may or may not require multiple
# attack runs. For the escort flight we must just assume a flight plan
# for the escort to fly. If the strike flight doesn't need to overfly
# the target, the escorts are needlessly going in harms way. If the
# strike flight needs multiple passes, the escorts may leave before the
# escorted aircraft do.
#
# Another possible option would be to use Search Then Engage for join ->
# ingress and egress -> split, but use a Search Then Engage in Zone task
# for the target area that is set to end on a flag flip that occurs when
# the strike aircraft finish their attack task.
#
# https://forums.eagle.ru/forum/english/digital-combat-simulator/dcs-world-2-5/bugs-and-problems-ai/ai-ad/250183-task-follow-and-escort-temporarily-aborted
waypoint.add_task(ControlledTask(EngageTargets(
# TODO: From doctrine.
max_distance=nm_to_meter(30),
targets=[Targets.All.Air.Planes.Fighters]
)))
# We could set this task to end at the split point. pydcs doesn't
# currently support that task end condition though, and we don't really
# need it.
class LandingPointBuilder(PydcsWaypointBuilder): class LandingPointBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint: def build(self) -> MovingPoint:

View File

@ -52,6 +52,7 @@ class FlightWaypointType(Enum):
JOIN = 16 JOIN = 16
SPLIT = 17 SPLIT = 17
LOITER = 18 LOITER = 18
INGRESS_ESCORT = 19
class PredefinedWaypointCategory(Enum): class PredefinedWaypointCategory(Enum):

View File

@ -132,7 +132,7 @@ class FlightPlanBuilder:
if not isinstance(location, TheaterGroundObject): if not isinstance(location, TheaterGroundObject):
raise InvalidObjectiveLocation(flight.flight_type, location) raise InvalidObjectiveLocation(flight.flight_type, location)
builder = WaypointBuilder(self.doctrine) builder = WaypointBuilder(flight, self.doctrine)
builder.ascent(flight.from_cp) builder.ascent(flight.from_cp)
builder.hold(self._hold_point(flight)) builder.hold(self._hold_point(flight))
builder.join(self.package.waypoints.join) builder.join(self.package.waypoints.join)
@ -222,7 +222,7 @@ class FlightPlanBuilder:
) )
start = end.point_from_heading(heading - 180, diameter) start = end.point_from_heading(heading - 180, diameter)
builder = WaypointBuilder(self.doctrine) builder = WaypointBuilder(flight, self.doctrine)
builder.ascent(flight.from_cp) builder.ascent(flight.from_cp)
builder.race_track(start, end, patrol_alt) builder.race_track(start, end, patrol_alt)
builder.rtb(flight.from_cp) builder.rtb(flight.from_cp)
@ -264,7 +264,7 @@ class FlightPlanBuilder:
orbit1p = orbit_center.point_from_heading(heading + 180, radius) orbit1p = orbit_center.point_from_heading(heading + 180, radius)
# Create points # Create points
builder = WaypointBuilder(self.doctrine) builder = WaypointBuilder(flight, self.doctrine)
builder.ascent(flight.from_cp) builder.ascent(flight.from_cp)
builder.hold(self._hold_point(flight)) builder.hold(self._hold_point(flight))
builder.join(self.package.waypoints.join) builder.join(self.package.waypoints.join)
@ -290,7 +290,7 @@ class FlightPlanBuilder:
if custom_targets is None: if custom_targets is None:
custom_targets = [] custom_targets = []
builder = WaypointBuilder(self.doctrine) builder = WaypointBuilder(flight, self.doctrine)
builder.ascent(flight.from_cp) builder.ascent(flight.from_cp)
builder.hold(self._hold_point(flight)) builder.hold(self._hold_point(flight))
builder.join(self.package.waypoints.join) builder.join(self.package.waypoints.join)
@ -328,17 +328,12 @@ class FlightPlanBuilder:
def generate_escort(self, flight: Flight) -> None: def generate_escort(self, flight: Flight) -> None:
assert self.package.waypoints is not None assert self.package.waypoints is not None
patrol_alt = random.randint( builder = WaypointBuilder(flight, self.doctrine)
self.doctrine.min_patrol_altitude,
self.doctrine.max_patrol_altitude
)
builder = WaypointBuilder(self.doctrine)
builder.ascent(flight.from_cp) builder.ascent(flight.from_cp)
builder.hold(self._hold_point(flight)) builder.hold(self._hold_point(flight))
builder.join(self.package.waypoints.join) builder.join(self.package.waypoints.join)
builder.race_track(self.package.waypoints.ingress, builder.escort(self.package.waypoints.ingress,
self.package.waypoints.egress, patrol_alt) self.package.target, self.package.waypoints.egress)
builder.split(self.package.waypoints.split) builder.split(self.package.waypoints.split)
builder.rtb(flight.from_cp) builder.rtb(flight.from_cp)
@ -366,7 +361,7 @@ class FlightPlanBuilder:
center = ingress.point_from_heading(heading, distance / 2) center = ingress.point_from_heading(heading, distance / 2)
egress = ingress.point_from_heading(heading, distance) egress = ingress.point_from_heading(heading, distance)
builder = WaypointBuilder(self.doctrine) builder = WaypointBuilder(flight, self.doctrine)
builder.ascent(flight.from_cp, is_helo) builder.ascent(flight.from_cp, is_helo)
builder.hold(self._hold_point(flight)) builder.hold(self._hold_point(flight))
builder.join(self.package.waypoints.join) builder.join(self.package.waypoints.join)
@ -379,33 +374,39 @@ class FlightPlanBuilder:
flight.points = builder.build() flight.points = builder.build()
# TODO: Make a model for the waypoint builder and use that in the UI. # TODO: Make a model for the waypoint builder and use that in the UI.
def generate_ascend_point(self, departure: ControlPoint) -> FlightWaypoint: def generate_ascend_point(self, flight: Flight,
departure: ControlPoint) -> FlightWaypoint:
"""Generate ascend point. """Generate ascend point.
Args: Args:
flight: The flight to generate the descend point for.
departure: Departure airfield or carrier. departure: Departure airfield or carrier.
""" """
builder = WaypointBuilder(self.doctrine) builder = WaypointBuilder(flight, self.doctrine)
builder.ascent(departure) builder.ascent(departure)
return builder.build()[0] return builder.build()[0]
def generate_descend_point(self, arrival: ControlPoint) -> FlightWaypoint: def generate_descend_point(self, flight: Flight,
arrival: ControlPoint) -> FlightWaypoint:
"""Generate approach/descend point. """Generate approach/descend point.
Args: Args:
flight: The flight to generate the descend point for.
arrival: Arrival airfield or carrier. arrival: Arrival airfield or carrier.
""" """
builder = WaypointBuilder(self.doctrine) builder = WaypointBuilder(flight, self.doctrine)
builder.descent(arrival) builder.descent(arrival)
return builder.build()[0] return builder.build()[0]
def generate_rtb_waypoint(self, arrival: ControlPoint) -> FlightWaypoint: def generate_rtb_waypoint(self, flight: Flight,
arrival: ControlPoint) -> FlightWaypoint:
"""Generate RTB landing point. """Generate RTB landing point.
Args: Args:
flight: The flight to generate the landing waypoint for.
arrival: Arrival airfield or carrier. arrival: Arrival airfield or carrier.
""" """
builder = WaypointBuilder(self.doctrine) builder = WaypointBuilder(flight, self.doctrine)
builder.land(arrival) builder.land(arrival)
return builder.build()[0] return builder.build()[0]

View File

@ -22,12 +22,14 @@ CAP_DURATION = 30 # Minutes
INGRESS_TYPES = { INGRESS_TYPES = {
FlightWaypointType.INGRESS_CAS, FlightWaypointType.INGRESS_CAS,
FlightWaypointType.INGRESS_ESCORT,
FlightWaypointType.INGRESS_SEAD, FlightWaypointType.INGRESS_SEAD,
FlightWaypointType.INGRESS_STRIKE, FlightWaypointType.INGRESS_STRIKE,
} }
IP_TYPES = { IP_TYPES = {
FlightWaypointType.INGRESS_CAS, FlightWaypointType.INGRESS_CAS,
FlightWaypointType.INGRESS_ESCORT,
FlightWaypointType.INGRESS_SEAD, FlightWaypointType.INGRESS_SEAD,
FlightWaypointType.INGRESS_STRIKE, FlightWaypointType.INGRESS_STRIKE,
FlightWaypointType.PATROL_TRACK, FlightWaypointType.PATROL_TRACK,

View File

@ -8,11 +8,12 @@ from dcs.unit import Unit
from game.data.doctrine import Doctrine from game.data.doctrine import Doctrine
from game.utils import nm_to_meter from game.utils import nm_to_meter
from theater import ControlPoint, MissionTarget, TheaterGroundObject from theater import ControlPoint, MissionTarget, TheaterGroundObject
from .flight import FlightWaypoint, FlightWaypointType from .flight import Flight, FlightWaypoint, FlightWaypointType
class WaypointBuilder: class WaypointBuilder:
def __init__(self, doctrine: Doctrine) -> None: def __init__(self, flight: Flight, doctrine: Doctrine) -> None:
self.flight = flight
self.doctrine = doctrine self.doctrine = doctrine
self.waypoints: List[FlightWaypoint] = [] self.waypoints: List[FlightWaypoint] = []
self.ingress_point: Optional[FlightWaypoint] = None self.ingress_point: Optional[FlightWaypoint] = None
@ -127,6 +128,9 @@ class WaypointBuilder:
def ingress_cas(self, position: Point, objective: MissionTarget) -> None: def ingress_cas(self, position: Point, objective: MissionTarget) -> None:
self._ingress(FlightWaypointType.INGRESS_CAS, position, objective) self._ingress(FlightWaypointType.INGRESS_CAS, position, objective)
def ingress_escort(self, position: Point, objective: MissionTarget) -> None:
self._ingress(FlightWaypointType.INGRESS_ESCORT, position, objective)
def ingress_sead(self, position: Point, objective: MissionTarget) -> None: def ingress_sead(self, position: Point, objective: MissionTarget) -> None:
self._ingress(FlightWaypointType.INGRESS_SEAD, position, objective) self._ingress(FlightWaypointType.INGRESS_SEAD, position, objective)
@ -199,6 +203,9 @@ class WaypointBuilder:
waypoint.description = description waypoint.description = description
waypoint.pretty_name = description waypoint.pretty_name = description
waypoint.name = name waypoint.name = name
# The target waypoints are only for the player's benefit. AI tasks for
# the target are set on the ingress point so they begin their attack
# *before* reaching the target.
waypoint.only_for_player = True waypoint.only_for_player = True
self.waypoints.append(waypoint) self.waypoints.append(waypoint)
# TODO: This seems wrong, but it's what was there before. # TODO: This seems wrong, but it's what was there before.
@ -231,6 +238,9 @@ class WaypointBuilder:
waypoint.description = name waypoint.description = name
waypoint.pretty_name = name waypoint.pretty_name = name
waypoint.name = name waypoint.name = name
# The target waypoints are only for the player's benefit. AI tasks for
# the target are set on the ingress point so they begin their attack
# *before* reaching the target.
waypoint.only_for_player = True waypoint.only_for_player = True
self.waypoints.append(waypoint) self.waypoints.append(waypoint)
# TODO: This seems wrong, but it's what was there before. # TODO: This seems wrong, but it's what was there before.
@ -305,3 +315,33 @@ class WaypointBuilder:
""" """
self.descent(arrival, is_helo) self.descent(arrival, is_helo)
self.land(arrival) self.land(arrival)
def escort(self, ingress: Point, target: MissionTarget,
egress: Point) -> None:
"""Creates the waypoints needed to escort the package.
Args:
ingress: The package ingress point.
target: The mission target.
egress: The package egress point.
"""
# This would preferably be no points at all, and instead the Escort task
# would begin on the join point and end on the split point, however the
# escort task does not appear to work properly (see the longer
# description in gen.aircraft.JoinPointBuilder), so instead we give
# the escort flights a flight plan including the ingress point, target
# area, and egress point.
self._ingress(FlightWaypointType.INGRESS_ESCORT, ingress, target)
waypoint = FlightWaypoint(
FlightWaypointType.TARGET_GROUP_LOC,
target.position.x,
target.position.y,
self.doctrine.ingress_altitude
)
waypoint.name = "TARGET"
waypoint.description = "Escort the package"
waypoint.pretty_name = "Target area"
self.waypoints.append(waypoint)
self.egress(egress, target)

View File

@ -111,19 +111,22 @@ class QFlightWaypointTab(QFrame):
self.subwindow.show() self.subwindow.show()
def on_ascend_waypoint(self): def on_ascend_waypoint(self):
ascend = self.planner.generate_ascend_point(self.flight.from_cp) ascend = self.planner.generate_ascend_point(self.flight,
self.flight.from_cp)
self.flight.points.append(ascend) self.flight.points.append(ascend)
self.flight_waypoint_list.update_list() self.flight_waypoint_list.update_list()
self.on_change() self.on_change()
def on_rtb_waypoint(self): def on_rtb_waypoint(self):
rtb = self.planner.generate_rtb_waypoint(self.flight.from_cp) rtb = self.planner.generate_rtb_waypoint(self.flight,
self.flight.from_cp)
self.flight.points.append(rtb) self.flight.points.append(rtb)
self.flight_waypoint_list.update_list() self.flight_waypoint_list.update_list()
self.on_change() self.on_change()
def on_descend_waypoint(self): def on_descend_waypoint(self):
descend = self.planner.generate_descend_point(self.flight.from_cp) descend = self.planner.generate_descend_point(self.flight,
self.flight.from_cp)
self.flight.points.append(descend) self.flight.points.append(descend)
self.flight_waypoint_list.update_list() self.flight_waypoint_list.update_list()
self.on_change() self.on_change()