Add plannable tankers.

This Pull Request lets users plan Tanker flights.

Features:

- Introduction of `Refueling` flight type.
- Tankers can be purchased at airbases and carriers.
- Tankers get planned by AI.
- Tankers are planned from airbases and at aircraft carriers.
- Tankers aim to be at high, fast, and 70 miles from the nearest threat.
  (A10s won't be able to tank)
- Tankers racetrack orbit for one hour.
- Optional Tickbox to enable legacy tankers.
- S-3B Tanker added to factions.
- KC-130 MPRS added to factions.
- Kneeboard shows planned tankers, their tacans, and radios.

Limitations:

- AI doesn't know whether to plan probe and drogue or boom refueling
  tankers.
- User can't choose tanker speed.  Heavily loaded aircraft may have
  trouble.
- User can't choose tanker altitude.  A-10s will not make it to high
  altitude.

Problems:

- Tanker callsigns do not increment, see attached image.  (Investigated:
  Need to use `FlyingType.callsign_dict`, instead of just
  `FlyingType.callsign`.  This seems like it might be significant work
  to do.).
- Having a flight of two or more tankers only spawns one tanker.
- Let me know if you have a solution, or feel free to commit one.

https://user-images.githubusercontent.com/74509817/120909602-d7bc3680-c633-11eb-80d7-eccd4e095770.png
This commit is contained in:
SnappyComebacks 2021-06-09 21:11:46 -07:00 committed by Dan Albert
parent a9dacf4a29
commit a53a648a63
31 changed files with 447 additions and 98 deletions

View File

@ -4,6 +4,10 @@ Saves from 3.x are not compatible with 4.0.
## Features/Improvements ## Features/Improvements
* **[Flight Planner]** Added ability to plan Tankers.
* **[Campaign AI]** AI will plan Tanker flights.
* **[Factions]** Added more tankers to factions.
## Fixes ## Fixes
# 3.0.0 # 3.0.0

View File

@ -798,6 +798,7 @@ CARRIER_CAPABLE = [
Su_33, Su_33,
A_4E_C, A_4E_C,
S_3B, S_3B,
S_3B_Tanker,
E_2C, E_2C,
UH_1H, UH_1H,
Mi_8MT, Mi_8MT,

View File

@ -167,7 +167,9 @@ class Faction:
faction.awacs = load_all_aircraft(json.get("awacs", [])) faction.awacs = load_all_aircraft(json.get("awacs", []))
faction.tankers = load_all_aircraft(json.get("tankers", [])) faction.tankers = load_all_aircraft(json.get("tankers", []))
faction.aircrafts = list(set(faction.aircrafts + faction.awacs)) faction.aircrafts = list(
set(faction.aircrafts + faction.awacs + faction.tankers)
)
faction.frontline_units = load_all_vehicles(json.get("frontline_units", [])) faction.frontline_units = load_all_vehicles(json.get("frontline_units", []))
faction.artillery_units = load_all_vehicles(json.get("artillery_units", [])) faction.artillery_units = load_all_vehicles(json.get("artillery_units", []))

View File

@ -375,6 +375,7 @@ class Operation:
cls.game.settings, cls.game.settings,
cls.game, cls.game,
cls.radio_registry, cls.radio_registry,
cls.tacan_registry,
cls.unit_map, cls.unit_map,
air_support=cls.airsupportgen.air_support, air_support=cls.airsupportgen.air_support,
) )

View File

@ -44,6 +44,7 @@ class Settings:
automate_aircraft_reinforcements: bool = False automate_aircraft_reinforcements: bool = False
restrict_weapons_by_date: bool = False restrict_weapons_by_date: bool = False
disable_legacy_aewc: bool = True disable_legacy_aewc: bool = True
disable_legacy_tanker: bool = True
generate_dark_kneeboard: bool = False generate_dark_kneeboard: bool = False
invulnerable_player_pilots: bool = True invulnerable_player_pilots: bool = True
auto_ato_behavior: AutoAtoBehavior = AutoAtoBehavior.Default auto_ato_behavior: AutoAtoBehavior = AutoAtoBehavior.Default

View File

@ -810,6 +810,7 @@ class Airfield(ControlPoint):
if self.is_friendly(for_player): if self.is_friendly(for_player):
yield from [ yield from [
FlightType.AEWC, FlightType.AEWC,
FlightType.REFUELING,
# TODO: FlightType.INTERCEPTION # TODO: FlightType.INTERCEPTION
# TODO: FlightType.LOGISTICS # TODO: FlightType.LOGISTICS
] ]
@ -959,7 +960,10 @@ class Carrier(NavalControlPoint):
yield from super().mission_types(for_player) yield from super().mission_types(for_player)
if self.is_friendly(for_player): if self.is_friendly(for_player):
yield FlightType.AEWC yield from [
FlightType.AEWC,
FlightType.REFUELING,
]
def capture(self, game: Game, for_player: bool) -> None: def capture(self, game: Game, for_player: bool) -> None:
raise RuntimeError("Carriers cannot be captured") raise RuntimeError("Carriers cannot be captured")

View File

@ -81,6 +81,7 @@ class FrontLine(MissionTarget):
yield from [ yield from [
FlightType.CAS, FlightType.CAS,
FlightType.AEWC, FlightType.AEWC,
FlightType.REFUELING
# TODO: FlightType.TROOP_TRANSPORT # TODO: FlightType.TROOP_TRANSPORT
# TODO: FlightType.EVAC # TODO: FlightType.EVAC
] ]

View File

@ -39,10 +39,12 @@ from dcs.planes import (
Su_33, Su_33,
Tu_22M3, Tu_22M3,
) )
from dcs.planes import IL_78M
from dcs.point import MovingPoint, PointAction from dcs.point import MovingPoint, PointAction
from dcs.task import ( from dcs.task import (
AWACS, AWACS,
AWACSTaskAction, AWACSTaskAction,
ActivateBeaconCommand,
AntishipStrike, AntishipStrike,
AttackGroup, AttackGroup,
Bombing, Bombing,
@ -61,8 +63,10 @@ from dcs.task import (
OptReactOnThreat, OptReactOnThreat,
OptRestrictJettison, OptRestrictJettison,
OrbitAction, OrbitAction,
Refueling,
RunwayAttack, RunwayAttack,
StartCommand, StartCommand,
Tanker,
Targets, Targets,
Transport, Transport,
WeaponType, WeaponType,
@ -80,7 +84,7 @@ from game.data.weapons import Pylon
from game.db import GUN_RELIANT_AIRFRAMES from game.db import GUN_RELIANT_AIRFRAMES
from game.factions.faction import Faction from game.factions.faction import Faction
from game.settings import Settings from game.settings import Settings
from game.squadrons import Pilot, Squadron from game.squadrons import Pilot
from game.theater.controlpoint import ( from game.theater.controlpoint import (
Airfield, Airfield,
ControlPoint, ControlPoint,
@ -103,13 +107,15 @@ from gen.flights.flight import (
) )
from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
from gen.runways import RunwayData from gen.runways import RunwayData
from .airsupportgen import AirSupport, AwacsInfo from gen.tacan import TacanBand, TacanRegistry
from .airsupportgen import AirSupport, AwacsInfo, TankerInfo
from .callsigns import callsign_for_support_unit from .callsigns import callsign_for_support_unit
from .flights.flightplan import ( from .flights.flightplan import (
AwacsFlightPlan, AwacsFlightPlan,
CasFlightPlan, CasFlightPlan,
LoiterFlightPlan, LoiterFlightPlan,
PatrollingFlightPlan, PatrollingFlightPlan,
RefuelingFlightPlan,
SweepFlightPlan, SweepFlightPlan,
) )
from .flights.traveltime import GroundSpeed, TotEstimator from .flights.traveltime import GroundSpeed, TotEstimator
@ -684,6 +690,7 @@ class AircraftConflictGenerator:
settings: Settings, settings: Settings,
game: Game, game: Game,
radio_registry: RadioRegistry, radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,
unit_map: UnitMap, unit_map: UnitMap,
air_support: AirSupport, air_support: AirSupport,
) -> None: ) -> None:
@ -691,6 +698,7 @@ class AircraftConflictGenerator:
self.game = game self.game = game
self.settings = settings self.settings = settings
self.radio_registry = radio_registry self.radio_registry = radio_registry
self.tacan_registy = tacan_registry
self.unit_map = unit_map self.unit_map = unit_map
self.flights: List[FlightData] = [] self.flights: List[FlightData] = []
self.air_support = air_support self.air_support = air_support
@ -824,7 +832,10 @@ class AircraftConflictGenerator:
OptReactOnThreat(OptReactOnThreat.Values.EvadeFire) OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)
) )
if flight.flight_type == FlightType.AEWC: if (
flight.flight_type == FlightType.AEWC
or flight.flight_type == FlightType.REFUELING
):
channel = self.radio_registry.alloc_uhf() channel = self.radio_registry.alloc_uhf()
else: else:
channel = self.get_intra_flight_channel(unit_type) channel = self.get_intra_flight_channel(unit_type)
@ -879,6 +890,24 @@ class AircraftConflictGenerator:
) )
) )
if isinstance(flight.flight_plan, RefuelingFlightPlan):
callsign = callsign_for_support_unit(group)
tacan = self.tacan_registy.alloc_for_band(TacanBand.Y)
variant = db.unit_type_name(flight.flight_plan.flight.unit_type)
self.air_support.tankers.append(
TankerInfo(
group_name=str(group.name),
callsign=callsign,
variant=variant,
freq=channel,
tacan=tacan,
start_time=flight.flight_plan.patrol_start_time,
end_time=flight.flight_plan.patrol_end_time,
blue=flight.departure.captured,
)
)
def _generate_at_airport( def _generate_at_airport(
self, self,
name: str, name: str,
@ -1457,6 +1486,32 @@ class AircraftConflictGenerator:
group.points[0].tasks.append(AWACSTaskAction()) group.points[0].tasks.append(AWACSTaskAction())
def configure_refueling(
self,
group: FlyingGroup,
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
) -> None:
group.task = Refueling.name
if not isinstance(flight.flight_plan, RefuelingFlightPlan):
logging.error(
f"Cannot configure racetrack refueling tasks for {flight} because it "
"does not have an racetrack refueling flight plan."
)
return
self._setup_group(group, package, flight, dynamic_runways)
self.configure_behavior(
flight,
group,
react_on_threat=OptReactOnThreat.Values.EvadeFire,
roe=OptROE.Values.WeaponHold,
restrict_jettison=True,
)
def configure_escort( def configure_escort(
self, self,
group: FlyingGroup, group: FlyingGroup,
@ -1535,6 +1590,8 @@ class AircraftConflictGenerator:
self.configure_sweep(group, package, flight, dynamic_runways) self.configure_sweep(group, package, flight, dynamic_runways)
elif flight_type == FlightType.AEWC: elif flight_type == FlightType.AEWC:
self.configure_awacs(group, package, flight, dynamic_runways) self.configure_awacs(group, package, flight, dynamic_runways)
elif flight_type == FlightType.REFUELING:
self.configure_refueling(group, package, flight, dynamic_runways)
elif flight_type in [FlightType.CAS, FlightType.BAI]: elif flight_type in [FlightType.CAS, FlightType.BAI]:
self.configure_cas(group, package, flight, dynamic_runways) self.configure_cas(group, package, flight, dynamic_runways)
elif flight_type == FlightType.DEAD: elif flight_type == FlightType.DEAD:
@ -1602,7 +1659,7 @@ class AircraftConflictGenerator:
for idx, point in enumerate(filtered_points): for idx, point in enumerate(filtered_points):
PydcsWaypointBuilder.for_waypoint( PydcsWaypointBuilder.for_waypoint(
point, group, package, flight, self.m point, group, package, flight, self.m, self.air_support
).build() ).build()
# Set here rather than when the FlightData is created so they waypoints # Set here rather than when the FlightData is created so they waypoints
@ -1676,12 +1733,14 @@ class PydcsWaypointBuilder:
package: Package, package: Package,
flight: Flight, flight: Flight,
mission: Mission, mission: Mission,
air_support: AirSupport,
) -> None: ) -> None:
self.waypoint = waypoint self.waypoint = waypoint
self.group = group self.group = group
self.package = package self.package = package
self.flight = flight self.flight = flight
self.mission = mission self.mission = mission
self.air_support = air_support
def build(self) -> MovingPoint: def build(self) -> MovingPoint:
waypoint = self.group.add_waypoint( waypoint = self.group.add_waypoint(
@ -1717,6 +1776,7 @@ class PydcsWaypointBuilder:
package: Package, package: Package,
flight: Flight, flight: Flight,
mission: Mission, mission: Mission,
air_support: AirSupport,
) -> PydcsWaypointBuilder: ) -> PydcsWaypointBuilder:
builders = { builders = {
FlightWaypointType.DROP_OFF: CargoStopBuilder, FlightWaypointType.DROP_OFF: CargoStopBuilder,
@ -1736,7 +1796,7 @@ class PydcsWaypointBuilder:
FlightWaypointType.PICKUP: CargoStopBuilder, FlightWaypointType.PICKUP: CargoStopBuilder,
} }
builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder) builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder)
return builder(waypoint, group, package, flight, mission) return builder(waypoint, group, package, flight, mission, air_support)
def _viggen_client_tot(self) -> bool: def _viggen_client_tot(self) -> bool:
"""Viggen player aircraft consider any waypoint with a TOT set to be a target ("M") waypoint. """Viggen player aircraft consider any waypoint with a TOT set to be a target ("M") waypoint.
@ -2119,6 +2179,8 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
# is their first priority and they will not engage any targets because # is their first priority and they will not engage any targets because
# they're fully focused on orbiting. If the STE task is first, they will # they're fully focused on orbiting. If the STE task is first, they will
# engage targets if available and orbit if they find nothing to shoot. # engage targets if available and orbit if they find nothing to shoot.
if self.flight.flight_type is FlightType.REFUELING:
self.configure_refueling_actions(waypoint)
# TODO: Move the properties of this task into the flight plan? # TODO: Move the properties of this task into the flight plan?
# CAP is the only current user of this so it's not a big deal, but might # CAP is the only current user of this so it's not a big deal, but might
@ -2133,17 +2195,48 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
) )
) )
racetrack = ControlledTask( # TODO: Set orbit speeds for all race tracks and remove this special case.
OrbitAction( if isinstance(flight_plan, RefuelingFlightPlan):
orbit = OrbitAction(
altitude=waypoint.alt,
pattern=OrbitAction.OrbitPattern.RaceTrack,
speed=int(flight_plan.patrol_speed.kph),
)
else:
orbit = OrbitAction(
altitude=waypoint.alt, pattern=OrbitAction.OrbitPattern.RaceTrack altitude=waypoint.alt, pattern=OrbitAction.OrbitPattern.RaceTrack
) )
)
racetrack = ControlledTask(orbit)
self.set_waypoint_tot(waypoint, flight_plan.patrol_start_time) self.set_waypoint_tot(waypoint, flight_plan.patrol_start_time)
racetrack.stop_after_time(int(flight_plan.patrol_end_time.total_seconds())) racetrack.stop_after_time(int(flight_plan.patrol_end_time.total_seconds()))
waypoint.add_task(racetrack) waypoint.add_task(racetrack)
return waypoint return waypoint
def configure_refueling_actions(self, waypoint: MovingPoint) -> None:
waypoint.add_task(Tanker())
if self.flight.unit_type != IL_78M:
tanker_info = self.air_support.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,
)
)
class RaceTrackEndBuilder(PydcsWaypointBuilder): class RaceTrackEndBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint: def build(self) -> MovingPoint:

View File

@ -54,6 +54,8 @@ class TankerInfo:
variant: str variant: str
freq: RadioFrequency freq: RadioFrequency
tacan: TacanChannel tacan: TacanChannel
start_time: Optional[timedelta]
end_time: Optional[timedelta]
blue: bool blue: bool
@ -100,84 +102,86 @@ class AirSupportConflictGenerator:
else self.conflict.red_cp else self.conflict.red_cp
) )
fallback_tanker_number = 0 if not self.game.settings.disable_legacy_tanker:
for i, tanker_unit_type in enumerate( fallback_tanker_number = 0
self.game.faction_for(player=True).tankers
): for i, tanker_unit_type in enumerate(
alt, airspeed = self._get_tanker_params(tanker_unit_type) self.game.faction_for(player=True).tankers
variant = db.unit_type_name(tanker_unit_type) ):
freq = self.radio_registry.alloc_uhf() alt, airspeed = self._get_tanker_params(tanker_unit_type)
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y) variant = db.unit_type_name(tanker_unit_type)
tanker_heading = ( freq = self.radio_registry.alloc_uhf()
self.conflict.red_cp.position.heading_between_point( tacan = self.tacan_registry.alloc_for_band(TacanBand.Y)
self.conflict.blue_cp.position tanker_heading = (
self.conflict.red_cp.position.heading_between_point(
self.conflict.blue_cp.position
)
+ TANKER_HEADING_OFFSET * i
) )
+ TANKER_HEADING_OFFSET * i tanker_position = player_cp.position.point_from_heading(
) tanker_heading, TANKER_DISTANCE
tanker_position = player_cp.position.point_from_heading( )
tanker_heading, TANKER_DISTANCE tanker_group = self.mission.refuel_flight(
) country=self.mission.country(self.game.player_country),
tanker_group = self.mission.refuel_flight( name=namegen.next_tanker_name(
country=self.mission.country(self.game.player_country), self.mission.country(self.game.player_country), tanker_unit_type
name=namegen.next_tanker_name( ),
self.mission.country(self.game.player_country), tanker_unit_type airport=None,
), plane_type=tanker_unit_type,
airport=None, position=tanker_position,
plane_type=tanker_unit_type, altitude=alt,
position=tanker_position, race_distance=58000,
altitude=alt, frequency=freq.mhz,
race_distance=58000, start_type=StartType.Warm,
frequency=freq.mhz, speed=airspeed,
start_type=StartType.Warm, tacanchannel=str(tacan),
speed=airspeed, )
tacanchannel=str(tacan), tanker_group.set_frequency(freq.mhz)
)
tanker_group.set_frequency(freq.mhz)
callsign = callsign_for_support_unit(tanker_group) callsign = callsign_for_support_unit(tanker_group)
tacan_callsign = { tacan_callsign = {
"Texaco": "TEX", "Texaco": "TEX",
"Arco": "ARC", "Arco": "ARC",
"Shell": "SHL", "Shell": "SHL",
}.get(callsign) }.get(callsign)
if tacan_callsign is None: if tacan_callsign is None:
# The dict above is all the callsigns currently in the game, but # The dict above is all the callsigns currently in the game, but
# non-Western countries don't use the callsigns and instead just # non-Western countries don't use the callsigns and instead just
# use numbers. It's possible that none of those nations have # use numbers. It's possible that none of those nations have
# TACAN compatible refueling aircraft, but fallback just in # TACAN compatible refueling aircraft, but fallback just in
# case. # case.
tacan_callsign = f"TK{fallback_tanker_number}" tacan_callsign = f"TK{fallback_tanker_number}"
fallback_tanker_number += 1 fallback_tanker_number += 1
if tanker_unit_type != IL_78M: if tanker_unit_type != IL_78M:
# Override PyDCS tacan channel. # Override PyDCS tacan channel.
tanker_group.points[0].tasks.pop() tanker_group.points[0].tasks.pop()
tanker_group.points[0].tasks.append( tanker_group.points[0].tasks.append(
ActivateBeaconCommand( ActivateBeaconCommand(
tacan.number, tacan.number,
tacan.band.value, tacan.band.value,
tacan_callsign, tacan_callsign,
True, True,
tanker_group.units[0].id, tanker_group.units[0].id,
True, True,
)
)
tanker_group.points[0].tasks.append(SetInvisibleCommand(True))
tanker_group.points[0].tasks.append(SetImmortalCommand(True))
self.air_support.tankers.append(
TankerInfo(
str(tanker_group.name),
callsign,
variant,
freq,
tacan,
blue=True,
) )
) )
tanker_group.points[0].tasks.append(SetInvisibleCommand(True))
tanker_group.points[0].tasks.append(SetImmortalCommand(True))
self.air_support.tankers.append(
TankerInfo(
str(tanker_group.name),
callsign,
variant,
freq,
tacan,
blue=True,
)
)
if not self.game.settings.disable_legacy_aewc: if not self.game.settings.disable_legacy_aewc:
possible_awacs = [ possible_awacs = [
a a

View File

@ -183,6 +183,7 @@ class Package:
FlightType.TARCAP, FlightType.TARCAP,
FlightType.BARCAP, FlightType.BARCAP,
FlightType.AEWC, FlightType.AEWC,
FlightType.REFUELING,
FlightType.SWEEP, FlightType.SWEEP,
FlightType.ESCORT, FlightType.ESCORT,
] ]

View File

@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
import math
import operator import operator
import random import random
from collections import defaultdict from collections import defaultdict
@ -524,6 +525,24 @@ class ObjectiveFinder:
raise RuntimeError("Found no friendly control points. You probably lost.") raise RuntimeError("Found no friendly control points. You probably lost.")
return farthest return farthest
def closest_friendly_control_point(self) -> ControlPoint:
"""Finds the friendly control point that is closest to any threats."""
threat_zones = self.game.threat_zone_for(not self.is_player)
closest = None
min_distance = meters(math.inf)
for cp in self.friendly_control_points():
if isinstance(cp, OffMapSpawn):
continue
distance = threat_zones.distance_to_threat(cp.position)
if distance < min_distance:
closest = cp
min_distance = distance
if closest is None:
raise RuntimeError("Found no friendly control points. You probably lost.")
return closest
def enemy_control_points(self) -> Iterator[ControlPoint]: def enemy_control_points(self) -> Iterator[ControlPoint]:
"""Iterates over all enemy control points.""" """Iterates over all enemy control points."""
return ( return (
@ -582,6 +601,7 @@ class CoalitionMissionPlanner:
MAX_SEAD_RANGE = nautical_miles(150) MAX_SEAD_RANGE = nautical_miles(150)
MAX_STRIKE_RANGE = nautical_miles(150) MAX_STRIKE_RANGE = nautical_miles(150)
MAX_AWEC_RANGE = nautical_miles(200) MAX_AWEC_RANGE = nautical_miles(200)
MAX_TANKER_RANGE = nautical_miles(200)
def __init__(self, game: Game, is_player: bool) -> None: def __init__(self, game: Game, is_player: bool) -> None:
self.game = game self.game = game
@ -628,6 +648,11 @@ class CoalitionMissionPlanner:
asap=True, asap=True,
) )
yield ProposedMission(
self.objective_finder.closest_friendly_control_point(),
[ProposedFlight(FlightType.REFUELING, 1, self.MAX_TANKER_RANGE)],
)
# Find friendly CPs within 100 nmi from an enemy airfield, plan CAP. # Find friendly CPs within 100 nmi from an enemy airfield, plan CAP.
for cp in self.objective_finder.vulnerable_control_points(): for cp in self.objective_finder.vulnerable_control_points():
# Plan CAP in such a way, that it is established during the whole desired mission length # Plan CAP in such a way, that it is established during the whole desired mission length

View File

@ -51,10 +51,13 @@ from dcs.planes import (
F_5E_3, F_5E_3,
F_86F_Sabre, F_86F_Sabre,
IL_76MD, IL_76MD,
I_16, IL_78M,
JF_17, JF_17,
J_11A, J_11A,
Ju_88A4, Ju_88A4,
KC130,
KC135MPRS,
KC_135,
KJ_2000, KJ_2000,
L_39ZA, L_39ZA,
MQ_9_Reaper, MQ_9_Reaper,
@ -77,6 +80,7 @@ from dcs.planes import (
P_51D_30_NA, P_51D_30_NA,
RQ_1A_Predator, RQ_1A_Predator,
S_3B, S_3B,
S_3B_Tanker,
SpitfireLFMkIX, SpitfireLFMkIX,
SpitfireLFMkIXCW, SpitfireLFMkIXCW,
Su_17M4, Su_17M4,
@ -103,8 +107,8 @@ from dcs.unittype import FlyingType
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
from pydcs_extensions.a4ec.a4ec import A_4E_C from pydcs_extensions.a4ec.a4ec import A_4E_C
from pydcs_extensions.f22a.f22a import F_22A from pydcs_extensions.f22a.f22a import F_22A
from pydcs_extensions.jas39.jas39 import JAS39Gripen, JAS39Gripen_AG
from pydcs_extensions.hercules.hercules import Hercules from pydcs_extensions.hercules.hercules import Hercules
from pydcs_extensions.jas39.jas39 import JAS39Gripen, JAS39Gripen_AG
from pydcs_extensions.mb339.mb339 import MB_339PAN from pydcs_extensions.mb339.mb339 import MB_339PAN
from pydcs_extensions.su57.su57 import Su_57 from pydcs_extensions.su57.su57 import Su_57
@ -401,6 +405,15 @@ AEWC_CAPABLE = [
KJ_2000, KJ_2000,
] ]
# Priority is given to the tankers that can carry the most fuel.
REFUELING_CAPABALE = [
KC_135,
KC135MPRS,
IL_78M,
KC130,
S_3B_Tanker,
]
def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]: def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
cap_missions = (FlightType.BARCAP, FlightType.TARCAP, FlightType.SWEEP) cap_missions = (FlightType.BARCAP, FlightType.TARCAP, FlightType.SWEEP)
@ -428,6 +441,8 @@ def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
return CAP_CAPABLE return CAP_CAPABLE
elif task == FlightType.AEWC: elif task == FlightType.AEWC:
return AEWC_CAPABLE return AEWC_CAPABLE
elif task == FlightType.REFUELING:
return REFUELING_CAPABALE
elif task == FlightType.TRANSPORT: elif task == FlightType.TRANSPORT:
return TRANSPORT_CAPABLE return TRANSPORT_CAPABLE
else: else:

View File

@ -69,6 +69,7 @@ class FlightType(Enum):
AEWC = "AEW&C" AEWC = "AEW&C"
TRANSPORT = "Transport" TRANSPORT = "Transport"
SEAD_ESCORT = "SEAD Escort" SEAD_ESCORT = "SEAD Escort"
REFUELING = "Refueling"
def __str__(self) -> str: def __str__(self) -> str:
return self.value return self.value

View File

@ -16,12 +16,21 @@ from functools import cached_property
from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple
from dcs.mapping import Point from dcs.mapping import Point
from dcs.planes import E_3A, E_2C, A_50, KJ_2000 from dcs.planes import (
E_3A,
E_2C,
A_50,
IL_78M,
KC130,
KC135MPRS,
KC_135,
KJ_2000,
S_3B_Tanker,
)
from dcs.unit import Unit from dcs.unit import Unit
from shapely.geometry import Point as ShapelyPoint from shapely.geometry import Point as ShapelyPoint
from game.data.doctrine import Doctrine from game.data.doctrine import Doctrine
from game.squadrons import Pilot
from game.theater import ( from game.theater import (
Airfield, Airfield,
ControlPoint, ControlPoint,
@ -31,7 +40,7 @@ from game.theater import (
TheaterGroundObject, TheaterGroundObject,
) )
from game.theater.theatergroundobject import EwrGroundObject from game.theater.theatergroundobject import EwrGroundObject
from game.utils import Distance, Speed, feet, meters, nautical_miles from game.utils import Distance, Speed, feet, meters, nautical_miles, knots
from .closestairfields import ObjectiveDistanceCache from .closestairfields import ObjectiveDistanceCache
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
from .traveltime import GroundSpeed, TravelTime from .traveltime import GroundSpeed, TravelTime
@ -769,6 +778,28 @@ class AwacsFlightPlan(LoiterFlightPlan):
return self.push_time return self.push_time
@dataclass(frozen=True)
class RefuelingFlightPlan(PatrollingFlightPlan):
takeoff: FlightWaypoint
land: FlightWaypoint
divert: Optional[FlightWaypoint]
bullseye: FlightWaypoint
#: Racetrack speed.
patrol_speed: Speed
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.takeoff
yield from self.nav_to
yield self.patrol_start
yield self.patrol_end
yield from self.nav_from
yield self.land
if self.divert is not None:
yield self.divert
yield self.bullseye
@dataclass(frozen=True) @dataclass(frozen=True)
class AirliftFlightPlan(FlightPlan): class AirliftFlightPlan(FlightPlan):
takeoff: FlightWaypoint takeoff: FlightWaypoint
@ -919,6 +950,8 @@ class FlightPlanBuilder:
return self.generate_aewc(flight) return self.generate_aewc(flight)
elif task == FlightType.TRANSPORT: elif task == FlightType.TRANSPORT:
return self.generate_transport(flight) return self.generate_transport(flight)
elif task == FlightType.REFUELING:
return self.generate_refueling_racetrack(flight)
raise PlanningError(f"{task} flight plan generation not implemented") raise PlanningError(f"{task} flight plan generation not implemented")
def regenerate_package_waypoints(self) -> None: def regenerate_package_waypoints(self) -> None:
@ -1612,6 +1645,88 @@ class FlightPlanBuilder:
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
) )
def generate_refueling_racetrack(self, flight: Flight) -> RefuelingFlightPlan:
location = self.package.target
closest_boundary = self.threat_zones.closest_boundary(location.position)
heading_to_threat_boundary = location.position.heading_between_point(
closest_boundary
)
distance_to_threat = meters(
location.position.distance_to_point(closest_boundary)
)
orbit_heading = heading_to_threat_boundary
# Station 70nm outside the threat zone.
threat_buffer = nautical_miles(70)
if self.threat_zones.threatened(location.position):
orbit_distance = distance_to_threat + threat_buffer
else:
orbit_distance = distance_to_threat - threat_buffer
racetrack_center = location.position.point_from_heading(
orbit_heading, orbit_distance.meters
)
racetrack_half_distance = Distance.from_nautical_miles(20).meters
racetrack_start = racetrack_center.point_from_heading(
orbit_heading + 90, racetrack_half_distance
)
racetrack_end = racetrack_center.point_from_heading(
orbit_heading - 90, racetrack_half_distance
)
builder = WaypointBuilder(flight, self.game, self.is_player)
tanker_type = flight.unit_type
if tanker_type is KC_135:
# ~300 knots IAS.
speed = knots(445)
altitude = feet(24000)
elif tanker_type is KC135MPRS:
# ~300 knots IAS.
speed = knots(440)
altitude = feet(23000)
elif tanker_type is KC130:
# ~210 knots IAS, roughly the max for the KC-130 at altitude.
speed = knots(370)
altitude = feet(22000)
elif tanker_type is S_3B_Tanker:
# ~265 knots IAS.
speed = knots(320)
altitude = feet(12000)
elif tanker_type is IL_78M:
# ~280 knots IAS.
speed = knots(400)
altitude = feet(21000)
else:
# ~280 knots IAS.
speed = knots(400)
altitude = feet(21000)
racetrack = builder.race_track(racetrack_start, racetrack_end, altitude)
return RefuelingFlightPlan(
package=self.package,
flight=flight,
takeoff=builder.takeoff(flight.departure),
nav_to=builder.nav_path(
flight.departure.position, racetrack_start, altitude
),
nav_from=builder.nav_path(racetrack_end, flight.arrival.position, altitude),
patrol_start=racetrack[0],
patrol_end=racetrack[1],
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert),
bullseye=builder.bullseye(),
patrol_duration=timedelta(hours=1),
patrol_speed=speed,
# TODO: Factor out a common base of the combat and non-combat race-tracks.
# No harm in setting this, but we ought to clean up a bit.
engagement_distance=meters(0),
)
@staticmethod @staticmethod
def target_waypoint( def target_waypoint(
flight: Flight, builder: WaypointBuilder, target: StrikeTarget flight: Flight, builder: WaypointBuilder, target: StrikeTarget

View File

@ -143,4 +143,6 @@ class QUnitInfoWindow(QDialog):
aircraft_tasks = aircraft_tasks + f"{FlightType.OCA_RUNWAY}, " aircraft_tasks = aircraft_tasks + f"{FlightType.OCA_RUNWAY}, "
if self.unit_type in gen.flights.ai_flight_planner_db.STRIKE_CAPABLE: if self.unit_type in gen.flights.ai_flight_planner_db.STRIKE_CAPABLE:
aircraft_tasks = aircraft_tasks + f"{FlightType.STRIKE}, " aircraft_tasks = aircraft_tasks + f"{FlightType.STRIKE}, "
if self.unit_type in gen.flights.ai_flight_planner_db.REFUELING_CAPABALE:
aircraft_tasks = aircraft_tasks + f"{FlightType.REFUELING}, "
return aircraft_tasks[:-2] return aircraft_tasks[:-2]

View File

@ -1,5 +1,5 @@
import logging import logging
from typing import Optional, Set, Type from typing import Set, Type
from PySide2.QtCore import Qt from PySide2.QtCore import Qt
from PySide2.QtWidgets import ( from PySide2.QtWidgets import (
@ -13,7 +13,6 @@ from PySide2.QtWidgets import (
QWidget, QWidget,
) )
from dcs.helicopters import helicopter_map from dcs.helicopters import helicopter_map
from dcs.task import CAP, CAS, AWACS, Transport
from dcs.unittype import FlyingType, UnitType from dcs.unittype import FlyingType, UnitType
from game import db from game import db
@ -45,8 +44,6 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
def init_ui(self): def init_ui(self):
main_layout = QVBoxLayout() main_layout = QVBoxLayout()
tasks = [CAP, CAS, AWACS, Transport]
scroll_content = QWidget() scroll_content = QWidget()
task_box_layout = QGridLayout() task_box_layout = QGridLayout()
row = 0 row = 0

View File

@ -494,6 +494,28 @@ class QSettingsWindow(QDialog):
general_layout.addWidget(old_awac_label, 1, 0) general_layout.addWidget(old_awac_label, 1, 0)
general_layout.addWidget(old_awac, 1, 1, Qt.AlignRight) general_layout.addWidget(old_awac, 1, 1, Qt.AlignRight)
def set_old_tanker(value: bool) -> None:
self.game.settings.disable_legacy_tanker = not value
old_tanker = QCheckBox()
old_tanker.setChecked(not self.game.settings.disable_legacy_tanker)
old_tanker.toggled.connect(set_old_tanker)
old_tanker_info = (
"If checked, an invulnerable friendly Tanker aircraft that begins the "
"mission on station will be be spawned. This behavior will be removed in a "
"future release."
)
old_tanker.setToolTip(old_tanker_info)
old_tanker_label = QLabel(
"Spawn invulnerable, always-available Tanker aircraft (deprecated)."
)
old_tanker_label.setToolTip(old_tanker_info)
general_layout.addWidget(old_tanker_label, 2, 0)
general_layout.addWidget(old_tanker, 2, 1, Qt.AlignRight)
campaign_layout.addWidget(HqAutomationSettingsBox(self.game)) campaign_layout.addWidget(HqAutomationSettingsBox(self.game))
def initGeneratorLayout(self): def initGeneratorLayout(self):

View File

@ -32,7 +32,8 @@
], ],
"tankers": [ "tankers": [
"KC_135", "KC_135",
"KC130" "KC130",
"S-3B Tanker"
], ],
"frontline_units": [ "frontline_units": [
"MBT_M1A2_Abrams", "MBT_M1A2_Abrams",

View File

@ -33,7 +33,9 @@
], ],
"tankers": [ "tankers": [
"KC_135", "KC_135",
"KC130" "KC135MPRS",
"KC130",
"S-3B Tanker"
], ],
"frontline_units": [ "frontline_units": [
"MBT_M1A2_Abrams", "MBT_M1A2_Abrams",

View File

@ -28,7 +28,9 @@
], ],
"tankers": [ "tankers": [
"KC_135", "KC_135",
"KC130" "KC135MPRS",
"KC130",
"S-3B Tanker"
], ],
"frontline_units": [ "frontline_units": [
"MBT_M1A2_Abrams", "MBT_M1A2_Abrams",

View File

@ -15,6 +15,10 @@
"awacs": [ "awacs": [
"E_2C" "E_2C"
], ],
"tankers": [
"KC_135",
"KC130"
],
"frontline_units": [ "frontline_units": [
"MBT_M60A3_Patton", "MBT_M60A3_Patton",
"APC_M113", "APC_M113",

View File

@ -18,6 +18,10 @@
"awacs": [ "awacs": [
"E_2C" "E_2C"
], ],
"tankers": [
"KC_135",
"KC130"
],
"frontline_units": [ "frontline_units": [
"MBT_M60A3_Patton", "MBT_M60A3_Patton",
"APC_M113", "APC_M113",

View File

@ -32,7 +32,8 @@
], ],
"tankers": [ "tankers": [
"KC_135", "KC_135",
"KC130" "KC130",
"S-3B Tanker"
], ],
"frontline_units": [ "frontline_units": [
"MBT_M1A2_Abrams", "MBT_M1A2_Abrams",

View File

@ -32,7 +32,9 @@
], ],
"tankers": [ "tankers": [
"KC_135", "KC_135",
"KC130" "KC135MPRS",
"KC130",
"S-3B Tanker"
], ],
"frontline_units": [ "frontline_units": [
"MBT_M1A2_Abrams", "MBT_M1A2_Abrams",

View File

@ -33,7 +33,9 @@
], ],
"tankers": [ "tankers": [
"KC_135", "KC_135",
"KC130" "KC135MPRS",
"KC130",
"S-3B Tanker"
], ],
"frontline_units": [ "frontline_units": [
"MBT_M1A2_Abrams", "MBT_M1A2_Abrams",

View File

@ -28,7 +28,9 @@
], ],
"tankers": [ "tankers": [
"KC_135", "KC_135",
"KC130" "KC135MPRS",
"KC130",
"S-3B Tanker"
], ],
"frontline_units": [ "frontline_units": [
"MBT_M1A2_Abrams", "MBT_M1A2_Abrams",

View File

@ -17,7 +17,7 @@
"E_2C" "E_2C"
], ],
"tankers": [ "tankers": [
"S_3B_Tanker" "S-3B Tanker"
], ],
"frontline_units": [ "frontline_units": [
"MBT_M60A3_Patton", "MBT_M60A3_Patton",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -422,6 +422,36 @@
"year-of-variant-introduction": "1995" "year-of-variant-introduction": "1995"
} }
}], }],
"KC130": [{
"default": {
"name": "KC-130",
"text": "The Lockheed Martin (previously Lockheed) KC-130 is a family of the extended-range tanker version of the C-130 Hercules transport aircraft modified for aerial refueling.",
"country-of-origin": "USA",
"manufacturer": "Lockheed Martin",
"role": "Tanker",
"year-of-variant-introduction": "1962"
}
}],
"KC-135": [{
"default": {
"name": "KC-135 Stratotanker",
"text": "The Boeing KC-135 Stratotanker is a military aerial refueling aircraft that was developed from the Boeing 367-80 prototype, alongside the Boeing 707 airliner.",
"country-of-origin": "USA",
"manufacturer": "Beoing",
"role": "Tanker",
"year-of-variant-introduction": "1957"
}
}],
"KC135MPRS": [{
"default": {
"name": "KC-135 Stratotanker MPRS",
"text": "The Boeing KC-135 Stratotanker is a military aerial refueling aircraft that was developed from the Boeing 367-80 prototype, alongside the Boeing 707 airliner. This model has the Multi-point Refueling System modification, allowing for probe and drogue refuelling.",
"country-of-origin": "USA",
"manufacturer": "Beoing",
"role": "Tanker",
"year-of-variant-introduction": "1994"
}
}],
"L-39C": [{ "L-39C": [{
"default": { "default": {
"name": "L-39C Albatros", "name": "L-39C Albatros",
@ -743,6 +773,16 @@
"year-of-variant-introduction": "1984" "year-of-variant-introduction": "1984"
} }
}], }],
"S-3B Tanker": [{
"default": {
"name": "S-3B Tanker",
"text": "The Lockheed S-3 Viking is a 4-crew, twin-engine turbofan-powered jet aircraft that was used by the U.S. Navy (USN) primarily for anti-submarine warfare. In the late 1990s, the S-3B's mission focus shifted to surface warfare and aerial refueling. The Viking also provided electronic warfare and surface surveillance capabilities to a carrier battle group. A carrier-based, subsonic, all-weather, long-range, multi-mission aircraft, it carried automated weapon systems and was capable of extended missions with in-flight refueling. Because of its characteristic sound, it was nicknamed the \"War Hoover\" after the vacuum cleaner brand. The S-3 was phased out from front-line fleet service aboard aircraft carriers in January 2009, with its missions taken over by aircraft like the P-3C Orion, P-8 Poseidon, Sikorsky SH-60 Seahawk and Boeing F/A-18E/F Super Hornet",
"country-of-origin": "USA",
"manufacturer": "Lockheed",
"role": "Carrier-based Tanker",
"year-of-variant-introduction": "1984"
}
}],
"SA342L": [{ "SA342L": [{
"default": { "default": {
"name": "SA 342L Gazelle", "name": "SA 342L Gazelle",