diff --git a/game/ato/flight.py b/game/ato/flight.py index 52e1de59..e33305df 100644 --- a/game/ato/flight.py +++ b/game/ato/flight.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import Optional, List, TYPE_CHECKING +from datetime import datetime, timedelta +from typing import Any, Optional, List, TYPE_CHECKING from gen.flights.loadouts import Loadout @@ -14,6 +15,7 @@ if TYPE_CHECKING: from .flighttype import FlightType from .flightwaypoint import FlightWaypoint from .package import Package + from .starttype import StartType class Flight: @@ -24,7 +26,7 @@ class Flight: squadron: Squadron, count: int, flight_type: FlightType, - start_type: str, + start_type: StartType, divert: Optional[ControlPoint], custom_name: Optional[str] = None, cargo: Optional[TransferOrder] = None, diff --git a/game/ato/starttype.py b/game/ato/starttype.py new file mode 100644 index 00000000..81171751 --- /dev/null +++ b/game/ato/starttype.py @@ -0,0 +1,15 @@ +from enum import Enum, unique + + +@unique +class StartType(Enum): + """The start type for a Flight. + + This is distinct from dcs.mission.StartType because we need a fourth state: + IN_FLIGHT. + """ + + IN_FLIGHT = "In Flight" + RUNWAY = "Runway" + COLD = "Cold" + WARM = "Warm" diff --git a/game/commander/packagebuilder.py b/game/commander/packagebuilder.py index 8a820fb4..a1f5d195 100644 --- a/game/commander/packagebuilder.py +++ b/game/commander/packagebuilder.py @@ -6,6 +6,7 @@ from game.utils import nautical_miles from ..ato.package import Package from game.theater import MissionTarget, OffMapSpawn, ControlPoint from ..ato.flight import Flight +from ..ato.starttype import StartType if TYPE_CHECKING: from game.dcs.aircrafttype import AircraftType @@ -24,7 +25,7 @@ class PackageBuilder: air_wing: AirWing, is_player: bool, package_country: str, - start_type: str, + start_type: StartType, asap: bool, ) -> None: self.closest_airfields = closest_airfields diff --git a/game/settings/settings.py b/game/settings/settings.py index c2b94619..533a03ae 100644 --- a/game/settings/settings.py +++ b/game/settings/settings.py @@ -13,6 +13,7 @@ from .choicesoption import choices_option from .minutesoption import minutes_option from .optiondescription import OptionDescription, SETTING_DESCRIPTION_KEY from .skilloption import skill_option +from ..ato.starttype import StartType @unique @@ -323,12 +324,12 @@ class Settings: "option only allows the player to wait on the ground." ), ) - default_start_type: str = choices_option( + default_start_type: StartType = choices_option( "Default start type for AI aircraft", page=MISSION_GENERATOR_PAGE, section=GAMEPLAY_SECTION, - choices=["Cold", "Warm", "Runway", "In Flight"], - default="Cold", + choices={v.value: v for v in StartType}, + default=StartType.COLD, detail=( "Warning: Options other than Cold will significantly reduce the number of " "targets available for OCA/Aircraft missions, and OCA/Aircraft flights " diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index 983d1f46..9c7e23ee 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -49,6 +49,7 @@ from .theatergroundobject import ( TheaterGroundObject, BuildingGroundObject, ) +from ..ato.starttype import StartType from ..dcs.aircrafttype import AircraftType from ..dcs.groundunittype import GroundUnitType from ..utils import nautical_miles @@ -681,7 +682,7 @@ class ControlPoint(MissionTarget, ABC): self.base.set_strength_to_minimum() @property - def required_aircraft_start_type(self) -> Optional[str]: + def required_aircraft_start_type(self) -> Optional[StartType]: return None @abstractmethod @@ -1151,8 +1152,8 @@ class OffMapSpawn(ControlPoint): return True @property - def required_aircraft_start_type(self) -> Optional[str]: - return "In Flight" + def required_aircraft_start_type(self) -> Optional[StartType]: + return StartType.IN_FLIGHT @property def heading(self) -> Heading: diff --git a/game/weather.py b/game/weather.py index 47287184..5f862759 100644 --- a/game/weather.py +++ b/game/weather.py @@ -5,17 +5,16 @@ import logging import random from dataclasses import dataclass, field from enum import Enum -from typing import Optional, TYPE_CHECKING, Any +from typing import Optional, TYPE_CHECKING from dcs.cloud_presets import Clouds as PydcsClouds from dcs.weather import CloudPreset, Weather as PydcsWeather, Wind - -from game.settings import Settings from game.utils import Distance, Heading, meters, interpolate, Pressure, inches_hg from game.theater.seasonalconditions import determine_season if TYPE_CHECKING: + from game.settings import Settings from game.theater import ConflictTheater from game.theater.seasonalconditions import SeasonalConditions diff --git a/gen/aircraft.py b/gen/aircraft.py index f18c0337..9d2cbcee 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -14,7 +14,7 @@ from dcs.condition import CoalitionHasAirdrome, TimeAfter from dcs.country import Country from dcs.flyingunit import FlyingUnit from dcs.mapping import Point -from dcs.mission import Mission, StartType +from dcs.mission import Mission, StartType as DcsStartType from dcs.planes import ( AJS37, B_17G, @@ -67,6 +67,7 @@ from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup from dcs.unittype import FlyingType from game import db +from game.ato.starttype import StartType from game.data.weapons import Pylon, WeaponType as WeaponTypeEnum from game.dcs.aircrafttype import AircraftType from game.factions.faction import Faction @@ -258,19 +259,19 @@ class AircraftConflictGenerator: return total @staticmethod - def _start_type(start_type: str) -> StartType: + def _start_type(start_type: str) -> DcsStartType: if start_type == "Runway": - return StartType.Runway + return DcsStartType.Runway elif start_type == "Cold": - return StartType.Cold - return StartType.Warm + return DcsStartType.Cold + return DcsStartType.Warm @staticmethod def _start_type_at_group( start_type: str, unit_type: Type[FlyingType], at: Union[ShipGroup, StaticGroup], - ) -> StartType: + ) -> DcsStartType: group_units = at.units # Setting Su-33s starting from the non-supercarrier Kuznetsov to take off from runway # to work around a DCS AI issue preventing Su-33s from taking off when set to "Takeoff from ramp" (#1352) @@ -279,7 +280,7 @@ class AircraftConflictGenerator: and group_units[0] is not None and group_units[0].type == KUZNECOW.id ): - return StartType.Runway + return DcsStartType.Runway else: return AircraftConflictGenerator._start_type(start_type) @@ -490,7 +491,7 @@ class AircraftConflictGenerator: parking_slots=None, ) - def _generate_inflight( + def _generate_over_departure( self, name: str, side: Country, flight: Flight, origin: ControlPoint ) -> FlyingGroup[Any]: assert flight.count > 0 @@ -652,7 +653,7 @@ class AircraftConflictGenerator: continue for flight in package.flights: logging.info(f"Generating flight: {flight.unit_type}") - group = self.generate_planned_flight(flight.from_cp, country, flight) + group = self.generate_planned_flight(country, flight) self.unit_map.add_aircraft(group, flight) self.setup_flight_group(group, package, flight, dynamic_runways) self.create_waypoints(group, package, flight) @@ -691,7 +692,7 @@ class AircraftConflictGenerator: squadron, 1, FlightType.BARCAP, - "Cold", + StartType.COLD, divert=None, ) @@ -753,14 +754,14 @@ class AircraftConflictGenerator: coalition = self.game.coalition_for(flight.departure.captured).coalition_id trigger.add_condition(CoalitionHasAirdrome(coalition, flight.from_cp.id)) - def generate_planned_flight( - self, cp: ControlPoint, country: Country, flight: Flight + def generate_flight_at_departure( + self, country: Country, flight: Flight, start_type: StartType ) -> FlyingGroup[Any]: - name = namegen.next_aircraft_name(country, cp.id, flight) - group: FlyingGroup[Any] + name = namegen.next_aircraft_name(country, flight.departure.id, flight) + cp = flight.departure try: - if flight.start_type == "In Flight": - group = self._generate_inflight( + if start_type is StartType.IN_FLIGHT: + group = self._generate_over_departure( name=name, side=country, flight=flight, origin=cp ) return group @@ -777,7 +778,7 @@ class AircraftConflictGenerator: side=country, unit_type=flight.unit_type.dcs_unit_type, count=flight.count, - start_type=flight.start_type, + start_type=start_type.value, at=carrier_group, ) else: @@ -788,7 +789,7 @@ class AircraftConflictGenerator: side=country, unit_type=flight.unit_type.dcs_unit_type, count=flight.count, - start_type=flight.start_type, + start_type=start_type.value, cp=cp, ) @@ -801,22 +802,26 @@ class AircraftConflictGenerator: side=country, unit_type=flight.unit_type.dcs_unit_type, count=flight.count, - start_type=flight.start_type, + start_type=start_type.value, airport=cp.airport, ) - except Exception as e: + except NoParkingSlotError: # Generated when there is no place on Runway or on Parking Slots - logging.error(e) - logging.warning( + logging.exception( "No room on runway or parking slots. Starting from the air." ) - flight.start_type = "In Flight" - group = self._generate_inflight( + flight.start_type = StartType.IN_FLIGHT + group = self._generate_over_departure( name=name, side=country, flight=flight, origin=cp ) group.points[0].alt = 1500 return group + def generate_planned_flight( + self, country: Country, flight: Flight + ) -> FlyingGroup[Any]: + return self.generate_flight_at_departure(country, flight, flight.start_type) + @staticmethod def set_reduced_fuel( flight: Flight, group: FlyingGroup[Any], unit_type: Type[FlyingType] @@ -1360,7 +1365,7 @@ class AircraftConflictGenerator: # Late activation causes the aircraft to not be spawned # until triggered. self.set_activation_time(flight, group, start_time) - elif flight.start_type == "Cold": + elif flight.start_type is StartType.COLD: # Setting the start time causes the AI to wait until the # specified time to begin their startup sequence. self.set_startup_time(flight, group, start_time) @@ -1374,7 +1379,7 @@ class AircraftConflictGenerator: @staticmethod def should_activate_late(flight: Flight) -> bool: - if flight.start_type != "Cold": + if flight.start_type is StartType.COLD: # Avoid spawning aircraft in the air or on the runway until it's # time for their mission. Also avoid burning through gas spawning # hot aircraft hours before their takeoff time. diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index 924ba270..427dc2bf 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -19,6 +19,7 @@ from dcs.mapping import Point from dcs.unit import Unit from shapely.geometry import Point as ShapelyPoint +from game.ato.starttype import StartType from game.data.doctrine import Doctrine from game.dcs.aircrafttype import FuelConsumption from game.flightplan import IpZoneGeometry, JoinZoneGeometry, HoldZoneGeometry @@ -275,7 +276,7 @@ class FlightPlan: return start_time def estimate_startup(self) -> timedelta: - if self.flight.start_type == "Cold": + if self.flight.start_type is StartType.COLD: if self.flight.client_count: return timedelta(minutes=10) else: @@ -284,7 +285,7 @@ class FlightPlan: return timedelta() def estimate_ground_ops(self) -> timedelta: - if self.flight.start_type in ("Runway", "In Flight"): + if self.flight.start_type in {StartType.RUNWAY, StartType.IN_FLIGHT}: return timedelta() if self.flight.from_cp.is_fleet: return timedelta(minutes=2)