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,