mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
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:
parent
a245ba80c3
commit
9a81121ac1
@ -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.
|
||||
|
||||
@ -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()
|
||||
|
||||
87
game/ato/flightplans/shiprecoverytanker.py
Normal file
87
game/ato/flightplans/shiprecoverytanker.py
Normal 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())
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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."
|
||||
|
||||
@ -177,6 +177,7 @@ class AircraftGenerator:
|
||||
self.mission_data,
|
||||
dynamic_runways,
|
||||
self.use_client,
|
||||
self.unit_map,
|
||||
).configure()
|
||||
)
|
||||
return group
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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(
|
||||
|
||||
56
game/missiongenerator/aircraft/waypoints/recoverytanker.py
Normal file
56
game/missiongenerator/aircraft/waypoints/recoverytanker.py
Normal 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,
|
||||
)
|
||||
)
|
||||
@ -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:
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user