Make TOT waypoints non-optional for flight plans.

Flights without a meaningful TOT make the code around startup time (and
other scheduling behaviors) unnecessarily complicated because they have
to handle unpredictable flight plans. We can simplify this by requiring
that all flight plans have a waypoint associated with their TOT. For
custom flight plans, we can just fall back to the takeoff waypoint. For
RTB flight plans (which are only synthetic flight plans injected for
aborted flights), we can use the abort point.

This also means that all flight plans now have, at the very least, a
departure waypoint. Deleting this waypoint is invalid even for custom
flights, so that's no a problem.
This commit is contained in:
Dan Albert 2022-09-02 20:54:41 -07:00
parent 4f1e3da70a
commit 452848fd2a
11 changed files with 33 additions and 24 deletions

View File

@ -8,6 +8,7 @@ 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 .flightplans.planningerror import PlanningError
from .flightplans.waypointbuilder import WaypointBuilder
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
@ -89,7 +90,11 @@ class Flight(SidcDescribable):
from .flightplans.custom import CustomFlightPlan, CustomLayout from .flightplans.custom import CustomFlightPlan, CustomLayout
self.flight_plan: FlightPlan[Any] = CustomFlightPlan( self.flight_plan: FlightPlan[Any] = CustomFlightPlan(
self, CustomLayout(custom_waypoints=[]) self,
CustomLayout(
departure=WaypointBuilder(self, self.coalition).takeoff(self.departure),
custom_waypoints=[],
),
) )
def __getstate__(self) -> dict[str, Any]: def __getstate__(self) -> dict[str, Any]:

View File

@ -41,7 +41,7 @@ class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout]):
return Builder return Builder
@property @property
def tot_waypoint(self) -> FlightWaypoint | None: def tot_waypoint(self) -> FlightWaypoint:
return self.layout.drop_off return self.layout.drop_off
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:

View File

@ -47,7 +47,7 @@ class AirliftFlightPlan(StandardFlightPlan[AirliftLayout]):
return Builder return Builder
@property @property
def tot_waypoint(self) -> FlightWaypoint | None: def tot_waypoint(self) -> FlightWaypoint:
return self.layout.drop_off return self.layout.drop_off
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:

View File

@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Type
from .flightplan import FlightPlan, Layout from .flightplan import FlightPlan, Layout
from .ibuilder import IBuilder from .ibuilder import IBuilder
from .waypointbuilder import WaypointBuilder
from ..flightwaypointtype import FlightWaypointType from ..flightwaypointtype import FlightWaypointType
if TYPE_CHECKING: if TYPE_CHECKING:
@ -18,6 +19,7 @@ class CustomLayout(Layout):
custom_waypoints: list[FlightWaypoint] custom_waypoints: list[FlightWaypoint]
def iter_waypoints(self) -> Iterator[FlightWaypoint]: def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
yield from self.custom_waypoints yield from self.custom_waypoints
@ -27,7 +29,7 @@ class CustomFlightPlan(FlightPlan[CustomLayout]):
return Builder return Builder
@property @property
def tot_waypoint(self) -> FlightWaypoint | None: def tot_waypoint(self) -> FlightWaypoint:
target_types = ( target_types = (
FlightWaypointType.PATROL_TRACK, FlightWaypointType.PATROL_TRACK,
FlightWaypointType.TARGET_GROUP_LOC, FlightWaypointType.TARGET_GROUP_LOC,
@ -37,7 +39,7 @@ class CustomFlightPlan(FlightPlan[CustomLayout]):
for waypoint in self.waypoints: for waypoint in self.waypoints:
if waypoint in target_types: if waypoint in target_types:
return waypoint return waypoint
return None return self.layout.departure
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
if waypoint == self.tot_waypoint: if waypoint == self.tot_waypoint:
@ -54,7 +56,8 @@ class CustomFlightPlan(FlightPlan[CustomLayout]):
class Builder(IBuilder[CustomFlightPlan, CustomLayout]): class Builder(IBuilder[CustomFlightPlan, CustomLayout]):
def layout(self) -> CustomLayout: def layout(self) -> CustomLayout:
return CustomLayout([]) builder = WaypointBuilder(self.flight, self.coalition)
return CustomLayout(builder.takeoff(self.flight.departure), [])
def build(self) -> CustomFlightPlan: def build(self) -> CustomFlightPlan:
return CustomFlightPlan(self.flight, self.layout()) return CustomFlightPlan(self.flight, self.layout())

View File

@ -34,7 +34,7 @@ class FerryFlightPlan(StandardFlightPlan[FerryLayout]):
return Builder return Builder
@property @property
def tot_waypoint(self) -> FlightWaypoint | None: def tot_waypoint(self) -> FlightWaypoint:
return self.layout.arrival return self.layout.arrival
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:

View File

@ -10,6 +10,7 @@ from __future__ import annotations
import math import math
from abc import ABC from abc import ABC
from collections.abc import Iterator from collections.abc import Iterator
from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from functools import cached_property from functools import cached_property
from typing import Any, Generic, TYPE_CHECKING, TypeGuard, TypeVar from typing import Any, Generic, TYPE_CHECKING, TypeGuard, TypeVar
@ -40,7 +41,10 @@ INGRESS_TYPES = {
} }
@dataclass(frozen=True)
class Layout(ABC): class Layout(ABC):
departure: FlightWaypoint
@property @property
def waypoints(self) -> list[FlightWaypoint]: def waypoints(self) -> list[FlightWaypoint]:
"""A list of all waypoints in the flight plan, in order.""" """A list of all waypoints in the flight plan, in order."""
@ -135,7 +139,7 @@ class FlightPlan(ABC, Generic[LayoutT]):
return self.flight.unit_type.fuel_consumption.cruise return self.flight.unit_type.fuel_consumption.cruise
@property @property
def tot_waypoint(self) -> FlightWaypoint | None: def tot_waypoint(self) -> FlightWaypoint:
"""The waypoint that is associated with the package TOT, or None. """The waypoint that is associated with the package TOT, or None.
Note that the only flight plans that should have no target waypoints are Note that the only flight plans that should have no target waypoints are
@ -246,19 +250,12 @@ class FlightPlan(ABC, Generic[LayoutT]):
if waypoint == end: if waypoint == end:
return return
def takeoff_time(self) -> timedelta | None: def takeoff_time(self) -> timedelta:
tot_waypoint = self.tot_waypoint return self.tot - self._travel_time_to_waypoint(self.tot_waypoint)
if tot_waypoint is None:
return None
return self.tot - self._travel_time_to_waypoint(tot_waypoint)
def startup_time(self) -> timedelta | None: def startup_time(self) -> timedelta | None:
takeoff_time = self.takeoff_time()
if takeoff_time is None:
return None
start_time: timedelta = ( start_time: timedelta = (
takeoff_time - self.estimate_startup() - self.estimate_ground_ops() self.takeoff_time() - self.estimate_startup() - self.estimate_ground_ops()
) )
# In case FP math has given us some barely below zero time, round to # In case FP math has given us some barely below zero time, round to

View File

@ -85,7 +85,7 @@ class PatrollingFlightPlan(StandardFlightPlan[LayoutT], ABC):
return {self.layout.patrol_start, self.layout.patrol_end} return {self.layout.patrol_start, self.layout.patrol_end}
@property @property
def tot_waypoint(self) -> FlightWaypoint | None: def tot_waypoint(self) -> FlightWaypoint:
return self.layout.patrol_start return self.layout.patrol_start
@property @property

View File

@ -40,8 +40,8 @@ class RtbFlightPlan(StandardFlightPlan[RtbLayout]):
return 1 return 1
@property @property
def tot_waypoint(self) -> FlightWaypoint | None: def tot_waypoint(self) -> FlightWaypoint:
return None return self.layout.abort_location
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
return None return None

View File

@ -12,7 +12,6 @@ if TYPE_CHECKING:
@dataclass(frozen=True) @dataclass(frozen=True)
class StandardLayout(Layout, ABC): class StandardLayout(Layout, ABC):
departure: FlightWaypoint
arrival: FlightWaypoint arrival: FlightWaypoint
divert: FlightWaypoint | None divert: FlightWaypoint | None
bullseye: FlightWaypoint bullseye: FlightWaypoint

View File

@ -54,7 +54,7 @@ class SweepFlightPlan(LoiterFlightPlan):
return {self.layout.sweep_end} return {self.layout.sweep_end}
@property @property
def tot_waypoint(self) -> FlightWaypoint | None: def tot_waypoint(self) -> FlightWaypoint:
return self.layout.sweep_end return self.layout.sweep_end
@property @property

View File

@ -148,7 +148,12 @@ class QFlightWaypointTab(QFrame):
if not isinstance(self.flight.flight_plan, CustomFlightPlan): if not isinstance(self.flight.flight_plan, CustomFlightPlan):
self.flight.flight_plan = CustomFlightPlan( self.flight.flight_plan = CustomFlightPlan(
self.flight, self.flight,
CustomLayout(custom_waypoints=self.flight.flight_plan.waypoints), CustomLayout(
departure=WaypointBuilder(self.flight, self.coalition).takeoff(
self.flight.departure
),
custom_waypoints=self.flight.flight_plan.waypoints[1:],
),
) )
def confirm_recreate(self, task: FlightType) -> None: def confirm_recreate(self, task: FlightType) -> None: