Recovery Tanker for carriers. (#2649)

Implement recovery tankers for carriers.

UnitMap gets a little more data to store.  Recovery tankers depend on the unit map.
This commit is contained in:
SnappyComebacks 2022-12-19 21:08:19 -07:00 committed by GitHub
parent a245ba80c3
commit 9a81121ac1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 204 additions and 12 deletions

View File

@ -14,6 +14,7 @@ Saves from 6.0.0 are compatible with 6.1.0
* **[Factions]** Defaulted bluefor modern to use Georgian and Ukrainian liveries for Russian aircraft.
* **[Factions]** Added Peru.
* **[Flight Planning]** Refueling flights planned on aircraft carriers will act as a recovery tanker for the carrier.
* **[Loadouts]** Adjusted F-15E loadouts.
* **[Modding]** Added support for the HMS Ariadne, Achilles, and Castle class.
* **[Modding]** Added HMS Invincible to the game data as a helicopter carrier.

View File

@ -3,6 +3,8 @@ from __future__ import annotations
from typing import Any, TYPE_CHECKING, Type
from game.ato import FlightType
from game.ato.flightplans.shiprecoverytanker import RecoveryTankerFlightPlan
from game.theater.controlpoint import Carrier
from .aewc import AewcFlightPlan
from .airassault import AirAssaultFlightPlan
from .airlift import AirliftFlightPlan
@ -33,8 +35,13 @@ 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
target = flight.package.target
if target.is_friendly(flight.squadron.player) and isinstance(
target, Carrier
):
return RecoveryTankerFlightPlan.builder_type()
if target.is_friendly(flight.squadron.player) or isinstance(
target, FrontLine
):
return TheaterRefuelingFlightPlan.builder_type()
return PackageRefuelingFlightPlan.builder_type()

View File

@ -0,0 +1,87 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
from typing import Iterator, Type
from game.ato.flightplans.standard import StandardFlightPlan, StandardLayout
from game.ato.flightplans.ibuilder import IBuilder
from game.ato.flightplans.standard import StandardLayout
from game.ato.flightplans.waypointbuilder import WaypointBuilder
from game.ato.flightwaypoint import FlightWaypoint
from game.utils import feet
@dataclass(frozen=True)
class RecoveryTankerLayout(StandardLayout):
nav_to: list[FlightWaypoint]
recovery_ship: FlightWaypoint
nav_from: list[FlightWaypoint]
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
yield from self.nav_to
yield self.recovery_ship
yield from self.nav_from
yield self.arrival
if self.divert is not None:
yield self.divert
yield self.bullseye
class RecoveryTankerFlightPlan(StandardFlightPlan[RecoveryTankerLayout]):
@staticmethod
def builder_type() -> Type[Builder]:
return Builder
@property
def tot_waypoint(self) -> FlightWaypoint:
return self.layout.recovery_ship
@property
def mission_departure_time(self) -> timedelta:
return timedelta(hours=2)
@property
def patrol_start_time(self) -> timedelta:
return self.package.time_over_target
@property
def patrol_end_time(self) -> timedelta:
return self.tot + self.mission_departure_time
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:
if waypoint == self.tot_waypoint:
return self.tot + self.mission_departure_time
return None
class Builder(IBuilder[RecoveryTankerFlightPlan, RecoveryTankerLayout]):
def layout(self) -> RecoveryTankerLayout:
# TODO: Propagate the ship position.
ship = self.package.target.position
builder = WaypointBuilder(self.flight, self.coalition)
recovery = builder.recovery_tanker(ship)
tanker_type = self.flight.unit_type
altitude = tanker_type.preferred_patrol_altitude
return RecoveryTankerLayout(
departure=builder.takeoff(self.flight.departure),
nav_to=builder.nav_path(self.flight.departure.position, ship, altitude),
nav_from=builder.nav_path(ship, self.flight.arrival.position, altitude),
recovery_ship=recovery,
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
)
def build(self) -> RecoveryTankerFlightPlan:
return RecoveryTankerFlightPlan(self.flight, self.layout())

View File

@ -23,7 +23,7 @@ from game.theater import (
TheaterGroundObject,
TheaterUnit,
)
from game.utils import Distance, meters, nautical_miles
from game.utils import Distance, feet, meters, nautical_miles
if TYPE_CHECKING:
from game.coalition import Coalition
@ -204,6 +204,19 @@ class WaypointBuilder:
pretty_name="Refuel",
)
def recovery_tanker(self, position: Point) -> FlightWaypoint:
alt_type: AltitudeReference = "BARO"
return FlightWaypoint(
"RECOVERY",
FlightWaypointType.RECOVERY_TANKER,
position,
feet(6000),
alt_type,
description="Recovery tanker for aircraft carriers",
pretty_name="Recovery",
)
def split(self, position: Point) -> FlightWaypoint:
alt_type: AltitudeReference = "BARO"
if self.is_helo:

View File

@ -49,3 +49,4 @@ class FlightWaypointType(IntEnum):
REFUEL = 29 # Should look for nearby tanker to refuel from.
CARGO_STOP = 30 # Stopover landing point using the LandingReFuAr waypoint type
INGRESS_AIR_ASSAULT = 31
RECOVERY_TANKER = 32

View File

@ -24,6 +24,7 @@ from dcs.unitgroup import FlyingGroup
from game.ato import Flight, FlightType
from game.ato.flightplans.aewc import AewcFlightPlan
from game.ato.flightplans.shiprecoverytanker import RecoveryTankerFlightPlan
from game.ato.flightplans.theaterrefueling import TheaterRefuelingFlightPlan
@ -246,7 +247,10 @@ class AircraftBehavior:
def configure_refueling(self, group: FlyingGroup[Any], flight: Flight) -> None:
group.task = Refueling.name
if not isinstance(flight.flight_plan, TheaterRefuelingFlightPlan):
if not (
isinstance(flight.flight_plan, TheaterRefuelingFlightPlan)
or isinstance(flight.flight_plan, RecoveryTankerFlightPlan)
):
logging.error(
f"Cannot configure racetrack refueling tasks for {flight} because it "
"does not have an racetrack refueling flight plan."

View File

@ -177,6 +177,7 @@ class AircraftGenerator:
self.mission_data,
dynamic_runways,
self.use_client,
self.unit_map,
).configure()
)
return group

View File

@ -10,6 +10,7 @@ from dcs.unit import Skill
from dcs.unitgroup import FlyingGroup
from game.ato import Flight, FlightType
from game.ato.flightplans.shiprecoverytanker import RecoveryTankerFlightPlan
from game.callsigns import callsign_for_support_unit
from game.data.weapons import Pylon, WeaponType as WeaponTypeEnum
from game.missiongenerator.missiondata import MissionData, AwacsInfo, TankerInfo
@ -19,6 +20,7 @@ from game.radio.radios import RadioFrequency, RadioRegistry
from game.radio.tacan import TacanBand, TacanRegistry, TacanUsage
from game.runways import RunwayData
from game.squadrons import Pilot
from game.unitmap import UnitMap
from .aircraftbehavior import AircraftBehavior
from .aircraftpainter import AircraftPainter
from .flightdata import FlightData
@ -44,6 +46,7 @@ class FlightGroupConfigurator:
mission_data: MissionData,
dynamic_runways: dict[str, RunwayData],
use_client: bool,
unit_map: UnitMap,
) -> None:
self.flight = flight
self.group = group
@ -56,6 +59,7 @@ class FlightGroupConfigurator:
self.mission_data = mission_data
self.dynamic_runways = dynamic_runways
self.use_client = use_client
self.unit_map = unit_map
def configure(self) -> FlightData:
AircraftBehavior(self.flight.flight_type).apply_to(self.flight, self.group)
@ -97,6 +101,7 @@ class FlightGroupConfigurator:
self.time,
self.game.settings,
self.mission_data,
self.unit_map,
).create_waypoints()
return FlightData(
@ -156,7 +161,9 @@ class FlightGroupConfigurator:
blue=self.flight.departure.captured,
)
)
elif isinstance(self.flight.flight_plan, TheaterRefuelingFlightPlan):
elif isinstance(
self.flight.flight_plan, TheaterRefuelingFlightPlan
) or isinstance(self.flight.flight_plan, RecoveryTankerFlightPlan):
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y, TacanUsage.AirToAir)
self.mission_data.tankers.append(
TankerInfo(

View File

@ -12,6 +12,7 @@ from game.ato import Flight, FlightWaypoint
from game.ato.flightwaypointtype import FlightWaypointType
from game.missiongenerator.missiondata import MissionData
from game.theater import MissionTarget, TheaterUnit
from game.unitmap import UnitMap
TARGET_WAYPOINTS = (
FlightWaypointType.TARGET_GROUP_LOC,
@ -29,6 +30,7 @@ class PydcsWaypointBuilder:
mission: Mission,
elapsed_mission_time: timedelta,
mission_data: MissionData,
unit_map: UnitMap,
) -> None:
self.waypoint = waypoint
self.group = group
@ -37,6 +39,7 @@ class PydcsWaypointBuilder:
self.mission = mission
self.elapsed_mission_time = elapsed_mission_time
self.mission_data = mission_data
self.unit_map = unit_map
def build(self) -> MovingPoint:
waypoint = self.group.add_waypoint(

View File

@ -0,0 +1,56 @@
from dcs.point import MovingPoint
from dcs.task import ActivateBeaconCommand, RecoveryTanker
from game.ato import FlightType
from game.missiongenerator.missiondata import TankerInfo
from game.utils import feet, knots
from .pydcswaypointbuilder import PydcsWaypointBuilder
class RecoveryTankerBuilder(PydcsWaypointBuilder):
def add_tasks(self, waypoint: MovingPoint) -> None:
if self.flight.flight_type == FlightType.REFUELING:
group_id = self._get_carrier_group_id()
speed = knots(250).meters_per_second
altitude = feet(6000).meters
# Last waypoint has index of 1.
last_waypoint = 2
recovery_tanker = RecoveryTanker(group_id, speed, altitude, last_waypoint)
waypoint.add_task(recovery_tanker)
self.configure_tanker_tacan(waypoint)
def _get_carrier_group_id(self) -> int:
name = self.package.target.name
carrier_position = self.package.target.position
theater_objects = self.unit_map.theater_objects
for key, value in theater_objects.items():
# Check name and position in case there are multiple of same carrier.
if name in key and value.theater_unit.position == carrier_position:
theater_mapping = value
break
assert theater_mapping is not None
return theater_mapping.dcs_group_id
def configure_tanker_tacan(self, waypoint: MovingPoint) -> None:
if self.flight.unit_type.dcs_unit_type.tacan:
tanker_info = self.mission_data.tankers[-1]
tacan = tanker_info.tacan
tacan_callsign = {
"Texaco": "TEX",
"Arco": "ARC",
"Shell": "SHL",
}.get(tanker_info.callsign)
waypoint.add_task(
ActivateBeaconCommand(
tacan.number,
tacan.band.value,
tacan_callsign,
bearing=True,
unit_id=self.group.units[0].id,
aa=True,
)
)

View File

@ -17,8 +17,12 @@ from game.ato.flightstate import InFlight, WaitingForStart
from game.ato.flightwaypointtype import FlightWaypointType
from game.ato.starttype import StartType
from game.missiongenerator.aircraft.waypoints.cargostop import CargoStopBuilder
from game.missiongenerator.aircraft.waypoints.recoverytanker import (
RecoveryTankerBuilder,
)
from game.missiongenerator.missiondata import MissionData
from game.settings import Settings
from game.unitmap import UnitMap
from game.utils import pairwise
from .baiingress import BaiIngressBuilder
from .landingzone import LandingZoneBuilder
@ -50,6 +54,7 @@ class WaypointGenerator:
time: datetime,
settings: Settings,
mission_data: MissionData,
unit_map: UnitMap,
) -> None:
self.flight = flight
self.group = group
@ -58,6 +63,7 @@ class WaypointGenerator:
self.time = time
self.settings = settings
self.mission_data = mission_data
self.unit_map = unit_map
def create_waypoints(self) -> tuple[timedelta, list[FlightWaypoint]]:
for waypoint in self.flight.points:
@ -135,6 +141,7 @@ class WaypointGenerator:
FlightWaypointType.PICKUP_ZONE: LandingZoneBuilder,
FlightWaypointType.DROPOFF_ZONE: LandingZoneBuilder,
FlightWaypointType.REFUEL: RefuelPointBuilder,
FlightWaypointType.RECOVERY_TANKER: RecoveryTankerBuilder,
FlightWaypointType.CARGO_STOP: CargoStopBuilder,
}
builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder)
@ -145,6 +152,7 @@ class WaypointGenerator:
self.mission,
self.elapsed_mission_time,
self.mission_data,
self.unit_map,
)
def _estimate_min_fuel_for(self, waypoints: list[FlightWaypoint]) -> None:

View File

@ -147,7 +147,7 @@ class GroundObjectGenerator:
vehicle_unit.position = unit.position
vehicle_unit.heading = unit.position.heading.degrees
vehicle_group.add_unit(vehicle_unit)
self._register_theater_unit(unit, vehicle_group.units[-1])
self._register_theater_unit(vehicle_group.id, unit, vehicle_group.units[-1])
if vehicle_group is None:
raise RuntimeError(f"Error creating VehicleGroup for {group_name}")
return vehicle_group
@ -180,7 +180,7 @@ class GroundObjectGenerator:
ship_unit.position = unit.position
ship_unit.heading = unit.position.heading.degrees
ship_group.add_unit(ship_unit)
self._register_theater_unit(unit, ship_group.units[-1])
self._register_theater_unit(ship_group.id, unit, ship_group.units[-1])
if ship_group is None:
raise RuntimeError(f"Error creating ShipGroup for {group_name}")
return ship_group
@ -194,7 +194,7 @@ class GroundObjectGenerator:
heading=unit.position.heading.degrees,
dead=not unit.alive,
)
self._register_theater_unit(unit, static_group.units[0])
self._register_theater_unit(static_group.id, unit, static_group.units[0])
@staticmethod
def enable_eplrs(group: VehicleGroup, unit_type: Type[VehicleType]) -> None:
@ -209,10 +209,11 @@ class GroundObjectGenerator:
def _register_theater_unit(
self,
dcs_group_id: int,
theater_unit: TheaterUnit,
dcs_unit: Unit,
) -> None:
self.unit_map.add_theater_unit_mapping(theater_unit, dcs_unit)
self.unit_map.add_theater_unit_mapping(dcs_group_id, theater_unit, dcs_unit)
def add_trigger_zone_for_scenery(self, scenery: SceneryUnit) -> None:
# Align the trigger zones to the faction color on the DCS briefing/F10 map.

View File

@ -34,6 +34,7 @@ class FrontLineUnit:
@dataclass(frozen=True)
class TheaterUnitMapping:
dcs_group_id: int
theater_unit: TheaterUnit
dcs_unit: Unit
@ -104,14 +105,16 @@ class UnitMap:
return self.front_line_units.get(name, None)
def add_theater_unit_mapping(
self, theater_unit: TheaterUnit, dcs_unit: Unit
self, dcs_group_id: int, theater_unit: TheaterUnit, dcs_unit: Unit
) -> None:
# Deaths for units at TGOs are recorded in the corresponding GroundUnit within
# the GroundGroup, so we have to match the dcs unit with the liberation unit
name = str(dcs_unit.name)
if name in self.theater_objects:
raise RuntimeError(f"Duplicate TGO unit: {name}")
self.theater_objects[name] = TheaterUnitMapping(theater_unit, dcs_unit)
self.theater_objects[name] = TheaterUnitMapping(
dcs_group_id, theater_unit, dcs_unit
)
def theater_units(self, name: str) -> Optional[TheaterUnitMapping]:
return self.theater_objects.get(name, None)

View File

@ -32,7 +32,7 @@ pluggy==1.0.0
pre-commit==2.19.0
py==1.11.0
pydantic==1.9.1
-e git+https://github.com/pydcs/dcs@f12d70ea844076f95c74ffab92ec3dc9fdee32e4#egg=pydcs
-e git+https://github.com/pydcs/dcs@e755b655b7b28e9af3c1fa42263424b568413c04#egg=pydcs
pyinstaller==5.2
pyinstaller-hooks-contrib==2022.8
pyparsing==3.0.9