mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
- Fixes ferry flights causing errors when "Nothing" is not available - Logs a warning when a frontline stance is suddenly no longer available and uses defense stance as fallback which used to be the default. An investigation is still required to determine the cause of this...
378 lines
15 KiB
Python
378 lines
15 KiB
Python
import logging
|
|
from typing import Any, Optional, Type
|
|
|
|
from dcs.task import (
|
|
AWACS,
|
|
AWACSTaskAction,
|
|
AntishipStrike,
|
|
CAP,
|
|
CAS,
|
|
EPLRS,
|
|
Escort,
|
|
FighterSweep,
|
|
GroundAttack,
|
|
Nothing,
|
|
OptROE,
|
|
OptRTBOnBingoFuel,
|
|
OptRTBOnOutOfAmmo,
|
|
OptReactOnThreat,
|
|
OptRestrictJettison,
|
|
Refueling,
|
|
RunwayAttack,
|
|
Transport,
|
|
SEAD,
|
|
SwitchWaypoint,
|
|
OptJettisonEmptyTanks,
|
|
MainTask,
|
|
PinpointStrike,
|
|
)
|
|
from dcs.unitgroup import FlyingGroup
|
|
|
|
from game.ato import Flight, FlightType
|
|
from game.ato.flightplans.aewc import AewcFlightPlan
|
|
from game.ato.flightplans.theaterrefueling import TheaterRefuelingFlightPlan
|
|
from game.ato.flightwaypointtype import FlightWaypointType
|
|
|
|
|
|
class AircraftBehavior:
|
|
def __init__(self, task: FlightType) -> None:
|
|
self.task = task
|
|
|
|
def apply_to(self, flight: Flight, group: FlyingGroup[Any]) -> None:
|
|
if self.task in [
|
|
FlightType.BARCAP,
|
|
FlightType.TARCAP,
|
|
FlightType.INTERCEPTION,
|
|
]:
|
|
self.configure_cap(group, flight)
|
|
elif self.task == FlightType.SWEEP:
|
|
self.configure_sweep(group, flight)
|
|
elif self.task == FlightType.AEWC:
|
|
self.configure_awacs(group, flight)
|
|
elif self.task == FlightType.REFUELING:
|
|
self.configure_refueling(group, flight)
|
|
elif self.task in [FlightType.CAS, FlightType.BAI]:
|
|
self.configure_cas(group, flight)
|
|
elif self.task == FlightType.DEAD:
|
|
self.configure_dead(group, flight)
|
|
elif self.task == FlightType.SEAD:
|
|
self.configure_sead(group, flight)
|
|
elif self.task == FlightType.SEAD_ESCORT:
|
|
self.configure_sead_escort(group, flight)
|
|
elif self.task == FlightType.STRIKE:
|
|
self.configure_strike(group, flight)
|
|
elif self.task == FlightType.ANTISHIP:
|
|
self.configure_anti_ship(group, flight)
|
|
elif self.task == FlightType.ESCORT:
|
|
self.configure_escort(group, flight)
|
|
elif self.task == FlightType.OCA_RUNWAY:
|
|
self.configure_runway_attack(group, flight)
|
|
elif self.task == FlightType.OCA_AIRCRAFT:
|
|
self.configure_oca_strike(group, flight)
|
|
elif self.task in [
|
|
FlightType.TRANSPORT,
|
|
FlightType.AIR_ASSAULT,
|
|
]:
|
|
self.configure_transport(group, flight)
|
|
elif self.task == FlightType.FERRY:
|
|
self.configure_ferry(group, flight)
|
|
else:
|
|
self.configure_unknown_task(group, flight)
|
|
|
|
self.configure_eplrs(group, flight)
|
|
|
|
def configure_behavior(
|
|
self,
|
|
flight: Flight,
|
|
group: FlyingGroup[Any],
|
|
react_on_threat: OptReactOnThreat.Values = OptReactOnThreat.Values.EvadeFire,
|
|
roe: Optional[int] = None,
|
|
rtb_winchester: Optional[OptRTBOnOutOfAmmo.Values] = None,
|
|
restrict_jettison: Optional[bool] = None,
|
|
mission_uses_gun: bool = True,
|
|
rtb_on_bingo: bool = True,
|
|
) -> None:
|
|
group.points[0].tasks.clear()
|
|
group.points[0].tasks.append(OptReactOnThreat(react_on_threat))
|
|
if roe is not None:
|
|
group.points[0].tasks.append(OptROE(roe))
|
|
if restrict_jettison is not None:
|
|
group.points[0].tasks.append(OptRestrictJettison(restrict_jettison))
|
|
if rtb_winchester is not None:
|
|
group.points[0].tasks.append(OptRTBOnOutOfAmmo(rtb_winchester))
|
|
|
|
# Confiscate the bullets of AI missions that do not rely on the gun. There is no
|
|
# "all but gun" RTB winchester option, so air to ground missions with mixed
|
|
# weapon types will insist on using all of their bullets after running out of
|
|
# missiles and bombs. Take away their bullets so they don't strafe a Tor.
|
|
#
|
|
# Exceptions are made for player flights and for airframes where the gun is
|
|
# essential like the A-10 or warbirds.
|
|
if not mission_uses_gun and not self.flight_always_keeps_gun(flight):
|
|
for unit in group.units:
|
|
unit.gun = 0
|
|
|
|
group.points[0].tasks.append(OptRTBOnBingoFuel(rtb_on_bingo))
|
|
group.points[0].tasks.append(OptJettisonEmptyTanks())
|
|
# Do not restrict afterburner.
|
|
# https://forums.eagle.ru/forum/english/digital-combat-simulator/dcs-world-2-5/bugs-and-problems-ai/ai-ad/7121294-ai-stuck-at-high-aoa-after-making-sharp-turn-if-afterburner-is-restricted
|
|
|
|
@staticmethod
|
|
def configure_eplrs(group: FlyingGroup[Any], flight: Flight) -> None:
|
|
if flight.unit_type.eplrs_capable:
|
|
group.points[0].tasks.append(EPLRS(group.id))
|
|
|
|
def configure_cap(self, group: FlyingGroup[Any], flight: Flight) -> None:
|
|
self.configure_task(flight, group, CAP)
|
|
if not flight.unit_type.gunfighter:
|
|
ammo_type = OptRTBOnOutOfAmmo.Values.AAM
|
|
else:
|
|
ammo_type = OptRTBOnOutOfAmmo.Values.Cannon
|
|
|
|
self.configure_behavior(flight, group, rtb_winchester=ammo_type)
|
|
|
|
def configure_sweep(self, group: FlyingGroup[Any], flight: Flight) -> None:
|
|
self.configure_task(flight, group, FighterSweep)
|
|
if not flight.unit_type.gunfighter:
|
|
ammo_type = OptRTBOnOutOfAmmo.Values.AAM
|
|
else:
|
|
ammo_type = OptRTBOnOutOfAmmo.Values.Cannon
|
|
|
|
self.configure_behavior(flight, group, rtb_winchester=ammo_type)
|
|
|
|
def configure_cas(self, group: FlyingGroup[Any], flight: Flight) -> None:
|
|
self.configure_task(flight, group, CAS)
|
|
self.configure_behavior(
|
|
flight,
|
|
group,
|
|
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
|
roe=OptROE.Values.OpenFire,
|
|
rtb_winchester=OptRTBOnOutOfAmmo.Values.Unguided,
|
|
restrict_jettison=True,
|
|
)
|
|
|
|
def configure_dead(self, group: FlyingGroup[Any], flight: Flight) -> None:
|
|
# Only CAS and SEAD are capable of the Attack Group task. SEAD is arguably more
|
|
# appropriate but it has an extremely limited list of capable aircraft, whereas
|
|
# CAS has a much wider selection of units.
|
|
#
|
|
# Note that the only effect that the DCS task type has is in determining which
|
|
# waypoint actions the group may perform.
|
|
|
|
self.configure_task(flight, group, SEAD, CAS)
|
|
self.configure_behavior(
|
|
flight,
|
|
group,
|
|
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
|
roe=OptROE.Values.OpenFire,
|
|
rtb_winchester=OptRTBOnOutOfAmmo.Values.All,
|
|
restrict_jettison=True,
|
|
mission_uses_gun=False,
|
|
)
|
|
|
|
def configure_sead(self, group: FlyingGroup[Any], flight: Flight) -> None:
|
|
# CAS is able to perform all the same tasks as SEAD using a superset of the
|
|
# available aircraft, and F-14s are not able to be SEAD despite having TALDs.
|
|
# https://forums.eagle.ru/topic/272112-cannot-assign-f-14-to-sead/
|
|
|
|
self.configure_task(flight, group, SEAD, CAS)
|
|
self.configure_behavior(
|
|
flight,
|
|
group,
|
|
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
|
roe=OptROE.Values.OpenFire,
|
|
# Guided includes ARMs and TALDs (among other things, but those are the useful
|
|
# weapons for SEAD).
|
|
rtb_winchester=OptRTBOnOutOfAmmo.Values.Guided,
|
|
restrict_jettison=True,
|
|
mission_uses_gun=False,
|
|
)
|
|
|
|
def configure_strike(self, group: FlyingGroup[Any], flight: Flight) -> None:
|
|
self.configure_task(flight, group, GroundAttack, PinpointStrike)
|
|
self.configure_behavior(
|
|
flight,
|
|
group,
|
|
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
|
roe=OptROE.Values.OpenFire,
|
|
restrict_jettison=True,
|
|
mission_uses_gun=False,
|
|
)
|
|
|
|
def configure_anti_ship(self, group: FlyingGroup[Any], flight: Flight) -> None:
|
|
self.configure_task(flight, group, AntishipStrike, CAS)
|
|
self.configure_behavior(
|
|
flight,
|
|
group,
|
|
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
|
roe=OptROE.Values.OpenFire,
|
|
restrict_jettison=True,
|
|
mission_uses_gun=False,
|
|
)
|
|
|
|
def configure_runway_attack(self, group: FlyingGroup[Any], flight: Flight) -> None:
|
|
self.configure_task(flight, group, RunwayAttack)
|
|
self.configure_behavior(
|
|
flight,
|
|
group,
|
|
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
|
roe=OptROE.Values.OpenFire,
|
|
restrict_jettison=True,
|
|
mission_uses_gun=False,
|
|
)
|
|
|
|
def configure_oca_strike(self, group: FlyingGroup[Any], flight: Flight) -> None:
|
|
self.configure_task(flight, group, CAS)
|
|
self.configure_behavior(
|
|
flight,
|
|
group,
|
|
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
|
roe=OptROE.Values.OpenFire,
|
|
restrict_jettison=True,
|
|
)
|
|
|
|
def configure_awacs(self, group: FlyingGroup[Any], flight: Flight) -> None:
|
|
self.configure_task(flight, group, AWACS)
|
|
if not isinstance(flight.flight_plan, AewcFlightPlan):
|
|
logging.error(
|
|
f"Cannot configure AEW&C tasks for {flight} because it does not have "
|
|
"an AEW&C flight plan."
|
|
)
|
|
return
|
|
|
|
# Awacs task action
|
|
self.configure_behavior(
|
|
flight,
|
|
group,
|
|
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
|
roe=OptROE.Values.WeaponHold,
|
|
restrict_jettison=True,
|
|
)
|
|
|
|
group.points[0].tasks.append(AWACSTaskAction())
|
|
|
|
def configure_refueling(self, group: FlyingGroup[Any], flight: Flight) -> None:
|
|
self.configure_task(flight, group, Refueling)
|
|
if not isinstance(flight.flight_plan, TheaterRefuelingFlightPlan):
|
|
logging.error(
|
|
f"Cannot configure racetrack refueling tasks for {flight} because it "
|
|
"does not have an racetrack refueling flight plan."
|
|
)
|
|
return
|
|
|
|
self.configure_behavior(
|
|
flight,
|
|
group,
|
|
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
|
roe=OptROE.Values.WeaponHold,
|
|
restrict_jettison=True,
|
|
)
|
|
|
|
def configure_escort(self, group: FlyingGroup[Any], flight: Flight) -> None:
|
|
# Escort groups are actually given the CAP task so they can perform the
|
|
# Search Then Engage task, which we have to use instead of the Escort
|
|
# task for the reasons explained in JoinPointBuilder.
|
|
self.configure_task(flight, group, Escort)
|
|
if flight.flight_plan.is_formation(flight.flight_plan):
|
|
index = flight.flight_plan.get_index_of_wpt_by_type(
|
|
FlightWaypointType.SPLIT
|
|
)
|
|
if index > 0:
|
|
group.add_trigger_action(SwitchWaypoint(None, index))
|
|
else:
|
|
logging.warning(f"Couldn't determine SPLIT for {group.name}")
|
|
self.configure_behavior(
|
|
flight, group, roe=OptROE.Values.OpenFire, restrict_jettison=True
|
|
)
|
|
|
|
def configure_sead_escort(self, group: FlyingGroup[Any], flight: Flight) -> None:
|
|
# CAS is able to perform all the same tasks as SEAD using a superset of the
|
|
# available aircraft, and F-14s are not able to be SEAD despite having TALDs.
|
|
# https://forums.eagle.ru/topic/272112-cannot-assign-f-14-to-sead/
|
|
self.configure_task(flight, group, SEAD)
|
|
index = flight.flight_plan.get_index_of_wpt_by_type(FlightWaypointType.SPLIT)
|
|
if index > 0 and flight.flight_plan.is_formation(flight.flight_plan):
|
|
group.add_trigger_action(SwitchWaypoint(None, index))
|
|
if index < 1:
|
|
logging.warning(f"Couldn't determine SPLIT for {group.name}")
|
|
self.configure_behavior(
|
|
flight,
|
|
group,
|
|
roe=OptROE.Values.OpenFire,
|
|
# Guided includes ARMs and TALDs (among other things, but those are the useful
|
|
# weapons for SEAD).
|
|
rtb_winchester=OptRTBOnOutOfAmmo.Values.Guided,
|
|
restrict_jettison=True,
|
|
mission_uses_gun=False,
|
|
)
|
|
|
|
def configure_transport(self, group: FlyingGroup[Any], flight: Flight) -> None:
|
|
self.configure_task(flight, group, Transport)
|
|
roe = OptROE.Values.WeaponHold
|
|
if flight.is_hercules:
|
|
group.task = GroundAttack.name
|
|
roe = OptROE.Values.OpenFire
|
|
self.configure_behavior(
|
|
flight,
|
|
group,
|
|
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
|
roe=roe,
|
|
restrict_jettison=True,
|
|
)
|
|
|
|
def configure_ferry(self, group: FlyingGroup[Any], flight: Flight) -> None:
|
|
self.configure_task(flight, group, Nothing)
|
|
self.configure_behavior(
|
|
flight,
|
|
group,
|
|
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
|
roe=OptROE.Values.WeaponHold,
|
|
restrict_jettison=True,
|
|
rtb_on_bingo=False,
|
|
)
|
|
|
|
def configure_unknown_task(self, group: FlyingGroup[Any], flight: Flight) -> None:
|
|
logging.error(f"Unhandled flight type: {flight.flight_type}")
|
|
self.configure_behavior(flight, group)
|
|
|
|
@staticmethod
|
|
def flight_always_keeps_gun(flight: Flight) -> bool:
|
|
# Never take bullets from players. They're smart enough to know when to use it
|
|
# and when to RTB.
|
|
if flight.client_count > 0:
|
|
return True
|
|
|
|
return flight.unit_type.always_keeps_gun
|
|
|
|
@staticmethod
|
|
def configure_task(
|
|
flight: Flight,
|
|
group: FlyingGroup[Any],
|
|
preferred_task: Type[MainTask],
|
|
fallback_task: Optional[Type[MainTask]] = None,
|
|
) -> None:
|
|
ac_type = flight.unit_type.dcs_unit_type.id
|
|
|
|
# Not all aircraft are always compatible with the preferred task,
|
|
# so a common fallback is to use CAS instead.
|
|
# Sometimes it's also the other way around,
|
|
# i.e. the preferred task is available while CAS isn't
|
|
# This method should allow for dynamic choice between tasks,
|
|
# obviously depending on what's preferred and compatible...
|
|
|
|
if preferred_task in flight.unit_type.dcs_unit_type.tasks:
|
|
group.task = preferred_task.name
|
|
elif fallback_task and fallback_task in flight.unit_type.dcs_unit_type.tasks:
|
|
group.task = fallback_task.name
|
|
elif flight.unit_type.dcs_unit_type.task_default and preferred_task == Nothing:
|
|
group.task = flight.unit_type.dcs_unit_type.task_default.name
|
|
logging.warning(
|
|
f"{ac_type} is not capable of 'Nothing', using default task '{group.task}'"
|
|
)
|
|
else:
|
|
fallback_part = f" nor {fallback_task.name}" if fallback_task else ""
|
|
raise RuntimeError(
|
|
f"{ac_type} is neither capable of {preferred_task.name}"
|
|
f"{fallback_part}. Can't generate {flight.flight_type} flight."
|
|
)
|