mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Merge 'upstream/develop' into new-plugin-system
This commit is contained in:
@@ -131,7 +131,7 @@ class AircraftAllocator:
|
||||
|
||||
@staticmethod
|
||||
def preferred_aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
|
||||
cap_missions = (FlightType.BARCAP, FlightType.CAP, FlightType.TARCAP)
|
||||
cap_missions = (FlightType.BARCAP, FlightType.TARCAP)
|
||||
if task in cap_missions:
|
||||
return CAP_PREFERRED
|
||||
elif task == FlightType.CAS:
|
||||
@@ -147,7 +147,7 @@ class AircraftAllocator:
|
||||
|
||||
@staticmethod
|
||||
def capable_aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
|
||||
cap_missions = (FlightType.BARCAP, FlightType.CAP, FlightType.TARCAP)
|
||||
cap_missions = (FlightType.BARCAP, FlightType.TARCAP)
|
||||
if task in cap_missions:
|
||||
return CAP_CAPABLE
|
||||
elif task == FlightType.CAS:
|
||||
@@ -404,7 +404,7 @@ class CoalitionMissionPlanner:
|
||||
# Find friendly CPs within 100 nmi from an enemy airfield, plan CAP.
|
||||
for cp in self.objective_finder.vulnerable_control_points():
|
||||
yield ProposedMission(cp, [
|
||||
ProposedFlight(FlightType.CAP, 2, self.MAX_CAP_RANGE),
|
||||
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
|
||||
])
|
||||
|
||||
# Find front lines, plan CAP.
|
||||
@@ -493,11 +493,7 @@ class CoalitionMissionPlanner:
|
||||
error = random.randint(-margin, margin)
|
||||
yield max(0, time + error)
|
||||
|
||||
dca_types = (
|
||||
FlightType.BARCAP,
|
||||
FlightType.CAP,
|
||||
FlightType.INTERCEPTION,
|
||||
)
|
||||
dca_types = (FlightType.BARCAP, FlightType.INTERCEPTION)
|
||||
|
||||
non_dca_packages = [p for p in self.ato.packages if
|
||||
p.primary_task not in dca_types]
|
||||
|
||||
@@ -326,7 +326,7 @@ SEAD_CAPABLE = [
|
||||
F_4E,
|
||||
FA_18C_hornet,
|
||||
F_15E,
|
||||
# F_16C_50, Not yet
|
||||
F_16C_50,
|
||||
AV8BNA,
|
||||
JF_17,
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from enum import Enum
|
||||
from typing import Dict, Optional
|
||||
from typing import Dict, Iterable, Optional
|
||||
|
||||
from game import db
|
||||
from dcs.unittype import UnitType
|
||||
@@ -8,7 +8,7 @@ from theater.controlpoint import ControlPoint, MissionTarget
|
||||
|
||||
|
||||
class FlightType(Enum):
|
||||
CAP = 0
|
||||
CAP = 0 # Do not use. Use BARCAP or TARCAP.
|
||||
TARCAP = 1
|
||||
BARCAP = 2
|
||||
CAS = 3
|
||||
@@ -152,6 +152,14 @@ class Flight:
|
||||
return self.flight_type.name + " | " + str(self.count) + "x" + db.unit_type_name(self.unit_type) \
|
||||
+ " (" + str(len(self.points)) + " wpt)"
|
||||
|
||||
def waypoint_with_type(
|
||||
self,
|
||||
types: Iterable[FlightWaypointType]) -> Optional[FlightWaypoint]:
|
||||
for waypoint in self.points:
|
||||
if waypoint.waypoint_type in types:
|
||||
return waypoint
|
||||
return None
|
||||
|
||||
|
||||
# Test
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -69,8 +69,6 @@ class FlightPlanBuilder:
|
||||
logging.error("BAI flight plan generation not implemented")
|
||||
elif task == FlightType.BARCAP:
|
||||
self.generate_barcap(flight)
|
||||
elif task == FlightType.CAP:
|
||||
self.generate_barcap(flight)
|
||||
elif task == FlightType.CAS:
|
||||
self.generate_cas(flight)
|
||||
elif task == FlightType.DEAD:
|
||||
@@ -103,8 +101,10 @@ class FlightPlanBuilder:
|
||||
logging.error(
|
||||
"Troop transport flight plan generation not implemented"
|
||||
)
|
||||
except InvalidObjectiveLocation as ex:
|
||||
logging.error(f"Could not create flight plan: {ex}")
|
||||
else:
|
||||
logging.error(f"Unsupported task type: {task.name}")
|
||||
except InvalidObjectiveLocation:
|
||||
logging.exception(f"Could not create flight plan")
|
||||
|
||||
def regenerate_package_waypoints(self) -> None:
|
||||
ingress_point = self._ingress_point()
|
||||
@@ -424,7 +424,6 @@ class FlightPlanBuilder:
|
||||
def _heading_to_package_airfield(self, point: Point) -> int:
|
||||
return self.package_airfield().position.heading_between_point(point)
|
||||
|
||||
# TODO: Set ingress/egress/join/split points in the Package.
|
||||
def package_airfield(self) -> ControlPoint:
|
||||
# We'll always have a package, but if this is being planned via the UI
|
||||
# it could be the first flight in the package.
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterable, Optional
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game.utils import meter_to_nm
|
||||
from gen.ato import Package
|
||||
@@ -17,25 +19,101 @@ from gen.flights.flight import (
|
||||
|
||||
|
||||
CAP_DURATION = 30 # Minutes
|
||||
CAP_TYPES = (FlightType.BARCAP, FlightType.CAP)
|
||||
|
||||
INGRESS_TYPES = {
|
||||
FlightWaypointType.INGRESS_CAS,
|
||||
FlightWaypointType.INGRESS_SEAD,
|
||||
FlightWaypointType.INGRESS_STRIKE,
|
||||
}
|
||||
|
||||
IP_TYPES = {
|
||||
FlightWaypointType.INGRESS_CAS,
|
||||
FlightWaypointType.INGRESS_SEAD,
|
||||
FlightWaypointType.INGRESS_STRIKE,
|
||||
FlightWaypointType.PATROL_TRACK,
|
||||
}
|
||||
|
||||
|
||||
class GroundSpeed:
|
||||
@classmethod
|
||||
def for_package(cls, package: Package) -> int:
|
||||
speeds = []
|
||||
for flight in package.flights:
|
||||
speeds.append(cls.for_flight(flight))
|
||||
return min(speeds) # knots
|
||||
|
||||
@staticmethod
|
||||
def for_flight(_flight: Flight) -> int:
|
||||
# TODO: Gather data so this is useful.
|
||||
def mission_speed(package: Package) -> int:
|
||||
speeds = set()
|
||||
for flight in package.flights:
|
||||
waypoint = flight.waypoint_with_type(IP_TYPES)
|
||||
if waypoint is None:
|
||||
logging.error(f"Could not find ingress point for {flight}.")
|
||||
if flight.points:
|
||||
logging.warning(
|
||||
"Using first waypoint for mission altitude.")
|
||||
waypoint = flight.points[0]
|
||||
else:
|
||||
logging.warning(
|
||||
"Flight has no waypoints. Assuming mission altitude "
|
||||
"of 25000 feet.")
|
||||
waypoint = FlightWaypoint(FlightWaypointType.NAV, 0, 0,
|
||||
25000)
|
||||
speeds.add(GroundSpeed.for_flight(flight, waypoint.alt))
|
||||
return min(speeds)
|
||||
|
||||
@classmethod
|
||||
def for_flight(cls, flight: Flight, altitude: int) -> int:
|
||||
if not issubclass(flight.unit_type, FlyingType):
|
||||
raise TypeError("Flight has non-flying unit")
|
||||
|
||||
# TODO: Expose both a cruise speed and target speed.
|
||||
# The cruise speed can be used for ascent, hold, join, and RTB to save
|
||||
# on fuel, but mission speed will be fast enough to keep the flight
|
||||
# safer.
|
||||
return 400 # knots
|
||||
|
||||
c_sound_sea_level = 661.5
|
||||
|
||||
# DCS's max speed is in kph at 0 MSL. Convert to knots.
|
||||
max_speed = flight.unit_type.max_speed * 0.539957
|
||||
if max_speed > c_sound_sea_level:
|
||||
# Aircraft is supersonic. Limit to mach 0.8 to conserve fuel and
|
||||
# account for heavily loaded jets.
|
||||
return int(cls.from_mach(0.8, altitude))
|
||||
|
||||
# For subsonic aircraft, assume the aircraft can reasonably perform at
|
||||
# 80% of its maximum, and that it can maintain the same mach at altitude
|
||||
# as it can at sea level. This probably isn't great assumption, but
|
||||
# might. be sufficient given the wiggle room. We can come up with
|
||||
# another heuristic if needed.
|
||||
mach = max_speed * 0.8 / c_sound_sea_level
|
||||
return int(cls.from_mach(mach, altitude)) # knots
|
||||
|
||||
@staticmethod
|
||||
def from_mach(mach: float, altitude: int) -> float:
|
||||
"""Returns the ground speed in knots for the given mach and altitude.
|
||||
|
||||
Args:
|
||||
mach: The mach number to convert to ground speed.
|
||||
altitude: The altitude in feet.
|
||||
|
||||
Returns:
|
||||
The ground speed corresponding to the given altitude and mach number
|
||||
in knots.
|
||||
"""
|
||||
# https://www.grc.nasa.gov/WWW/K-12/airplane/atmos.html
|
||||
if altitude <= 36152:
|
||||
temperature_f = 59 - 0.00356 * altitude
|
||||
else:
|
||||
# There's another formula for altitudes over 82k feet, but we better
|
||||
# not be planning waypoints that high...
|
||||
temperature_f = -70
|
||||
|
||||
temperature_k = (temperature_f + 459.67) * (5 / 9)
|
||||
|
||||
# https://www.engineeringtoolbox.com/specific-heat-ratio-d_602.html
|
||||
# Dependent on temperature, but varies very little (+/-0.001)
|
||||
# between -40F and 180F.
|
||||
heat_capacity_ratio = 1.4
|
||||
|
||||
# https://www.grc.nasa.gov/WWW/K-12/airplane/sound.html
|
||||
gas_constant = 286 # m^2/s^2/K
|
||||
c_sound = math.sqrt(heat_capacity_ratio * gas_constant * temperature_k)
|
||||
# c_sound is in m/s, convert to knots.
|
||||
return (c_sound * 1.944) * mach
|
||||
|
||||
|
||||
class TravelTime:
|
||||
@@ -72,7 +150,7 @@ class TotEstimator:
|
||||
# Takeoff immediately.
|
||||
return 0
|
||||
|
||||
if self.package.primary_task in CAP_TYPES:
|
||||
if self.package.primary_task == FlightType.BARCAP:
|
||||
start_time = self.timing.race_track_start
|
||||
else:
|
||||
start_time = self.timing.join
|
||||
@@ -97,13 +175,7 @@ class TotEstimator:
|
||||
The earliest possible TOT for the given flight in seconds. Returns 0
|
||||
if an ingress point cannot be found.
|
||||
"""
|
||||
stop_types = {
|
||||
FlightWaypointType.PATROL_TRACK,
|
||||
FlightWaypointType.INGRESS_CAS,
|
||||
FlightWaypointType.INGRESS_SEAD,
|
||||
FlightWaypointType.INGRESS_STRIKE,
|
||||
}
|
||||
time_to_ingress = self.estimate_waypoints_to_target(flight, stop_types)
|
||||
time_to_ingress = self.estimate_waypoints_to_target(flight, IP_TYPES)
|
||||
if time_to_ingress is None:
|
||||
logging.warning(
|
||||
f"Found no ingress types. Cannot estimate TOT for {flight}")
|
||||
@@ -111,7 +183,7 @@ class TotEstimator:
|
||||
# the package.
|
||||
return 0
|
||||
|
||||
if self.package.primary_task in CAP_TYPES:
|
||||
if self.package.primary_task == FlightType.BARCAP:
|
||||
# The racetrack start *is* the target. The package target is the
|
||||
# protected objective.
|
||||
time_to_target = 0
|
||||
@@ -119,7 +191,7 @@ class TotEstimator:
|
||||
assert self.package.waypoints is not None
|
||||
time_to_target = TravelTime.between_points(
|
||||
self.package.waypoints.ingress, self.package.target.position,
|
||||
GroundSpeed.for_package(self.package))
|
||||
GroundSpeed.mission_speed(self.package))
|
||||
return sum([
|
||||
self.estimate_startup(flight),
|
||||
self.estimate_ground_ops(flight),
|
||||
@@ -146,30 +218,38 @@ class TotEstimator:
|
||||
self, flight: Flight,
|
||||
stop_types: Iterable[FlightWaypointType]) -> Optional[int]:
|
||||
total = 0
|
||||
# TODO: This is AGL. We want MSL.
|
||||
previous_altitude = 0
|
||||
previous_position = flight.from_cp.position
|
||||
for waypoint in flight.points:
|
||||
position = Point(waypoint.x, waypoint.y)
|
||||
total += TravelTime.between_points(
|
||||
previous_position, position,
|
||||
self.speed_to_waypoint(flight, waypoint)
|
||||
self.speed_to_waypoint(flight, waypoint, previous_altitude)
|
||||
)
|
||||
previous_position = position
|
||||
previous_altitude = waypoint.alt
|
||||
if waypoint.waypoint_type in stop_types:
|
||||
return total
|
||||
|
||||
return None
|
||||
|
||||
def speed_to_waypoint(self, flight: Flight,
|
||||
waypoint: FlightWaypoint) -> int:
|
||||
def speed_to_waypoint(self, flight: Flight, waypoint: FlightWaypoint,
|
||||
from_altitude: int) -> int:
|
||||
# TODO: Adjust if AGL.
|
||||
# We don't have an exact heightmap, but we should probably be performing
|
||||
# *some* adjustment for NTTR since the minimum altitude of the map is
|
||||
# near 2000 ft MSL.
|
||||
alt_for_speed = min(from_altitude, waypoint.alt)
|
||||
pre_join = (FlightWaypointType.LOITER, FlightWaypointType.JOIN)
|
||||
if waypoint.waypoint_type == FlightWaypointType.ASCEND_POINT:
|
||||
# Flights that start airborne already have some altitude and a good
|
||||
# amount of speed.
|
||||
factor = 1.0 if flight.start_type == "In Flight" else 0.5
|
||||
return int(GroundSpeed.for_flight(flight) * factor)
|
||||
return int(GroundSpeed.for_flight(flight, alt_for_speed) * factor)
|
||||
elif waypoint.waypoint_type in pre_join:
|
||||
return GroundSpeed.for_flight(flight)
|
||||
return GroundSpeed.for_package(self.package)
|
||||
return GroundSpeed.for_flight(flight, alt_for_speed)
|
||||
return GroundSpeed.mission_speed(self.package)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -197,24 +277,24 @@ class PackageWaypointTiming:
|
||||
|
||||
@property
|
||||
def race_track_start(self) -> int:
|
||||
if self.package.primary_task in CAP_TYPES:
|
||||
if self.package.primary_task == FlightType.BARCAP:
|
||||
return self.package.time_over_target
|
||||
else:
|
||||
return self.ingress
|
||||
|
||||
@property
|
||||
def race_track_end(self) -> int:
|
||||
if self.package.primary_task in CAP_TYPES:
|
||||
if self.package.primary_task == FlightType.BARCAP:
|
||||
return self.target + CAP_DURATION * 60
|
||||
else:
|
||||
return self.egress
|
||||
|
||||
def push_time(self, flight: Flight, hold_point: Point) -> int:
|
||||
def push_time(self, flight: Flight, hold_point: FlightWaypoint) -> int:
|
||||
assert self.package.waypoints is not None
|
||||
return self.join - TravelTime.between_points(
|
||||
hold_point,
|
||||
Point(hold_point.x, hold_point.y),
|
||||
self.package.waypoints.join,
|
||||
GroundSpeed.for_flight(flight)
|
||||
GroundSpeed.for_flight(flight, hold_point.alt)
|
||||
)
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[int]:
|
||||
@@ -224,15 +304,9 @@ class PackageWaypointTiming:
|
||||
FlightWaypointType.TARGET_SHIP,
|
||||
)
|
||||
|
||||
ingress_types = (
|
||||
FlightWaypointType.INGRESS_CAS,
|
||||
FlightWaypointType.INGRESS_SEAD,
|
||||
FlightWaypointType.INGRESS_STRIKE,
|
||||
)
|
||||
|
||||
if waypoint.waypoint_type == FlightWaypointType.JOIN:
|
||||
return self.join
|
||||
elif waypoint.waypoint_type in ingress_types:
|
||||
elif waypoint.waypoint_type in INGRESS_TYPES:
|
||||
return self.ingress
|
||||
elif waypoint.waypoint_type in target_types:
|
||||
return self.target
|
||||
@@ -247,7 +321,7 @@ class PackageWaypointTiming:
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint,
|
||||
flight: Flight) -> Optional[int]:
|
||||
if waypoint.waypoint_type == FlightWaypointType.LOITER:
|
||||
return self.push_time(flight, Point(waypoint.x, waypoint.y))
|
||||
return self.push_time(flight, waypoint)
|
||||
elif waypoint.waypoint_type == FlightWaypointType.PATROL:
|
||||
return self.race_track_end
|
||||
return None
|
||||
@@ -256,7 +330,17 @@ class PackageWaypointTiming:
|
||||
def for_package(cls, package: Package) -> PackageWaypointTiming:
|
||||
assert package.waypoints is not None
|
||||
|
||||
group_ground_speed = GroundSpeed.for_package(package)
|
||||
# TODO: Plan similar altitudes for the in-country leg of the mission.
|
||||
# Waypoint altitudes for a given flight *shouldn't* differ too much
|
||||
# between the join and split points, so we don't need speeds for each
|
||||
# leg individually since they should all be fairly similar. This doesn't
|
||||
# hold too well right now since nothing is stopping each waypoint from
|
||||
# jumping 20k feet each time, but that's a huge waste of energy we
|
||||
# should be avoiding anyway.
|
||||
if not package.flights:
|
||||
raise ValueError("Cannot plan TOT for package with no flights")
|
||||
|
||||
group_ground_speed = GroundSpeed.mission_speed(package)
|
||||
|
||||
ingress = package.time_over_target - TravelTime.between_points(
|
||||
package.waypoints.ingress,
|
||||
|
||||
@@ -267,12 +267,6 @@ class WaypointBuilder:
|
||||
waypoint.pretty_name = "Race-track start"
|
||||
self.waypoints.append(waypoint)
|
||||
|
||||
# TODO: Does this actually do anything?
|
||||
# orbit0.targets.append(location)
|
||||
# Note: Targets of PATROL TRACK waypoints are the points to be defended.
|
||||
# orbit0.targets.append(flight.from_cp)
|
||||
# orbit0.targets.append(center)
|
||||
|
||||
def race_track_end(self, position: Point, altitude: int) -> None:
|
||||
"""Creates a racetrack end waypoint.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user