diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index d9621457..5deb0c38 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -3,9 +3,9 @@ from __future__ import annotations import logging import operator from dataclasses import dataclass -from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple +from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple, Type -from dcs.unittype import UnitType +from dcs.unittype import FlyingType, UnitType from game import db from game.data.radar_db import UNITS_WITH_RADAR @@ -15,9 +15,13 @@ from gen import Conflict from gen.ato import Package from gen.flights.ai_flight_planner_db import ( CAP_CAPABLE, + CAP_PREFERRED, CAS_CAPABLE, + CAS_PREFERRED, SEAD_CAPABLE, + SEAD_PREFERRED, STRIKE_CAPABLE, + STRIKE_PREFERRED, ) from gen.flights.closestairfields import ( ClosestAirfields, @@ -102,30 +106,63 @@ class AircraftAllocator: maximum allowed range. If insufficient aircraft are available for the mission, None is returned. + Airfields are searched ordered nearest to farthest from the target and + searched twice. The first search looks for aircraft which prefer the + mission type, and the second search looks for any aircraft which are + capable of the mission type. For example, an F-14 from a nearby carrier + will be preferred for the CAP of an airfield that has only F-16s, but if + the carrier has only F/A-18s the F-16s will be used for CAP instead. + Note that aircraft *will* be removed from the global inventory on success. This is to ensure that the same aircraft are not matched twice on subsequent calls. If the found aircraft are not used, the caller is responsible for returning them to the inventory. """ - cap_missions = (FlightType.BARCAP, FlightType.CAP, FlightType.TARCAP) - if flight.task in cap_missions: - types = CAP_CAPABLE - elif flight.task == FlightType.CAS: - types = CAS_CAPABLE - elif flight.task in (FlightType.DEAD, FlightType.SEAD): - types = SEAD_CAPABLE - elif flight.task == FlightType.STRIKE: - types = STRIKE_CAPABLE - elif flight.task == FlightType.ESCORT: - types = CAP_CAPABLE - else: - logging.error(f"Unplannable flight type: {flight.task}") - return None + result = self.find_aircraft_of_type( + flight, self.preferred_aircraft_for_task(flight.task) + ) + if result is not None: + return result + return self.find_aircraft_of_type( + flight, self.capable_aircraft_for_task(flight.task) + ) - # TODO: Implement mission type weighting for aircraft. - # We should avoid assigning F/A-18s to CAP missions when there are F-15s - # available, since the F/A-18 is capable of performing other tasks that - # the F-15 is not capable of. + @staticmethod + def preferred_aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]: + cap_missions = (FlightType.BARCAP, FlightType.CAP, FlightType.TARCAP) + if task in cap_missions: + return CAP_PREFERRED + elif task == FlightType.CAS: + return CAS_PREFERRED + elif task in (FlightType.DEAD, FlightType.SEAD): + return SEAD_PREFERRED + elif task == FlightType.STRIKE: + return STRIKE_PREFERRED + elif task == FlightType.ESCORT: + return CAP_PREFERRED + else: + return [] + + @staticmethod + def capable_aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]: + cap_missions = (FlightType.BARCAP, FlightType.CAP, FlightType.TARCAP) + if task in cap_missions: + return CAP_CAPABLE + elif task == FlightType.CAS: + return CAS_CAPABLE + elif task in (FlightType.DEAD, FlightType.SEAD): + return SEAD_CAPABLE + elif task == FlightType.STRIKE: + return STRIKE_CAPABLE + elif task == FlightType.ESCORT: + return CAP_CAPABLE + else: + logging.error(f"Unplannable flight type: {task}") + return [] + + def find_aircraft_of_type( + self, flight: ProposedFlight, types: List[Type[FlyingType]], + ) -> Optional[Tuple[ControlPoint, UnitType]]: airfields_in_range = self.closest_airfields.airfields_within( flight.max_distance ) @@ -312,7 +349,8 @@ class ObjectiveFinder: yield from cp.ground_objects yield from self.front_lines() - def closest_airfields_to(self, location: MissionTarget) -> ClosestAirfields: + @staticmethod + def closest_airfields_to(location: MissionTarget) -> ClosestAirfields: """Returns the closest airfields to the given location.""" return ObjectiveDistanceCache.get_closest_airfields(location) diff --git a/gen/flights/ai_flight_planner_db.py b/gen/flights/ai_flight_planner_db.py index dd51fd26..715a7f66 100644 --- a/gen/flights/ai_flight_planner_db.py +++ b/gen/flights/ai_flight_planner_db.py @@ -80,6 +80,10 @@ from pydcs_extensions.a4ec.a4ec import A_4E_C from pydcs_extensions.mb339.mb339 import MB_339PAN from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_M +# TODO: These lists really ought to be era (faction) dependent. +# Factions which have F-5s, F-86s, and A-4s will should prefer F-5s for CAP, but +# factions that also have F-4s should not. + INTERCEPT_CAPABLE = [ MiG_21Bis, MiG_25PD, @@ -150,6 +154,42 @@ CAP_CAPABLE = [ Rafale_M, ] +CAP_PREFERRED = [ + MiG_15bis, + MiG_19P, + MiG_21Bis, + MiG_23MLD, + MiG_25PD, + MiG_29A, + MiG_29G, + MiG_29S, + MiG_31, + + Su_27, + J_11A, + Su_30, + Su_33, + + M_2000C, + Mirage_2000_5, + + F_86F_Sabre, + F_14B, + F_15C, + + P_51D_30_NA, + P_51D, + + SpitfireLFMkIXCW, + SpitfireLFMkIX, + + Bf_109K_4, + FW_190D9, + FW_190A8, + + Rafale_M, +] + # Used for CAS (Close air support) and BAI (Battlefield Interdiction) CAS_CAPABLE = [ @@ -228,6 +268,59 @@ CAS_CAPABLE = [ RQ_1A_Predator ] +CAS_PREFERRED = [ + Su_17M4, + Su_24M, + Su_24MR, + Su_25, + Su_25T, + Su_25TM, + Su_34, + + JF_17, + + A_10A, + A_10C, + A_10C_2, + AV8BNA, + + F_15E, + + Tornado_GR4, + + C_101CC, + MB_339PAN, + L_39ZA, + AJS37, + + SA342M, + SA342L, + OH_58D, + + AH_64A, + AH_64D, + AH_1W, + + UH_1H, + + Mi_8MT, + Mi_28N, + Mi_24V, + Ka_50, + + P_47D_30, + P_47D_30bl1, + P_47D_40, + A_20G, + + A_4E_C, + Rafale_A_S, + + WingLoong_I, + MQ_9_Reaper, + RQ_1A_Predator +] + # Aircraft used for SEAD / DEAD tasks SEAD_CAPABLE = [ F_4E, @@ -252,6 +345,12 @@ SEAD_CAPABLE = [ Rafale_A_S ] +SEAD_PREFERRED = [ + F_4E, + Su_25T, + Tornado_IDS, +] + # Aircraft used for Strike mission STRIKE_CAPABLE = [ MiG_15bis, @@ -309,6 +408,15 @@ STRIKE_CAPABLE = [ ] +STRIKE_PREFERRED = [ + AJS37, + F_15E, + Tornado_GR4, + + A_20G, + B_17G, +] + ANTISHIP_CAPABLE = [ Su_24M, Su_17M4,