Dan's massive refactor

Squashing 8 commits by DanAlbert:

- Track theater in ControlPoint.
Simplifies finding the owning theater of a control point. Not used yet.

- Clean some cruft out of FlightPlanBuilder.
- Clean up silly some exception handling.
- Move FlightPlan instantiation into the builder.
I'm working on moving the builder to be owned by the Flight, which will simplify callers that need to create (or recreate) flight plans for a flight.

- Simplify IBuilder constructor.
We have access to the theater via the flight's departure airbase now.

- 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.

- Remove superfluous constructors.
- Remove unused Package field.
This commit is contained in:
Raffson 2022-08-24 19:25:30 +02:00
parent ede4ce1362
commit 48938fc529
No known key found for this signature in database
GPG Key ID: B0402B2C9B764D99
40 changed files with 787 additions and 768 deletions

View File

View File

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

View File

@ -9,8 +9,31 @@ from game.ato.flightplans.waypointbuilder import WaypointBuilder
from game.utils import Distance, Heading, Speed, feet, knots, meters, nautical_miles from game.utils import Distance, Heading, Speed, feet, knots, meters, nautical_miles
class Builder(IBuilder): class AewcFlightPlan(PatrollingFlightPlan[PatrollingLayout]):
def build(self) -> 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[Builder]:
return Builder
class Builder(IBuilder[AewcFlightPlan, PatrollingLayout]):
def layout(self) -> PatrollingLayout:
racetrack_half_distance = nautical_miles(30).meters racetrack_half_distance = nautical_miles(30).meters
location = self.package.target location = self.package.target
@ -67,25 +90,5 @@ class Builder(IBuilder):
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
) )
def build(self) -> AewcFlightPlan:
class AewcFlightPlan(PatrollingFlightPlan[PatrollingLayout]): return AewcFlightPlan(self.flight, self.layout())
@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

View File

@ -2,7 +2,8 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from typing import TYPE_CHECKING, Iterator, Type from typing import Iterator, TYPE_CHECKING, Type
from game.ato.flightplans.airlift import AirliftLayout from game.ato.flightplans.airlift import AirliftLayout
from game.ato.flightplans.standard import StandardFlightPlan from game.ato.flightplans.standard import StandardFlightPlan
from game.theater.controlpoint import ControlPointType from game.theater.controlpoint import ControlPointType
@ -12,12 +13,57 @@ from .ibuilder import IBuilder
from .waypointbuilder import WaypointBuilder from .waypointbuilder import WaypointBuilder
if TYPE_CHECKING: if TYPE_CHECKING:
from ..flight import Flight
from ..flightwaypoint import FlightWaypoint from ..flightwaypoint import FlightWaypoint
class Builder(IBuilder): @dataclass(frozen=True)
def build(self) -> AirAssaultLayout: class AirAssaultLayout(AirliftLayout):
target: FlightWaypoint
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
yield from self.nav_to_pickup
if self.pickup:
yield self.pickup
yield from self.nav_to_drop_off
yield self.drop_off
yield self.target
yield from self.nav_to_home
yield self.arrival
if self.divert is not None:
yield self.divert
yield self.bullseye
class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout]):
@staticmethod
def builder_type() -> Type[Builder]:
return Builder
@property
def tot_waypoint(self) -> FlightWaypoint | None:
return self.layout.drop_off
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
if waypoint == self.tot_waypoint:
return self.tot
return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
return None
@property
def engagement_distance(self) -> Distance:
# The radius of the WaypointZone created at the target location
return meters(2500)
@property
def mission_departure_time(self) -> timedelta:
return self.package.time_over_target
class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
def layout(self) -> AirAssaultLayout:
altitude = feet(1500) if self.flight.is_helo else self.doctrine.ingress_altitude altitude = feet(1500) if self.flight.is_helo else self.doctrine.ingress_altitude
altitude_is_agl = self.flight.is_helo altitude_is_agl = self.flight.is_helo
@ -78,51 +124,5 @@ class Builder(IBuilder):
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
) )
def build(self) -> AirAssaultFlightPlan:
@dataclass(frozen=True) return AirAssaultFlightPlan(self.flight, self.layout())
class AirAssaultLayout(AirliftLayout):
target: FlightWaypoint
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
yield from self.nav_to_pickup
if self.pickup:
yield self.pickup
yield from self.nav_to_drop_off
yield self.drop_off
yield self.target
yield from self.nav_to_home
yield self.arrival
if self.divert is not None:
yield self.divert
yield self.bullseye
class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout]):
def __init__(self, flight: Flight, layout: AirAssaultLayout) -> None:
super().__init__(flight, layout)
@staticmethod
def builder_type() -> Type[Builder]:
return Builder
@property
def tot_waypoint(self) -> FlightWaypoint | None:
return self.layout.drop_off
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
if waypoint == self.tot_waypoint:
return self.tot
return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
return None
@property
def engagement_distance(self) -> Distance:
# The radius of the WaypointZone created at the target location
return meters(2500)
@property
def mission_departure_time(self) -> timedelta:
return self.package.time_over_target

View File

@ -4,8 +4,8 @@ from collections.abc import Iterator
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from typing import TYPE_CHECKING, Type from typing import TYPE_CHECKING, Type
from game.theater.missiontarget import MissionTarget
from game.theater.missiontarget import MissionTarget
from game.utils import feet from game.utils import feet
from .ibuilder import IBuilder from .ibuilder import IBuilder
from .planningerror import PlanningError from .planningerror import PlanningError
@ -13,12 +13,58 @@ from .standard import StandardFlightPlan, StandardLayout
from .waypointbuilder import WaypointBuilder from .waypointbuilder import WaypointBuilder
if TYPE_CHECKING: if TYPE_CHECKING:
from ..flight import Flight
from ..flightwaypoint import FlightWaypoint from ..flightwaypoint import FlightWaypoint
class Builder(IBuilder): @dataclass(frozen=True)
def build(self) -> AirliftLayout: class AirliftLayout(StandardLayout):
nav_to_pickup: list[FlightWaypoint]
pickup: FlightWaypoint | None
nav_to_drop_off: list[FlightWaypoint]
drop_off: FlightWaypoint
stopover: FlightWaypoint | None
nav_to_home: list[FlightWaypoint]
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
yield from self.nav_to_pickup
if self.pickup is not None:
yield self.pickup
yield from self.nav_to_drop_off
yield self.drop_off
if self.stopover is not None:
yield self.stopover
yield from self.nav_to_home
yield self.arrival
if self.divert is not None:
yield self.divert
yield self.bullseye
class AirliftFlightPlan(StandardFlightPlan[AirliftLayout]):
@staticmethod
def builder_type() -> Type[Builder]:
return Builder
@property
def tot_waypoint(self) -> FlightWaypoint | None:
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
# lines so no need to wait for escorts or for other missions to complete.
return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
return None
@property
def mission_departure_time(self) -> timedelta:
return self.package.time_over_target
class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
def layout(self) -> AirliftLayout:
cargo = self.flight.cargo cargo = self.flight.cargo
if cargo is None: if cargo is None:
raise PlanningError( raise PlanningError(
@ -97,52 +143,5 @@ class Builder(IBuilder):
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
) )
def build(self) -> AirliftFlightPlan:
@dataclass(frozen=True) return AirliftFlightPlan(self.flight, self.layout())
class AirliftLayout(StandardLayout):
nav_to_pickup: list[FlightWaypoint]
pickup: FlightWaypoint | None
nav_to_drop_off: list[FlightWaypoint]
drop_off: FlightWaypoint
stopover: FlightWaypoint | None
nav_to_home: list[FlightWaypoint]
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
yield from self.nav_to_pickup
if self.pickup is not None:
yield self.pickup
yield from self.nav_to_drop_off
yield self.drop_off
if self.stopover is not None:
yield self.stopover
yield from self.nav_to_home
yield self.arrival
if self.divert is not None:
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.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
# lines so no need to wait for escorts or for other missions to complete.
return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
return None
@property
def mission_departure_time(self) -> timedelta:
return self.package.time_over_target

View File

@ -20,8 +20,8 @@ class AntiShipFlightPlan(FormationAttackFlightPlan):
return Builder return Builder
class Builder(FormationAttackBuilder): class Builder(FormationAttackBuilder[AntiShipFlightPlan, FormationAttackLayout]):
def build(self) -> FormationAttackLayout: def layout(self) -> FormationAttackLayout:
location = self.package.target location = self.package.target
from game.transfers import CargoShip from game.transfers import CargoShip
@ -40,3 +40,6 @@ class Builder(FormationAttackBuilder):
@staticmethod @staticmethod
def anti_ship_targets_for_tgo(tgo: NavalGroundObject) -> list[StrikeTarget]: def anti_ship_targets_for_tgo(tgo: NavalGroundObject) -> list[StrikeTarget]:
return [StrikeTarget(f"{g.group_name} at {tgo.name}", g) for g in tgo.groups] return [StrikeTarget(f"{g.group_name} at {tgo.name}", g) for g in tgo.groups]
def build(self) -> AntiShipFlightPlan:
return AntiShipFlightPlan(self.flight, self.layout())

View File

@ -19,8 +19,8 @@ class BaiFlightPlan(FormationAttackFlightPlan):
return Builder return Builder
class Builder(FormationAttackBuilder): class Builder(FormationAttackBuilder[BaiFlightPlan, FormationAttackLayout]):
def build(self) -> FormationAttackLayout: def layout(self) -> FormationAttackLayout:
location = self.package.target location = self.package.target
from game.transfers import Convoy from game.transfers import Convoy
@ -38,3 +38,6 @@ class Builder(FormationAttackBuilder):
raise InvalidObjectiveLocation(self.flight.flight_type, location) raise InvalidObjectiveLocation(self.flight.flight_type, location)
return self._build(FlightWaypointType.INGRESS_BAI, targets) return self._build(FlightWaypointType.INGRESS_BAI, targets)
def build(self) -> BaiFlightPlan:
return BaiFlightPlan(self.flight, self.layout())

View File

@ -12,8 +12,28 @@ from .patrolling import PatrollingFlightPlan, PatrollingLayout
from .waypointbuilder import WaypointBuilder from .waypointbuilder import WaypointBuilder
class Builder(CapBuilder): class BarCapFlightPlan(PatrollingFlightPlan[PatrollingLayout]):
def build(self) -> 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
class Builder(CapBuilder[BarCapFlightPlan, PatrollingLayout]):
def layout(self) -> PatrollingLayout:
location = self.package.target location = self.package.target
if isinstance(location, FrontLine): if isinstance(location, FrontLine):
@ -46,22 +66,5 @@ class Builder(CapBuilder):
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
) )
def build(self) -> BarCapFlightPlan:
class BarCapFlightPlan(PatrollingFlightPlan[PatrollingLayout]): return BarCapFlightPlan(self.flight, self.layout())
@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

View File

@ -2,12 +2,14 @@ from __future__ import annotations
import random import random
from abc import ABC from abc import ABC
from typing import TYPE_CHECKING from typing import Any, TYPE_CHECKING, TypeVar
from dcs import Point from dcs import Point
from shapely.geometry import Point as ShapelyPoint from shapely.geometry import Point as ShapelyPoint
from game.utils import Heading, meters, nautical_miles from game.utils import Heading, meters, nautical_miles
from .flightplan import FlightPlan
from .patrolling import PatrollingLayout
from ..closestairfields import ObjectiveDistanceCache from ..closestairfields import ObjectiveDistanceCache
from ..flightplans.ibuilder import IBuilder from ..flightplans.ibuilder import IBuilder
from ..flightplans.planningerror import PlanningError from ..flightplans.planningerror import PlanningError
@ -15,8 +17,11 @@ from ..flightplans.planningerror import PlanningError
if TYPE_CHECKING: if TYPE_CHECKING:
from game.theater import MissionTarget from game.theater import MissionTarget
FlightPlanT = TypeVar("FlightPlanT", bound=FlightPlan[Any])
LayoutT = TypeVar("LayoutT", bound=PatrollingLayout)
class CapBuilder(IBuilder, ABC):
class CapBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
def cap_racetrack_for_objective( def cap_racetrack_for_objective(
self, location: MissionTarget, barcap: bool self, location: MissionTarget, barcap: bool
) -> tuple[Point, Point]: ) -> tuple[Point, Point]:

View File

@ -17,8 +17,58 @@ if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint from ..flightwaypoint import FlightWaypoint
class Builder(IBuilder): @dataclass(frozen=True)
def build(self) -> CasLayout: class CasLayout(PatrollingLayout):
target: FlightWaypoint
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
yield from self.nav_to
yield self.patrol_start
yield self.target
yield self.patrol_end
yield from self.nav_from
yield self.departure
if self.divert is not None:
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.layout.patrol_start, self.layout.target, self.layout.patrol_end}
def request_escort_at(self) -> FlightWaypoint | None:
return self.layout.patrol_start
def dismiss_escort_at(self) -> FlightWaypoint | None:
return self.layout.patrol_end
class Builder(IBuilder[CasFlightPlan, CasLayout]):
def layout(self) -> CasLayout:
location = self.package.target location = self.package.target
if not isinstance(location, FrontLine): if not isinstance(location, FrontLine):
@ -71,52 +121,5 @@ class Builder(IBuilder):
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
) )
def build(self) -> CasFlightPlan:
@dataclass(frozen=True) return CasFlightPlan(self.flight, self.layout())
class CasLayout(PatrollingLayout):
target: FlightWaypoint
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
yield from self.nav_to
yield self.patrol_start
yield self.target
yield self.patrol_end
yield from self.nav_from
yield self.departure
if self.divert is not None:
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.layout.patrol_start, self.layout.target, self.layout.patrol_end}
def request_escort_at(self) -> FlightWaypoint | None:
return self.layout.patrol_start
def dismiss_escort_at(self) -> FlightWaypoint | None:
return self.layout.patrol_end

View File

@ -13,11 +13,6 @@ if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint from ..flightwaypoint import FlightWaypoint
class Builder(IBuilder):
def build(self) -> CustomLayout:
return CustomLayout([])
@dataclass(frozen=True) @dataclass(frozen=True)
class CustomLayout(Layout): class CustomLayout(Layout):
custom_waypoints: list[FlightWaypoint] custom_waypoints: list[FlightWaypoint]
@ -55,3 +50,11 @@ class CustomFlightPlan(FlightPlan[CustomLayout]):
@property @property
def mission_departure_time(self) -> timedelta: def mission_departure_time(self) -> timedelta:
return self.package.time_over_target return self.package.time_over_target
class Builder(IBuilder[CustomFlightPlan, CustomLayout]):
def layout(self) -> CustomLayout:
return CustomLayout([])
def build(self) -> CustomFlightPlan:
return CustomFlightPlan(self.flight, self.layout())

View File

@ -22,8 +22,8 @@ class DeadFlightPlan(FormationAttackFlightPlan):
return Builder return Builder
class Builder(FormationAttackBuilder): class Builder(FormationAttackBuilder[DeadFlightPlan, FormationAttackLayout]):
def build(self) -> FormationAttackLayout: def layout(self) -> FormationAttackLayout:
location = self.package.target location = self.package.target
is_ewr = isinstance(location, EwrGroundObject) is_ewr = isinstance(location, EwrGroundObject)
@ -36,3 +36,6 @@ class Builder(FormationAttackBuilder):
raise InvalidObjectiveLocation(self.flight.flight_type, location) raise InvalidObjectiveLocation(self.flight.flight_type, location)
return self._build(FlightWaypointType.INGRESS_DEAD) return self._build(FlightWaypointType.INGRESS_DEAD)
def build(self) -> DeadFlightPlan:
return DeadFlightPlan(self.flight, self.layout())

View File

@ -16,8 +16,8 @@ class EscortFlightPlan(FormationAttackFlightPlan):
return Builder return Builder
class Builder(FormationAttackBuilder): class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
def build(self) -> FormationAttackLayout: def layout(self) -> FormationAttackLayout:
assert self.package.waypoints is not None assert self.package.waypoints is not None
builder = WaypointBuilder(self.flight, self.coalition) builder = WaypointBuilder(self.flight, self.coalition)
@ -51,3 +51,6 @@ class Builder(FormationAttackBuilder):
divert=builder.divert(self.flight.divert), divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
) )
def build(self) -> EscortFlightPlan:
return EscortFlightPlan(self.flight, self.layout())

View File

@ -15,36 +15,6 @@ if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint from ..flightwaypoint import FlightWaypoint
class Builder(IBuilder):
def build(self) -> FerryLayout:
if self.flight.departure == self.flight.arrival:
raise PlanningError(
f"Cannot plan ferry self.flight: departure and arrival are both "
f"{self.flight.departure}"
)
altitude_is_agl = self.flight.unit_type.dcs_unit_type.helicopter
altitude = (
feet(1500)
if altitude_is_agl
else self.flight.unit_type.preferred_patrol_altitude
)
builder = WaypointBuilder(self.flight, self.coalition)
return FerryLayout(
departure=builder.takeoff(self.flight.departure),
nav_to_destination=builder.nav_path(
self.flight.departure.position,
self.flight.arrival.position,
altitude,
altitude_is_agl,
),
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
)
@dataclass(frozen=True) @dataclass(frozen=True)
class FerryLayout(StandardLayout): class FerryLayout(StandardLayout):
nav_to_destination: list[FlightWaypoint] nav_to_destination: list[FlightWaypoint]
@ -78,3 +48,36 @@ class FerryFlightPlan(StandardFlightPlan[FerryLayout]):
@property @property
def mission_departure_time(self) -> timedelta: def mission_departure_time(self) -> timedelta:
return self.package.time_over_target return self.package.time_over_target
class Builder(IBuilder[FerryFlightPlan, FerryLayout]):
def layout(self) -> FerryLayout:
if self.flight.departure == self.flight.arrival:
raise PlanningError(
f"Cannot plan ferry self.flight: departure and arrival are both "
f"{self.flight.departure}"
)
altitude_is_agl = self.flight.unit_type.dcs_unit_type.helicopter
altitude = (
feet(1500)
if altitude_is_agl
else self.flight.unit_type.preferred_patrol_altitude
)
builder = WaypointBuilder(self.flight, self.coalition)
return FerryLayout(
departure=builder.takeoff(self.flight.departure),
nav_to_destination=builder.nav_path(
self.flight.departure.position,
self.flight.arrival.position,
altitude,
altitude_is_agl,
),
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
)
def build(self) -> FerryFlightPlan:
return FerryFlightPlan(self.flight, self.layout())

View File

@ -8,15 +8,14 @@ generating the waypoints for the mission.
from __future__ import annotations from __future__ import annotations
import math import math
from abc import ABC, abstractmethod from abc import ABC
from collections.abc import Iterator from collections.abc import Iterator
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, Type, TypeGuard, TypeVar from typing import Any, Generic, TYPE_CHECKING, TypeGuard, TypeVar
from game.typeguard import self_type_guard from game.typeguard import self_type_guard
from game.utils import Distance, Speed, meters from game.utils import Distance, Speed, meters
from .ibuilder import IBuilder
from .planningerror import PlanningError from .planningerror import PlanningError
from ..flightwaypointtype import FlightWaypointType from ..flightwaypointtype import FlightWaypointType
from ..starttype import StartType from ..starttype import StartType
@ -64,11 +63,6 @@ class FlightPlan(ABC, Generic[LayoutT]):
def package(self) -> Package: def package(self) -> Package:
return self.flight.package return self.flight.package
@staticmethod
@abstractmethod
def builder_type() -> Type[IBuilder]:
...
@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."""

View File

@ -1,196 +0,0 @@
from __future__ import annotations
from typing import Any, TYPE_CHECKING, Type
from game.ato import FlightType
from game.ato.closestairfields import ObjectiveDistanceCache
from game.data.doctrine import Doctrine
from game.flightplan import IpZoneGeometry, JoinZoneGeometry
from game.flightplan.refuelzonegeometry import RefuelZoneGeometry
from .aewc import AewcFlightPlan
from .airassault import AirAssaultFlightPlan
from .airlift import AirliftFlightPlan
from .antiship import AntiShipFlightPlan
from .bai import BaiFlightPlan
from .barcap import BarCapFlightPlan
from .cas import CasFlightPlan
from .dead import DeadFlightPlan
from .escort import EscortFlightPlan
from .ferry import FerryFlightPlan
from .flightplan import FlightPlan
from .ocaaircraft import OcaAircraftFlightPlan
from .ocarunway import OcaRunwayFlightPlan
from .packagerefueling import PackageRefuelingFlightPlan
from .planningerror import PlanningError
from .sead import SeadFlightPlan
from .strike import StrikeFlightPlan
from .sweep import SweepFlightPlan
from .tarcap import TarCapFlightPlan
from .theaterrefueling import TheaterRefuelingFlightPlan
from .waypointbuilder import WaypointBuilder
if TYPE_CHECKING:
from game.ato import Flight, FlightWaypoint, Package
from game.coalition import Coalition
from game.theater import ConflictTheater, ControlPoint, FrontLine
from game.threatzones import ThreatZones
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
@property
def doctrine(self) -> Doctrine:
return self.coalition.doctrine
@property
def threat_zones(self) -> ThreatZones:
return self.coalition.opponent.threat_zone
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.regenerate_package_waypoints()
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 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
):
return TheaterRefuelingFlightPlan
return PackageRefuelingFlightPlan
plan_dict: dict[FlightType, Type[FlightPlan[Any]]] = {
FlightType.ANTISHIP: AntiShipFlightPlan,
FlightType.BAI: BaiFlightPlan,
FlightType.BARCAP: BarCapFlightPlan,
FlightType.CAS: CasFlightPlan,
FlightType.DEAD: DeadFlightPlan,
FlightType.ESCORT: EscortFlightPlan,
FlightType.OCA_AIRCRAFT: OcaAircraftFlightPlan,
FlightType.OCA_RUNWAY: OcaRunwayFlightPlan,
FlightType.SEAD: SeadFlightPlan,
FlightType.SEAD_ESCORT: EscortFlightPlan,
FlightType.STRIKE: StrikeFlightPlan,
FlightType.SWEEP: SweepFlightPlan,
FlightType.TARCAP: TarCapFlightPlan,
FlightType.AEWC: AewcFlightPlan,
FlightType.TRANSPORT: AirliftFlightPlan,
FlightType.FERRY: FerryFlightPlan,
FlightType.AIR_ASSAULT: AirAssaultFlightPlan,
}
return plan_dict.get(task)
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"
)
layout = plan_type.builder_type()(flight, self.theater).build()
return plan_type(flight, layout)
def regenerate_flight_plans(self) -> None:
new_flights: list[Flight] = []
for old_flight in self.package.flights:
old_flight.flight_plan = self.generate_flight_plan(old_flight)
new_flights.append(old_flight)
self.package.flights = new_flights
def regenerate_package_waypoints(self) -> None:
from game.ato.packagewaypoints import PackageWaypoints
package_airfield = self.package_airfield()
# Start by picking the best IP for the attack.
ingress_point = IpZoneGeometry(
self.package.target.position,
package_airfield.position,
self.coalition,
).find_best_ip()
join_point = JoinZoneGeometry(
self.package.target.position,
package_airfield.position,
ingress_point,
self.coalition,
).find_best_join_point()
refuel_point = RefuelZoneGeometry(
package_airfield.position,
join_point,
self.coalition,
).find_best_refuel_point()
# And the split point based on the best route from the IP. Since that's no
# different than the best route *to* the IP, this is the same as the join point.
# TODO: Estimate attack completion point based on the IP and split from there?
self.package.waypoints = PackageWaypoints(
WaypointBuilder.perturb(join_point),
ingress_point,
WaypointBuilder.perturb(join_point),
refuel_point,
)
# TODO: Make a model for the waypoint builder and use that in the UI.
def generate_rtb_waypoint(
self, flight: Flight, arrival: ControlPoint
) -> FlightWaypoint:
"""Generate RTB landing point.
Args:
flight: The flight to generate the landing waypoint for.
arrival: Arrival airfield or carrier.
"""
builder = WaypointBuilder(flight, self.coalition)
return builder.land(arrival)
def package_airfield(self) -> ControlPoint:
# We'll always have a package, but if this is being planned via the UI
# it could be the first flight in the package.
if not self.package.flights:
raise PlanningError(
"Cannot determine source airfield for package with no flights"
)
# The package airfield is either the flight's airfield (when there is no
# package) or the closest airfield to the objective that is the
# departure airfield for some flight in the package.
cache = ObjectiveDistanceCache.get_closest_airfields(self.package.target)
for airfield in cache.operational_airfields:
for flight in self.package.flights:
if flight.departure == airfield:
return airfield
raise PlanningError("Could not find any airfield assigned to this package")

View File

@ -0,0 +1,66 @@
from __future__ import annotations
from typing import Any, TYPE_CHECKING, Type
from game.ato import FlightType
from .aewc import AewcFlightPlan
from .airassault import AirAssaultFlightPlan
from .airlift import AirliftFlightPlan
from .antiship import AntiShipFlightPlan
from .bai import BaiFlightPlan
from .barcap import BarCapFlightPlan
from .cas import CasFlightPlan
from .dead import DeadFlightPlan
from .escort import EscortFlightPlan
from .ferry import FerryFlightPlan
from .ibuilder import IBuilder
from .ocaaircraft import OcaAircraftFlightPlan
from .ocarunway import OcaRunwayFlightPlan
from .packagerefueling import PackageRefuelingFlightPlan
from .planningerror import PlanningError
from .sead import SeadFlightPlan
from .strike import StrikeFlightPlan
from .sweep import SweepFlightPlan
from .tarcap import TarCapFlightPlan
from .theaterrefueling import TheaterRefuelingFlightPlan
if TYPE_CHECKING:
from game.ato import Flight
from game.theater import FrontLine
class FlightPlanBuilderTypes:
@staticmethod
def for_flight(flight: Flight) -> Type[IBuilder[Any, Any]]:
if flight.flight_type is FlightType.REFUELING:
if flight.package.target.is_friendly(flight.squadron.player) or isinstance(
flight.package.target, FrontLine
):
return TheaterRefuelingFlightPlan.builder_type()
return PackageRefuelingFlightPlan.builder_type()
builder_dict: dict[FlightType, Type[IBuilder[Any, Any]]] = {
FlightType.ANTISHIP: AntiShipFlightPlan.builder_type(),
FlightType.BAI: BaiFlightPlan.builder_type(),
FlightType.BARCAP: BarCapFlightPlan.builder_type(),
FlightType.CAS: CasFlightPlan.builder_type(),
FlightType.DEAD: DeadFlightPlan.builder_type(),
FlightType.ESCORT: EscortFlightPlan.builder_type(),
FlightType.OCA_AIRCRAFT: OcaAircraftFlightPlan.builder_type(),
FlightType.OCA_RUNWAY: OcaRunwayFlightPlan.builder_type(),
FlightType.SEAD: SeadFlightPlan.builder_type(),
FlightType.SEAD_ESCORT: EscortFlightPlan.builder_type(),
FlightType.STRIKE: StrikeFlightPlan.builder_type(),
FlightType.SWEEP: SweepFlightPlan.builder_type(),
FlightType.TARCAP: TarCapFlightPlan.builder_type(),
FlightType.AEWC: AewcFlightPlan.builder_type(),
FlightType.TRANSPORT: AirliftFlightPlan.builder_type(),
FlightType.FERRY: FerryFlightPlan.builder_type(),
FlightType.AIR_ASSAULT: AirAssaultFlightPlan.builder_type(),
}
try:
return builder_dict[flight.flight_type]
except KeyError as ex:
raise PlanningError(
f"{flight.flight_type} flight plan generation not implemented"
) from ex

View File

@ -11,6 +11,7 @@ from dcs import Point
from game.flightplan import HoldZoneGeometry from game.flightplan import HoldZoneGeometry
from game.theater import MissionTarget from game.theater import MissionTarget
from game.utils import Speed, meters from game.utils import Speed, meters
from .flightplan import FlightPlan
from .formation import FormationFlightPlan, FormationLayout from .formation import FormationFlightPlan, FormationLayout
from .ibuilder import IBuilder from .ibuilder import IBuilder
from .planningerror import PlanningError from .planningerror import PlanningError
@ -151,10 +152,11 @@ class FormationAttackLayout(FormationLayout):
yield self.bullseye yield self.bullseye
FlightPlanT = TypeVar("FlightPlanT", bound=FlightPlan[FormationAttackLayout])
LayoutT = TypeVar("LayoutT", bound=FormationAttackLayout) LayoutT = TypeVar("LayoutT", bound=FormationAttackLayout)
class FormationAttackBuilder(IBuilder, ABC): class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
def _build( def _build(
self, self,
ingress_type: FlightWaypointType, ingress_type: FlightWaypointType,

View File

@ -1,7 +1,9 @@
from __future__ import annotations from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import TYPE_CHECKING from typing import Any, Generic, TYPE_CHECKING, TypeVar
from .flightplan import FlightPlan, Layout
if TYPE_CHECKING: if TYPE_CHECKING:
from game.coalition import Coalition from game.coalition import Coalition
@ -10,16 +12,26 @@ if TYPE_CHECKING:
from game.threatzones import ThreatZones from game.threatzones import ThreatZones
from ..flight import Flight from ..flight import Flight
from ..package import Package from ..package import Package
from .flightplan import Layout
class IBuilder(ABC): FlightPlanT = TypeVar("FlightPlanT", bound=FlightPlan[Any])
def __init__(self, flight: Flight, theater: ConflictTheater) -> None: LayoutT = TypeVar("LayoutT", bound=Layout)
class IBuilder(ABC, Generic[FlightPlanT, LayoutT]):
def __init__(self, flight: Flight) -> None:
self.flight = flight self.flight = flight
self.theater = theater
@property
def theater(self) -> ConflictTheater:
return self.flight.departure.theater
@abstractmethod @abstractmethod
def build(self) -> Layout: def layout(self) -> LayoutT:
...
@abstractmethod
def build(self) -> FlightPlanT:
... ...
@property @property

View File

@ -19,8 +19,8 @@ class OcaAircraftFlightPlan(FormationAttackFlightPlan):
return Builder return Builder
class Builder(FormationAttackBuilder): class Builder(FormationAttackBuilder[OcaAircraftFlightPlan, FormationAttackLayout]):
def build(self) -> FormationAttackLayout: def layout(self) -> FormationAttackLayout:
location = self.package.target location = self.package.target
if not isinstance(location, Airfield): if not isinstance(location, Airfield):
@ -31,3 +31,6 @@ class Builder(FormationAttackBuilder):
raise InvalidObjectiveLocation(self.flight.flight_type, location) raise InvalidObjectiveLocation(self.flight.flight_type, location)
return self._build(FlightWaypointType.INGRESS_OCA_AIRCRAFT) return self._build(FlightWaypointType.INGRESS_OCA_AIRCRAFT)
def build(self) -> OcaAircraftFlightPlan:
return OcaAircraftFlightPlan(self.flight, self.layout())

View File

@ -19,8 +19,8 @@ class OcaRunwayFlightPlan(FormationAttackFlightPlan):
return Builder return Builder
class Builder(FormationAttackBuilder): class Builder(FormationAttackBuilder[OcaRunwayFlightPlan, FormationAttackLayout]):
def build(self) -> FormationAttackLayout: def layout(self) -> FormationAttackLayout:
location = self.package.target location = self.package.target
if not isinstance(location, Airfield): if not isinstance(location, Airfield):
@ -31,3 +31,6 @@ class Builder(FormationAttackBuilder):
raise InvalidObjectiveLocation(self.flight.flight_type, location) raise InvalidObjectiveLocation(self.flight.flight_type, location)
return self._build(FlightWaypointType.INGRESS_OCA_RUNWAY) return self._build(FlightWaypointType.INGRESS_OCA_RUNWAY)
def build(self) -> OcaRunwayFlightPlan:
return OcaRunwayFlightPlan(self.flight, self.layout())

View File

@ -6,65 +6,15 @@ from typing import Type
from dcs import Point from dcs import Point
from game.utils import Distance, Heading, feet, meters from game.utils import Distance, Heading, feet, meters
from .ibuilder import IBuilder
from .patrolling import PatrollingLayout from .patrolling import PatrollingLayout
from .theaterrefueling import ( from .refuelingflightplan import RefuelingFlightPlan
Builder as TheaterRefuelingBuilder,
TheaterRefuelingFlightPlan,
)
from .waypointbuilder import WaypointBuilder from .waypointbuilder import WaypointBuilder
from ..flightwaypoint import FlightWaypoint from ..flightwaypoint import FlightWaypoint
from ..flightwaypointtype import FlightWaypointType from ..flightwaypointtype import FlightWaypointType
class Builder(TheaterRefuelingBuilder): class PackageRefuelingFlightPlan(RefuelingFlightPlan):
def build(self) -> PatrollingLayout:
package_waypoints = self.package.waypoints
assert package_waypoints is not None
racetrack_half_distance = Distance.from_nautical_miles(20).meters
racetrack_center = package_waypoints.refuel
split_heading = Heading.from_degrees(
racetrack_center.heading_between_point(package_waypoints.split)
)
home_heading = split_heading.opposite
racetrack_start = racetrack_center.point_from_heading(
split_heading.degrees, racetrack_half_distance
)
racetrack_end = racetrack_center.point_from_heading(
home_heading.degrees, racetrack_half_distance
)
builder = WaypointBuilder(self.flight, self.coalition)
tanker_type = self.flight.unit_type
if tanker_type.patrol_altitude is not None:
altitude = tanker_type.patrol_altitude
else:
altitude = feet(21000)
racetrack = builder.race_track(racetrack_start, racetrack_end, altitude)
return PatrollingLayout(
departure=builder.takeoff(self.flight.departure),
nav_to=builder.nav_path(
self.flight.departure.position, racetrack_start, altitude
),
nav_from=builder.nav_path(
racetrack_end, self.flight.arrival.position, altitude
),
patrol_start=racetrack[0],
patrol_end=racetrack[1],
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
)
class PackageRefuelingFlightPlan(TheaterRefuelingFlightPlan):
@staticmethod @staticmethod
def builder_type() -> Type[Builder]: def builder_type() -> Type[Builder]:
return Builder return Builder
@ -122,3 +72,54 @@ class PackageRefuelingFlightPlan(TheaterRefuelingFlightPlan):
+ delay_split_to_refuel + delay_split_to_refuel
- timedelta(minutes=1.5) - timedelta(minutes=1.5)
) )
class Builder(IBuilder[PackageRefuelingFlightPlan, PatrollingLayout]):
def layout(self) -> PatrollingLayout:
package_waypoints = self.package.waypoints
assert package_waypoints is not None
racetrack_half_distance = Distance.from_nautical_miles(20).meters
racetrack_center = package_waypoints.refuel
split_heading = Heading.from_degrees(
racetrack_center.heading_between_point(package_waypoints.split)
)
home_heading = split_heading.opposite
racetrack_start = racetrack_center.point_from_heading(
split_heading.degrees, racetrack_half_distance
)
racetrack_end = racetrack_center.point_from_heading(
home_heading.degrees, racetrack_half_distance
)
builder = WaypointBuilder(self.flight, self.coalition)
tanker_type = self.flight.unit_type
if tanker_type.patrol_altitude is not None:
altitude = tanker_type.patrol_altitude
else:
altitude = feet(21000)
racetrack = builder.race_track(racetrack_start, racetrack_end, altitude)
return PatrollingLayout(
departure=builder.takeoff(self.flight.departure),
nav_to=builder.nav_path(
self.flight.departure.position, racetrack_start, altitude
),
nav_from=builder.nav_path(
racetrack_end, self.flight.arrival.position, altitude
),
patrol_start=racetrack[0],
patrol_end=racetrack[1],
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
)
def build(self) -> PackageRefuelingFlightPlan:
return PackageRefuelingFlightPlan(self.flight, self.layout())

View File

@ -0,0 +1,20 @@
from abc import ABC
from game.utils import Distance, Speed, knots, meters
from .patrolling import PatrollingFlightPlan, PatrollingLayout
class RefuelingFlightPlan(PatrollingFlightPlan[PatrollingLayout], ABC):
@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)

View File

@ -15,42 +15,6 @@ if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint from ..flightwaypoint import FlightWaypoint
class Builder(IBuilder):
def build(self) -> RtbLayout:
if not isinstance(self.flight.state, InFlight):
raise RuntimeError(f"Cannot abort {self} because it is not in flight")
current_position = self.flight.state.estimate_position()
current_altitude, altitude_reference = self.flight.state.estimate_altitude()
altitude_is_agl = self.flight.unit_type.dcs_unit_type.helicopter
altitude = (
feet(1500)
if altitude_is_agl
else self.flight.unit_type.preferred_patrol_altitude
)
builder = WaypointBuilder(self.flight, self.flight.coalition)
abort_point = builder.nav(
current_position, current_altitude, altitude_reference == "RADIO"
)
abort_point.name = "ABORT AND RTB"
abort_point.pretty_name = "Abort and RTB"
abort_point.description = "Abort mission and return to base"
return RtbLayout(
departure=builder.takeoff(self.flight.departure),
abort_location=abort_point,
nav_to_destination=builder.nav_path(
current_position,
self.flight.arrival.position,
altitude,
altitude_is_agl,
),
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
)
@dataclass(frozen=True) @dataclass(frozen=True)
class RtbLayout(StandardLayout): class RtbLayout(StandardLayout):
abort_location: FlightWaypoint abort_location: FlightWaypoint
@ -88,3 +52,42 @@ class RtbFlightPlan(StandardFlightPlan[RtbLayout]):
@property @property
def mission_departure_time(self) -> timedelta: def mission_departure_time(self) -> timedelta:
return timedelta() return timedelta()
class Builder(IBuilder[RtbFlightPlan, RtbLayout]):
def layout(self) -> RtbLayout:
if not isinstance(self.flight.state, InFlight):
raise RuntimeError(f"Cannot abort {self} because it is not in flight")
current_position = self.flight.state.estimate_position()
current_altitude, altitude_reference = self.flight.state.estimate_altitude()
altitude_is_agl = self.flight.unit_type.dcs_unit_type.helicopter
altitude = (
feet(1500)
if altitude_is_agl
else self.flight.unit_type.preferred_patrol_altitude
)
builder = WaypointBuilder(self.flight, self.flight.coalition)
abort_point = builder.nav(
current_position, current_altitude, altitude_reference == "RADIO"
)
abort_point.name = "ABORT AND RTB"
abort_point.pretty_name = "Abort and RTB"
abort_point.description = "Abort mission and return to base"
return RtbLayout(
departure=builder.takeoff(self.flight.departure),
abort_location=abort_point,
nav_to_destination=builder.nav_path(
current_position,
self.flight.arrival.position,
altitude,
altitude_is_agl,
),
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
)
def build(self) -> RtbFlightPlan:
return RtbFlightPlan(self.flight, self.layout())

View File

@ -8,14 +8,10 @@ from .formationattack import (
FormationAttackFlightPlan, FormationAttackFlightPlan,
FormationAttackLayout, FormationAttackLayout,
) )
from .. import Flight
from ..flightwaypointtype import FlightWaypointType from ..flightwaypointtype import FlightWaypointType
class SeadFlightPlan(FormationAttackFlightPlan): class SeadFlightPlan(FormationAttackFlightPlan):
def __init__(self, flight: Flight, layout: FormationAttackLayout) -> None:
super().__init__(flight, layout)
@staticmethod @staticmethod
def builder_type() -> Type[Builder]: def builder_type() -> Type[Builder]:
return Builder return Builder
@ -25,6 +21,9 @@ class SeadFlightPlan(FormationAttackFlightPlan):
return timedelta(minutes=1) return timedelta(minutes=1)
class Builder(FormationAttackBuilder): class Builder(FormationAttackBuilder[SeadFlightPlan, FormationAttackLayout]):
def build(self) -> FormationAttackLayout: def layout(self) -> FormationAttackLayout:
return self._build(FlightWaypointType.INGRESS_SEAD) return self._build(FlightWaypointType.INGRESS_SEAD)
def build(self) -> SeadFlightPlan:
return SeadFlightPlan(self.flight, self.layout())

View File

@ -19,8 +19,8 @@ class StrikeFlightPlan(FormationAttackFlightPlan):
return Builder return Builder
class Builder(FormationAttackBuilder): class Builder(FormationAttackBuilder[StrikeFlightPlan, FormationAttackLayout]):
def build(self) -> FormationAttackLayout: def layout(self) -> FormationAttackLayout:
location = self.package.target location = self.package.target
if not isinstance(location, TheaterGroundObject): if not isinstance(location, TheaterGroundObject):
@ -31,3 +31,6 @@ class Builder(FormationAttackBuilder):
targets.append(StrikeTarget(f"{unit.type.id} #{idx}", unit)) targets.append(StrikeTarget(f"{unit.type.id} #{idx}", unit))
return self._build(FlightWaypointType.INGRESS_STRIKE, targets) return self._build(FlightWaypointType.INGRESS_STRIKE, targets)
def build(self) -> StrikeFlightPlan:
return StrikeFlightPlan(self.flight, self.layout())

View File

@ -17,57 +17,6 @@ if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint from ..flightwaypoint import FlightWaypoint
class Builder(IBuilder):
def build(self) -> SweepLayout:
assert self.package.waypoints is not None
target = self.package.target.position
heading = Heading.from_degrees(
self.package.waypoints.join.heading_between_point(target)
)
start_pos = target.point_from_heading(
heading.degrees, -self.doctrine.sweep_distance.meters
)
builder = WaypointBuilder(self.flight, self.coalition)
start, end = builder.sweep(start_pos, target, self.doctrine.ingress_altitude)
hold = builder.hold(self._hold_point())
refuel = None
if self.package.waypoints is not None:
refuel = builder.refuel(self.package.waypoints.refuel)
return SweepLayout(
departure=builder.takeoff(self.flight.departure),
hold=hold,
nav_to=builder.nav_path(
hold.position, start.position, self.doctrine.ingress_altitude
),
nav_from=builder.nav_path(
end.position,
self.flight.arrival.position,
self.doctrine.ingress_altitude,
),
sweep_start=start,
sweep_end=end,
refuel=refuel,
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
)
def _hold_point(self) -> Point:
assert self.package.waypoints is not None
origin = self.flight.departure.position
target = self.package.target.position
join = self.package.waypoints.join
ip = self.package.waypoints.ingress
return HoldZoneGeometry(
target, origin, ip, join, self.coalition, self.theater
).find_best_hold_point()
@dataclass(frozen=True) @dataclass(frozen=True)
class SweepLayout(LoiterLayout): class SweepLayout(LoiterLayout):
nav_to: list[FlightWaypoint] nav_to: list[FlightWaypoint]
@ -145,3 +94,57 @@ class SweepFlightPlan(LoiterFlightPlan):
def mission_departure_time(self) -> timedelta: def mission_departure_time(self) -> timedelta:
return self.sweep_end_time return self.sweep_end_time
class Builder(IBuilder[SweepFlightPlan, SweepLayout]):
def layout(self) -> SweepLayout:
assert self.package.waypoints is not None
target = self.package.target.position
heading = Heading.from_degrees(
self.package.waypoints.join.heading_between_point(target)
)
start_pos = target.point_from_heading(
heading.degrees, -self.doctrine.sweep_distance.meters
)
builder = WaypointBuilder(self.flight, self.coalition)
start, end = builder.sweep(start_pos, target, self.doctrine.ingress_altitude)
hold = builder.hold(self._hold_point())
refuel = None
if self.package.waypoints is not None:
refuel = builder.refuel(self.package.waypoints.refuel)
return SweepLayout(
departure=builder.takeoff(self.flight.departure),
hold=hold,
nav_to=builder.nav_path(
hold.position, start.position, self.doctrine.ingress_altitude
),
nav_from=builder.nav_path(
end.position,
self.flight.arrival.position,
self.doctrine.ingress_altitude,
),
sweep_start=start,
sweep_end=end,
refuel=refuel,
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
)
def _hold_point(self) -> Point:
assert self.package.waypoints is not None
origin = self.flight.departure.position
target = self.package.target.position
join = self.package.waypoints.join
ip = self.package.waypoints.ingress
return HoldZoneGeometry(
target, origin, ip, join, self.coalition, self.theater
).find_best_hold_point()
def build(self) -> SweepFlightPlan:
return SweepFlightPlan(self.flight, self.layout())

View File

@ -15,44 +15,6 @@ if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint from ..flightwaypoint import FlightWaypoint
class Builder(CapBuilder):
def build(self) -> TarCapLayout:
location = self.package.target
preferred_alt = self.flight.unit_type.preferred_patrol_altitude
randomized_alt = preferred_alt + feet(random.randint(-2, 1) * 1000)
patrol_alt = max(
self.doctrine.min_patrol_altitude,
min(self.doctrine.max_patrol_altitude, randomized_alt),
)
builder = WaypointBuilder(self.flight, self.coalition)
orbit0p, orbit1p = self.cap_racetrack_for_objective(location, barcap=False)
start, end = builder.race_track(orbit0p, orbit1p, patrol_alt)
refuel = None
if self.package.waypoints is not None:
refuel = builder.refuel(self.package.waypoints.refuel)
return TarCapLayout(
departure=builder.takeoff(self.flight.departure),
nav_to=builder.nav_path(
self.flight.departure.position, orbit0p, patrol_alt
),
nav_from=builder.nav_path(
orbit1p, self.flight.arrival.position, patrol_alt
),
patrol_start=start,
patrol_end=end,
refuel=refuel,
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
)
@dataclass(frozen=True) @dataclass(frozen=True)
class TarCapLayout(PatrollingLayout): class TarCapLayout(PatrollingLayout):
refuel: FlightWaypoint | None refuel: FlightWaypoint | None
@ -124,3 +86,44 @@ class TarCapFlightPlan(PatrollingFlightPlan[TarCapLayout]):
if end is not None: if end is not None:
return end return end
return super().patrol_end_time return super().patrol_end_time
class Builder(CapBuilder[TarCapFlightPlan, TarCapLayout]):
def layout(self) -> TarCapLayout:
location = self.package.target
preferred_alt = self.flight.unit_type.preferred_patrol_altitude
randomized_alt = preferred_alt + feet(random.randint(-2, 1) * 1000)
patrol_alt = max(
self.doctrine.min_patrol_altitude,
min(self.doctrine.max_patrol_altitude, randomized_alt),
)
builder = WaypointBuilder(self.flight, self.coalition)
orbit0p, orbit1p = self.cap_racetrack_for_objective(location, barcap=False)
start, end = builder.race_track(orbit0p, orbit1p, patrol_alt)
refuel = None
if self.package.waypoints is not None:
refuel = builder.refuel(self.package.waypoints.refuel)
return TarCapLayout(
departure=builder.takeoff(self.flight.departure),
nav_to=builder.nav_path(
self.flight.departure.position, orbit0p, patrol_alt
),
nav_from=builder.nav_path(
orbit1p, self.flight.arrival.position, patrol_alt
),
patrol_start=start,
patrol_end=end,
refuel=refuel,
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
)
def build(self) -> TarCapFlightPlan:
return TarCapFlightPlan(self.flight, self.layout())

View File

@ -3,14 +3,25 @@ from __future__ import annotations
from datetime import timedelta from datetime import timedelta
from typing import Type from typing import Type
from game.utils import Distance, Heading, Speed, feet, knots, meters, nautical_miles from game.utils import Heading, feet, meters, nautical_miles
from .ibuilder import IBuilder from .ibuilder import IBuilder
from .patrolling import PatrollingFlightPlan, PatrollingLayout from .patrolling import PatrollingLayout
from .refuelingflightplan import RefuelingFlightPlan
from .waypointbuilder import WaypointBuilder from .waypointbuilder import WaypointBuilder
class Builder(IBuilder): class TheaterRefuelingFlightPlan(RefuelingFlightPlan):
def build(self) -> PatrollingLayout: @staticmethod
def builder_type() -> Type[Builder]:
return Builder
@property
def patrol_duration(self) -> timedelta:
return timedelta(hours=1)
class Builder(IBuilder[TheaterRefuelingFlightPlan, PatrollingLayout]):
def layout(self) -> PatrollingLayout:
racetrack_half_distance = nautical_miles(20).meters racetrack_half_distance = nautical_miles(20).meters
location = self.package.target location = self.package.target
@ -68,26 +79,5 @@ class Builder(IBuilder):
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
) )
def build(self) -> TheaterRefuelingFlightPlan:
class TheaterRefuelingFlightPlan(PatrollingFlightPlan[PatrollingLayout]): return TheaterRefuelingFlightPlan(self.flight, self.layout())
@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)

View File

@ -6,16 +6,17 @@ from dataclasses import dataclass, field
from datetime import timedelta from datetime import timedelta
from typing import Dict, List, Optional, TYPE_CHECKING from typing import Dict, List, Optional, TYPE_CHECKING
from .flightplans.formation import FormationFlightPlan
from game.db import Database from game.db import Database
from game.utils import Speed from game.utils import Speed
from .closestairfields import ObjectiveDistanceCache
from .flight import Flight from .flight import Flight
from .flightplans.formation import FormationFlightPlan
from .flighttype import FlightType from .flighttype import FlightType
from .packagewaypoints import PackageWaypoints from .packagewaypoints import PackageWaypoints
from .traveltime import TotEstimator from .traveltime import TotEstimator
if TYPE_CHECKING: if TYPE_CHECKING:
from game.theater import MissionTarget from game.theater import ControlPoint, MissionTarget
@dataclass @dataclass
@ -31,8 +32,6 @@ class Package:
#: The set of flights in the package. #: The set of flights in the package.
flights: List[Flight] = field(default_factory=list) flights: List[Flight] = field(default_factory=list)
delay: int = field(default=0)
#: True if the package ToT should be reset to ASAP whenever the player makes #: True if the package ToT should be reset to ASAP whenever the player makes
#: a change. This is really a UI property rather than a game property, but #: a change. This is really a UI property rather than a game property, but
#: we want it to persist in the save. #: we want it to persist in the save.
@ -193,6 +192,24 @@ class Package:
return "OCA Strike" return "OCA Strike"
return str(task) return str(task)
def departure_closest_to_target(self) -> ControlPoint:
# We'll always have a package, but if this is being planned via the UI
# it could be the first flight in the package.
if not self.flights:
raise RuntimeError(
"Cannot determine source airfield for package with no flights"
)
# The package airfield is either the flight's airfield (when there is no
# package) or the closest airfield to the objective that is the
# departure airfield for some flight in the package.
cache = ObjectiveDistanceCache.get_closest_airfields(self.target)
for airfield in cache.operational_airfields:
for flight in self.flights:
if flight.departure == airfield:
return airfield
raise RuntimeError("Could not find any airfield assigned to this package")
def __hash__(self) -> int: def __hash__(self) -> int:
# TODO: Far from perfect. Number packages? # TODO: Far from perfect. Number packages?
return hash(self.target.name) return hash(self.target.name)

View File

@ -1,8 +1,18 @@
from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional from typing import TYPE_CHECKING
from dcs import Point from dcs import Point
from game.ato.flightplans.waypointbuilder import WaypointBuilder
from game.flightplan import IpZoneGeometry, JoinZoneGeometry
from game.flightplan.refuelzonegeometry import RefuelZoneGeometry
if TYPE_CHECKING:
from game.ato import Package
from game.coalition import Coalition
@dataclass(frozen=True) @dataclass(frozen=True)
class PackageWaypoints: class PackageWaypoints:
@ -10,3 +20,37 @@ class PackageWaypoints:
ingress: Point ingress: Point
split: Point split: Point
refuel: Point refuel: Point
@staticmethod
def create(package: Package, coalition: Coalition) -> PackageWaypoints:
origin = package.departure_closest_to_target()
# Start by picking the best IP for the attack.
ingress_point = IpZoneGeometry(
package.target.position,
origin.position,
coalition,
).find_best_ip()
join_point = JoinZoneGeometry(
package.target.position,
origin.position,
ingress_point,
coalition,
).find_best_join_point()
refuel_point = RefuelZoneGeometry(
origin.position,
join_point,
coalition,
).find_best_refuel_point()
# And the split point based on the best route from the IP. Since that's no
# different than the best route *to* the IP, this is the same as the join point.
# TODO: Estimate attack completion point based on the IP and split from there?
return PackageWaypoints(
WaypointBuilder.perturb(join_point),
ingress_point,
WaypointBuilder.perturb(join_point),
refuel_point,
)

View File

@ -107,9 +107,8 @@ class MizCampaignLoader:
if self.mission.country(self.RED_COUNTRY.name) is None: if self.mission.country(self.RED_COUNTRY.name) is None:
self.mission.coalition["red"].add_country(self.RED_COUNTRY) self.mission.coalition["red"].add_country(self.RED_COUNTRY)
@staticmethod def control_point_from_airport(self, airport: Airport) -> ControlPoint:
def control_point_from_airport(airport: Airport) -> ControlPoint: cp = Airfield(airport, self.theater, starts_blue=airport.is_blue())
cp = Airfield(airport, starts_blue=airport.is_blue())
# Use the unlimited aircraft option to determine if an airfield should # Use the unlimited aircraft option to determine if an airfield should
# be owned by the player when the campaign is "inverted". # be owned by the player when the campaign is "inverted".
@ -252,20 +251,26 @@ class MizCampaignLoader:
for blue in (False, True): for blue in (False, True):
for group in self.off_map_spawns(blue): for group in self.off_map_spawns(blue):
control_point = OffMapSpawn( control_point = OffMapSpawn(
str(group.name), group.position, starts_blue=blue str(group.name), group.position, self.theater, starts_blue=blue
) )
control_point.captured_invert = group.late_activation control_point.captured_invert = group.late_activation
control_points[control_point.id] = control_point control_points[control_point.id] = control_point
for ship in self.carriers(blue): for ship in self.carriers(blue):
control_point = Carrier(ship.name, ship.position, starts_blue=blue) control_point = Carrier(
ship.name, ship.position, self.theater, starts_blue=blue
)
control_point.captured_invert = ship.late_activation control_point.captured_invert = ship.late_activation
control_points[control_point.id] = control_point control_points[control_point.id] = control_point
for ship in self.lhas(blue): for ship in self.lhas(blue):
control_point = Lha(ship.name, ship.position, starts_blue=blue) control_point = Lha(
ship.name, ship.position, self.theater, starts_blue=blue
)
control_point.captured_invert = ship.late_activation control_point.captured_invert = ship.late_activation
control_points[control_point.id] = control_point control_points[control_point.id] = control_point
for fob in self.fobs(blue): for fob in self.fobs(blue):
control_point = Fob(str(fob.name), fob.position, starts_blue=blue) control_point = Fob(
str(fob.name), fob.position, self.theater, starts_blue=blue
)
control_point.captured_invert = fob.late_activation control_point.captured_invert = fob.late_activation
control_points[control_point.id] = control_point control_points[control_point.id] = control_point

View File

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

View File

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

View File

@ -26,21 +26,21 @@ from typing import (
from uuid import UUID from uuid import UUID
from dcs.mapping import Point from dcs.mapping import Point
from dcs.terrain.terrain import Airport, ParkingSlot
from dcs.unitgroup import ShipGroup, StaticGroup
from dcs.unittype import ShipType
from dcs.ships import ( from dcs.ships import (
CVN_71, CVN_71,
CVN_72, CVN_72,
CVN_73, CVN_73,
CVN_75, CVN_75,
CV_1143_5, CV_1143_5,
KUZNECOW,
Stennis,
Forrestal, Forrestal,
KUZNECOW,
LHA_Tarawa, LHA_Tarawa,
Stennis,
Type_071, Type_071,
) )
from dcs.terrain.terrain import Airport, ParkingSlot
from dcs.unitgroup import ShipGroup, StaticGroup
from dcs.unittype import ShipType
from game.ato.closestairfields import ObjectiveDistanceCache from game.ato.closestairfields import ObjectiveDistanceCache
from game.ground_forces.combat_stance import CombatStance from game.ground_forces.combat_stance import CombatStance
@ -56,8 +56,8 @@ from game.sidc import (
Status, Status,
SymbolSet, SymbolSet,
) )
from game.utils import Distance, Heading, meters
from game.theater.presetlocation import PresetLocation from game.theater.presetlocation import PresetLocation
from game.utils import Distance, Heading, meters
from .base import Base from .base import Base
from .frontline import FrontLine from .frontline import FrontLine
from .missiontarget import MissionTarget from .missiontarget import MissionTarget
@ -320,6 +320,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
name: str, name: str,
position: Point, position: Point,
at: StartingPosition, at: StartingPosition,
theater: ConflictTheater,
starts_blue: bool, starts_blue: bool,
cptype: ControlPointType = ControlPointType.AIRBASE, cptype: ControlPointType = ControlPointType.AIRBASE,
) -> None: ) -> None:
@ -327,6 +328,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
self.id = uuid.uuid4() self.id = uuid.uuid4()
self.full_name = name self.full_name = name
self.at = at self.at = at
self.theater = theater
self.starts_blue = starts_blue self.starts_blue = starts_blue
self.connected_objectives: List[TheaterGroundObject] = [] self.connected_objectives: List[TheaterGroundObject] = []
self.preset_locations = PresetLocations() self.preset_locations = PresetLocations()
@ -1039,11 +1041,14 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
class Airfield(ControlPoint): class Airfield(ControlPoint):
def __init__(self, airport: Airport, starts_blue: bool) -> None: def __init__(
self, airport: Airport, theater: ConflictTheater, starts_blue: bool
) -> None:
super().__init__( super().__init__(
airport.name, airport.name,
airport.position, airport.position,
airport, airport,
theater,
starts_blue, starts_blue,
cptype=ControlPointType.AIRBASE, cptype=ControlPointType.AIRBASE,
) )
@ -1236,9 +1241,16 @@ class NavalControlPoint(ControlPoint, ABC):
class Carrier(NavalControlPoint): class Carrier(NavalControlPoint):
def __init__(self, name: str, at: Point, starts_blue: bool): def __init__(
self, name: str, at: Point, theater: ConflictTheater, starts_blue: bool
):
super().__init__( super().__init__(
name, at, at, starts_blue, cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP name,
at,
at,
theater,
starts_blue,
cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP,
) )
@property @property
@ -1275,8 +1287,12 @@ class Carrier(NavalControlPoint):
class Lha(NavalControlPoint): class Lha(NavalControlPoint):
def __init__(self, name: str, at: Point, starts_blue: bool): def __init__(
super().__init__(name, at, at, starts_blue, cptype=ControlPointType.LHA_GROUP) self, name: str, at: Point, theater: ConflictTheater, starts_blue: bool
):
super().__init__(
name, at, at, theater, starts_blue, cptype=ControlPointType.LHA_GROUP
)
@property @property
def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]: def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]:
@ -1305,9 +1321,16 @@ class OffMapSpawn(ControlPoint):
def runway_is_operational(self) -> bool: def runway_is_operational(self) -> bool:
return True return True
def __init__(self, name: str, position: Point, starts_blue: bool): def __init__(
self, name: str, position: Point, theater: ConflictTheater, starts_blue: bool
):
super().__init__( super().__init__(
name, position, position, starts_blue, cptype=ControlPointType.OFF_MAP name,
position,
position,
theater,
starts_blue,
cptype=ControlPointType.OFF_MAP,
) )
@property @property
@ -1364,8 +1387,12 @@ class OffMapSpawn(ControlPoint):
class Fob(ControlPoint): class Fob(ControlPoint):
def __init__(self, name: str, at: Point, starts_blue: bool): def __init__(
super().__init__(name, at, at, starts_blue, cptype=ControlPointType.FOB) self, name: str, at: Point, theater: ConflictTheater, starts_blue: bool
):
super().__init__(
name, at, at, theater, starts_blue, cptype=ControlPointType.FOB
)
self.name = name self.name = name
@property @property

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import logging import logging
from typing import Iterable, List, Optional, Any from typing import Iterable, List, Optional
from PySide2.QtCore import Signal from PySide2.QtCore import Signal
from PySide2.QtWidgets import ( from PySide2.QtWidgets import (
@ -14,10 +14,9 @@ from PySide2.QtWidgets import (
from game import Game from game import Game
from game.ato.flight import Flight from game.ato.flight import Flight
from game.ato.flightplans.custom import CustomFlightPlan, CustomLayout from game.ato.flightplans.custom import CustomFlightPlan, CustomLayout
from game.ato.flightplans.flightplan import FlightPlan
from game.ato.flightplans.flightplanbuilder import FlightPlanBuilder
from game.ato.flightplans.formationattack import FormationAttackFlightPlan from game.ato.flightplans.formationattack import FormationAttackFlightPlan
from game.ato.flightplans.planningerror import PlanningError from game.ato.flightplans.planningerror import PlanningError
from game.ato.flightplans.waypointbuilder import WaypointBuilder
from game.ato.flighttype import FlightType from game.ato.flighttype import FlightType
from game.ato.flightwaypoint import FlightWaypoint from game.ato.flightwaypoint import FlightWaypoint
from game.ato.loadouts import Loadout from game.ato.loadouts import Loadout
@ -38,7 +37,6 @@ class QFlightWaypointTab(QFrame):
self.game = game self.game = game
self.package = package self.package = package
self.flight = flight self.flight = flight
self.planner = FlightPlanBuilder(package, game.blue, game.theater)
self.flight_waypoint_list: Optional[QFlightWaypointList] = None self.flight_waypoint_list: Optional[QFlightWaypointList] = None
self.rtb_waypoint: Optional[QPushButton] = None self.rtb_waypoint: Optional[QPushButton] = None
@ -139,7 +137,7 @@ class QFlightWaypointTab(QFrame):
self.on_change() self.on_change()
def on_rtb_waypoint(self): def on_rtb_waypoint(self):
rtb = self.planner.generate_rtb_waypoint(self.flight, self.flight.from_cp) rtb = WaypointBuilder(self.flight, self.coalition).land(self.flight.arrival)
self.degrade_to_custom_flight_plan() self.degrade_to_custom_flight_plan()
assert isinstance(self.flight.flight_plan, CustomFlightPlan) assert isinstance(self.flight.flight_plan, CustomFlightPlan)
self.flight.flight_plan.layout.custom_waypoints.append(rtb) self.flight.flight_plan.layout.custom_waypoints.append(rtb)
@ -168,7 +166,7 @@ class QFlightWaypointTab(QFrame):
if result == QMessageBox.Yes: if result == QMessageBox.Yes:
self.flight.flight_type = task self.flight.flight_type = task
try: try:
self.planner.populate_flight_plan(self.flight) self.flight.recreate_flight_plan()
except PlanningError as ex: except PlanningError as ex:
self.flight.flight_type = original_task self.flight.flight_type = original_task
logging.exception("Could not recreate flight") logging.exception("Could not recreate flight")