diff --git a/gen/aircraft.py b/gen/aircraft.py index 5076ee8e..0c4aa97c 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -3,11 +3,11 @@ from __future__ import annotations import logging import random 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.action import AITaskPush, ActivateGroup, MessageToAll -from dcs.condition import CoalitionHasAirdrome, PartOfCoalitionInZone, TimeAfter +from dcs.action import AITaskPush, ActivateGroup +from dcs.condition import CoalitionHasAirdrome, TimeAfter from dcs.country import Country from dcs.flyingunit import FlyingUnit from dcs.helicopters import UH_1H, helicopter_map @@ -40,7 +40,6 @@ from dcs.task import ( ControlledTask, EPLRS, EngageTargets, - Escort, GroundAttack, OptROE, OptRTBOnBingoFuel, @@ -55,10 +54,10 @@ from dcs.task import ( Targets, Task, ) -from dcs.terrain.terrain import Airport, NoParkingSlotError +from dcs.terrain.terrain import Airport from dcs.translation import String 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 game import db @@ -548,7 +547,6 @@ class AircraftConflictGenerator: self.settings = settings self.conflict = conflict self.radio_registry = radio_registry - self.escort_targets: List[Tuple[FlyingGroup, int]] = [] self.flights: List[FlightData] = [] def get_intra_flight_channel(self, airframe: UnitType) -> RadioFrequency: @@ -633,10 +631,6 @@ class AircraftConflictGenerator: logging.warning(f"Unhandled departure control point: {cp.cptype}") 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( flight_type=flight.flight_type, units=group.units, @@ -808,8 +802,8 @@ 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, package, flight, timing, - dynamic_runways) + self.setup_flight_group(group, flight, dynamic_runways) + self.create_waypoints(group, package, flight, timing) def set_activation_time(self, flight: Flight, group: FlyingGroup, delay: int) -> None: @@ -988,8 +982,11 @@ class AircraftConflictGenerator: def configure_escort(self, group: FlyingGroup, flight: Flight, dynamic_runways: Dict[str, RunwayData]) -> None: - group.task = Escort.name - self._setup_group(group, Escort, flight, dynamic_runways) + # 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.configure_behavior(group, roe=OptROE.Values.OpenFire, restrict_jettison=True) @@ -998,8 +995,7 @@ class AircraftConflictGenerator: logging.error(f"Unhandled flight type: {flight.flight_type.name}") self.configure_behavior(group) - def setup_flight_group(self, group: FlyingGroup, package: Package, - flight: Flight, timing: PackageWaypointTiming, + def setup_flight_group(self, group: FlyingGroup, flight: Flight, dynamic_runways: Dict[str, RunwayData]) -> None: flight_type = flight.flight_type if flight_type in [FlightType.BARCAP, FlightType.TARCAP, @@ -1020,16 +1016,23 @@ class AircraftConflictGenerator: self.configure_eplrs(group, flight) + def create_waypoints(self, group: FlyingGroup, package: Package, + flight: Flight, timing: PackageWaypointTiming) -> None: + for waypoint in flight.points: waypoint.tot = None takeoff_point = FlightWaypoint.from_pydcs(group.points[0], flight.from_cp) self.set_takeoff_time(takeoff_point, package, flight, group) + + filtered_points = [] for point in flight.points: if point.only_for_player and not flight.client_count: continue + filtered_points.append(point) + for idx, point in enumerate(filtered_points): PydcsWaypointBuilder.for_waypoint( point, group, flight, timing, self.m ).build() @@ -1114,6 +1117,8 @@ class PydcsWaypointBuilder: mission: Mission) -> PydcsWaypointBuilder: builders = { FlightWaypointType.EGRESS: EgressPointBuilder, + FlightWaypointType.INGRESS_CAS: IngressBuilder, + FlightWaypointType.INGRESS_ESCORT: IngressBuilder, FlightWaypointType.INGRESS_SEAD: SeadIngressBuilder, FlightWaypointType.INGRESS_STRIKE: StrikeIngressBuilder, FlightWaypointType.JOIN: JoinPointBuilder, @@ -1236,8 +1241,48 @@ class JoinPointBuilder(PydcsWaypointBuilder): def build(self) -> MovingPoint: waypoint = super().build() self.set_waypoint_tot(waypoint, self.timing.join) + if self.flight.flight_type == FlightType.ESCORT: + self.configure_escort_tasks(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): def build(self) -> MovingPoint: diff --git a/gen/flights/flight.py b/gen/flights/flight.py index 82a25226..f47a8489 100644 --- a/gen/flights/flight.py +++ b/gen/flights/flight.py @@ -52,6 +52,7 @@ class FlightWaypointType(Enum): JOIN = 16 SPLIT = 17 LOITER = 18 + INGRESS_ESCORT = 19 class PredefinedWaypointCategory(Enum): diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index ed2b7bb0..e89d2f53 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -132,7 +132,7 @@ class FlightPlanBuilder: if not isinstance(location, TheaterGroundObject): raise InvalidObjectiveLocation(flight.flight_type, location) - builder = WaypointBuilder(self.doctrine) + builder = WaypointBuilder(flight, self.doctrine) builder.ascent(flight.from_cp) builder.hold(self._hold_point(flight)) builder.join(self.package.waypoints.join) @@ -222,7 +222,7 @@ class FlightPlanBuilder: ) start = end.point_from_heading(heading - 180, diameter) - builder = WaypointBuilder(self.doctrine) + builder = WaypointBuilder(flight, self.doctrine) builder.ascent(flight.from_cp) builder.race_track(start, end, patrol_alt) builder.rtb(flight.from_cp) @@ -264,7 +264,7 @@ class FlightPlanBuilder: orbit1p = orbit_center.point_from_heading(heading + 180, radius) # Create points - builder = WaypointBuilder(self.doctrine) + builder = WaypointBuilder(flight, self.doctrine) builder.ascent(flight.from_cp) builder.hold(self._hold_point(flight)) builder.join(self.package.waypoints.join) @@ -290,7 +290,7 @@ class FlightPlanBuilder: if custom_targets is None: custom_targets = [] - builder = WaypointBuilder(self.doctrine) + builder = WaypointBuilder(flight, self.doctrine) builder.ascent(flight.from_cp) builder.hold(self._hold_point(flight)) builder.join(self.package.waypoints.join) @@ -328,17 +328,12 @@ class FlightPlanBuilder: def generate_escort(self, flight: Flight) -> None: assert self.package.waypoints is not None - patrol_alt = random.randint( - self.doctrine.min_patrol_altitude, - self.doctrine.max_patrol_altitude - ) - - builder = WaypointBuilder(self.doctrine) + builder = WaypointBuilder(flight, self.doctrine) builder.ascent(flight.from_cp) builder.hold(self._hold_point(flight)) builder.join(self.package.waypoints.join) - builder.race_track(self.package.waypoints.ingress, - self.package.waypoints.egress, patrol_alt) + builder.escort(self.package.waypoints.ingress, + self.package.target, self.package.waypoints.egress) builder.split(self.package.waypoints.split) builder.rtb(flight.from_cp) @@ -366,7 +361,7 @@ class FlightPlanBuilder: center = ingress.point_from_heading(heading, distance / 2) egress = ingress.point_from_heading(heading, distance) - builder = WaypointBuilder(self.doctrine) + builder = WaypointBuilder(flight, self.doctrine) builder.ascent(flight.from_cp, is_helo) builder.hold(self._hold_point(flight)) builder.join(self.package.waypoints.join) @@ -379,33 +374,39 @@ class FlightPlanBuilder: flight.points = builder.build() # 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. Args: + flight: The flight to generate the descend point for. departure: Departure airfield or carrier. """ - builder = WaypointBuilder(self.doctrine) + builder = WaypointBuilder(flight, self.doctrine) builder.ascent(departure) 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. Args: + flight: The flight to generate the descend point for. arrival: Arrival airfield or carrier. """ - builder = WaypointBuilder(self.doctrine) + builder = WaypointBuilder(flight, self.doctrine) builder.descent(arrival) 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. Args: + flight: The flight to generate the landing waypoint for. arrival: Arrival airfield or carrier. """ - builder = WaypointBuilder(self.doctrine) + builder = WaypointBuilder(flight, self.doctrine) builder.land(arrival) return builder.build()[0] diff --git a/gen/flights/traveltime.py b/gen/flights/traveltime.py index 1b1c5622..8fb8b7af 100644 --- a/gen/flights/traveltime.py +++ b/gen/flights/traveltime.py @@ -22,12 +22,14 @@ CAP_DURATION = 30 # Minutes INGRESS_TYPES = { FlightWaypointType.INGRESS_CAS, + FlightWaypointType.INGRESS_ESCORT, FlightWaypointType.INGRESS_SEAD, FlightWaypointType.INGRESS_STRIKE, } IP_TYPES = { FlightWaypointType.INGRESS_CAS, + FlightWaypointType.INGRESS_ESCORT, FlightWaypointType.INGRESS_SEAD, FlightWaypointType.INGRESS_STRIKE, FlightWaypointType.PATROL_TRACK, diff --git a/gen/flights/waypointbuilder.py b/gen/flights/waypointbuilder.py index fd4b5aed..5ee8820c 100644 --- a/gen/flights/waypointbuilder.py +++ b/gen/flights/waypointbuilder.py @@ -8,11 +8,12 @@ from dcs.unit import Unit from game.data.doctrine import Doctrine from game.utils import nm_to_meter from theater import ControlPoint, MissionTarget, TheaterGroundObject -from .flight import FlightWaypoint, FlightWaypointType +from .flight import Flight, FlightWaypoint, FlightWaypointType class WaypointBuilder: - def __init__(self, doctrine: Doctrine) -> None: + def __init__(self, flight: Flight, doctrine: Doctrine) -> None: + self.flight = flight self.doctrine = doctrine self.waypoints: List[FlightWaypoint] = [] self.ingress_point: Optional[FlightWaypoint] = None @@ -127,6 +128,9 @@ class WaypointBuilder: def ingress_cas(self, position: Point, objective: MissionTarget) -> None: 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: self._ingress(FlightWaypointType.INGRESS_SEAD, position, objective) @@ -199,6 +203,9 @@ class WaypointBuilder: waypoint.description = description waypoint.pretty_name = description 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 self.waypoints.append(waypoint) # TODO: This seems wrong, but it's what was there before. @@ -231,6 +238,9 @@ class WaypointBuilder: waypoint.description = name waypoint.pretty_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 self.waypoints.append(waypoint) # TODO: This seems wrong, but it's what was there before. @@ -305,3 +315,33 @@ class WaypointBuilder: """ self.descent(arrival, is_helo) 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) diff --git a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py index 21a85a84..fc311120 100644 --- a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py +++ b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py @@ -111,19 +111,22 @@ class QFlightWaypointTab(QFrame): self.subwindow.show() 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_waypoint_list.update_list() self.on_change() 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_waypoint_list.update_list() self.on_change() 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_waypoint_list.update_list() self.on_change()