mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Estimate TOTs for packages.
We estimate the longest possible time from mission start to TOT for all flights in a package and use that to set the TOT (plus any delay used to stagger flights). This both cuts down on loiter time for shorter flights and ensures that long flights will make it to the target in time. This is also used to compute the start time for the AI, so the explicit delay option is no longer needed.
This commit is contained in:
@@ -33,6 +33,7 @@ from gen.flights.flight import (
|
||||
FlightType,
|
||||
)
|
||||
from gen.flights.flightplan import FlightPlanBuilder
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
from theater import (
|
||||
ControlPoint,
|
||||
FrontLine,
|
||||
@@ -185,11 +186,13 @@ class PackageBuilder:
|
||||
def __init__(self, location: MissionTarget,
|
||||
closest_airfields: ClosestAirfields,
|
||||
global_inventory: GlobalAircraftInventory,
|
||||
is_player: bool) -> None:
|
||||
is_player: bool,
|
||||
start_type: str) -> None:
|
||||
self.package = Package(location)
|
||||
self.allocator = AircraftAllocator(closest_airfields, global_inventory,
|
||||
is_player)
|
||||
self.global_inventory = global_inventory
|
||||
self.start_type = start_type
|
||||
|
||||
def plan_flight(self, plan: ProposedFlight) -> bool:
|
||||
"""Allocates aircraft for the given flight and adds them to the package.
|
||||
@@ -203,7 +206,8 @@ class PackageBuilder:
|
||||
if assignment is None:
|
||||
return False
|
||||
airfield, aircraft = assignment
|
||||
flight = Flight(aircraft, plan.num_aircraft, airfield, plan.task)
|
||||
flight = Flight(aircraft, plan.num_aircraft, airfield, plan.task,
|
||||
self.start_type)
|
||||
self.package.add_flight(flight)
|
||||
return True
|
||||
|
||||
@@ -444,11 +448,18 @@ class CoalitionMissionPlanner:
|
||||
|
||||
def plan_mission(self, mission: ProposedMission) -> None:
|
||||
"""Allocates aircraft for a proposed mission and adds it to the ATO."""
|
||||
|
||||
if self.game.settings.perf_ai_parking_start:
|
||||
start_type = "Cold"
|
||||
else:
|
||||
start_type = "Warm"
|
||||
|
||||
builder = PackageBuilder(
|
||||
mission.location,
|
||||
self.objective_finder.closest_airfields_to(mission.location),
|
||||
self.game.aircraft_inventory,
|
||||
self.is_player
|
||||
self.is_player,
|
||||
start_type
|
||||
)
|
||||
|
||||
missing_types: Set[FlightType] = set()
|
||||
@@ -497,16 +508,18 @@ class CoalitionMissionPlanner:
|
||||
margin=5
|
||||
)
|
||||
for package in self.ato.packages:
|
||||
tot = TotEstimator(package).earliest_tot()
|
||||
if package.primary_task in dca_types:
|
||||
# All CAP missions should be on station in 15-25 minutes.
|
||||
package.time_over_target = (20 + random.randint(-5, 5)) * 60
|
||||
# All CAP missions should be on station ASAP.
|
||||
package.time_over_target = tot
|
||||
else:
|
||||
# But other packages should be spread out a bit.
|
||||
package.delay = next(start_time)
|
||||
# TODO: Compute TOT based on package.
|
||||
package.time_over_target = (package.delay + 40) * 60
|
||||
for flight in package.flights:
|
||||
flight.scheduled_in = package.delay
|
||||
# But other packages should be spread out a bit. Note that take
|
||||
# times are delayed, but all aircraft will become active at
|
||||
# mission start. This makes it more worthwhile to attack enemy
|
||||
# airfields to hit grounded aircraft, since they're more likely
|
||||
# to be present. Runway and air started aircraft will be
|
||||
# delayed until their takeoff time by AirConflictGenerator.
|
||||
package.time_over_target = next(start_time) * 60 + tot
|
||||
|
||||
def message(self, title, text) -> None:
|
||||
"""Emits a planning message to the player.
|
||||
|
||||
@@ -97,7 +97,6 @@ class FlightWaypoint:
|
||||
# flight's offset in the UI.
|
||||
self.tot: Optional[int] = None
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_pydcs(cls, point: MovingPoint,
|
||||
from_cp: ControlPoint) -> "FlightWaypoint":
|
||||
@@ -130,13 +129,10 @@ class Flight:
|
||||
client_count: int = 0
|
||||
use_custom_loadout = False
|
||||
preset_loadout_name = ""
|
||||
start_type = "Runway"
|
||||
group = False # Contains DCS Mission group data after mission has been generated
|
||||
|
||||
# How long before this flight should take off
|
||||
scheduled_in = 0
|
||||
|
||||
def __init__(self, unit_type: UnitType, count: int, from_cp: ControlPoint, flight_type: FlightType):
|
||||
def __init__(self, unit_type: UnitType, count: int, from_cp: ControlPoint,
|
||||
flight_type: FlightType, start_type: str) -> None:
|
||||
self.unit_type = unit_type
|
||||
self.count = count
|
||||
self.from_cp = from_cp
|
||||
@@ -144,11 +140,16 @@ class Flight:
|
||||
self.points: List[FlightWaypoint] = []
|
||||
self.targets: List[MissionTarget] = []
|
||||
self.loadout: Dict[str, str] = {}
|
||||
self.start_type = "Runway"
|
||||
self.start_type = start_type
|
||||
# Late activation delay in seconds from mission start. This is not
|
||||
# the same as the flight's takeoff time. Takeoff time depends on the
|
||||
# mission's TOT and the other flights in the package. Takeoff time is
|
||||
# determined by AirConflictGenerator.
|
||||
self.scheduled_in = 0
|
||||
|
||||
def __repr__(self):
|
||||
return self.flight_type.name + " | " + str(self.count) + "x" + db.unit_type_name(self.unit_type) \
|
||||
+ " in " + str(self.scheduled_in) + " minutes (" + str(len(self.points)) + " wpt)"
|
||||
+ " (" + str(len(self.points)) + " wpt)"
|
||||
|
||||
|
||||
# Test
|
||||
@@ -157,6 +158,6 @@ if __name__ == '__main__':
|
||||
from theater import ControlPoint, Point, List
|
||||
|
||||
from_cp = ControlPoint(0, "AA", Point(0, 0), Point(0, 0), [], 0, 0)
|
||||
f = Flight(A_10C(), 4, from_cp, FlightType.CAS)
|
||||
f = Flight(A_10C(), 4, from_cp, FlightType.CAS, "Cold")
|
||||
f.scheduled_in = 50
|
||||
print(f)
|
||||
|
||||
285
gen/flights/traveltime.py
Normal file
285
gen/flights/traveltime.py
Normal file
@@ -0,0 +1,285 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterable, Optional
|
||||
|
||||
from dcs.mapping import Point
|
||||
|
||||
from game.utils import meter_to_nm
|
||||
from gen.ato import Package
|
||||
from gen.flights.flight import (
|
||||
Flight,
|
||||
FlightType,
|
||||
FlightWaypoint,
|
||||
FlightWaypointType,
|
||||
)
|
||||
|
||||
|
||||
CAP_DURATION = 30 # Minutes
|
||||
CAP_TYPES = (FlightType.BARCAP, FlightType.CAP)
|
||||
|
||||
|
||||
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.
|
||||
# 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
|
||||
|
||||
|
||||
class TravelTime:
|
||||
@staticmethod
|
||||
def between_points(a: Point, b: Point, speed: float) -> int:
|
||||
error_factor = 1.1
|
||||
distance = meter_to_nm(a.distance_to_point(b))
|
||||
hours = distance / speed
|
||||
seconds = hours * 3600
|
||||
return int(seconds * error_factor)
|
||||
|
||||
|
||||
class TotEstimator:
|
||||
# An extra five minutes given as wiggle room. Expected to be spent at the
|
||||
# hold point performing any last minute configuration.
|
||||
HOLD_TIME = 5 * 60
|
||||
|
||||
def __init__(self, package: Package) -> None:
|
||||
self.package = package
|
||||
self.timing = PackageWaypointTiming.for_package(package)
|
||||
|
||||
def mission_start_time(self, flight: Flight) -> int:
|
||||
takeoff_time = self.takeoff_time_for_flight(flight)
|
||||
startup_time = self.estimate_startup(flight)
|
||||
ground_ops_time = self.estimate_ground_ops(flight)
|
||||
return takeoff_time - startup_time - ground_ops_time
|
||||
|
||||
def takeoff_time_for_flight(self, flight: Flight) -> int:
|
||||
stop_types = {FlightWaypointType.JOIN, FlightWaypointType.PATROL_TRACK}
|
||||
travel_time = self.estimate_waypoints_to_target(flight, stop_types)
|
||||
if travel_time is None:
|
||||
logging.warning("Found no join point or patrol point. Cannot "
|
||||
f"estimate takeoff time takeoff time for {flight}")
|
||||
# Takeoff immediately.
|
||||
return 0
|
||||
|
||||
if self.package.primary_task in CAP_TYPES:
|
||||
start_time = self.timing.race_track_start
|
||||
else:
|
||||
start_time = self.timing.join
|
||||
return start_time - travel_time - self.HOLD_TIME
|
||||
|
||||
def earliest_tot(self) -> int:
|
||||
return max((
|
||||
self.earliest_tot_for_flight(f) for f in self.package.flights
|
||||
)) + self.HOLD_TIME
|
||||
|
||||
def earliest_tot_for_flight(self, flight: Flight) -> int:
|
||||
"""Estimate fastest time from mission start to the target position.
|
||||
|
||||
For CAP missions, this is time to race track start.
|
||||
|
||||
For other mission types this is the time to the mission target.
|
||||
|
||||
Args:
|
||||
flight: The flight to get the earliest TOT time for.
|
||||
|
||||
Returns:
|
||||
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)
|
||||
if time_to_ingress is None:
|
||||
logging.warning(
|
||||
f"Found no ingress types. Cannot estimate TOT for {flight}")
|
||||
# Return 0 so this flight's travel time does not affect the rest of
|
||||
# the package.
|
||||
return 0
|
||||
|
||||
if self.package.primary_task in CAP_TYPES:
|
||||
# The racetrack start *is* the target. The package target is the
|
||||
# protected objective.
|
||||
time_to_target = 0
|
||||
else:
|
||||
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))
|
||||
return sum([
|
||||
self.estimate_startup(flight),
|
||||
self.estimate_ground_ops(flight),
|
||||
time_to_ingress,
|
||||
time_to_target,
|
||||
])
|
||||
|
||||
@staticmethod
|
||||
def estimate_startup(flight: Flight) -> int:
|
||||
if flight.start_type == "Cold":
|
||||
return 10 * 60
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def estimate_ground_ops(flight: Flight) -> int:
|
||||
if flight.start_type in ("Runway", "In Flight"):
|
||||
return 0
|
||||
if flight.from_cp.is_fleet:
|
||||
return 2 * 60
|
||||
else:
|
||||
return 5 * 60
|
||||
|
||||
def estimate_waypoints_to_target(
|
||||
self, flight: Flight,
|
||||
stop_types: Iterable[FlightWaypointType]) -> Optional[int]:
|
||||
total = 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)
|
||||
)
|
||||
previous_position = position
|
||||
if waypoint.waypoint_type in stop_types:
|
||||
return total
|
||||
|
||||
return None
|
||||
|
||||
def speed_to_waypoint(self, flight: Flight,
|
||||
waypoint: FlightWaypoint) -> int:
|
||||
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)
|
||||
elif waypoint.waypoint_type in pre_join:
|
||||
return GroundSpeed.for_flight(flight)
|
||||
return GroundSpeed.for_package(self.package)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PackageWaypointTiming:
|
||||
#: The package being scheduled.
|
||||
package: Package
|
||||
|
||||
#: The package join time.
|
||||
join: int
|
||||
|
||||
#: The ingress waypoint TOT.
|
||||
ingress: int
|
||||
|
||||
#: The egress waypoint TOT.
|
||||
egress: int
|
||||
|
||||
#: The package split time.
|
||||
split: int
|
||||
|
||||
@property
|
||||
def target(self) -> int:
|
||||
"""The package time over target."""
|
||||
assert self.package.time_over_target is not None
|
||||
return self.package.time_over_target
|
||||
|
||||
@property
|
||||
def race_track_start(self) -> int:
|
||||
if self.package.primary_task in CAP_TYPES:
|
||||
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:
|
||||
return self.target + CAP_DURATION * 60
|
||||
else:
|
||||
return self.egress
|
||||
|
||||
def push_time(self, flight: Flight, hold_point: Point) -> int:
|
||||
assert self.package.waypoints is not None
|
||||
return self.join - TravelTime.between_points(
|
||||
hold_point,
|
||||
self.package.waypoints.join,
|
||||
GroundSpeed.for_flight(flight)
|
||||
)
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[int]:
|
||||
target_types = (
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
FlightWaypointType.TARGET_POINT,
|
||||
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:
|
||||
return self.ingress
|
||||
elif waypoint.waypoint_type in target_types:
|
||||
return self.target
|
||||
elif waypoint.waypoint_type == FlightWaypointType.EGRESS:
|
||||
return self.egress
|
||||
elif waypoint.waypoint_type == FlightWaypointType.SPLIT:
|
||||
return self.split
|
||||
elif waypoint.waypoint_type == FlightWaypointType.PATROL_TRACK:
|
||||
return self.race_track_start
|
||||
return None
|
||||
|
||||
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))
|
||||
elif waypoint.waypoint_type == FlightWaypointType.PATROL:
|
||||
return self.race_track_end
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def for_package(cls, package: Package) -> PackageWaypointTiming:
|
||||
assert package.waypoints is not None
|
||||
|
||||
group_ground_speed = GroundSpeed.for_package(package)
|
||||
|
||||
ingress = package.time_over_target - TravelTime.between_points(
|
||||
package.waypoints.ingress,
|
||||
package.target.position,
|
||||
group_ground_speed
|
||||
)
|
||||
|
||||
join = ingress - TravelTime.between_points(
|
||||
package.waypoints.join,
|
||||
package.waypoints.ingress,
|
||||
group_ground_speed
|
||||
)
|
||||
|
||||
egress = package.time_over_target + TravelTime.between_points(
|
||||
package.target.position,
|
||||
package.waypoints.egress,
|
||||
group_ground_speed
|
||||
)
|
||||
|
||||
split = egress + TravelTime.between_points(
|
||||
package.waypoints.egress,
|
||||
package.waypoints.split,
|
||||
group_ground_speed
|
||||
)
|
||||
|
||||
return cls(package, join, ingress, egress, split)
|
||||
Reference in New Issue
Block a user