diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index 7181cd9f..c905d993 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -20,7 +20,13 @@ from dcs.unit import Unit from game.data.doctrine import Doctrine from game.utils import nm_to_meter -from theater import ControlPoint, FrontLine, MissionTarget, TheaterGroundObject +from theater import ( + ControlPoint, + FrontLine, + MissionTarget, + SamGroundObject, + TheaterGroundObject, +) from .closestairfields import ObjectiveDistanceCache from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType from .traveltime import GroundSpeed, TravelTime @@ -616,13 +622,7 @@ class FlightPlanBuilder: raise RuntimeError("Flight must be a part of the package") if self.package.waypoints is None: self.regenerate_package_waypoints() - - try: - flight_plan = self.generate_flight_plan(flight, custom_targets) - except PlanningError: - logging.exception(f"Could not create flight plan") - return - flight.flight_plan = flight_plan + flight.flight_plan = self.generate_flight_plan(flight, custom_targets) def generate_flight_plan( self, flight: Flight, @@ -872,7 +872,7 @@ class FlightPlanBuilder: """ location = self.package.target - if not isinstance(location, TheaterGroundObject): + if not isinstance(location, SamGroundObject): logging.exception(f"Invalid Objective Location for DEAD flight {flight=} at {location=}") raise InvalidObjectiveLocation(flight.flight_type, location) @@ -897,9 +897,6 @@ class FlightPlanBuilder: """ location = self.package.target - if not isinstance(location, TheaterGroundObject): - raise InvalidObjectiveLocation(flight.flight_type, location) - # TODO: Unify these. # There doesn't seem to be any reason to treat the UI fragged missions # different from the automatic missions. @@ -1066,7 +1063,7 @@ class FlightPlanBuilder: return builder.land(arrival) def strike_flightplan( - self, flight: Flight, location: TheaterGroundObject, + self, flight: Flight, location: MissionTarget, targets: Optional[List[StrikeTarget]] = None) -> StrikeFlightPlan: assert self.package.waypoints is not None builder = WaypointBuilder(self.game.conditions, flight, self.doctrine, @@ -1116,8 +1113,8 @@ class FlightPlanBuilder: def _advancing_rendezvous_point(self, attack_transition: Point) -> Point: """Creates a rendezvous point that advances toward the target.""" heading = self._heading_to_package_airfield(attack_transition) - return attack_transition.point_from_heading(heading, - -self.doctrine.join_distance) + return attack_transition.point_from_heading( + heading, -self.doctrine.join_distance) def _rendezvous_should_retreat(self, attack_transition: Point) -> bool: transition_target_distance = attack_transition.distance_to_point( diff --git a/qt_ui/widgets/combos/QFlightTypeComboBox.py b/qt_ui/widgets/combos/QFlightTypeComboBox.py index 8adf0bbc..6ba9e455 100644 --- a/qt_ui/widgets/combos/QFlightTypeComboBox.py +++ b/qt_ui/widgets/combos/QFlightTypeComboBox.py @@ -1,107 +1,16 @@ """Combo box for selecting a flight's task type.""" -import logging -from typing import Iterator from PySide2.QtWidgets import QComboBox -from gen.flights.flight import FlightType -from theater import ( - ConflictTheater, - ControlPoint, - FrontLine, - MissionTarget, - TheaterGroundObject, -) +from theater import ConflictTheater, MissionTarget class QFlightTypeComboBox(QComboBox): """Combo box for selecting a flight task type.""" - COMMON_ENEMY_MISSIONS = [ - FlightType.TARCAP, - FlightType.ESCORT, - FlightType.SEAD, - FlightType.DEAD, - FlightType.SWEEP, - # TODO: FlightType.ELINT, - # TODO: FlightType.EWAR, - # TODO: FlightType.RECON, - ] - - COMMON_FRIENDLY_MISSIONS = [ - FlightType.BARCAP, - ] - - FRIENDLY_AIRBASE_MISSIONS = [ - # TODO: FlightType.INTERCEPTION - # TODO: FlightType.LOGISTICS - ] + COMMON_FRIENDLY_MISSIONS - - FRIENDLY_CARRIER_MISSIONS = [ - # TODO: FlightType.INTERCEPTION - # TODO: Buddy tanking for the A-4? - # TODO: Rescue chopper? - # TODO: Inter-ship logistics? - ] + COMMON_FRIENDLY_MISSIONS - - ENEMY_CARRIER_MISSIONS = [ - FlightType.ESCORT, - FlightType.BARCAP, - # TODO: FlightType.ANTISHIP - ] - - ENEMY_AIRBASE_MISSIONS = [ - # TODO: FlightType.STRIKE - ] + COMMON_ENEMY_MISSIONS - - FRIENDLY_GROUND_OBJECT_MISSIONS = [ - # TODO: FlightType.LOGISTICS - # TODO: FlightType.TROOP_TRANSPORT - ] + COMMON_FRIENDLY_MISSIONS - - ENEMY_GROUND_OBJECT_MISSIONS = [ - FlightType.STRIKE, - ] + COMMON_ENEMY_MISSIONS - - FRONT_LINE_MISSIONS = [ - FlightType.CAS, - # TODO: FlightType.TROOP_TRANSPORT - # TODO: FlightType.EVAC - ] + COMMON_ENEMY_MISSIONS - - # TODO: Add BAI missions after we have useful BAI targets. - def __init__(self, theater: ConflictTheater, target: MissionTarget) -> None: super().__init__() self.theater = theater self.target = target - for mission_type in self.mission_types_for_target(): + for mission_type in self.target.mission_types(for_player=True): self.addItem(mission_type.name, userData=mission_type) - - def mission_types_for_target(self) -> Iterator[FlightType]: - if isinstance(self.target, ControlPoint): - friendly = self.target.captured - fleet = self.target.is_fleet - if friendly: - if fleet: - yield from self.FRIENDLY_CARRIER_MISSIONS - else: - yield from self.FRIENDLY_AIRBASE_MISSIONS - else: - if fleet: - yield from self.ENEMY_CARRIER_MISSIONS - else: - yield from self.ENEMY_AIRBASE_MISSIONS - elif isinstance(self.target, TheaterGroundObject): - # TODO: Filter more based on the category. - friendly = self.target.control_point.captured - if friendly: - yield from self.FRIENDLY_GROUND_OBJECT_MISSIONS - else: - yield from self.ENEMY_GROUND_OBJECT_MISSIONS - elif isinstance(self.target, FrontLine): - yield from self.FRONT_LINE_MISSIONS - else: - logging.error( - f"Unhandled target type: {self.target.__class__.__name__}" - ) diff --git a/qt_ui/windows/mission/QPackageDialog.py b/qt_ui/windows/mission/QPackageDialog.py index 6298379f..30a9caf0 100644 --- a/qt_ui/windows/mission/QPackageDialog.py +++ b/qt_ui/windows/mission/QPackageDialog.py @@ -8,6 +8,7 @@ from PySide2.QtWidgets import ( QDialog, QHBoxLayout, QLabel, + QMessageBox, QPushButton, QTimeEdit, QVBoxLayout, @@ -16,7 +17,7 @@ from PySide2.QtWidgets import ( from game.game import Game from gen.ato import Package from gen.flights.flight import Flight -from gen.flights.flightplan import FlightPlanBuilder +from gen.flights.flightplan import FlightPlanBuilder, PlanningError from gen.flights.traveltime import TotEstimator from qt_ui.models import AtoModel, GameModel, PackageModel from qt_ui.uiconstants import EVENT_ICONS @@ -167,7 +168,15 @@ class QPackageDialog(QDialog): self.package_model.add_flight(flight) planner = FlightPlanBuilder(self.game, self.package_model.package, is_player=True) - planner.populate_flight_plan(flight) + try: + planner.populate_flight_plan(flight) + except PlanningError as ex: + self.game.aircraft_inventory.return_from_flight(flight) + self.package_model.delete_flight(flight) + logging.exception("Could not create flight") + QMessageBox.critical( + self, "Could not create flight", str(ex), QMessageBox.Ok + ) # noinspection PyUnresolvedReferences self.package_changed.emit() diff --git a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py index 5f031622..a9103454 100644 --- a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py +++ b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py @@ -1,3 +1,4 @@ +import logging from typing import List, Optional from PySide2.QtCore import Signal @@ -13,13 +14,15 @@ from PySide2.QtWidgets import ( from game import Game from gen.ato import Package from gen.flights.flight import Flight, FlightType -from gen.flights.flightplan import FlightPlanBuilder +from gen.flights.flightplan import ( + FlightPlanBuilder, + PlanningError, +) from qt_ui.windows.mission.flight.waypoints.QFlightWaypointList import \ QFlightWaypointList -from qt_ui.windows.mission.flight.waypoints\ +from qt_ui.windows.mission.flight.waypoints \ .QPredefinedWaypointSelectionWindow import \ QPredefinedWaypointSelectionWindow -from theater import FrontLine class QFlightWaypointTab(QFrame): @@ -55,17 +58,8 @@ class QFlightWaypointTab(QFrame): rlayout.addWidget(QLabel("Generator :")) rlayout.addWidget(QLabel("AI compatible")) - # TODO: Filter by objective type. self.recreate_buttons.clear() - recreate_types = [ - FlightType.CAS, - FlightType.CAP, - FlightType.DEAD, - FlightType.ESCORT, - FlightType.SEAD, - FlightType.STRIKE - ] - for task in recreate_types: + for task in self.package.target.mission_types(for_player=True): def make_closure(arg): def closure(): return self.confirm_recreate(arg) @@ -142,19 +136,17 @@ class QFlightWaypointTab(QFrame): QMessageBox.No, QMessageBox.Yes ) + original_task = self.flight.flight_type if result == QMessageBox.Yes: - # TODO: Should be buttons for both BARCAP and TARCAP. - # BARCAP and TARCAP behave differently. TARCAP arrives a few minutes - # ahead of the rest of the package and stays until the package - # departs, whereas BARCAP usually isn't part of a strike package and - # has a fixed mission time. - if task == FlightType.CAP: - if self.package.target.is_friendly(to_player=True): - task = FlightType.BARCAP - else: - task = FlightType.TARCAP self.flight.flight_type = task - self.planner.populate_flight_plan(self.flight) + try: + self.planner.populate_flight_plan(self.flight) + except PlanningError as ex: + self.flight.flight_type = original_task + logging.exception("Could not recreate flight") + QMessageBox.critical( + self, "Could not recreate flight", str(ex), QMessageBox.Ok + ) self.flight_waypoint_list.update_list() self.on_change() diff --git a/theater/__init__.py b/theater/__init__.py index 8fb31434..c5b83a16 100644 --- a/theater/__init__.py +++ b/theater/__init__.py @@ -2,3 +2,4 @@ from .base import * from .conflicttheater import * from .controlpoint import * from .missiontarget import MissionTarget +from .theatergroundobject import SamGroundObject diff --git a/theater/conflicttheater.py b/theater/conflicttheater.py index 78f3c052..c0b373ce 100644 --- a/theater/conflicttheater.py +++ b/theater/conflicttheater.py @@ -18,6 +18,7 @@ from dcs.terrain import ( ) from dcs.terrain.terrain import Terrain +from gen.flights.flight import FlightType from .controlpoint import ControlPoint, MissionTarget from .landmap import Landmap, load_landmap, poly_contains @@ -354,6 +355,14 @@ class FrontLine(MissionTarget): """Returns True if the objective is in friendly territory.""" return False + def mission_types(self, for_player: bool) -> Iterator[FlightType]: + yield from [ + FlightType.CAS, + # TODO: FlightType.TROOP_TRANSPORT + # TODO: FlightType.EVAC + ] + yield from super().mission_types(for_player) + @property def position(self): """ diff --git a/theater/controlpoint.py b/theater/controlpoint.py index f7514b71..4dba0bd7 100644 --- a/theater/controlpoint.py +++ b/theater/controlpoint.py @@ -2,8 +2,8 @@ from __future__ import annotations import itertools import re -from typing import Dict, List, TYPE_CHECKING from enum import Enum +from typing import Dict, Iterator, List, TYPE_CHECKING from dcs.mapping import Point from dcs.ships import ( @@ -20,12 +20,12 @@ from .base import Base from .missiontarget import MissionTarget from .theatergroundobject import ( BaseDefenseGroundObject, - SamGroundObject, TheaterGroundObject, ) if TYPE_CHECKING: from game import Game + from gen.flights.flight import FlightType class ControlPointType(Enum): @@ -237,3 +237,28 @@ class ControlPoint(MissionTarget): from .start_generator import BaseDefenseGenerator self.base_defenses = [] BaseDefenseGenerator(game, self).generate() + + def mission_types(self, for_player: bool) -> Iterator[FlightType]: + yield from super().mission_types(for_player) + if self.is_friendly(for_player): + if self.is_fleet: + yield from [ + # TODO: FlightType.INTERCEPTION + # TODO: Buddy tanking for the A-4? + # TODO: Rescue chopper? + # TODO: Inter-ship logistics? + ] + else: + yield from [ + # TODO: FlightType.INTERCEPTION + # TODO: FlightType.LOGISTICS + ] + else: + if self.is_fleet: + yield from [ + # TODO: FlightType.ANTISHIP + ] + else: + yield from [ + # TODO: FlightType.STRIKE + ] diff --git a/theater/missiontarget.py b/theater/missiontarget.py index ea9ccec8..c442fe42 100644 --- a/theater/missiontarget.py +++ b/theater/missiontarget.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import Iterator, TYPE_CHECKING + from dcs.mapping import Point +if TYPE_CHECKING: + from gen.flights.flight import FlightType + class MissionTarget: def __init__(self, name: str, position: Point) -> None: @@ -21,3 +26,18 @@ class MissionTarget: def is_friendly(self, to_player: bool) -> bool: """Returns True if the objective is in friendly territory.""" raise NotImplementedError + + def mission_types(self, for_player: bool) -> Iterator[FlightType]: + from gen.flights.flight import FlightType + if self.is_friendly(for_player): + yield FlightType.BARCAP + else: + yield from [ + FlightType.ESCORT, + FlightType.TARCAP, + FlightType.SEAD, + FlightType.SWEEP, + # TODO: FlightType.ELINT, + # TODO: FlightType.EWAR, + # TODO: FlightType.RECON, + ] diff --git a/theater/theatergroundobject.py b/theater/theatergroundobject.py index 293c392f..ff3840cf 100644 --- a/theater/theatergroundobject.py +++ b/theater/theatergroundobject.py @@ -1,7 +1,7 @@ from __future__ import annotations import itertools -from typing import List, TYPE_CHECKING +from typing import Iterator, List, TYPE_CHECKING from dcs.mapping import Point from dcs.unit import Unit @@ -9,6 +9,8 @@ from dcs.unitgroup import Group if TYPE_CHECKING: from .controlpoint import ControlPoint + from gen.flights.flight import FlightType + from .missiontarget import MissionTarget NAME_BY_CATEGORY = { @@ -114,7 +116,18 @@ class TheaterGroundObject(MissionTarget): return "BLUE" if self.control_point.captured else "RED" def is_friendly(self, to_player: bool) -> bool: - return not self.control_point.is_friendly(to_player) + return self.control_point.is_friendly(to_player) + + def mission_types(self, for_player: bool) -> Iterator[FlightType]: + from gen.flights.flight import FlightType + if self.is_friendly(for_player): + yield from [ + # TODO: FlightType.LOGISTICS + # TODO: FlightType.TROOP_TRANSPORT + ] + else: + yield FlightType.STRIKE + yield from super().mission_types(for_player) class BuildingGroundObject(TheaterGroundObject): @@ -240,6 +253,12 @@ class SamGroundObject(BaseDefenseGroundObject): else: return super().group_name + def mission_types(self, for_player: bool) -> Iterator[FlightType]: + from gen.flights.flight import FlightType + if not self.is_friendly(for_player): + yield FlightType.DEAD + yield from super().mission_types(for_player) + class EwrGroundObject(BaseDefenseGroundObject): def __init__(self, name: str, group_id: int, position: Point,