Recovery tanker support (#429)

* fix conflict

* squash bugs and reuse patrol layout

* fix tanker tacan and formatting

* fix unlimited fuel option

* update pretense for tanker changes

* reuse refueling flight plan and bugfix for sunken carrier

changelog

* remove unitmap dependency

* formatting and more unit map removal

* more formatting

* typing and black

* keep tanker out of clouds

* fix if there are no clouds

* better cloud handling

* groundwork for recovery task

* remove changes to game/commander

* Finishing up recovery tankers

---------

Co-authored-by: Raffson <Raffson@users.noreply.github.com>
This commit is contained in:
Druss99 2024-12-22 23:39:10 -05:00 committed by GitHub
parent a4671571bc
commit dd7e4c908e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 395 additions and 25 deletions

View File

@ -41,6 +41,7 @@
* **[Mission Generation]** Enable Supercarrier's LSO & Airboss stations
* **[UX]** Default settings are now loaded from Default.zip
* **[Autoplanner]** Plan Air-to-Air Escorts for AWACS & Tankers
* **[Package Planning]** Ability to plan recovery tanker flights
## Fixes
* **[UI/UX]** A-10A flights can be edited again

View File

@ -60,6 +60,16 @@ class CustomFlightPlan(FlightPlan[CustomLayout]):
def mission_departure_time(self) -> datetime:
return self.package.time_over_target
@property
def landing_time(self) -> datetime:
arrival = (
self.layout.custom_waypoints[-1]
if self.layout.custom_waypoints
else self.layout.departure
)
return_time = self.total_time_between_waypoints(self.tot_waypoint, arrival)
return self.tot + return_time
class Builder(IBuilder[CustomFlightPlan, CustomLayout]):
def __init__(

View File

@ -296,6 +296,10 @@ class FlightPlan(ABC, Generic[LayoutT]):
"""The time that the mission is complete and the flight RTBs."""
raise NotImplementedError
@property
def landing_time(self) -> datetime:
raise NotImplementedError
@self_type_guard
def is_loiter(self, flight_plan: FlightPlan[Any]) -> TypeGuard[LoiterFlightPlan]:
return False

View File

@ -3,6 +3,7 @@ from __future__ import annotations
from typing import Any, TYPE_CHECKING, Type
from game.ato import FlightType
from game.theater.controlpoint import NavalControlPoint
from .aewc import AewcFlightPlan
from .airassault import AirAssaultFlightPlan
from .airlift import AirliftFlightPlan
@ -22,6 +23,7 @@ from .planningerror import PlanningError
from .pretensecargo import PretenseCargoFlightPlan
from .sead import SeadFlightPlan
from .seadsweep import SeadSweepFlightPlan
from .shiprecoverytanker import RecoveryTankerFlightPlan
from .strike import StrikeFlightPlan
from .sweep import SweepFlightPlan
from .tarcap import TarCapFlightPlan
@ -64,6 +66,7 @@ class FlightPlanBuilderTypes:
FlightType.AIR_ASSAULT: AirAssaultFlightPlan.builder_type(),
FlightType.PRETENSE_CARGO: PretenseCargoFlightPlan.builder_type(),
FlightType.ARMED_RECON: ArmedReconFlightPlan.builder_type(),
FlightType.RECOVERY: RecoveryTankerFlightPlan.builder_type(),
}
try:
return builder_dict[flight.flight_type]

View File

@ -0,0 +1,61 @@
from __future__ import annotations
from datetime import timedelta
from typing import Type
from game.ato.flightplans.ibuilder import IBuilder
from game.ato.flightplans.waypointbuilder import WaypointBuilder
from .patrolling import PatrollingLayout
from .refuelingflightplan import RefuelingFlightPlan
from .. import FlightWaypoint
from ...utils import knots
class RecoveryTankerFlightPlan(RefuelingFlightPlan):
@staticmethod
def builder_type() -> Type[Builder]:
return Builder
@property
def patrol_duration(self) -> timedelta:
return self.flight.coalition.game.settings.desired_tanker_on_station_time
@property
def tot_waypoint(self) -> FlightWaypoint:
return self.layout.departure
class Builder(IBuilder[RecoveryTankerFlightPlan, PatrollingLayout]):
def layout(self) -> PatrollingLayout:
builder = WaypointBuilder(self.flight)
altitude = builder.get_patrol_altitude
station_time = self.coalition.game.settings.desired_tanker_on_station_time
time_to_landing = station_time.total_seconds()
hdg = (self.coalition.game.conditions.weather.wind.at_0m.direction + 180) % 360
recovery_ship = self.package.target.position.point_from_heading(
hdg, time_to_landing * knots(20).meters_per_second
)
recovery_tanker = builder.recovery_tanker(recovery_ship)
patrol_end = builder.race_track_end(recovery_tanker.position, altitude)
patrol_end.only_for_player = True # avoid generating the waypoints
return PatrollingLayout(
departure=builder.takeoff(self.flight.departure),
nav_to=builder.nav_path(
self.flight.departure.position, recovery_ship, altitude
),
nav_from=builder.nav_path(
recovery_ship, self.flight.arrival.position, altitude
),
patrol_start=recovery_tanker,
patrol_end=patrol_end,
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
custom_waypoints=list(),
)
def build(self, dump_debug_info: bool = False) -> RecoveryTankerFlightPlan:
return RecoveryTankerFlightPlan(self.flight, self.layout())

View File

@ -3,6 +3,7 @@ from __future__ import annotations
from abc import ABC
from copy import deepcopy
from dataclasses import dataclass
from datetime import datetime
from typing import TYPE_CHECKING, TypeVar, Optional
from game.ato.flightplans.flightplan import FlightPlan, Layout
@ -86,3 +87,10 @@ class StandardFlightPlan(FlightPlan[LayoutT], ABC):
others are guaranteed to have certain properties like departure and arrival points,
potentially a divert field, and a bullseye
"""
@property
def landing_time(self) -> datetime:
return_time = self.total_time_between_waypoints(
self.tot_waypoint, self.layout.arrival
)
return self.tot + return_time

View File

@ -800,3 +800,18 @@ class WaypointBuilder:
x_adj = random.randint(int(-deviation.meters), int(deviation.meters))
y_adj = random.randint(int(-deviation.meters), int(deviation.meters))
return point + Vector2(x_adj, y_adj)
@staticmethod
def recovery_tanker(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",
only_for_player=True, # for visual purposes in Retribution only
)

View File

@ -60,6 +60,7 @@ class FlightType(Enum):
SEAD_SWEEP = "SEAD Sweep" # Reintroduce legacy "engage-whatever-you-can-find" SEAD
PRETENSE_CARGO = "Cargo Transport" # For Pretense campaign AI cargo planes
ARMED_RECON = "Armed Recon"
RECOVERY = "Recovery"
def __str__(self) -> str:
return self.value
@ -117,6 +118,7 @@ class FlightType(Enum):
FlightType.INTERCEPTION: AirEntity.FIGHTER,
FlightType.OCA_AIRCRAFT: AirEntity.ATTACK_STRIKE,
FlightType.OCA_RUNWAY: AirEntity.ATTACK_STRIKE,
FlightType.RECOVERY: AirEntity.TANKER,
FlightType.REFUELING: AirEntity.TANKER,
FlightType.SEAD: AirEntity.SUPPRESSION_OF_ENEMY_AIR_DEFENCE,
FlightType.SEAD_ESCORT: AirEntity.SUPPRESSION_OF_ENEMY_AIR_DEFENCE,

View File

@ -52,3 +52,4 @@ class FlightWaypointType(IntEnum):
INGRESS_ANTI_SHIP = 32
INGRESS_SEAD_SWEEP = 33
INGRESS_ARMED_RECON = 34
RECOVERY_TANKER = 35 # Tanker recovery point

View File

@ -188,6 +188,7 @@ class Package(RadioFrequencyContainer):
FlightType.BARCAP,
FlightType.AEWC,
FlightType.FERRY,
FlightType.RECOVERY,
FlightType.REFUELING,
FlightType.SWEEP,
FlightType.SEAD_ESCORT,

View File

@ -40,6 +40,8 @@ class MissionScheduler:
p for p in self.coalition.ato.packages if p.primary_task not in dca_types
]
carrier_etas = []
start_time = start_time_generator(
count=len(non_dca_packages),
earliest=5 * 60,
@ -47,6 +49,8 @@ class MissionScheduler:
margin=5 * 60,
)
for package in self.coalition.ato.packages:
if package.primary_task is FlightType.RECOVERY:
continue
tot = TotEstimator(package).earliest_tot(now)
if package.primary_task in dca_types:
previous_end_time = previous_cap_end_time[package.target]
@ -74,3 +78,19 @@ class MissionScheduler:
# to be present. Runway and air started aircraft will be
# delayed until their takeoff time by AirConflictGenerator.
package.time_over_target = next(start_time) + tot
arrivals = []
for f in package.flights:
if f.departure.is_fleet and not f.is_helo:
arrivals.append(f.flight_plan.landing_time - timedelta(minutes=10))
if arrivals:
carrier_etas.append(min(arrivals))
for package in [
p
for p in self.coalition.ato.packages
if p.primary_task is FlightType.RECOVERY
]:
if carrier_etas:
package.time_over_target = carrier_etas.pop(0)
else:
break

View File

@ -246,6 +246,9 @@ class ObjectiveFinder:
raise RuntimeError("Found no friendly control points. You probably lost.")
return closest
def friendly_naval_control_points(self) -> Iterator[ControlPoint]:
return (cp for cp in self.friendly_control_points() if cp.is_fleet)
def enemy_control_points(self) -> Iterator[ControlPoint]:
"""Iterates over all enemy control points."""
return (

View File

@ -53,7 +53,8 @@ class PackageBuilder:
if pf:
target = (
pf.departure
if pf.flight_type in [FlightType.AEWC, FlightType.REFUELING]
if pf.flight_type
in [FlightType.AEWC, FlightType.REFUELING, FlightType.RECOVERY]
else target
)
heli = pf.is_helo

View File

@ -14,6 +14,7 @@ from game.commander.tasks.compound.interdictreinforcements import (
InterdictReinforcements,
)
from game.commander.tasks.compound.protectairspace import ProtectAirSpace
from game.commander.tasks.compound.recoverysupport import RecoverySupport
from game.commander.tasks.compound.theatersupport import TheaterSupport
from game.commander.theaterstate import TheaterState
from game.htn import CompoundTask, Method
@ -34,3 +35,4 @@ class PlanNextAction(CompoundTask[TheaterState]):
yield [AttackBuildings()]
yield [AttackShips()]
yield [DegradeIads()]
yield [RecoverySupport()] # for recovery tankers

View File

@ -0,0 +1,16 @@
from collections.abc import Iterator
from game.commander.tasks.primitive.recovery import PlanRecovery
from game.commander.theaterstate import TheaterState
from game.htn import CompoundTask, Method
class RecoverySupport(CompoundTask[TheaterState]):
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
yield [PlanRecoverySupport()]
class PlanRecoverySupport(CompoundTask[TheaterState]):
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
for target in state.recovery_targets:
yield [PlanRecovery(target)]

View File

@ -16,7 +16,7 @@ from game.commander.tasks.theatercommandertask import TheaterCommanderTask
from game.commander.theaterstate import TheaterState
from game.data.groups import GroupTask
from game.settings import AutoAtoBehavior
from game.theater import MissionTarget
from game.theater import MissionTarget, ControlPoint
from game.theater.theatergroundobject import IadsGroundObject, NavalGroundObject
from game.utils import Distance, meters
@ -51,6 +51,15 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
return False
return self.fulfill_mission(state)
def apply_effects(self, state: TheaterState) -> None:
seen: set[ControlPoint] = set()
if not self.package:
return
for f in self.package.flights:
if f.departure.is_fleet and not f.is_helo and f.departure not in seen:
state.recovery_targets[f.departure] += f.count
seen.add(f.departure)
def execute(self, coalition: Coalition) -> None:
if self.package is None:
raise RuntimeError("Attempted to execute failed package planning task")

View File

@ -22,6 +22,7 @@ class PlanAewc(PackagePlanningTask[MissionTarget]):
def apply_effects(self, state: TheaterState) -> None:
state.aewc_targets.remove(self.target)
super().apply_effects(state)
def propose_flights(self) -> None:
self.propose_flight(FlightType.AEWC, 1)

View File

@ -19,6 +19,7 @@ class PlanAirAssault(PackagePlanningTask[ControlPoint]):
def apply_effects(self, state: TheaterState) -> None:
state.vulnerable_control_points.remove(self.target)
super().apply_effects(state)
def propose_flights(self) -> None:
self.propose_flight(FlightType.AIR_ASSAULT, self.get_flight_size())

View File

@ -1,7 +1,6 @@
from __future__ import annotations
from dataclasses import dataclass
from random import randint
from game.ato.flighttype import FlightType
from game.commander.missionproposals import EscortType
@ -21,6 +20,7 @@ class PlanAntiShip(PackagePlanningTask[NavalGroundObject]):
def apply_effects(self, state: TheaterState) -> None:
state.eliminate_ship(self.target)
super().apply_effects(state)
def propose_flights(self) -> None:
size = self.get_flight_size()

View File

@ -20,6 +20,7 @@ class PlanAntiShipping(PackagePlanningTask[CargoShip]):
def apply_effects(self, state: TheaterState) -> None:
state.enemy_shipping.remove(self.target)
super().apply_effects(state)
def propose_flights(self) -> None:
size = self.get_flight_size()

View File

@ -19,6 +19,7 @@ class PlanArmedRecon(PackagePlanningTask[ControlPoint]):
def apply_effects(self, state: TheaterState) -> None:
state.control_point_priority_queue.remove(self.target)
super().apply_effects(state)
def propose_flights(self) -> None:
self.propose_flight(FlightType.ARMED_RECON, self.get_flight_size())

View File

@ -19,6 +19,7 @@ class PlanBai(PackagePlanningTask[VehicleGroupGroundObject]):
def apply_effects(self, state: TheaterState) -> None:
state.eliminate_battle_position(self.target)
super().apply_effects(state)
def propose_flights(self) -> None:
tgt_count = self.target.alive_unit_count

View File

@ -1,8 +1,6 @@
from __future__ import annotations
import random
from dataclasses import dataclass
from random import randint
from game.ato.flighttype import FlightType
from game.commander.tasks.packageplanningtask import PackagePlanningTask
@ -21,6 +19,7 @@ class PlanBarcap(PackagePlanningTask[ControlPoint]):
def apply_effects(self, state: TheaterState) -> None:
state.barcaps_needed[self.target] -= 1
super().apply_effects(state)
def propose_flights(self) -> None:
size = self.get_flight_size()

View File

@ -28,6 +28,7 @@ class PlanCas(PackagePlanningTask[FrontLine]):
def apply_effects(self, state: TheaterState) -> None:
state.vulnerable_front_lines.remove(self.target)
super().apply_effects(state)
def propose_flights(self) -> None:
size = self.get_flight_size()

View File

@ -19,6 +19,7 @@ class PlanConvoyInterdiction(PackagePlanningTask[Convoy]):
def apply_effects(self, state: TheaterState) -> None:
state.enemy_convoys.remove(self.target)
super().apply_effects(state)
def propose_flights(self) -> None:
self.propose_flight(FlightType.BAI, 2)

View File

@ -23,6 +23,7 @@ class PlanDead(PackagePlanningTask[IadsGroundObject]):
def apply_effects(self, state: TheaterState) -> None:
state.eliminate_air_defense(self.target)
super().apply_effects(state)
def propose_flights(self) -> None:
tgt_count = self.target.alive_unit_count

View File

@ -22,6 +22,7 @@ class PlanOcaStrike(PackagePlanningTask[ControlPoint]):
def apply_effects(self, state: TheaterState) -> None:
state.oca_targets.remove(self.target)
super().apply_effects(state)
def propose_flights(self) -> None:
size = self.get_flight_size()

View File

@ -0,0 +1,32 @@
from __future__ import annotations
from dataclasses import dataclass
from game.ato.flighttype import FlightType
from game.commander.tasks.packageplanningtask import PackagePlanningTask
from game.commander.theaterstate import TheaterState
from game.theater import ControlPoint
@dataclass
class PlanRecovery(PackagePlanningTask[ControlPoint]):
def preconditions_met(self, state: TheaterState) -> bool:
if (
state.context.coalition.player
and not state.context.settings.auto_ato_behavior_tankers
):
return False
ac_per_tanker = state.context.settings.aircraft_per_recovery_tanker
if not (
self.target in state.recovery_targets
and state.recovery_targets[self.target] >= ac_per_tanker
):
return False
return super().preconditions_met(state)
def apply_effects(self, state: TheaterState) -> None:
ac_per_tanker = state.context.settings.aircraft_per_recovery_tanker
state.recovery_targets[self.target] -= ac_per_tanker
def propose_flights(self) -> None:
self.propose_flight(FlightType.RECOVERY, 1)

View File

@ -22,6 +22,7 @@ class PlanRefueling(PackagePlanningTask[MissionTarget]):
def apply_effects(self, state: TheaterState) -> None:
state.refueling_targets.remove(self.target)
super().apply_effects(state)
def propose_flights(self) -> None:
self.propose_flight(FlightType.REFUELING, 1)

View File

@ -20,6 +20,7 @@ class PlanStrike(PackagePlanningTask[TheaterGroundObject]):
def apply_effects(self, state: TheaterState) -> None:
state.strike_targets.remove(self.target)
super().apply_effects(state)
def propose_flights(self) -> None:
tgt_count = self.target.alive_unit_count

View File

@ -15,7 +15,12 @@ from game.ground_forces.combat_stance import CombatStance
from game.htn import WorldState
from game.profiling import MultiEventTracer
from game.settings import Settings
from game.theater import ConflictTheater, ControlPoint, FrontLine, MissionTarget
from game.theater import (
ConflictTheater,
ControlPoint,
FrontLine,
MissionTarget,
)
from game.theater.theatergroundobject import (
BuildingGroundObject,
IadsGroundObject,
@ -51,6 +56,7 @@ class TheaterState(WorldState["TheaterState"]):
vulnerable_front_lines: list[FrontLine]
aewc_targets: list[MissionTarget]
refueling_targets: list[MissionTarget]
recovery_targets: dict[ControlPoint, int]
enemy_air_defenses: list[IadsGroundObject]
threatening_air_defenses: list[Union[IadsGroundObject, NavalGroundObject]]
detecting_air_defenses: list[Union[IadsGroundObject, NavalGroundObject]]
@ -118,6 +124,7 @@ class TheaterState(WorldState["TheaterState"]):
vulnerable_front_lines=list(self.vulnerable_front_lines),
aewc_targets=list(self.aewc_targets),
refueling_targets=list(self.refueling_targets),
recovery_targets=dict(self.recovery_targets),
enemy_air_defenses=list(self.enemy_air_defenses),
enemy_convoys=list(self.enemy_convoys),
enemy_shipping=list(self.enemy_shipping),
@ -186,6 +193,7 @@ class TheaterState(WorldState["TheaterState"]):
vulnerable_front_lines=list(finder.front_lines()),
aewc_targets=[finder.farthest_friendly_control_point()],
refueling_targets=[finder.closest_friendly_control_point()],
recovery_targets={cp: 0 for cp in finder.friendly_naval_control_points()},
enemy_air_defenses=list(finder.enemy_air_defenses()),
threatening_air_defenses=[],
detecting_air_defenses=[],

View File

@ -262,6 +262,12 @@ class AircraftType(UnitType[Type[FlyingType]]):
):
enrich[FlightType.ARMED_RECON] = value
if FlightType.RECOVERY not in self.task_priorities:
if (
value := self.task_priorities.get(FlightType.REFUELING)
) and self.carrier_capable is True:
enrich[FlightType.RECOVERY] = value
self.task_priorities.update(enrich)
def __eq__(self, other: object) -> bool:

View File

@ -554,6 +554,7 @@ class Game:
FlightType.TRANSPORT,
FlightType.AEWC,
FlightType.REFUELING,
FlightType.RECOVERY,
]:
# BARCAPs will be planned at most locations on smaller theaters,
# rendering culling fairly useless. BARCAP packages don't really

View File

@ -30,22 +30,33 @@ from dcs.task import (
OptNoReportWaypointPass,
OptRadioUsageContact,
OptRadioSilence,
Tanker,
RecoveryTanker,
ActivateBeaconCommand,
ControlledTask,
)
from dcs.unitgroup import FlyingGroup
from dcs.unitgroup import FlyingGroup, ShipGroup
from game.ato import Flight, FlightType
from game.ato import Flight, FlightType, Package
from game.ato.flightplans.aewc import AewcFlightPlan
from game.ato.flightplans.formationattack import FormationAttackLayout
from game.ato.flightplans.packagerefueling import PackageRefuelingFlightPlan
from game.ato.flightplans.shiprecoverytanker import RecoveryTankerFlightPlan
from game.ato.flightplans.theaterrefueling import TheaterRefuelingFlightPlan
from game.utils import nautical_miles
from game.missiongenerator.missiondata import MissionData
from game.utils import nautical_miles, knots, feet
class AircraftBehavior:
def __init__(self, task: FlightType) -> None:
def __init__(self, task: FlightType, mission_data: MissionData) -> None:
self.task = task
self.mission_data = mission_data
def apply_to(self, flight: Flight, group: FlyingGroup[Any]) -> None:
def apply_to(
self,
flight: Flight,
group: FlyingGroup[Any],
) -> None:
if self.task in [
FlightType.BARCAP,
FlightType.TARCAP,
@ -58,6 +69,8 @@ class AircraftBehavior:
self.configure_awacs(group, flight)
elif self.task == FlightType.REFUELING:
self.configure_refueling(group, flight)
elif self.task == FlightType.RECOVERY:
self.configure_recovery(group, flight)
elif self.task in [FlightType.CAS, FlightType.BAI]:
self.configure_cas(group, flight)
elif self.task == FlightType.ARMED_RECON:
@ -317,6 +330,7 @@ class AircraftBehavior:
if not (
isinstance(flight.flight_plan, TheaterRefuelingFlightPlan)
or isinstance(flight.flight_plan, PackageRefuelingFlightPlan)
or isinstance(flight.flight_plan, RecoveryTankerFlightPlan)
):
logging.error(
f"Cannot configure racetrack refueling tasks for {flight} because it "
@ -332,6 +346,95 @@ class AircraftBehavior:
restrict_jettison=True,
)
def configure_recovery(
self,
group: FlyingGroup[Any],
flight: Flight,
) -> None:
self.configure_refueling(group, flight)
if not isinstance(flight.flight_plan, RecoveryTankerFlightPlan):
logging.error(
f"Cannot configure recovery task for {flight} because it "
"does not have an recovery tanker flight plan."
)
return
self.configure_tanker_tacan(flight, group)
clouds = flight.squadron.coalition.game.conditions.weather.clouds
speed = knots(250).meters_per_second
altitude = feet(6000).meters
if clouds is not None:
if abs(clouds.base - altitude) < feet(1000).meters:
altitude = clouds.base - feet(1000).meters
if altitude < feet(2000).meters:
altitude = clouds.base + feet(6000).meters
naval_group = self._get_carrier_group(flight.package)
last_waypoint = len(naval_group.points) # last waypoint of the CVN/LHA
tanker_tos = flight.coalition.game.settings.desired_tanker_on_station_time
lua_predicate = f"""
local lowfuel = false
for i, unitObject in pairs(Group.getByName('{group.name}'):getUnits()) do
if Unit.getFuel(unitObject) < 0.2 then lowfuel = true end
end
return lowfuel
"""
tanker = ControlledTask(Tanker())
tanker.stop_after_duration(int(tanker_tos.total_seconds()) + 1)
tanker.stop_if_lua_predicate(lua_predicate)
group.points[0].add_task(tanker)
recovery = ControlledTask(
RecoveryTanker(naval_group.id, speed, altitude, last_waypoint)
)
recovery.stop_if_lua_predicate(lua_predicate)
recovery.stop_after_duration(int(tanker_tos.total_seconds()) + 1)
group.points[0].add_task(recovery)
def configure_tanker_tacan(self, flight: Flight, group: FlyingGroup[Any]) -> None:
tanker_info = self.mission_data.tankers[-1]
tacan = tanker_info.tacan
if flight.unit_type.dcs_unit_type.tacan and tacan:
if flight.tcn_name is None:
cs = tanker_info.callsign[:-2]
csn = tanker_info.callsign[-1]
tacan_callsign = {
"Texaco": "TX",
"Arco": "AC",
"Shell": "SH",
}.get(cs)
if tacan_callsign:
tacan_callsign = tacan_callsign + csn
else:
tacan_callsign = cs[0:2] + csn
else:
tacan_callsign = flight.tcn_name
group.points[0].add_task(
ActivateBeaconCommand(
tacan.number,
tacan.band.value,
tacan_callsign.upper(),
bearing=True,
unit_id=group.units[0].id,
aa=True,
)
)
def _get_carrier_group(self, package: Package) -> ShipGroup:
name = package.target.name
carrier_position = package.target.position
for carrier in self.mission_data.carriers:
if carrier.ship_group.position == carrier_position:
return carrier.ship_group
raise RuntimeError(
f"Could not find a carrier in the mission matching {name} at "
f"({carrier_position.x}, {carrier_position.y})"
)
def configure_escort(self, group: FlyingGroup[Any], flight: Flight) -> None:
self.configure_task(flight, group, Escort)
self.configure_behavior(

View File

@ -15,6 +15,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
from game.missiongenerator.logisticsgenerator import LogisticsGenerator
@ -79,12 +80,14 @@ class FlightGroupConfigurator:
self.use_client = use_client
def configure(self) -> FlightData:
AircraftBehavior(self.flight.flight_type).apply_to(self.flight, self.group)
flight_channel = self.setup_radios()
AircraftBehavior(self.flight.flight_type, self.mission_data).apply_to(
self.flight, self.group
)
AircraftPainter(self.flight, self.group).apply_livery()
self.setup_props()
self.setup_payloads()
self.setup_fuel()
flight_channel = self.setup_radios()
laser_codes: list[Optional[int]] = []
for unit, member in zip(self.group.units, self.flight.iter_members()):
@ -197,7 +200,11 @@ class FlightGroupConfigurator:
if freq not in self.radio_registry.allocated_channels:
self.radio_registry.reserve(freq)
if self.flight.flight_type in {FlightType.AEWC, FlightType.REFUELING}:
if self.flight.flight_type in {
FlightType.AEWC,
FlightType.REFUELING,
FlightType.RECOVERY,
}:
self.register_air_support(freq)
elif self.flight.client_count and self.flight.squadron.radio_presets:
freq = self.flight.squadron.radio_presets["intra_flight"][0]
@ -222,9 +229,11 @@ class FlightGroupConfigurator:
unit=self.group.units[0],
)
)
elif isinstance(
self.flight.flight_plan, TheaterRefuelingFlightPlan
) or isinstance(self.flight.flight_plan, PackageRefuelingFlightPlan):
elif (
isinstance(self.flight.flight_plan, TheaterRefuelingFlightPlan)
or isinstance(self.flight.flight_plan, PackageRefuelingFlightPlan)
or isinstance(self.flight.flight_plan, RecoveryTankerFlightPlan)
):
tacan = self.flight.tacan
if tacan is None and self.flight.squadron.aircraft.dcs_unit_type.tacan:
try:

View File

@ -100,7 +100,7 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
ActivateBeaconCommand(
tacan.number,
tacan.band.value,
tacan_callsign,
tacan_callsign.upper(),
bearing=True,
unit_id=self.group.units[0].id,
aa=True,

View File

@ -9,8 +9,6 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
class RaceTrackEndBuilder(PydcsWaypointBuilder):
def add_tasks(self, waypoint: MovingPoint) -> None:
flight_plan = self.flight.flight_plan
# Unlimited fuel option : enable at racetrack end. Must be first option to work.
if self.flight.squadron.coalition.game.settings.ai_unlimited_fuel:
waypoint.tasks.insert(0, SetUnlimitedFuelCommand(True))

View File

@ -8,6 +8,17 @@ class RefuelPointBuilder(PydcsWaypointBuilder):
def add_tasks(self, waypoint: MovingPoint) -> None:
if not self.ai_despawn(waypoint, True):
refuel = ControlledTask(RefuelingTaskAction())
refuel.start_probability(10)
refuel.start_if_lua_predicate(self._get_lua_predicate(0.2))
refuel.stop_if_lua_predicate(self._get_lua_predicate(0.5))
waypoint.add_task(refuel)
return super().add_tasks(waypoint)
def _get_lua_predicate(self, fuel_level: float) -> str:
return f"""
local okfuel = true
for i, unitObject in pairs(Group.getByName('{self.group.name}'):getUnits()) do
--trigger.action.outText(tostring(Unit.getFuel(unitObject)), 15)
if Unit.getFuel(unitObject) < {fuel_level} then okfuel = false; break end
end
return lowfuel
"""

View File

@ -5,6 +5,7 @@ from datetime import datetime
from typing import Optional, TYPE_CHECKING
from dcs.flyingunit import FlyingUnit
from dcs.unitgroup import ShipGroup
from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
@ -58,6 +59,7 @@ class CarrierInfo(UnitInfo):
tacan: TacanChannel
icls_channel: int | None
link4_freq: RadioFrequency | None
ship_group: ShipGroup
@dataclass

View File

@ -650,6 +650,7 @@ class GenericCarrierGenerator(GroundObjectGenerator):
icls_channel=icls,
link4_freq=link4,
blue=self.control_point.captured,
ship_group=ship_group,
)
)

View File

@ -77,12 +77,14 @@ class PretenseFlightGroupConfigurator(FlightGroupConfigurator):
self.use_client = use_client
def configure(self) -> FlightData:
AircraftBehavior(self.flight.flight_type).apply_to(self.flight, self.group)
flight_channel = self.setup_radios()
AircraftBehavior(self.flight.flight_type, self.mission_data).apply_to(
self.flight, self.group
)
AircraftPainter(self.flight, self.group).apply_livery()
self.setup_props()
self.setup_payloads()
self.setup_fuel()
flight_channel = self.setup_radios()
laser_codes: list[Optional[int]] = []
for unit, pilot in zip(self.group.units, self.flight.roster.members):

View File

@ -744,6 +744,7 @@ class PretenseGenericCarrierGenerator(GenericCarrierGenerator):
icls_channel=icls,
link4_freq=link4,
blue=self.control_point.captured,
ship_group=ship_group,
)
)

View File

@ -247,6 +247,17 @@ class Settings:
"provided the faction has access to them."
),
)
aircraft_per_recovery_tanker: int = bounded_int_option(
"Number of aircraft per recovery tanker",
page=CAMPAIGN_DOCTRINE_PAGE,
section=GENERAL_SECTION,
default=4,
min=2,
max=12,
detail=(
"A higher number will force the autoplanner to generate less recovery tankers."
),
)
oca_target_autoplanner_min_aircraft_count: int = bounded_int_option(
"Minimum number of aircraft (at vulnerable airfields) for auto-planner to plan OCA packages against",
page=CAMPAIGN_DOCTRINE_PAGE,

View File

@ -1369,6 +1369,9 @@ class NavalControlPoint(
if self.is_friendly(for_player):
yield from [
FlightType.AEWC,
FlightType.RECOVERY,
FlightType.REFUELING,
# TODO: FlightType.INTERCEPTION
# TODO: Buddy tanking for the A-4?
# TODO: Rescue chopper?
@ -1382,8 +1385,7 @@ class NavalControlPoint(
yield from super().mission_types(for_player)
if self.is_friendly(for_player):
yield from [
FlightType.AEWC,
FlightType.REFUELING,
# Nothing yet
]
@property

View File

@ -159,6 +159,21 @@ class QAutoCreateDialog(QDialog):
self.refueling_type,
)
hbox = QHBoxLayout()
self.recovery = self._create_checkbox("Recovery")
self.recovery_count = _spinbox_template()
self.recovery_count.setValue(1)
hbox.addWidget(self.recovery)
hbox.addWidget(self.recovery_count)
self.recovery_type = self._create_type_selector(FlightType.RECOVERY)
hbox.addWidget(self.recovery_type, 1)
self.layout.addLayout(hbox)
self.checkboxes[self.recovery] = (
FlightType.RECOVERY,
self.recovery_count,
self.recovery_type,
)
self.create_button = QPushButton("Create")
self.create_button.setProperty("style", "start-button")
self.create_button.clicked.connect(self.on_create_clicked)

View File

@ -26,3 +26,4 @@ variants:
S-3B Tanker: {}
tasks:
Refueling: 0
Recovery: 0