Move FlightPlan creation into Flight.

For now this is just a callsite cleanup. Later, this will make it easier
to separate unscheduled and scheduled flights into different classes
without complicating the layout/scheduling.
This commit is contained in:
Dan Albert 2022-08-21 19:25:58 -07:00
parent 7a45391c22
commit a101527906
9 changed files with 59 additions and 114 deletions

View File

@ -7,10 +7,12 @@ from typing import Any, List, Optional, TYPE_CHECKING
from dcs import Point from dcs import Point
from dcs.planes import C_101CC, C_101EB, Su_33 from dcs.planes import C_101CC, C_101EB, Su_33
from .flightplans.planningerror import PlanningError
from .flightroster import FlightRoster from .flightroster import FlightRoster
from .flightstate import FlightState, Navigating, Uninitialized from .flightstate import FlightState, Navigating, Uninitialized
from .flightstate.killed import Killed from .flightstate.killed import Killed
from .loadouts import Loadout from .loadouts import Loadout
from .packagewaypoints import PackageWaypoints
from ..sidc import ( from ..sidc import (
Entity, Entity,
SidcDescribable, SidcDescribable,
@ -81,9 +83,9 @@ class Flight(SidcDescribable):
# Used for simulating the travel to first contact. # Used for simulating the travel to first contact.
self.state: FlightState = Uninitialized(self, squadron.settings) self.state: FlightState = Uninitialized(self, squadron.settings)
# Will be replaced with a more appropriate FlightPlan by # Will be replaced with a more appropriate FlightPlan later, but start with a
# FlightPlanBuilder, but an empty flight plan the flight begins with an # cheaply constructed one since adding more flights to the package may affect
# empty flight plan. # the optimal layout.
from .flightplans.custom import CustomFlightPlan, CustomLayout from .flightplans.custom import CustomFlightPlan, CustomLayout
self.flight_plan: FlightPlan[Any] = CustomFlightPlan( self.flight_plan: FlightPlan[Any] = CustomFlightPlan(
@ -243,3 +245,24 @@ class Flight(SidcDescribable):
for pilot in self.roster.pilots: for pilot in self.roster.pilots:
if pilot is not None: if pilot is not None:
results.kill_pilot(self, pilot) results.kill_pilot(self, pilot)
def recreate_flight_plan(self) -> None:
self.flight_plan = self._make_flight_plan()
def _make_flight_plan(self) -> FlightPlan[Any]:
from game.navmesh import NavMeshError
from .flightplans.flightplanbuildertypes import FlightPlanBuilderTypes
try:
if self.package.waypoints is None:
self.package.waypoints = PackageWaypoints.create(
self.package, self.coalition
)
builder = FlightPlanBuilderTypes.for_flight(self)(self)
return builder.build()
except NavMeshError as ex:
color = "blue" if self.squadron.player else "red"
raise PlanningError(
f"Could not plan {color} {self.flight_type.value} from "
f"{self.departure} to {self.package.target}"
) from ex

View File

@ -13,7 +13,6 @@ from .cas import CasFlightPlan
from .dead import DeadFlightPlan from .dead import DeadFlightPlan
from .escort import EscortFlightPlan from .escort import EscortFlightPlan
from .ferry import FerryFlightPlan from .ferry import FerryFlightPlan
from .flightplan import FlightPlan
from .ibuilder import IBuilder from .ibuilder import IBuilder
from .ocaaircraft import OcaAircraftFlightPlan from .ocaaircraft import OcaAircraftFlightPlan
from .ocarunway import OcaRunwayFlightPlan from .ocarunway import OcaRunwayFlightPlan
@ -24,59 +23,18 @@ from .strike import StrikeFlightPlan
from .sweep import SweepFlightPlan from .sweep import SweepFlightPlan
from .tarcap import TarCapFlightPlan from .tarcap import TarCapFlightPlan
from .theaterrefueling import TheaterRefuelingFlightPlan from .theaterrefueling import TheaterRefuelingFlightPlan
from ..packagewaypoints import PackageWaypoints
if TYPE_CHECKING: if TYPE_CHECKING:
from game.ato import Flight, Package from game.ato import Flight
from game.coalition import Coalition from game.theater import FrontLine
from game.theater import ConflictTheater, FrontLine
class FlightPlanBuilder: class FlightPlanBuilderTypes:
"""Generates flight plans for flights.""" @staticmethod
def for_flight(flight: Flight) -> Type[IBuilder[Any, Any]]:
def __init__(
self, package: Package, coalition: Coalition, theater: ConflictTheater
) -> None:
# TODO: Plan similar altitudes for the in-country leg of the mission.
# Waypoint altitudes for a given flight *shouldn't* differ too much
# between the join and split points, so we don't need speeds for each
# leg individually since they should all be fairly similar. This doesn't
# hold too well right now since nothing is stopping each waypoint from
# jumping 20k feet each time, but that's a huge waste of energy we
# should be avoiding anyway.
self.package = package
self.coalition = coalition
self.theater = theater
@property
def is_player(self) -> bool:
return self.coalition.player
def populate_flight_plan(self, flight: Flight) -> None:
"""Creates a default flight plan for the given mission."""
if flight not in self.package.flights:
raise RuntimeError("Flight must be a part of the package")
from game.navmesh import NavMeshError
try:
if self.package.waypoints is None:
self.package.waypoints = PackageWaypoints.create(
self.package, self.coalition
)
flight.flight_plan = self.generate_flight_plan(flight)
except NavMeshError as ex:
color = "blue" if self.is_player else "red"
raise PlanningError(
f"Could not plan {color} {flight.flight_type.value} from "
f"{flight.departure} to {flight.package.target}"
) from ex
def builder_type(self, flight: Flight) -> Type[IBuilder[Any, Any]]:
if flight.flight_type is FlightType.REFUELING: if flight.flight_type is FlightType.REFUELING:
if self.package.target.is_friendly(self.is_player) or isinstance( if flight.package.target.is_friendly(flight.squadron.player) or isinstance(
self.package.target, FrontLine flight.package.target, FrontLine
): ):
return TheaterRefuelingFlightPlan.builder_type() return TheaterRefuelingFlightPlan.builder_type()
return PackageRefuelingFlightPlan.builder_type() return PackageRefuelingFlightPlan.builder_type()
@ -106,6 +64,3 @@ class FlightPlanBuilder:
raise PlanningError( raise PlanningError(
f"{flight.flight_type} flight plan generation not implemented" f"{flight.flight_type} flight plan generation not implemented"
) from ex ) from ex
def generate_flight_plan(self, flight: Flight) -> FlightPlan[Any]:
return self.builder_type(flight)(flight).build()

View File

@ -6,7 +6,6 @@ from typing import Dict, Iterable, Optional, Set, TYPE_CHECKING
from game.ato.airtaaskingorder import AirTaskingOrder from game.ato.airtaaskingorder import AirTaskingOrder
from game.ato.closestairfields import ObjectiveDistanceCache from game.ato.closestairfields import ObjectiveDistanceCache
from game.ato.flightplans.flightplanbuilder import FlightPlanBuilder
from game.ato.flighttype import FlightType from game.ato.flighttype import FlightType
from game.ato.package import Package from game.ato.package import Package
from game.commander.missionproposals import EscortType, ProposedFlight, ProposedMission from game.commander.missionproposals import EscortType, ProposedFlight, ProposedMission
@ -190,12 +189,9 @@ class PackageFulfiller:
# flights that will rendezvous with their package will be affected by # flights that will rendezvous with their package will be affected by
# the other flights in the package. Escorts will not be able to # the other flights in the package. Escorts will not be able to
# contribute to this. # contribute to this.
flight_plan_builder = FlightPlanBuilder(
builder.package, self.coalition, self.theater
)
for flight in builder.package.flights: for flight in builder.package.flights:
with tracer.trace("Flight plan population"): with tracer.trace("Flight plan population"):
flight_plan_builder.populate_flight_plan(flight) flight.recreate_flight_plan()
needed_escorts = self.check_needed_escorts(builder) needed_escorts = self.check_needed_escorts(builder)
for escort in escorts: for escort in escorts:
@ -221,7 +217,7 @@ class PackageFulfiller:
for flight in package.flights: for flight in package.flights:
if not flight.flight_plan.waypoints: if not flight.flight_plan.waypoints:
with tracer.trace("Flight plan population"): with tracer.trace("Flight plan population"):
flight_plan_builder.populate_flight_plan(flight) flight.recreate_flight_plan()
if package.has_players and self.player_missions_asap: if package.has_players and self.player_missions_asap:
package.auto_asap = True package.auto_asap = True

View File

@ -11,7 +11,6 @@ from faker import Faker
from game.ato import Flight, FlightType, Package from game.ato import Flight, FlightType, Package
from game.settings import AutoAtoBehavior, Settings from game.settings import AutoAtoBehavior, Settings
from .pilot import Pilot, PilotStatus from .pilot import Pilot, PilotStatus
from ..ato.flightplans.flightplanbuilder import FlightPlanBuilder
from ..db.database import Database from ..db.database import Database
from ..utils import meters from ..utils import meters
@ -19,7 +18,7 @@ if TYPE_CHECKING:
from game import Game from game import Game
from game.coalition import Coalition from game.coalition import Coalition
from game.dcs.aircrafttype import AircraftType from game.dcs.aircrafttype import AircraftType
from game.theater import ControlPoint, ConflictTheater, MissionTarget from game.theater import ControlPoint, MissionTarget
from .operatingbases import OperatingBases from .operatingbases import OperatingBases
from .squadrondef import SquadronDef from .squadrondef import SquadronDef
@ -335,9 +334,7 @@ class Squadron:
def arrival(self) -> ControlPoint: def arrival(self) -> ControlPoint:
return self.location if self.destination is None else self.destination return self.location if self.destination is None else self.destination
def plan_relocation( def plan_relocation(self, destination: ControlPoint) -> None:
self, destination: ControlPoint, theater: ConflictTheater
) -> None:
if destination == self.location: if destination == self.location:
logging.warning( logging.warning(
f"Attempted to plan relocation of {self} to current location " f"Attempted to plan relocation of {self} to current location "
@ -356,7 +353,7 @@ class Squadron:
if not destination.can_operate(self.aircraft): if not destination.can_operate(self.aircraft):
raise RuntimeError(f"{self} cannot operate at {destination}.") raise RuntimeError(f"{self} cannot operate at {destination}.")
self.destination = destination self.destination = destination
self.replan_ferry_flights(theater) self.replan_ferry_flights()
def cancel_relocation(self) -> None: def cancel_relocation(self) -> None:
if self.destination is None: if self.destination is None:
@ -371,9 +368,9 @@ class Squadron:
self.destination = None self.destination = None
self.cancel_ferry_flights() self.cancel_ferry_flights()
def replan_ferry_flights(self, theater: ConflictTheater) -> None: def replan_ferry_flights(self) -> None:
self.cancel_ferry_flights() self.cancel_ferry_flights()
self.plan_ferry_flights(theater) self.plan_ferry_flights()
def cancel_ferry_flights(self) -> None: def cancel_ferry_flights(self) -> None:
for package in self.coalition.ato.packages: for package in self.coalition.ato.packages:
@ -384,7 +381,7 @@ class Squadron:
if not package.flights: if not package.flights:
self.coalition.ato.remove_package(package) self.coalition.ato.remove_package(package)
def plan_ferry_flights(self, theater: ConflictTheater) -> None: def plan_ferry_flights(self) -> None:
if self.destination is None: if self.destination is None:
raise RuntimeError( raise RuntimeError(
f"Cannot plan ferry flights for {self} because there is no destination." f"Cannot plan ferry flights for {self} because there is no destination."
@ -394,17 +391,14 @@ class Squadron:
return return
package = Package(self.destination, self.flight_db) package = Package(self.destination, self.flight_db)
builder = FlightPlanBuilder(package, self.coalition, theater)
while remaining: while remaining:
size = min(remaining, self.aircraft.max_group_size) size = min(remaining, self.aircraft.max_group_size)
self.plan_ferry_flight(builder, package, size) self.plan_ferry_flight(package, size)
remaining -= size remaining -= size
package.set_tot_asap() package.set_tot_asap()
self.coalition.ato.add_package(package) self.coalition.ato.add_package(package)
def plan_ferry_flight( def plan_ferry_flight(self, package: Package, size: int) -> None:
self, builder: FlightPlanBuilder, package: Package, size: int
) -> None:
start_type = self.location.required_aircraft_start_type start_type = self.location.required_aircraft_start_type
if start_type is None: if start_type is None:
start_type = self.settings.default_start_type start_type = self.settings.default_start_type
@ -419,7 +413,7 @@ class Squadron:
divert=None, divert=None,
) )
package.add_flight(flight) package.add_flight(flight)
builder.populate_flight_plan(flight) flight.recreate_flight_plan()
@classmethod @classmethod
def create_from( def create_from(

View File

@ -43,7 +43,6 @@ from dcs.mapping import Point
from game.ato.ai_flight_planner_db import aircraft_for_task from game.ato.ai_flight_planner_db import aircraft_for_task
from game.ato.closestairfields import ObjectiveDistanceCache from game.ato.closestairfields import ObjectiveDistanceCache
from game.ato.flight import Flight from game.ato.flight import Flight
from game.ato.flightplans.flightplanbuilder import FlightPlanBuilder
from game.ato.flighttype import FlightType from game.ato.flighttype import FlightType
from game.ato.package import Package from game.ato.package import Package
from game.dcs.aircrafttype import AircraftType from game.dcs.aircrafttype import AircraftType
@ -364,10 +363,7 @@ class AirliftPlanner:
transfer.transport = transport transfer.transport = transport
self.package.add_flight(flight) self.package.add_flight(flight)
planner = FlightPlanBuilder( flight.recreate_flight_plan()
self.package, self.game.coalition_for(self.for_player), self.game.theater
)
planner.populate_flight_plan(flight)
return flight_size return flight_size

View File

@ -1,30 +1,25 @@
import logging import logging
from typing import Callable, Iterator, Optional from typing import Callable, Iterator, Optional
from PySide2.QtCore import ( from PySide2.QtCore import QItemSelection, QItemSelectionModel, QModelIndex, Qt
QItemSelectionModel,
QModelIndex,
Qt,
QItemSelection,
)
from PySide2.QtWidgets import ( from PySide2.QtWidgets import (
QAbstractItemView, QAbstractItemView,
QDialog,
QListView,
QVBoxLayout,
QPushButton,
QHBoxLayout,
QLabel,
QCheckBox, QCheckBox,
QComboBox, QComboBox,
QDialog,
QHBoxLayout,
QLabel,
QListView,
QPushButton,
QVBoxLayout,
) )
from game.squadrons import Pilot, Squadron
from game.theater import ControlPoint, ConflictTheater
from game.ato.flighttype import FlightType from game.ato.flighttype import FlightType
from game.squadrons import Pilot, Squadron
from game.theater import ConflictTheater, ControlPoint
from qt_ui.delegates import TwoColumnRowDelegate from qt_ui.delegates import TwoColumnRowDelegate
from qt_ui.errorreporter import report_errors from qt_ui.errorreporter import report_errors
from qt_ui.models import SquadronModel, AtoModel from qt_ui.models import AtoModel, SquadronModel
class PilotDelegate(TwoColumnRowDelegate): class PilotDelegate(TwoColumnRowDelegate):
@ -144,7 +139,6 @@ class SquadronDialog(QDialog):
super().__init__(parent) super().__init__(parent)
self.ato_model = ato_model self.ato_model = ato_model
self.squadron_model = squadron_model self.squadron_model = squadron_model
self.theater = theater
self.setMinimumSize(1000, 440) self.setMinimumSize(1000, 440)
self.setWindowTitle(str(squadron_model.squadron)) self.setWindowTitle(str(squadron_model.squadron))
@ -200,7 +194,7 @@ class SquadronDialog(QDialog):
if destination is None: if destination is None:
self.squadron.cancel_relocation() self.squadron.cancel_relocation()
else: else:
self.squadron.plan_relocation(destination, self.theater) self.squadron.plan_relocation(destination)
self.ato_model.replace_from_game(player=True) self.ato_model.replace_from_game(player=True)
def check_disabled_button_states( def check_disabled_button_states(

View File

@ -16,7 +16,6 @@ from PySide2.QtWidgets import (
) )
from game.ato.flight import Flight from game.ato.flight import Flight
from game.ato.flightplans.flightplanbuilder import FlightPlanBuilder
from game.ato.flightplans.planningerror import PlanningError from game.ato.flightplans.planningerror import PlanningError
from game.ato.package import Package from game.ato.package import Package
from game.game import Game from game.game import Game
@ -181,11 +180,8 @@ class QPackageDialog(QDialog):
def add_flight(self, flight: Flight) -> None: def add_flight(self, flight: Flight) -> None:
"""Adds the new flight to the package.""" """Adds the new flight to the package."""
self.package_model.add_flight(flight) self.package_model.add_flight(flight)
planner = FlightPlanBuilder(
self.package_model.package, self.game.blue, self.game.theater
)
try: try:
planner.populate_flight_plan(flight) flight.recreate_flight_plan()
self.package_model.update_tot() self.package_model.update_tot()
EventStream.put_nowait(GameUpdateEvents().new_flight(flight)) EventStream.put_nowait(GameUpdateEvents().new_flight(flight))
except PlanningError as ex: except PlanningError as ex:

View File

@ -4,7 +4,6 @@ from PySide2.QtWidgets import QGroupBox, QLabel, QMessageBox, QVBoxLayout
from game import Game from game import Game
from game.ato.flight import Flight from game.ato.flight import Flight
from game.ato.flightplans.flightplanbuilder import FlightPlanBuilder
from game.ato.flightplans.planningerror import PlanningError from game.ato.flightplans.planningerror import PlanningError
from game.ato.traveltime import TotEstimator from game.ato.traveltime import TotEstimator
from qt_ui.models import PackageModel from qt_ui.models import PackageModel
@ -71,16 +70,10 @@ class FlightAirfieldDisplay(QGroupBox):
self.flight.divert = divert self.flight.divert = divert
try: try:
self.update_flight_plan() self.flight.recreate_flight_plan()
except PlanningError as ex: except PlanningError as ex:
self.flight.divert = old_divert self.flight.divert = old_divert
logging.exception("Could not change divert airfield") logging.exception("Could not change divert airfield")
QMessageBox.critical( QMessageBox.critical(
self, "Could not update flight plan", str(ex), QMessageBox.Ok self, "Could not update flight plan", str(ex), QMessageBox.Ok
) )
def update_flight_plan(self) -> None:
planner = FlightPlanBuilder(
self.package_model.package, self.game.blue, self.game.theater
)
planner.populate_flight_plan(self.flight)

View File

@ -14,7 +14,6 @@ from PySide2.QtWidgets import (
from game import Game from game import Game
from game.ato.flight import Flight from game.ato.flight import Flight
from game.ato.flightplans.custom import CustomFlightPlan, CustomLayout from game.ato.flightplans.custom import CustomFlightPlan, CustomLayout
from game.ato.flightplans.flightplanbuilder import FlightPlanBuilder
from game.ato.flightplans.formationattack import FormationAttackFlightPlan from game.ato.flightplans.formationattack import FormationAttackFlightPlan
from game.ato.flightplans.planningerror import PlanningError from game.ato.flightplans.planningerror import PlanningError
from game.ato.flightplans.waypointbuilder import WaypointBuilder from game.ato.flightplans.waypointbuilder import WaypointBuilder
@ -38,7 +37,6 @@ class QFlightWaypointTab(QFrame):
self.game = game self.game = game
self.package = package self.package = package
self.flight = flight self.flight = flight
self.planner = FlightPlanBuilder(package, game.blue, game.theater)
self.flight_waypoint_list: Optional[QFlightWaypointList] = None self.flight_waypoint_list: Optional[QFlightWaypointList] = None
self.rtb_waypoint: Optional[QPushButton] = None self.rtb_waypoint: Optional[QPushButton] = None
@ -168,7 +166,7 @@ class QFlightWaypointTab(QFrame):
if result == QMessageBox.Yes: if result == QMessageBox.Yes:
self.flight.flight_type = task self.flight.flight_type = task
try: try:
self.planner.populate_flight_plan(self.flight) self.flight.recreate_flight_plan()
except PlanningError as ex: except PlanningError as ex:
self.flight.flight_type = original_task self.flight.flight_type = original_task
logging.exception("Could not recreate flight") logging.exception("Could not recreate flight")