Split aircraft spawning into its own class.

This commit is contained in:
Dan Albert 2021-10-23 20:51:39 -07:00
parent 88b4039e47
commit c8f30b3289
2 changed files with 222 additions and 269 deletions

View File

@ -1,22 +1,13 @@
from __future__ import annotations
import logging
import random
from functools import cached_property
from typing import Any, Dict, List, TYPE_CHECKING, Type, Union
from typing import Any, Dict, List, TYPE_CHECKING
from dcs import helicopters
from dcs.country import Country
from dcs.mapping import Point
from dcs.mission import Mission, StartType as DcsStartType
from dcs.planes import (
Su_33,
)
from dcs.point import PointAction
from dcs.ships import KUZNECOW
from dcs.terrain.terrain import Airport, NoParkingSlotError
from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup
from dcs.unittype import FlyingType
from dcs.mission import Mission
from dcs.terrain.terrain import NoParkingSlotError
from dcs.unitgroup import FlyingGroup, StaticGroup
from game.ato.airtaaskingorder import AirTaskingOrder
from game.ato.flight import Flight
@ -32,29 +23,18 @@ from game.settings import Settings
from game.theater.controlpoint import (
Airfield,
ControlPoint,
NavalControlPoint,
OffMapSpawn,
)
from game.unitmap import UnitMap
from game.utils import meters
from gen.flights.traveltime import GroundSpeed
from gen.naming import namegen
from gen.runways import RunwayData
from .aircraftpainter import AircraftPainter
from .flightdata import FlightData
from .flightgroupconfigurator import FlightGroupConfigurator
from .flightgroupspawner import FlightGroupSpawner
if TYPE_CHECKING:
from game import Game
from game.squadrons import Squadron
WARM_START_HELI_ALT = meters(500)
WARM_START_ALTITUDE = meters(3000)
RTB_ALTITUDE = meters(800)
RTB_DISTANCE = 5000
HELI_ALT = 500
class AircraftGenerator:
def __init__(
@ -95,171 +75,6 @@ class AircraftGenerator:
total += flight.client_count
return total
@staticmethod
def _start_type(start_type: str) -> DcsStartType:
if start_type == "Runway":
return DcsStartType.Runway
elif start_type == "Cold":
return DcsStartType.Cold
return DcsStartType.Warm
@staticmethod
def _start_type_at_group(
start_type: str,
unit_type: Type[FlyingType],
at: Union[ShipGroup, StaticGroup],
) -> 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)
if (
unit_type.id == Su_33.id
and group_units[0] is not None
and group_units[0].type == KUZNECOW.id
):
return DcsStartType.Runway
else:
return AircraftGenerator._start_type(start_type)
def _generate_at_airport(
self,
name: str,
side: Country,
unit_type: Type[FlyingType],
count: int,
start_type: str,
airport: Airport,
) -> FlyingGroup[Any]:
assert count > 0
# TODO: Delayed runway starts should be converted to air starts for multiplayer.
# Runway starts do not work with late activated aircraft in multiplayer. Instead
# of spawning on the runway the aircraft will spawn on the taxiway, potentially
# somewhere that they don't fit anyway. We should either upgrade these to air
# starts or (less likely) downgrade to warm starts to avoid the issue when the
# player is generating the mission for multiplayer (which would need a new
# option).
logging.info("airgen: {} for {} at {}".format(unit_type, side.id, airport))
return self.m.flight_group_from_airport(
country=side,
name=name,
aircraft_type=unit_type,
airport=airport,
maintask=None,
start_type=self._start_type(start_type),
group_size=count,
parking_slots=None,
)
def _generate_over_departure(
self, name: str, side: Country, flight: Flight, origin: ControlPoint
) -> FlyingGroup[Any]:
assert flight.count > 0
at = origin.position
alt_type = "RADIO"
if isinstance(origin, OffMapSpawn):
alt = flight.flight_plan.waypoints[0].alt
alt_type = flight.flight_plan.waypoints[0].alt_type
elif flight.unit_type in helicopters.helicopter_map.values():
alt = WARM_START_HELI_ALT
else:
alt = WARM_START_ALTITUDE
speed = GroundSpeed.for_flight(flight, alt)
pos = Point(at.x + random.randint(100, 1000), at.y + random.randint(100, 1000))
logging.info(
"airgen: {} for {} at {} at {}".format(
flight.unit_type, side.id, alt, int(speed.kph)
)
)
group = self.m.flight_group(
country=side,
name=name,
aircraft_type=flight.unit_type.dcs_unit_type,
airport=None,
position=pos,
altitude=alt.meters,
speed=speed.kph,
maintask=None,
group_size=flight.count,
)
group.points[0].alt_type = alt_type
return group
def _generate_at_group(
self,
name: str,
side: Country,
unit_type: Type[FlyingType],
count: int,
start_type: str,
at: Union[ShipGroup, StaticGroup],
) -> FlyingGroup[Any]:
assert count > 0
logging.info("airgen: {} for {} at unit {}".format(unit_type, side.id, at))
return self.m.flight_group_from_unit(
country=side,
name=name,
aircraft_type=unit_type,
pad_group=at,
maintask=None,
start_type=self._start_type_at_group(start_type, unit_type, at),
group_size=count,
)
def _generate_at_cp_helipad(
self,
name: str,
side: Country,
unit_type: Type[FlyingType],
count: int,
start_type: str,
cp: ControlPoint,
) -> FlyingGroup[Any]:
assert count > 0
logging.info(
"airgen at cp's helipads : {} for {} at {}".format(
unit_type, side.id, cp.name
)
)
try:
helipad = self.helipads[cp].pop()
except IndexError as ex:
raise RuntimeError(f"Not enough helipads available at {cp}") from ex
group = self._generate_at_group(
name=name,
side=side,
unit_type=unit_type,
count=count,
start_type=start_type,
at=helipad,
)
# Note : A bit dirty, need better support in pydcs
group.points[0].action = PointAction.FromGroundArea
group.points[0].type = "TakeOffGround"
group.units[0].heading = helipad.units[0].heading
if start_type != "Cold":
group.points[0].action = PointAction.FromGroundAreaHot
group.points[0].type = "TakeOffGroundHot"
for i in range(count - 1):
try:
helipad = self.helipads[cp].pop()
group.units[1 + i].position = Point(helipad.x, helipad.y)
group.units[1 + i].heading = helipad.units[0].heading
except IndexError as ex:
raise RuntimeError(f"Not enough helipads available at {cp}") from ex
return group
def clear_parking_slots(self) -> None:
for cp in self.game.theater.controlpoints:
for parking_slot in cp.parking_slots:
@ -330,23 +145,18 @@ class AircraftGenerator:
divert=None,
)
group = self._generate_at_airport(
name=namegen.next_aircraft_name(country, flight.departure.id, flight),
side=country,
unit_type=squadron.aircraft.dcs_unit_type,
count=1,
start_type="Cold",
airport=squadron.location.airport,
)
group.uncontrolled = True
group = FlightGroupSpawner(
flight, country, self.m, self.helipads
).create_idle_aircraft()
AircraftPainter(flight, group).apply_livery()
self.unit_map.add_aircraft(group, flight)
def create_and_configure_flight(
self, flight: Flight, country: Country, dynamic_runways: Dict[str, RunwayData]
) -> FlyingGroup[Any]:
group = self.generate_planned_flight(country, flight)
group = FlightGroupSpawner(
flight, country, self.m, self.helipads
).create_flight_group()
self.flights.append(
FlightGroupConfigurator(
flight,
@ -362,71 +172,3 @@ class AircraftGenerator:
).configure()
)
return group
def generate_flight_at_departure(
self, country: Country, flight: Flight, start_type: StartType
) -> FlyingGroup[Any]:
name = namegen.next_aircraft_name(country, flight.departure.id, flight)
cp = flight.departure
try:
if start_type is StartType.IN_FLIGHT:
group = self._generate_over_departure(
name=name, side=country, flight=flight, origin=cp
)
return group
elif isinstance(cp, NavalControlPoint):
group_name = cp.get_carrier_group_name()
carrier_group = self.m.find_group(group_name)
if not isinstance(carrier_group, ShipGroup):
raise RuntimeError(
f"Carrier group {carrier_group} is a "
"{carrier_group.__class__.__name__}, expected a ShipGroup"
)
return self._generate_at_group(
name=name,
side=country,
unit_type=flight.unit_type.dcs_unit_type,
count=flight.count,
start_type=start_type.value,
at=carrier_group,
)
else:
# If the flight is an helicopter flight, then prioritize dedicated helipads
if flight.unit_type.helicopter:
return self._generate_at_cp_helipad(
name=name,
side=country,
unit_type=flight.unit_type.dcs_unit_type,
count=flight.count,
start_type=start_type.value,
cp=cp,
)
if not isinstance(cp, Airfield):
raise RuntimeError(
f"Attempted to spawn at airfield for non-airfield {cp}"
)
return self._generate_at_airport(
name=name,
side=country,
unit_type=flight.unit_type.dcs_unit_type,
count=flight.count,
start_type=start_type.value,
airport=cp.airport,
)
except NoParkingSlotError:
# Generated when there is no place on Runway or on Parking Slots
logging.exception(
"No room on runway or parking slots. Starting from the air."
)
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)

View File

@ -0,0 +1,211 @@
import logging
import random
from typing import Any, Union
from dcs import Mission, Point
from dcs.country import Country
from dcs.mission import StartType as DcsStartType
from dcs.planes import Su_33
from dcs.point import PointAction
from dcs.ships import KUZNECOW
from dcs.terrain import Airport, NoParkingSlotError
from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup
from game.ato import Flight
from game.ato.starttype import StartType
from game.theater import Airfield, ControlPoint, NavalControlPoint, OffMapSpawn
from game.utils import meters
from gen.flights.traveltime import GroundSpeed
from gen.naming import namegen
WARM_START_HELI_ALT = meters(500)
WARM_START_ALTITUDE = meters(3000)
RTB_ALTITUDE = meters(800)
RTB_DISTANCE = 5000
HELI_ALT = 500
class FlightGroupSpawner:
def __init__(
self,
flight: Flight,
country: Country,
mission: Mission,
helipads: dict[ControlPoint, list[StaticGroup]],
) -> None:
self.flight = flight
self.country = country
self.mission = mission
self.helipads = helipads
def create_flight_group(self) -> FlyingGroup[Any]:
return self.generate_flight_at_departure()
def create_idle_aircraft(self) -> FlyingGroup[Any]:
assert isinstance(self.flight.squadron.location, Airfield)
group = self._generate_at_airport(
name=namegen.next_aircraft_name(
self.country, self.flight.departure.id, self.flight
),
airport=self.flight.squadron.location.airport,
)
group.uncontrolled = True
return group
def generate_flight_at_departure(self) -> FlyingGroup[Any]:
name = namegen.next_aircraft_name(
self.country, self.flight.departure.id, self.flight
)
cp = self.flight.departure
try:
if self.flight.start_type is StartType.IN_FLIGHT:
group = self._generate_over_departure(name, cp)
return group
elif isinstance(cp, NavalControlPoint):
group_name = cp.get_carrier_group_name()
carrier_group = self.mission.find_group(group_name)
if not isinstance(carrier_group, ShipGroup):
raise RuntimeError(
f"Carrier group {carrier_group} is a "
"{carrier_group.__class__.__name__}, expected a ShipGroup"
)
return self._generate_at_group(name, carrier_group)
else:
# If the flight is an helicopter flight, then prioritize dedicated
# helipads
if self.flight.unit_type.helicopter:
return self._generate_at_cp_helipad(name, cp)
if not isinstance(cp, Airfield):
raise RuntimeError(
f"Attempted to spawn at airfield for non-airfield {cp}"
)
return self._generate_at_airport(name, cp.airport)
except NoParkingSlotError:
# Generated when there is no place on Runway or on Parking Slots
logging.exception(
"No room on runway or parking slots. Starting from the air."
)
self.flight.start_type = StartType.IN_FLIGHT
group = self._generate_over_departure(name, cp)
group.points[0].alt = 1500
return group
def _generate_at_airport(self, name: str, airport: Airport) -> FlyingGroup[Any]:
# TODO: Delayed runway starts should be converted to air starts for multiplayer.
# Runway starts do not work with late activated aircraft in multiplayer. Instead
# of spawning on the runway the aircraft will spawn on the taxiway, potentially
# somewhere that they don't fit anyway. We should either upgrade these to air
# starts or (less likely) downgrade to warm starts to avoid the issue when the
# player is generating the mission for multiplayer (which would need a new
# option).
return self.mission.flight_group_from_airport(
country=self.country,
name=name,
aircraft_type=self.flight.unit_type.dcs_unit_type,
airport=airport,
maintask=None,
start_type=self.dcs_start_type(),
group_size=self.flight.count,
parking_slots=None,
)
def _generate_over_departure(
self, name: str, origin: ControlPoint
) -> FlyingGroup[Any]:
at = origin.position
alt_type = "RADIO"
if isinstance(origin, OffMapSpawn):
alt = self.flight.flight_plan.waypoints[0].alt
alt_type = self.flight.flight_plan.waypoints[0].alt_type
elif self.flight.unit_type.helicopter:
alt = WARM_START_HELI_ALT
else:
alt = WARM_START_ALTITUDE
speed = GroundSpeed.for_flight(self.flight, alt)
pos = Point(at.x + random.randint(100, 1000), at.y + random.randint(100, 1000))
group = self.mission.flight_group(
country=self.country,
name=name,
aircraft_type=self.flight.unit_type.dcs_unit_type,
airport=None,
position=pos,
altitude=alt.meters,
speed=speed.kph,
maintask=None,
group_size=self.flight.count,
)
group.points[0].alt_type = alt_type
return group
def _generate_at_group(
self, name: str, at: Union[ShipGroup, StaticGroup]
) -> FlyingGroup[Any]:
return self.mission.flight_group_from_unit(
country=self.country,
name=name,
aircraft_type=self.flight.unit_type.dcs_unit_type,
pad_group=at,
maintask=None,
start_type=self._start_type_at_group(at),
group_size=self.flight.count,
)
def _generate_at_cp_helipad(self, name: str, cp: ControlPoint) -> FlyingGroup[Any]:
try:
helipad = self.helipads[cp].pop()
except IndexError as ex:
raise RuntimeError(f"Not enough helipads available at {cp}") from ex
group = self._generate_at_group(name, helipad)
# Note : A bit dirty, need better support in pydcs
group.points[0].action = PointAction.FromGroundArea
group.points[0].type = "TakeOffGround"
group.units[0].heading = helipad.units[0].heading
if self.flight.start_type != "Cold":
group.points[0].action = PointAction.FromGroundAreaHot
group.points[0].type = "TakeOffGroundHot"
for i in range(self.flight.count - 1):
try:
helipad = self.helipads[cp].pop()
group.units[1 + i].position = Point(helipad.x, helipad.y)
group.units[1 + i].heading = helipad.units[0].heading
except IndexError as ex:
raise RuntimeError(f"Not enough helipads available at {cp}") from ex
return group
def dcs_start_type(self) -> DcsStartType:
if self.flight.start_type is StartType.RUNWAY:
return DcsStartType.Runway
elif self.flight.start_type is StartType.COLD:
return DcsStartType.Cold
elif self.flight.start_type is StartType.WARM:
return DcsStartType.Warm
raise ValueError(
f"There is no pydcs StartType matching {self.flight.start_type}"
)
def _start_type_at_group(
self,
at: Union[ShipGroup, StaticGroup],
) -> 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)
if (
self.flight.unit_type.dcs_unit_type == Su_33
and group_units[0] is not None
and group_units[0].type == KUZNECOW.id
):
return DcsStartType.Runway
else:
return self.dcs_start_type()