mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
This is an attempt to remove a lot of our supposedly unnecessary error handling. Every aircraft should have a price, a description, a name, etc; and none of those should require carrying around the faction's country as context. This moves all the data for aircraft into yaml files (only one converted here as an example). Most of the "extended unit info" isn't actually being read yet. To replace the renaming of units based on the county, we instead generate multiple types of each unit when necessary. The CF-18 is just as much a first-class type as the F/A-18 is. This doesn't work in its current state because it does break all the existing names for aircraft that are used in the faction and squadron files, and we no longer let those errors go as a warning. It will be an annoying one time switch, but it allows us to define the names that get used in these files instead of being sensitive to changes as they happen in pydcs, and allows faction designers to specifically choose, for example, the Su-22 instead of the Su-17. One thing not handled by this is aircraft task capability. This is because the lists in ai_flight_planner_db.py are a priority list, and to move it out to a yaml file we'd need to assign a weight to it that would be used to stack rank each aircraft. That's doable, but it makes it much more difficult to see the ordering of aircraft at a glance, and much more annoying to move aircraft around in the priority list. I don't think this is worth doing, and the priority lists will remain in their own separate lists. This includes the converted I used to convert all the old unit info and factions to the new format. This doesn't need to live long, but we may want to reuse it in the future so we want it in the version history.
335 lines
11 KiB
Python
335 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import timedelta
|
|
from enum import Enum
|
|
from typing import List, Optional, TYPE_CHECKING, Union
|
|
|
|
from dcs.mapping import Point
|
|
from dcs.point import MovingPoint, PointAction
|
|
from dcs.unit import Unit
|
|
|
|
from game import db
|
|
from game.dcs.aircrafttype import AircraftType
|
|
from game.squadrons import Pilot, Squadron
|
|
from game.theater.controlpoint import ControlPoint, MissionTarget
|
|
from game.utils import Distance, meters
|
|
from gen.flights.loadouts import Loadout
|
|
|
|
if TYPE_CHECKING:
|
|
from game.transfers import TransferOrder
|
|
from gen.ato import Package
|
|
from gen.flights.flightplan import FlightPlan
|
|
|
|
|
|
class FlightType(Enum):
|
|
"""Enumeration of mission types.
|
|
|
|
The value of each enumeration is the name that will be shown in the UI.
|
|
|
|
These values are persisted to the save game as well since they are a part of
|
|
each flight and thus a part of the ATO, so changing these values will break
|
|
save compat.
|
|
|
|
When adding new mission types to this list, you will also need to update:
|
|
|
|
* flightplan.py: Add waypoint population in generate_flight_plan. Add a new flight
|
|
plan type if necessary, though most are a subclass of StrikeFlightPlan.
|
|
* aircraft.py: Add a configuration method and call it in setup_flight_group. This is
|
|
responsible for configuring waypoint 0 actions like setting ROE, threat reaction,
|
|
and mission abort parameters (winchester, bingo, etc).
|
|
* Implementations of MissionTarget.mission_types: A mission type can only be planned
|
|
against compatible targets. The mission_types method of each target class defines
|
|
which missions may target it.
|
|
* ai_flight_planner_db.py: Add the new mission type to aircraft_for_task that
|
|
returns the list of compatible aircraft in order of preference.
|
|
|
|
You may also need to update:
|
|
|
|
* flight.py: Add a new waypoint type if necessary. Most mission types will need
|
|
these, as aircraft.py uses the ingress point type to specialize AI tasks, and non-
|
|
strike-like missions will need more specialized control.
|
|
* ai_flight_planner.py: Use the new mission type in propose_missions so the AI will
|
|
plan the new mission type.
|
|
"""
|
|
|
|
TARCAP = "TARCAP"
|
|
BARCAP = "BARCAP"
|
|
CAS = "CAS"
|
|
INTERCEPTION = "Intercept"
|
|
STRIKE = "Strike"
|
|
ANTISHIP = "Anti-ship"
|
|
SEAD = "SEAD"
|
|
DEAD = "DEAD"
|
|
ESCORT = "Escort"
|
|
BAI = "BAI"
|
|
SWEEP = "Fighter sweep"
|
|
OCA_RUNWAY = "OCA/Runway"
|
|
OCA_AIRCRAFT = "OCA/Aircraft"
|
|
AEWC = "AEW&C"
|
|
TRANSPORT = "Transport"
|
|
SEAD_ESCORT = "SEAD Escort"
|
|
REFUELING = "Refueling"
|
|
|
|
def __str__(self) -> str:
|
|
return self.value
|
|
|
|
@classmethod
|
|
def from_name(cls, name: str) -> FlightType:
|
|
for entry in cls:
|
|
if name == entry.value:
|
|
return entry
|
|
raise KeyError(f"No FlightType with name {name}")
|
|
|
|
|
|
class FlightWaypointType(Enum):
|
|
"""Enumeration of waypoint types.
|
|
|
|
The value of the enum has no meaning but should remain stable to prevent breaking
|
|
save game compatibility.
|
|
|
|
When adding a new waypoint type, you will also need to update:
|
|
|
|
* waypointbuilder.py: Add a builder to simplify construction of the new waypoint
|
|
type unless the new waypoint type will be a parameter to an existing builder
|
|
method (such as how escort ingress waypoints work).
|
|
* aircraft.py: Associate AI actions with the new waypoint type by subclassing
|
|
PydcsWaypointBuilder and using it in PydcsWaypointBuilder.for_waypoint.
|
|
"""
|
|
|
|
TAKEOFF = 0 # Take off point
|
|
ASCEND_POINT = 1 # Ascension point after take off
|
|
PATROL = 2 # Patrol point
|
|
PATROL_TRACK = 3 # Patrol race track
|
|
NAV = 4 # Nav point
|
|
INGRESS_STRIKE = 5 # Ingress strike (For generator, means that this should have bombing on next TARGET_POINT points)
|
|
INGRESS_SEAD = 6 # Ingress sead (For generator, means that this should attack groups on TARGET_GROUP_LOC points)
|
|
INGRESS_CAS = 7 # Ingress cas (should start CAS task)
|
|
CAS = 8 # Should do CAS there
|
|
EGRESS = 9 # Should stop attack
|
|
DESCENT_POINT = 10 # Should start descending to pattern alt
|
|
LANDING_POINT = 11 # Should land there
|
|
TARGET_POINT = 12 # A target building or static object, position
|
|
TARGET_GROUP_LOC = 13 # A target group approximate location
|
|
TARGET_SHIP = 14 # Unused.
|
|
CUSTOM = 15 # User waypoint (no specific behaviour)
|
|
JOIN = 16
|
|
SPLIT = 17
|
|
LOITER = 18
|
|
INGRESS_ESCORT = 19
|
|
INGRESS_DEAD = 20
|
|
INGRESS_SWEEP = 21
|
|
INGRESS_BAI = 22
|
|
DIVERT = 23
|
|
INGRESS_OCA_RUNWAY = 24
|
|
INGRESS_OCA_AIRCRAFT = 25
|
|
PICKUP = 26
|
|
DROP_OFF = 27
|
|
BULLSEYE = 28
|
|
|
|
|
|
class FlightWaypoint:
|
|
def __init__(
|
|
self,
|
|
waypoint_type: FlightWaypointType,
|
|
x: float,
|
|
y: float,
|
|
alt: Distance = meters(0),
|
|
) -> None:
|
|
"""Creates a flight waypoint.
|
|
|
|
Args:
|
|
waypoint_type: The waypoint type.
|
|
x: X cooidinate of the waypoint.
|
|
y: Y coordinate of the waypoint.
|
|
alt: Altitude of the waypoint. By default this is AGL, but it can be
|
|
changed to MSL by setting alt_type to "RADIO".
|
|
"""
|
|
self.waypoint_type = waypoint_type
|
|
self.x = x
|
|
self.y = y
|
|
self.alt = alt
|
|
self.alt_type = "BARO"
|
|
self.name = ""
|
|
# TODO: Merge with pretty_name.
|
|
# Only used in the waypoint list in the flight edit page. No sense
|
|
# having three names. A short and long form is enough.
|
|
self.description = ""
|
|
self.targets: List[Union[MissionTarget, Unit]] = []
|
|
self.obj_name = ""
|
|
self.pretty_name = ""
|
|
self.only_for_player = False
|
|
self.flyover = False
|
|
|
|
# These are set very late by the air conflict generator (part of mission
|
|
# generation). We do it late so that we don't need to propagate changes
|
|
# to waypoint times whenever the player alters the package TOT or the
|
|
# flight's offset in the UI.
|
|
self.tot: Optional[timedelta] = None
|
|
self.departure_time: Optional[timedelta] = None
|
|
|
|
@property
|
|
def position(self) -> Point:
|
|
return Point(self.x, self.y)
|
|
|
|
@classmethod
|
|
def from_pydcs(cls, point: MovingPoint, from_cp: ControlPoint) -> "FlightWaypoint":
|
|
waypoint = FlightWaypoint(
|
|
FlightWaypointType.NAV,
|
|
point.position.x,
|
|
point.position.y,
|
|
meters(point.alt),
|
|
)
|
|
waypoint.alt_type = point.alt_type
|
|
# Other actions exist... but none of them *should* be the first
|
|
# waypoint for a flight.
|
|
waypoint.waypoint_type = {
|
|
PointAction.TurningPoint: FlightWaypointType.NAV,
|
|
PointAction.FlyOverPoint: FlightWaypointType.NAV,
|
|
PointAction.FromParkingArea: FlightWaypointType.TAKEOFF,
|
|
PointAction.FromParkingAreaHot: FlightWaypointType.TAKEOFF,
|
|
PointAction.FromRunway: FlightWaypointType.TAKEOFF,
|
|
}[point.action]
|
|
if waypoint.waypoint_type == FlightWaypointType.NAV:
|
|
waypoint.name = "NAV"
|
|
waypoint.pretty_name = "Nav"
|
|
waypoint.description = "Nav"
|
|
else:
|
|
waypoint.name = "TAKEOFF"
|
|
waypoint.pretty_name = "Takeoff"
|
|
waypoint.description = "Takeoff"
|
|
waypoint.description = f"Takeoff from {from_cp.name}"
|
|
return waypoint
|
|
|
|
|
|
class FlightRoster:
|
|
def __init__(self, squadron: Squadron, initial_size: int = 0) -> None:
|
|
self.squadron = squadron
|
|
self.pilots: list[Optional[Pilot]] = []
|
|
self.resize(initial_size)
|
|
|
|
@property
|
|
def max_size(self) -> int:
|
|
return len(self.pilots)
|
|
|
|
@property
|
|
def player_count(self) -> int:
|
|
return len([p for p in self.pilots if p is not None and p.player])
|
|
|
|
@property
|
|
def missing_pilots(self) -> int:
|
|
return len([p for p in self.pilots if p is None])
|
|
|
|
def resize(self, new_size: int) -> None:
|
|
if self.max_size > new_size:
|
|
self.squadron.return_pilots(
|
|
[p for p in self.pilots[new_size:] if p is not None]
|
|
)
|
|
self.pilots = self.pilots[:new_size]
|
|
return
|
|
self.pilots.extend(
|
|
[
|
|
self.squadron.claim_available_pilot()
|
|
for _ in range(new_size - self.max_size)
|
|
]
|
|
)
|
|
|
|
def set_pilot(self, index: int, pilot: Optional[Pilot]) -> None:
|
|
if pilot is not None:
|
|
self.squadron.claim_pilot(pilot)
|
|
if (current_pilot := self.pilots[index]) is not None:
|
|
self.squadron.return_pilot(current_pilot)
|
|
self.pilots[index] = pilot
|
|
|
|
def clear(self) -> None:
|
|
self.squadron.return_pilots([p for p in self.pilots if p is not None])
|
|
|
|
|
|
class Flight:
|
|
def __init__(
|
|
self,
|
|
package: Package,
|
|
country: str,
|
|
squadron: Squadron,
|
|
count: int,
|
|
flight_type: FlightType,
|
|
start_type: str,
|
|
departure: ControlPoint,
|
|
arrival: ControlPoint,
|
|
divert: Optional[ControlPoint],
|
|
custom_name: Optional[str] = None,
|
|
cargo: Optional[TransferOrder] = None,
|
|
roster: Optional[FlightRoster] = None,
|
|
) -> None:
|
|
self.package = package
|
|
self.country = country
|
|
self.squadron = squadron
|
|
if roster is None:
|
|
self.roster = FlightRoster(self.squadron, initial_size=count)
|
|
else:
|
|
self.roster = roster
|
|
self.departure = departure
|
|
self.arrival = arrival
|
|
self.divert = divert
|
|
self.flight_type = flight_type
|
|
# TODO: Replace with FlightPlan.
|
|
self.targets: List[MissionTarget] = []
|
|
self.loadout = Loadout.default_for(self)
|
|
self.start_type = start_type
|
|
self.use_custom_loadout = False
|
|
self.custom_name = custom_name
|
|
|
|
# Only used by transport missions.
|
|
self.cargo = cargo
|
|
|
|
# Will be replaced with a more appropriate FlightPlan by
|
|
# FlightPlanBuilder, but an empty flight plan the flight begins with an
|
|
# empty flight plan.
|
|
from gen.flights.flightplan import CustomFlightPlan
|
|
|
|
self.flight_plan: FlightPlan = CustomFlightPlan(
|
|
package=package, flight=self, custom_waypoints=[]
|
|
)
|
|
|
|
@property
|
|
def count(self) -> int:
|
|
return self.roster.max_size
|
|
|
|
@property
|
|
def client_count(self) -> int:
|
|
return self.roster.player_count
|
|
|
|
@property
|
|
def unit_type(self) -> AircraftType:
|
|
return self.squadron.aircraft
|
|
|
|
@property
|
|
def from_cp(self) -> ControlPoint:
|
|
return self.departure
|
|
|
|
@property
|
|
def points(self) -> List[FlightWaypoint]:
|
|
return self.flight_plan.waypoints[1:]
|
|
|
|
def resize(self, new_size: int) -> None:
|
|
self.roster.resize(new_size)
|
|
|
|
def set_pilot(self, index: int, pilot: Optional[Pilot]) -> None:
|
|
self.roster.set_pilot(index, pilot)
|
|
|
|
@property
|
|
def missing_pilots(self) -> int:
|
|
return self.roster.missing_pilots
|
|
|
|
def clear_roster(self) -> None:
|
|
self.roster.clear()
|
|
|
|
def __repr__(self):
|
|
if self.custom_name:
|
|
return f"{self.custom_name} {self.count} x {self.unit_type}"
|
|
return f"[{self.flight_type}] {self.count} x {self.unit_type}"
|
|
|
|
def __str__(self):
|
|
if self.custom_name:
|
|
return f"{self.custom_name} {self.count} x {self.unit_type}"
|
|
return f"[{self.flight_type}] {self.count} x {self.unit_type}"
|