mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Merge remote-tracking branch 'upstream/develop' into new-plugin-system
This commit is contained in:
405
gen/aircraft.py
405
gen/aircraft.py
@@ -57,14 +57,14 @@ from dcs.task import (
|
||||
)
|
||||
from dcs.terrain.terrain import Airport, NoParkingSlotError
|
||||
from dcs.translation import String
|
||||
from dcs.triggers import Event, TriggerOnce
|
||||
from dcs.triggers import Event, TriggerOnce, TriggerRule
|
||||
from dcs.unitgroup import FlyingGroup, Group, ShipGroup, StaticGroup
|
||||
from dcs.unittype import FlyingType, UnitType
|
||||
|
||||
from game import db
|
||||
from game.data.cap_capabilities_db import GUNFIGHTERS
|
||||
from game.settings import Settings
|
||||
from game.utils import meter_to_nm, nm_to_meter
|
||||
from game.utils import nm_to_meter
|
||||
from gen.airfields import RunwayData
|
||||
from gen.airsupportgen import AirSupport
|
||||
from gen.ato import AirTaskingOrder, Package
|
||||
@@ -76,9 +76,10 @@ from gen.flights.flight import (
|
||||
FlightWaypointType,
|
||||
)
|
||||
from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
|
||||
from theater import MissionTarget, TheaterGroundObject
|
||||
from theater import TheaterGroundObject
|
||||
from theater.controlpoint import ControlPoint, ControlPointType
|
||||
from .conflictgen import Conflict
|
||||
from .flights.traveltime import PackageWaypointTiming, TotEstimator
|
||||
from .naming import namegen
|
||||
|
||||
WARM_START_HELI_AIRSPEED = 120
|
||||
@@ -86,8 +87,6 @@ WARM_START_HELI_ALT = 500
|
||||
WARM_START_ALTITUDE = 3000
|
||||
WARM_START_AIRSPEED = 550
|
||||
|
||||
CAP_DURATION = 30 # minutes
|
||||
|
||||
RTB_ALTITUDE = 800
|
||||
RTB_DISTANCE = 5000
|
||||
HELI_ALT = 500
|
||||
@@ -217,7 +216,7 @@ class FlightData:
|
||||
#: True if this flight belongs to the player's coalition.
|
||||
friendly: bool
|
||||
|
||||
#: Number of minutes after mission start the flight is set to depart.
|
||||
#: Number of seconds after mission start the flight is set to depart.
|
||||
departure_delay: int
|
||||
|
||||
#: Arrival airport.
|
||||
@@ -537,148 +536,6 @@ AIRCRAFT_DATA["P-51D-30-NA"] = AIRCRAFT_DATA["P-51D"]
|
||||
AIRCRAFT_DATA["P-47D-30"] = AIRCRAFT_DATA["P-51D"]
|
||||
|
||||
|
||||
@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) -> Optional[int]:
|
||||
cap_types = (FlightType.BARCAP, FlightType.CAP)
|
||||
if self.package.primary_task in cap_types:
|
||||
# CAP flights don't have hold points, and we don't calculate takeoff
|
||||
# times yet or adjust the TOT based on when the flight can arrive,
|
||||
# so if we set a TOT that gives the flight a lot of extra time it
|
||||
# will just fly to the start point slowly, possibly slowly enough to
|
||||
# stall and crash. Just don't set a TOT for these points and let the
|
||||
# CAP get on station ASAP.
|
||||
return None
|
||||
else:
|
||||
return self.ingress
|
||||
|
||||
@property
|
||||
def race_track_end(self) -> int:
|
||||
cap_types = (FlightType.BARCAP, FlightType.CAP)
|
||||
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 - self.travel_time(
|
||||
hold_point,
|
||||
self.package.waypoints.join,
|
||||
self.flight_ground_speed(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.time_over_target is not None
|
||||
assert package.waypoints is not None
|
||||
|
||||
group_ground_speed = cls.package_ground_speed(package)
|
||||
|
||||
ingress = package.time_over_target - cls.travel_time(
|
||||
package.waypoints.ingress,
|
||||
package.target.position,
|
||||
group_ground_speed
|
||||
)
|
||||
|
||||
join = ingress - cls.travel_time(
|
||||
package.waypoints.join,
|
||||
package.waypoints.ingress,
|
||||
group_ground_speed
|
||||
)
|
||||
|
||||
egress = package.time_over_target + cls.travel_time(
|
||||
package.target.position,
|
||||
package.waypoints.egress,
|
||||
group_ground_speed
|
||||
)
|
||||
|
||||
split = egress + cls.travel_time(
|
||||
package.waypoints.egress,
|
||||
package.waypoints.split,
|
||||
group_ground_speed
|
||||
)
|
||||
|
||||
return cls(package, join, ingress, egress, split)
|
||||
|
||||
@classmethod
|
||||
def package_ground_speed(cls, package: Package) -> int:
|
||||
speeds = []
|
||||
for flight in package.flights:
|
||||
speeds.append(cls.flight_ground_speed(flight))
|
||||
return min(speeds) # knots
|
||||
|
||||
@staticmethod
|
||||
def flight_ground_speed(_flight: Flight) -> int:
|
||||
# TODO: Gather data so this is useful.
|
||||
return 400 # knots
|
||||
|
||||
@staticmethod
|
||||
def travel_time(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 AircraftConflictGenerator:
|
||||
def __init__(self, mission: Mission, conflict: Conflict, settings: Settings,
|
||||
game, radio_registry: RadioRegistry):
|
||||
@@ -706,8 +563,13 @@ class AircraftConflictGenerator:
|
||||
except KeyError:
|
||||
return get_fallback_channel(airframe)
|
||||
|
||||
def _start_type(self) -> StartType:
|
||||
return self.settings.cold_start and StartType.Cold or StartType.Warm
|
||||
@staticmethod
|
||||
def _start_type(start_type: str) -> StartType:
|
||||
if start_type == "Runway":
|
||||
return StartType.Runway
|
||||
elif start_type == "Cold":
|
||||
return StartType.Cold
|
||||
return StartType.Warm
|
||||
|
||||
def _setup_group(self, group: FlyingGroup, for_task: Type[Task],
|
||||
flight: Flight, dynamic_runways: Dict[str, RunwayData]):
|
||||
@@ -813,15 +675,10 @@ class AircraftConflictGenerator:
|
||||
return runways[0]
|
||||
|
||||
def _generate_at_airport(self, name: str, side: Country,
|
||||
unit_type: FlyingType, count: int,
|
||||
client_count: int,
|
||||
airport: Optional[Airport] = None,
|
||||
start_type=None) -> FlyingGroup:
|
||||
unit_type: FlyingType, count: int, start_type: str,
|
||||
airport: Optional[Airport] = None) -> FlyingGroup:
|
||||
assert count > 0
|
||||
|
||||
if start_type is None:
|
||||
start_type = self._start_type()
|
||||
|
||||
logging.info("airgen: {} for {} at {}".format(unit_type, side.id, airport))
|
||||
return self.m.flight_group_from_airport(
|
||||
country=side,
|
||||
@@ -829,11 +686,11 @@ class AircraftConflictGenerator:
|
||||
aircraft_type=unit_type,
|
||||
airport=airport,
|
||||
maintask=None,
|
||||
start_type=start_type,
|
||||
start_type=self._start_type(start_type),
|
||||
group_size=count,
|
||||
parking_slots=None)
|
||||
|
||||
def _generate_inflight(self, name: str, side: Country, unit_type: FlyingType, count: int, client_count: int, at: Point) -> FlyingGroup:
|
||||
def _generate_inflight(self, name: str, side: Country, unit_type: FlyingType, count: int, at: Point) -> FlyingGroup:
|
||||
assert count > 0
|
||||
|
||||
if unit_type in helicopters.helicopter_map.values():
|
||||
@@ -855,21 +712,16 @@ class AircraftConflictGenerator:
|
||||
altitude=alt,
|
||||
speed=speed,
|
||||
maintask=None,
|
||||
start_type=self._start_type(),
|
||||
group_size=count)
|
||||
|
||||
group.points[0].alt_type = "RADIO"
|
||||
return group
|
||||
|
||||
def _generate_at_group(self, name: str, side: Country,
|
||||
unit_type: FlyingType, count: int, client_count: int,
|
||||
at: Union[ShipGroup, StaticGroup],
|
||||
start_type=None) -> FlyingGroup:
|
||||
unit_type: FlyingType, count: int, start_type: str,
|
||||
at: Union[ShipGroup, StaticGroup]) -> FlyingGroup:
|
||||
assert count > 0
|
||||
|
||||
if start_type is None:
|
||||
start_type = self._start_type()
|
||||
|
||||
logging.info("airgen: {} for {} at unit {}".format(unit_type, side.id, at))
|
||||
return self.m.flight_group_from_unit(
|
||||
country=side,
|
||||
@@ -877,34 +729,9 @@ class AircraftConflictGenerator:
|
||||
aircraft_type=unit_type,
|
||||
pad_group=at,
|
||||
maintask=None,
|
||||
start_type=start_type,
|
||||
start_type=self._start_type(start_type),
|
||||
group_size=count)
|
||||
|
||||
def _generate_group(self, name: str, side: Country, unit_type: FlyingType, count: int, client_count: int, at: db.StartingPosition):
|
||||
if isinstance(at, Point):
|
||||
return self._generate_inflight(name, side, unit_type, count, client_count, at)
|
||||
elif isinstance(at, Group):
|
||||
takeoff_ban = unit_type in db.CARRIER_TAKEOFF_BAN
|
||||
ai_ban = client_count == 0 and self.settings.only_player_takeoff
|
||||
|
||||
if not takeoff_ban and not ai_ban:
|
||||
return self._generate_at_group(name, side, unit_type, count, client_count, at)
|
||||
else:
|
||||
return self._generate_inflight(name, side, unit_type, count, client_count, at.position)
|
||||
elif isinstance(at, Airport):
|
||||
takeoff_ban = unit_type in db.TAKEOFF_BAN
|
||||
ai_ban = client_count == 0 and self.settings.only_player_takeoff
|
||||
|
||||
if not takeoff_ban and not ai_ban:
|
||||
try:
|
||||
return self._generate_at_airport(name, side, unit_type, count, client_count, at)
|
||||
except NoParkingSlotError:
|
||||
logging.info("No parking slot found at " + at.name + ", switching to air start.")
|
||||
pass
|
||||
return self._generate_inflight(name, side, unit_type, count, client_count, at.position)
|
||||
else:
|
||||
assert False
|
||||
|
||||
def _add_radio_waypoint(self, group: FlyingGroup, position, altitude: int, airspeed: int = 600):
|
||||
point = group.add_waypoint(position, altitude, airspeed)
|
||||
point.alt_type = "RADIO"
|
||||
@@ -975,110 +802,91 @@ class AircraftConflictGenerator:
|
||||
logging.info(f"Generating flight: {flight.unit_type}")
|
||||
group = self.generate_planned_flight(flight.from_cp, country,
|
||||
flight)
|
||||
self.setup_flight_group(group, flight, timing, dynamic_runways)
|
||||
self.setup_group_activation_trigger(flight, group)
|
||||
self.setup_flight_group(group, package, flight, timing,
|
||||
dynamic_runways)
|
||||
|
||||
def setup_group_activation_trigger(self, flight, group):
|
||||
if flight.scheduled_in > 0 and flight.client_count == 0:
|
||||
def set_activation_time(self, flight: Flight, group: FlyingGroup,
|
||||
delay: int) -> None:
|
||||
# Note: Late activation causes the waypoint TOTs to look *weird* in the
|
||||
# mission editor. Waypoint times will be relative to the group
|
||||
# activation time rather than in absolute local time. A flight delayed
|
||||
# until 09:10 when the overall mission start time is 09:00, with a join
|
||||
# time of 09:30 will show the join time as 00:30, not 09:30.
|
||||
group.late_activation = True
|
||||
|
||||
if flight.start_type != "In Flight" and flight.from_cp.cptype not in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP]:
|
||||
group.late_activation = False
|
||||
group.uncontrolled = True
|
||||
activation_trigger = TriggerOnce(
|
||||
Event.NoEvent, f"FlightLateActivationTrigger{group.id}")
|
||||
activation_trigger.add_condition(TimeAfter(seconds=delay))
|
||||
|
||||
activation_trigger = TriggerOnce(Event.NoEvent, "FlightStartTrigger" + str(group.id))
|
||||
activation_trigger.add_condition(TimeAfter(seconds=flight.scheduled_in * 60))
|
||||
if (flight.from_cp.cptype == ControlPointType.AIRBASE):
|
||||
if flight.from_cp.captured:
|
||||
activation_trigger.add_condition(
|
||||
CoalitionHasAirdrome(self.game.get_player_coalition_id(), flight.from_cp.id))
|
||||
else:
|
||||
activation_trigger.add_condition(
|
||||
CoalitionHasAirdrome(self.game.get_enemy_coalition_id(), flight.from_cp.id))
|
||||
self.prevent_spawn_at_hostile_airbase(flight, activation_trigger)
|
||||
activation_trigger.add_action(ActivateGroup(group.id))
|
||||
self.m.triggerrules.triggers.append(activation_trigger)
|
||||
|
||||
if flight.flight_type == FlightType.INTERCEPTION:
|
||||
self.setup_interceptor_triggers(group, flight, activation_trigger)
|
||||
def set_startup_time(self, flight: Flight, group: FlyingGroup,
|
||||
delay: int) -> None:
|
||||
# Uncontrolled causes the AI unit to spawn, but not begin startup.
|
||||
group.uncontrolled = True
|
||||
|
||||
group.add_trigger_action(StartCommand())
|
||||
activation_trigger.add_action(AITaskPush(group.id, len(group.tasks)))
|
||||
activation_trigger = TriggerOnce(Event.NoEvent,
|
||||
f"FlightStartTrigger{group.id}")
|
||||
activation_trigger.add_condition(TimeAfter(seconds=delay))
|
||||
|
||||
self.m.triggerrules.triggers.append(activation_trigger)
|
||||
else:
|
||||
group.late_activation = True
|
||||
activation_trigger = TriggerOnce(Event.NoEvent, "FlightLateActivationTrigger" + str(group.id))
|
||||
activation_trigger.add_condition(TimeAfter(seconds=flight.scheduled_in*60))
|
||||
self.prevent_spawn_at_hostile_airbase(flight, activation_trigger)
|
||||
group.add_trigger_action(StartCommand())
|
||||
activation_trigger.add_action(AITaskPush(group.id, len(group.tasks)))
|
||||
self.m.triggerrules.triggers.append(activation_trigger)
|
||||
|
||||
if(flight.from_cp.cptype == ControlPointType.AIRBASE):
|
||||
if flight.from_cp.captured:
|
||||
activation_trigger.add_condition(CoalitionHasAirdrome(self.game.get_player_coalition_id(), flight.from_cp.id))
|
||||
else:
|
||||
activation_trigger.add_condition(CoalitionHasAirdrome(self.game.get_enemy_coalition_id(), flight.from_cp.id))
|
||||
def prevent_spawn_at_hostile_airbase(self, flight: Flight,
|
||||
trigger: TriggerRule) -> None:
|
||||
# Prevent delayed flights from spawning at airbases if they were
|
||||
# captured before they've spawned.
|
||||
if flight.from_cp.cptype != ControlPointType.AIRBASE:
|
||||
return
|
||||
|
||||
if flight.flight_type == FlightType.INTERCEPTION:
|
||||
self.setup_interceptor_triggers(group, flight, activation_trigger)
|
||||
|
||||
activation_trigger.add_action(ActivateGroup(group.id))
|
||||
self.m.triggerrules.triggers.append(activation_trigger)
|
||||
|
||||
def setup_interceptor_triggers(self, group, flight, activation_trigger):
|
||||
|
||||
detection_zone = self.m.triggers.add_triggerzone(flight.from_cp.position, radius=25000, hidden=False, name="ITZ")
|
||||
if flight.from_cp.captured:
|
||||
activation_trigger.add_condition(PartOfCoalitionInZone(self.game.get_enemy_color(), detection_zone.id)) # TODO : support unit type in part of coalition
|
||||
activation_trigger.add_action(MessageToAll(String("WARNING : Enemy aircraft have been detected in the vicinity of " + flight.from_cp.name + ". Interceptors are taking off."), 20))
|
||||
coalition = self.game.get_player_coalition_id()
|
||||
else:
|
||||
activation_trigger.add_condition(PartOfCoalitionInZone(self.game.get_player_color(), detection_zone.id))
|
||||
activation_trigger.add_action(MessageToAll(String("WARNING : We have detected that enemy aircraft are scrambling for an interception on " + flight.from_cp.name + " airbase."), 20))
|
||||
coalition = self.game.get_enemy_coalition_id()
|
||||
|
||||
trigger.add_condition(
|
||||
CoalitionHasAirdrome(coalition, flight.from_cp.id))
|
||||
|
||||
def generate_planned_flight(self, cp, country, flight:Flight):
|
||||
try:
|
||||
if flight.client_count == 0 and self.game.settings.perf_ai_parking_start:
|
||||
flight.start_type = "Cold"
|
||||
|
||||
if flight.start_type == "In Flight":
|
||||
group = self._generate_group(
|
||||
group = self._generate_inflight(
|
||||
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
|
||||
side=country,
|
||||
unit_type=flight.unit_type,
|
||||
count=flight.count,
|
||||
client_count=0,
|
||||
at=cp.position)
|
||||
elif cp.is_fleet:
|
||||
group_name = cp.get_carrier_group_name()
|
||||
group = self._generate_at_group(
|
||||
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
|
||||
side=country,
|
||||
unit_type=flight.unit_type,
|
||||
count=flight.count,
|
||||
start_type=flight.start_type,
|
||||
at=self.m.find_group(group_name))
|
||||
else:
|
||||
st = StartType.Runway
|
||||
if flight.start_type == "Cold":
|
||||
st = StartType.Cold
|
||||
elif flight.start_type == "Warm":
|
||||
st = StartType.Warm
|
||||
|
||||
if cp.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP]:
|
||||
group_name = cp.get_carrier_group_name()
|
||||
group = self._generate_at_group(
|
||||
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
|
||||
side=country,
|
||||
unit_type=flight.unit_type,
|
||||
count=flight.count,
|
||||
client_count=0,
|
||||
at=self.m.find_group(group_name),
|
||||
start_type=st)
|
||||
else:
|
||||
group = self._generate_at_airport(
|
||||
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
|
||||
side=country,
|
||||
unit_type=flight.unit_type,
|
||||
count=flight.count,
|
||||
client_count=0,
|
||||
airport=cp.airport,
|
||||
start_type=st)
|
||||
group = self._generate_at_airport(
|
||||
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
|
||||
side=country,
|
||||
unit_type=flight.unit_type,
|
||||
count=flight.count,
|
||||
start_type=flight.start_type,
|
||||
airport=cp.airport)
|
||||
except Exception as e:
|
||||
# Generated when there is no place on Runway or on Parking Slots
|
||||
logging.error(e)
|
||||
logging.warning("No room on runway or parking slots. Starting from the air.")
|
||||
flight.start_type = "In Flight"
|
||||
group = self._generate_group(
|
||||
group = self._generate_inflight(
|
||||
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
|
||||
side=country,
|
||||
unit_type=flight.unit_type,
|
||||
count=flight.count,
|
||||
client_count=0,
|
||||
at=cp.position)
|
||||
group.points[0].alt = 1500
|
||||
|
||||
@@ -1184,8 +992,8 @@ class AircraftConflictGenerator:
|
||||
logging.error(f"Unhandled flight type: {flight.flight_type.name}")
|
||||
self.configure_behavior(group)
|
||||
|
||||
def setup_flight_group(self, group: FlyingGroup, flight: Flight,
|
||||
timing: PackageWaypointTiming,
|
||||
def setup_flight_group(self, group: FlyingGroup, package: Package,
|
||||
flight: Flight, timing: PackageWaypointTiming,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> None:
|
||||
flight_type = flight.flight_type
|
||||
if flight_type in [FlightType.CAP, FlightType.BARCAP, FlightType.TARCAP,
|
||||
@@ -1209,6 +1017,9 @@ class AircraftConflictGenerator:
|
||||
for waypoint in flight.points:
|
||||
waypoint.tot = None
|
||||
|
||||
takeoff_point = FlightWaypoint.from_pydcs(group.points[0],
|
||||
flight.from_cp)
|
||||
self.set_takeoff_time(takeoff_point, package, flight, group)
|
||||
for point in flight.points:
|
||||
if point.only_for_player and not flight.client_count:
|
||||
continue
|
||||
@@ -1219,9 +1030,53 @@ class AircraftConflictGenerator:
|
||||
|
||||
# Set here rather than when the FlightData is created so they waypoints
|
||||
# have their TOTs set.
|
||||
self.flights[-1].waypoints = flight.points
|
||||
self.flights[-1].waypoints = [takeoff_point] + flight.points
|
||||
self._setup_custom_payload(flight, group)
|
||||
|
||||
def set_takeoff_time(self, waypoint: FlightWaypoint, package: Package,
|
||||
flight: Flight, group: FlyingGroup) -> None:
|
||||
estimator = TotEstimator(package)
|
||||
start_time = estimator.mission_start_time(flight)
|
||||
|
||||
if start_time > 0:
|
||||
if self.should_activate_late(flight):
|
||||
# Late activation causes the aircraft to not be spawned until
|
||||
# triggered.
|
||||
self.set_activation_time(flight, group, start_time)
|
||||
elif flight.start_type == "Cold":
|
||||
# Setting the start time causes the AI to wait until the
|
||||
# specified time to begin their startup sequence.
|
||||
self.set_startup_time(flight, group, start_time)
|
||||
|
||||
# And setting *our* waypoint TOT causes the takeoff time to show up in
|
||||
# the player's kneeboard.
|
||||
waypoint.tot = estimator.takeoff_time_for_flight(flight)
|
||||
|
||||
@staticmethod
|
||||
def should_activate_late(flight: Flight) -> bool:
|
||||
if flight.client_count:
|
||||
# Never delay players. Note that cold start player flights with
|
||||
# AI members will still be marked as uncontrolled until the start
|
||||
# trigger fires to postpone engine start.
|
||||
#
|
||||
# Player flights that start on the runway or in the air will start
|
||||
# immediately, and AI flight members will not be delayed.
|
||||
return False
|
||||
|
||||
if flight.start_type != "Cold":
|
||||
# Avoid spawning aircraft in the air or on the runway until it's
|
||||
# time for their mission. Also avoid burning through gas spawning
|
||||
# hot aircraft hours before their takeoff time.
|
||||
return True
|
||||
|
||||
if flight.from_cp.is_fleet:
|
||||
# Carrier spawns will crowd the carrier deck, especially without
|
||||
# super carrier.
|
||||
# TODO: Is there enough parking on the supercarrier?
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class PydcsWaypointBuilder:
|
||||
def __init__(self, waypoint: FlightWaypoint, group: FlyingGroup,
|
||||
@@ -1248,10 +1103,8 @@ class PydcsWaypointBuilder:
|
||||
waypoint.speed_locked = False
|
||||
|
||||
@classmethod
|
||||
def for_waypoint(cls, waypoint: FlightWaypoint,
|
||||
group: FlyingGroup,
|
||||
flight: Flight,
|
||||
timing: PackageWaypointTiming,
|
||||
def for_waypoint(cls, waypoint: FlightWaypoint, group: FlyingGroup,
|
||||
flight: Flight, timing: PackageWaypointTiming,
|
||||
mission: Mission) -> PydcsWaypointBuilder:
|
||||
builders = {
|
||||
FlightWaypointType.EGRESS: EgressPointBuilder,
|
||||
@@ -1396,9 +1249,7 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
|
||||
pattern=OrbitAction.OrbitPattern.RaceTrack
|
||||
))
|
||||
|
||||
start = self.timing.race_track_start
|
||||
if start is not None:
|
||||
self.set_waypoint_tot(waypoint, start)
|
||||
self.set_waypoint_tot(waypoint, self.timing.race_track_start)
|
||||
racetrack.stop_after_time(self.timing.race_track_end)
|
||||
waypoint.add_task(racetrack)
|
||||
return waypoint
|
||||
|
||||
@@ -52,7 +52,7 @@ class Package:
|
||||
delay: int = field(default=0)
|
||||
|
||||
#: Desired TOT measured in seconds from mission start.
|
||||
time_over_target: Optional[int] = field(default=None)
|
||||
time_over_target: int = field(default=0)
|
||||
|
||||
waypoints: Optional[PackageWaypoints] = field(default=None)
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import datetime
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
@@ -116,9 +117,10 @@ class BriefingGenerator(MissionInfoGenerator):
|
||||
assert not flight.client_units
|
||||
aircraft = flight.aircraft_type
|
||||
flight_unit_name = db.unit_type_name(aircraft)
|
||||
delay = datetime.timedelta(seconds=flight.departure_delay)
|
||||
self.description += (
|
||||
f"{flight.flight_type.name} {flight_unit_name} x {flight.size}, "
|
||||
f"departing in {flight.departure_delay} minutes\n"
|
||||
f"departing in {delay}\n"
|
||||
)
|
||||
|
||||
def generate(self):
|
||||
|
||||
@@ -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)
|
||||
flight.targetPoint = self.package.target
|
||||
return True
|
||||
@@ -445,11 +449,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()
|
||||
@@ -498,16 +509,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,14 +129,11 @@ 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
|
||||
targetPoint = None # Contains either None or a Strike/SEAD target point location
|
||||
|
||||
# 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
|
||||
@@ -145,11 +141,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
|
||||
@@ -158,6 +159,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