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.planes import C_101CC, C_101EB, Su_33
from .flightplans.planningerror import PlanningError
from .flightroster import FlightRoster
from .flightstate import FlightState, Navigating, Uninitialized
from .flightstate.killed import Killed
from .loadouts import Loadout
from .packagewaypoints import PackageWaypoints
from ..sidc import (
Entity,
SidcDescribable,
@ -81,9 +83,9 @@ class Flight(SidcDescribable):
# Used for simulating the travel to first contact.
self.state: FlightState = Uninitialized(self, squadron.settings)
# Will be replaced with a more appropriate FlightPlan by
# FlightPlanBuilder, but an empty flight plan the flight begins with an
# empty flight plan.
# Will be replaced with a more appropriate FlightPlan later, but start with a
# cheaply constructed one since adding more flights to the package may affect
# the optimal layout.
from .flightplans.custom import CustomFlightPlan, CustomLayout
self.flight_plan: FlightPlan[Any] = CustomFlightPlan(
@ -243,3 +245,24 @@ class Flight(SidcDescribable):
for pilot in self.roster.pilots:
if pilot is not None:
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 .escort import EscortFlightPlan
from .ferry import FerryFlightPlan
from .flightplan import FlightPlan
from .ibuilder import IBuilder
from .ocaaircraft import OcaAircraftFlightPlan
from .ocarunway import OcaRunwayFlightPlan
@ -24,59 +23,18 @@ from .strike import StrikeFlightPlan
from .sweep import SweepFlightPlan
from .tarcap import TarCapFlightPlan
from .theaterrefueling import TheaterRefuelingFlightPlan
from ..packagewaypoints import PackageWaypoints
if TYPE_CHECKING:
from game.ato import Flight, Package
from game.coalition import Coalition
from game.theater import ConflictTheater, FrontLine
from game.ato import Flight
from game.theater import FrontLine
class FlightPlanBuilder:
"""Generates flight plans for flights."""
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]]:
class FlightPlanBuilderTypes:
@staticmethod
def for_flight(flight: Flight) -> Type[IBuilder[Any, Any]]:
if flight.flight_type is FlightType.REFUELING:
if self.package.target.is_friendly(self.is_player) or isinstance(
self.package.target, FrontLine
if flight.package.target.is_friendly(flight.squadron.player) or isinstance(
flight.package.target, FrontLine
):
return TheaterRefuelingFlightPlan.builder_type()
return PackageRefuelingFlightPlan.builder_type()
@ -106,6 +64,3 @@ class FlightPlanBuilder:
raise PlanningError(
f"{flight.flight_type} flight plan generation not implemented"
) 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.closestairfields import ObjectiveDistanceCache
from game.ato.flightplans.flightplanbuilder import FlightPlanBuilder
from game.ato.flighttype import FlightType
from game.ato.package import Package
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
# the other flights in the package. Escorts will not be able to
# contribute to this.
flight_plan_builder = FlightPlanBuilder(
builder.package, self.coalition, self.theater
)
for flight in builder.package.flights:
with tracer.trace("Flight plan population"):
flight_plan_builder.populate_flight_plan(flight)
flight.recreate_flight_plan()
needed_escorts = self.check_needed_escorts(builder)
for escort in escorts:
@ -221,7 +217,7 @@ class PackageFulfiller:
for flight in package.flights:
if not flight.flight_plan.waypoints:
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:
package.auto_asap = True

View File

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

View File

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

View File

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

View File

@ -4,7 +4,6 @@ from PySide2.QtWidgets import QGroupBox, QLabel, QMessageBox, QVBoxLayout
from game import Game
from game.ato.flight import Flight
from game.ato.flightplans.flightplanbuilder import FlightPlanBuilder
from game.ato.flightplans.planningerror import PlanningError
from game.ato.traveltime import TotEstimator
from qt_ui.models import PackageModel
@ -71,16 +70,10 @@ class FlightAirfieldDisplay(QGroupBox):
self.flight.divert = divert
try:
self.update_flight_plan()
self.flight.recreate_flight_plan()
except PlanningError as ex:
self.flight.divert = old_divert
logging.exception("Could not change divert airfield")
QMessageBox.critical(
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.ato.flight import Flight
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.planningerror import PlanningError
from game.ato.flightplans.waypointbuilder import WaypointBuilder
@ -38,7 +37,6 @@ class QFlightWaypointTab(QFrame):
self.game = game
self.package = package
self.flight = flight
self.planner = FlightPlanBuilder(package, game.blue, game.theater)
self.flight_waypoint_list: Optional[QFlightWaypointList] = None
self.rtb_waypoint: Optional[QPushButton] = None
@ -168,7 +166,7 @@ class QFlightWaypointTab(QFrame):
if result == QMessageBox.Yes:
self.flight.flight_type = task
try:
self.planner.populate_flight_plan(self.flight)
self.flight.recreate_flight_plan()
except PlanningError as ex:
self.flight.flight_type = original_task
logging.exception("Could not recreate flight")