From 769fe12159d9fb7b46e94cc3a77db54130d05134 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Fri, 11 Mar 2022 16:00:48 -0800 Subject: [PATCH] Split flight plan layout into a separate class. During package planning we don't care about the details of the flight plan, just the layout (to check if the layout is threatened and we need escorts). Splitting these will allow us to reduce the amount of work that must be done in each loop of the planning phase, potentially caching attempted flight plans between loops. --- game/ato/flight.py | 13 +- game/ato/flightplans/aewc.py | 40 +++--- game/ato/flightplans/airlift.py | 50 ++++---- game/ato/flightplans/antiship.py | 12 +- game/ato/flightplans/bai.py | 12 +- game/ato/flightplans/barcap.py | 31 +++-- game/ato/flightplans/cas.py | 90 +++++-------- game/ato/flightplans/custom.py | 25 ++-- game/ato/flightplans/dead.py | 12 +- game/ato/flightplans/escort.py | 15 +-- game/ato/flightplans/ferry.py | 36 +++--- game/ato/flightplans/flightplan.py | 33 +++-- game/ato/flightplans/flightplanbuilder.py | 13 +- game/ato/flightplans/formation.py | 65 ++++------ game/ato/flightplans/formationattack.py | 118 +++++++----------- game/ato/flightplans/ibuilder.py | 4 +- game/ato/flightplans/loiter.py | 39 +++--- game/ato/flightplans/ocaaircraft.py | 14 ++- game/ato/flightplans/ocarunway.py | 12 +- game/ato/flightplans/packagerefueling.py | 37 +++--- game/ato/flightplans/patrolling.py | 101 +++++++-------- game/ato/flightplans/rtb.py | 37 +++--- game/ato/flightplans/sead.py | 24 ++-- game/ato/flightplans/standard.py | 33 +++-- game/ato/flightplans/strike.py | 12 +- game/ato/flightplans/sweep.py | 82 +++++------- game/ato/flightplans/tarcap.py | 91 ++++++-------- game/ato/flightplans/theaterrefueling.py | 43 ++++--- .../aircraft/waypoints/casingress.py | 2 +- game/server/flights/routes.py | 8 +- 30 files changed, 510 insertions(+), 594 deletions(-) diff --git a/game/ato/flight.py b/game/ato/flight.py index db5f5d24..29c56ec6 100644 --- a/game/ato/flight.py +++ b/game/ato/flight.py @@ -27,6 +27,7 @@ if TYPE_CHECKING: from game.squadrons import Squadron, Pilot from game.theater import ControlPoint, MissionTarget from game.transfers import TransferOrder + from .flightplans.flightplan import FlightPlan from .flighttype import FlightType from .flightwaypoint import FlightWaypoint from .package import Package @@ -84,10 +85,11 @@ class Flight(SidcDescribable): # Will be replaced with a more appropriate FlightPlan by # FlightPlanBuilder, but an empty flight plan the flight begins with an # empty flight plan. - from game.ato.flightplans.flightplan import FlightPlan - from .flightplans.custom import CustomFlightPlan + from .flightplans.custom import CustomFlightPlan, CustomLayout - self.flight_plan: FlightPlan = CustomFlightPlan(self, []) + self.flight_plan: FlightPlan[Any] = CustomFlightPlan( + self, CustomLayout(custom_waypoints=[]) + ) def __getstate__(self) -> dict[str, Any]: state = self.__dict__.copy() @@ -196,9 +198,8 @@ class Flight(SidcDescribable): def abort(self) -> None: from .flightplans.rtb import RtbFlightPlan - self.flight_plan = RtbFlightPlan.builder_type()( - self, self.coalition.game.theater - ).build() + layout = RtbFlightPlan.builder_type()(self, self.coalition.game.theater).build() + self.flight_plan = RtbFlightPlan(self, layout) self.set_state( Navigating( diff --git a/game/ato/flightplans/aewc.py b/game/ato/flightplans/aewc.py index 82b9b659..3f78d680 100644 --- a/game/ato/flightplans/aewc.py +++ b/game/ato/flightplans/aewc.py @@ -4,17 +4,15 @@ from datetime import timedelta from typing import Type from game.ato.flightplans.ibuilder import IBuilder -from game.ato.flightplans.patrolling import PatrollingFlightPlan +from game.ato.flightplans.patrolling import PatrollingFlightPlan, PatrollingLayout from game.ato.flightplans.waypointbuilder import WaypointBuilder -from game.utils import Heading, feet, knots, meters, nautical_miles +from game.utils import Distance, Heading, Speed, feet, knots, meters, nautical_miles class Builder(IBuilder): - def build(self) -> AewcFlightPlan: + def build(self) -> PatrollingLayout: racetrack_half_distance = nautical_miles(30).meters - patrol_duration = timedelta(hours=4) - location = self.package.target closest_boundary = self.threat_zones.closest_boundary(location.position) @@ -52,15 +50,9 @@ class Builder(IBuilder): else: altitude = feet(25000) - if self.flight.unit_type.preferred_patrol_speed(altitude) is not None: - speed = self.flight.unit_type.preferred_patrol_speed(altitude) - else: - speed = knots(390) - racetrack = builder.race_track(racetrack_start, racetrack_end, altitude) - return AewcFlightPlan( - flight=self.flight, + return PatrollingLayout( departure=builder.takeoff(self.flight.departure), nav_to=builder.nav_path( self.flight.departure.position, racetrack_start, altitude @@ -73,15 +65,27 @@ class Builder(IBuilder): arrival=builder.land(self.flight.arrival), divert=builder.divert(self.flight.divert), bullseye=builder.bullseye(), - patrol_duration=patrol_duration, - patrol_speed=speed, - # TODO: Factor out a common base of the combat and non-combat race-tracks. - # No harm in setting this, but we ought to clean up a bit. - engagement_distance=meters(0), ) -class AewcFlightPlan(PatrollingFlightPlan): +class AewcFlightPlan(PatrollingFlightPlan[PatrollingLayout]): + @property + def patrol_duration(self) -> timedelta: + return timedelta(hours=4) + + @property + def patrol_speed(self) -> Speed: + altitude = self.layout.patrol_start.alt + if self.flight.unit_type.preferred_patrol_speed(altitude) is not None: + return self.flight.unit_type.preferred_patrol_speed(altitude) + return knots(390) + + @property + def engagement_distance(self) -> Distance: + # TODO: Factor out a common base of the combat and non-combat race-tracks. + # No harm in setting this, but we ought to clean up a bit. + return meters(0) + @staticmethod def builder_type() -> Type[IBuilder]: return Builder diff --git a/game/ato/flightplans/airlift.py b/game/ato/flightplans/airlift.py index 7b2a7eed..f097d550 100644 --- a/game/ato/flightplans/airlift.py +++ b/game/ato/flightplans/airlift.py @@ -1,13 +1,14 @@ from __future__ import annotations from collections.abc import Iterator +from dataclasses import dataclass from datetime import timedelta from typing import TYPE_CHECKING, Type from game.utils import feet from .ibuilder import IBuilder from .planningerror import PlanningError -from .standard import StandardFlightPlan +from .standard import StandardFlightPlan, StandardLayout from .waypointbuilder import WaypointBuilder if TYPE_CHECKING: @@ -16,7 +17,7 @@ if TYPE_CHECKING: class Builder(IBuilder): - def build(self) -> AirliftFlightPlan: + def build(self) -> AirliftLayout: cargo = self.flight.cargo if cargo is None: raise PlanningError( @@ -39,8 +40,7 @@ class Builder(IBuilder): altitude_is_agl, ) - return AirliftFlightPlan( - flight=self.flight, + return AirliftLayout( departure=builder.takeoff(self.flight.departure), nav_to_pickup=nav_to_pickup, pickup=pickup, @@ -63,30 +63,13 @@ class Builder(IBuilder): ) -class AirliftFlightPlan(StandardFlightPlan): - def __init__( - self, - flight: Flight, - departure: FlightWaypoint, - nav_to_pickup: list[FlightWaypoint], - pickup: FlightWaypoint | None, - nav_to_drop_off: list[FlightWaypoint], - drop_off: FlightWaypoint, - nav_to_home: list[FlightWaypoint], - arrival: FlightWaypoint, - divert: FlightWaypoint | None, - bullseye: FlightWaypoint, - ) -> None: - super().__init__(flight, departure, arrival, divert, bullseye) - self.nav_to_pickup = nav_to_pickup - self.pickup = pickup - self.nav_to_drop_off = nav_to_drop_off - self.drop_off = drop_off - self.nav_to_home = nav_to_home - - @staticmethod - def builder_type() -> Type[Builder]: - return Builder +@dataclass(frozen=True) +class AirliftLayout(StandardLayout): + nav_to_pickup: list[FlightWaypoint] + pickup: FlightWaypoint | None + nav_to_drop_off: list[FlightWaypoint] + drop_off: FlightWaypoint + nav_to_home: list[FlightWaypoint] def iter_waypoints(self) -> Iterator[FlightWaypoint]: yield self.departure @@ -101,9 +84,18 @@ class AirliftFlightPlan(StandardFlightPlan): yield self.divert yield self.bullseye + +class AirliftFlightPlan(StandardFlightPlan[AirliftLayout]): + def __init__(self, flight: Flight, layout: AirliftLayout) -> None: + super().__init__(flight, layout) + + @staticmethod + def builder_type() -> Type[Builder]: + return Builder + @property def tot_waypoint(self) -> FlightWaypoint | None: - return self.drop_off + return self.layout.drop_off def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: # TOT planning isn't really useful for transports. They're behind the front diff --git a/game/ato/flightplans/antiship.py b/game/ato/flightplans/antiship.py index 505ba322..4326ecc2 100644 --- a/game/ato/flightplans/antiship.py +++ b/game/ato/flightplans/antiship.py @@ -4,7 +4,11 @@ from typing import Type from game.theater import NavalControlPoint from game.theater.theatergroundobject import NavalGroundObject -from .formationattack import FormationAttackBuilder, FormationAttackFlightPlan +from .formationattack import ( + FormationAttackBuilder, + FormationAttackFlightPlan, + FormationAttackLayout, +) from .invalidobjectivelocation import InvalidObjectiveLocation from .waypointbuilder import StrikeTarget from ..flightwaypointtype import FlightWaypointType @@ -16,8 +20,8 @@ class AntiShipFlightPlan(FormationAttackFlightPlan): return Builder -class Builder(FormationAttackBuilder[AntiShipFlightPlan]): - def build(self) -> FormationAttackFlightPlan: +class Builder(FormationAttackBuilder): + def build(self) -> FormationAttackLayout: location = self.package.target from game.transfers import CargoShip @@ -31,7 +35,7 @@ class Builder(FormationAttackBuilder[AntiShipFlightPlan]): else: raise InvalidObjectiveLocation(self.flight.flight_type, location) - return self._build(AntiShipFlightPlan, FlightWaypointType.INGRESS_BAI, targets) + return self._build(FlightWaypointType.INGRESS_BAI, targets) @staticmethod def anti_ship_targets_for_tgo(tgo: NavalGroundObject) -> list[StrikeTarget]: diff --git a/game/ato/flightplans/bai.py b/game/ato/flightplans/bai.py index 9fb164c6..837e7352 100644 --- a/game/ato/flightplans/bai.py +++ b/game/ato/flightplans/bai.py @@ -3,7 +3,11 @@ from __future__ import annotations from typing import Type from game.theater.theatergroundobject import TheaterGroundObject -from .formationattack import FormationAttackBuilder, FormationAttackFlightPlan +from .formationattack import ( + FormationAttackBuilder, + FormationAttackFlightPlan, + FormationAttackLayout, +) from .invalidobjectivelocation import InvalidObjectiveLocation from .waypointbuilder import StrikeTarget from ..flightwaypointtype import FlightWaypointType @@ -15,8 +19,8 @@ class BaiFlightPlan(FormationAttackFlightPlan): return Builder -class Builder(FormationAttackBuilder[BaiFlightPlan]): - def build(self) -> FormationAttackFlightPlan: +class Builder(FormationAttackBuilder): + def build(self) -> FormationAttackLayout: location = self.package.target from game.transfers import Convoy @@ -33,4 +37,4 @@ class Builder(FormationAttackBuilder[BaiFlightPlan]): else: raise InvalidObjectiveLocation(self.flight.flight_type, location) - return self._build(BaiFlightPlan, FlightWaypointType.INGRESS_BAI, targets) + return self._build(FlightWaypointType.INGRESS_BAI, targets) diff --git a/game/ato/flightplans/barcap.py b/game/ato/flightplans/barcap.py index 76ea3064..b2f4abe3 100644 --- a/game/ato/flightplans/barcap.py +++ b/game/ato/flightplans/barcap.py @@ -1,18 +1,19 @@ from __future__ import annotations import random +from datetime import timedelta from typing import Type from game.theater import FrontLine -from game.utils import feet +from game.utils import Distance, Speed, feet from .capbuilder import CapBuilder from .invalidobjectivelocation import InvalidObjectiveLocation -from .patrolling import PatrollingFlightPlan +from .patrolling import PatrollingFlightPlan, PatrollingLayout from .waypointbuilder import WaypointBuilder class Builder(CapBuilder): - def build(self) -> BarCapFlightPlan: + def build(self) -> PatrollingLayout: location = self.package.target if isinstance(location, FrontLine): @@ -27,16 +28,10 @@ class Builder(CapBuilder): min(self.doctrine.max_patrol_altitude, randomized_alt), ) - patrol_speed = self.flight.unit_type.preferred_patrol_speed(patrol_alt) - builder = WaypointBuilder(self.flight, self.coalition) start, end = builder.race_track(start_pos, end_pos, patrol_alt) - return BarCapFlightPlan( - flight=self.flight, - patrol_duration=self.doctrine.cap_duration, - patrol_speed=patrol_speed, - engagement_distance=self.doctrine.cap_engagement_range, + return PatrollingLayout( departure=builder.takeoff(self.flight.departure), nav_to=builder.nav_path( self.flight.departure.position, start.position, patrol_alt @@ -52,7 +47,21 @@ class Builder(CapBuilder): ) -class BarCapFlightPlan(PatrollingFlightPlan): +class BarCapFlightPlan(PatrollingFlightPlan[PatrollingLayout]): @staticmethod def builder_type() -> Type[Builder]: return Builder + + @property + def patrol_duration(self) -> timedelta: + return self.flight.coalition.doctrine.cap_duration + + @property + def patrol_speed(self) -> Speed: + return self.flight.unit_type.preferred_patrol_speed( + self.layout.patrol_start.alt + ) + + @property + def engagement_distance(self) -> Distance: + return self.flight.coalition.doctrine.cap_engagement_range diff --git a/game/ato/flightplans/cas.py b/game/ato/flightplans/cas.py index 608b81d1..9c24219f 100644 --- a/game/ato/flightplans/cas.py +++ b/game/ato/flightplans/cas.py @@ -1,24 +1,24 @@ from __future__ import annotations from collections.abc import Iterator +from dataclasses import dataclass from datetime import timedelta from typing import TYPE_CHECKING, Type from game.theater import FrontLine -from game.utils import Distance, Speed, meters +from game.utils import Distance, Speed, kph, meters from .ibuilder import IBuilder from .invalidobjectivelocation import InvalidObjectiveLocation -from .patrolling import PatrollingFlightPlan +from .patrolling import PatrollingFlightPlan, PatrollingLayout from .waypointbuilder import WaypointBuilder from ..flightwaypointtype import FlightWaypointType if TYPE_CHECKING: - from ..flight import Flight from ..flightwaypoint import FlightWaypoint class Builder(IBuilder): - def build(self) -> CasFlightPlan: + def build(self) -> CasLayout: location = self.package.target if not isinstance(location, FrontLine): @@ -41,24 +41,13 @@ class Builder(IBuilder): builder = WaypointBuilder(self.flight, self.coalition) - # 2021-08-02: patrol_speed will currently have no effect because - # CAS doesn't use OrbitAction. But all PatrollingFlightPlan are expected - # to have patrol_speed is_helo = self.flight.unit_type.dcs_unit_type.helicopter ingress_egress_altitude = ( self.doctrine.ingress_altitude if not is_helo else meters(50) ) - patrol_speed = self.flight.unit_type.preferred_patrol_speed( - ingress_egress_altitude - ) use_agl_ingress_egress = is_helo - from game.missiongenerator.frontlineconflictdescription import FRONTLINE_LENGTH - - return CasFlightPlan( - flight=self.flight, - patrol_duration=self.doctrine.cas_duration, - patrol_speed=patrol_speed, + return CasLayout( departure=builder.takeoff(self.flight.departure), nav_to=builder.nav_path( self.flight.departure.position, @@ -75,7 +64,6 @@ class Builder(IBuilder): patrol_start=builder.ingress( FlightWaypointType.INGRESS_CAS, ingress, location ), - engagement_distance=meters(FRONTLINE_LENGTH) / 2, target=builder.cas(center), patrol_end=builder.egress(egress, location), arrival=builder.land(self.flight.arrival), @@ -84,42 +72,9 @@ class Builder(IBuilder): ) -class CasFlightPlan(PatrollingFlightPlan): - def __init__( - self, - flight: Flight, - departure: FlightWaypoint, - arrival: FlightWaypoint, - divert: FlightWaypoint | None, - bullseye: FlightWaypoint, - nav_to: list[FlightWaypoint], - nav_from: list[FlightWaypoint], - patrol_start: FlightWaypoint, - patrol_end: FlightWaypoint, - patrol_duration: timedelta, - patrol_speed: Speed, - engagement_distance: Distance, - target: FlightWaypoint, - ) -> None: - super().__init__( - flight, - departure, - arrival, - divert, - bullseye, - nav_to, - nav_from, - patrol_start, - patrol_end, - patrol_duration, - patrol_speed, - engagement_distance, - ) - self.target = target - - @staticmethod - def builder_type() -> Type[Builder]: - return Builder +@dataclass(frozen=True) +class CasLayout(PatrollingLayout): + target: FlightWaypoint def iter_waypoints(self) -> Iterator[FlightWaypoint]: yield self.departure @@ -133,12 +88,35 @@ class CasFlightPlan(PatrollingFlightPlan): yield self.divert yield self.bullseye + +class CasFlightPlan(PatrollingFlightPlan[CasLayout]): + @staticmethod + def builder_type() -> Type[Builder]: + return Builder + + @property + def patrol_duration(self) -> timedelta: + return self.flight.coalition.doctrine.cas_duration + + @property + def patrol_speed(self) -> Speed: + # 2021-08-02: patrol_speed will currently have no effect because + # CAS doesn't use OrbitAction. But all PatrollingFlightPlan are expected + # to have patrol_speed + return kph(0) + + @property + def engagement_distance(self) -> Distance: + from game.missiongenerator.frontlineconflictdescription import FRONTLINE_LENGTH + + return meters(FRONTLINE_LENGTH) / 2 + @property def combat_speed_waypoints(self) -> set[FlightWaypoint]: - return {self.patrol_start, self.target, self.patrol_end} + return {self.layout.patrol_start, self.layout.target, self.layout.patrol_end} def request_escort_at(self) -> FlightWaypoint | None: - return self.patrol_start + return self.layout.patrol_start def dismiss_escort_at(self) -> FlightWaypoint | None: - return self.patrol_end + return self.layout.patrol_end diff --git a/game/ato/flightplans/custom.py b/game/ato/flightplans/custom.py index 8cbafbee..652a9cae 100644 --- a/game/ato/flightplans/custom.py +++ b/game/ato/flightplans/custom.py @@ -1,35 +1,36 @@ from __future__ import annotations from collections.abc import Iterator +from dataclasses import dataclass from datetime import timedelta from typing import TYPE_CHECKING, Type -from .flightplan import FlightPlan +from .flightplan import FlightPlan, Layout from .ibuilder import IBuilder from ..flightwaypointtype import FlightWaypointType if TYPE_CHECKING: - from ..flight import Flight from ..flightwaypoint import FlightWaypoint class Builder(IBuilder): - def build(self) -> CustomFlightPlan: - return CustomFlightPlan(self.flight, []) + def build(self) -> CustomLayout: + return CustomLayout([]) -class CustomFlightPlan(FlightPlan): - def __init__(self, flight: Flight, waypoints: list[FlightWaypoint]) -> None: - super().__init__(flight) - self.custom_waypoints = waypoints - - @staticmethod - def builder_type() -> Type[Builder]: - return Builder +@dataclass(frozen=True) +class CustomLayout(Layout): + custom_waypoints: list[FlightWaypoint] def iter_waypoints(self) -> Iterator[FlightWaypoint]: yield from self.custom_waypoints + +class CustomFlightPlan(FlightPlan[CustomLayout]): + @staticmethod + def builder_type() -> Type[Builder]: + return Builder + @property def tot_waypoint(self) -> FlightWaypoint | None: target_types = ( diff --git a/game/ato/flightplans/dead.py b/game/ato/flightplans/dead.py index c6949385..b3a38a54 100644 --- a/game/ato/flightplans/dead.py +++ b/game/ato/flightplans/dead.py @@ -7,7 +7,11 @@ from game.theater.theatergroundobject import ( EwrGroundObject, SamGroundObject, ) -from .formationattack import FormationAttackBuilder, FormationAttackFlightPlan +from .formationattack import ( + FormationAttackBuilder, + FormationAttackFlightPlan, + FormationAttackLayout, +) from .invalidobjectivelocation import InvalidObjectiveLocation from ..flightwaypointtype import FlightWaypointType @@ -18,8 +22,8 @@ class DeadFlightPlan(FormationAttackFlightPlan): return Builder -class Builder(FormationAttackBuilder[DeadFlightPlan]): - def build(self) -> FormationAttackFlightPlan: +class Builder(FormationAttackBuilder): + def build(self) -> FormationAttackLayout: location = self.package.target is_ewr = isinstance(location, EwrGroundObject) @@ -31,4 +35,4 @@ class Builder(FormationAttackBuilder[DeadFlightPlan]): ) raise InvalidObjectiveLocation(self.flight.flight_type, location) - return self._build(DeadFlightPlan, FlightWaypointType.INGRESS_DEAD) + return self._build(FlightWaypointType.INGRESS_DEAD) diff --git a/game/ato/flightplans/escort.py b/game/ato/flightplans/escort.py index 0304fb69..a1008ce3 100644 --- a/game/ato/flightplans/escort.py +++ b/game/ato/flightplans/escort.py @@ -1,9 +1,12 @@ from __future__ import annotations -from datetime import timedelta from typing import Type -from .formationattack import FormationAttackBuilder, FormationAttackFlightPlan +from .formationattack import ( + FormationAttackBuilder, + FormationAttackFlightPlan, + FormationAttackLayout, +) from .waypointbuilder import WaypointBuilder @@ -13,8 +16,8 @@ class EscortFlightPlan(FormationAttackFlightPlan): return Builder -class Builder(FormationAttackBuilder[EscortFlightPlan]): - def build(self) -> FormationAttackFlightPlan: +class Builder(FormationAttackBuilder): + def build(self) -> FormationAttackLayout: assert self.package.waypoints is not None builder = WaypointBuilder(self.flight, self.coalition) @@ -28,11 +31,9 @@ class Builder(FormationAttackBuilder[EscortFlightPlan]): if self.package.waypoints.refuel is not None: refuel = builder.refuel(self.package.waypoints.refuel) - return EscortFlightPlan( - flight=self.flight, + return FormationAttackLayout( departure=builder.takeoff(self.flight.departure), hold=hold, - hold_duration=timedelta(minutes=5), nav_to=builder.nav_path( hold.position, join.position, self.doctrine.ingress_altitude ), diff --git a/game/ato/flightplans/ferry.py b/game/ato/flightplans/ferry.py index 08c98ce8..5c27cd49 100644 --- a/game/ato/flightplans/ferry.py +++ b/game/ato/flightplans/ferry.py @@ -1,22 +1,22 @@ from __future__ import annotations from collections.abc import Iterator +from dataclasses import dataclass from datetime import timedelta from typing import TYPE_CHECKING, Type from game.utils import feet from .ibuilder import IBuilder from .planningerror import PlanningError -from .standard import StandardFlightPlan +from .standard import StandardFlightPlan, StandardLayout from .waypointbuilder import WaypointBuilder if TYPE_CHECKING: - from ..flight import Flight from ..flightwaypoint import FlightWaypoint class Builder(IBuilder): - def build(self) -> FerryFlightPlan: + def build(self) -> FerryLayout: if self.flight.departure == self.flight.arrival: raise PlanningError( f"Cannot plan ferry self.flight: departure and arrival are both " @@ -31,8 +31,7 @@ class Builder(IBuilder): ) builder = WaypointBuilder(self.flight, self.coalition) - return FerryFlightPlan( - flight=self.flight, + return FerryLayout( departure=builder.takeoff(self.flight.departure), nav_to_destination=builder.nav_path( self.flight.departure.position, @@ -46,22 +45,9 @@ class Builder(IBuilder): ) -class FerryFlightPlan(StandardFlightPlan): - def __init__( - self, - flight: Flight, - departure: FlightWaypoint, - arrival: FlightWaypoint, - divert: FlightWaypoint | None, - bullseye: FlightWaypoint, - nav_to_destination: list[FlightWaypoint], - ) -> None: - super().__init__(flight, departure, arrival, divert, bullseye) - self.nav_to_destination = nav_to_destination - - @staticmethod - def builder_type() -> Type[Builder]: - return Builder +@dataclass(frozen=True) +class FerryLayout(StandardLayout): + nav_to_destination: list[FlightWaypoint] def iter_waypoints(self) -> Iterator[FlightWaypoint]: yield self.departure @@ -71,9 +57,15 @@ class FerryFlightPlan(StandardFlightPlan): yield self.divert yield self.bullseye + +class FerryFlightPlan(StandardFlightPlan[FerryLayout]): + @staticmethod + def builder_type() -> Type[Builder]: + return Builder + @property def tot_waypoint(self) -> FlightWaypoint | None: - return self.arrival + return self.layout.arrival def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: # TOT planning isn't really useful for ferries. They're behind the front diff --git a/game/ato/flightplans/flightplan.py b/game/ato/flightplans/flightplan.py index 5c7f33fe..3021b658 100644 --- a/game/ato/flightplans/flightplan.py +++ b/game/ato/flightplans/flightplan.py @@ -12,7 +12,7 @@ from abc import ABC, abstractmethod from collections.abc import Iterator from datetime import timedelta from functools import cached_property -from typing import TYPE_CHECKING, Type, TypeGuard +from typing import Any, Generic, TYPE_CHECKING, Type, TypeGuard, TypeVar from game.typeguard import self_type_guard from game.utils import Distance, Speed, meters @@ -41,9 +41,24 @@ INGRESS_TYPES = { } -class FlightPlan(ABC): - def __init__(self, flight: Flight) -> None: +class Layout(ABC): + @property + def waypoints(self) -> list[FlightWaypoint]: + """A list of all waypoints in the flight plan, in order.""" + return list(self.iter_waypoints()) + + def iter_waypoints(self) -> Iterator[FlightWaypoint]: + """Iterates over all waypoints in the flight plan, in order.""" + raise NotImplementedError + + +LayoutT = TypeVar("LayoutT", bound=Layout) + + +class FlightPlan(ABC, Generic[LayoutT]): + def __init__(self, flight: Flight, layout: LayoutT) -> None: self.flight = flight + self.layout = layout @property def package(self) -> Package: @@ -61,7 +76,7 @@ class FlightPlan(ABC): def iter_waypoints(self) -> Iterator[FlightWaypoint]: """Iterates over all waypoints in the flight plan, in order.""" - raise NotImplementedError + yield from self.layout.iter_waypoints() def edges( self, until: FlightWaypoint | None = None @@ -296,13 +311,17 @@ class FlightPlan(ABC): raise NotImplementedError @self_type_guard - def is_loiter(self, flight_plan: FlightPlan) -> TypeGuard[LoiterFlightPlan]: + def is_loiter(self, flight_plan: FlightPlan[Any]) -> TypeGuard[LoiterFlightPlan]: return False @self_type_guard - def is_patrol(self, flight_plan: FlightPlan) -> TypeGuard[PatrollingFlightPlan]: + def is_patrol( + self, flight_plan: FlightPlan[Any] + ) -> TypeGuard[PatrollingFlightPlan[Any]]: return False @self_type_guard - def is_formation(self, flight_plan: FlightPlan) -> TypeGuard[FormationFlightPlan]: + def is_formation( + self, flight_plan: FlightPlan[Any] + ) -> TypeGuard[FormationFlightPlan]: return False diff --git a/game/ato/flightplans/flightplanbuilder.py b/game/ato/flightplans/flightplanbuilder.py index 3f4ca46e..5e93c928 100644 --- a/game/ato/flightplans/flightplanbuilder.py +++ b/game/ato/flightplans/flightplanbuilder.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Type +from typing import Any, TYPE_CHECKING, Type from game.ato import FlightType from game.ato.closestairfields import ObjectiveDistanceCache @@ -82,8 +82,8 @@ class FlightPlanBuilder: f"{flight.departure} to {flight.package.target}" ) from ex - def plan_type(self, task: FlightType) -> Type[FlightPlan] | None: - plan_type: Type[FlightPlan] + def plan_type(self, task: FlightType) -> Type[FlightPlan[Any]] | None: + plan_type: Type[FlightPlan[Any]] if task == FlightType.REFUELING: if self.package.target.is_friendly(self.is_player) or isinstance( self.package.target, FrontLine @@ -91,7 +91,7 @@ class FlightPlanBuilder: return TheaterRefuelingFlightPlan return PackageRefuelingFlightPlan - plan_dict: dict[FlightType, Type[FlightPlan]] = { + plan_dict: dict[FlightType, Type[FlightPlan[Any]]] = { FlightType.ANTISHIP: AntiShipFlightPlan, FlightType.BAI: BaiFlightPlan, FlightType.BARCAP: BarCapFlightPlan, @@ -111,13 +111,14 @@ class FlightPlanBuilder: } return plan_dict.get(task) - def generate_flight_plan(self, flight: Flight) -> FlightPlan: + def generate_flight_plan(self, flight: Flight) -> FlightPlan[Any]: plan_type = self.plan_type(flight.flight_type) if plan_type is None: raise PlanningError( f"{flight.flight_type} flight plan generation not implemented" ) - return plan_type.builder_type()(flight, self.theater).build() + layout = plan_type.builder_type()(flight, self.theater).build() + return plan_type(flight, layout) def regenerate_flight_plans(self) -> None: new_flights: list[Flight] = [] diff --git a/game/ato/flightplans/formation.py b/game/ato/flightplans/formation.py index bb5df82e..5aa39263 100644 --- a/game/ato/flightplans/formation.py +++ b/game/ato/flightplans/formation.py @@ -1,52 +1,31 @@ from __future__ import annotations from abc import ABC, abstractmethod +from dataclasses import dataclass from datetime import timedelta from functools import cached_property -from typing import TYPE_CHECKING, TypeGuard +from typing import Any, TYPE_CHECKING, TypeGuard from game.typeguard import self_type_guard from game.utils import Speed from .flightplan import FlightPlan -from .loiter import LoiterFlightPlan +from .loiter import LoiterFlightPlan, LoiterLayout from ..traveltime import GroundSpeed, TravelTime if TYPE_CHECKING: - from ..flight import Flight from ..flightwaypoint import FlightWaypoint -class FormationFlightPlan(LoiterFlightPlan, ABC): - def __init__( - self, - flight: Flight, - departure: FlightWaypoint, - arrival: FlightWaypoint, - divert: FlightWaypoint | None, - bullseye: FlightWaypoint, - nav_to: list[FlightWaypoint], - nav_from: list[FlightWaypoint], - hold: FlightWaypoint, - hold_duration: timedelta, - join: FlightWaypoint, - split: FlightWaypoint, - refuel: FlightWaypoint, - ) -> None: - super().__init__( - flight, - departure, - arrival, - divert, - bullseye, - nav_to, - nav_from, - hold, - hold_duration, - ) - self.join = join - self.split = split - self.refuel = refuel +@dataclass(frozen=True) +class FormationLayout(LoiterLayout, ABC): + nav_to: list[FlightWaypoint] + join: FlightWaypoint + split: FlightWaypoint + refuel: FlightWaypoint + nav_from: list[FlightWaypoint] + +class FormationFlightPlan(LoiterFlightPlan, ABC): @property @abstractmethod def package_speed_waypoints(self) -> set[FlightWaypoint]: @@ -57,10 +36,10 @@ class FormationFlightPlan(LoiterFlightPlan, ABC): return self.package_speed_waypoints def request_escort_at(self) -> FlightWaypoint | None: - return self.join + return self.layout.join def dismiss_escort_at(self) -> FlightWaypoint | None: - return self.split + return self.layout.split @cached_property def best_flight_formation_speed(self) -> Speed: @@ -90,7 +69,7 @@ class FormationFlightPlan(LoiterFlightPlan, ABC): @property def travel_time_to_rendezvous(self) -> timedelta: """The estimated time between the first waypoint and the join point.""" - return self._travel_time_to_waypoint(self.join) + return self._travel_time_to_waypoint(self.layout.join) @property @abstractmethod @@ -103,18 +82,18 @@ class FormationFlightPlan(LoiterFlightPlan, ABC): ... def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: - if waypoint == self.join: + if waypoint == self.layout.join: return self.join_time - elif waypoint == self.split: + elif waypoint == self.layout.split: return self.split_time return None @property def push_time(self) -> timedelta: return self.join_time - TravelTime.between_points( - self.hold.position, - self.join.position, - GroundSpeed.for_flight(self.flight, self.hold.alt), + self.layout.hold.position, + self.layout.join.position, + GroundSpeed.for_flight(self.flight, self.layout.hold.alt), ) @property @@ -122,5 +101,7 @@ class FormationFlightPlan(LoiterFlightPlan, ABC): return self.split_time @self_type_guard - def is_formation(self, flight_plan: FlightPlan) -> TypeGuard[FormationFlightPlan]: + def is_formation( + self, flight_plan: FlightPlan[Any] + ) -> TypeGuard[FormationFlightPlan]: return True diff --git a/game/ato/flightplans/formationattack.py b/game/ato/flightplans/formationattack.py index 6be043f2..20d1cdd4 100644 --- a/game/ato/flightplans/formationattack.py +++ b/game/ato/flightplans/formationattack.py @@ -2,15 +2,16 @@ from __future__ import annotations from abc import ABC from collections.abc import Iterator +from dataclasses import dataclass from datetime import timedelta -from typing import Generic, TYPE_CHECKING, Type, TypeVar +from typing import TYPE_CHECKING, TypeVar from dcs import Point from game.flightplan import HoldZoneGeometry from game.theater import MissionTarget from game.utils import Speed, meters -from .formation import FormationFlightPlan +from .formation import FormationFlightPlan, FormationLayout from .ibuilder import IBuilder from .planningerror import PlanningError from .waypointbuilder import StrikeTarget, WaypointBuilder @@ -23,64 +24,16 @@ if TYPE_CHECKING: class FormationAttackFlightPlan(FormationFlightPlan, ABC): - def __init__( - self, - flight: Flight, - departure: FlightWaypoint, - arrival: FlightWaypoint, - divert: FlightWaypoint | None, - bullseye: FlightWaypoint, - nav_to: list[FlightWaypoint], - nav_from: list[FlightWaypoint], - hold: FlightWaypoint, - hold_duration: timedelta, - join: FlightWaypoint, - split: FlightWaypoint, - refuel: FlightWaypoint, - ingress: FlightWaypoint, - targets: list[FlightWaypoint], - lead_time: timedelta = timedelta(), - ) -> None: - super().__init__( - flight, - departure, - arrival, - divert, - bullseye, - nav_to, - nav_from, - hold, - hold_duration, - join, - split, - refuel, - ) - self.ingress = ingress - self.targets = targets - self.lead_time = lead_time - - def iter_waypoints(self) -> Iterator[FlightWaypoint]: - yield self.departure - yield self.hold - yield from self.nav_to - yield self.join - yield self.ingress - yield from self.targets - yield self.split - if self.refuel is not None: - yield self.refuel - yield from self.nav_from - yield self.arrival - if self.divert is not None: - yield self.divert - yield self.bullseye + @property + def lead_time(self) -> timedelta: + return timedelta() @property def package_speed_waypoints(self) -> set[FlightWaypoint]: return { - self.ingress, - self.split, - } | set(self.targets) + self.layout.ingress, + self.layout.split, + } | set(self.layout.targets) def speed_between_waypoints(self, a: FlightWaypoint, b: FlightWaypoint) -> Speed: # FlightWaypoint is only comparable by identity, so adding @@ -94,7 +47,7 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC): @property def tot_waypoint(self) -> FlightWaypoint: - return self.targets[0] + return self.layout.targets[0] @property def tot_offset(self) -> timedelta: @@ -138,18 +91,20 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC): @property def join_time(self) -> timedelta: - travel_time = self.travel_time_between_waypoints(self.join, self.ingress) + travel_time = self.travel_time_between_waypoints( + self.layout.join, self.layout.ingress + ) return self.ingress_time - travel_time @property def split_time(self) -> timedelta: travel_time_ingress = self.travel_time_between_waypoints( - self.ingress, self.target_area_waypoint + self.layout.ingress, self.target_area_waypoint ) travel_time_egress = self.travel_time_between_waypoints( - self.target_area_waypoint, self.split + self.target_area_waypoint, self.layout.split ) - minutes_at_target = 0.75 * len(self.targets) + minutes_at_target = 0.75 * len(self.layout.targets) timedelta_at_target = timedelta(minutes=minutes_at_target) return ( self.ingress_time @@ -162,29 +117,49 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC): def ingress_time(self) -> timedelta: tot = self.tot travel_time = self.travel_time_between_waypoints( - self.ingress, self.target_area_waypoint + self.layout.ingress, self.target_area_waypoint ) return tot - travel_time def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: - if waypoint == self.ingress: + if waypoint == self.layout.ingress: return self.ingress_time - elif waypoint in self.targets: + elif waypoint in self.layout.targets: return self.tot return super().tot_for_waypoint(waypoint) -FlightPlanT = TypeVar("FlightPlanT", bound=FormationAttackFlightPlan) +@dataclass(frozen=True) +class FormationAttackLayout(FormationLayout): + ingress: FlightWaypoint + targets: list[FlightWaypoint] + + def iter_waypoints(self) -> Iterator[FlightWaypoint]: + yield self.departure + yield self.hold + yield from self.nav_to + yield self.join + yield self.ingress + yield from self.targets + yield self.split + if self.refuel is not None: + yield self.refuel + yield from self.nav_from + yield self.arrival + if self.divert is not None: + yield self.divert + yield self.bullseye -class FormationAttackBuilder(IBuilder, ABC, Generic[FlightPlanT]): +LayoutT = TypeVar("LayoutT", bound=FormationAttackLayout) + + +class FormationAttackBuilder(IBuilder, ABC): def _build( self, - plan_type: Type[FlightPlanT], ingress_type: FlightWaypointType, targets: list[StrikeTarget] | None = None, - lead_time: timedelta = timedelta(), - ) -> FlightPlanT: + ) -> FormationAttackLayout: assert self.package.waypoints is not None builder = WaypointBuilder(self.flight, self.coalition, targets) @@ -208,11 +183,9 @@ class FormationAttackBuilder(IBuilder, ABC, Generic[FlightPlanT]): if self.package.waypoints.refuel is not None: refuel = builder.refuel(self.package.waypoints.refuel) - return plan_type( - flight=self.flight, + return FormationAttackLayout( departure=builder.takeoff(self.flight.departure), hold=hold, - hold_duration=timedelta(minutes=5), nav_to=builder.nav_path( hold.position, join.position, self.doctrine.ingress_altitude ), @@ -231,7 +204,6 @@ class FormationAttackBuilder(IBuilder, ABC, Generic[FlightPlanT]): arrival=builder.land(self.flight.arrival), divert=builder.divert(self.flight.divert), bullseye=builder.bullseye(), - lead_time=lead_time, ) @staticmethod diff --git a/game/ato/flightplans/ibuilder.py b/game/ato/flightplans/ibuilder.py index c0e9ff28..3ef5de2f 100644 --- a/game/ato/flightplans/ibuilder.py +++ b/game/ato/flightplans/ibuilder.py @@ -10,7 +10,7 @@ if TYPE_CHECKING: from game.threatzones import ThreatZones from ..flight import Flight from ..package import Package - from .flightplan import FlightPlan + from .flightplan import Layout class IBuilder(ABC): @@ -19,7 +19,7 @@ class IBuilder(ABC): self.theater = theater @abstractmethod - def build(self) -> FlightPlan: + def build(self) -> Layout: ... @property diff --git a/game/ato/flightplans/loiter.py b/game/ato/flightplans/loiter.py index 3a30f968..c0e4ce56 100644 --- a/game/ato/flightplans/loiter.py +++ b/game/ato/flightplans/loiter.py @@ -1,36 +1,27 @@ from __future__ import annotations from abc import ABC, abstractmethod +from dataclasses import dataclass from datetime import timedelta -from typing import TYPE_CHECKING, TypeGuard +from typing import Any, TYPE_CHECKING, TypeGuard from game.typeguard import self_type_guard from .flightplan import FlightPlan -from .standard import StandardFlightPlan +from .standard import StandardFlightPlan, StandardLayout if TYPE_CHECKING: - from ..flight import Flight from ..flightwaypoint import FlightWaypoint -class LoiterFlightPlan(StandardFlightPlan, ABC): - def __init__( - self, - flight: Flight, - departure: FlightWaypoint, - arrival: FlightWaypoint, - divert: FlightWaypoint | None, - bullseye: FlightWaypoint, - nav_to: list[FlightWaypoint], - nav_from: list[FlightWaypoint], - hold: FlightWaypoint, - hold_duration: timedelta, - ) -> None: - super().__init__(flight, departure, arrival, divert, bullseye) - self.nav_to = nav_to - self.nav_from = nav_from - self.hold = hold - self.hold_duration = hold_duration +@dataclass(frozen=True) +class LoiterLayout(StandardLayout, ABC): + hold: FlightWaypoint + + +class LoiterFlightPlan(StandardFlightPlan[Any], ABC): + @property + def hold_duration(self) -> timedelta: + return timedelta(minutes=5) @property @abstractmethod @@ -38,7 +29,7 @@ class LoiterFlightPlan(StandardFlightPlan, ABC): ... def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: - if waypoint == self.hold: + if waypoint == self.layout.hold: return self.push_time return None @@ -46,10 +37,10 @@ class LoiterFlightPlan(StandardFlightPlan, ABC): self, a: FlightWaypoint, b: FlightWaypoint ) -> timedelta: travel_time = super().travel_time_between_waypoints(a, b) - if a != self.hold: + if a != self.layout.hold: return travel_time return travel_time + self.hold_duration @self_type_guard - def is_loiter(self, flight_plan: FlightPlan) -> TypeGuard[LoiterFlightPlan]: + def is_loiter(self, flight_plan: FlightPlan[Any]) -> TypeGuard[LoiterFlightPlan]: return True diff --git a/game/ato/flightplans/ocaaircraft.py b/game/ato/flightplans/ocaaircraft.py index b831a5e0..f8de4648 100644 --- a/game/ato/flightplans/ocaaircraft.py +++ b/game/ato/flightplans/ocaaircraft.py @@ -4,7 +4,11 @@ import logging from typing import Type from game.theater import Airfield -from .formationattack import FormationAttackBuilder, FormationAttackFlightPlan +from .formationattack import ( + FormationAttackBuilder, + FormationAttackFlightPlan, + FormationAttackLayout, +) from .invalidobjectivelocation import InvalidObjectiveLocation from ..flightwaypointtype import FlightWaypointType @@ -15,8 +19,8 @@ class OcaAircraftFlightPlan(FormationAttackFlightPlan): return Builder -class Builder(FormationAttackBuilder[OcaAircraftFlightPlan]): - def build(self) -> FormationAttackFlightPlan: +class Builder(FormationAttackBuilder): + def build(self) -> FormationAttackLayout: location = self.package.target if not isinstance(location, Airfield): @@ -26,6 +30,4 @@ class Builder(FormationAttackBuilder[OcaAircraftFlightPlan]): ) raise InvalidObjectiveLocation(self.flight.flight_type, location) - return self._build( - OcaAircraftFlightPlan, FlightWaypointType.INGRESS_OCA_AIRCRAFT - ) + return self._build(FlightWaypointType.INGRESS_OCA_AIRCRAFT) diff --git a/game/ato/flightplans/ocarunway.py b/game/ato/flightplans/ocarunway.py index feabbc17..97d49cef 100644 --- a/game/ato/flightplans/ocarunway.py +++ b/game/ato/flightplans/ocarunway.py @@ -4,7 +4,11 @@ import logging from typing import Type from game.theater import Airfield -from .formationattack import FormationAttackBuilder, FormationAttackFlightPlan +from .formationattack import ( + FormationAttackBuilder, + FormationAttackFlightPlan, + FormationAttackLayout, +) from .invalidobjectivelocation import InvalidObjectiveLocation from ..flightwaypointtype import FlightWaypointType @@ -15,8 +19,8 @@ class OcaRunwayFlightPlan(FormationAttackFlightPlan): return Builder -class Builder(FormationAttackBuilder[OcaRunwayFlightPlan]): - def build(self) -> FormationAttackFlightPlan: +class Builder(FormationAttackBuilder): + def build(self) -> FormationAttackLayout: location = self.package.target if not isinstance(location, Airfield): @@ -26,4 +30,4 @@ class Builder(FormationAttackBuilder[OcaRunwayFlightPlan]): ) raise InvalidObjectiveLocation(self.flight.flight_type, location) - return self._build(OcaRunwayFlightPlan, FlightWaypointType.INGRESS_OCA_RUNWAY) + return self._build(FlightWaypointType.INGRESS_OCA_RUNWAY) diff --git a/game/ato/flightplans/packagerefueling.py b/game/ato/flightplans/packagerefueling.py index 5683bd32..d63796f8 100644 --- a/game/ato/flightplans/packagerefueling.py +++ b/game/ato/flightplans/packagerefueling.py @@ -5,7 +5,8 @@ from typing import Type from dcs import Point -from game.utils import Distance, Heading, feet, knots, meters +from game.utils import Distance, Heading, feet, meters +from .patrolling import PatrollingLayout from .theaterrefueling import ( Builder as TheaterRefuelingBuilder, TheaterRefuelingFlightPlan, @@ -16,18 +17,11 @@ from ..flightwaypointtype import FlightWaypointType class Builder(TheaterRefuelingBuilder): - def build(self) -> PackageRefuelingFlightPlan: + def build(self) -> PatrollingLayout: package_waypoints = self.package.waypoints assert package_waypoints is not None racetrack_half_distance = Distance.from_nautical_miles(20).meters - # TODO: Only consider aircraft that can refuel with this tanker type. - refuel_time_minutes = 5 - for self.flight in self.package.flights: - flight_size = self.flight.roster.max_size - refuel_time_minutes = refuel_time_minutes + 4 * flight_size + 1 - - patrol_duration = timedelta(minutes=refuel_time_minutes) racetrack_center = package_waypoints.refuel @@ -52,17 +46,9 @@ class Builder(TheaterRefuelingBuilder): else: altitude = feet(21000) - # TODO: Could use self.flight.unit_type.preferred_patrol_speed(altitude). - if tanker_type.patrol_speed is not None: - speed = tanker_type.patrol_speed - else: - # ~280 knots IAS at 21000. - speed = knots(400) - racetrack = builder.race_track(racetrack_start, racetrack_end, altitude) - return PackageRefuelingFlightPlan( - flight=self.flight, + return PatrollingLayout( departure=builder.takeoff(self.flight.departure), nav_to=builder.nav_path( self.flight.departure.position, racetrack_start, altitude @@ -75,11 +61,6 @@ class Builder(TheaterRefuelingBuilder): arrival=builder.land(self.flight.arrival), divert=builder.divert(self.flight.divert), bullseye=builder.bullseye(), - patrol_duration=patrol_duration, - patrol_speed=speed, - # TODO: Factor out a common base of the combat and non-combat race-tracks. - # No harm in setting this, but we ought to clean up a bit. - engagement_distance=meters(0), ) @@ -88,6 +69,16 @@ class PackageRefuelingFlightPlan(TheaterRefuelingFlightPlan): def builder_type() -> Type[Builder]: return Builder + @property + def patrol_duration(self) -> timedelta: + # TODO: Only consider aircraft that can refuel with this tanker type. + refuel_time_minutes = 5 + for self.flight in self.package.flights: + flight_size = self.flight.roster.max_size + refuel_time_minutes = refuel_time_minutes + 4 * flight_size + 1 + + return timedelta(minutes=refuel_time_minutes) + def target_area_waypoint(self) -> FlightWaypoint: return FlightWaypoint( "TARGET AREA", diff --git a/game/ato/flightplans/patrolling.py b/game/ato/flightplans/patrolling.py index f8928834..43645f24 100644 --- a/game/ato/flightplans/patrolling.py +++ b/game/ato/flightplans/patrolling.py @@ -1,53 +1,63 @@ from __future__ import annotations -from abc import ABC +from abc import ABC, abstractmethod from collections.abc import Iterator +from dataclasses import dataclass from datetime import timedelta -from typing import TYPE_CHECKING, TypeGuard +from typing import Any, TYPE_CHECKING, TypeGuard, TypeVar -from game.ato.flightplans.standard import StandardFlightPlan +from game.ato.flightplans.standard import StandardFlightPlan, StandardLayout from game.typeguard import self_type_guard from game.utils import Distance, Speed if TYPE_CHECKING: - from ..flight import Flight from ..flightwaypoint import FlightWaypoint from .flightplan import FlightPlan -class PatrollingFlightPlan(StandardFlightPlan, ABC): - def __init__( - self, - flight: Flight, - departure: FlightWaypoint, - arrival: FlightWaypoint, - divert: FlightWaypoint | None, - bullseye: FlightWaypoint, - nav_to: list[FlightWaypoint], - nav_from: list[FlightWaypoint], - patrol_start: FlightWaypoint, - patrol_end: FlightWaypoint, - patrol_duration: timedelta, - patrol_speed: Speed, - engagement_distance: Distance, - ) -> None: - super().__init__(flight, departure, arrival, divert, bullseye) - self.nav_to = nav_to - self.nav_from = nav_from - self.patrol_start = patrol_start - self.patrol_end = patrol_end +@dataclass(frozen=True) +class PatrollingLayout(StandardLayout): + nav_to: list[FlightWaypoint] + patrol_start: FlightWaypoint + patrol_end: FlightWaypoint + nav_from: list[FlightWaypoint] - # Maximum time to remain on station. - self.patrol_duration = patrol_duration + def iter_waypoints(self) -> Iterator[FlightWaypoint]: + yield self.departure + yield from self.nav_to + yield self.patrol_start + yield self.patrol_end + yield from self.nav_from + yield self.arrival + if self.divert is not None: + yield self.divert + yield self.bullseye - # Racetrack speed TAS. - self.patrol_speed = patrol_speed - # The engagement range of any Search Then Engage task, or the radius of a - # Search Then Engage in Zone task. Any enemies of the appropriate type for - # this mission within this range of the flight's current position (or the - # center of the zone) will be engaged by the flight. - self.engagement_distance = engagement_distance +LayoutT = TypeVar("LayoutT", bound=PatrollingLayout) + + +class PatrollingFlightPlan(StandardFlightPlan[LayoutT], ABC): + @property + @abstractmethod + def patrol_duration(self) -> timedelta: + """Maximum time to remain on station.""" + + @property + @abstractmethod + def patrol_speed(self) -> Speed: + """Racetrack speed TAS.""" + + @property + @abstractmethod + def engagement_distance(self) -> Distance: + """The maximum engagement distance. + + The engagement range of any Search Then Engage task, or the radius of a Search + Then Engage in Zone task. Any enemies of the appropriate type for this mission + within this range of the flight's current position (or the center of the zone) + will be engaged by the flight. + """ @property def patrol_start_time(self) -> timedelta: @@ -61,38 +71,29 @@ class PatrollingFlightPlan(StandardFlightPlan, ABC): return self.patrol_start_time + self.patrol_duration def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: - if waypoint == self.patrol_start: + if waypoint == self.layout.patrol_start: return self.patrol_start_time return None def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: - if waypoint == self.patrol_end: + if waypoint == self.layout.patrol_end: return self.patrol_end_time return None - def iter_waypoints(self) -> Iterator[FlightWaypoint]: - yield self.departure - yield from self.nav_to - yield self.patrol_start - yield self.patrol_end - yield from self.nav_from - yield self.arrival - if self.divert is not None: - yield self.divert - yield self.bullseye - @property def package_speed_waypoints(self) -> set[FlightWaypoint]: - return {self.patrol_start, self.patrol_end} + return {self.layout.patrol_start, self.layout.patrol_end} @property def tot_waypoint(self) -> FlightWaypoint | None: - return self.patrol_start + return self.layout.patrol_start @property def mission_departure_time(self) -> timedelta: return self.patrol_end_time @self_type_guard - def is_patrol(self, flight_plan: FlightPlan) -> TypeGuard[PatrollingFlightPlan]: + def is_patrol( + self, flight_plan: FlightPlan[Any] + ) -> TypeGuard[PatrollingFlightPlan[Any]]: return True diff --git a/game/ato/flightplans/rtb.py b/game/ato/flightplans/rtb.py index 8f84a9bc..b8806c53 100644 --- a/game/ato/flightplans/rtb.py +++ b/game/ato/flightplans/rtb.py @@ -1,22 +1,22 @@ from __future__ import annotations from collections.abc import Iterator +from dataclasses import dataclass from datetime import timedelta from typing import TYPE_CHECKING, Type from game.utils import feet from .ibuilder import IBuilder -from .standard import StandardFlightPlan +from .standard import StandardFlightPlan, StandardLayout from .waypointbuilder import WaypointBuilder from ..flightstate import InFlight if TYPE_CHECKING: - from ..flight import Flight from ..flightwaypoint import FlightWaypoint class Builder(IBuilder): - def build(self) -> RtbFlightPlan: + def build(self) -> RtbLayout: if not isinstance(self.flight.state, InFlight): raise RuntimeError(f"Cannot abort {self} because it is not in flight") @@ -36,8 +36,7 @@ class Builder(IBuilder): abort_point.name = "ABORT AND RTB" abort_point.pretty_name = "Abort and RTB" abort_point.description = "Abort mission and return to base" - return RtbFlightPlan( - flight=self.flight, + return RtbLayout( departure=builder.takeoff(self.flight.departure), abort_location=abort_point, nav_to_destination=builder.nav_path( @@ -52,24 +51,10 @@ class Builder(IBuilder): ) -class RtbFlightPlan(StandardFlightPlan): - def __init__( - self, - flight: Flight, - departure: FlightWaypoint, - arrival: FlightWaypoint, - divert: FlightWaypoint | None, - bullseye: FlightWaypoint, - abort_location: FlightWaypoint, - nav_to_destination: list[FlightWaypoint], - ) -> None: - super().__init__(flight, departure, arrival, divert, bullseye) - self.abort_location = abort_location - self.nav_to_destination = nav_to_destination - - @staticmethod - def builder_type() -> Type[Builder]: - return Builder +@dataclass(frozen=True) +class RtbLayout(StandardLayout): + abort_location: FlightWaypoint + nav_to_destination: list[FlightWaypoint] def iter_waypoints(self) -> Iterator[FlightWaypoint]: yield self.departure @@ -80,6 +65,12 @@ class RtbFlightPlan(StandardFlightPlan): yield self.divert yield self.bullseye + +class RtbFlightPlan(StandardFlightPlan[RtbLayout]): + @staticmethod + def builder_type() -> Type[Builder]: + return Builder + @property def abort_index(self) -> int: return 1 diff --git a/game/ato/flightplans/sead.py b/game/ato/flightplans/sead.py index 5b3157f9..b82b59f4 100644 --- a/game/ato/flightplans/sead.py +++ b/game/ato/flightplans/sead.py @@ -3,20 +3,28 @@ from __future__ import annotations from datetime import timedelta from typing import Type -from .formationattack import FormationAttackBuilder, FormationAttackFlightPlan +from .formationattack import ( + FormationAttackBuilder, + FormationAttackFlightPlan, + FormationAttackLayout, +) +from .. import Flight from ..flightwaypointtype import FlightWaypointType class SeadFlightPlan(FormationAttackFlightPlan): + def __init__(self, flight: Flight, layout: FormationAttackLayout) -> None: + super().__init__(flight, layout) + @staticmethod def builder_type() -> Type[Builder]: return Builder + @property + def lead_time(self) -> timedelta: + return timedelta(minutes=1) -class Builder(FormationAttackBuilder[SeadFlightPlan]): - def build(self) -> FormationAttackFlightPlan: - return self._build( - SeadFlightPlan, - FlightWaypointType.INGRESS_SEAD, - lead_time=timedelta(minutes=1), - ) + +class Builder(FormationAttackBuilder): + def build(self) -> FormationAttackLayout: + return self._build(FlightWaypointType.INGRESS_SEAD) diff --git a/game/ato/flightplans/standard.py b/game/ato/flightplans/standard.py index 7277271f..6fe1fa1a 100644 --- a/game/ato/flightplans/standard.py +++ b/game/ato/flightplans/standard.py @@ -1,33 +1,30 @@ from __future__ import annotations from abc import ABC -from typing import TYPE_CHECKING +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypeVar -from game.ato.flightplans.flightplan import FlightPlan +from game.ato.flightplans.flightplan import FlightPlan, Layout if TYPE_CHECKING: - from ..flight import Flight from ..flightwaypoint import FlightWaypoint -class StandardFlightPlan(FlightPlan, ABC): +@dataclass(frozen=True) +class StandardLayout(Layout, ABC): + departure: FlightWaypoint + arrival: FlightWaypoint + divert: FlightWaypoint | None + bullseye: FlightWaypoint + + +LayoutT = TypeVar("LayoutT", bound=StandardLayout) + + +class StandardFlightPlan(FlightPlan[LayoutT], ABC): """Base type for all non-custom flight plans. We can't reason about custom flight plans so they get special treatment, but all others are guaranteed to have certain properties like departure and arrival points, potentially a divert field, and a bullseye """ - - def __init__( - self, - flight: Flight, - departure: FlightWaypoint, - arrival: FlightWaypoint, - divert: FlightWaypoint | None, - bullseye: FlightWaypoint, - ) -> None: - super().__init__(flight) - self.departure = departure - self.arrival = arrival - self.divert = divert - self.bullseye = bullseye diff --git a/game/ato/flightplans/strike.py b/game/ato/flightplans/strike.py index 04d608ac..145917d7 100644 --- a/game/ato/flightplans/strike.py +++ b/game/ato/flightplans/strike.py @@ -3,7 +3,11 @@ from __future__ import annotations from typing import Type from game.theater import TheaterGroundObject -from .formationattack import FormationAttackBuilder, FormationAttackFlightPlan +from .formationattack import ( + FormationAttackBuilder, + FormationAttackFlightPlan, + FormationAttackLayout, +) from .invalidobjectivelocation import InvalidObjectiveLocation from .waypointbuilder import StrikeTarget from ..flightwaypointtype import FlightWaypointType @@ -15,8 +19,8 @@ class StrikeFlightPlan(FormationAttackFlightPlan): return Builder -class Builder(FormationAttackBuilder[StrikeFlightPlan]): - def build(self) -> FormationAttackFlightPlan: +class Builder(FormationAttackBuilder): + def build(self) -> FormationAttackLayout: location = self.package.target if not isinstance(location, TheaterGroundObject): @@ -26,4 +30,4 @@ class Builder(FormationAttackBuilder[StrikeFlightPlan]): for idx, unit in enumerate(location.strike_targets): targets.append(StrikeTarget(f"{unit.type.id} #{idx}", unit)) - return self._build(StrikeFlightPlan, FlightWaypointType.INGRESS_STRIKE, targets) + return self._build(FlightWaypointType.INGRESS_STRIKE, targets) diff --git a/game/ato/flightplans/sweep.py b/game/ato/flightplans/sweep.py index e502d94a..7c351968 100644 --- a/game/ato/flightplans/sweep.py +++ b/game/ato/flightplans/sweep.py @@ -1,5 +1,6 @@ from __future__ import annotations +from dataclasses import dataclass from datetime import timedelta from typing import Iterator, TYPE_CHECKING, Type @@ -7,18 +8,17 @@ from dcs import Point from game.utils import Heading from .ibuilder import IBuilder -from .loiter import LoiterFlightPlan +from .loiter import LoiterFlightPlan, LoiterLayout from .waypointbuilder import WaypointBuilder from ..traveltime import GroundSpeed, TravelTime from ...flightplan import HoldZoneGeometry if TYPE_CHECKING: - from ..flight import Flight from ..flightwaypoint import FlightWaypoint class Builder(IBuilder): - def build(self) -> SweepFlightPlan: + def build(self) -> SweepLayout: assert self.package.waypoints is not None target = self.package.target.position heading = Heading.from_degrees( @@ -38,12 +38,9 @@ class Builder(IBuilder): if self.package.waypoints is not None: refuel = builder.refuel(self.package.waypoints.refuel) - return SweepFlightPlan( - flight=self.flight, - lead_time=timedelta(minutes=5), + return SweepLayout( departure=builder.takeoff(self.flight.departure), hold=hold, - hold_duration=timedelta(minutes=5), nav_to=builder.nav_path( hold.position, start.position, self.doctrine.ingress_altitude ), @@ -71,42 +68,13 @@ class Builder(IBuilder): ).find_best_hold_point() -class SweepFlightPlan(LoiterFlightPlan): - def __init__( - self, - flight: Flight, - departure: FlightWaypoint, - arrival: FlightWaypoint, - divert: FlightWaypoint | None, - bullseye: FlightWaypoint, - nav_to: list[FlightWaypoint], - nav_from: list[FlightWaypoint], - hold: FlightWaypoint, - hold_duration: timedelta, - sweep_start: FlightWaypoint, - sweep_end: FlightWaypoint, - refuel: FlightWaypoint, - lead_time: timedelta, - ) -> None: - super().__init__( - flight, - departure, - arrival, - divert, - bullseye, - nav_to, - nav_from, - hold, - hold_duration, - ) - self.sweep_start = sweep_start - self.sweep_end = sweep_end - self.refuel = refuel - self.lead_time = lead_time - - @staticmethod - def builder_type() -> Type[Builder]: - return Builder +@dataclass(frozen=True) +class SweepLayout(LoiterLayout): + nav_to: list[FlightWaypoint] + sweep_start: FlightWaypoint + sweep_end: FlightWaypoint + refuel: FlightWaypoint | None + nav_from: list[FlightWaypoint] def iter_waypoints(self) -> Iterator[FlightWaypoint]: yield self.departure @@ -122,13 +90,23 @@ class SweepFlightPlan(LoiterFlightPlan): yield self.divert yield self.bullseye + +class SweepFlightPlan(LoiterFlightPlan): + @property + def lead_time(self) -> timedelta: + return timedelta(minutes=5) + + @staticmethod + def builder_type() -> Type[Builder]: + return Builder + @property def combat_speed_waypoints(self) -> set[FlightWaypoint]: - return {self.sweep_end} + return {self.layout.sweep_end} @property def tot_waypoint(self) -> FlightWaypoint | None: - return self.sweep_end + return self.layout.sweep_end @property def tot_offset(self) -> timedelta: @@ -137,7 +115,7 @@ class SweepFlightPlan(LoiterFlightPlan): @property def sweep_start_time(self) -> timedelta: travel_time = self.travel_time_between_waypoints( - self.sweep_start, self.sweep_end + self.layout.sweep_start, self.layout.sweep_end ) return self.sweep_end_time - travel_time @@ -146,23 +124,23 @@ class SweepFlightPlan(LoiterFlightPlan): return self.tot def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: - if waypoint == self.sweep_start: + if waypoint == self.layout.sweep_start: return self.sweep_start_time - if waypoint == self.sweep_end: + if waypoint == self.layout.sweep_end: return self.sweep_end_time return None def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: - if waypoint == self.hold: + if waypoint == self.layout.hold: return self.push_time return None @property def push_time(self) -> timedelta: return self.sweep_end_time - TravelTime.between_points( - self.hold.position, - self.sweep_end.position, - GroundSpeed.for_flight(self.flight, self.hold.alt), + self.layout.hold.position, + self.layout.sweep_end.position, + GroundSpeed.for_flight(self.flight, self.layout.hold.alt), ) def mission_departure_time(self) -> timedelta: diff --git a/game/ato/flightplans/tarcap.py b/game/ato/flightplans/tarcap.py index cc7bca33..6ec4c1b5 100644 --- a/game/ato/flightplans/tarcap.py +++ b/game/ato/flightplans/tarcap.py @@ -2,21 +2,21 @@ from __future__ import annotations import random from collections.abc import Iterator +from dataclasses import dataclass from datetime import timedelta from typing import TYPE_CHECKING, Type from game.utils import Distance, Speed, feet from .capbuilder import CapBuilder -from .patrolling import PatrollingFlightPlan +from .patrolling import PatrollingFlightPlan, PatrollingLayout from .waypointbuilder import WaypointBuilder if TYPE_CHECKING: - from ..flight import Flight from ..flightwaypoint import FlightWaypoint class Builder(CapBuilder): - def build(self) -> TarCapFlightPlan: + def build(self) -> TarCapLayout: location = self.package.target preferred_alt = self.flight.unit_type.preferred_patrol_altitude @@ -25,7 +25,6 @@ class Builder(CapBuilder): self.doctrine.min_patrol_altitude, min(self.doctrine.max_patrol_altitude, randomized_alt), ) - patrol_speed = self.flight.unit_type.preferred_patrol_speed(patrol_alt) builder = WaypointBuilder(self.flight, self.coalition) orbit0p, orbit1p = self.cap_racetrack_for_objective(location, barcap=False) @@ -37,16 +36,7 @@ class Builder(CapBuilder): if self.package.waypoints is not None: refuel = builder.refuel(self.package.waypoints.refuel) - return TarCapFlightPlan( - flight=self.flight, - lead_time=timedelta(minutes=2), - # Note that this duration only has an effect if there are no - # flights in the package that have requested escort. If the package - # requests an escort the CAP self.flight will remain on station for the - # duration of the escorted mission, or until it is winchester/bingo. - patrol_duration=self.doctrine.cap_duration, - patrol_speed=patrol_speed, - engagement_distance=self.doctrine.cap_engagement_range, + return TarCapLayout( departure=builder.takeoff(self.flight.departure), nav_to=builder.nav_path( self.flight.departure.position, orbit0p, patrol_alt @@ -63,44 +53,9 @@ class Builder(CapBuilder): ) -class TarCapFlightPlan(PatrollingFlightPlan): - def __init__( - self, - flight: Flight, - departure: FlightWaypoint, - arrival: FlightWaypoint, - divert: FlightWaypoint | None, - bullseye: FlightWaypoint, - nav_to: list[FlightWaypoint], - nav_from: list[FlightWaypoint], - patrol_start: FlightWaypoint, - patrol_end: FlightWaypoint, - patrol_duration: timedelta, - patrol_speed: Speed, - engagement_distance: Distance, - refuel: FlightWaypoint | None, - lead_time: timedelta, - ) -> None: - super().__init__( - flight, - departure, - arrival, - divert, - bullseye, - nav_to, - nav_from, - patrol_start, - patrol_end, - patrol_duration, - patrol_speed, - engagement_distance, - ) - self.refuel = refuel - self.lead_time = lead_time - - @staticmethod - def builder_type() -> Type[Builder]: - return Builder +@dataclass(frozen=True) +class TarCapLayout(PatrollingLayout): + refuel: FlightWaypoint | None def iter_waypoints(self) -> Iterator[FlightWaypoint]: yield self.departure @@ -115,16 +70,44 @@ class TarCapFlightPlan(PatrollingFlightPlan): yield self.divert yield self.bullseye + +class TarCapFlightPlan(PatrollingFlightPlan[TarCapLayout]): + @property + def lead_time(self) -> timedelta: + return timedelta(minutes=2) + + @property + def patrol_duration(self) -> timedelta: + # Note that this duration only has an effect if there are no + # flights in the package that have requested escort. If the package + # requests an escort the CAP self.flight will remain on station for the + # duration of the escorted mission, or until it is winchester/bingo. + return self.flight.coalition.doctrine.cap_duration + + @property + def patrol_speed(self) -> Speed: + return self.flight.unit_type.preferred_patrol_speed( + self.layout.patrol_start.alt + ) + + @property + def engagement_distance(self) -> Distance: + return self.flight.coalition.doctrine.cap_engagement_range + + @staticmethod + def builder_type() -> Type[Builder]: + return Builder + @property def combat_speed_waypoints(self) -> set[FlightWaypoint]: - return {self.patrol_start, self.patrol_end} + return {self.layout.patrol_start, self.layout.patrol_end} @property def tot_offset(self) -> timedelta: return -self.lead_time def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: - if waypoint == self.patrol_end: + if waypoint == self.layout.patrol_end: return self.patrol_end_time return super().depart_time_for_waypoint(waypoint) diff --git a/game/ato/flightplans/theaterrefueling.py b/game/ato/flightplans/theaterrefueling.py index b9cb0e51..f40d3cf5 100644 --- a/game/ato/flightplans/theaterrefueling.py +++ b/game/ato/flightplans/theaterrefueling.py @@ -3,18 +3,16 @@ from __future__ import annotations from datetime import timedelta from typing import Type -from game.utils import Heading, feet, knots, meters, nautical_miles +from game.utils import Distance, Heading, Speed, feet, knots, meters, nautical_miles from .ibuilder import IBuilder -from .patrolling import PatrollingFlightPlan +from .patrolling import PatrollingFlightPlan, PatrollingLayout from .waypointbuilder import WaypointBuilder class Builder(IBuilder): - def build(self) -> TheaterRefuelingFlightPlan: + def build(self) -> PatrollingLayout: racetrack_half_distance = nautical_miles(20).meters - patrol_duration = timedelta(hours=1) - location = self.package.target closest_boundary = self.threat_zones.closest_boundary(location.position) @@ -53,17 +51,9 @@ class Builder(IBuilder): else: altitude = feet(21000) - # TODO: Could use self.flight.unit_type.preferred_patrol_speed(altitude). - if tanker_type.patrol_speed is not None: - speed = tanker_type.patrol_speed - else: - # ~280 knots IAS at 21000. - speed = knots(400) - racetrack = builder.race_track(racetrack_start, racetrack_end, altitude) - return TheaterRefuelingFlightPlan( - flight=self.flight, + return PatrollingLayout( departure=builder.takeoff(self.flight.departure), nav_to=builder.nav_path( self.flight.departure.position, racetrack_start, altitude @@ -76,15 +66,28 @@ class Builder(IBuilder): arrival=builder.land(self.flight.arrival), divert=builder.divert(self.flight.divert), bullseye=builder.bullseye(), - patrol_duration=patrol_duration, - patrol_speed=speed, - # TODO: Factor out a common base of the combat and non-combat race-tracks. - # No harm in setting this, but we ought to clean up a bit. - engagement_distance=meters(0), ) -class TheaterRefuelingFlightPlan(PatrollingFlightPlan): +class TheaterRefuelingFlightPlan(PatrollingFlightPlan[PatrollingLayout]): @staticmethod def builder_type() -> Type[Builder]: return Builder + + @property + def patrol_duration(self) -> timedelta: + return timedelta(hours=1) + + @property + def patrol_speed(self) -> Speed: + # TODO: Could use self.flight.unit_type.preferred_patrol_speed(altitude). + if self.flight.unit_type.patrol_speed is not None: + return self.flight.unit_type.patrol_speed + # ~280 knots IAS at 21000. + return knots(400) + + @property + def engagement_distance(self) -> Distance: + # TODO: Factor out a common base of the combat and non-combat race-tracks. + # No harm in setting this, but we ought to clean up a bit. + return meters(0) diff --git a/game/missiongenerator/aircraft/waypoints/casingress.py b/game/missiongenerator/aircraft/waypoints/casingress.py index 55a25ebc..d63b20dc 100644 --- a/game/missiongenerator/aircraft/waypoints/casingress.py +++ b/game/missiongenerator/aircraft/waypoints/casingress.py @@ -13,7 +13,7 @@ class CasIngressBuilder(PydcsWaypointBuilder): if isinstance(self.flight.flight_plan, CasFlightPlan): waypoint.add_task( EngageTargetsInZone( - position=self.flight.flight_plan.target.position, + position=self.flight.flight_plan.layout.target.position, radius=int(self.flight.flight_plan.engagement_distance.meters), targets=[ Targets.All.GroundUnits.GroundVehicles, diff --git a/game/server/flights/routes.py b/game/server/flights/routes.py index 4ec0684f..886e3fa0 100644 --- a/game/server/flights/routes.py +++ b/game/server/flights/routes.py @@ -4,8 +4,8 @@ from fastapi import APIRouter, Depends from shapely.geometry import LineString, Point as ShapelyPoint from game import Game -from game.ato.flightplans.patrolling import PatrollingFlightPlan from game.ato.flightplans.cas import CasFlightPlan +from game.ato.flightplans.patrolling import PatrollingFlightPlan from game.server import GameContext from game.server.flights.models import FlightJs from game.server.leaflet import LeafletPoly, ShapelyUtil @@ -41,10 +41,10 @@ def commit_boundary( flight = game.db.flights.get(flight_id) if not isinstance(flight.flight_plan, PatrollingFlightPlan): return [] - start = flight.flight_plan.patrol_start - end = flight.flight_plan.patrol_end + start = flight.flight_plan.layout.patrol_start + end = flight.flight_plan.layout.patrol_end if isinstance(flight.flight_plan, CasFlightPlan): - center = flight.flight_plan.target.position + center = flight.flight_plan.layout.target.position commit_center = ShapelyPoint(center.x, center.y) else: commit_center = LineString(